.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 进行高效的消息分发,共同构建了一个适合分布式环境的实时通信基础设施,有力支撑了聊天、通知、实时数据同步等高并发实时场景。

相关推荐
SEO-狼术2 小时前
Enable Secure Communication with TLS Support
.net
用户2190326527352 小时前
Java后端必须的Docker 部署 Redis 集群完整指南
linux·后端
VX:Fegn08952 小时前
计算机毕业设计|基于springboot + vue音乐管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·课程设计
bcbnb3 小时前
苹果手机iOS应用管理全指南与隐藏功能详解
后端
用户47949283569153 小时前
面试官:DNS 解析过程你能说清吗?DNS 解析全流程深度剖析
前端·后端·面试
开心猴爷3 小时前
iOS应用发布:App Store上架完整步骤与销售范围管理
后端
JSON_L3 小时前
Fastadmin API接口实现多语言提示语
后端·php·fastadmin
开心猴爷3 小时前
HTTPS和HTTP的区别及自定义证书使用教程
后端