Azure, SignalR and Web Api do not send a message to the client

I was inspired by the video " " Scaling a web page in real time using ASP.NET SignalR "in 56 minutes and section 11 seconds ..

Imagine a web chat client that uses SignalR to communicate with a server. When a client connects, its endpoint information is stored in the Azure table.

The chat client can send a message to another chat client through SignalR, which looks at the endpoint of the destination client of interest (possibly on a different instance), and then through the Web API sends a message to another client instance through SignalR.

To demonstrate that I downloaded the sample application on github .

This all works when there is one instance of Azure. However, if there are MULTIPLE azure instances, the very last call of SignalR from server to client silently fails. It as a dynamic code simply does not exist or its exit from a "bad" thread, or the message was somehow sent to the wrong instance, or I just made a mistake.

We will be very grateful for any ideas.

Webpage configured with this

<input type="radio" name='ClientId' value='A' style='width:30px'/>Chat client A</br> <input type="radio" name='ClientId' value='B' style='width:30px'/>Chat client B</br> <input type='button' id='register' value='Register' /> <input type='text' id='txtMessage' size='50' /><input type='button' id='send' value='Send' /> <div id='history'> </div> 

and JS -

 <script type="text/javascript"> $(function () { // Declare a proxy to reference the hub. var chat = $.connection.chatHub; chat.client.sendMessageToClient = function (message) { $('#history').append("<br/>" + message); }; // Start the connection. $.connection.hub.start().done(function () { $('#register').click(function () { // Call the Send method on the hub. chat.server.register($('input[name=ClientId]:checked', '#myForm').val()); }); $('#send').click(function () { // Call the Send method on the hub. chat.server.sendMessageToServer($('input[name=ClientId]:checked', '#myForm').val(), $('#txtMessage').val()); }); }); }); </script> 

The hub is as follows. (I have a small storage class to store endpoint information in an Azure table). Notice the static SendMessageToClient method. This is what ultimately fails. It is called from the Web Api class (below)

 public class ChatHub : Hub { public void Register(string chatClientId) { Storage.RegisterChatEndPoint(chatClientId, this.Context.ConnectionId); } /// <summary> /// Receives the message and sends it to the SignalR client. /// </summary> /// <param name="message">The message.</param> /// <param name="connectionId">The connection id.</param> public static void SendMessageToClient(string message, string connectionId) { GlobalHost.ConnectionManager.GetHubContext<ChatHub>().Clients.Client(connectionId).SendMessageToClient(message); Debug.WriteLine("Sending a message to the client on SignalR connection id: " + connectionId); Debug.WriteLine("Via the Web Api end point: " + RoleEnvironment.CurrentRoleInstance.InstanceEndpoints["WebApi"].IPEndpoint.ToString()); } /// <summary> /// Sends the message to other instance. /// </summary> /// <param name="chatClientId">The chat client id.</param> /// <param name="message">The message.</param> public void SendMessageToServer(string chatClientId, string message) { // Get the chatClientId of the destination. string otherChatClient = (chatClientId == "A" ? "B" : "A"); // Find out this other chatClientId end point ChatClientEntity chatClientEntity = Storage.GetChatClientEndpoint(otherChatClient); if (chatClientEntity != null) ChatWebApiController.SendMessage(chatClientEntity.WebRoleEndPoint, chatClientEntity.SignalRConnectionId, message); } } 

Finally, ChateWebApiController is

 public class ChatWebApiController : ApiController { [HttpGet] public void SendMessage(string message, string connectionId) { //return message; ChatHub.SendMessageToClient(message, connectionId); } /// <summary> /// This calls the method above but on a different instance via Web API /// </summary> /// <param name="endPoint">The end point.</param> /// <param name="connectionId">The connection id.</param> /// <param name="message">The message.</param> public static void SendMessage(string endPoint, string connectionId, string message) { HttpClient client = new HttpClient(); client.BaseAddress = new Uri("http://" + endPoint); // Add an Accept header for JSON format. client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); string url = "http://" + endPoint + "/api/ChatWebApi/SendMessage/?Message=" + HttpUtility.UrlEncode(message) + "&ConnectionId=" + connectionId; client.GetAsync(url); } } 
+6
source share
2 answers

Firstly, in the absence of any understanding of the community of this problem, I probably devoted too much time to get to the point. I expect Microsoft to publish some recommendations on these issues in the coming months, but until then we are mostly ourselves.

The answer to this problem is extremely complex, but it all makes sense when you understand how SignalR actually works under the hood. Sorry for the long answer, but it is necessary to give this problem the energy it deserves.

This solution only applies to Azure and SignalR multi-instance connections. If you are not on Azure (i.e., Windows Server), then it probably won’t apply to you, or if you plan to run only one Azure instance, then it won’t apply to you. It is important to browse http://channel9.msdn.com/Events/Build/2013/3-502 , especially from 43 minutes to 14 seconds to the end.

Here we go...

If you read the side of the box, you're lucky that SignalR connected to Azure will use WebSockets. This would make our life simple, since the only open socket connection between the client and Azure by its nature would be constantly tied to one Azure instance, and all communications could flow through this channel.

If you believe in it, then you are mistaken.

In the current version, SignalR vs. Azure does not use WebSockets. (This is documented at http://www.asp.net/signalr/overview/getting-started/supported-platforms ) IE10, because the client will use the "Forever Frame" - a somewhat fuzzy and exotic use of inline frames. Reading the excellent book found at http://campusmvp.net/signalr-ebook suggests that it supports a connection that is always open to the server. This is not entirely true. Using Fiddler shows that it opens an HTTP connection every time a client needs to interact with the server, although the initial messages (which cause the OnConnect method to be called) are always open. URL of this format / signalr / connect? Transport = foreverFrame & connectionToken = You will see that the icon in Fiddler is the green arrow pointing down, which means “loading”.

We know that Azure uses a load balancer. Given that the frame will forever establish a new connection every time it needs to send a message to the server, while it knows the load balancer to always send the message back to the Azure instance, which was responsible for establishing the server part of the SignalR connection? The answer ... it is not; and depending on the application, this may or may not be a problem. If the Azure message just needs to be recorded or any other actions, then do not read further. You have no problem. Your server-side method will be called and you will complete the action; plain.

However, if the message needs to be either sent back to the client using SignalR, or sent to another client (i.e. the chat application), then you still have a lot of work to do. Which of several instances can actually send a message? How to find him? How can you get a message for this other instance?

To demonstrate how all these aspects interact, I wrote a demo application that can be found at https://github.com/daveapsgithub/AzureSignalRInteration The application has many details on its web page, but in short, if you run it, you can easily You will see that the only instance that successfully sends a message to the client is the instance that received the "OnConnect" method from. Attempting to send a message to the client in any other instance will fail.

It also demonstrates that the load balancer is a shunt message for different instances, and an attempt to respond to any instance that is not an “OnConnected” instance will fail. Fortunately, regardless of the instance that receives the message, the SignalR connection identifier remains unchanged for this client. (as expected)

Based on these lessons, I revised my initial question and updated the project, which can be found at https://github.com/daveapsgithub/AzureSignalRWebApi2 Now processing the Azure Table storage is a bit more complicated. Since no parameters can be provided for the OnConnected method, we must first store the SignalR connection identifier and WebApi endpoint in the Azure table storage when OnConnected is called. Subsequently, when each client then "registers itself as client identifier" A "or" client identifier "B, this registration call should then search for Azure table storage for that connection identifier and set the client identifier accordingly.

When A sends a message to B, we do not know in which instance the message appears. But now this is not a problem, because we just look at the endpoint "B", make a call to WebApi, and then SignalR can send a message B.

There are two main issues you need to know about. If you are debugging and have a breakpoint in OnConnected and executing the code, then the client will probably disconnect and send a subsequent reconnect request (be sure to look at Fiddler). When the OnConnected check completes, you will see that it is called again as part of the reconnect request. What could be the problem? The problem is that the reconnect request is on another HTTP request that was supposed to go through the load balancer. Now you will be debugging a completely different instance with a different WebApi endpoint, which must be stored in the database. This instance, although it was received through an OnConnected message, is not an OnConnected instance. The first instance that received the OnConnected message is the only instance that can send messages to the client. So, in short, don't do any time-consuming activities in OnConnected (and if you need to use some Async template so that it runs in a separate thread so that OnConnected can return quickly).

Secondly, do not use two instances of IE10 to test SignalR applications that use this architecture. Use IE and another browser. If you open one IE that establishes a SignalR connection and then opens another IE, the SignalR connection of the first browser will be canceled and the first IE will start using the SignalR connection of the second IE. This is actually hard to believe, but refer to the Compute Emulator output windows to test this insanity.

Since the first SignalR abandoned its original connection, its Azure instance will also be “moved to another instance, the WebApi endpoint will not be updated in the Azure table, and any messages that are sent to it.

I updated the source code posted as part of the source question to demonstrate its functionality. In addition to changes to the Azure table storage class, the code changes were minor. We just need to add the code to the Onconnected method.

 public override System.Threading.Tasks.Task OnConnected() { Storage.RegisterChatEndPoint(this.Context.ConnectionId); staticEndPoint = RoleEnvironment.CurrentRoleInstance.InstanceEndpoints["WebApi"].IPEndpoint.ToString(); staticConnectionId = this.Context.ConnectionId; return base.OnConnected(); } public void Register(string chatClientId) { Storage.RegisterChatClientId(chatClientId, this.Context.ConnectionId); } 
+7
source

As commented, you definitely want to consider supported scaling solutions

It would seem, given your use of Azure, that the most relevant would be scaling the Azure service bus .

Could there be a typo in one of these dynamic method calls? In the following method

  public static void SendMessageToClient(string message, string connectionId) { GlobalHost.ConnectionManager.GetHubContext<ChatHub>().Clients .Client(connectionId).SendMessageToClient(message); ..... } 

Should a customer be called a camel?

  GlobalHost.ConnectionManager.GetHubContext<ChatHub>().Clients .Client(connectionId).sendMessageToClient(message); 
+1
source

Source: https://habr.com/ru/post/951554/


All Articles