通过ASP.NET Core开发WebSocket服务器实现双向实时通信插图

通过ASP.NET Core开发WebSocket服务器:从零构建双向实时通信

你好,我是源码库的博主。在开发现代Web应用时,我们常常会遇到需要实时推送数据的场景,比如在线聊天室、实时数据仪表盘、协同编辑或是游戏。传统的HTTP请求-响应模式在这里就显得力不从心了。这时,WebSocket协议就成了我们的“利器”。今天,我就来手把手带你用ASP.NET Core搭建一个稳固的WebSocket服务器,并分享一些我实践中踩过的“坑”和心得。

WebSocket提供了全双工、长连接的通信通道,一旦握手建立,客户端和服务器就可以在任何时间互相发送数据,延迟极低。ASP.NET Core对WebSocket的支持非常友好,让我们可以专注于业务逻辑,而不用太操心底层的协议细节。

第一步:创建项目与基础配置

首先,我们创建一个新的ASP.NET Core Web API项目。你可以使用Visual Studio的向导,或者像我一样,在命令行里快速搞定:

dotnet new webapi -n WebSocketDemo
cd WebSocketDemo

接下来,我们需要显式地启用WebSocket支持。虽然在新版ASP.NET Core中,Kestrel服务器默认支持WebSocket,但在中间件管道中启用它仍然是必要步骤。打开 `Program.cs` 文件,在 `var app = builder.Build();` 之后,添加以下代码:

// ... builder.Build() 之后
app.UseWebSockets(); // 启用WebSocket中间件
// ... 之后才是 app.UseAuthorization() 和 app.MapControllers()

踩坑提示:`app.UseWebSockets()` 的调用顺序至关重要!它必须放在 `app.UseRouting()` 之后,但在 `app.UseEndpoints`(或像 `MapControllers` 这样的终端中间件)之前。如果顺序错了,WebSocket请求可能无法被正确拦截和处理。

第二步:创建自定义的WebSocket处理中间件

我们不打算把逻辑全塞在Controller里,创建一个独立的中间件会让结构更清晰,也更容易管理连接的生命周期。在项目根目录新建一个文件夹 `Middleware`,然后添加一个类 `WebSocketServerMiddleware.cs`。

这个中间件的核心是 `InvokeAsync` 方法。它会检查当前HTTP请求是否为WebSocket升级请求,如果是,则接受连接并开始通信循环。

using System.Net.WebSockets;
using System.Text;

namespace WebSocketDemo.Middleware;

public class WebSocketServerMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;

    public WebSocketServerMiddleware(RequestDelegate next, ILogger logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // 1. 判断是否为WebSocket请求
        if (context.WebSockets.IsWebSocketRequest)
        {
            _logger.LogInformation("收到WebSocket连接请求。");
            using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
            await HandleWebSocketConnection(webSocket);
        }
        else
        {
            // 如果不是WebSocket请求,交给管道中的下一个中间件
            await _next(context);
        }
    }

    private async Task HandleWebSocketConnection(WebSocket webSocket)
    {
        var buffer = new byte[1024 * 4]; // 4KB的缓冲区
        try
        {
            // 2. 循环接收消息
            while (webSocket.State == WebSocketState.Open)
            {
                var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None);

                if (result.MessageType == WebSocketMessageType.Text)
                {
                    // 3. 处理收到的文本消息
                    string receivedMessage = Encoding.UTF8.GetString(buffer, 0, result.Count);
                    _logger.LogInformation($"服务器收到消息: {receivedMessage}");

                    // 4. 构造回复(这里简单做回声)
                    string echoMessage = $"Echo: {receivedMessage} at {DateTime.Now:HH:mm:ss}";
                    var echoBuffer = Encoding.UTF8.GetBytes(echoMessage);
                    await webSocket.SendAsync(new ArraySegment(echoBuffer),
                                              WebSocketMessageType.Text,
                                              true, // 消息结束标志
                                              CancellationToken.None);
                }
                else if (result.MessageType == WebSocketMessageType.Close)
                {
                    // 5. 处理客户端发来的关闭请求
                    _logger.LogInformation("收到关闭帧,准备关闭连接。");
                    if (webSocket.State == WebSocketState.Open || webSocket.State == WebSocketState.CloseReceived)
                    {
                        await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure,
                                                   "Closed by the server",
                                                   CancellationToken.None);
                    }
                    break;
                }
            }
        }
        catch (WebSocketException ex)
        {
            _logger.LogError(ex, "WebSocket通信发生异常。");
            // 尝试优雅关闭
            if (webSocket.State == WebSocketState.Open)
            {
                await webSocket.CloseAsync(WebSocketCloseStatus.InternalServerError,
                                           "Server error",
                                           CancellationToken.None);
            }
        }
        finally
        {
            webSocket.Dispose();
            _logger.LogInformation("WebSocket连接已关闭并释放。");
        }
    }
}

实战经验:注意 `WebSocket.ReceiveAsync` 方法。它返回的 `WebSocketReceiveResult` 对象中的 `EndOfMessage` 属性很重要。对于文本或二进制消息,如果消息很大,可能会被分片传输。上面的例子为了简单,假设每次接收都是一个完整的消息(我们传递的缓冲区也足够大)。在生产环境中,你需要一个循环来累积数据,直到 `EndOfMessage` 为 `true`。

第三步:注册中间件并定义访问端点

中间件写好了,我们需要把它插入到请求管道中,并指定一个URL路径来监听WebSocket连接。回到 `Program.cs` 文件。

// ... 在 app.UseWebSockets(); 之后

// 将我们的自定义中间件映射到特定的路径,例如 “/ws”
app.Map("/ws", builder =>
{
    builder.UseMiddleware();
});

// ... 其他的Map或Run配置

这样,所有发送到 `ws://yourdomain/ws` 的连接请求,都会被我们的 `WebSocketServerMiddleware` 处理。

第四步:编写一个简单的HTML客户端进行测试

服务器端完成了,我们得有个客户端来测试。在 `wwwroot` 文件夹下(如果没有就新建一个),创建一个 `index.html` 文件。为了快速测试,我们还需要启用静态文件服务。在 `Program.cs` 中 `app.UseRouting()` 之前添加:

app.UseDefaultFiles(); // 默认寻找 index.html
app.UseStaticFiles();  // 启用静态文件服务

然后,编写我们的测试客户端HTML:




    WebSocket 客户端测试


    

ASP.NET Core WebSocket 测试




let socket = null; const output = document.getElementById('output'); const messageInput = document.getElementById('messageInput'); const btnSend = document.getElementById('btnSend'); const btnDisconnect = document.getElementById('btnDisconnect'); function log(msg) { output.innerHTML += `[${new Date().toLocaleTimeString()}] ${msg}n`; } function connect() { // 注意协议是 ws 或 wss (对应 http 和 https) const url = `ws://${window.location.host}/ws`; socket = new WebSocket(url); socket.onopen = function(e) { log("连接已建立!"); btnDisconnect.disabled = false; messageInput.disabled = false; btnSend.disabled = false; }; socket.onmessage = function(event) { log(`收到服务器回复: ${event.data}`); }; socket.onclose = function(event) { log(`连接关闭,代码: ${event.code}, 原因: ${event.reason}`); btnDisconnect.disabled = true; messageInput.disabled = true; btnSend.disabled = true; socket = null; }; socket.onerror = function(error) { log(`WebSocket 错误: ${error.message}`); }; } function send() { if (!socket || socket.readyState !== WebSocket.OPEN) { alert("连接未就绪!"); return; } const message = messageInput.value; if (!message.trim()) return; log(`发送: ${message}`); socket.send(message); messageInput.value = ''; } function disconnect() { if (socket) { socket.close(1000, "用户主动断开"); } }

第五步:运行与进阶思考

现在,一切就绪!在命令行运行 `dotnet run`,然后用浏览器打开 `https://localhost:xxxx` (或 `http://localhost:xxxx`),点击“连接”按钮。在输入框里发送消息,你应该会立刻看到服务器的回声回复。

踩坑提示:如果你在部署到生产环境(如IIS、Nginx反向代理后面)时遇到连接问题,很可能是因为代理服务器没有正确配置以支持WebSocket。对于Nginx,需要在 `location` 块中添加 `proxy_set_header Upgrade $http_upgrade;` 和 `proxy_set_header Connection "upgrade";` 等指令。

这个示例是一个简单的起点。在实际项目中,你还需要考虑:

  1. 连接管理:使用一个静态的 `ConcurrentDictionary` 来存储所有活跃的 `WebSocket` 对象,以便实现广播或向特定客户端发送消息。
  2. 心跳与超时:定期发送Ping/Pong帧来保持连接活跃,并检测死连接。
  3. 身份验证与授权:在 `AcceptWebSocketAsync` 之前,从 `HttpContext` 中验证用户身份(如通过JWT Token)。
  4. 消息协议:定义更复杂的消息格式(如JSON),并包含消息类型、发送者、目标等信息。

希望这篇教程能帮你顺利打开ASP.NET Core实时通信的大门。WebSocket的世界很有趣,动手试试,遇到问题多查文档和社区,你一定能构建出强大的实时应用。编码愉快!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。