1. 程式人生 > >基於SignalR的服務端和客戶端通訊處理

基於SignalR的服務端和客戶端通訊處理

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的服務端和客戶端的相互配合,相互通訊過程。