ASP.NET Core控制台应用:如何处理Ctrl-C/SIGINT以优雅关闭WebSocket?
解决ASP.NET Core控制台应用WebSocket优雅关闭问题
问题根源
你当前的实现无效,核心原因有两个:
ProcessExit事件触发时机过晚——此时Kestrel已进入关闭流程,正在等待未完成的请求超时,无法及时取消ReceiveAsync的阻塞。- 每次调用
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
相关产品推荐
相关产品推荐

