.NET8 实时通信秘籍:WebSocket 全双工通信 + 分布式推送,代码实操全解析

引言

在当今实时交互日益成为核心需求的网络应用生态中,传统的 HTTP 请求-响应模式已难以满足即时通讯、在线协作、实时数据推送等高互动性场景。为突破这一瓶颈,WebSocket 协议应运而生。

作为一种建立在 TCP 基础上的全双工通信协议,WebSocket 通过在客户端与服务器之间建立持久化的单一连接,实现了高效、低延迟的双向实时数据传输。它不仅克服了轮询与长轮询带来的延迟高、头部开销大、服务器负载重等固有缺陷,更以轻量级的帧结构取代了繁复的 HTTP 请求,真正让实时交互变得简洁而强大。从即时聊天、金融看板到协同编辑、在线游戏,WebSocket 已成为支撑现代实时 Web 应用的基石技术,持续推动着交互体验迈向真正的"实时"时代。

WebSocket是什么

  • 它是一种持久化连接协议:一旦客户端与服务器建立 WebSocket 连接,该连接将保持打开状态,直到显式关闭。
  • 基于 TCP,但与传统的 HTTP 不同,它不需要每次通信都重新建立连接。
  • 使用简单的握手过程(基于 HTTP 升级机制)从 HTTP 切换到 WebSocket 协议。
  • 数据可以以"消息"形式双向传输,支持文本(如 JSON)和二进制数据。

解决了什么问题

在 WebSocket 出现之前,Web 应用若想实现"实时通信",通常使用以下技术:

  1. 轮询(Polling):客户端定时向服务器发送请求,询问是否有新数据。
  2. 长轮询(Long Polling):客户端发送请求后,服务器保持连接直到有数据才返回。
  3. SSE(Server-Sent Events):仅支持服务器向客户端单向推送。

这些方法存在明显缺点

  • 延迟高:轮询无法做到真正实时
  • 资源浪费:频繁建立 HTTP 请求,开销大(头部信息多、连接反复创建)
  • 无法双向通信:SSE 仅支持服务器推;轮询仍是"请求-响应"模式。

优势与解决的问题

问题 WebSocket 如何解决
实时性差 建立持久连接,数据可即时推送,延迟极低
通信单向 支持客户端和服务器任意一方主动发送数据(全双工)
高开销 握手后通信头部极小,节省带宽和服务器资源
多次连接消耗 一个连接长期复用,减少 TCP 握手和 HTTP 开销

典型应用场景

  • 聊天应用(如微信网页版)
  • 在线协作文档编辑
  • 实时游戏
  • 股票行情、实时数据仪表盘
  • 视频弹幕系统

代码实践

创建websocket的中间件 WebSocketMiddleware

C# 复制代码
public class  `WebSocketMiddleware`
{
    private readonly RequestDelegate _next;
    private readonly WebSocketHandler _webSocketHandler;

    public WebSocketMiddleware(RequestDelegate next, WebSocketHandler webSocketHandler)
    {
        _next = next;
        _webSocketHandler = webSocketHandler;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        if (context.Request.Path == "/ws")
        {
            if (context.WebSockets.IsWebSocketRequest)
            {
                await _webSocketHandler.HandleWebSocketAsync(context);
            }
            else
            {
                context.Response.StatusCode = 400;
            }
        }
        else
        {
            await _next(context);
        }
    }
}

websocket处理类WebSocketHandler,从cookie中获取用户的登录信息来鉴权(预留代码)

C# 复制代码
public class WebSocketHandler
{
    private readonly WebSocketConnectionManager _connectionManager;

    public WebSocketHandler(WebSocketConnectionManager connectionManager)
    {
        _connectionManager = connectionManager;
    }

    public async Task HandleWebSocketAsync(HttpContext context)
    {
        // 从 Cookie 中获取身份信息
        var userInfo = await ValidateAndExtractUserInfo(context);
        if (userInfo == null)
        {
            context.Response.StatusCode = 401;
            return;
        }

        using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
        var connectionId = Guid.NewGuid().ToString();

        // 将连接与用户信息关联并存储
        _connectionManager.AddConnection(connectionId, webSocket, userInfo);

        try
        {
            await ProcessWebSocketMessages(connectionId, webSocket);
        }
        finally
        {
            // 确保连接断开时清理资源
            _connectionManager.RemoveConnection(connectionId);
        }
    }

    private async Task ValidateAndExtractUserInfo(HttpContext context)
    {
        // 从请求中提取 Cookie
        //if (!context.Request.Cookies.TryGetValue("auth_token", out var authToken))
        //{
        //    return null;
        //}

        // 验证身份信息(此处为预留实现)
        var userInfo = await ValidateAuthTokenAsync("authToken");
        return userInfo;
    }

    private async Task ValidateAuthTokenAsync(string authToken)
    {
        // 身份验证逻辑预留
        // 实际实现可能包括 JWT 解析、数据库查询等
        await Task.CompletedTask;

        // 示例返回,实际应根据验证结果返回
        return new UserInfo
        {
            Id = "user123",
            UserName = "zhangsan",
            ConnectedAt = DateTime.UtcNow
        };
    }

    private async Task ProcessWebSocketMessages(string connectionId, WebSocket webSocket)
    {
        var buffer = new byte[1024 * 4];

        while (webSocket.State == WebSocketState.Open)
        {
            var result = await webSocket.ReceiveAsync(
                new ArraySegment(buffer), CancellationToken.None);

            if (result.MessageType == WebSocketMessageType.Close)
            {
                await webSocket.CloseAsync(
                    WebSocketCloseStatus.NormalClosure,
                    "Connection closed by client",
                    CancellationToken.None);
                break;
            }

            // 处理收到的消息
            await HandleReceivedMessage(connectionId, buffer, result);
        }
    }

    private async Task HandleReceivedMessage(string connectionId, byte[] buffer, WebSocketReceiveResult result)
    {
        if (result.MessageType == WebSocketMessageType.Text)
        {
            string message = Encoding.UTF8.GetString(buffer, 0, result.Count);
            Console.WriteLine($"Received message from {connectionId}: {message}");
        }
        // 消息处理逻辑
        await Task.CompletedTask;
    }
}

WebSocketConnectionManager存储用户的websocket连接信息

C# 复制代码
public class WebSocketConnectionManager
{
    // 使用线程安全的字典存储用户连接
    private static readonly ConcurrentDictionary> _connections = new();

    public void AddConnection(string connectionId, WebSocket webSocket, UserInfo user)
    {

        if (_connections.ContainsKey(connectionId))
        {
            _connections[connectionId].Add(webSocket);
        }
        else
        {
            _connections.TryAdd(connectionId, new List() { webSocket });
        }
    }

    public void RemoveConnection(string connectionId)
    {
        _connections.TryRemove(connectionId, out _);
    }

    public List GetWebSocket(string connectionId)
    {
        if (!_connections.ContainsKey(connectionId))
        {
            return [];
        }
        return _connections.TryGetValue(connectionId, out var socket) ? socket : [];
    }

    public IEnumerable GetAllSockets()
    {
        return _connections.Values.SelectMany(list => list).ToList();
    }
}

program 配置

C# 复制代码
public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        // Add services to the container.

        builder.Services.AddControllers();
        // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
        builder.Services.AddEndpointsApiExplorer();
        builder.Services.AddSwaggerGen();

        // 注册WebSocketHandler与WebSocketConnectionManager到DI
        builder.Services.AddTransient();
        // WebSocketConnectionManager要使用单例模式,因为WebSocketConnectionManager中存储了所有WebSocket连接
        builder.Services.AddSingleton();

        var app = builder.Build();

        // Configure the HTTP request pipeline.
        if (app.Environment.IsDevelopment())
        {
            app.UseSwagger();
            app.UseSwaggerUI();
        }

        app.UseHttpsRedirection();

        app.UseAuthorization();

        app.MapControllers();
        // 启用websocket
        app.UseWebSockets();
        // 注册websocket中间件
        app.UseMiddleware();

        app.Run();
    }
}

前端调用

js 复制代码
    // 创建 WebSocket 连接
    const socket = new WebSocket('wss://localhost:7233/ws');

    // 连接打开时的处理
    socket.onopen = function(event) {
        console.log('WebSocket 连接已建立');
    };

    // 接收消息时的处理
    socket.onmessage = function(event) {
        console.log('收到消息:', event.data);
    };

    // 连接关闭时的处理
    socket.onclose = function(event) {
        console.log('WebSocket 连接已关闭');
    };

    // 发生错误时的处理
    socket.onerror = function(error) {
        console.error('WebSocket 错误:', error);
    };

    // 发送消息到服务器
    function sendMessage1() {
        const message = document.getElementById('message').value;
        console.log('发送消息:', message);
        if (socket.readyState === WebSocket.OPEN) {
            socket.send(JSON.stringify(message));
        }
    }

    // 关闭连接
    function closeConnection() {
        socket.close();
    }
    
    <div class="text-center">
   
    <h1 class="display-4">webSocket</h1>
    <br/>
    <div>
        
        
    </div>
</div>

实际效果

分布式系统中使用

在分布式系统架构中,为实现高效、可扩展的实时消息推送,结合 WebSocketRedis Pub/Sub 是一种经典且强大的设计模式。该模式的核心思路是通过 Redis 的发布订阅机制,解耦 WebSocket 连接所在的业务服务器与触发消息的事件源,从而实现跨进程、跨服务器的实时消息广播。

其工作流程可精炼为以下几个关键步骤:

  1. 连接建立与订阅 :当用户客户端与某台业务服务器成功建立 WebSocket 连接后,该服务器会立即以用户唯一标识(如 UserID)为频道名,向 Redis 发起订阅(SUBSCRIBE)。
  2. 统一的事件发布 :系统中任何服务(如业务逻辑服务、数据库变更监听器)在检测到与该用户相关的数据变化时,无需感知该用户实际连接在哪台服务器,只需向 Redis 对应的用户频道发布(PUBLISH)一条变更消息。
  3. 精准的消息路由与推送 :Redis 会将该消息推送至所有订阅了该频道的业务服务器。每台服务器在收到消息后,通过其内部维护的 用户-WebSocket 连接映射表,精准定位到当前连接在本机的该用户所有 WebSocket 会话,并即时将消息下发至对应的客户端。

这种架构的优势在于:

  • 解耦与扩展性:发布者与订阅者(WebSocket 服务器)完全解耦,便于系统水平扩展。
  • 精准推送:消息仅被推送给关联用户当前所在的服务节点,避免广播风暴。
  • 状态分散:各WebSocket服务器仅维护连接到自身的会话,无需全局中央存储,架构更清晰。

综上所述,通过 WebSocket 维持实时连接 ,并借助 Redis Pub/Sub 进行高效的消息分发,共同构建了一个适合分布式环境的实时通信基础设施,有力支撑了聊天、通知、实时数据同步等高并发实时场景。

相关推荐
Java编程爱好者几秒前
线程池用完不Shutdown,CPU和内存都快哭了
后端
神奇小汤圆15 分钟前
Unsafe魔法类深度解析:Java底层操作的终极指南
后端
神奇小汤圆1 小时前
浅析二叉树、B树、B+树和MySQL索引底层原理
后端
文艺理科生1 小时前
Nginx 路径映射深度解析:从本地开发到生产交付的底层哲学
前端·后端·架构
千寻girling1 小时前
主管:”人家 Node 框架都用 Nest.js 了 , 你怎么还在用 Express ?“
前端·后端·面试
南极企鹅1 小时前
springBoot项目有几个端口
java·spring boot·后端
Luke君607971 小时前
Spring Flux方法总结
后端
define95271 小时前
高版本 MySQL 驱动的 DNS 陷阱
后端
忧郁的Mr.Li2 小时前
SpringBoot中实现多数据源配置
java·spring boot·后端
暮色妖娆丶2 小时前
SpringBoot 启动流程源码分析 ~ 它其实不复杂
spring boot·后端·spring