通常情况下SignalR可以与Web站点部署在一起,但是在某些情况下应该考虑将SignalR自托管在WinForm或者Windows Service中,比如客户端高度依赖SignalR传递信息,且这种依赖是持续的而不能中断。这是因为SignalR需要把每一个连接的客户端的信息保存在内存中,但是当IIS站点负载较重或者长时间没有访问的情况下,应用程序池会自动回收,这样所有的连接信息就丢失了,而客户端无法获得这个信息,用户就会以为程序出了BUG。如果托管在Windows Service中,除非程序出错或者重启,这个信息不会丢失。
以下是在Service端需要做的工作。
一、在Windows Service程序中通过Nuget添加Microsoft.AspNet.SignalR.SelfHost和Microsoft.Owin.Cors。
在App.Config中添加一个配置
这句的意思是在系统启动的时候首先执行eWHService.Startup中的代码,Startup.cs如下
using Owin; using Microsoft.Owin.Cors; using Microsoft.AspNet.SignalR; namespace eWHService { public class Startup { public void Configuration(IAppBuilder app) { app.UseCors(CorsOptions.AllowAll); app.MapSignalR(new HubConfiguration { EnableDetailedErrors = true }); } } }
由于SignalR是自托管的,其站点与应用程序本身的站点一定是不一样的,因此必须启用跨域访问才能实现。
二、添加另外一个设置
这句的意思是把SignalR托管在1080这个端口,客户端将通过http://IP或域名:1080这个地址来访问
在Service的OnStart中添加
WebApp.Start(System.Configuration.ConfigurationManager.AppSettings["SignalRURL"]);
三、 添加Hub Hub是SignalR服务器与客户端浏览器的一个通道,用于双方互相调用对方的程序。添加一个Hub类如下
using System; using Microsoft.AspNet.SignalR; using Microsoft.AspNet.SignalR.Hubs; using System.Threading.Tasks; namespace SignalR { [HubName("job")] //供客户端引用的Hub的名称 public class JobHub:Hub { private readonly ConnectionManager _connectionManager; public JobHub() : this(ConnectionManager.Instance) { } public JobHub(ConnectionManager connectionManager) { _connectionManager = connectionManager; } public override Task OnConnected() { Register(); return base.OnConnected(); } public override Task OnReconnected() { Register(); return base.OnReconnected(); } public override Task OnDisconnected(bool stopCalled) { Unregister(); return base.OnDisconnected(stopCalled); } private void Register() { _connectionManager.RegisterClient(Context.ConnectionId, Context.QueryString["MonitorJob"]); } private void Unregister() { _connectionManager.RemoveClient(Context.ConnectionId); } #region "Server functions for client" #endregion } }
Hub要做2件事
1 定义客户端Connected, Disconnected和Reconnected事件的处理程序。
Connected在客户端发起连接的时候发生,同一台PC机,不同的浏览器,同一个浏览器不同的窗口或者TAB,会激发单独的Connected事件
Reconnected是在出现网络故障又在短时间内恢复的情况下发生,但在服务器端出现故障又恢复的情况下不会发生
Disconnected在客户端窗口离开,刷新或者主动断开(比较少见)的情况下发生。
以上三种情况,服务器端要做的通常就是管理到底哪些客户端连接上了,以及什么样的信息需要发送给这个客户端。详见ConnectionManager类
在这三个事件中,可以通过Context.ConnectionID得到客户端的唯一标识,将来向客户端发送消息也要通过这个标识来找到对应的客户端。同时可以通过Context.QueryString得到客户端发起连接请求时发送的额外参数,这些参数一般就包括什么样的信息是这个客户端所关心的
2 定义客户端JS可以直接调用的服务器端程序
四、 定义ConnectionManager,代码如下
using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNet.SignalR; namespace SignalR { public class ConnectionManager { private readonly static ConnectionManager _instance = new ConnectionManager(); private readonly Dictionary> _clients = new Dictionary>(); private ConnectionManager() { } /// /// Gets the instance. /// public static ConnectionManager Instance { get { return _instance; } } public IEnumerable GetAllClient() { return _clients.Keys; } public void RegisterClient(string psConnectionID, string psJob) { lock (_clients) { IList arrClient = new List(); string[] aJobs = psJob.Split(','); foreach(string sJob in aJobs) { string[] aTmp = sJob.Split('|'); arrClient.Add(new SignalRClient { JobName = aTmp[0], Param = aTmp[1] }); } if (_clients.ContainsKey(psConnectionID)) { _clients[psConnectionID] = arrClient; } else { _clients.Add(psConnectionID,arrClient); } } } public void RemoveClient(string psConnectionID) { lock (_clients) { _clients.Remove(psConnectionID); } } //向客户端发送消息 public void JobChanged(string psJobCategory, string psJobName,string psParam, string psOrderId ) { IHubContext oHub = GlobalHost.ConnectionManager.GetHubContext(); foreach(KeyValuePair> kvp in _clients) { if (kvp.Value.Count(x=>x.JobName==psJobName && x.Param==psParam)>0) { oHub.Clients.Client(kvp.Key).JobChanged(psJobCategory,psJobName, psOrderId); } } } } }
ConnectionManager必须是一个单例类,因此务必要有一个私有的构造函数
2 需要维护一个包含客户端连接信息的存储结构(如Dictionary或者List),存储的内容是客户端的ConnectionID以及它所关心的信息。至于如何组织这个数据结构可以根据项目的实际需要
3 定义向客户端发送消息的函数 这个(或多个)函数的作用,就是在各种事件发生的情况下,从存储的连接信息中找到对这个事件感兴趣的客户端并把相应的信息发出去。在上面的例子中,
3.1 oHub.Clients.Client(ConnectionID) 可以根据某个ConnectionID找到客户端,其数据类型是dynamic
3.2 dynamic类型可以定义任意的方法和属性而不需要事先定义,这里JobChanged就是一个凭空造出来的方法,这个方法真正是在客户端的js中定义的
以下是在Web端需要做的工作
一、 下载signalR客户端的库并引用,目前用的是2.2.0版本。没有特别的理由,不要轻易升级,因为客户会造成breaking change,导致莫名其妙的错误。注意:使用SignalR之前必须首先引用jquery
二、 定义一个SignalRProcessor的Augular服务,代码如下
app.factory('signalRHubProxy', ['$rootScope','$timeout', function ($rootScope, $timeout) { function signalRHubProxyFactory(serverUrl, hubName,querystring) { var connection = $.hubConnection(serverUrl);//连接定义在Windows Service中的SignalR站点 connection.logging = true; if (querystring!=undefined) connection.qs = querystring; var proxy = connection.createHubProxy(hubName); proxy.on('dummyMethod', function (message) { }); connection.start({ transport: ['webSockets', 'serverSentEvents', 'longPolling'] } )//优先使用webSockets,其次是serverSentEvents,最后使用longPolling .done(function () { }) .fail(function (m) { alert(m) }); connection.disconnected(function () {//中断的情况下自动重连 $timeout(function () { connection.start({ transport: ['webSockets', 'serverSentEvents', 'longPolling'] }).done(function () { }); }, 2000); }); return { on: function (eventName, callback) { proxy.on(eventName, function () { var ret = arguments; $rootScope.$apply(function () { if (callback) { callback.apply(callback,ret); } }); }); }, off: function (eventName, callback) { proxy.off(eventName, function (result) { $rootScope.$apply(function () { if (callback) { callback(result); } }); }); }, invoke: function (methodName, args, callback) { args.unshift(methodName); proxy.invoke.apply(proxy,args) .done(function (result) { $rootScope.$apply(function () { if (callback) { callback(result); } }); }).fail(function (error) { console.log(error); }); }, connection: connection }; }; return signalRHubProxyFactory; }]);
三、在客户端合适的位置(通常是在MainFrame中)调用SignarlRProcessor
var jobProxy = new signalRHubProxy("http://localhost:1080", 'job', "MonitorJob=" + monitorJob); jobProxy.on('jobChanged', function (jobCategory, jobName, orderId) {//jobChanged就是在ConnectionManager中定义的那个dynamic的方法,参数就是这个方法的三个参数 if (jobCategory == "JobAdded") SetJobCount(jobName, 1); else SetJobCount(jobName, -1); });
Web站点与Windows Service如何通信
通常情况下业务发生在Web站点,怎样把相关的消息发给Windows Service,并进而传递到客户端呢?
可以采用的方法包括WCF Service, Socket通讯, 数据库/文件集成,消息队列以及.net Remoting
Socket通讯是最底层的网络通讯方式, Service端启动端口侦听,Web端通过此端口直接发送字符串,此方法过于底层,需要考虑的细节太多
数据库/文件集成 Web端把数据写入数据库或者文件,Service端轮询看是否有新数据
WCF Service是比较先进的方式,可以通过http, namedpipes, messagequeue等方式与客户端通信,但需要在Service端再托管一个站点,比较麻烦。
.net Remoting是一个相对于WCF比较过时的技术,但实现起来比较简单,对于简单的数据通讯比较合适。
以下是使用.net Remoting的步骤
1 定义一个独立的类库WinServiceConnector,添加JobEventConnection类如下
using System; namespace WinServiceConnector { public delegate void JobAddedEventHandler(object sender, string jobName, string param, string orderId); public delegate void JobRemovedEventHandler(object sender, string jobName, string param, string orderId); public class JobEventConnection : MarshalByRefObject,IJobEvent { public JobEventConnection() { } //定义委托,实际的实现在Service中定义 public static event JobAddedEventHandler JobAddedEvent; public static event JobRemovedEventHandler JobRemovedEvent; public void OnJobAddedEvent(object sender, string jobName, string param, string orderId) { if (JobAddedEvent != null) { JobAddedEvent(sender, jobName, param, orderId); } } public void OnJobRemovedEvent(object sender, string jobName, string param, string orderId) { if (JobRemovedEvent != null) { JobRemovedEvent(sender, jobName,param, orderId); } } public void JobAdded(string jobName, string param, string orderId) { OnJobAddedEvent(this, jobName, param, orderId); } public void JobRemoved(string jobName, string param, string orderId) { OnJobRemovedEvent(this, jobName,param, orderId); } } }
JobEventConnection类是可以远程(remoting)调用的类. JobAdded,JobRemoved是类中可以调用的方法。
2 在Service和Web.MVC中分别添加引用
3 在Web.config中添加如下配置项
4 在Web.MVC中需要通知Service的地方,添加如下代码
JobEvent.JobEventBase oEvent = new JobEvent.JobAddedEvent("TobeManualInspected", "1", "");
oEvent.RaiseEvent();
JobEventBase代码如下
using System; using System.Collections.Generic; using System.Linq; using eWH.Domain.DomainModel; using WinServiceConnector; namespace JobEvent { public abstract class JobEventBase { protected JobEventConnection moRemotingConnection = null; public JobEventBase() { moRemotingConnection = (JobEventConnection)System.Activator.GetObject(typeof(JobEventConnection), WebSetting.JobEventRemotingUrl); //从Remoting地址中生成一个JobEventConnection对象 } public abstract void RaiseEvent(); } }
JobEventBase有若干子类(每个类对应一个事件),JobAddedEvent如下
using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace JobEvent { public class JobAddedEvent:JobEventBase { private string JobName { get; set; } private string Param { get; set; } private string OrderId { get; set; } public JobAddedEvent(string jobName, string param, string orderId) { this.JobName = jobName; this.Param = param; this.OrderId = orderId; } public override void RaiseEvent() { moRemotingConnection.JobAdded(this.JobName, this.Param,this.OrderId); } } }
5 在Service端添加新的配置项
注册远程对象如下
public void RegisterRemotingURI() { int nPortNo = Convert.ToInt32(System.Configuration.ConfigurationManager.AppSettings["PendingJobEventRemotingPort"]); TcpChannel oJobChannel = new TcpChannel(nPortNo); ChannelServices.RegisterChannel(oJobChannel, false); RemotingConfiguration.CustomErrorsMode = CustomErrorsModes.Off; RemotingConfiguration.RegisterWellKnownServiceType(typeof(JobEventConnection), "JobEvent", WellKnownObjectMode.SingleCall); JobEventConnection.JobAddedEvent += new JobAddedEventHandler(Job_Added); JobEventConnection.JobRemovedEvent += new JobRemovedEventHandler(Job_Removed); } //下面这2个方法向JS客户端发送消息 private void Job_Added(object sender, string jobName, string department, string orderId) { ConnectionManager.Instance.JobChanged("JobAdded", jobName, department, orderId); } private void Job_Removed(object sender, string jobName, string department, string orderId) { ConnectionManager.Instance.JobChanged("JobRemoved", jobName, department, orderId); }