大家好,我是码农刚子。本文详解 WebSocket 全双工通信原理、前端 API 及 C# 服务端三种实现(ASP.NET Core/Fleck/HttpListener),涵盖心跳重连、安全扩展等工程实践,附完整源码助你快速上手实时应用开发。

1. 引言
在传统 Web 开发中,HTTP 协议一直是客户端与服务器交互的主流方式。但 HTTP 有一个天生的缺陷:它只能由客户端主动发起请求,服务器被动响应。这就导致服务器无法主动向客户端推送数据。为了实现实时通信(如聊天、股票行情、在线游戏等),开发者不得不借助轮询、长轮询等“模拟”方案,但这些方案要么浪费带宽,要么延迟较高。
WebSocket 的出现彻底解决了这个问题。它允许客户端和服务器之间建立一条持久、全双工的通信通道,双方可以随时向对方发送数据,真正实现了“即时通讯”。本文将带你从零开始,掌握 WebSocket 的核心概念、工作原理,并手把手实现一个完整的 WebSocket 应用——包含前端 HTML/JavaScript 以及 C# 后端服务端代码。
2. WebSocket 是什么?
WebSocket 是一种在单个 TCP 连接上提供全双工通信的协议,由 IETF 标准化为 RFC 6455。它设计用于 Web 浏览器和服务器之间的实时数据交换,但也适用于任何客户端-服务器场景。
简单类比:
- HTTP 就像写信:你写好信寄出去,对方收到后回信,你才能收到回复。每次通信都需要重新“寄信”。
- WebSocket 就像打电话:双方先拨通电话(建立连接),之后任何一方都可以随时说话,另一方随时收听,直到挂断电话(关闭连接)。
WebSocket 使用 ws:// 或 wss:// 作为协议前缀(wss 是加密版本,类似 HTTPS)。
3. 为什么需要 WebSocket?
传统 HTTP 在实时场景下面临三大痛点:
| 痛点 | 说明 |
|---|---|
| 单向请求 | 只有客户端能发起请求,服务器无法主动推送 |
| 短连接 | 每次请求都需要重新建立 TCP 连接(三次握手),开销大 |
| 实时性差 | 轮询(Polling)需要频繁发起请求,浪费带宽且延迟不可控 |
常见的“伪实时”方案:
- 短轮询:客户端每隔几秒就发一次请求,询问是否有新数据。缺点:大量无效请求,浪费资源。
- 长轮询:客户端发起请求后,服务器保持连接打开,直到有数据或超时才返回。缺点:占用连接资源,实现复杂。
WebSocket 通过一次握手建立持久连接,后续所有数据都通过该连接传输,帧头仅 2 字节,通信效率极高,延迟达到毫秒级。因此,它已成为实时 Web 应用的事实标准。
4. WebSocket 工作原理
WebSocket 通信分为两个阶段:握手阶段和数据传输阶段。
4.1 握手阶段
握手使用 HTTP 协议,客户端发送一个特殊的 GET 请求,包含 Upgrade: websocket 头,表明希望将协议升级为 WebSocket。服务器如果支持,则返回状态码 101 Switching Protocols,协议切换完成。此后,该 TCP 连接就不再是 HTTP 协议,而是 WebSocket 协议。
示例请求头:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
响应头:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
4.2 数据传输阶段
握手成功后,连接进入全双工模式。双方可以随时发送数据帧,帧格式轻量,支持文本(UTF-8)和二进制数据。通信保持持久,直到一方主动关闭连接或网络中断。
5. 前端 WebSocket API 及示例
浏览器原生提供了 WebSocket 对象,无需第三方库。
5.1 核心 API
// 创建连接
const socket = new WebSocket('ws://localhost:8080');
// 事件监听
socket.onopen = function() { /* 连接建立 */ };
socket.onmessage = function(event) { /* 收到消息,event.data 为数据 */ };
socket.onerror = function(error) { /* 出错 */ };
socket.onclose = function() { /* 连接关闭 */ };
// 发送数据
socket.send('Hello Server!');
// 关闭连接
socket.close();
5.2 完整前端示例
下面是一个简单的聊天界面 HTML,与任何 WebSocket 服务端兼容。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket 聊天示例</title>
<style>
#output { border: 1px solid #ccc; height: 300px; overflow-y: auto; padding: 10px; margin-bottom: 10px; }
.msg { margin: 5px 0; }
.self { color: blue; }
.server { color: green; }
</style>
</head>
<body>
<h2>WebSocket 聊天</h2>
<div id="output"></div>
<input id="input" type="text" placeholder="输入消息..." />
<button onclick="sendMessage()">发送</button>
<button onclick="closeConnection()">断开</button>
<script>
// 1. 创建连接(根据实际后端地址修改)
const socket = new WebSocket('ws://localhost:8080/ws');
// 2. 连接成功
socket.onopen = function() {
appendMessage('系统', '✅ 连接已建立', 'system');
};
// 3. 收到消息
socket.onmessage = function(event) {
appendMessage('服务器', event.data, 'server');
};
// 4. 错误处理
socket.onerror = function(error) {
console.error('WebSocket 错误:', error);
appendMessage('系统', '❌ 发生错误', 'system');
};
// 5. 连接关闭
socket.onclose = function() {
appendMessage('系统', '🔌 连接已关闭', 'system');
};
// 6. 发送消息
function sendMessage() {
const input = document.getElementById('input');
const text = input.value.trim();
if (text === '') return;
if (socket.readyState === WebSocket.OPEN) {
socket.send(text);
appendMessage('我', text, 'self');
input.value = '';
} else {
alert('连接未打开');
}
}
// 7. 关闭连接
function closeConnection() {
socket.close();
}
// 辅助:在界面中追加消息
function appendMessage(sender, text, type) {
const output = document.getElementById('output');
const div = document.createElement('div');
div.className = 'msg ' + (type || '');
div.textContent = `[${sender}] ${text}`;
output.appendChild(div);
output.scrollTop = output.scrollHeight;
}
// 回车发送
document.getElementById('input').addEventListener('keyup', function(e) {
if (e.key === 'Enter') sendMessage();
});
</script>
</body>
</html>
6. C# 后端 WebSocket 服务端实现
在 C# 中,我们有多种方式搭建 WebSocket 服务,下面分别介绍 ASP.NET Core 原生、轻量级 Fleck 和 HttpListener 自托管 三种方案,你可以根据项目需求选择。
6.1 方案一:ASP.NET Core 原生 WebSocket(推荐)
适用于 .NET Core / .NET 5+ 的 Web 应用程序,与 MVC/WebAPI 无缝集成。
步骤:
- 创建一个空的 ASP.NET Core Web 项目。
- 在
Program.cs中启用 WebSocket 中间件并定义/ws路由。
using System.Collections.Concurrent;
using System.Net.WebSockets;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// 配置 WebSocket
app.UseWebSockets(new WebSocketOptions
{
KeepAliveInterval = TimeSpan.FromSeconds(30),
ReceiveBufferSize = 4 * 1024
});
// 存储所有活跃连接(线程安全)
var connections = new ConcurrentDictionary<string, WebSocket>();
app.Map("/ws", async context =>
{
if (context.WebSockets.IsWebSocketRequest)
{
var webSocket = await context.WebSockets.AcceptWebSocketAsync();
var clientId = Guid.NewGuid().ToString();
connections.TryAdd(clientId, webSocket);
Console.WriteLine($"✅ 客户端 {clientId} 已连接");
await HandleWebSocket(webSocket, clientId, connections);
}
else
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
}
});
app.Run();
// 处理单个 WebSocket 连接
static async Task HandleWebSocket(
WebSocket ws,
string clientId,
ConcurrentDictionary<string, WebSocket> connections)
{
var buffer = new byte[1024 * 4];
WebSocketReceiveResult result;
while (ws.State == WebSocketState.Open)
{
try
{
result = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Text)
{
var receivedText = Encoding.UTF8.GetString(buffer, 0, result.Count);
Console.WriteLine($"📩 来自 {clientId}: {receivedText}");
// 回显消息
var reply = $"服务器回显: {receivedText}";
var replyBytes = Encoding.UTF8.GetBytes(reply);
await ws.SendAsync(
new ArraySegment<byte>(replyBytes),
WebSocketMessageType.Text,
true,
CancellationToken.None);
// 如果需要广播,可以调用 Broadcast 方法(见下文)
// await Broadcast($"{clientId}: {receivedText}", connections);
}
else if (result.MessageType == WebSocketMessageType.Close)
{
await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "正常关闭", CancellationToken.None);
}
}
catch (WebSocketException)
{
// 处理异常,如连接突然断开
break;
}
}
// 移除断开的连接
connections.TryRemove(clientId, out _);
Console.WriteLine($"🔌 客户端 {clientId} 已断开");
}
// 广播方法(可选)
static async Task Broadcast(string message, ConcurrentDictionary<string, WebSocket> connections)
{
var bytes = Encoding.UTF8.GetBytes(message);
var tasks = connections.Values
.Where(ws => ws.State == WebSocketState.Open)
.Select(ws => ws.SendAsync(
new ArraySegment<byte>(bytes),
WebSocketMessageType.Text,
true,
CancellationToken.None));
await Task.WhenAll(tasks);
}
启动:运行项目,服务端监听在 ws://localhost:5000/ws(默认端口)。
6.2 方案二:轻量级 Fleck 库
Fleck 是一个纯 C# 实现的 WebSocket 服务器,不依赖 ASP.NET Core,API 极其简洁,适合控制台应用、桌面程序或微服务。
安装:NuGet 包 Fleck
dotnet add package Fleck
基础回声服务器:
using Fleck;
var server = new WebSocketServer("ws://0.0.0.0:8181");
server.Start(socket =>
{
socket.OnOpen = () => Console.WriteLine("✅ 客户端已连接");
socket.OnClose = () => Console.WriteLine("🔌 客户端已断开");
socket.OnMessage = message =>
{
Console.WriteLine($"📩 收到: {message}");
socket.Send($"Echo: {message}");
};
});
Console.WriteLine("🚀 Fleck 服务器运行在 ws://localhost:8181");
Console.ReadLine();
广播聊天室版本:
var allSockets = new List<IWebSocketConnection>();
var server = new WebSocketServer("ws://0.0.0.0:8181");
server.Start(socket =>
{
socket.OnOpen = () =>
{
Console.WriteLine("✅ 新客户端连接");
allSockets.Add(socket);
};
socket.OnClose = () =>
{
Console.WriteLine("🔌 客户端断开");
allSockets.Remove(socket);
};
socket.OnMessage = message =>
{
Console.WriteLine($"📩 广播: {message}");
foreach (var s in allSockets.ToList())
{
s.Send(message); // 广播给所有人
}
};
});
Console.WriteLine("🚀 聊天室运行在 ws://localhost:8181");
Console.ReadLine();
Fleck 甚至支持 SSL(wss://),只需提供证书即可。
6.3 方案三:HttpListener 自托管
如果你不想依赖任何框架,可以使用 HttpListener 实现原生 WebSocket 支持(.NET Framework 4.5+ / .NET Core)。
using System.Net;
using System.Net.WebSockets;
using System.Text;
var listener = new HttpListener();
listener.Prefixes.Add("http://localhost:8080/");
listener.Start();
Console.WriteLine("🚀 HttpListener 服务器运行在 ws://localhost:8080");
while (true)
{
var context = await listener.GetContextAsync();
if (context.Request.IsWebSocketRequest)
{
var wsContext = await context.AcceptWebSocketAsync(null);
var ws = wsContext.WebSocket;
_ = HandleConnection(ws);
}
else
{
context.Response.StatusCode = 400;
context.Response.Close();
}
}
async Task HandleConnection(WebSocket ws)
{
var buffer = new byte[1024 * 4];
Console.WriteLine("✅ 客户端已连接");
while (ws.State == WebSocketState.Open)
{
var result = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Text)
{
var msg = Encoding.UTF8.GetString(buffer, 0, result.Count);
Console.WriteLine($"📩 收到: {msg}");
var reply = Encoding.UTF8.GetBytes($"Echo: {msg}");
await ws.SendAsync(new ArraySegment<byte>(reply), WebSocketMessageType.Text, true, CancellationToken.None);
}
else if (result.MessageType == WebSocketMessageType.Close)
{
await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "关闭", CancellationToken.None);
}
}
Console.WriteLine("🔌 客户端已断开");
}
7. 工程实践要点
7.1 心跳保活与自动重连
网络抖动可能导致连接意外断开,我们需要在客户端实现心跳和重连机制。常见的做法是客户端定时发送 ping 消息,服务端回复 pong,若超时未收到则触发重连。
以下是前端增强版客户端的核心逻辑(可结合前面的 HTML 使用):
class ReconnectingWebSocket {
constructor(url, options = {}) {
this.url = url;
this.heartbeatInterval = options.heartbeatInterval || 30000;
this.maxRetries = options.maxRetries || 5;
this.retryCount = 0;
this.ws = null;
this.heartbeatTimer = null;
this.reconnectTimer = null;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('✅ 连接成功');
this.retryCount = 0;
this.startHeartbeat();
this.onopen && this.onopen();
};
this.ws.onmessage = (e) => {
// 如果收到 pong,重置心跳计时器
if (e.data === 'pong') {
this.resetHeartbeat();
}
this.onmessage && this.onmessage(e);
};
this.ws.onclose = () => {
console.log('🔌 连接断开');
this.stopHeartbeat();
this.reconnect();
this.onclose && this.onclose();
};
this.ws.onerror = (e) => {
this.onerror && this.onerror(e);
};
}
startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send('ping');
}
}, this.heartbeatInterval);
}
resetHeartbeat() {
clearInterval(this.heartbeatTimer);
this.startHeartbeat();
}
stopHeartbeat() {
clearInterval(this.heartbeatTimer);
}
reconnect() {
if (this.retryCount >= this.maxRetries) {
console.log('❌ 重连次数已达上限');
return;
}
this.retryCount++;
const delay = Math.min(1000 * Math.pow(2, this.retryCount), 30000);
console.log(`🔄 将在 ${delay}ms 后第 ${this.retryCount} 次重连`);
this.reconnectTimer = setTimeout(() => this.connect(), delay);
}
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(data);
}
}
close() {
this.stopHeartbeat();
clearTimeout(this.reconnectTimer);
this.ws.close();
}
}
在服务端(以 ASP.NET Core 为例),可以在收到 "ping" 消息时回复 "pong":
if (receivedText == "ping")
{
await ws.SendAsync(Encoding.UTF8.GetBytes("pong"), WebSocketMessageType.Text, true, CancellationToken.None);
}
7.2 安全建议
- 使用 wss://:生产环境务必使用 TLS 加密,防止数据被窃听或篡改。
- 身份认证:在握手阶段,可以通过 URL 参数或
Sec-WebSocket-Protocol携带 JWT Token,服务端验证通过后才接受连接。 - 限流控制:对每个连接的消息频率进行限制,防止恶意客户端发送大量数据导致服务端资源耗尽。
- 输入校验:服务端接收到的消息必须进行校验和消毒,防止注入攻击(如 XSS)。
7.3 扩展性与负载均衡
当服务需要横向扩展(多实例)时,WebSocket 连接是状态化的,需要解决消息路由问题。常用方案:
- Redis Pub/Sub:当一个实例收到消息后,通过 Redis 发布,其他实例订阅并推送给各自连接的客户端。
- 消息队列:如 RabbitMQ、Kafka,用于广播或点对点消息。
- 粘性会话(Sticky Sessions):在负载均衡器(如 Nginx)上配置,将同一个客户端的请求始终转发到同一台服务器。
Nginx 配置示例:
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
7.4 连接管理
- 使用
ConcurrentDictionary存储连接,确保线程安全。 - 在连接关闭时及时清理资源。
- 设置合理的
ReceiveBufferSize和KeepAliveInterval。
8. 总结
通过本文,你已经学习了:
- WebSocket 的核心概念、优势及工作原理
- 浏览器端完整的 API 和示例代码
- C# 服务端的三种实现方式(ASP.NET Core、Fleck、HttpListener)
- 心跳、重连、安全、扩展等工程实践要点
WebSocket 技术极大地丰富了 Web 应用的实时交互能力,无论是在线聊天、协同编辑、游戏还是物联网,它都发挥着关键作用。希望这篇教程能帮助你快速上手,并在实际项目中灵活运用。