大家好,我是码农刚子。本文详解 WebSocket 全双工通信原理、前端 API 及 C# 服务端三种实现(ASP.NET Core/Fleck/HttpListener),涵盖心跳重连、安全扩展等工程实践,附完整源码助你快速上手实时应用开发。

WebSocket 快速入门教程

1. 引言

在传统 Web 开发中,HTTP 协议一直是客户端与服务器交互的主流方式。但 HTTP 有一个天生的缺陷:它只能由客户端主动发起请求,服务器被动响应。这就导致服务器无法主动向客户端推送数据。为了实现实时通信(如聊天、股票行情、在线游戏等),开发者不得不借助轮询、长轮询等“模拟”方案,但这些方案要么浪费带宽,要么延迟较高。

WebSocket 的出现彻底解决了这个问题。它允许客户端和服务器之间建立一条持久、全双工的通信通道,双方可以随时向对方发送数据,真正实现了“即时通讯”。本文将带你从零开始,掌握 WebSocket 的核心概念、工作原理,并手把手实现一个完整的 WebSocket 应用——包含前端 HTML/JavaScript 以及 C# 后端服务端代码。

2. WebSocket 是什么?

WebSocket 是一种在单个 TCP 连接上提供全双工通信的协议,由 IETF 标准化为 RFC 6455。它设计用于 Web 浏览器和服务器之间的实时数据交换,但也适用于任何客户端-服务器场景。

简单类比:

  • HTTP 就像写信:你写好信寄出去,对方收到后回信,你才能收到回复。每次通信都需要重新“寄信”。
  • WebSocket 就像打电话:双方先拨通电话(建立连接),之后任何一方都可以随时说话,另一方随时收听,直到挂断电话(关闭连接)。

WebSocket 使用 ws://wss:// 作为协议前缀(wss 是加密版本,类似 HTTPS)。

3. 为什么需要 WebSocket?

传统 HTTP 在实时场景下面临三大痛点:

痛点 说明
单向请求 只有客户端能发起请求,服务器无法主动推送
短连接 每次请求都需要重新建立 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<byte>(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

8. 总结

通过本文,你已经学习了:

  • WebSocket 的核心概念、优势及工作原理
  • 浏览器端完整的 API 和示例代码
  • C# 服务端的三种实现方式(ASP.NET Core、Fleck、HttpListener)
  • 心跳、重连、安全、扩展等工程实践要点

WebSocket 技术极大地丰富了 Web 应用的实时交互能力,无论是在线聊天、协同编辑、游戏还是物联网,它都发挥着关键作用。希望这篇教程能帮助你快速上手,并在实际项目中灵活运用。