想象一下使用SignalR与服务器通信的基于Web的聊天客户端.客户端连接时,其端点信息存储在Azure表中.
聊天客户端可以通过SignalR向另一个聊天客户端发送消息,SignalR查找感兴趣的目标客户端的终点(可能在不同的实例上),然后使用Web API通过SignalR将消息发送到客户端的另一个实例.
为了证明我有uploaded a sample application to github.
当存在单个Azure实例时,这一切都有效.但是,如果存在MULTIPLE azure实例,则从服务器到客户端的SignalR的最终调用将无效.它就像动态代码不存在或者它从“坏”线程中消失或消息以某种方式被发送到错误的实例或者我刚刚犯了一个蠢的错误.
任何想法将不胜感激.
网页设置为此
<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>
和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>
枢纽如下. (我有一个小存储类来存储Azure表中的端点信息).请注意静态方法SendMessageToClient.这是最终失败的.它是从Web Api类调用的(下面)
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's end point ChatClientEntity chatClientEntity = Storage.GetChatClientEndpoint(otherChatClient); if (chatClientEntity != null) ChatWebApiController.SendMessage(chatClientEntity.WebRoleEndPoint,chatClientEntity.SignalRConnectionId,message); } }
最后,ChateWebApiController就是这样
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); } }
解决方法
这个问题的答案非常复杂,但是当你理解SignalR实际上是如何工作时,这一切都是有意义的.为长期答案道歉但是为了给这个问题提供它应得的能量是必要的.
此解决方案仅适用于多实例Azure和SignalR通信.如果您不在Azure(即Windows Server)上,那么它可能不会适用于您,或者如果您计划仅运行一个Azure实例,那么这将不适用于您.
这是必不可少的观看http://channel9.msdn.com/Events/Build/2013/3-502,特别是从43分14秒到结束.
开始了…
如果您“阅读框的侧面”,您将会相信连接到Azure的SignalR将使用WebSockets.这将使我们的生活变得简单,因为客户端和Azure之间的单个开放套接字连接本身将不断绑定到单个Azure实例,并且所有通信都可以通过该通道流动.
如果你相信这一点,那你就错了.
在当前版本中,针对Azure的SignalR不使用WebSockets. (这是在http://www.asp.net/signalr/overview/getting-started/supported-platforms记录的)IE10作为客户端将使用“永远帧” – 嵌入式iframe的一些不明确和异乎寻常的使用.阅读http://campusmvp.net/signalr-ebook上发现的优秀电子书会表明它与服务器保持“永远”的联系.情况并非完全如此.使用Fiddler表明,每次客户端需要与服务器通信时,它都会打开HTTP连接,尽管初始通信(导致调用OnConnect方法)将永久保持打开状态. URL将是这种格式/ signalr / connect?transport = foreverFrame& connectionToken =您将看到Fiddler中的图标是向下指向的绿色箭头,表示“下载”.
我们知道Azure使用负载均衡器.鉴于永久帧将在每次需要向服务器发送消息时建立新连接,那么负载均衡器如何知道始终将消息发送回负责建立SignalR连接的服务器端的Azure实例?答案……它没有;并且根据应用,这可能是也可能不是问题.如果只需要记录到Azure的消息或采取其他一些操作,则不再阅读.你没有问题.将调用您的服务器端方法并执行操作;简单.
但是,如果消息需要通过SignalR发送回客户端或发送到另一个客户端(即聊天应用程序),那么您还有很多工作要做.可以实际发送消息的多个实例中的哪一个?我怎么找到它?如何向其他实例发送消息?
为了演示所有这些方面如何相互作用,我编写了一个可以在https://github.com/daveapsgithub/AzureSignalRInteration找到的演示应用程序
该应用程序在其网页上有许多详细信息,但简而言之,如果您运行它,您将很容易看到将成功向客户端发送消息的唯一实例是接收“OnConnect”方法的实例.尝试在任何其他实例上向客户端发送消息将无声地失败.
此外,它还演示了负载均衡器将消息分流到各种实例,并且尝试在任何不是“OnConnected”实例的实例上进行回复都将无声地失败.幸运的是,无论接收消息的实例如何,SignalR连接ID对于该客户端保持不变. (如你所料)
考虑到这些课程,我重新审视了我的原始问题,并更新了可以在https://github.com/daveapsgithub/AzureSignalRWebApi2找到的项目.现在,Azure表存储的处理稍微复杂一些.由于无法为OnConnected方法提供任何参数,因此我们需要在调用OnConnected时将SignalR连接ID和WebApi端点存储在Azure表存储中.随后,当每个客户端将其自身“注册”为客户端ID“A”或客户端ID“B”时,此注册调用需要查找该Azure连接ID的Azure表存储并适当地设置客户端ID.
当A向B发送消息时,我们不知道该消息出现在什么实例上.但现在这不是问题,因为我们只是查找’B’的终点,对它进行WebApi调用,然后SignalR可以向B发送消息.
您需要注意两个主要缺陷.如果您正在调试并在OnConnected中有一个断点并逐步执行代码,那么客户端可能会超时并发送后续的重新连接请求(请务必查看Fiddler).检查OnConnected后,您将看到它作为重新连接请求的一部分再次被调用.可能是什么问题呢?问题是重新连接请求是在必须通过负载均衡器的不同HTTP请求上.您现在将使用将要存储在数据库中的不同WebApi端点调试完全不同的实例.此实例虽然是通过“OnConnected”消息接收的,但不是“OnConnected”实例.收到OnConnected消息的第一个实例是唯一可以将消息发送回客户端的实例.总而言之,不要在OnConnected中执行任何耗时的活动(如果必须使用某些异步模式使其在单独的线程上运行,以便OnConnected可以快速返回).
其次,不要使用IE10的两个实例来测试使用此架构的SignalR应用程序.使用IE和其他浏览器.如果您打开一个建立SignalR连接然后打开另一个IE的IE,则放弃第一个浏览器的SignalR连接,并且第一个IE开始使用第二个IE的SignalR连接.这实际上很难相信,但可以参考Compute Emulator输出窗口来验证这种疯狂.
由于第一个SignalR已放弃其原始连接,因此其Azure实例也将“移动”到另一个实例,Azure表中的WebApi端点将不会更新,并且发送给它的任何消息都将无声地失败.
我已经更新了作为原始问题的一部分发布的源代码,以证明它有效.
除了对Azure表存储类的更改之外,代码更改也很小.我们只需要在Onconnected方法中添加一些代码.
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); }