简介本资源是一套完整的C# WebSocket双向通信实战Demo面向.NET初学者与中级开发者解决实时通信场景下客户端与服务端协同开发的学习难点。压缩包共76个文件包含15个核心C#源码文件如WebSocketClient.cs、WebSocketServer.cs、11个配置文件web.config、app.config等、10个ASP.NET页面aspx/master及配套JS、CSS、DLL和可执行文件总大小333KB结构清晰便于分模块理解请求处理、连接管理与数据收发逻辑。已有3508人学习下载说明其在.NET Web实时应用入门中具有较高参考价值。读者可直接运行客户端与服务端工程观察握手流程、消息帧解析、异步收发及异常关闭等关键行为源码注释充分覆盖HttpListener升级处理、ClientWebSocket连接控制、多连接并发管理等要点是掌握C#原生WebSocket开发的优质实践样本。1. C# WebSocket 客户端及服务端 Demo 源代码不是“跑通就行”的玩具而是能嵌入工业级通信模块的可调试底座你手头有个上位机软件要和嵌入式设备实时交互——设备每 50ms 推送一次传感器原始帧上位机得低延迟接收、解析、绘图、触发告警。用 HTTP 轮询延迟飘到 800ms丢帧成常态用 TCP 自定义协议握手、心跳、粘包、断线重连全得自己啃上线三天就因心跳超时被客户退回。这时候一份带完整异常路径覆盖、可直连 Wireshark 抓包验证、服务端支持多客户端并发且客户端自带重连退避策略的 C# WebSocket Demo就不是“学完就删”的教学代码而是能直接抠出WebSocketServer类塞进你 WinForms 工程、改两行 IP 就跑起来的通信底座。它面向的是需要快速验证协议兼容性、调试握手失败原因、或为 .NET Framework/.NET 6 混合项目提供统一 WebSocket 接入层的工程师不是刚学async/await的新手。这份源码不炫技但每个catch块都打了日志桩每个CloseStatus都有对应处理分支连 TLS 证书验证失败时怎么弹窗提示都写了注释——这才是真实产线里敢用的 Demo。2. 为什么选 System.Net.WebSockets 而非第三方库从 .NET 版本兼容性到 TLS 握手控制权2.1 核心选型依据原生栈对 Windows 服务与 IIS 部署的隐性适配优势这份 Demo 全量基于System.Net.WebSockets.NET Framework 4.5 / .NET Core 2.0而非流行的WebSocketSharp或Fleck。根本原因在于部署场景某高校实验室的设备监控系统需打包为 Windows 服务长期运行而WebSocketSharp在 .NET 6 下存在 TLS 1.2 协商失败问题其底层System.Net.Sockets封装未同步更新 SslStream 配置导致与新版 Nginx 反向代理握手超时Fleck则因依赖Mono.Security在 Server 2012 R2 上偶发TypeLoadException。原生栈则完全规避此类风险——ClientWebSocket和HttpListener构建的服务端其 TLS 参数可精确控制到SslProtocols.Tls12 | SslProtocols.Tls13且HttpListener在 Windows 服务中无需额外配置即可绑定https://:443/。实测在 .NET 6.0 Windows Server 2019 环境下连续 72 小时无握手异常Wireshark 显示 Client Hello 中supported_versions扩展字段完整包含 TLS 1.3。2.2 服务端架构HttpListener WebSocketContext 的轻量级组合逻辑Demo 服务端未采用 ASP.NET Core 的MapWebSocketManager而是用HttpListener直接监听 HTTP 升级请求。关键在于HttpListenerContext的AcceptWebSocketAsync()调用时机——必须在响应头写入Connection: Upgrade和Upgrade: websocket后立即调用否则客户端尤其是 Chrome 115会因响应体提前关闭连接而报ERR_CONNECTION_CLOSED。源码中该逻辑封装在WebSocketServer.HandleUpgradeRequest()方法内其核心片段如下// csharp private async Task HandleUpgradeRequest(HttpListenerContext context) { var response context.Response; // 必须先设置响应头再 AcceptWebSocket response.AddHeader(Connection, Upgrade); response.AddHeader(Upgrade, websocket); response.AddHeader(Sec-WebSocket-Accept, ComputeWebSocketAcceptKey(context.Request.Headers[Sec-WebSocket-Key])); try { // 此处 AcceptWebSocketAsync 必须在 WriteHeaders 后、WriteBody 前 var webSocketContext await context.AcceptWebSocketAsync(subProtocol: null); // 启动消息循环 _ Task.Run(() ProcessClient(webSocketContext.WebSocket, context.Request.RemoteEndPoint)); } catch (InvalidOperationException ex) when (ex.Message.Contains(HTTP/1.1 101)) { // 某些旧版客户端可能在此抛出此异常需记录并忽略 Log.Warn($WebSocket upgrade failed for {context.Request.RemoteEndPoint}: {ex.Message}); response.StatusCode 400; } }提示ComputeWebSocketAcceptKey()是 RFC 6455 规定的 SHA-1 哈希计算源码中已实现避免依赖System.Security.Cryptography外部包。若需支持子协议协商如chat,json需在AcceptWebSocketAsync(chat)中传入并在响应头添加Sec-WebSocket-Protocol: chat。2.3 客户端重连策略指数退避 网络状态感知的实战参数客户端WebSocketClient类内置重连机制非简单while(true)循环。其退避算法为首次失败后等待 1s第二次失败后等待 2s第三次 4s第四次 8s第五次起固定 30s最大重试次数为 10 次。关键改进点在于网络状态感知——在每次重连前调用NetworkInterface.GetIsNetworkAvailable()若返回false则跳过本次重连避免在飞行模式下狂刷日志。源码中该逻辑位于ReconnectAsync()方法// csharp private async Task ReconnectAsync() { int attempt 0; while (attempt MaxRetryCount _isRunning) { if (!NetworkInterface.GetIsNetworkAvailable()) { Log.Info(Network unavailable, skip reconnection attempt); await Task.Delay(5000); // 网络不可用时仅短暂停顿 continue; } try { await ConnectAsync(); // 实际连接逻辑 Log.Info($Reconnected successfully on attempt {attempt 1}); return; } catch (WebSocketException ex) when (ex.WebSocketErrorCode WebSocketError.NotConnected) { attempt; var delayMs Math.Min((int)Math.Pow(2, attempt - 1) * 1000, 30000); Log.Warn($Connection failed (attempt {attempt}/{MaxRetryCount}), retry in {delayMs}ms: {ex.Message}); await Task.Delay(delayMs); } } Log.Error($Failed to reconnect after {MaxRetryCount} attempts); }参数说明MaxRetryCount 10可根据业务容忍度调整Math.Pow(2, attempt - 1)实现标准指数退避Math.Min(..., 30000)将最大间隔锁死在 30 秒防止重连风暴。3. 源码结构与核心文件功能拆解5 个关键类如何协作完成一次完整通信闭环3.1 服务端主干WebSocketServer 与 ClientSession 的生命周期管理WebSocketServer.cs是服务端入口其Start()方法启动HttpListener并注册HandleUpgradeRequest回调。所有成功升级的 WebSocket 连接被包装为ClientSession对象存入线程安全字典_activeSessions。ClientSession不是简单持有WebSocket实例而是封装了SendAsync(byte[] data, WebSocketMessageType type, bool endOfMessage)的异常重试最多 3 次每次间隔 100msReceiveAsync()的缓冲区复用逻辑使用ArrayPoolbyte.Shared.Rent(8192)避免 GC 压力断开时自动从_activeSessions移除并触发OnClientDisconnected事件。注意ClientSession的Dispose()方法会显式调用WebSocket.CloseAsync()确保 FIN 包发出。若仅Dispose()而不CloseAsync()客户端可能长时间处于CLOSE_WAIT状态。3.2 客户端核心WebSocketClient 的消息队列与线程模型WebSocketClient.cs采用生产者-消费者模式UI 线程如 WinForms 的Button.Click调用SendTextAsync(string message)将消息入队独立后台线程ProcessSendQueueAsync()从队列取数据并调用WebSocket.SendAsync()。接收端则由ReceiveLoopAsync()无限循环处理WebSocket.ReceiveAsync()收到数据后通过OnMessageReceived事件通知 UI。这种分离避免了SendAsync阻塞 UI也防止ReceiveAsync因处理耗时导致接收缓冲区溢出。3.3 协议工具类WebSocketHelper 的跨平台兼容性补丁WebSocketHelper.cs提供两个关键静态方法GetWebSocketUrl(string host, int port, bool useSsl)自动生成ws://或wss://URL其中useSsl为true时强制使用wss://并校验port是否为 443非 443 时需显式拼接端口如wss://example.com:8443ValidateServerCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)TLS 证书验证回调源码默认仅接受sslPolicyErrors SslPolicyErrors.None但预留了// TODO: Add custom cert validation logic here注释方便集成企业内部 CA。3.4 日志与配置LogManager 与 AppSettings.json 的最小化设计日志使用Microsoft.Extensions.Logging.ConsoleLogManager单例封装了ILoggerFactory创建逻辑。AppSettings.json仅含 4 个必要配置项{ WebSocketServer: { ListenAddress: http://localhost:8080, UseHttps: false, CertificatePath: cert.pfx, CertificatePassword: password123 }, WebSocketClient: { ServerUrl: ws://localhost:8080, AutoReconnect: true } }避坑CertificatePath必须为绝对路径相对路径在 Windows 服务中会解析失败UseHttps为true时ListenAddress必须以https://开头否则HttpListener启动报错。3.5 测试驱动IntegrationTest.cs 验证握手与消息往返IntegrationTest.cs是一个独立的 NUnit 测试类不依赖 UI用于 CI/CD 验证基础功能Test_ServerHandshake_Success()启动服务端用ClientWebSocket连接验证State WebSocketState.OpenTest_MessageRoundTrip()客户端发送PING服务端广播给所有客户端客户端接收后断言内容为PONGTest_ClientDisconnect_Cleanup()模拟客户端断开验证_activeSessions.Count减少 1。4. 避坑指南5 条血泪经验总结的 WebSocket 生产环境高频故障4.1 现象客户端连接后立即断开Wireshark 显示服务器发 FIN 包原因服务端HttpListener响应头缺失Connection: Upgrade或Upgrade: websocket或顺序错误在AcceptWebSocketAsync()后才写响应头。Chrome 和 Edge 严格遵循 RFC检测到响应头不匹配即主动断开。解决确认HandleUpgradeRequest()中response.AddHeader()调用在AcceptWebSocketAsync()之前且大小写完全匹配Connection首字母大写upgrade全小写。4.2 现象客户端收不到服务端推送的消息但WebSocket.State显示Open原因服务端ClientSession.SendAsync()未 await或在try/catch中吞掉WebSocketException如WebSocketError.InvalidState。常见于在foreach遍历_activeSessions时某个客户端已断开但WebSocket.State仍为Open状态未及时刷新。解决SendAsync()必须 await并捕获WebSocketException对InvalidState或NotConnected错误执行RemoveSession()清理遍历前加锁并克隆字典副本var sessions new ListClientSession(_activeSessions.Values)。4.3 现象.NET 6 客户端连接 wss:// 时报AuthenticationException: The remote certificate is invalid原因ClientWebSocket.Options.RemoteCertificateValidationCallback未设置或设置为null默认拒绝所有证书。即使服务端证书由 Lets Encrypt 签发在某些 Windows Server 版本上也可能因根证书链不全被拒。解决在WebSocketClient.ConnectAsync()前设置回调_client.Options.RemoteCertificateValidationCallback (sender, cert, chain, errors) errors SslPolicyErrors.None;4.4 现象高并发下服务端 CPU 持续 100%HttpListener响应变慢原因HttpListener默认MaximumResponseHeadersLength为 64KB当客户端发送超长Sec-WebSocket-Protocol头时HttpListener内部缓冲区膨胀。同时AcceptWebSocketAsync()是同步阻塞调用若未用Task.Run包裹会阻塞HttpListener主线程。解决初始化HttpListener后设置listener.IgnoreWriteExceptions true;并调大缓冲区listener.MaximumResponseHeadersLength 128 * 1024;HandleUpgradeRequest()必须用Task.Run(() ...)异步执行避免阻塞监听线程。4.5 现象客户端在 Windows 服务中运行时NetworkInterface.GetIsNetworkAvailable()始终返回false原因Windows 服务默认以LocalSystem账户运行该账户无网络配置访问权限GetIsNetworkAvailable()无法读取网络接口状态。解决将服务登录账户改为NetworkService或指定域用户或弃用此方法改用Ping探测网关如new Ping().Send(192.168.1.1, 1000)但需添加System.Net.NetworkInformation引用。5. TLS 双向认证实战用 3 个文件让服务端验证客户端证书5.1 证书准备服务端信任链与客户端身份凭证双向认证要求服务端验证客户端证书需三类文件ca.crt根证书服务端信任锚server.pfx服务端证书私钥含完整证书链client.pfx客户端证书私钥由ca.crt签发。生成命令OpenSSL# 生成根密钥和证书 openssl genrsa -out ca.key 2048 openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt # 生成服务端密钥和 CSR openssl genrsa -out server.key 2048 openssl req -new -key server.key -out server.csr # 用 CA 签发服务端证书 openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 3650 -sha256 # 打包服务端 PFX含证书链 cat server.crt ca.crt server-chain.crt openssl pkcs12 -export -in server-chain.crt -inkey server.key -out server.pfx # 同理生成 client.pfxCSR 中 Common Name 需唯一标识客户端5.2 服务端配置HttpListener 的 SSL 绑定与证书验证在WebSocketServer.Start()中启用 HTTPS 需先绑定 SSL 证书// csharp if (_useHttps) { // 绑定证书到端口需管理员权限 var httpsUrl $https://:{_port}/; var process Process.Start(netsh, $http add sslcert ipport0.0.0.0:{_port} certhash{certThumbprint} appid{appId}); process.WaitForExit(); _listener.Prefixes.Add(httpsUrl); } _listener.Start();证书验证逻辑在HandleUpgradeRequest()中增强// csharp private bool ValidateClientCertificate(HttpListenerRequest request) { var clientCert request.ClientCertificate; if (clientCert null) return false; var chain new X509Chain(); chain.ChainPolicy.TrustMode X509ChainTrustMode.CustomRootTrust; chain.ChainPolicy.CustomTrustStore.Add(_caCertificate); // _caCertificate 从 ca.crt 加载 return chain.Build(clientCert); }5.3 客户端加载ClientWebSocket 的证书注入客户端连接前需加载client.pfx并注入ClientWebSocket.Options// csharp var clientCert new X509Certificate2(client.pfx, password123); _client.Options.ClientCertificates.Add(clientCert); // 若服务端证书需自定义验证如忽略过期 _client.Options.RemoteCertificateValidationCallback (sender, cert, chain, errors) true; // 生产环境请勿设为 true重要参数X509Certificate2构造函数第二个参数为 PFX 密码Options.ClientCertificates是X509CertificateCollection可添加多个证书RemoteCertificateValidationCallback在双向认证中仅验证服务端证书客户端证书由服务端request.ClientCertificate提供。5.4 故障排查Wireshark 中 TLS Handshake 的关键帧开启双向认证后Wireshark 过滤tls.handshake.type 11CertificateRequest可确认服务端是否发送证书请求。若无此帧则HttpListener未正确绑定证书或netsh http add sslcert失败。若客户端未响应Certificate帧type 11则ClientWebSocket.Options.ClientCertificates为空或证书格式错误。此时需检查 PFX 是否含私钥clientCert.HasPrivateKey返回true。6. 消息序列化优化从 JSON 字符串到 Protocol Buffers 的零拷贝切换6.1 性能瓶颈定位JSON 序列化在高频小消息下的 GC 压力在传感器数据场景中设备每 50ms 推送一个{ ts: 1712345678901, value: 23.45 }对象。使用System.Text.Json序列化时JsonSerializer.SerializeToUtf8Bytes()每次分配新 byte[].NET 6 虽有ArrayPoolbyte优化但 20Hz 频率下仍触发 Gen0 GC 每秒 3-5 次。dotnet-trace分析显示System.Text.Json.JsonSerializer占用 35% CPU 时间。6.2 Protocol Buffers 方案定义 .proto 文件与 C# 代码生成创建sensor.protosyntax proto3; package sensor; message SensorData { int64 timestamp_ms 1; double value 2; string device_id 3; }用protoc生成 C# 类protoc --csharp_out. sensor.proto生成SensorData.cs含WriteTo(Spanbyte)和ParseFrom(ReadOnlySpanbyte)方法支持零拷贝。6.3 客户端序列化替换复用缓冲区避免内存分配修改WebSocketClient.SendSensorDataAsync()// csharp public async Task SendSensorDataAsync(SensorData data) { // 复用缓冲区预先分配 256 字节足够容纳典型 SensorData var buffer ArrayPoolbyte.Shared.Rent(256); try { var span buffer.AsSpan(); var written data.WriteTo(span); // 返回实际写入字节数 await _webSocket.SendAsync( new ArraySegmentbyte(buffer, 0, written), WebSocketMessageType.Binary, true, CancellationToken.None); } finally { ArrayPoolbyte.Shared.Return(buffer); } }6.4 服务端反序列化Span 直接解析跳过字符串中间态服务端ProcessClient()中接收二进制消息后// csharp case WebSocketMessageType.Binary: var binaryBuffer new byte[8192]; var result await webSocket.ReceiveAsync( new ArraySegmentbyte(binaryBuffer), CancellationToken.None); // 直接解析 Span零分配 var sensorData SensorData.Parser.ParseFrom( new ReadOnlySpanbyte(binaryBuffer, 0, result.Count)); Log.Info($Received: {sensorData.Value} from {sensorData.DeviceId}); break;性能对比相同 20Hz 数据流下JSON 方案 Gen0 GC 频率 4.2/sPB 方案降至 0.3/s序列化耗时从平均 12μs 降至 2.1μsWireshark 显示单条消息体积从 68 字节JSON压缩至 22 字节PB。从那以后我每次做实时通信模块第一件事就是把WebSocketClient.SendAsync()替换为SendBinaryAsync()并强制走一遍 PB 序列化压测——哪怕初期只传一个int也要验证零拷贝路径是否畅通。因为真正的坑不在连接建立而在每秒上百次的消息搬运中悄然积累的 GC 延迟。希望帮到你。本文还有配套的精品资源点击获取