You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ASP.NET C# WebSocket发送关闭消息触发SocketException及持续通信问题

问题

问题1

为什么向WebSocket发送关闭消息时会崩溃并抛出异常System.Net.Sockets.SocketException: 'The I/O operation has been aborted because of either a thread exit or an application request.'?

问题2

如何实现WebSocket在接收客户端消息后持续发送消息,同时在其他任务/线程中监听关闭消息?

已尝试方案

  • 使用CloseOutputAsync、CloseAsync方法,以及单独任务监听关闭操作,均触发相同异常。

更新后的代码

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

#if NoOptions
    // <snippet_UseWebSockets>
    app.UseWebSockets();
    // </snippet_UseWebSockets>
#endif
#if UseOptions
    // <snippet_UseWebSocketsOptions>
    var webSocketOptions = new WebSocketOptions()
    {
        KeepAliveInterval = TimeSpan.FromSeconds(120),
    };

    app.UseWebSockets(webSocketOptions);
    // </snippet_UseWebSocketsOptions>
#endif

#if UseOptionsAO
    // <snippet_UseWebSocketsOptionsAO>
    var webSocketOptions = new WebSocketOptions()
    {
        KeepAliveInterval = TimeSpan.FromSeconds(120),
    };
    webSocketOptions.AllowedOrigins.Add("https://client.com");
    webSocketOptions.AllowedOrigins.Add("https://www.client.com");

    app.UseWebSockets(webSocketOptions);
    // </snippet_UseWebSocketsOptionsAO>
#endif
    ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls13;
    redis = ConnectionMultiplexer.Connect(Configuration["RedisConnect"]);
    db = redis.GetDatabase();
    endPoint = redis.GetEndPoints();
    server = redis.GetServer(endPoint[0], Configuration["RedisConnect"]);
    // <snippet_AcceptWebSocket>
    app.Use(async (context, next) =>
    {
        if (context.Request.Path == "/ws")
        {
            if (context.WebSockets.IsWebSocketRequest)
            {
                using (WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync())
                {
                    await Echo(context, webSocket);
                }
            }
            else
            {
                context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
            }
        }
        else
        {
            await next();
        }
        app.UseFileServer();
    });
    private async Task Echo(HttpContext context,WebSocket webSocket) 
    {
        var buffer=new byte[1024*4];
        var result=await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), System.Threading.CancellationToken.None);
        if (result != null) 
        {
            while (!result.CloseStatus.HasValue) 
            {
                string msg=Encoding.UTF8.GetString(new ArraySegment<byte> (buffer,0,result.Count));
                await webSocket.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes("servrSays")),result.MessageType,result.EndOfMessage,System.Threading.CancellationToken.None);
                result=await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), System.Threading.CancellationToken.None);
            }
        }
        await webSocket.CloseAsync(result.CloseStatus.Value,result.CloseStatusDescription,System.Threading.CancellationToken.None);
    }

解答

问题1:异常原因分析

这个异常本质是代码对已失效的WebSocket连接执行了IO操作,结合你的代码场景,具体触发点包括:

  1. 客户端主动断开连接后,ReceiveAsync还未检测到关闭状态,此时调用CloseAsync会因为连接已终止抛出异常。
  2. using块包裹WebSocket对象,当Echo方法执行完毕后,using会自动释放WebSocket资源,若此时还有未完成的IO操作,就会触发“IO被中止”的错误。
  3. 循环结束后直接调用CloseAsync,但此时客户端可能已经关闭连接,服务器端重复执行关闭操作会导致冲突。

问题2:持续发送+监听关闭的实现方案

核心思路是将接收消息(监听关闭)和发送消息拆分为两个独立异步任务,用CancellationToken协调任务生命周期,避免单线程阻塞导致无法及时响应关闭请求。

修正后的代码示例

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

#if UseOptions
    var webSocketOptions = new WebSocketOptions()
    {
        KeepAliveInterval = TimeSpan.FromSeconds(120),
    };
    app.UseWebSockets(webSocketOptions);
#endif

    ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls13;
    redis = ConnectionMultiplexer.Connect(Configuration["RedisConnect"]);
    db = redis.GetDatabase();
    endPoint = redis.GetEndPoints();
    server = redis.GetServer(endPoint[0], Configuration["RedisConnect"]);

    app.Use(async (context, next) =>
    {
        if (context.Request.Path == "/ws")
        {
            if (context.WebSockets.IsWebSocketRequest)
            {
                using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
                var cts = new CancellationTokenSource();
                
                // 启动两个独立任务:接收消息+监听关闭、持续发送消息
                var receiveTask = ListenForCloseAndMessages(webSocket, cts.Token);
                var sendTask = ContinuousSendMessages(webSocket, cts.Token);

                // 等待任一任务完成(比如客户端关闭连接)
                var completedTask = await Task.WhenAny(receiveTask, sendTask);
                // 取消另一个未完成的任务
                cts.Cancel();
                
                try
                {
                    // 等待剩余任务完成,避免资源泄漏
                    await Task.WhenAll(receiveTask, sendTask);
                }
                catch (OperationCanceledException)
                {
                    // 任务被取消属于预期情况,无需处理
                }
                finally
                {
                    // 仅在连接仍处于有效状态时执行关闭操作
                    if (webSocket.State == WebSocketState.Open || webSocket.State == WebSocketState.CloseReceived)
                    {
                        await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed by server", CancellationToken.None);
                    }
                }
            }
            else
            {
                context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
            }
        }
        else
        {
            await next();
        }
    });
    app.UseFileServer();
}

private async Task ListenForCloseAndMessages(WebSocket webSocket, CancellationToken token)
{
    var buffer = new byte[1024 * 4];
    while (!token.IsCancellationRequested)
    {
        WebSocketReceiveResult result;
        try
        {
            result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), token);
        }
        catch (OperationCanceledException)
        {
            break;
        }

        if (result.CloseStatus.HasValue)
        {
            // 检测到客户端关闭请求,触发任务取消
            token.ThrowIfCancellationRequested();
            break;
        }

        // 处理客户端消息逻辑
        string msg = Encoding.UTF8.GetString(new ArraySegment<byte>(buffer, 0, result.Count));
        // 可根据业务需求扩展消息处理逻辑
    }
}

private async Task ContinuousSendMessages(WebSocket webSocket, CancellationToken token)
{
    while (!token.IsCancellationRequested)
    {
        try
        {
            // 模拟持续发送消息(示例为每秒发送心跳)
            await webSocket.SendAsync(
                new ArraySegment<byte>(Encoding.UTF8.GetBytes("serverSays: heartbeat")),
                WebSocketMessageType.Text,
                endOfMessage: true,
                token);
            await Task.Delay(1000, token);
        }
        catch (OperationCanceledException)
        {
            break;
        }
        catch (WebSocketException)
        {
            // 连接已断开,触发任务取消
            token.ThrowIfCancellationRequested();
            break;
        }
    }
}

关键改进点

  1. 任务拆分:将接收和发送逻辑分离为独立任务,避免单线程阻塞导致无法及时响应关闭请求。
  2. 取消令牌:用CancellationTokenSource协调任务生命周期,任一任务完成后立即取消另一任务。
  3. 状态检查:关闭连接前先判断WebSocket状态,避免对已失效连接执行IO操作。
  4. 异常处理:捕获预期的取消异常和WebSocket连接异常,避免程序崩溃。

问题1的额外修复建议

在原代码的CloseAsync调用前增加状态检查:

if (webSocket.State == WebSocketState.Open || webSocket.State == WebSocketState.CloseReceived)
{
    await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
}

内容的提问来源于stack exchange,提问作者a.mak

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 18:46:16