C#实现WebSocket全双工通信实战项目
命名空间中的核心组件围绕一个抽象基类展开,形成清晰的职责划分。理解这些类之间的关系是设计可扩展、高可用 WebSocket 架构的前提。尽管不直接暴露,但其底层依赖实现 HTTP 升级请求。因此可通过反射或子类化方式注入自定义行为(受限于当前 .NET 版本)。不过,从 .NET 6 开始,可通过的扩展点间接影响底层行为。某些 WebSocket 服务要求在握手阶段传递认证信息(如 JWT Tok
简介:WebSocket是一种基于TCP的协议,通过HTTP升级实现客户端与服务器之间的持久化、全双工通信,显著提升实时数据交互效率。在C#中,借助.NET Framework或.NET Core提供的System.Net.WebSockets命名空间,可高效实现WebSocket客户端与服务器端的异步通信。本文介绍WebSocket协议原理,详细讲解ClientWebSocket与WebSocket类的使用,涵盖连接建立、数据收发、连接关闭等核心流程,并提供完整示例代码。结合Mywebsocket项目分析,帮助开发者掌握C#环境下WebSocket的实际应用,适用于在线聊天、实时游戏、金融行情推送等高实时性场景。
1. WebSocket协议原理与HTTP升级机制
协议设计思想与全双工通信模型
WebSocket协议通过单一TCP连接实现客户端与服务器之间的全双工通信,克服了HTTP半双工模式的固有延迟。其核心在于利用HTTP协议完成初始握手后,通过 Upgrade: websocket 头字段请求协议升级,服务器返回 101 Switching Protocols 状态码确认切换,此后通信不再受限于请求-响应模式。
握手过程与帧结构解析
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
客户端发送含特殊头字段的HTTP请求,服务器验证后响应101状态码完成升级。随后的数据传输以帧(Frame)为单位,帧结构包含 Opcode (定义数据类型)、 Mask (客户端到服务端必须掩码)、 Payload Length 等字段,确保安全与完整性。
技术对比与应用场景优势
| 技术方式 | 延迟 | 连接开销 | 实时性 | 适用场景 |
|---|---|---|---|---|
| 长轮询 | 高 | 高 | 中 | 兼容旧系统 |
| Server-Sent Events | 中 | 中 | 中 | 单向推送(如通知) |
| WebSocket | 低 | 低 | 高 | 聊天、游戏、实时交易等 |
相比长轮询频繁建立连接,WebSocket在一次握手后保持持久通道,显著降低延迟与服务器负载,成为高并发实时系统的首选方案。理解该机制是后续C#中使用 ClientWebSocket 和 HttpListenerWebSocket 构建稳定通信的基础。
2. System.Net.WebSockets命名空间详解
System.Net.WebSockets 是 .NET Framework 4.5 及更高版本中引入的核心命名空间,专为实现 WebSocket 协议而设计。它提供了完整的客户端与服务器端通信能力,允许开发者在 C# 应用程序中构建高性能、低延迟的双向实时通信系统。该命名空间屏蔽了底层 TCP 连接和 HTTP 升级细节,封装出统一的异步编程模型,使开发者能够以标准化方式处理连接建立、数据传输与状态管理。
本章将深入剖析 System.Net.WebSockets 中的关键类型及其协作机制,涵盖从类继承结构到状态机行为、协议兼容性等多个维度,帮助具备五年以上开发经验的技术人员掌握其内部运行逻辑,并为后续构建企业级 WebSocket 解决方案打下坚实基础。
2.1 核心类与接口概述
System.Net.WebSockets 命名空间中的核心组件围绕一个抽象基类展开,形成清晰的职责划分。理解这些类之间的关系是设计可扩展、高可用 WebSocket 架构的前提。
2.1.1 WebSocket抽象类的继承体系
WebSocket 类是一个抽象类(abstract class),位于整个命名空间的顶层,定义了所有 WebSocket 实现必须遵循的基本方法和属性。它是客户端与服务端共用的统一接口,确保无论使用何种具体实现,代码层面都能保持一致性。
public abstract class WebSocket : IDisposable
{
public abstract WebSocketState State { get; }
public abstract string SubProtocol { get; }
public abstract Task CloseAsync(WebSocketCloseStatus closeStatus,
string statusDescription, CancellationToken token);
public abstract Task<WebSocketReceiveResult> ReceiveAsync(
ArraySegment<byte> buffer, CancellationToken token);
public abstract Task SendAsync(ArraySegment<byte> buffer,
WebSocketMessageType messageType, bool endOfMessage, CancellationToken token);
}
上述代码展示了 WebSocket 抽象类的主要成员:
- State : 表示当前连接的状态(如打开、关闭等)。
- SubProtocol : 返回握手成功后协商选定的子协议名称。
- CloseAsync() : 异步发送关闭帧并终止连接。
- ReceiveAsync() / SendAsync() : 核心的数据收发方法,支持文本、二进制和控制帧。
继承结构图(Mermaid)
classDiagram
class WebSocket {
<<abstract>>
+WebSocketState State
+string SubProtocol
+Task CloseAsync()
+Task~WebSocketReceiveResult~ ReceiveAsync()
+Task SendAsync()
}
class ClientWebSocket {
+Task ConnectAsync(Uri, CancellationToken)
+ClientWebSocketOptions Options
}
class HttpListenerWebSocket {
// 内部由 AcceptWebSocketAsync 创建
}
WebSocket <|-- ClientWebSocket
WebSocket <|-- HttpListenerWebSocket
note right of WebSocket
所有实现都继承自 WebSocket 抽象类,
提供统一的 API 接口用于异步通信。
end note
此图清晰地表明: ClientWebSocket 和 HttpListenerWebSocket 分别代表客户端和服务端的具体实现,均继承自 WebSocket 抽象类。这种设计模式体现了面向接口编程的思想,使得高层逻辑可以依赖于抽象而非具体实现,极大增强了系统的可测试性和可替换性。
例如,在单元测试中,可以通过模拟 WebSocket 接口来验证消息处理逻辑,而无需真正发起网络请求。
此外, WebSocket 类实现了 IDisposable 接口,意味着使用者必须显式调用 Dispose() 或使用 using 语句块释放非托管资源(如套接字句柄)。未正确释放可能导致连接泄漏或端口耗尽问题,尤其在高并发场景下影响显著。
2.1.2 ClientWebSocket与HttpListenerWebSocket的差异分析
尽管两者都继承自 WebSocket ,但它们的应用场景和生命周期管理存在本质区别。
| 特性 | ClientWebSocket |
HttpListenerWebSocket |
|---|---|---|
| 使用位置 | 客户端应用(主动发起连接) | 服务端应用(被动接受升级) |
| 创建方式 | new ClientWebSocket() | 通过 HttpContext.AcceptWebSocketAsync() 获取 |
| 配置选项 | 支持设置缓冲区、超时、头信息等 | 不可配置,由 HTTP 上下文决定 |
| 生命周期控制 | 调用 ConnectAsync() 启动握手 |
在收到合法 Upgrade 请求后自动创建 |
| 子协议选择 | 客户端提出建议,等待服务端确认 | 服务端可在回调中选择接受的子协议 |
示例代码:ClientWebSocket 初始化
var client = new ClientWebSocket();
client.Options.SetRequestHeader("Authorization", "Bearer xyz");
client.Options.KeepAliveInterval = TimeSpan.FromSeconds(30);
await client.ConnectAsync(new Uri("wss://example.com/ws"), cancellationToken);
逐行解析:
new ClientWebSocket()—— 实例化客户端对象,此时尚未建立任何连接。SetRequestHeader()—— 设置自定义 HTTP 头,常用于身份认证或跟踪标识。KeepAliveInterval—— 设置心跳间隔,防止 NAT 超时断开连接。ConnectAsync()—— 发起异步握手请求,触发 HTTP 升级流程。
值得注意的是, ClientWebSocketOptions 提供了丰富的配置项,包括代理设置、证书验证回调、自动解压缩等高级功能,适合复杂网络环境下的定制需求。
相比之下, HttpListenerWebSocket 的创建完全依赖于服务端监听逻辑:
if (context.Request.IsWebSocketRequest)
{
var webSocketContext = await context.AcceptWebSocketAsync(subProtocol: "chat-v1");
var socket = webSocketContext.WebSocket;
// 开始处理消息循环
}
此处 AcceptWebSocketAsync() 方法会自动完成协议升级响应,并返回封装好的 WebSocket 实例。开发者无法直接实例化该类,也不能修改其配置参数,体现了“被动接收”的特性。
这一对比揭示了一个重要原则: 客户端主导连接发起与配置,服务端主导协议合规性检查与资源分配 。
2.1.3 WebSocketContext与WebSocketReceiveResult的作用解析
除了主通信类外, System.Net.WebSockets 还定义了两个关键辅助类型: WebSocketContext 和 WebSocketReceiveResult ,分别用于上下文传递和接收结果描述。
WebSocketContext 结构说明
WebSocketContext 是 HttpListenerContext.AcceptWebSocketAsync() 返回的结果容器,包含以下主要属性:
| 属性 | 类型 | 说明 |
|---|---|---|
| WebSocket | WebSocket | 已升级的 WebSocket 连接实例 |
| RequestUri | Uri | 原始 HTTP 请求地址 |
| Headers | NameValueCollection | 客户端发送的所有 HTTP 头 |
| Origin | string | 来源域(Origin header) |
| User | IPrincipal | 经过身份验证的用户信息(需配合认证中间件) |
这使得服务端可以在处理 WebSocket 消息前,先对原始请求进行安全校验。例如:
if (!IsValidOrigin(context.Headers["Origin"]))
{
context.Response.StatusCode = 403;
context.Response.Close();
return;
}
WebSocketReceiveResult 类型详解
每次调用 ReceiveAsync() 后,返回的是 WebSocketReceiveResult 对象,其结构如下:
public class WebSocketReceiveResult
{
public int Count { get; } // 实际接收到的字节数
public WebSocketMessageType MessageType { get; } // 消息类型(Text/Binary/Close)
public bool EndOfMessage { get; } // 是否为完整消息的最后一帧
public long? PayloadLength { get; } // 原始有效载荷长度(仅首帧)
}
该对象对于处理分片消息至关重要。当一条消息被拆分为多个帧传输时,只有第一个帧携带完整的 PayloadLength ,其余帧的 Count 累加构成完整数据。
实际应用场景代码示例:
var buffer = new byte[1024];
var receivedResults = new List<ArraySegment<byte>>();
while (true)
{
WebSocketReceiveResult result;
do
{
result = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), token);
if (result.MessageType == WebSocketMessageType.Close)
{
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", token);
return;
}
var currentSegment = new ArraySegment<byte>(buffer, 0, result.Count);
receivedResults.Add(currentSegment);
} while (!result.EndOfMessage);
// 所有帧接收完毕,合并为完整消息
var fullMessage = MergeSegments(receivedResults);
ProcessMessage(fullMessage);
receivedResults.Clear();
}
逻辑分析:
- 循环读取直到
EndOfMessage == true,表示当前消息已完整接收。 - 若中途收到
Close帧,则应立即响应并关闭连接。 - 使用
MergeSegments合并所有片段,还原原始大数据包。
这种机制支持任意大小的消息传输,突破单帧限制(最大约 2^63 字节),适用于文件流、音视频推送等场景。
综上所述, WebSocketContext 和 WebSocketReceiveResult 共同构成了上下文感知与精细化控制的基础,是实现健壮通信逻辑不可或缺的部分。
2.2 WebSocket状态机模型
WebSocket 并非简单的“连接即通”,而是遵循严格的状态变迁规则。 System.Net.WebSockets 将 RFC6455 规范中的状态机映射为 WebSocketState 枚举,确保每一步操作都在合法状态下执行。
2.2.1 WebSocketState枚举值的含义及转换逻辑
WebSocketState 是一个枚举类型,定义了连接可能处于的五种状态:
| 枚举值 | 描述 |
|---|---|
None |
初始状态,尚未开始连接 |
Connecting |
正在执行握手过程 |
Open |
连接已建立,可自由收发数据 |
CloseSent |
本地已发送 Close 帧,等待对方回应 |
CloseReceived |
收到对方 Close 帧,等待本地关闭 |
Closed |
连接完全关闭 |
Aborted |
因异常强制中断 |
状态转换流程图(Mermaid)
stateDiagram-v2
[*] --> None
None --> Connecting : ConnectAsync()
Connecting --> Open : 握手成功
Open --> CloseSent : CloseAsync()
Open --> CloseReceived : 收到Close帧
CloseSent --> Closed : 收到Close响应
CloseReceived --> Closed : 发送Close响应
Open --> Aborted : 异常中断
CloseSent --> Aborted : 异常中断
CloseReceived --> Aborted : 异常中断
Aborted --> [*]
Closed --> [*]
note right of Open
数据收发仅在此状态允许
end note
该图展示了标准的状态跃迁路径。任何非法跳转(如从 Connecting 直接到 Closed )都会引发 InvalidOperationException 。
例如,尝试在 Connecting 状态下调用 SendAsync() 将抛出异常:
// ❌ 错误示范
await client.ConnectAsync(uri, ct); // 此处仍在等待握手完成
await client.SendAsync(data, Binary, true, ct); // 可能失败!
正确的做法是确保状态进入 Open 后再进行通信:
await client.ConnectAsync(uri, ct);
if (client.State == WebSocketState.Open)
{
await client.SendAsync(data, Binary, true, ct);
}
2.2.2 连接建立、运行、关闭各阶段的状态变迁
连接的完整生命周期可分为三个阶段:
1. 建立阶段(None → Connecting → Open)
- 调用
ConnectAsync()或AcceptWebSocketAsync()触发状态变为Connecting。 - 成功完成 HTTP 升级后,状态切换至
Open。 - 若握手失败(如 401 未授权),则直接跳转至
Closed。
2. 运行阶段(Open)
- 此阶段是唯一允许调用
SendAsync和ReceiveAsync的状态。 - 可随时发起关闭流程(调用
CloseAsync),进入CloseSent。 - 若收到对方的
Close帧,则进入CloseReceived。
3. 关闭阶段(CloseSent/CloseReceived → Closed)
WebSocket 要求双方交换关闭帧以实现优雅关闭:
// 主动关闭方
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "bye", ct);
// 等待对方回执
await socket.ReceiveAsync(buffer, ct); // 应收到对方 Close 帧
若一方不响应关闭帧, .NET 默认会在一段时间后强制关闭(取决于平台实现)。
2.2.3 异常状态下状态迁移的处理策略
当发生网络故障、协议错误或内部异常时,状态可能直接跳转至 Aborted 。
常见触发条件包括:
- TCP 连接中断
- 掩码错误(客户端未掩码发送)
- 非法帧格式(保留位设置错误)
- 心跳超时
一旦进入 Aborted 状态,所有进一步的操作都将失败。因此,必须结合 try-catch 进行防护:
try
{
await socket.SendAsync(data, Text, true, ct);
}
catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely)
{
// 连接提前关闭,需清理资源
CleanupConnection(socket);
}
推荐的做法是监听状态变化事件(虽然 .NET 未提供原生事件),可通过轮询或包装器实现:
async Task MonitorState(WebSocket socket, CancellationToken ct)
{
while (!ct.IsCancellationRequested && socket.State != WebSocketState.Closed)
{
Console.WriteLine($"Current state: {socket.State}");
await Task.Delay(1000, ct);
}
}
合理的状态监控有助于快速发现连接异常,提升系统可观测性。
2.3 协议版本与扩展支持
2.3.1 .NET对RFC6455标准的兼容性实现
System.Net.WebSockets 完全遵循 RFC6455 标准,支持:
- 标准握手流程(HTTP Upgrade + Sec-WebSocket-Key)
- 掩码机制(客户端→服务端必须掩码)
- 控制帧(Ping/Pong/Close)
- 分片传输(FIN 标志控制)
- 错误码标准化(如 1006 表示异常关闭)
这意味着与其他语言实现(如 Node.js、Python Tornado)可无缝互操作。
唯一例外是某些老旧浏览器或中间件可能要求 Hixie-76 等旧版协议,但 .NET 不支持这些已被废弃的草案。
2.3.2 子协议(SubProtocol)协商机制的应用场景
子协议用于在 WebSocket 连接之上定义更高层的应用语义。例如:
"chat-v1":表示聊天协议 v1 版本"json.rpc":基于 JSON-RPC 的远程调用"graphql-ws":GraphQL 订阅协议
客户端指定子协议:
client.Options.AddSubProtocol("chat-v1");
client.Options.AddSubProtocol("file-transfer-v2");
服务端选择子协议:
var acceptedProtocol = context.Request.Headers["Sec-WebSocket-Protocol"]
.Split(',')
.FirstOrDefault(p => SupportedProtocols.Contains(p.Trim()));
await context.AcceptWebSocketAsync(acceptedProtocol);
若客户端请求的子协议服务端不支持,可返回 null 拒绝升级,或选择默认协议。
子协议的存在提升了协议的灵活性和版本管理能力,避免硬编码消息格式。
2.3.3 扩展字段(Extensions)的预留支持情况
WebSocket 扩展(如 permessage-deflate)可用于压缩消息以减少带宽消耗。然而,截至 .NET 6, System.Net.WebSockets 并未内置任何扩展支持 。
虽然 ClientWebSocketOptions 中有 DangerousDeflateOptions 属性,但它仅用于调试目的,且文档明确标注“不保证稳定性”。
因此,在需要压缩的生产环境中,通常采用以下替代方案:
- 应用层压缩:在发送前手动压缩 payload
- 使用 SignalR(基于 ASP.NET Core),其内置对
permessage-deflate的支持 - 自行实现扩展解析(需深入帧结构处理)
尽管缺乏原生扩展支持,但基础协议的稳定性与跨平台一致性仍使其成为构建可靠系统的首选。
本章通过对 System.Net.WebSockets 命名空间的全面解析,揭示了 .NET 平台下 WebSocket 实现的技术内核。从抽象类的设计哲学到状态机的严谨控制,再到协议级别的兼容保障,每一层都体现了现代异步通信框架的工程智慧。下一章将聚焦于客户端实现,展示如何利用这些组件构建稳定可靠的 WebSocket 客户端应用。
3. ClientWebSocket客户端连接实现
在现代分布式系统与实时通信架构中,客户端主动建立稳定、高效的双向通信通道已成为不可或缺的能力。 ClientWebSocket 作为 .NET Framework 4.5 及 .NET Core/.NET 5+ 中 System.Net.WebSockets 命名空间的核心类之一,为开发者提供了标准的 WebSocket 客户端编程接口。它封装了底层协议握手、帧解析、状态管理等复杂逻辑,使开发者能够以简洁的异步 API 实现与远程 WebSocket 服务端的安全连接和数据交互。
本章将深入剖析 ClientWebSocket 的完整初始化流程、安全连接配置机制以及实际场景下的异步连接实战策略。通过逐层拆解其构造过程、参数设置、异常处理与重连设计,帮助具备五年以上开发经验的工程师构建高可用、低延迟的客户端通信模块,适用于金融行情推送、IoT设备控制、在线协作工具等对实时性要求极高的应用场景。
3.1 客户端初始化与配置
3.1.1 创建ClientWebSocket实例并设置缓冲区大小
ClientWebSocket 是一个密封类(sealed class),继承自抽象基类 WebSocket ,专用于发起客户端侧的 WebSocket 连接请求。创建其实例非常简单,仅需调用默认构造函数即可:
using System;
using System.Net.WebSockets;
var client = new ClientWebSocket();
然而,若不进行合理的初始化配置,可能导致性能瓶颈或内存溢出问题。其中最关键的一项配置是 接收缓冲区大小(Receive Buffer Size) 。该值决定了每次调用 ReceiveAsync 方法时用于暂存网络数据的最大字节数。
缓冲区设置示例代码:
var client = new ClientWebSocket();
// 设置发送和接收缓冲区为 16KB
client.Options.SetBuffer(
receiveBufferSize: 16 * 1024,
sendBufferSize: 16 * 1024);
代码逻辑逐行解读:
- 第1行:实例化
ClientWebSocket对象。- 第4~5行:调用
Options.SetBuffer()方法设定内部缓冲区尺寸。receiveBufferSize: 指定从服务端读取数据时使用的最大内存块大小。sendBufferSize: 控制批量发送数据时的临时存储空间。参数说明:
- 推荐值范围通常为8KB ~ 64KB,具体取决于应用的数据吞吐量。
- 若消息平均较大(如图像流、音频帧),应适当增大缓冲区避免频繁分片。
- 设置过大会增加单个连接的内存占用,在高并发场景下需权衡资源消耗。
此外,缓冲区并非一次性分配全部内存,而是按需使用。因此合理预估消息长度有助于提升 I/O 效率。
不同业务场景下的缓冲区建议配置表:
| 场景类型 | 平均消息大小 | 推荐缓冲区大小 | 说明 |
|---|---|---|---|
| 聊天消息 | < 1KB | 8KB | 小消息高频传输,小缓冲即可 |
| 行情数据推送 | 1~4KB | 16KB | 需支持连续推送,防止阻塞 |
| 文件片段传输 | 10~64KB | 64KB | 大数据包需大缓冲减少分片次数 |
| 视频帧流 | > 100KB | 动态调整 | 建议采用分段接收 + 自定义缓冲池 |
此表格表明,缓冲区配置并非“越大越好”,而应结合业务特征动态优化。
3.1.2 配置KeepAliveInterval与超时参数优化连接稳定性
长时间运行的 WebSocket 客户端必须面对网络波动、NAT超时、防火墙中断等问题。为了维持连接活性,.NET 提供了 KeepAliveInterval 属性来自动发送 Ping 帧探测服务端可达性。
示例:启用心跳保活机制
var client = new ClientWebSocket();
// 设置每30秒发送一次Ping帧
client.Options.KeepAliveInterval = TimeSpan.FromSeconds(30);
// 允许带外数据(Out-of-band data)处理(可选)
client.Options.DontFragment = true; // 禁止分片,提升小消息效率
代码逻辑分析:
KeepAliveInterval设为TimeSpan.FromSeconds(30)表示每隔30秒自动向服务端发送一个 Ping 帧。- 当服务端响应 Pong 时,连接被视为活跃;否则在若干次失败后触发关闭。
DontFragment = true表示尽可能不将消息分片发送,适用于小消息但会限制单帧最大长度。
该机制基于 RFC6455 标准中的 Ping/Pong 控制帧实现,有效防止中间代理设备因无流量而断开连接。
超时相关选项详解:
| 属性名称 | 类型 | 默认值 | 作用说明 |
|---|---|---|---|
KeepAliveInterval |
TimeSpan |
30秒 | 心跳间隔,0表示禁用心跳 |
Proxy |
IWebProxy |
null | 自定义代理服务器 |
Credentials |
ICredentials |
null | HTTP认证凭据 |
DangerousDeflateOptions |
WebSocketDeflateOptions |
null | 启用压缩扩展(实验性) |
值得注意的是, KeepAliveInterval 并非 TCP 层 Keep-Alive,而是应用层的 WebSocket 协议级保活机制,更具语义意义且兼容性更强。
连接稳定性优化流程图(Mermaid)
graph TD
A[启动ClientWebSocket] --> B{是否配置KeepAlive?}
B -- 是 --> C[设置KeepAliveInterval=30s]
B -- 否 --> D[依赖外部心跳机制]
C --> E[连接建立成功]
E --> F[每30秒自动发送Ping]
F --> G{收到Pong?}
G -- 是 --> H[继续正常通信]
G -- 否 --> I[尝试重连或标记失败]
I --> J[执行恢复策略]
该流程清晰展示了心跳机制在整个生命周期中的作用路径。对于需要7×24小时运行的服务(如监控系统),强烈建议开启此功能。
此外,还可以配合 CancellationToken 实现连接超时控制:
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); // 10秒超时
try
{
await client.ConnectAsync(uri, cts.Token);
}
catch (OperationCanceledException) when (!cts.IsCancellationRequested)
{
throw new TimeoutException("WebSocket连接超时");
}
此处利用
CancellationTokenSource构造限时令牌,捕获超时异常并转换为更明确的错误类型,便于上层统一处理。
综上所述,合理的初始化不仅包括对象创建,更涵盖缓冲区、保活、超时等关键参数的精细化配置,直接影响系统的健壮性与用户体验。
3.2 建立安全与非安全连接
3.2.1 使用ws://和wss://协议连接远程服务端
WebSocket 支持两种 URI 模式:
- ws://host:port/path —— 明文传输,适用于内网或测试环境。
- wss://host:port/path —— 加密传输(WebSocket Secure),基于 TLS/SSL,生产环境推荐使用。
示例:连接不同协议的服务端
var client = new ClientWebSocket();
Uri wsUri = new Uri("ws://localhost:8080/chat");
Uri wssUri = new Uri("wss://api.example.com/feed");
await client.ConnectAsync(wsUri, CancellationToken.None); // 非安全连接
// 或
await client.ConnectAsync(wssUri, CancellationToken.None); // 安全连接
执行逻辑说明:
- 调用
ConnectAsync(Uri, CancellationToken)发起连接。- 内部自动检测 URI Scheme:
- 若为
ws,则通过 HTTP 发起升级请求。- 若为
wss,则通过 HTTPS 发起加密升级。- 握手成功后返回,进入 OPEN 状态。
虽然两者语法一致,但 wss 在底层涉及完整的 SSL/TLS 握手过程,安全性更高,尤其适合公网部署的应用。
协议对比表格:
| 特性 | ws:// | wss:// |
|---|---|---|
| 传输层 | TCP + HTTP Upgrade | TCP + TLS + HTTP Upgrade |
| 数据是否加密 | 否 | 是 |
| 是否易被窃听 | 是 | 否 |
| 是否可通过CDN支持 | 有限 | 支持良好 |
| 浏览器兼容性 | 所有 | 所有 |
| 适用场景 | 内网调试 | 生产环境、移动端 |
由此可见,除非处于完全可信网络中,否则一律推荐使用 wss:// 。
3.2.2 处理SSL/TLS证书验证失败问题
在连接 wss:// 服务端时,常遇到自签名证书或域名不匹配导致的验证失败。此时 .NET 会抛出 AuthenticationException 。
常见异常信息:
The remote certificate is invalid according to the validation procedure.
为解决此类问题,可通过 ClientWebSocketOptions.RemoteCertificateValidationCallback 自定义验证逻辑。
示例:忽略证书错误(仅限测试)
client.Options.RemoteCertificateValidationCallback =
(sender, certificate, chain, sslPolicyErrors) =>
{
// 忽略所有证书错误(仅用于开发)
return true;
};
警告:此做法存在严重安全风险!禁止在生产环境中使用。
生产级解决方案:严格校验 + 白名单机制
client.Options.RemoteCertificateValidationCallback =
(sender, cert, chain, errors) =>
{
if (errors == SslPolicyErrors.None)
return true;
// 允许特定指纹的自签证书
var allowedThumbprints = new HashSet<string>
{
"A1B2C3D4E5F6..." // 预置信任的证书指纹
};
return allowedThumbprints.Contains(cert.GetCertHashString());
};
参数解释:
sender: 发起事件的对象。certificate: 远程服务器提供的 X509 证书。chain: 证书链信息。sslPolicyErrors: 检测到的策略错误(如过期、无效CA等)。
通过比对证书指纹(Thumbprint),可在保证安全的前提下接入私有 CA 或测试环境。
3.2.3 自定义HttpClientHandler进行高级网络控制
尽管 ClientWebSocket 不直接暴露 HttpClientHandler ,但其底层依赖 HttpMessageInvoker 实现 HTTP 升级请求。因此可通过反射或子类化方式注入自定义行为(受限于当前 .NET 版本)。
不过,从 .NET 6 开始,可通过 ClientWebSocketOptions 的扩展点间接影响底层行为。
替代方案:使用 SocketsHttpHandler 配合 HttpMessageInvoker
var handler = new SocketsHttpHandler
{
ConnectTimeout = TimeSpan.FromSeconds(10),
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2),
MaxConnectionsPerServer = 100
};
var invoker = new HttpMessageInvoker(handler);
// 通过反射设置内部handler(非公开API,慎用)
typeof(ClientWebSocketOptions)
.GetProperty("HttpMessageInvoker", BindingFlags.NonPublic | BindingFlags.Instance)?
.SetValue(client.Options, invoker);
⚠️ 注意:上述操作属于 非公开API调用 ,可能在未来版本中失效,仅建议在特殊需求且充分测试后使用。
更稳妥的做法是通过反向代理或中间网关统一处理连接策略,保持客户端轻量化。
3.3 异步连接流程实战
3.3.1 调用ConnectAsync方法发起异步握手
ConnectAsync 是建立 WebSocket 连接的核心方法,执行完整的 HTTP Upgrade 流程。
完整连接示例:
var client = new ClientWebSocket();
client.Options.SetBuffer(16 * 1024, 16 * 1024);
client.Options.KeepAliveInterval = TimeSpan.FromSeconds(30);
Uri serverUri = new Uri("wss://echo.websocket.org");
try
{
await client.ConnectAsync(serverUri, CancellationToken.None);
Console.WriteLine($"连接成功,当前状态:{client.State}");
}
catch (WebSocketException ex)
{
Console.WriteLine($"WebSocket异常:{ex.Message}");
}
catch (TaskCanceledException)
{
Console.WriteLine("连接超时");
}
执行流程分解:
- 客户端构造
HTTP GET请求,添加Upgrade: websocket头。- 自动生成
Sec-WebSocket-Key并计算预期的Accept值。- 发送请求,等待服务端返回
101 Switching Protocols。- 验证响应头合法性,完成协议切换。
- 更改内部状态为
WebSocketState.Open。
该过程全程异步,不会阻塞主线程,适合集成进后台服务或 UI 应用。
3.3.2 捕获握手异常并实现重连机制
网络不稳定是常态,因此必须设计健壮的异常处理与自动重连逻辑。
常见握手异常类型:
| 异常类型 | 可能原因 | 应对策略 |
|---|---|---|
WebSocketException |
协议错误、服务不可达 | 重试 |
HttpRequestException |
DNS解析失败、连接拒绝 | 延迟重试 |
SecurityException |
TLS协商失败 | 检查证书 |
InvalidOperationException |
已连接或已关闭 | 状态检查 |
指数退避重连机制实现:
public async Task ConnectWithRetryAsync(Uri uri, int maxRetries = 5)
{
int attempt = 0;
TimeSpan delay = TimeSpan.FromSeconds(1);
while (attempt < maxRetries)
{
try
{
await client.ConnectAsync(uri, CancellationToken.None);
Console.WriteLine("连接成功");
return;
}
catch (Exception ex) when (ex is WebSocketException || ex is HttpRequestException)
{
attempt++;
if (attempt >= maxRetries)
throw;
Console.WriteLine($"第{attempt}次连接失败:{ex.Message},{delay}秒后重试");
await Task.Delay(delay);
delay = TimeSpan.FromSeconds(delay.TotalSeconds * 2); // 指数增长
}
}
}
逻辑分析:
- 使用
while循环控制最大重试次数。- 捕获常见网络异常,排除编程错误。
- 每次失败后等待时间翻倍,避免雪崩效应。
- 最终仍失败则向上抛出,由调用方决定后续动作。
该模式广泛应用于微服务治理、边缘计算节点等场景。
3.3.3 添加自定义HTTP头信息以支持身份认证
某些 WebSocket 服务要求在握手阶段传递认证信息(如 JWT Token、API Key)。
示例:添加 Authorization 头
client.Options.SetRequestHeader("Authorization", "Bearer eyJhbGciOiJIUzI1Ni...");
client.Options.SetRequestHeader("X-Device-ID", "device-12345");
这些头字段会在 Upgrade 请求中一并发送,服务端可在
HttpContext中读取。
支持的自定义头限制:
| 类型 | 是否允许 | 说明 |
|---|---|---|
| Authorization | ✅ | 常用于Token认证 |
| User-Agent | ✅ | 标识客户端类型 |
| Cookie | ⚠️ | 受SameSite策略影响 |
| Content-Type | ❌ | Upgrade请求无body |
| Transfer-Encoding | ❌ | 不支持 |
⚠️ 注意:某些浏览器或中间件可能会过滤敏感头,请提前测试。
该能力使得 WebSocket 能无缝集成进现有 OAuth2/JWT 认证体系,实现端到端安全通信。
4. WebSocket服务器端监听与AcceptWebSocketAsync
在构建基于 .NET 的 WebSocket 通信系统时,服务端的稳定性、可扩展性以及协议兼容性是决定整体架构质量的关键因素。 System.Net.WebSockets 提供了强大的底层支持,而结合 HttpListener 可以实现一个轻量级但功能完整的 WebSocket 服务端。本章将深入探讨如何使用 HttpListener 实现对 WebSocket 协议升级请求的识别与处理,并通过 AcceptWebSocketAsync 方法完成从 HTTP 到 WebSocket 的协议切换。同时,围绕多客户端连接管理机制的设计,分析连接池、会话绑定、资源限流等关键实践策略,为构建高并发实时服务打下坚实基础。
4.1 基于HttpListener的轻量级服务端搭建
4.1.1 启动HttpListener监听指定URI前缀
HttpListener 是 .NET Framework 和 .NET Core/.NET 5+ 中用于接收和响应 HTTP 请求的低层类,虽然它不具备 ASP.NET 的高级路由与中间件能力,但在需要精细控制协议行为(如自定义握手逻辑)的场景中具有独特优势。要启动一个能够处理 WebSocket 连接的服务端,首先必须配置并启动 HttpListener 监听特定 URI 前缀。
以下是一个典型的初始化代码示例:
using System;
using System.Net;
using System.Threading.Tasks;
public class WebSocketServer
{
private HttpListener _listener;
public async Task StartAsync(string[] prefixes)
{
if (!HttpListener.IsSupported)
throw new PlatformNotSupportedException("HttpListener is not supported on this platform.");
_listener = new HttpListener();
foreach (var prefix in prefixes)
{
_listener.Prefixes.Add(prefix); // e.g., http://localhost:8080/ws/
}
_listener.Start();
Console.WriteLine("WebSocket server started. Listening on:");
foreach (var p in prefixes) Console.WriteLine($" {p}");
while (_listener.IsListening)
{
var contextTask = _listener.GetContextAsync();
_ = HandleClientAsync(await contextTask); // Fire-and-forget per client
}
}
private async Task HandleClientAsync(HttpListenerContext context)
{
if (context.Request.IsWebSocketRequest)
{
await HandleWebSocketRequestAsync(context);
}
else
{
context.Response.StatusCode = 400;
context.Response.Close();
}
}
private async Task HandleWebSocketRequestAsync(HttpListenerContext context)
{
// 升级逻辑将在下一节详述
}
}
代码逻辑逐行解读:
- 第7~9行 :检查平台是否支持
HttpListener,某些精简运行时环境可能不包含该组件。 - 第11行 :创建新的
HttpListener实例。 - 第13~16行 :添加 URI 前缀,这些前缀必须注册到操作系统权限中(Windows 需管理员或 URLACL 授权),否则抛出异常。
- 第18行 :调用
Start()开始监听网络请求。 - 第22行起 :进入无限循环,持续调用
GetContextAsync()异步获取到来的 HTTP 请求上下文。 - 第24行 :采用“即发即弃”模式处理每个客户端请求,避免阻塞主监听线程。
- 第28~33行 :判断是否为合法的 WebSocket 升级请求,依据标准由
IsWebSocketRequest属性自动完成。
⚠️ 注意:若未正确设置 URLACL 权限,在 Windows 上运行会抛出
Access Denied错误。可通过命令行注册:
bash netsh http add urlacl url=http://+:8080/ws/ user=Everyone
此外,Linux/macOS 下需确保进程有绑定端口的权限(如使用 sudo 或 Capabilities)。
| 参数 | 类型 | 说明 |
|---|---|---|
prefixes |
string[] |
必须以 / 结尾,格式为 scheme://host:port/path/ |
IsWebSocketRequest |
bool |
检查是否存在 Upgrade: websocket 头及必要字段 |
4.1.2 接收HTTP请求并判断是否为WebSocket升级请求
WebSocket 连接始于一次标准的 HTTP 请求,其特征在于特殊的头部字段组合。 HttpListener 提供了便捷属性来识别此类请求,但也应理解其底层判定逻辑,以便进行定制化验证。
核心升级请求头示例:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: chat, superchat
Origin: http://example.com
当 HttpListener 收到该请求后, context.Request.IsWebSocketRequest 将返回 true ,表示满足基本升级条件。
然而,在生产环境中建议增加额外校验,防止恶意伪造请求。例如:
private bool IsValidWebSocketHandshake(HttpListenerRequest request)
{
return request.Headers["Upgrade"]?.Equals("websocket", StringComparison.OrdinalIgnoreCase) == true &&
request.Headers["Connection"]?.Split(',')
.Any(h => h.Trim().Equals("upgrade", StringComparison.OrdinalIgnoreCase)) == true &&
!string.IsNullOrEmpty(request.Headers["Sec-WebSocket-Key"]) &&
request.Headers["Sec-WebSocket-Version"] == "13";
}
此方法显式验证关键头字段,增强安全性。
状态流程图(Mermaid)
sequenceDiagram
participant Client
participant Server
Client->>Server: 发送HTTP GET请求 + Upgrade头
Server->>Server: 解析请求头
alt 是有效WebSocket请求?
Server-->>Client: 返回101 Switching Protocols
Server->>Server: 调用AcceptWebSocketAsync
Note right of Server: 协议切换完成
else 无效请求
Server-->>Client: 返回400 Bad Request
Server->>Server: 关闭连接
end
上述流程清晰地展示了从原始 HTTP 请求到协议升级的决策路径。
进一步地,可在日志中记录握手详情以供调试:
Console.WriteLine($"New WebSocket handshake from {context.Request.RemoteEndPoint}");
Console.WriteLine($"Key: {context.Request.Headers["Sec-WebSocket-Key"]}");
Console.WriteLine($"Protocols requested: {context.Request.Headers["Sec-WebSocket-Protocol"]}");
这有助于排查跨域、协议不匹配等问题。
最后提醒: HttpListener 不适用于 IIS 托管环境——IIS 使用自己的管道模型,通常需改用 ASP.NET Core WebSockets Middleware 或 SignalR 。但在独立进程、微服务网关、边缘设备通信等场景中, HttpListener 构建的轻量级服务极具价值。
4.2 协议升级处理流程
4.2.1 调用HttpContext.AcceptWebSocketAsync方法触发升级
一旦确认收到有效的 WebSocket 升级请求,下一步便是执行协议切换操作。 .NET 提供了 HttpListenerContext.AcceptWebSocketAsync(subProtocol) 方法,用于异步完成从 HTTP 到 WebSocket 的转换。
示例代码:
private async Task HandleWebSocketRequestAsync(HttpListenerContext context)
{
WebSocket webSocket = null;
try
{
var result = await context.AcceptWebSocketAsync(subProtocol: null);
webSocket = result.WebSocket;
Console.WriteLine($"WebSocket connection established with {context.Request.RemoteEndPoint}");
await EchoLoopAsync(webSocket);
}
catch (Exception ex)
{
Console.WriteLine($"Error during WebSocket handshake: {ex.Message}");
}
finally
{
webSocket?.Dispose();
context.Response.Close(); // 显式关闭底层响应流
}
}
private async Task EchoLoopAsync(WebSocket socket)
{
var buffer = new byte[1024];
while (socket.State == WebSocketState.Open)
{
var result = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Close)
{
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
break;
}
await socket.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count),
result.MessageType,
result.EndOfMessage,
CancellationToken.None);
}
}
参数说明:
| 参数 | 类型 | 作用 |
|---|---|---|
subProtocol |
string |
指定选定的子协议名称;若为 null 表示无协商或默认接受 |
result.WebSocket |
WebSocket |
成功升级后返回的全双工通信通道对象 |
CancellationToken |
CancellationToken |
支持取消挂起的操作 |
逻辑分析:
- 第4行 :调用
AcceptWebSocketAsync触发内部协议升级流程,包括生成Sec-WebSocket-Accept值并发送101 Switching Protocols响应。 - 第6行 :获取升级后的
WebSocket对象,可用于后续收发消息。 - 第11~12行 :异常捕获确保握手失败时不崩溃服务。
- 第17~27行 :实现简单回声逻辑,演示数据读写闭环。
值得注意的是, AcceptWebSocketAsync 内部已自动完成 RFC6455 规范规定的 Accept Key 计算:
acceptKey = Base64Encode(SHA1(secKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
开发者无需手动计算,极大简化实现复杂度。
4.2.2 实现WebSocket子协议选择逻辑
子协议(SubProtocol)允许客户端和服务端在建立连接前就应用层语义达成一致,常用于区分不同类型的消息格式(如 JSON vs Protobuf)、业务模块(如 chat vs notification)等。
客户端可通过 Sec-WebSocket-Protocol 头发送多个候选协议:
Sec-WebSocket-Protocol: proto.v1.json, proto.v2.msgpack
服务端应在 AcceptWebSocketAsync 调用中选择其支持的一项:
private string SelectSubProtocol(HttpListenerRequest request)
{
var clientProtocols = (request.Headers["Sec-WebSocket-Protocol"] ?? "")
.Split(',')
.Select(p => p.Trim())
.Where(p => !string.IsNullOrEmpty(p));
const string preferred = "proto.v2.msgpack";
return clientProtocols.Contains(preferred) ? preferred : null;
}
然后传入选定值:
var selectedProto = SelectSubProtocol(context.Request);
var result = await context.AcceptWebSocketAsync(selectedProto);
成功后,客户端可通过 webSocket.SubProtocol 获取最终协商结果:
Console.WriteLine($"Negotiated sub-protocol: {webSocket.SubProtocol}");
| 客户端请求协议列表 | 服务端选择逻辑 | 最终结果 |
|---|---|---|
chat, video |
支持 video |
video |
binary/v1 |
不支持任何 | null → 握手仍成功,但无子协议 |
| (未发送) | 返回非null值 | 抛出异常!必须是客户端提议中的 |
⚠️ 若服务端返回的 subProtocol 不在客户端提议范围内,则握手失败,返回 426 Upgrade Required 。
因此,务必保证所选协议属于客户端提供的集合。
4.2.3 在Upgrade事件中注入自定义验证逻辑
尽管 AcceptWebSocketAsync 自动处理大部分握手细节,但在实际项目中往往需要插入身份认证、IP 限流、Origin 校验等安全措施。
由于 HttpListener 不提供“中间件”机制,所有验证必须在调用 AcceptWebSocketAsync 前手动完成。
示例:JWT Token 验证(通过查询参数)
private bool ValidateJwtToken(HttpListenerRequest request)
{
var token = request.QueryString["token"];
if (string.IsNullOrEmpty(token)) return false;
try
{
var handler = new JwtSecurityTokenHandler();
var principal = handler.ValidateToken(token, GetValidationParameters(), out _);
return principal.Identity.IsAuthenticated;
}
catch
{
return false;
}
}
示例:跨域 Origin 检查
private bool IsOriginAllowed(HttpListenerRequest request)
{
var origin = request.Headers["Origin"];
var allowedOrigins = new[] { "http://localhost:3000", "https://myapp.com" };
return string.IsNullOrEmpty(origin) || allowedOrigins.Contains(origin);
}
整合进主流程:
private async Task HandleWebSocketRequestAsync(HttpListenerContext context)
{
if (!IsOriginAllowed(context.Request))
{
context.Response.StatusCode = 403;
context.Response.Close();
return;
}
if (!ValidateJwtToken(context.Request))
{
context.Response.StatusCode = 401;
context.Response.Close();
return;
}
try
{
var result = await context.AcceptWebSocketAsync(null);
await ProcessMessagesAsync(result.WebSocket);
}
catch (Exception ex)
{
// 日志记录
}
finally
{
context.Response.Close();
}
}
安全检查流程图(Mermaid)
graph TD
A[收到HTTP请求] --> B{IsWebSocketRequest?}
B -- No --> C[返回400]
B -- Yes --> D[检查Origin]
D -- 不合法 --> E[返回403]
D -- 合法 --> F[验证Token]
F -- 失败 --> G[返回401]
F -- 成功 --> H[AcceptWebSocketAsync]
H --> I[开始WebSocket通信]
通过这种方式,即使使用轻量级 HttpListener ,也能构建具备完整安全控制能力的服务端。
4.3 多客户端连接管理
4.3.1 维护活跃连接集合(ConcurrentDictionary)
随着并发用户增长,必须有效管理所有已建立的 WebSocket 连接。推荐使用线程安全的 ConcurrentDictionary<TKey, TValue> 存储活动连接。
private static readonly ConcurrentDictionary<string, WebSocketConnection> _connections
= new();
public class WebSocketConnection
{
public WebSocket Socket { get; set; }
public DateTime ConnectedAt { get; set; }
public string UserId { get; set; }
public string ConnectionId => Socket.GetHashCode().ToString("X8");
}
在连接建立后注册:
var conn = new WebSocketConnection
{
Socket = webSocket,
ConnectedAt = DateTime.UtcNow,
UserId = ExtractUserId(context) // 从token或其他方式提取
};
_connections.TryAdd(conn.ConnectionId, conn);
断开时清理:
finally
{
_connections.TryRemove(conn.ConnectionId, out _);
webSocket?.Dispose();
}
优势对比表:
| 集合类型 | 线程安全 | 性能 | 适用场景 |
|---|---|---|---|
Dictionary<string, T> |
❌ | 高 | 单线程测试 |
Dictionary + lock |
✅ | 中(锁竞争) | 小规模连接 |
ConcurrentDictionary |
✅ | 高(无全局锁) | 高并发生产环境 |
利用其原子性操作,可安全实现广播功能:
public async Task BroadcastAsync(byte[] message)
{
var tasks = _connections.Values
.Where(c => c.Socket.State == WebSocketState.Open)
.Select(c => c.Socket.SendAsync(message, WebSocketMessageType.Text, true, CancellationToken.None));
await Task.WhenAll(tasks);
}
4.3.2 设计连接标识与会话上下文绑定机制
单纯依赖 WebSocket.GetHashCode() 不够稳定,应设计统一的连接 ID 生成策略,并关联用户会话信息。
推荐方案:GUID + 用户元数据
public class SessionContext
{
public string ConnectionId { get; } = Guid.NewGuid().ToString("n");
public ClaimsPrincipal User { get; set; }
public Dictionary<string, object> Metadata { get; set; } = new();
public DateTime LastActivity { get; set; } = DateTime.UtcNow;
}
在握手完成后注入:
var session = new SessionContext
{
User = ParseClaimsFromToken(token),
Metadata = { ["Device"] = "WebClient" }
};
_connections.TryAdd(session.ConnectionId, new WebSocketEntry
{
Socket = webSocket,
Session = session
});
这样即可实现:
- 按用户推送消息(如通知)
- 查询某用户的全部设备连接
- 实现踢人下线功能
public void KickUser(string userId)
{
var connections = _connections.Values
.Where(c => c.Session.User.FindFirst(ClaimTypes.NameIdentifier)?.Value == userId);
foreach (var conn in connections)
{
_ = conn.Socket.CloseAsync(WebSocketCloseStatus.PolicyViolated, "Kicked", default);
_connections.TryRemove(conn.Session.ConnectionId, out _);
}
}
4.3.3 实现连接池资源复用与限流控制
为防止单点过载,需引入连接数限制与速率控制。
连接总数限制(SemaphoreSlim)
private readonly SemaphoreSlim _connectionLimiter = new(100, 100); // 最大100连接
public async Task HandleWebSocketRequestAsync(HttpListenerContext context)
{
if (!_connectionLimiter.Wait(0))
{
context.Response.StatusCode = 503;
context.Response.StatusDescription = "Server too busy";
context.Response.Close();
return;
}
try
{
await _connectionLimiter.WaitAsync();
// ... 正常处理 ...
}
finally
{
_connectionLimiter.Release();
}
}
每IP连接限流(滑动窗口)
private static readonly ConcurrentDictionary<string, List<DateTime>> _ipTracker
= new();
private bool AllowConnectionFromIp(string ip)
{
var window = _ipTracker.GetOrAdd(ip, _ => new List<DateTime>());
var now = DateTime.UtcNow;
window.RemoveAll(t => t < now.AddSeconds(-60)); // 清理旧记录
if (window.Count >= 5) return false; // 每分钟最多5个连接
window.Add(now);
return true;
}
综合以上机制,可构建出健壮、安全、可扩展的 WebSocket 服务端基础设施,支撑在线聊天、实时监控、金融行情等多种高实时性应用场景。
5. WebSocket异步数据传输与完整通信闭环构建
5.1 发送与接收数据的统一模型
在 .NET 的 System.Net.WebSockets 命名空间中,所有 WebSocket 数据交互均通过 SendAsync 和 ReceiveAsync 方法实现。这两个方法构成了异步全双工通信的核心接口,支持文本、二进制和关闭帧三种消息类型。
public enum WebSocketMessageType
{
Text = 0, // UTF-8 编码的文本数据
Binary = 1, // 任意二进制流
Close = 2 // 关闭控制帧
}
发送数据时需将消息封装为 ArraySegment<byte> ,以避免不必要的内存拷贝,提升高并发场景下的性能表现:
private async Task SendTextMessageAsync(WebSocket socket, string message)
{
var buffer = Encoding.UTF8.GetBytes(message);
var segment = new ArraySegment<byte>(buffer, 0, buffer.Length);
await socket.SendAsync(
segment,
WebSocketMessageType.Text,
true, // FIN = true 表示完整消息
CancellationToken.None);
}
其中参数说明如下:
- segment :实际要发送的数据段;
- messageType :指定消息类型;
- endOfMessage (FIN) :是否为最后一个分片;
- cancellationToken :支持取消操作。
接收端则依赖 WebSocketReceiveResult 对象解析返回结果:
var receiveBuffer = new byte[1024];
var segment = new ArraySegment<byte>(receiveBuffer);
WebSocketReceiveResult result = await socket.ReceiveAsync(segment, CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Close)
{
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
}
else if (result.MessageType == WebSocketMessageType.Text)
{
string text = Encoding.UTF8.GetString(receiveBuffer, 0, result.Count);
Console.WriteLine($"Received: {text}");
}
该模型实现了统一的消息处理入口,适用于客户端和服务端双向通信。
5.2 消息帧的分片与重组处理
当传输大数据包(如文件或高清图像)超过单帧限制时,必须使用分片机制。WebSocket 协议通过 FIN 标志位 控制消息完整性:
| FIN | 含义 |
|---|---|
| 1 | 完整消息,无需拼接 |
| 0 | 连续帧开始或中间部分,需等待后续帧 |
例如,一个 4KB 的 JSON 消息可被拆分为多个 1KB 的帧进行传输:
const int MaxFrameSize = 1024;
var payload = Encoding.UTF8.GetBytes(largeJsonString);
int offset = 0;
while (offset < payload.Length)
{
int currentSize = Math.Min(MaxFrameSize, payload.Length - offset);
bool isFinal = (offset + currentSize) >= payload.Length;
var frameSegment = new ArraySegment<byte>(payload, offset, currentSize);
await socket.SendAsync(frameSegment, WebSocketMessageType.Text, isFinal, CancellationToken.None);
offset += currentSize;
}
接收端需持续读取直到 WebSocketReceiveResult.EndOfMessage 为 true 才能完成重组:
var fullMessage = new MemoryStream();
do
{
var tempBuffer = new byte[1024];
var segment = new ArraySegment<byte>(tempBuffer);
var result = await socket.ReceiveAsync(segment, CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Close)
break;
fullMessage.Write(tempBuffer, 0, result.Count);
} while (!result.EndOfMessage);
string finalData = Encoding.UTF8.GetString(fullMessage.ToArray());
值得注意的是,根据 RFC6455 规范, 客户端发送的所有帧必须进行掩码处理 ,而服务端自动解掩码。.NET 底层已透明处理此逻辑,开发者无需手动干预。
5.3 心跳检测与异常恢复机制
长时间空闲连接易被中间代理或防火墙中断。为此应定期发送 Ping/Pong 帧维持活性:
sequenceDiagram
participant Client
participant Server
Client->>Server: 发送 Ping 帧
Server-->>Client: 自动回复 Pong 帧
Note right of Server: 若未收到回应,视为连接失效
.NET 并不直接暴露 Ping/Pong API,但可通过以下方式触发:
await socket.SendAsync(
new ArraySegment<byte>(new byte[0]),
WebSocketMessageType.Ping,
true,
CancellationToken.None);
同时监听网络异常并启动重连逻辑:
try
{
await ListenForMessages(socket);
}
catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely)
{
Console.WriteLine("连接中断,尝试重连...");
await ReconnectAsync(); // 实现指数退避重连策略
}
优雅关闭连接也至关重要:
await socket.CloseAsync(
WebSocketCloseStatus.NormalClosure,
"Client closing",
CancellationToken.None);
这会向对端发送 Close 帧,通知其正常终止会话,避免资源泄漏。
5.4 MyWebSocket.sln项目集成与实时通信应用落地
MyWebSocket.sln 是一个典型的三层结构解决方案:
| 项目模块 | 职责描述 |
|---|---|
| Common | 共享实体类、协议定义、工具方法 |
| Server | 使用 HttpListener 构建 WebSocket 服务端 |
| Client | WPF/Console 客户端发起连接并收发消息 |
典型应用场景包括:
-
在线聊天室
- 多用户连接至同一广播通道
- 服务端接收消息后群发给所有活跃连接
- 支持表情包(Binary帧)上传与展示 -
股票行情推送
json {"symbol":"AAPL","price":198.75,"timestamp":"2025-04-05T10:23:00Z"}
- 服务端每秒批量推送上千条更新
- 客户端采用缓冲队列+UI线程调度防止阻塞 -
远程指令控制系统
- 设备端作为 WebSocket 客户端上报状态
- 管理平台下发配置变更、重启等命令
- 使用子协议protocol="cmd.v1"区分业务类型
调试过程中可通过日志记录关键事件:
void Log(string msg) => Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {msg}");
输出示例:
[10:22:01] Connected to wss://server.com/feed
[10:22:02] Sent Auth Token: abc123xyz
[10:22:03] Received: {"status":"authenticated"}
[10:22:04] Ping sent, Pong received in 12ms
[10:22:34] Error: Connection closed (1006)
[10:22:35] Reconnecting attempt #1...
通过上述机制,最终构建出稳定可靠的 WebSocket 通信闭环,支撑企业级实时系统运行。
简介:WebSocket是一种基于TCP的协议,通过HTTP升级实现客户端与服务器之间的持久化、全双工通信,显著提升实时数据交互效率。在C#中,借助.NET Framework或.NET Core提供的System.Net.WebSockets命名空间,可高效实现WebSocket客户端与服务器端的异步通信。本文介绍WebSocket协议原理,详细讲解ClientWebSocket与WebSocket类的使用,涵盖连接建立、数据收发、连接关闭等核心流程,并提供完整示例代码。结合Mywebsocket项目分析,帮助开发者掌握C#环境下WebSocket的实际应用,适用于在线聊天、实时游戏、金融行情推送等高实时性场景。
更多推荐

所有评论(0)