SignaR可以用来实现服务器向客户端推送信息,但是SignalR的每个Hub的生命周期很短,不能长期停留在内存里。因此,如果服务器想要对客户端推送消息时,应该采用signalR的一个“全局”的上下文来实现。此功能的应用背景是:服务器的数据发生了更新,它希望向所有的用户、某个分组的用户或者是单个用户发送推送消息,而不是由客户端发送请求来检测数据是否有更新。这才是真正的SignalR要做的事!
在Asp.Net有关SignalR的介绍里有提到向所有用户发送推送消息的实现方式(参见:http://www.asp.net/signalr/overview/signalr-20/getting-started-with-signalr-20/tutorial-server-broadcast-with-signalr-20)。但是向分组的用户或单个用户发送消息的实现方式,并没有提供。本文将说明如何向分组推送消息,其他的两种实现很相似。
另外,在我发现Signalr有提供全局机制之前,我采用了让一个Hub长期停留在内存的方式实现。大致意思为:
我需要实现一个Hub,它可以动态地检测数据库数据的变化,然后推送给前端相关的用户。如果直接在继承Hub的类里实现,并调用的话,也就意味着由SignalR创建的所有Hub都要长时间地停留在内存里,而且由于我的推送信息是根据组(Group)进行的。结果导致的问题就是:有太多的Hub停留在内存了;而且针对某个分组的消息,会因为有多个Hub(每个Hub的实现是一样的)而发送多次。
后来,我想到了一个解决这个的办法,就是我只在内存里保留一个Hub的实例。这样就不会出现一条消息多次发送的问题了。具体的做法是:
当客户端第一次新建一个Hub时,我就将这个Hub的实例保存起来。当其他的客户端再次新建Hub时,这些Hub不再保存。当这些客户端要提供注册的分组信息时,我就直接将这些分组信息注册到我保存的那个Hub去,而我的所有推送信息只在这个Hub里发出。
老实说,系统已经运行很久了,这种方式还一直正常地使用着,没出现什么问题。不过,既然SignalR有提供全局的实现,那么,我们还是采用它提供的实现为好。
一、前置条件:
如何添加SignalR的类库引用以及初始化一个Hub,参见:http://www.asp.net/signalr/overview/signalr-20/getting-started-with-signalr-20/tutorial-getting-started-with-signalr-20
二、创建Hub Class
实现一个Hub以及一个Ticker。Hub的实现很简单,就是要求将当前的Hub连接注册到Ticker上去。Ticker采用单例实现。具体代码如下:
public class ChatHub : Hub { private readonly ChatTicker ticker; public ChatHub() { ticker = ChatTicker.Instance; } public void Send(string group, string username) { //注册到全局 ticker.GlobalContext.Groups.Add(Context.ConnectionId, group); Clients.All.broadcastMessage(group, "user register:" + username); } } public class ChatTicker { #region 实现一个单例 private static readonly ChatTicker _instance = new ChatTicker(GlobalHost.ConnectionManager.GetHubContext<ChatHub>()); private readonly IHubContext m_context; private ChatTicker(IHubContext context) { m_context = context; //这里不能直接调用Sender,因为Sender是一个不退出的“死循环”,否则这个构造函数将不会退出。 //其他的流程也将不会再执行下去了。所以要采用异步的方式。 Task.Run(() => Sender()); } public IHubContext GlobalContext { get { return m_context; } } public static ChatTicker Instance { get { return _instance; } } #endregion public void Sender() { int count = 0; while (true) { Thread.Sleep(500); int tag = count%2; //动态绑定前端的js函数 broadcaseMessage m_context.Clients.Group(tag + "").broadcastMessage("group is:" + tag, "current count:" + count); count++; } } }
三、前端测试代码
前端通过调用 Sender进行用户的监听注册,后台通过broadcaseMessage进行动态绑定。
<!DOCTYPE html> <html> <head> <title>SignalR Simple Chat</title> <style type="text/css"> .container { background-color: #99CCFF; border: thick solid #808080; padding: 20px; margin: 20px; } </style> </head> <body> <div class="container"> <label>input your name:</label> <input type="text" id="message" /> <input type="button" id="sendmessage" value="Register" /> <input type="hidden" id="displayname" /> <ul id="discussion"></ul> </div> <!--Script references. --> <!--Reference the jQuery library. --> <script src="Scripts/jquery-1.6.4.min.js"></script> <!--Reference the SignalR library. --> <script src="Scripts/jquery.signalR-2.0.2.min.js"></script> <!--Reference the autogenerated SignalR hub script. --> <script src="signalr/hubs"></script> <!--Add script to update the page and send messages.--> <script type="text/javascript"> $(function () { // Declare a proxy to reference the hub. $.connection.hub.logging = true; var chat = $.connection.chatHub; // Create a function that the hub can call to broadcast messages. chat.client.broadcastMessage = function (name, message) { // Html encode display name and message. var encodedName = $('<div />').text(name).html(); var encodedMsg = $('<div />').text(message).html(); // Add the message to the page. $('#discussion').append('<li><strong>' + encodedName + '</strong>: ' + encodedMsg + '</li>'); }; // Get the user name and store it to prepend to messages. $('#displayname').val(prompt('Enter your group, choose: 0 or 1:', '')); // Set initial focus to message input box. $('#message').focus(); // Start the connection. $.connection.hub.start().done(function () { $('#sendmessage').click(function () { // Call the Send method on the hub. chat.server.send($('#displayname').val(), $('#message').val()); // Clear text box and reset focus for next comment. $('#message').val('').focus(); }); }); }); </script> </body> </html>
四、测试:
前端输入分组0或1,并输入用户名。点击注册之后可以见到: