基于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的服务端和客户端通讯处理_第1张图片

 

SignalR可以将参数序列化和反序列化. 这些参数被序列化的格式叫做Hub 协议, 所以Hub协议就是一种用来序列化和反序列化的格式.

Hub协议的默认协议是JSON, 还支持另外一个协议是MessagePack。MessagePack是二进制格式的, 它比JSON更紧凑, 而且处理起来更简单快速, 因为它是二进制的.

此外, SignalR也可以扩展使用其它协议。

 

2、基于SignalR构建的Winform服务端和客户端案例

服务单界面效果如下所示,主要功能为启动服务、停止服务,广播消息和查看连接客户端信息。

基于SignalR的服务端和客户端通讯处理_第2张图片

 客户端主要就是实时获取在线用户列表,以及发送、应答消息,消息可以群发,也可以针对特定的客户端进行消息一对一发送。

 客户端1:

基于SignalR的服务端和客户端通讯处理_第3张图片

客户端2:

基于SignalR的服务端和客户端通讯处理_第4张图片

构建的项目工程,包括服务端、客户端和两个之间的通讯对象类,如下所示。

基于SignalR的服务端和客户端通讯处理_第5张图片

服务端引用

基于SignalR的服务端和客户端通讯处理_第6张图片

客户端引用

基于SignalR的服务端和客户端通讯处理_第7张图片

服务端启动代码,想要定义一个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程序来处理它的启动,停止的,如下所示。

基于SignalR的服务端和客户端通讯处理_第8张图片

因此界面上通过按钮事件进行启动,启动服务的代码如下所示。

        private void btnStart_Click(object sender, EventArgs e)
        {
            this.btnStart.Enabled = false;
            WriteToInfo("正在连接中....");

            Task.Run(() =>
            {
                ServerStart();
            });
        }

 这里通过启动另外一个线程的处理,通过WebApp.Start启动入口类,并传入配置好的端口连接地址。

        /// 
        /// 开启服务
        /// 
        private void ServerStart()
        {
            try
            {
                //开启服务
                signalR = WebApp.Start(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>

停止服务代码如下所示,通过一个异步操作停止服务。

        /// 
        /// 停止服务
        /// 
        /// 
        private async Task StopServer()
        {
            if (signalR != null)
            {
                //向客户端广播消息
                hubContext = GlobalHost.ConnectionManager.GetHubContext();
                await hubContext.Clients.All.SendClose("服务端已关闭");

                //释放对象
                signalR.Dispose();
                signalR = null;

                WriteToInfo("服务端已关闭");
            }
        }

服务端对SignalR客户端的管理是通过一个继承于Hub的类SignalRHub进行管理,这个就是整个SignalR的核心了,它主要有几个函数需要重写,如OnConnected、OnDisconnected、OnReconnected、以及一个通用的消息发送AddMessage函数。

基于SignalR的服务端和客户端通讯处理_第9张图片

 

 客户端有接入的时候,我们会通过参数获取连接客户端的信息,并统一广播当前客户的状态信息,如下所示是服务端对于接入客户端的管理代码。

        /// 
        /// 在连接上时
        /// 
        public override Task OnConnected()
        {
            var client = JsonConvert.DeserializeObject(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事件进行处理,并初始化相关信息,如下代码所示。

        /// 
        /// 初始化服务连接
        /// 
        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("CheckLogin", this.txtUser.Text);

                HubConnection_Succeed();//处理连接后的初始化

                。。。。。。
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.StackTrace);
                this.richTextBox.AppendText("服务器连接失败:" + ex.Message);

                InitControlStatus(false);
                return;
            }
        }

客户端根据收到的不同协议信息,进行不同的事件处理,如下代码所示。

        /// 
        /// 对各种协议的事件进行处理
        /// 
        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();
            });
        }

例如我们对收到的文本信息,如一对一的发送消息或者广播消息,统一进行展示处理。

        /// 
        /// 处理文本消息
        /// 
        /// 
        /// 
        private void DealText(string data, BaseMessage basemsg)
        {
            //JSON转换为文本消息
            var msg = JsonConvert.DeserializeObject(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();
            }));
        }

客户端对消息的处理界面

基于SignalR的服务端和客户端通讯处理_第10张图片

而客户端发送消息,则是统一通过调用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的服务端和客户端的相互配合,相互通讯过程。

你可能感兴趣的:(基于SignalR的服务端和客户端通讯处理)