WebSocket 快速入门教程(附示例源码)

|----------|--------------------------------|
| 短连接 | 每次请求都需要重新建立 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 原生轻量级 FleckHttpListener 自托管 三种方案,你可以根据项目需求选择。

6.1 方案一:ASP.NET Core 原生 WebSocket(推荐)

适用于 .NET Core / .NET 5+ 的 Web 应用程序,与 MVC/WebAPI 无缝集成。

步骤:

  1. 创建一个空的 ASP.NET Core Web 项目。

  2. 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 存储连接,确保线程安全。
  • 在连接关闭时及时清理资源。
  • 设置合理的 ReceiveBufferSizeKeepAliveInterval
相关推荐
xy34532 小时前
Wireshark捕获过滤器——语法元素详解
网络·测试工具·渗透测试·wireshark
DLYSB_3 小时前
生命通道:如何用 HIS 医疗系统直连网络声光终端,打造“零延误”的危急值响应网关?
java·网络·数据库·报警灯
德福危险4 小时前
富有想象力的XSS盲打:靶机练习之Tempus_fugit5
服务器·网络·xss
哥是强混5 小时前
Means:基于 .NET 10 打造的开源自部署 S3 兼容对象存储服务
网络·数据库·.net
Piko6146 小时前
锐捷 VSU 虚拟化堆叠配置
运维·网络·笔记
IpdataCloud7 小时前
AI生成的Avalon恶意框架怎么防?用IP离线库识别模块化C2通信
运维·人工智能·网络协议·tcp/ip·ip
爱研究的小梁8 小时前
乾元通多链路聚合通信主流落地应用案例
网络·信息与通信
Bobolink_8 小时前
跨境网络丢包优化实践,从5%到0.2%的排查与调整过程
网络·跨境网络·丢包优化
Piko6148 小时前
锐捷交换机 DHCP Server部署
运维·网络·笔记