跳到主要内容
版本:2.0

创建WebSocket客户端

定义

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

一、可配置项

继承HttpClient

二、支持插件接口

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

三、创建客户端

3.1 创建常规客户端

var client = new WebSocketClient();
client.Setup(new TouchSocketConfig()
.SetRemoteIPHost("ws://127.0.0.1:7789/ws")
.ConfigureContainer(a =>
{
a.AddConsoleLogger();
}));
client.Connect();
client.Logger.Info("连接成功");

3.2 创建WSs客户端

当需要连接到由证书机构颁发的网址(例如:小程序、物联网等)时,仅需要设置带有wss的url即可。

wss://127.0.0.1:7789/ws

当连接自定义证书的Ssl:wss://127.0.0.1:7789/ws

var client = new WebSocketClient();

client.Setup(new TouchSocketConfig()
.SetRemoteIPHost(new IPHost("wss://127.0.0.1:7789/ws"))
.SetClientSslOption(
new ClientSslOption()
{
ClientCertificates = new X509CertificateCollection() { new X509Certificate2("RRQMSocket.pfx", "RRQMSocket") },
SslProtocols = SslProtocols.Tls12,
TargetHost = "127.0.0.1",
CertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => { return true; }
}));

client.Connect();

Console.WriteLine("连接成功");
注意

当使用域名连接时,TargetHost为域名,例如连接到IPHost("wss://baidu.com")时,TargetHost应当填写:baidu.com

四、连接服务器

WebSocketClient可以使用默认配置直接连接到服务器,同时也支持使用多种方法定义连接。

4.1 直接连接

使用url直接建立连接,这一般是服务器也只是普通的ws服务器的情况下。

var client = new WebSocketClient();
client.Setup(new TouchSocketConfig()
.ConfigureContainer(a =>
{
a.AddConsoleLogger();
})
.SetRemoteIPHost("ws://127.0.0.1:7789/ws"));
client.Connect();

client.Logger.Info("通过ws://127.0.0.1:7789/ws连接成功");

4.2 带Query参数连接

带Query参数连接,实际上还是通过url直接连接。

var client = new WebSocketClient();
client.Setup(new TouchSocketConfig()
.ConfigureContainer(a =>
{
a.AddConsoleLogger();
})
.SetRemoteIPHost("ws://127.0.0.1:7789/wsquery?token=123456"));
client.Connect();

client.Logger.Info("通过ws://127.0.0.1:7789/wsquery?token=123456连接成功");

4.3 使用特定Header连接

一般的,当某些服务器安全级别较高时,可能会定制特定的header用于验证连接。

var client = new WebSocketClient();
client.Setup(new TouchSocketConfig()
.ConfigureContainer(a =>
{
a.AddConsoleLogger();
})
.ConfigurePlugins(a =>
{
a.Add(nameof(IWebSocketHandshakingPlugin.OnWebSocketHandshaking), async (IWebSocket client, HttpContextEventArgs e) =>
{
e.Context.Request.Headers.Add("token", "123456");
await e.InvokeNext();
});
})
.SetRemoteIPHost("ws://127.0.0.1:7789/wsheader"));
client.Connect();

client.Logger.Info("通过ws://127.0.0.1:7789/wsheader连接成功");
提示

实际上OnWebSocketHandshaking就是插件委托,也可以自己封装到插件使用。

4.4 使用Post方式连接

WebSocket默认情况下是基于GET方式连接的,但是在一些更特殊的情况下,需要以POST,甚至其他方式连接,那么可以使用以下方式实现。

using var client = new WebSocketClient();
client.Setup(new TouchSocketConfig()
.ConfigureContainer(a =>
{
a.AddConsoleLogger();
})
.ConfigurePlugins(a =>
{
a.Add(nameof(IWebSocketHandshakingPlugin.OnWebSocketHandshaking), async (IWebSocket client, HttpContextEventArgs e) =>
{
e.Context.Request.Method = HttpMethod.Post;//将请求方法改为Post
await e.InvokeNext();
});
})
.SetRemoteIPHost("ws://127.0.0.1:7789/postws"));
client.Connect();

client.Logger.Info("通过ws://127.0.0.1:7789/postws连接成功");
提示

使用此方式时,基本上就能完全定制请求连接了。比如一些Cookie等。

五、发送数据

因为客户端是从HttpClientBase派生,则可以直接使用扩展方法,进行发送。

5.1 发送文本类消息

client.Send("Text");

5.2 发送二进制消息

client.Send(new byte[10]);

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

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");

client.Send(frame);
备注

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

六、接收数据

6.1 订阅Received事件实现

client.Received = (c, e) =>
{
switch (e.DataFrame.Opcode)
{
case WSDataType.Cont:
break;
case WSDataType.Text:
break;
case WSDataType.Binary:
break;
case WSDataType.Close:
break;
case WSDataType.Ping:
break;
case WSDataType.Pong:
break;
default:
break;
}
};

6.2 使用插件实现

【定义插件】

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 client = new WebSocketClient();
client.Setup(new TouchSocketConfig()
.SetRemoteIPHost("ws://127.0.0.1:7789/ws")
.ConfigureContainer(a =>
{
a.AddConsoleLogger();
})
.ConfigurePlugins(a =>
{
a.Add<MyWebSocketPlugin>();
}));
client.Connect();

6.3 使用WebSocket显式ReadAsync

using (var client = GetClient())
{
//当WebSocket想要使用ReadAsync时,需要设置此值为true
client.AllowAsyncRead = true;

while (true)
{
using (var receiveResult = await client.ReadAsync(CancellationToken.None))
{
if (receiveResult.IsClosed)
{
//断开连接了
break;
}

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

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

注意

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

七、其他操作

7.1 握手机制

WebSocket拥有独立的握手机制,直接获取IsHandshaked属性即可。

7.2 Ping机制

WebSocket有自己的PingPong机制。所以直接调用已有方法即可。

client.Ping();
client.Pong();
建议

WebSocket是双向通讯,所以支持客户端和服务器双向操作PingPong报文。但是一般来说都是客户端执行Ping,服务器回应Pong

7.3 断线重连

WebSocket断线重连,可以直接使用Tcp断线重连插件。

.ConfigurePlugins(a =>
{
a.UseReconnection();
})

八、关闭连接

关闭Websocket,应该发送关闭报文。

myWSClient.Close("close");

本文示例Demo