WebSocket 深入
大约 12 分钟约 3644 字
WebSocket 深入
简介
WebSocket 提供在单个 TCP 连接上进行全双工通信的能力,客户端和服务端可以在任意时刻互相发送消息。ASP.NET Core 内置 WebSocket 中间件,支持原生 WebSocket 协议。与 HTTP 的请求-响应模型不同,WebSocket 是持久化连接,适合实时聊天、协同编辑、在线游戏、股票行情推送、IoT 设备通信等低延迟场景。深入理解 WebSocket 握手流程、消息帧格式、连接管理、心跳保活和广播机制,有助于在生产环境中构建可靠的实时通信系统。
特点
WebSocket 握手流程
协议升级过程
WebSocket 握手(HTTP 升级):
客户端 服务器
│ │
│── GET /ws HTTP/1.1 ─────────────→│
│ Host: localhost:5000 │
│ Upgrade: websocket │
│ Connection: Upgrade │
│ Sec-WebSocket-Key: dGhlIHNhbXBs│
│ Sec-WebSocket-Version: 13 │
│ │
│ 服务器验证 Sec-WebSocket-Key │
│ 计算: Base64(SHA1(Key + "258EA..."))│
│ │
│←── HTTP/1.1 101 Switching ──────│
│ Upgrade: websocket │
│ Connection: Upgrade │
│ Sec-WebSocket-Accept: s3pPL... │
│ │
│ ════════════════════════════════│
│ 连接建立完成,开始双向通信 │
│ ════════════════════════════════│
│ │
│←── {"type":"message","text":"Hi"}─│ 服务端发送
│── {"type":"message","text":"Hello"}→│ 客户端发送
│←── {"type":"message","text":"OK"}──│ 服务端发送
│ │
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 |
+-------------------------------- - - - - - - - - - - - - - - - +基础实现
WebSocket 中间件配置
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// 配置 WebSocket 选项
app.UseWebSockets(new WebSocketOptions
{
KeepAliveInterval = TimeSpan.FromSeconds(30), // 心跳间隔
// 注意:.NET 8 中一些高级选项需要通过代码配置
});
// 简单的 Echo WebSocket 端点
app.Map("/ws/echo", async (HttpContext context) =>
{
if (!context.WebSockets.IsWebSocketRequest)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
return;
}
using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
var buffer = new byte[1024 * 4];
var receiveResult = await webSocket.ReceiveAsync(buffer, CancellationToken.None);
while (!receiveResult.CloseStatus.HasValue)
{
// Echo:将收到的消息原样返回
await webSocket.SendAsync(
buffer[..receiveResult.Count],
receiveResult.MessageType,
receiveResult.EndOfMessage,
CancellationToken.None);
receiveResult = await webSocket.ReceiveAsync(buffer, CancellationToken.None);
}
// 客户端发起关闭
await webSocket.CloseAsync(
receiveResult.CloseStatus.Value,
receiveResult.CloseStatusDescription,
CancellationToken.None);
});
app.Run();接收完整消息
// WebSocket 消息可能被分片(EndOfMessage = false)
// 需要循环接收直到 EndOfMessage = true
public static async Task<string> ReceiveFullMessageAsync(
WebSocket webSocket,
byte[] buffer,
CancellationToken ct = default)
{
var messageBuilder = new StringBuilder();
WebSocketReceiveResult result;
do
{
result = await webSocket.ReceiveAsync(buffer, ct);
if (result.MessageType == WebSocketMessageType.Close)
{
throw new WebSocketException("客户端关闭了连接");
}
if (result.MessageType == WebSocketMessageType.Text)
{
var textChunk = Encoding.UTF8.GetString(buffer, 0, result.Count);
messageBuilder.Append(textChunk);
}
else if (result.MessageType == WebSocketMessageType.Binary)
{
// 二进制消息需要使用 MemoryStream 拼接
throw new NotSupportedException("二进制消息拼接未实现");
}
} while (!result.EndOfMessage);
return messageBuilder.ToString();
}
// 使用
app.Map("/ws/chat", async (HttpContext context) =>
{
if (!context.WebSockets.IsWebSocketRequest) return;
using var ws = await context.WebSockets.AcceptWebSocketAsync();
var buffer = new byte[1024 * 4];
while (ws.State == WebSocketState.Open)
{
try
{
var message = await ReceiveFullMessageAsync(ws, buffer);
Console.WriteLine($"收到消息: {message}");
var response = JsonSerializer.Serialize(new
{
type = "reply",
text = $"服务端收到: {message}",
timestamp = DateTime.UtcNow
});
var responseBytes = Encoding.UTF8.GetBytes(response);
await ws.SendAsync(responseBytes, WebSocketMessageType.Text, true, CancellationToken.None);
}
catch (WebSocketException)
{
break; // 连接断开
}
}
});连接管理与广播
WebSocket 管理器
// 线程安全的 WebSocket 连接管理器
public class WebSocketConnectionManager
{
// 连接字典:ConnectionId → (WebSocket, Metadata)
private readonly ConcurrentDictionary<string, WebSocketConnection> _connections = new();
/// <summary>
/// 添加新连接
/// </summary>
public string AddConnection(WebSocket webSocket, string? userId = null, string? room = null)
{
var connectionId = Guid.NewGuid().ToString("N")[..8];
var connection = new WebSocketConnection
{
Id = connectionId,
WebSocket = webSocket,
UserId = userId,
Room = room,
ConnectedAt = DateTime.UtcNow
};
_connections[connectionId] = connection;
return connectionId;
}
/// <summary>
/// 移除连接
/// </summary>
public void RemoveConnection(string connectionId)
{
_connections.TryRemove(connectionId, out _);
}
/// <summary>
/// 获取所有活跃连接
/// </summary>
public IReadOnlyCollection<WebSocketConnection> GetAllConnections()
=> _connections.Values.Where(c => c.WebSocket.State == WebSocketState.Open).ToList();
/// <summary>
/// 获取指定房间的连接
/// </summary>
public IReadOnlyCollection<WebSocketConnection> GetRoomConnections(string room)
=> _connections.Values
.Where(c => c.WebSocket.State == WebSocketState.Open && c.Room == room)
.ToList();
/// <summary>
/// 广播消息到所有连接
/// </summary>
public async Task<int> BroadcastAsync(string message, CancellationToken ct = default)
{
var bytes = Encoding.UTF8.GetBytes(message);
var sentCount = 0;
var deadConnections = new List<string>();
foreach (var (id, conn) in _connections)
{
if (conn.WebSocket.State != WebSocketState.Open)
{
deadConnections.Add(id);
continue;
}
try
{
await conn.WebSocket.SendAsync(bytes, WebSocketMessageType.Text, true, ct);
sentCount++;
}
catch (WebSocketException)
{
deadConnections.Add(id);
}
}
// 清理已断开的连接
foreach (var id in deadConnections)
{
_connections.TryRemove(id, out _);
}
return sentCount;
}
/// <summary>
/// 发送消息到指定房间
/// </summary>
public async Task<int> SendToRoomAsync(string room, string message, CancellationToken ct = default)
{
var bytes = Encoding.UTF8.GetBytes(message);
var sentCount = 0;
foreach (var conn in GetRoomConnections(room))
{
try
{
await conn.WebSocket.SendAsync(bytes, WebSocketMessageType.Text, true, ct);
sentCount++;
}
catch (WebSocketException)
{
RemoveConnection(conn.Id);
}
}
return sentCount;
}
/// <summary>
/// 发送消息到指定用户
/// </summary>
public async Task<bool> SendToUserAsync(string userId, string message, CancellationToken ct = default)
{
var connection = _connections.Values
.FirstOrDefault(c => c.UserId == userId && c.WebSocket.State == WebSocketState.Open);
if (connection == null) return false;
var bytes = Encoding.UTF8.GetBytes(message);
await connection.WebSocket.SendAsync(bytes, WebSocketMessageType.Text, true, ct);
return true;
}
public int ConnectionCount => _connections.Count;
}
public class WebSocketConnection
{
public string Id { get; set; } = "";
public WebSocket WebSocket { get; set; } = null!;
public string? UserId { get; set; }
public string? Room { get; set; }
public DateTime ConnectedAt { get; set; }
}聊天室实现
// 注册管理器
builder.Services.AddSingleton<WebSocketConnectionManager>();
// 聊天 WebSocket 端点
app.Map("/ws/chat", async (HttpContext context, WebSocketConnectionManager manager) =>
{
if (!context.WebSockets.IsWebSocketRequest)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
return;
}
using var ws = await context.WebSockets.AcceptWebSocketAsync();
var buffer = new byte[1024 * 4];
var connectionId = manager.AddConnection(ws);
Console.WriteLine($"[WebSocket] 新连接: {connectionId},当前连接数: {manager.ConnectionCount}");
try
{
while (ws.State == WebSocketState.Open)
{
var message = await ReceiveFullMessageAsync(ws, buffer);
// 解析消息
try
{
var msg = JsonSerializer.Deserialize<ChatMessage>(message);
if (msg == null) continue;
switch (msg.Type)
{
case "join":
// 加入房间
var conn = manager.GetAllConnections().FirstOrDefault(c => c.Id == connectionId);
if (conn != null) conn.Room = msg.Room ?? "default";
var joinNotice = JsonSerializer.Serialize(new
{
type = "system",
text = "用户加入了房间",
room = msg.Room,
timestamp = DateTime.UtcNow
});
await manager.SendToRoomAsync(msg.Room ?? "default", joinNotice);
break;
case "message":
// 转发消息到房间
var chatMsg = JsonSerializer.Serialize(new
{
type = "message",
text = msg.Text,
from = msg.From ?? "anonymous",
room = msg.Room,
timestamp = DateTime.UtcNow
});
await manager.SendToRoomAsync(msg.Room ?? "default", chatMsg);
break;
case "leave":
var leaveNotice = JsonSerializer.Serialize(new
{
type = "system",
text = "用户离开了房间",
timestamp = DateTime.UtcNow
});
await manager.SendToRoomAsync(msg.Room ?? "default", leaveNotice);
break;
}
}
catch (JsonException)
{
// 忽略格式错误的消息
var errorMsg = JsonSerializer.Serialize(new
{
type = "error",
text = "消息格式错误"
});
var errorBytes = Encoding.UTF8.GetBytes(errorMsg);
await ws.SendAsync(errorBytes, WebSocketMessageType.Text, true);
}
}
}
catch (WebSocketException)
{
// 连接断开
}
finally
{
manager.RemoveConnection(connectionId);
Console.WriteLine($"[WebSocket] 连接断开: {connectionId},当前连接数: {manager.ConnectionCount}");
}
});
public record ChatMessage(string Type, string? Text = null, string? From = null, string? Room = null);心跳保活
Ping/Pong 机制
// WebSocket 协议内置 Ping/Pong 机制
// 服务端发送 Ping 帧,客户端自动回复 Pong 帧
// 如果超过一定时间未收到 Pong,说明连接已断开
// 方式 1:使用内置的 KeepAliveInterval(推荐)
app.UseWebSockets(new WebSocketOptions
{
KeepAliveInterval = TimeSpan.FromSeconds(30) // 每 30 秒发送一次 Ping
});
// 注意:ASP.NET Core 的 WebSocket 实现会自动发送 Ping
// 客户端(浏览器)会自动回复 Pong
// 如果 Pong 未在超时时间内返回,连接会被关闭
// 方式 2:手动实现心跳(自定义控制)
public class WebSocketHeartbeatMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<WebSocketHeartbeatMiddleware> _logger;
public WebSocketHeartbeatMiddleware(RequestDelegate next, ILogger<WebSocketHeartbeatMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
if (!context.WebSockets.IsWebSocketRequest)
{
await _next(context);
return;
}
using var ws = await context.WebSockets.AcceptWebSocketAsync();
// 启动心跳任务
using var cts = new CancellationTokenSource();
var heartbeatTask = SendHeartbeatAsync(ws, cts.Token);
var receiveTask = ReceiveMessagesAsync(ws, context, cts.Token);
// 等待任一任务完成
var completedTask = await Task.WhenAny(heartbeatTask, receiveTask);
cts.Cancel(); // 取消另一个任务
try
{
await Task.WhenAll(heartbeatTask, receiveTask);
}
catch (OperationCanceledException) { }
}
private async Task SendHeartbeatAsync(WebSocket ws, CancellationToken ct)
{
while (!ct.IsCancellationRequested && ws.State == WebSocketState.Open)
{
try
{
await Task.Delay(TimeSpan.FromSeconds(30), ct);
// 发送 Ping 帧
await ws.SendAsync(
Array.Empty<byte>(),
WebSocketMessageType.Ping,
true,
ct);
_logger.LogDebug("[WebSocket] Ping 已发送");
}
catch (OperationCanceledException) { break; }
catch (WebSocketException)
{
_logger.LogWarning("[WebSocket] Ping 发送失败,连接可能已断开");
break;
}
}
}
private async Task ReceiveMessagesAsync(WebSocket ws, HttpContext context, CancellationToken ct)
{
var buffer = new byte[1024 * 4];
while (!ct.IsCancellationRequested && ws.State == WebSocketState.Open)
{
var result = await ws.ReceiveAsync(buffer, ct);
// 处理消息...
}
}
}消息协议设计
JSON 消息协议
// 标准消息格式
public class WsMessage
{
public string Type { get; set; } = ""; // 消息类型
public string? Id { get; set; } // 消息 ID(用于确认)
public object? Data { get; set; } // 消息数据
public long? Timestamp { get; set; } // 时间戳
public string? From { get; set; } // 发送者
public string? To { get; set; } // 接收者(null = 广播)
public string? Room { get; set; } // 目标房间
}
// 消息类型枚举
public static class WsMessageTypes
{
// 系统消息
public const string Connect = "connect";
public const string Disconnect = "disconnect";
public const string Heartbeat = "heartbeat";
public const string Error = "error";
// 业务消息
public const string Chat = "chat";
public const string Notification = "notification";
public const string Presence = "presence"; // 在线状态
public const string Typing = "typing"; // 正在输入
// 命令消息
public const string JoinRoom = "join_room";
public const string LeaveRoom = "leave_room";
public const string Subscribe = "subscribe";
public const string Unsubscribe = "unsubscribe";
}
// 消息处理器
public class WsMessageHandler
{
private readonly WebSocketConnectionManager _manager;
private readonly ILogger<WsMessageHandler> _logger;
public WsMessageHandler(WebSocketConnectionManager manager, ILogger<WsMessageHandler> logger)
{
_manager = manager;
_logger = logger;
}
public async Task HandleAsync(string connectionId, string rawMessage)
{
WsMessage? message;
try
{
message = JsonSerializer.Deserialize<WsMessage>(rawMessage);
}
catch (JsonException)
{
await _manager.SendToUserAsync(
GetUserId(connectionId),
CreateError("消息格式错误"));
return;
}
if (message == null) return;
switch (message.Type)
{
case WsMessageTypes.JoinRoom:
await HandleJoinRoom(connectionId, message);
break;
case WsMessageTypes.Chat:
await HandleChat(connectionId, message);
break;
case WsMessageTypes.Heartbeat:
await HandleHeartbeat(connectionId);
break;
default:
_logger.LogWarning("未知消息类型: {Type}", message.Type);
break;
}
}
private async Task HandleJoinRoom(string connectionId, WsMessage message)
{
var room = message.Room ?? "default";
var conn = _manager.GetAllConnections().FirstOrDefault(c => c.Id == connectionId);
if (conn != null) conn.Room = room;
var response = CreateSystemMessage($"已加入房间: {room}");
await _manager.SendToRoomAsync(room, response);
}
private async Task HandleChat(string connectionId, WsMessage message)
{
var conn = _manager.GetAllConnections().FirstOrDefault(c => c.Id == connectionId);
if (conn?.Room == null) return;
var response = JsonSerializer.Serialize(new
{
type = WsMessageTypes.Chat,
data = message.Data,
from = conn.UserId ?? connectionId,
timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
});
await _manager.SendToRoomAsync(conn.Room, response);
}
private async Task HandleHeartbeat(string connectionId)
{
// 客户端心跳响应,更新连接活跃时间
var conn = _manager.GetAllConnections().FirstOrDefault(c => c.Id == connectionId);
if (conn != null)
{
// 可以在这里更新活跃时间
}
}
private string CreateError(string text) => JsonSerializer.Serialize(new { type = "error", text });
private string CreateSystemMessage(string text) => JsonSerializer.Serialize(new { type = "system", text });
private string GetUserId(string connectionId) => connectionId; // 简化实现
}客户端实现
JavaScript 客户端
class WebSocketClient {
constructor(url, options = {}) {
this.url = url;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = options.maxReconnectAttempts || 10;
this.reconnectInterval = options.reconnectInterval || 3000;
this.heartbeatInterval = options.heartbeatInterval || 30000;
this.messageHandlers = new Map();
this.ws = null;
this.heartbeatTimer = null;
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('[WebSocket] 连接建立');
this.reconnectAttempts = 0;
this.startHeartbeat();
this.emit('connected');
};
this.ws.onmessage = (event) => {
const message = JSON.parse(event.data);
this.emit(message.type, message);
};
this.ws.onclose = (event) => {
console.log(`[WebSocket] 连接关闭: code=${event.code}, reason=${event.reason}`);
this.stopHeartbeat();
this.emit('disconnected', { code: event.code });
if (this.reconnectAttempts < this.maxReconnectAttempts) {
const delay = this.reconnectInterval * Math.pow(1.5, this.reconnectAttempts);
console.log(`[WebSocket] ${delay}ms 后重连 (第 ${this.reconnectAttempts + 1} 次)`);
setTimeout(() => {
this.reconnectAttempts++;
this.connect();
}, delay);
}
};
this.ws.onerror = (error) => {
console.error('[WebSocket] 错误:', error);
this.emit('error', error);
};
}
send(type, data) {
if (this.ws?.readyState !== WebSocket.OPEN) {
console.warn('[WebSocket] 连接未就绪');
return;
}
this.ws.send(JSON.stringify({ type, data, timestamp: Date.now() }));
}
on(type, handler) {
if (!this.messageHandlers.has(type)) {
this.messageHandlers.set(type, []);
}
this.messageHandlers.get(type).push(handler);
}
emit(type, data) {
const handlers = this.messageHandlers.get(type) || [];
handlers.forEach(h => h(data));
}
startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
this.send('heartbeat', { timestamp: Date.now() });
}, this.heartbeatInterval);
}
stopHeartbeat() {
clearInterval(this.heartbeatTimer);
}
disconnect() {
this.stopHeartbeat();
this.maxReconnectAttempts = 0; // 阻止自动重连
this.ws?.close();
}
}
// 使用
const client = new WebSocketClient('ws://localhost:5000/ws/chat');
client.on('connected', () => {
client.send('join_room', { room: 'general' });
});
client.on('chat', (message) => {
console.log(`${message.from}: ${message.data}`);
});
client.on('system', (message) => {
console.log(`[系统] ${message.text}`);
});
client.connect();
// 发送消息
document.getElementById('sendBtn').addEventListener('click', () => {
const text = document.getElementById('messageInput').value;
client.send('chat', { text });
});生产环境考量
Nginx 反向代理配置
# WebSocket 需要 HTTP 升级支持
upstream websocket_backend {
server localhost:5000;
}
server {
listen 80;
server_name example.com;
location /ws/ {
proxy_pass http://websocket_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade; # 关键:传递 Upgrade 头
proxy_set_header Connection "upgrade"; # 关键:传递 Connection 头
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400s; # 超时设为 24 小时
proxy_send_timeout 86400s;
}
}连接数限制与监控
// 监控端点
app.MapGet("/ws/stats", (WebSocketConnectionManager manager) =>
{
var connections = manager.GetAllConnections();
return Results.Ok(new
{
totalConnections = manager.ConnectionCount,
activeConnections = connections.Count,
rooms = connections
.GroupBy(c => c.Room ?? "default")
.Select(g => new { room = g.Key, count = g.Count() })
.ToList(),
uptime = DateTime.UtcNow
});
});
// Kestrel 连接限制
builder.WebHost.ConfigureKestrel(options =>
{
options.Limits.MaxConcurrentConnections = 10000;
options.Limits.MaxConcurrentUpgradedConnections = 10000; // WebSocket 升级连接
});优点
缺点
总结
ASP.NET Core 使用 UseWebSockets() 启用 WebSocket 中间件,KeepAliveInterval 配置心跳间隔。连接管理器用 ConcurrentDictionary 管理连接,支持广播、房间消息和定向消息。消息协议建议使用 JSON 格式,包含 type、data、from、to、room 等字段。客户端需要自行实现重连(指数退避)和心跳机制。生产环境注意 Nginx 需要配置 Upgrade 和 Connection 头。单服务器有连接数上限,多服务器场景需要 Redis Pub/Sub 或消息队列做跨节点广播,或考虑使用 SignalR 封装这些复杂度。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
- 框架能力的真正重点是它在请求链路中的位置和对上下游的影响。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
- 画清执行顺序、入参来源、失败返回和日志记录点。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
- 知道 API 名称,却不知道它应该放在请求链路的哪个位置。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
- 继续补齐协议选型、网关治理、端点可观测性和契约演进策略。
适用场景
- 当你准备把《WebSocket 深入》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
复盘问题
- 如果把《WebSocket 深入》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《WebSocket 深入》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《WebSocket 深入》最大的收益和代价分别是什么?
