WebSocket
一、WebSocket 基础
1. WebSocket 是什么?
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。
特点:
- ✅ 全双工通信 - 客户端和服务器可以同时发送数据
- ✅ 持久连接 - 建立连接后保持打开状态
- ✅ 低延迟 - 无需每次发送 HTTP 请求头
- ✅ 服务器推送 - 服务器可以主动向客户端推送数据
- ✅ 跨域友好 - 通过 Origin 头部验证来源
2. WebSocket vs HTTP
| 特性 | WebSocket | HTTP |
|---|---|---|
| 连接方式 | 持久连接 | 请求-响应后断开 |
| 通信方式 | 全双工 | 半双工 |
| 协议开销 | 小(只有数据帧) | 大(每次都有头部) |
| 服务器推送 | ✅ 支持 | ❌ 不支持(需要轮询) |
| 使用场景 | 实时通信、游戏 | REST API、网页 |
3. WebSocket 的优势
相比 HTTP 轮询:
- ✅ 减少网络开销
- ✅ 降低延迟
- ✅ 减少服务器压力
相比 HTTP 长轮询:
- ✅ 真正的双向通信
- ✅ 更低的延迟
- ✅ 更少的连接数
二、WebSocket 协议
1. WebSocket 握手
客户端请求:
http
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: http://example.com服务器响应:
http
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=握手后:
- 连接升级为 WebSocket 协议
- 使用二进制帧进行通信
- 不再使用 HTTP 协议
2. WebSocket 帧结构
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+-------------------------------+
| Extended payload length continued, if payload len == 127 |
+-+-+-+-+-------+-+-------------+-------------------------------+
| |Masking-key, if MASK set to 1 |
+-+-+-+-+-------+-+-------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-+-+-+-+-------+-+-------------+-------------------------------+关键字段:
- FIN - 是否最后一帧
- Opcode - 操作码(文本、二进制、关闭等)
- Mask - 是否掩码(客户端必须掩码)
- Payload length - 数据长度
- Masking-key - 掩码密钥(如果 Mask=1)
- Payload Data - 实际数据
Opcode 值:
0x0- 继续帧(Continuation)0x1- 文本帧(Text)0x2- 二进制帧(Binary)0x8- 关闭帧(Close)0x9- Ping 帧0xA- Pong 帧
3. WebSocket 状态
连接状态:
CONNECTING- 连接中(0)OPEN- 已打开(1)CLOSING- 关闭中(2)CLOSED- 已关闭(3)
三、WebSocket 使用
1. JavaScript 客户端
基本使用:
javascript
// 创建 WebSocket 连接
const ws = new WebSocket('ws://localhost:8080/chat');
// 连接打开
ws.onopen = () => {
console.log('连接已建立');
ws.send('Hello Server');
};
// 接收消息
ws.onmessage = (event) => {
console.log('收到消息:', event.data);
};
// 连接错误
ws.onerror = (error) => {
console.error('错误:', error);
};
// 连接关闭
ws.onclose = (event) => {
console.log('连接已关闭', event.code, event.reason);
};
// 发送消息
ws.send('Hello');
ws.send(JSON.stringify({ type: 'message', data: 'Hello' }));
// 发送二进制数据
const buffer = new ArrayBuffer(8);
ws.send(buffer);
// 关闭连接
ws.close();发送不同类型数据:
javascript
// 文本
ws.send('Hello');
// JSON
ws.send(JSON.stringify({ message: 'Hello' }));
// 二进制(ArrayBuffer)
const buffer = new ArrayBuffer(8);
ws.send(buffer);
// 二进制(Blob)
const blob = new Blob(['Hello']);
ws.send(blob);2. .NET Core 服务端
安装包:
bash
dotnet add package Microsoft.AspNetCore.WebSockets基本实现:
csharp
// Program.cs
app.UseWebSockets();
app.Map("/ws", async context =>
{
if (context.WebSockets.IsWebSocketRequest)
{
var webSocket = await context.WebSockets.AcceptWebSocketAsync();
await Echo(webSocket);
}
else
{
context.Response.StatusCode = 400;
}
});
async Task Echo(WebSocket webSocket)
{
var buffer = new byte[1024 * 4];
while (webSocket.State == WebSocketState.Open)
{
var result = await webSocket.ReceiveAsync(
new ArraySegment<byte>(buffer),
CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Text)
{
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
Console.WriteLine($"收到: {message}");
// 回显消息
var echo = $"Echo: {message}";
await webSocket.SendAsync(
new ArraySegment<byte>(Encoding.UTF8.GetBytes(echo)),
WebSocketMessageType.Text,
true,
CancellationToken.None);
}
else if (result.MessageType == WebSocketMessageType.Close)
{
await webSocket.CloseAsync(
WebSocketCloseStatus.NormalClosure,
"Close received",
CancellationToken.None);
}
}
}3. 处理不同类型消息
csharp
async Task HandleWebSocket(WebSocket webSocket)
{
var buffer = new byte[1024 * 4];
while (webSocket.State == WebSocketState.Open)
{
var result = await webSocket.ReceiveAsync(
new ArraySegment<byte>(buffer),
CancellationToken.None);
switch (result.MessageType)
{
case WebSocketMessageType.Text:
await HandleTextMessage(webSocket, buffer, result.Count);
break;
case WebSocketMessageType.Binary:
await HandleBinaryMessage(webSocket, buffer, result.Count);
break;
case WebSocketMessageType.Close:
await webSocket.CloseAsync(
WebSocketCloseStatus.NormalClosure,
"Close received",
CancellationToken.None);
break;
}
}
}
async Task HandleTextMessage(WebSocket webSocket, byte[] buffer, int count)
{
var message = Encoding.UTF8.GetString(buffer, 0, count);
var data = JsonSerializer.Deserialize<Message>(message);
// 处理消息
var response = ProcessMessage(data);
// 发送响应
var responseJson = JsonSerializer.Serialize(response);
await webSocket.SendAsync(
new ArraySegment<byte>(Encoding.UTF8.GetBytes(responseJson)),
WebSocketMessageType.Text,
true,
CancellationToken.None);
}四、WebSocket 高级特性
1. 心跳机制(Ping/Pong)
保持连接活跃:
csharp
// 服务端发送 Ping
_ = Task.Run(async () =>
{
while (webSocket.State == WebSocketState.Open)
{
await Task.Delay(30000); // 30秒
try
{
await webSocket.SendAsync(
new ArraySegment<byte>(new byte[] { 0x9, 0x0 }), // Ping 帧
WebSocketMessageType.Text,
true,
CancellationToken.None);
}
catch
{
// 连接已断开
break;
}
}
});客户端处理 Ping:
javascript
ws.on('ping', () => {
ws.pong();
});2. 消息队列
实现消息队列:
csharp
public class WebSocketManager
{
private readonly ConcurrentDictionary<string, WebSocket> _sockets = new();
private readonly ConcurrentQueue<Message> _messageQueue = new();
public void AddSocket(string id, WebSocket socket)
{
_sockets.TryAdd(id, socket);
}
public async Task SendToAllAsync(string message)
{
var buffer = Encoding.UTF8.GetBytes(message);
var tasks = _sockets.Values
.Where(s => s.State == WebSocketState.Open)
.Select(s => s.SendAsync(
new ArraySegment<byte>(buffer),
WebSocketMessageType.Text,
true,
CancellationToken.None));
await Task.WhenAll(tasks);
}
public async Task SendToAsync(string id, string message)
{
if (_sockets.TryGetValue(id, out var socket) &&
socket.State == WebSocketState.Open)
{
var buffer = Encoding.UTF8.GetBytes(message);
await socket.SendAsync(
new ArraySegment<byte>(buffer),
WebSocketMessageType.Text,
true,
CancellationToken.None);
}
}
}3. 房间/组管理
csharp
public class RoomManager
{
private readonly ConcurrentDictionary<string, HashSet<string>> _rooms = new();
private readonly ConcurrentDictionary<string, string> _userRooms = new();
public void JoinRoom(string userId, string roomId)
{
_rooms.AddOrUpdate(roomId,
new HashSet<string> { userId },
(key, set) => { set.Add(userId); return set; });
_userRooms[userId] = roomId;
}
public void LeaveRoom(string userId)
{
if (_userRooms.TryRemove(userId, out var roomId))
{
if (_rooms.TryGetValue(roomId, out var users))
{
users.Remove(userId);
if (users.Count == 0)
{
_rooms.TryRemove(roomId, out _);
}
}
}
}
public async Task BroadcastToRoomAsync(string roomId, string message)
{
if (_rooms.TryGetValue(roomId, out var users))
{
var tasks = users.Select(userId =>
_socketManager.SendToAsync(userId, message));
await Task.WhenAll(tasks);
}
}
}五、SignalR(ASP.NET Core)
1. SignalR 是什么?
SignalR 是 ASP.NET Core 提供的实时通信框架,基于 WebSocket,并提供降级方案。
特点:
- ✅ 自动选择最佳传输方式(WebSocket、Server-Sent Events、长轮询)
- ✅ 自动重连
- ✅ 支持客户端组
- ✅ 支持横向扩展(Redis、Azure SignalR)
2. SignalR 使用
安装包:
bash
dotnet add package Microsoft.AspNetCore.SignalR创建 Hub:
csharp
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
public async Task JoinRoom(string roomName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, roomName);
}
public async Task SendToRoom(string roomName, string message)
{
await Clients.Group(roomName).SendAsync("ReceiveMessage", message);
}
public override async Task OnConnectedAsync()
{
await Clients.All.SendAsync("UserConnected", Context.ConnectionId);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
await Clients.All.SendAsync("UserDisconnected", Context.ConnectionId);
await base.OnDisconnectedAsync(exception);
}
}配置:
csharp
// Program.cs
builder.Services.AddSignalR();
var app = builder.Build();
app.MapHub<ChatHub>("/chathub");
app.Run();客户端(JavaScript):
javascript
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chathub")
.build();
connection.on("ReceiveMessage", (user, message) => {
console.log(`${user}: ${message}`);
});
connection.start().then(() => {
connection.invoke("SendMessage", "Alice", "Hello");
});
connection.onclose(() => {
console.log("连接已关闭");
});3. SignalR 客户端组
csharp
public class ChatHub : Hub
{
public async Task JoinGroup(string groupName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
}
public async Task LeaveGroup(string groupName)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
}
public async Task SendToGroup(string groupName, string message)
{
await Clients.Group(groupName).SendAsync("ReceiveMessage", message);
}
}六、WebSocket 安全
1. 使用 WSS(WebSocket Secure)
WSS = WebSocket + TLS
javascript
// 使用 WSS
const ws = new WebSocket('wss://example.com/chat');服务器配置:
csharp
app.UseWebSockets();
// HTTPS 配置
app.UseHttpsRedirection();2. Origin 验证
csharp
app.UseWebSockets(new WebSocketOptions
{
AllowedOrigins = { "https://example.com", "https://www.example.com" }
});3. 认证和授权
csharp
app.UseAuthentication();
app.UseAuthorization();
app.Map("/ws", async context =>
{
if (!context.User.Identity.IsAuthenticated)
{
context.Response.StatusCode = 401;
return;
}
if (context.WebSockets.IsWebSocketRequest)
{
var webSocket = await context.WebSockets.AcceptWebSocketAsync();
await HandleWebSocket(webSocket, context.User);
}
});4. 速率限制
csharp
public class RateLimitedWebSocketMiddleware
{
private readonly ConcurrentDictionary<string, RateLimiter> _limiters = new();
public async Task InvokeAsync(HttpContext context)
{
var ip = context.Connection.RemoteIpAddress?.ToString();
var limiter = _limiters.GetOrAdd(ip, _ => new RateLimiter(100, TimeSpan.FromMinutes(1)));
if (!await limiter.TryAcquireAsync())
{
context.Response.StatusCode = 429;
return;
}
await _next(context);
}
}七、性能优化
1. 连接管理
连接池:
csharp
public class WebSocketPool
{
private readonly ConcurrentDictionary<string, WebSocket> _pool = new();
public async Task<WebSocket> GetOrCreateAsync(string key, Func<Task<WebSocket>> factory)
{
if (_pool.TryGetValue(key, out var socket) &&
socket.State == WebSocketState.Open)
{
return socket;
}
var newSocket = await factory();
_pool[key] = newSocket;
return newSocket;
}
}2. 消息压缩
csharp
// 压缩消息
var message = "Hello World";
var compressed = Compress(message);
await webSocket.SendAsync(
new ArraySegment<byte>(compressed),
WebSocketMessageType.Binary,
true,
CancellationToken.None);3. 批量发送
csharp
public async Task SendBatchAsync(List<string> messages)
{
var tasks = messages.Select(message =>
{
var buffer = Encoding.UTF8.GetBytes(message);
return webSocket.SendAsync(
new ArraySegment<byte>(buffer),
WebSocketMessageType.Text,
true,
CancellationToken.None);
});
await Task.WhenAll(tasks);
}八、常见面试题
Q1: WebSocket 和 HTTP 长轮询的区别?
| 特性 | WebSocket | HTTP 长轮询 |
|---|---|---|
| 连接 | 持久连接 | 每次请求保持一段时间 |
| 服务器推送 | ✅ 实时 | ⚠️ 伪推送(延迟) |
| 开销 | 小 | 大(每次都有 HTTP 头) |
| 实现复杂度 | 中等 | 简单 |
Q2: WebSocket 如何保持连接?
心跳机制:
- 使用 Ping/Pong 帧
- 定期发送 Ping,接收方响应 Pong
- 如果长时间没有收到响应,关闭连接
csharp
// 心跳检测
_ = Task.Run(async () =>
{
while (webSocket.State == WebSocketState.Open)
{
await Task.Delay(30000); // 30秒
await webSocket.SendAsync(
new ArraySegment<byte>(new byte[] { 0x9, 0x0 }), // Ping 帧
WebSocketMessageType.Text,
true,
CancellationToken.None);
}
});Q3: WebSocket 的安全性如何保证?
- 使用 WSS - WebSocket Secure(基于 TLS)
- 验证 Origin - 防止跨站请求伪造
- 认证 Token - 验证用户身份
- 速率限制 - 防止 DoS 攻击
Q4: WebSocket 如何实现负载均衡?
方案:
- 粘性会话(Sticky Session) - 同一客户端总是路由到同一服务器
- 消息总线 - 使用 Redis、RabbitMQ 等消息总线
- SignalR Backplane - SignalR 的横向扩展方案
csharp
// SignalR Redis Backplane
builder.Services.AddSignalR()
.AddStackExchangeRedis("localhost:6379", options =>
{
options.Configuration.ChannelPrefix = "MyApp";
});Q5: WebSocket 和 Server-Sent Events (SSE) 的区别?
| 特性 | WebSocket | SSE |
|---|---|---|
| 通信方式 | 全双工 | 单向(服务器→客户端) |
| 协议 | WebSocket | HTTP |
| 浏览器支持 | 现代浏览器 | 现代浏览器 |
| 使用场景 | 实时双向通信 | 服务器推送 |
九、最佳实践
- ✅ 使用 WSS - 生产环境必须使用加密
- ✅ 实现心跳 - 保持连接活跃
- ✅ 错误处理 - 正确处理连接错误
- ✅ 重连机制 - 连接断开时自动重连
- ✅ 消息队列 - 防止消息丢失
- ✅ 连接管理 - 限制连接数,防止资源耗尽
- ✅ 监控 - 监控连接数和消息量
- ✅ 认证授权 - 验证用户身份
- ❌ 不要忽略错误 - 正确处理异常
- ❌ 不要忽略安全 - 使用 WSS 和认证