跳到主要内容
版本:2.0.0

创建WebSocket服务器

定义

命名空间:TouchSocket.Http.WebSockets
程序集:TouchSocket.Http.dll

一、说明

WebSocket是基于Http协议的升级协议,所以应当挂载在http服务器执行。

二、可配置项

继承HttpService

三、支持插件接口

插件方法功能
IWebSocketHandshakingPlugin当收到握手请求之前,可以进行连接验证等
IWebSocketHandshakedPlugin当成功握手响应之后
IWebSocketReceivedPlugin当收到Websocket的数据报文
IWebSocketClosingPlugin当收到关闭请求时触发。如果对方直接断开连接,则此方法则不会触发,届时可以考虑使用ITcpDisconnectedPlugin

四、创建WebSocket服务

4.1 简单直接创建

通过插件创建的话,只能指定一个特殊url路由。如果想获得连接前的Http请求,也必须再添加一个实现IWebSocketPlugin接口的插件,然后从OnHandshaking方法中捕获。

var service = new HttpService();
service.Setup(new TouchSocketConfig()//加载配置
.SetListenIPHosts(7789)
.ConfigureContainer(a =>
{
a.AddConsoleLogger();
})
.ConfigurePlugins(a =>
{
a.UseWebSocket()//添加WebSocket功能
.SetWSUrl("/ws")//设置url直接可以连接。
.UseAutoPong();//当收到ping报文时自动回应pong
}));

service.Start();

service.Logger.Info("服务器已启动");

4.2 验证连接

可以对连接的UrlQueryHeader等参数进行验证,然后决定是否执行WebSocket连接。

var service = new HttpService();
service.Setup(new TouchSocketConfig()//加载配置
.SetListenIPHosts(7789)
.ConfigureContainer(a =>
{
a.AddConsoleLogger();
})
.ConfigurePlugins(a =>
{
a.UseWebSocket()//添加WebSocket功能
.SetVerifyConnection(VerifyConnection)
.UseAutoPong();//当收到ping报文时自动回应pong
}));

service.Start();

service.Logger.Info("服务器已启动");
/// <summary>
/// 验证websocket的连接
/// </summary>
/// <param name="client"></param>
/// <param name="context"></param>
/// <returns></returns>
private static bool VerifyConnection(IHttpSocketClient client, HttpContext context)
{
if (!context.Request.IsUpgrade())//如果不包含升级协议的header,就直接返回false。
{
return false;
}
if (context.Request.UrlEquals("/ws"))//以此连接,则直接可以连接
{
return true;
}
else if (context.Request.UrlEquals("/wsquery"))//以此连接,则需要传入token才可以连接
{
if (context.Request.Query.Get("token") == "123456")
{
return true;
}
else
{
context.Response
.SetStatus(403, "token不正确")
.Answer();
}
}
else if (context.Request.UrlEquals("/wsheader"))//以此连接,则需要从header传入token才可以连接
{
if (context.Request.Headers.Get("token") == "123456")
{
return true;
}
else
{
context.Response
.SetStatus(403, "token不正确")
.Answer();
}
}
return false;
}

4.3 通过WebApi创建

通过WebApi的方式会更加灵活,也能很方便的获得Http相关参数。还能实现多个Url的连接路由。 实现步骤:

  1. 必须配置ConfigureRpcStore,和注册MyServer
  2. 必须添加WebApiParserPlugin
var service = new HttpService();
service.Setup(new TouchSocketConfig()//加载配置
.SetListenIPHosts(7789)
.ConfigureContainer(a =>
{
a.AddConsoleLogger();
})
.ConfigurePlugins(a =>
{
a.UseWebApi()
.ConfigureRpcStore(store =>
{
store.RegisterServer<MyServer>();
});
}));

service.Start();

service.Logger.Info("服务器已启动");
public class MyServer : RpcServer
{
private readonly ILog m_logger;

public MyServer(ILog logger)
{
this.m_logger = logger;
}

[Router("/[api]/[action]")]
[WebApi(HttpMethodType.GET)]
public void ConnectWS(IWebApiCallContext callContext)
{
if (callContext.Caller is HttpSocketClient socketClient)
{
if (socketClient.SwitchProtocolToWebSocket(callContext.HttpContext))
{
m_logger.Info("WS通过WebApi连接");
}
}
}
}

4.4 通过Http上下文直接创建

使用上下文直接创建的优点在于能更加个性化的实现WebSocket的连接。

class MyHttpPlugin : PluginBase, IHttpPlugin<IHttpSocketClient>
{
public async Task OnHttpRequest(IHttpSocketClient client, HttpContextEventArgs e)
{
if (e.Context.Request.UrlEquals("/GetSwitchToWebSocket"))
{
var result =await client.SwitchProtocolToWebSocket(e.Context);
return;
}
await e.InvokeNext();
}
}

4.5 创建基于Ssl的WebSocket服务

创建WSs服务器时,其他配置不变,只需要在config中配置SslOption代码即可,放置了一个自制Ssl证书,密码为“RRQMSocket”以供测试。使用配置非常方便。

var config = new TouchSocketConfig();
config.SetServiceSslOption(new ServiceSslOption() //Ssl配置,当为null的时候,相当于创建了ws服务器,当赋值的时候,相当于wss服务器。
{
Certificate = new X509Certificate2("RRQMSocket.pfx", "RRQMSocket"),
SslProtocols = SslProtocols.Tls12
});

五、接收消息

WebSocket服务器接收消息,目前有两种方式。第一种就是通过订阅IWebSocketReceivedPlugin插件完全异步的接收消息。第二种就是调用WebSocket,然后调用ReadAsync方法异步阻塞式读取。

5.1 插件接收消息

【定义插件】

public class MyWebSocketPlugin : PluginBase, IWebSocketReceivedPlugin
{
private readonly ILog m_logger;

public MyWebSocketPlugin(ILog logger)
{
this.m_logger = logger;
}
public async Task OnWebSocketReceived(IWebSocket client, WSDataFrameEventArgs e)
{
switch (e.DataFrame.Opcode)
{
case WSDataType.Cont:
m_logger.Info($"收到中间数据,长度为:{e.DataFrame.PayloadLength}");

return;

case WSDataType.Text:
m_logger.Info(e.DataFrame.ToText());

if (!client.Client.IsClient)
{
client.Send("我已收到");
}
return;

case WSDataType.Binary:
if (e.DataFrame.FIN)
{
m_logger.Info($"收到二进制数据,长度为:{e.DataFrame.PayloadLength}");
}
else
{
m_logger.Info($"收到未结束的二进制数据,长度为:{e.DataFrame.PayloadLength}");
}
return;

case WSDataType.Close:
{
m_logger.Info("远程请求断开");
client.Close("断开");
}
return;

case WSDataType.Ping:
break;

case WSDataType.Pong:
break;

default:
break;
}

await e.InvokeNext();
}
}

【使用】

var service = new HttpService();
service.Setup(new TouchSocketConfig()//加载配置
.SetListenIPHosts(7789)
.ConfigureContainer(a =>
{
a.AddConsoleLogger();
})
.ConfigurePlugins(a =>
{
a.UseWebSocket()//添加WebSocket功能
.SetWSUrl("/ws");
a.Add<MyWebSocketPlugin>();//自定义插件。
}));

service.Start();
提示

插件的所有函数,都是可能被并发执行的,所以应当做好线程安全。

5.2 WebSocket显式ReadAsync

WebSocket显式ReadAsync数据,实际上也要用到插件,但是,使用的仅仅是IWebSocketHandshakedPlugin,因为我们只需要拦截握手成功的消息。

class MyReadWebSocketPlugin : PluginBase, IWebSocketHandshakedPlugin
{
private readonly ILog m_logger;

public MyReadWebSocketPlugin(ILog logger)
{
this.m_logger = logger;
}
public async Task OnWebSocketHandshaked(IWebSocket client, HttpContextEventArgs e)
{
//当WebSocket想要使用ReadAsync时,需要设置此值为true
client.AllowAsyncRead = true;

//此处即表明websocket已连接
while (true)
{
using (var receiveResult = await client.ReadAsync(CancellationToken.None))
{

if (receiveResult.DataFrame == null)
{
break;
}

//判断是否为最后数据
//例如发送方发送了一个10Mb的数据,接收时可能会多次接收,所以需要此属性判断。
if (receiveResult.DataFrame.FIN)
{
if (receiveResult.DataFrame.IsText)
{
m_logger.Info($"WebSocket文本:{receiveResult.DataFrame.ToText()}");
}
}

}
}

//此处即表明websocket已断开连接
m_logger.Info("WebSocket断开连接");
await e.InvokeNext();
}
}

【使用】

private static HttpService CreateHttpService()
{
var service = new HttpService();
service.Setup(new TouchSocketConfig()//加载配置
.SetListenIPHosts(7789)
.ConfigureContainer(a =>
{
a.AddConsoleLogger();
})
.ConfigurePlugins(a =>
{
a.UseWebSocket()//添加WebSocket功能
.SetWSUrl("/ws")//设置url直接可以连接。
.UseAutoPong();//当收到ping报文时自动回应pong

a.Add<MyReadWebSocketPlugin>();
}));

service.Start();

service.Logger.Info("服务器已启动");
service.Logger.Info("直接连接地址=>ws://127.0.0.1:7789/ws");
return service;
}
信息

ReadAsync的方式是属于同步不阻塞的接收方式(和当下Aspnetcore模式一样)。他不会单独占用线程,只会阻塞当前Task。所以可以大量使用,不需要考虑性能问题。同时,ReadAsync的好处就是单线程访问上下文,这样在处理ws分包时是非常方便的。

注意

使用该方式,会阻塞IWebSocketHandshakedPlugin的插件传递。在收到WebSocket消息的时候,不会再触发插件。

六、回复、响应数据

要回复Websocket消息,必须使用HttpSocketClient对象。

6.1 如何获取SocketClient?

(1)直接获取所有在线客户端

通过service.GetClients方法,获取当前在线的所有客户端。

HttpSocketClient[] socketClients = service.GetClients();
foreach (var item in socketClients)
{
if (item.Protocol == Protocol.WebSocket)//先判断是不是websocket协议
{
if (item.Id == "id")//再按指定id发送,或者直接广播发送
{

}
}
}
注意

由于HttpSocketClient的生命周期是由框架控制的,所以最好尽量不要直接引用该实例,可以引用HttpSocketClient.Id,然后再通过服务器查找。

(2)通过Id获取

先调用service.GetIds方法,获取当前在线的所有客户端的Id,然后选择需要的Id,通过TryGetSocketClient方法,获取到想要的客户端。

string[] ids = service.GetIds();
if (service.TryGetSocketClient(ids[0], out HttpSocketClient socketClient))
{
}

6.2 发送文本类消息

socketClient.WebSocket.Send("Text");

6.3 发送二进制消息

socketClient.WebSocket.Send(new byte[10]);

6.4 直接发送自定义构建的数据帧

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.WebSocket.Send(frame);
备注

此部分功能就需要你对WebSocket有充分了解才可以操作。

七、服务器广播发送

//广播给所有人
foreach (var item in service.GetClients())
{
if (item.Protocol== Protocol.WebSocket)
{
item.WebSocket.Send("广播");
}
}
提示

在发送时,还可以自己过滤Id。

本文示例Demo