SignalR是一個.NET Core/.NET Framework的實時通訊的框架,一般應用在ASP.NET上,當然也可以應用在Winform上實現服務端和客戶端的消息通訊,本篇隨筆主要基於SignalR的構建一個基於Winform的服務端和客戶端的通訊處理案例,介紹其中的處理過程。 ...
SignalR是一個.NET Core/.NET Framework的實時通訊的框架,一般應用在ASP.NET上,當然也可以應用在Winform上實現服務端和客戶端的消息通訊,本篇隨筆主要基於SignalR的構建一個基於Winform的服務端和客戶端的通訊處理案例,介紹其中的處理過程。
1、SignalR基礎知識
SignalR是一個.NET Core/.NET Framework的開源實時框架. SignalR的可使用Web Socket, Server Sent Events 和 Long Polling作為底層傳輸方式。
SignalR基於這三種技術構建, 抽象於它們之上, 它讓你更好的關註業務問題而不是底層傳輸技術問題。
SignalR將整個信息的交換封裝起來,客戶端和伺服器都是使用JSON來溝通的,在服務端聲明的所有Hub信息,都會生成JavaScript輸出到客戶端,.NET則依賴Proxy來生成代理對象,而Proxy的內部則是將JSON轉換成對象。
RPC
RPC (Remote Procedure Call). 它的優點就是可以像調用本地方法一樣調用遠程服務.
SignalR採用RPC範式來進行客戶端與伺服器端之間的通信.
SignalR利用底層傳輸來讓伺服器可以調用客戶端的方法, 反之亦然, 這些方法可以帶參數, 參數也可以是複雜對象, SignalR負責序列化和反序列化.
Hub
Hub是SignalR的一個組件, 它運行在ASP.NET Core應用里. 所以它是伺服器端的一個類.
Hub使用RPC接受從客戶端發來的消息, 也能把消息發送給客戶端. 所以它就是一個通信用的Hub.
在ASP.NET Core里, 自己創建的Hub類需要繼承於基類Hub。在Hub類裡面, 我們就可以調用所有客戶端上的方法了. 同樣客戶端也可以調用Hub類里的方法.
SignalR可以將參數序列化和反序列化. 這些參數被序列化的格式叫做Hub 協議, 所以Hub協議就是一種用來序列化和反序列化的格式.
Hub協議的預設協議是JSON, 還支持另外一個協議是MessagePack。MessagePack是二進位格式的, 它比JSON更緊湊, 而且處理起來更簡單快速, 因為它是二進位的.
此外, SignalR也可以擴展使用其它協議。
2、基於SignalR構建的Winform服務端和客戶端案例
服務單界面效果如下所示,主要功能為啟動服務、停止服務,廣播消息和查看連接客戶端信息。
客戶端主要就是實時獲取線上用戶列表,以及發送、應答消息,消息可以群發,也可以針對特定的客戶端進行消息一對一發送。
客戶端1:
客戶端2:
構建的項目工程,包括服務端、客戶端和兩個之間的通訊對象類,如下所示。
服務端引用
客戶端引用
服務端啟動代碼,想要定義一個Startup類,用來承載SignalR的入口處理。
[assembly: OwinStartup(typeof(SignalRServer.Startup))] namespace SignalRServer { public class Startup { public void Configuration(IAppBuilder app) { var config = new HubConfiguration(); config.EnableDetailedErrors = true; //設置可以跨域訪問 app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll); //映射到預設的管理 app.MapSignalR(config); } } }
我們前面介紹過,服務端使用Winform程式來處理它的啟動,停止的,如下所示。
因此界面上通過按鈕事件進行啟動,啟動服務的代碼如下所示。
private void btnStart_Click(object sender, EventArgs e) { this.btnStart.Enabled = false; WriteToInfo("正在連接中...."); Task.Run(() => { ServerStart(); }); }
這裡通過啟動另外一個線程的處理,通過WebApp.Start啟動入口類,並傳入配置好的埠連接地址。
/// <summary> /// 開啟服務 /// </summary> private void ServerStart() { try { //開啟服務 signalR = WebApp.Start<Startup>(serverUrl); InitControlState(true); } catch (Exception ex) { //服務失敗時的處理 WriteToInfo("服務開啟失敗,原因:" + ex.Message); InitControlState(false); return; } WriteToInfo("服務開啟成功 : " + serverUrl); }
連接地址我們配置在xml文件裡面,其中的 serverUrl 就是指向下麵的鍵url, 配置的url如下所示:
<?xml version="1.0" encoding="utf-8"?> <configuration> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2"/> </startup> <appSettings> <add key="url" value="http://localhost:17284"/> </appSettings>
停止服務代碼如下所示,通過一個非同步操作停止服務。
/// <summary> /// 停止服務 /// </summary> /// <returns></returns> private async Task StopServer() { if (signalR != null) { //向客戶端廣播消息 hubContext = GlobalHost.ConnectionManager.GetHubContext<SignalRHub>(); await hubContext.Clients.All.SendClose("服務端已關閉"); //釋放對象 signalR.Dispose(); signalR = null; WriteToInfo("服務端已關閉"); } }
服務端對SignalR客戶端的管理是通過一個繼承於Hub的類SignalRHub進行管理,這個就是整個SignalR的核心了,它主要有幾個函數需要重寫,如OnConnected、OnDisconnected、OnReconnected、以及一個通用的消息發送AddMessage函數。
客戶端有接入的時候,我們會通過參數獲取連接客戶端的信息,並統一廣播當前客戶的狀態信息,如下所示是服務端對於接入客戶端的管理代碼。
/// <summary> /// 在連接上時 /// </summary> public override Task OnConnected() { var client = JsonConvert.DeserializeObject<ClientModel>(Context.QueryString.Get("Param")); if (client != null) { client.ConnId = Context.ConnectionId; //將客戶端連接加入列表 if (!Portal.gc.ClientList.Exists(e => e.ConnId == client.ConnId)) { Portal.gc.ClientList.Add(client); } Groups.Add(client.ConnId, "Client"); //向服務端寫入一些數據 Portal.gc.MainForm.WriteToInfo("客戶端連接ID:" + Context.ConnectionId); Portal.gc.MainForm.WriteToInfo(string.Format("客戶端 【{0}】接入: {1} , IP地址: {2} \n 客戶端總數: {3}", client.Name, Context.ConnectionId, client.IPAddress, Portal.gc.ClientList.Count)); //先所有連接客戶端廣播連接客戶狀態 var imcp = new StateMessage() { Client = client, MsgType = MsgType.State, FromConnId = client.ConnId, Success = true }; var jsonStr = JsonConvert.SerializeObject(imcp); Clients.Group("Client", new string[0]).addMessage(jsonStr); return base.OnConnected(); } return Task.FromResult(0); }
客戶端的接入,需要對相應的HubConnection事件進行處理,並初始化相關信息,如下代碼所示。
/// <summary> /// 初始化服務連接 /// </summary> private void InitHub() { 。。。。。。 //連接的時候傳遞參數Param var param = new Dictionary<string, string> { { "Param", JsonConvert.SerializeObject(client) } }; //創建連接對象,並實現相關事件 Connection = new HubConnection(serverUrl, param); 。。。。。。//實現相關事件 Connection.Closed += HubConnection_Closed; Connection.Received += HubConnection_Received; Connection.Reconnected += HubConnection_Succeed; Connection.TransportConnectTimeout = new TimeSpan(3000); //綁定一個集線器 hubProxy = Connection.CreateHubProxy("SignalRHub"); AddProtocal(); }
private async Task StartConnect() { try { //開始連接 await Connection.Start(); await hubProxy.Invoke<CommonResult>("CheckLogin", this.txtUser.Text); HubConnection_Succeed();//處理連接後的初始化 。。。。。。 } catch (Exception ex) { Console.WriteLine(ex.StackTrace); this.richTextBox.AppendText("伺服器連接失敗:" + ex.Message); InitControlStatus(false); return; } }
客戶端根據收到的不同協議信息,進行不同的事件處理,如下代碼所示。
/// <summary> /// 對各種協議的事件進行處理 /// </summary> private void AddProtocal() { //接收實時信息 hubProxy.On<string>("AddMessage", DealMessage); //連接上觸發connected處理 hubProxy.On("logined", () => this.Invoke((Action)(() => { this.Text = string.Format("當前用戶:{0}", this.txtUser.Text); richTextBox.AppendText(string.Format("以名稱【" + this.txtUser.Text + "】連接成功!" + Environment.NewLine)); InitControlStatus(true); })) ); //服務端拒絕的處理 hubProxy.On("rejected", () => this.Invoke((Action)(() => { richTextBox.AppendText(string.Format("無法使用名稱【" + this.txtUser.Text + "】進行連接!" + Environment.NewLine)); InitControlStatus(false); CloseHub(); })) ); //客戶端收到服務關閉消息 hubProxy.On("SendClose", () => { CloseHub(); }); }
例如我們對收到的文本信息,如一對一的發送消息或者廣播消息,統一進行展示處理。
/// <summary> /// 處理文本消息 /// </summary> /// <param name="data"></param> /// <param name="basemsg"></param> private void DealText(string data, BaseMessage basemsg) { //JSON轉換為文本消息 var msg = JsonConvert.DeserializeObject<TextMessage>(data); var ownerClient = ClientList.FirstOrDefault(f => f.ConnId == basemsg.FromConnId); var ownerName = ownerClient == null ? "系統廣播" : ownerClient.Name; this.Invoke(new Action(() => { richTextBox.AppendText(string.Format("{0} - {1}:\n {2}" + Environment.NewLine, DateTime.Now, ownerName, msg.Message)); richTextBox.ScrollToCaret(); })); }
客戶端對消息的處理界面
而客戶端發送消息,則是統一通過調用Hub的AddMessage方法進行發送即可,如下代碼所示。
private void BtnSendMessage_Click(object sender, EventArgs e) { if (txtMessage.Text.Length == 0) return; var message = new TextMessage() { MsgType = MsgType.Text, FromConnId = client.ConnId, ToConnId = this.toId, Message = txtMessage.Text, Success = true }; hubProxy.Invoke("AddMessage", JsonConvert.SerializeObject(message)); txtMessage.Text = string.Empty; txtMessage.Focus(); }
其中的hubProxy是我們前面連接服務端的時候,構造出的一個代理對象
hubProxy = Connection.CreateHubProxy("SignalRHub");
客戶端關閉的時候,我們銷毀相關的對象即可。
private void Form1_FormClosing(object sender, FormClosingEventArgs e) { if (Connection != null) { Connection.Stop(); Connection.Dispose(); } }
以上就是SignalR的服務端和客戶端的相互配合,相互通訊過程。