---
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"/>
