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

ASP.NET Core控制台应用:如何处理Ctrl-C/SIGINT以优雅关闭WebSocket?

解决ASP.NET Core控制台应用WebSocket优雅关闭问题

问题根源

你当前的实现无效,核心原因有两个:

  1. ProcessExit事件触发时机过晚——此时Kestrel已进入关闭流程,正在等待未完成的请求超时,无法及时取消ReceiveAsync的阻塞。
  2. 每次调用ReceiveAsync都创建新的CancellationTokenSource,且未关联WebSocket自身的关闭状态,无法协同处理主动关闭和主机停止两种场景。

正确实现方案

利用ASP.NET Core提供的IHostApplicationLifetime获取主机停止令牌,将其传递给WebSocket处理逻辑,实现ReceiveAsync的及时取消和优雅关闭。

步骤1:注入并获取主机停止令牌

在启动时通过IHostApplicationLifetime获取ApplicationStopping令牌,该令牌会在收到Ctrl-C(SIGINT)时立即触发取消信号:

var host = Host.CreateDefaultBuilder(args)
    .ConfigureWebHostDefaults(webBuilder =>
    {
        webBuilder.UseStartup<Startup>();
    })
    .ConfigureServices(services =>
    {
        // 可选:调整关闭超时时间,默认5秒
        services.Configure<HostOptions>(options =>
        {
            options.ShutdownTimeout = TimeSpan.FromSeconds(10);
        });
    })
    .Build();

// 获取主机停止令牌
var stoppingToken = host.Services.GetRequiredService<IHostApplicationLifetime>().ApplicationStopping;

await host.RunAsync(stoppingToken);

步骤2:改造WebSocketAdapter类

将停止令牌注入WebSocket处理类,关联到ReceiveAsync的取消逻辑,并在取消时主动关闭WebSocket:

public class WebSocketAdapter : IWebSocket
{
    private readonly WebSocket _websocket;
    private readonly CancellationToken _stoppingToken;

    public WebSocketAdapter(WebSocket websocket, CancellationToken stoppingToken)
    {
        _websocket = websocket;
        _stoppingToken = stoppingToken;
    }

    public async Task<WebSocketReceiveResult> ReceiveAsync()
    {
        byte[] buffer = new byte[1024];
        var arraySegment = new ArraySegment<byte>(buffer);

        // 关联主机停止令牌和WebSocket自身的关闭状态
        using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(
            _stoppingToken, 
            _websocket.CloseStatus.HasValue ? CancellationToken.None : _stoppingToken);

        try
        {
            return await _websocket.ReceiveAsync(arraySegment, linkedTokenSource.Token);
        }
        catch (OperationCanceledException)
        {
            // 主机停止时主动关闭WebSocket
            if (!_websocket.CloseStatus.HasValue)
            {
                await _websocket.CloseAsync(
                    WebSocketCloseStatus.NormalClosure, 
                    "Server shutting down", 
                    CancellationToken.None);
            }
            throw; // 抛出异常让上层循环退出
        }
    }

    // 实现其他IWebSocket接口方法...
}

步骤3:修改WebSocket循环逻辑

在消息循环中捕获OperationCanceledException,正常退出循环,避免请求超时:

public async Task WebSocketLoopAsync(IWebSocket webSocket)
{
    try
    {
        while (true)
        {
            var result = await webSocket.ReceiveAsync();
            
            if (result.CloseStatus.HasValue)
            {
                await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
                break;
            }

            // 处理WebSocket消息逻辑...
        }
    }
    catch (OperationCanceledException)
    {
        // 主机停止,正常退出循环
    }
}

步骤4:在WebSocket处理端点传递令牌

在WebSocket的请求处理方法中,将停止令牌传递给WebSocketAdapter:

public async Task HandleWebSocketAsync(HttpContext context)
{
    var stoppingToken = context.RequestServices.GetRequiredService<IHostApplicationLifetime>().ApplicationStopping;
    var webSocket = await context.WebSockets.AcceptWebSocketAsync();
    
    using var adapter = new WebSocketAdapter(webSocket, stoppingToken);
    await WebSocketLoopAsync(adapter);
}

效果说明

按下Ctrl-C后,ApplicationStopping令牌立即触发取消,ReceiveAsync会抛出OperationCanceledException,随后代码主动关闭WebSocket,消息循环正常退出,Kestrel无需等待超时即可完成优雅关闭。

内容的提问来源于stack exchange,提问作者Bart Friederichs

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 04:22:37