345 Star 2.7K Fork 808

GVP若汝棋茗/TouchSocket

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
克隆/下载
websocketclient.mdx 19.05 KB
一键复制 编辑 原始数据 按行查看 历史
若汝棋茗 提交于 2个月前 . 文档:存档v3.0文档
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665
---
id: websocketclient
title: 创建WebSocket客户端
---
import Tag from "@site/src/components/Tag.js";
import CardLink from "@site/src/components/CardLink.js";
### 定义
命名空间:TouchSocket.Http.WebSockets <br/>
程序集:[TouchSocket.Http.dll](https://www.nuget.org/packages/TouchSocket.Http)
## 一、可配置项
继承[HttpClient](./httpclient.mdx)
## 二、支持插件接口
| 插件方法| 功能 |
| --- | --- |
| IWebSocketHandshakingPlugin | 当收到握手请求之前,可以进行连接验证等 |
| IWebSocketHandshakedPlugin | 当成功握手响应之后 |
| IWebSocketReceivedPlugin | 当收到Websocket的数据报文 |
| IWebSocketClosingPlugin | 当收到关闭请求时,如果对方直接断开连接,此方法则不会触发。 |
| IWebSocketClosedPlugin | 当WebSocket连接断开时触发,无论是否正常断开。但如果是断网等操作,可能不会立即执行,需要结合心跳操作和`UseCheckClear`插件来进行清理。 |
## 三、创建客户端
### 3.1 创建常规客户端
```csharp showLineNumbers
var client = new WebSocketClient();
await client.SetupAsync(new TouchSocketConfig()
.SetRemoteIPHost("ws://127.0.0.1:7789/ws")
.ConfigureContainer(a =>
{
a.AddConsoleLogger();
}));
await client.ConnectAsync();
client.Logger.Info("连接成功");
```
### 3.2 创建WSs客户端
**当需要连接到由证书机构颁发的网址(例如:小程序、物联网等)时,仅需要设置带有`wss`的url即可。**
```csharp showLineNumbers
wss://127.0.0.1:7789/ws
```
**当连接自定义证书的Ssl:wss://127.0.0.1:7789/ws**
```csharp showLineNumbers
var client = new WebSocketClient();
await client.SetupAsync(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; }
}));
await client.ConnectAsync();
Console.WriteLine("连接成功");
```
:::caution 注意
当使用域名连接时,`TargetHost`为域名,例如连接到`IPHost("wss://baidu.com")`时,`TargetHost`应当填写:`baidu.com`
:::
## 四、连接服务器
`WebSocketClient`可以使用默认配置直接连接到服务器,同时也支持使用多种方法定义连接。
### 4.1 直接连接
使用`url`直接建立连接,这一般是服务器也只是普通的`ws`服务器的情况下。
```csharp showLineNumbers
var client = new WebSocketClient();
await client.SetupAsync(new TouchSocketConfig()
.ConfigureContainer(a =>
{
a.AddConsoleLogger();
})
.SetRemoteIPHost("ws://127.0.0.1:7789/ws"));
await client.ConnectAsync();
client.Logger.Info("通过ws://127.0.0.1:7789/ws连接成功");
```
### 4.2 带Query参数连接
带`Query`参数连接,实际上还是通过`url`直接连接。
```csharp showLineNumbers
var client = new WebSocketClient();
await client.SetupAsync(new TouchSocketConfig()
.ConfigureContainer(a =>
{
a.AddConsoleLogger();
})
.SetRemoteIPHost("ws://127.0.0.1:7789/wsquery?token=123456"));
await client.ConnectAsync();
client.Logger.Info("通过ws://127.0.0.1:7789/wsquery?token=123456连接成功");
```
### 4.3 使用特定Header连接
一般的,当某些服务器安全级别较高时,可能会定制特定的`header`用于验证连接。
```csharp showLineNumbers
var client = new WebSocketClient();
await client.SetupAsync(new TouchSocketConfig()
.ConfigureContainer(a =>
{
a.AddConsoleLogger();
})
.ConfigurePlugins(a =>
{
a.Add(typeof(IWebSocketHandshakingPlugin), async (IWebSocket client, HttpContextEventArgs e) =>
{
e.Context.Request.Headers.Add("token", "123456");
await e.InvokeNext();
});
})
.SetRemoteIPHost("ws://127.0.0.1:7789/wsheader"));
await client.ConnectAsync();
client.Logger.Info("通过ws://127.0.0.1:7789/wsheader连接成功");
```
:::tip 提示
实际上`OnWebSocketHandshaking`就是插件委托,也可以自己封装到插件使用。
:::
### 4.4 使用Post方式连接
`WebSocket`默认情况下是基于`Get`方式连接的,但是在一些更特殊的情况下,需要以`Post`,甚至其他方式连接,那么可以使用以下方式实现。
```csharp showLineNumbers
using var client = new WebSocketClient();
await client.SetupAsync(new TouchSocketConfig()
.ConfigureContainer(a =>
{
a.AddConsoleLogger();
})
.ConfigurePlugins(a =>
{
a.Add(typeof(IWebSocketHandshakingPlugin), async (IWebSocket client, HttpContextEventArgs e) =>
{
e.Context.Request.Method = HttpMethod.Post;//将请求方法改为Post
await e.InvokeNext();
});
})
.SetRemoteIPHost("ws://127.0.0.1:7789/postws"));
await client.ConnectAsync();
client.Logger.Info("通过ws://127.0.0.1:7789/postws连接成功");
```
:::tip 提示
使用此方式时,基本上就能完全定制请求连接了。比如一些`Cookie`等。
:::
## 五、发送数据
客户端定义了一些发送方法,方便开发者快速发送数据。
### 5.1 发送文本类消息
```csharp showLineNumbers
await client.SendAsync("Text");
```
### 5.2 发送二进制消息
```csharp showLineNumbers
await client.SendAsync(new byte[10]);
```
### 5.3 直接发送自定义构建的数据帧
```csharp showLineNumbers
using (var 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");
await client.SendAsync(frame);
}
```
:::info 备注
此部分功能就需要你对`WebSocket`有充分了解才可以操作。
:::
## 六、接收数据
### 6.1 订阅Received事件实现
```csharp showLineNumbers
client.Received = async (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;
}
await e.InvokeNext();
};
```
### 6.2 使用插件实现 <Tag>推荐</Tag>
【定义插件】
```csharp showLineNumbers
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.SendAsync("我已收到");
}
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();
}
}
```
【使用】
```csharp {10}
var client = new WebSocketClient();
await client.SetupAsync(new TouchSocketConfig()
.SetRemoteIPHost("ws://127.0.0.1:7789/ws")
.ConfigureContainer(a =>
{
a.AddConsoleLogger();
})
.ConfigurePlugins(a =>
{
a.Add<MyWebSocketPlugin>();
}));
await client.ConnectAsync();
```
### 6.3 使用WebSocket显式ReadAsync
```csharp showLineNumbers
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()}");
}
}
}
}
}
```
:::info 信息
`ReadAsync`的方式是属于**同步不阻塞**的接收方式。他不会单独占用线程,只会阻塞当前`Task`。所以可以大量使用,不需要考虑性能问题。同时,`ReadAsync`的好处就是单线程访问上下文,这样在处理ws分包时是非常方便的。
:::
:::caution 注意
使用该方式,会阻塞`IWebSocketHandshakedPlugin`的插件传递。在收到`WebSocket`消息的时候,不会再触发插件。
:::
### 6.4 接收中继数据
`WebSocket`协议本身是支持超大数据包的,但是这些包不会一次性接收,而是分多次接收的,同时会通过`Opcode`来表明其为中继数据。
下面将演示接收文本数据。
【方法1】
```csharp {22-49} showLineNumbers
//当WebSocket想要使用ReadAsync时,需要设置此值为true
client.AllowAsyncRead = true;
//此处即表明websocket已连接
MemoryStream stream = default;//中继包缓存
var isText = false;//标识是否为文本
while (true)
{
using (var receiveResult = await client.ReadAsync(CancellationToken.None))
{
if (receiveResult.IsCompleted)
{
break;
}
var dataFrame = receiveResult.DataFrame;
var data = receiveResult.DataFrame.PayloadData;
switch (dataFrame.Opcode)
{
case WSDataType.Cont:
{
//收到的是中继包
if (dataFrame.FIN)//判断是否为最终包
{
//是
if (isText)//判断是否为文本
{
this.m_logger.Info($"WebSocket文本:{Encoding.UTF8.GetString(stream.ToArray())}");
}
else
{
this.m_logger.Info($"WebSocket二进制:{stream.Length}长度");
}
}
else
{
//否,继续缓存
//如果是非net6.0即以上,即:NetFramework平台使用。原因是stream不支持span写入
//var segment = data.AsSegment();
//stream.Write(segment.Array, segment.Offset, segment.Count);
//如果是net6.0以上,直接写入span即可
stream.Write(data.Span);
}
}
break;
case WSDataType.Text:
{
if (dataFrame.FIN)//判断是不是最后的包
{
//是,则直接输出
//说明上次并没有中继数据缓存,直接输出本次内容即可
this.m_logger.Info($"WebSocket文本:{dataFrame.ToText()}");
}
else
{
isText = true;
//否,则说明数据太大了,分中继包了。
//则,初始化缓存容器
stream ??= new MemoryStream();
//下面则是缓存逻辑
//如果是非net6.0即以上,即:NetFramework平台使用。原因是stream不支持span写入
//var segment = data.AsSegment();
//stream.Write(segment.Array, segment.Offset, segment.Count);
//如果是net6.0以上,直接写入span即可
stream.Write(data.Span);
}
}
break;
case WSDataType.Binary:
{
if (dataFrame.FIN)//判断是不是最后的包
{
//是,则直接输出
//说明上次并没有中继数据缓存,直接输出本次内容即可
this.m_logger.Info($"WebSocket二进制:{data.Length}长度");
}
else
{
isText = false;
//否,则说明数据太大了,分中继包了。
//则,初始化缓存容器
stream ??= new MemoryStream();
//下面则是缓存逻辑
//如果是非net6.0即以上,即:NetFramework平台使用。原因是stream不支持span写入
//var segment = data.AsSegment();
//stream.Write(segment.Array, segment.Offset, segment.Count);
//如果是net6.0以上,直接写入span即可
stream.Write(data.Span);
}
}
break;
case WSDataType.Close:
break;
case WSDataType.Ping:
break;
case WSDataType.Pong:
break;
default:
break;
}
}
}
```
或者可以使用内置的扩展方法来进行一次性接收。
例如一次性接收文本:
```csharp showLineNumbers
var str = await client.ReadStringAsync();
```
一次性接收二进制数据:
```csharp showLineNumbers
using (MemoryStream stream=new MemoryStream())
{
var str = await client.ReadBinaryAsync(stream);
}
```
:::caution 注意
`ReadStringAsync`或者`ReadBinaryAsync`,都只接收对应的数据类型,如果收到非匹配数据则会抛出异常。
:::
【方法2】
使用消息组合器。如果想在`OnWebSocketReceived`进行合并消息,则可以使用该方法。
```csharp {30-74} showLineNumbers
public class MyWebSocketPlugin : PluginBase, IWebSocketReceivedPlugin
{
public MyWebSocketPlugin(ILog logger)
{
this.m_logger = logger;
}
private readonly ILog m_logger;
public async Task OnWebSocketReceived(IWebSocket client, WSDataFrameEventArgs e)
{
switch (e.DataFrame.Opcode)
{
case WSDataType.Close:
{
this.m_logger.Info("远程请求断开");
await client.CloseAsync("断开");
}
return;
case WSDataType.Ping:
this.m_logger.Info("Ping");
await client.PongAsync();//收到ping时,一般需要响应pong
break;
case WSDataType.Pong:
this.m_logger.Info("Pong");
break;
default:
{
//其他报文,需要考虑中继包的情况。所以需要手动合并 WSDataType.Cont类型的包。
//或者使用消息合并器
//获取消息组合器
var messageCombinator = client.GetMessageCombinator();
try
{
//尝试组合
if (messageCombinator.TryCombine(e.DataFrame, out var webSocketMessage))
{
//组合成功,必须using释放模式
using (webSocketMessage)
{
//合并后的消息
var dataType = webSocketMessage.Opcode;
//合并后的完整消息
var data = webSocketMessage.PayloadData;
if (dataType == WSDataType.Text)
{
//按文本处理
}
else if (dataType == WSDataType.Binary)
{
//按字节处理
}
else
{
//可能是其他自定义协议
}
}
}
}
catch (Exception ex)
{
this.m_logger.Exception(ex);
messageCombinator.Clear();//当组合发生异常时,应该清空组合器数据
}
}
break;
}
await e.InvokeNext();
}
}
```
:::caution 注意
使用消息组合器,实际上是由框架缓存了数据,所以,整体数据不要太大,不然可能会有爆内存的风险。
:::
## 七、其他操作
### 7.1 握手机制
`WebSocket`拥有独立的握手机制,直接获取`Online`属性即可。
### 7.2 Ping机制
`WebSocket`有自己的`Ping`、`Pong`机制。所以直接调用已有方法即可。
```csharp showLineNumbers
await client.PingAsync();
await client.PongAsync();
```
:::tip 建议
`WebSocket`是双向通讯,所以支持客户端和服务器双向操作`Ping`和`Pong`报文。但是一般来说都是客户端执行`Ping`,服务器回应`Pong`。
:::
### 7.3 断线重连
`WebSocket`断线重连,可以直接使用插件。
```csharp showLineNumbers
.ConfigurePlugins(a =>
{
a.UseWebSocketReconnection();
})
```
## 八、关闭连接
在使用`WebSocket`时,如果想主动关闭连接,可以使用`CloseAsync`方法,同时可以携带一个关闭原因。
默认关闭状态码为1000。意为:正常关闭。
```csharp showLineNumbers
await webSocket.CloseAsync("关闭");
```
如果你想使用其他状态码,可以参考如下代码。
```csharp showLineNumbers
await webSocket.CloseAsync(WebSocketCloseStatus.EndpointUnavailable,"关闭");//状态码为1001,意为:服务端不可用。
```
## 九、本文示例Demo
<CardLink link="https://gitee.com/RRQM_Home/TouchSocket/tree/master/examples/WebSocket/WebSocketConsoleApp"/>
Loading...
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
C#
1
https://gitee.com/RRQM_Home/TouchSocket.git
git@gitee.com:RRQM_Home/TouchSocket.git
RRQM_Home
TouchSocket
TouchSocket
master

搜索帮助