基于WebSocket实现的后台服务

基于WebSocket实现的后台服务,用于接收客户端的心跳消息,并根据心跳消息来维护客户端连接。

具体实现中,服务启动后会创建一个HttpListener对象,用于监听客户端的WebSocket连接请求。当客户端连接成功后,服务会为每个连接创建一个Task实例,用于接收客户端发送的心跳消息,并根据心跳消息更新心跳时间戳。服务还会定期向客户端发送心跳消息,以保持连接的活跃状态。

如果服务在一定时间内没有收到客户端发送的心跳消息,就会认为客户端已经掉线,服务会关闭连接并从连接列表中移除该客户端。

此服务适用于需要实现长连接的场景,例如实时消息推送、在线游戏等。需要注意的是,此服务只能用于WebSocket通信,客户端必须实现WebSocket协议。

cs 复制代码
using Microsoft.Extensions.Hosting;
using MSEBP.Kernel.Common.Logging;
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Authorization.WebApi
{
    /// <summary>
    /// 此代码只能用于 websocket通信,客户端必须websocket实现,暂时无用。
    /// </summary>
    public class WebSocketBackgroundService : IHostedService, IDisposable
    {
        private const int _heartBeatInterval = 30000; // 心跳间隔(毫秒)
        private const int _heartBeatTimeout = 60000; // 心跳超时时间(毫秒)
        private const int _clientIdLength = 10;

        private readonly CancellationTokenSource _cts = new CancellationTokenSource();
        private readonly ConcurrentDictionary<string, WebSocket> _clients = new ConcurrentDictionary<string, WebSocket>();
        private readonly ILogger _logger;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="logger"></param>
        public WebSocketBackgroundService(ILogger logger)
        {
            _logger = logger;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public async Task StartAsync(CancellationToken cancellationToken)
        {
            IPAddress localIp = Dns.GetHostEntry(Dns.GetHostName()).AddressList.FirstOrDefault(ip => ip.AddressFamily == AddressFamily.InterNetwork);
            if (localIp == null)
            {
                throw new Exception("Cannot find local IP address.");
            }

            IPEndPoint localEndPoint = new IPEndPoint(localIp, 8181);
            HttpListener listener = new HttpListener();
            //listener.Prefixes.Add($"http://{localEndPoint}/");
            listener.Start();

            _ = Task.Run(async () =>
            {
                try
                {
                    while (!_cts.IsCancellationRequested)
                    {
                        HttpListenerContext context = await listener.GetContextAsync();
                        if (context.Request.IsWebSocketRequest)
                        {
                            WebSocket webSocket = await AcceptWebSocketAsync(context);
                            _ = Task.Run(async () =>
                            {
                                await ReceiveHeartbeatAsync(webSocket);
                            }, _cts.Token);
                        }
                        else
                        {
                            context.Response.StatusCode = 400;
                            context.Response.Close();
                        }
                    }
                }
                catch (Exception ex)
                {
                    _logger.Error(ex, "WebSocket server error.");
                }
            }, _cts.Token);
        }

        private async Task<WebSocket> AcceptWebSocketAsync(HttpListenerContext context)
        {
            HttpListenerWebSocketContext wsContext = await context.AcceptWebSocketAsync(null);
            WebSocket webSocket = wsContext.WebSocket;
            return webSocket;
        }

        private async Task ReceiveHeartbeatAsync(WebSocket webSocket)
        {
            byte[] buffer = new byte[1024];
            CancellationToken token = _cts.Token;
            DateTime lastHeartbeatTime = DateTime.UtcNow;

            try
            {
                while (webSocket.State == WebSocketState.Open && !token.IsCancellationRequested)
                {
                    WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);

                    if (result.CloseStatus.HasValue)
                    {
                        await CloseWebSocketAsync(webSocket, result.CloseStatus.Value, result.CloseStatusDescription);
                        break;
                    }
                    else if (result.MessageType == WebSocketMessageType.Text)
                    {
                        string message = Encoding.UTF8.GetString(buffer, 0, result.Count).Trim();
                        if (message.StartsWith("heartbeat"))
                        {
                            lastHeartbeatTime = DateTime.UtcNow;
                            string clientId = message.Substring(0, Math.Min(message.Length, _clientIdLength));
                            _clients.TryAdd(clientId, webSocket);
                        }
                        else if (string.IsNullOrEmpty(message))
                        {
                            await CloseWebSocketAsync(webSocket, WebSocketCloseStatus.NormalClosure, "Closed by client");
                            break;
                        }
                        else
                        {
                            // 处理业务逻辑
                        }
                    }

                    // 检测心跳超时
                    if ((DateTime.UtcNow - lastHeartbeatTime).TotalMilliseconds > _heartBeatTimeout) 
                    { 
                        await CloseWebSocketAsync(webSocket, WebSocketCloseStatus.NormalClosure, "Heartbeat timeout");
                        break;
                    }
                }
            }
            catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely)
            {
                // WebSocket 连接被意外关闭,忽略异常
            }
            catch (Exception ex)
            {
                _logger.Error(ex, "WebSocket error.");
            }
            finally
            {
                // 移除客户端连接
                foreach (var item in _clients)
                {
                    if (item.Value == webSocket)
                    {
                        _clients.TryRemove(item.Key, out _);
                        break;
                    }
                }
                await CloseWebSocketAsync(webSocket, WebSocketCloseStatus.NormalClosure, "Closed by server");
            }
        }

        private async Task CloseWebSocketAsync(WebSocket webSocket, WebSocketCloseStatus closeStatus, string closeStatusDescription)
        {
            try
            {
                await webSocket.CloseAsync(closeStatus, closeStatusDescription, CancellationToken.None);
            }
            catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely)
            {
                // WebSocket 连接已经关闭,忽略异常
            }
            catch (Exception ex)
            {
                _logger.Error(ex, "Failed to close WebSocket.");
            }
        }

        public async Task StopAsync(CancellationToken cancellationToken)
        {
            _cts.Cancel();
            await Task.CompletedTask;
        }

        public void Dispose()
        {
            _cts.Dispose();
        }
    }
}
相关推荐
fujisheng66113 分钟前
FUI 编译期装配实践:从反射注册到 Source Generator
c#·unity3d
智码看视界23 分钟前
.NET 10 推理大模型TensorSharp 3.3.0 部署实测:纯.NET推理引擎反超llama.cpp 1.5倍,DFlash2提速62%
c#·.net·llama.cpp·.net 10·tensorsharp·本地大模型推理·开源推理引擎
orient.lu37 分钟前
第 24 章《WebSocket 通道》· nanobot WebSocket 通道源码深度解析:多路复用 + 重连空闲 + 媒体接入
websocket·源码解析·nanobot
格林威1 小时前
C# 图像异步落盘存储:基于Channel 配合 ArrayPool 实现异步落盘
开发语言·人工智能·数码相机·机器学习·计算机视觉·c#·视觉检测
软件黑马王子1 小时前
11.缓存池优化:窗口布局
开发语言·前端框架·c#
格林威1 小时前
C# 图像使用AVX2指令集:使用OpenCvSharp实现字节图像解压缩速度和map_image算子速度提升
开发语言·图像处理·人工智能·计算机视觉·c#·视觉检测·工业相机
软件黑马王子2 小时前
12.缓存池优化:对象上限
开发语言·前端框架·c#
唐青枫16 小时前
别把日志、权限写进业务方法:C#.NET 动态代理从原理到实战
c#·.net
fujisheng66117 小时前
Unity UI 生命周期状态机:处理 Covered、异步竞态与事务回滚
c#·unity3d