Skip to content

WebSocket

一、WebSocket 基础

1. WebSocket 是什么?

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。

特点:

  • 全双工通信 - 客户端和服务器可以同时发送数据
  • 持久连接 - 建立连接后保持打开状态
  • 低延迟 - 无需每次发送 HTTP 请求头
  • 服务器推送 - 服务器可以主动向客户端推送数据
  • 跨域友好 - 通过 Origin 头部验证来源

2. WebSocket vs HTTP

特性WebSocketHTTP
连接方式持久连接请求-响应后断开
通信方式全双工半双工
协议开销小(只有数据帧)大(每次都有头部)
服务器推送✅ 支持❌ 不支持(需要轮询)
使用场景实时通信、游戏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 长轮询的区别?

特性WebSocketHTTP 长轮询
连接持久连接每次请求保持一段时间
服务器推送✅ 实时⚠️ 伪推送(延迟)
开销大(每次都有 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 的安全性如何保证?

  1. 使用 WSS - WebSocket Secure(基于 TLS)
  2. 验证 Origin - 防止跨站请求伪造
  3. 认证 Token - 验证用户身份
  4. 速率限制 - 防止 DoS 攻击

Q4: WebSocket 如何实现负载均衡?

方案:

  1. 粘性会话(Sticky Session) - 同一客户端总是路由到同一服务器
  2. 消息总线 - 使用 Redis、RabbitMQ 等消息总线
  3. SignalR Backplane - SignalR 的横向扩展方案
csharp
// SignalR Redis Backplane
builder.Services.AddSignalR()
    .AddStackExchangeRedis("localhost:6379", options =>
    {
        options.Configuration.ChannelPrefix = "MyApp";
    });

Q5: WebSocket 和 Server-Sent Events (SSE) 的区别?

特性WebSocketSSE
通信方式全双工单向(服务器→客户端)
协议WebSocketHTTP
浏览器支持现代浏览器现代浏览器
使用场景实时双向通信服务器推送

九、最佳实践

  1. 使用 WSS - 生产环境必须使用加密
  2. 实现心跳 - 保持连接活跃
  3. 错误处理 - 正确处理连接错误
  4. 重连机制 - 连接断开时自动重连
  5. 消息队列 - 防止消息丢失
  6. 连接管理 - 限制连接数,防止资源耗尽
  7. 监控 - 监控连接数和消息量
  8. 认证授权 - 验证用户身份
  9. 不要忽略错误 - 正确处理异常
  10. 不要忽略安全 - 使用 WSS 和认证

基于 VitePress 构建 | Copyright © 2026-present