|----------|--------------------------------|
| 短连接 | 每次请求都需要重新建立 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(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。