--- id: createwebsocketservice title: 创建WebSocket服务器 --- ## 一、说明 WebSocket是基于Http协议的升级协议,所以应当挂载在http服务器执行。 ## 二、可配置项 继承[HttpService](../docs/createhttpservice.mdx) ## 三、支持插件接口 支持**ITcpPlugin、IHttpPlugin、IWebSocketPlugin** 声明自定义实例类,然后实现**IWebSocketPlugin**接口,即可实现下列事务的触发。或者继承自**WebSocketPluginBase**类,重写相应方法即可。 | 插件方法| 功能 | | --- | --- | | OnHandshaking | 当收到握手请求之前,可以进行连接验证等 | | OnHandshaked | 当成功握手响应之后 | | OnHandleWSDataFrame | 当收到Websocket的数据报文 | | OnClosing | 当收到关闭请求时 | ## 四、创建WebSocket服务 ### 4.1 简单通过插件创建 通过插件创建的话,只能指定一个特殊url路由。如果想获得连接前的Http请求,也必须再添加一个实现IWebSocketPlugin接口的插件,然后从OnHandshaking方法中捕获。 ```csharp var service = new HttpService(); service.Setup(new TouchSocketConfig()//加载配置 .UsePlugin() .SetListenIPHosts(new IPHost[] { new IPHost(7789) }) .ConfigureContainer(a => { a.AddConsoleLogger(); }) .ConfigurePlugins(a => { a.UseWebSocket()//添加WebSocket功能 .SetWSUrl("/ws"); //.SetCallback(WSCallback);//WSCallback回调函数是在WS收到数据时触发回调的。下面会用插件,所以我们不使用这种方式 a.Add();//MyWebSocketPlugin是继承自WebSocketPluginBase的插件。 })) .Start(); service.Logger.Info("Http服务器已启动"); service.Logger.Info("ws://127.0.0.1:7789/ws"); ``` [插件] ```csharp class MyWebSocketPlugin : WebSocketPluginBase { protected override void OnHandleWSDataFrame(HttpSocketClient client, WSDataFrameEventArgs e) { if (e.DataFrame.Opcode == WSDataType.Text)//文本数据 { client.Logger.Info($"收到信息:{e.DataFrame.ToText()}"); } else if (e.DataFrame.Opcode == WSDataType.Binary)//二进制 { byte[] data = e.DataFrame.PayloadData.ToArray(); } } } ``` ### 4.2 通过WebApi创建 通过WebApi的方式会更加灵活,也能很方便的获得Http相关参数。还能实现多个Url的连接路由。 实现步骤: 1. 必须启用插件 2. 必须配置ConfigureRpcStore,和注册MyServer 3. 必须添加WebApiParserPlugin ```csharp var service = new HttpService(); service.Setup(new TouchSocketConfig()//加载配置 .UsePlugin() .SetListenIPHosts(new IPHost[] { new IPHost(7789) }) .ConfigureContainer(a => { a.AddConsoleLogger(); }) .ConfigureRpcStore(a=> { a.RegisterServer(); }) .ConfigurePlugins(a => { a.UseWebApi(); a.UseWebSocket();//不用设置连接url })) .Start(); Console.WriteLine("服务器已启动,可使用下列地址连接"); Console.WriteLine("ws://127.0.0.1:7789/MyServer/ConnectWS"); ``` ```csharp public class MyServer : RpcServer { private readonly ILog m_logger; public MyServer(ILog logger) { this.m_logger = logger; } [Router("/[api]/[action]")] [WebApi(HttpMethodType.GET, MethodFlags = MethodFlags.IncludeCallContext)] public void ConnectWS(IWebApiCallContext callContext) { if (callContext.Caller is HttpSocketClient socketClient) { if (socketClient.SwitchProtocolToWebSocket(callContext.HttpContext)) { m_logger.Info("WS通过WebApi连接"); } } } } ``` ### 4.3 创建基于Ssl的WebSocket服务 创建WSs服务器时,其他配置不变,只需要在config中配置SslOption代码即可。 在[RRQMBox](https://gitee.com/RRQM_Home/RRQMBox/tree/master/Ssl%E8%AF%81%E4%B9%A6%E7%9B%B8%E5%85%B3)中,放置了一个自制Ssl证书,密码为“RRQMSocket”以供测试。使用配置非常方便。 ```csharp var config = new TouchSocketConfig(); config.UsePlugin() .SetReceiveType(ReceiveType.Auto) .SetListenIPHosts(new IPHost[] { new IPHost(7789) }) .SetServiceSslOption(new ServiceSslOption() //Ssl配置,当为null的时候,相当于创建了ws服务器,当赋值的时候,相当于wss服务器。 { Certificate = new X509Certificate2("RRQMSocket.pfx", "RRQMSocket"), SslProtocols = SslProtocols.Tls12 }); ``` ## 五、接收消息 ### 5.1 回调接收 在添加**WebSocketServerPlugin**插件后,可以调用**SetCallback**函数,然后设置一个回调函数(如下所示),然后该函数在服务器收到信息时,会触发(并发触发)。 ```csharp static void WSCallback(ITcpClientBase client, WSDataFrameEventArgs e) { switch (e.DataFrame.Opcode) { case WSDataType.Cont: Console.WriteLine($"收到中间数据,长度为:{e.DataFrame.PayloadLength}"); break; case WSDataType.Text: Console.WriteLine(e.DataFrame.ToText()); break; case WSDataType.Binary: if (e.DataFrame.FIN) { Console.WriteLine($"收到二进制数据,长度为:{e.DataFrame.PayloadLength}"); } else { Console.WriteLine($"收到未结束的二进制数据,长度为:{e.DataFrame.PayloadLength}"); } break; case WSDataType.Close: { Console.WriteLine("远程请求断开"); client.Close("断开"); } break; case WSDataType.Ping: break; case WSDataType.Pong: break; default: break; } } ``` :::tip 提示 该函数,可能被并发执行的,所以应当做好线程安全。 ::: ### 5.2 插件接口接收 WS服务器,虽然是Http的插件,但是也能触发插件接口。适用于WS的插件接口是**IWebSocketPlugin**(或者从**WebSocketPluginBase**继承),声明任意类,实现该接口即可。 【定义插件】 ```csharp public class MyWebSocketPlugin : WebSocketPluginBase { protected override void OnHandleWSDataFrame(HttpSocketClient client, WSDataFrameEventArgs e) { switch (e.DataFrame.Opcode) { case WSDataType.Cont: client.Logger.Info($"收到中间数据,长度为:{e.DataFrame.PayloadLength}"); break; case WSDataType.Text: client.Logger.Info(e.DataFrame.ToText()); break; case WSDataType.Binary: if (e.DataFrame.FIN) { client.Logger.Info($"收到二进制数据,长度为:{e.DataFrame.PayloadLength}"); } else { client.Logger.Info($"收到未结束的二进制数据,长度为:{e.DataFrame.PayloadLength}"); } break; case WSDataType.Close: { client.Logger.Info("远程请求断开"); client.Close("断开"); } break; case WSDataType.Ping: break; case WSDataType.Pong: break; default: break; } } } ``` 【使用】 ```csharp {13} var service = new HttpService(); service.Setup(new TouchSocketConfig()//加载配置 .UsePlugin() .SetListenIPHosts(new IPHost[] { new IPHost(7789) }) .ConfigureContainer(a => { a.AddConsoleLogger(); }) .ConfigurePlugins(a => { a.UseWebSocket()//添加WebSocket功能 .SetWSUrl("/ws"); a.Add();//MyWebSocketPlugin是继承自WebSocketPluginBase的插件。 })) .Start(); ``` :::tip 提示 插件的所有函数,都是可能被并发执行的,所以应当做好线程安全。 ::: ## 六、回复、响应数据 要回复Websocket消息,必须使用**HttpSocketClient**对象。 ### 6.1 如何获取SocketClient? #### (1)直接获取所有在线客户端 通过`service.GetClients`方法,获取当前在线的所有客户端。 ```csharp HttpSocketClient[] socketClients = service.GetClients(); foreach (var item in socketClients) { if (item.Protocol == Protocol.WebSocket)//先判断是不是websocket协议 { if (item.ID == "id")//再按指定id发送,或者直接广播发送 { } } } ``` :::caution 注意 由于HttpSocketClient的生命周期是由框架控制的,所以最好尽量不要直接引用该实例,可以引用HttpSocketClient.ID,然后再通过服务器查找。 ::: #### (2)通过ID获取 先调用`service.GetIDs`方法,获取当前在线的所有客户端的ID,然后选择需要的ID,通过TryGetSocketClient方法,获取到想要的客户端。 ```csharp string[] ids = service.GetIDs(); if (service.TryGetSocketClient(ids[0], out HttpSocketClient socketClient)) { } ``` ### 6.2 发送文本类消息 ```csharp socketClient.SendWithWS("Text"); ``` ### 6.3 发送二进制消息 ```csharp socketClient.SendWithWS(new byte[10]); ``` ### 6.4 发送分包的二进制 例如:发送的数据为{0,1,2,3,4,5,6,7,8,9},当设置packageSize为5时,会先发送{0,1,2,3,4}作为头包,然后发送{5,6,7,8,9}的后继包。 ```csharp byte[] data = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; socketClient.SubSendWithWS(data, 5); ``` ### 6.5 直接发送自定义构建的数据帧 ```csharp WSDataFrame frame=new WSDataFrame(); frame.Opcode= WSDataType.Text; frame.FIN= true; frame.RSV1= true; frame.RSV2= true; frame.RSV3= true; frame.AppendText("I"); frame.AppendText("Love"); frame.AppendText("U"); socketClient.SendWithWS(frame); ``` :::info 备注 此部分功能就需要你对Websocket有充分了解才可以操作。 ::: :::caution 注意 Websocket的所有发送,都是形如**SendWithWS**的扩展方法。不可直接Send。 ::: 下面演示如何在插件直接响应 ```csharp {9} public class MyWebSocketPlugin : WebSocketPluginBase { protected override void OnHandleWSDataFrame(HttpSocketClient client, WSDataFrameEventArgs e) { switch (e.DataFrame.Opcode) { case WSDataType.Text: client.Logger.Info(e.DataFrame.ToText()); client.SendWithWS("我已收到"); break; default: break; } } } ``` :::caution 注意 如果是使用的WSCallback回调接收,则需要将**ITcpClientBase**对象,强制转换为**HttpSocketClient**。 ```csharp {6} static void WSCallback(ITcpClientBase client, WSDataFrameEventArgs e) { switch (e.DataFrame.Opcode) { case WSDataType.Text: ((HttpSocketClient)client).SendWithWS("我已收到"); break; default: break; } } ``` ::: ## 七、服务器广播发送 ```csharp //广播给所有人 foreach (var item in service.GetClients()) { if (item.Protocol== Protocol.WebSocket) { item.SendWithWS("广播"); } } ``` :::tip 提示 在发送时,还可以自己过滤ID。 :::