---
id: websocketservice
title: 创建WebSocket服务器
---

### 定义

命名空间：TouchSocket.Http.WebSockets <br/>
程序集：[TouchSocket.Http.dll](https://www.nuget.org/packages/TouchSocket.Http)

## 一、说明

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


## 二、可配置项

继承[HttpService](./httpservice.mdx) 


## 三、支持插件接口

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

## 四、创建WebSocket服务

### 4.1 简单直接创建

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

```csharp showLineNumbers
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 验证连接

可以对连接的`Url`、`Query`、`Header`等参数进行验证，然后决定是否执行`WebSocket`连接。

```csharp showLineNumbers
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("服务器已启动");
```

```csharp showLineNumbers
/// <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`

```csharp showLineNumbers
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("服务器已启动");
```

```csharp showLineNumbers
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`的连接。

```csharp showLineNumbers
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”以供测试。使用配置非常方便。

```csharp showLineNumbers
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 插件接收消息

【定义插件】

```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.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();
    }
}
```

【使用】

```csharp {12}
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();
```

:::tip 提示

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

:::  

### 5.2 WebSocket显式ReadAsync

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

```csharp showLineNumbers
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();
    }
}
```

【使用】

```csharp {16}
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;
}
```

:::info 信息

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

:::  

:::caution 注意

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

::: 

## 六、回复、响应数据

要回复`Websocket`消息，必须使用**HttpSocketClient**对象。

### 6.1 如何获取SocketClient？

#### （1）直接获取所有在线客户端

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

```csharp showLineNumbers
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 showLineNumbers
string[] ids = service.GetIds();
if (service.TryGetSocketClient(ids[0], out HttpSocketClient socketClient))
{
}
```

### 6.2 发送文本类消息

```csharp showLineNumbers
socketClient.WebSocket.Send("Text");
```

### 6.3 发送二进制消息

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


### 6.4 直接发送自定义构建的数据帧
```csharp showLineNumbers
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);
```


:::info 备注

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

:::  


## 七、服务器广播发送

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

:::tip 提示

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

:::  

[本文示例Demo](https://gitee.com/RRQM_Home/TouchSocket/tree/master/examples/WebSocket/WebSocketConsoleApp)