为何TCP连接关闭时,io.ktor.websocket.WebSocketSession.send抛出CancellationException?
问题背景
当WebSocket会话因TCP连接断开或对等端主动关闭时,io.ktor.websocket.WebSocketSession.send方法会抛出CancellationException,而非更直观的ChannelClosedException类异常。这导致业务代码难以区分用户主动取消协程(比如调用scope.cancel()、withTimeout触发)和连接异常导致的发送失败,既增加了异常处理复杂度,也可能让真实发送故障被忽略。
设计原因
Ktor的WebSocket底层基于协程Channel实现:当连接关闭时,发送通道(WebSocketSession.outgoing)会被取消,而send方法本质是向该通道发送数据,因此会遵循协程Channel的语义抛出CancellationException。这种设计将连接中断视为协程上下文的取消信号,目的是自动终止相关协程任务、避免资源泄漏,但确实给业务层面的异常区分带来了困扰。
哪些函数会抛出此类CancellationException?
除了你列出的主动取消操作,Ktor中所有依赖WebSocket通道的操作,在连接关闭/通道取消时都会抛出CancellationException,例如:
WebSocketSession.receive():尝试接收消息时连接已关闭WebSocketSession.outgoing.send():直接操作发送通道时(和send方法等效)- 基于WebSocket通道的迭代器操作(如
for (frame in incoming))
解决方案
要区分用户主动取消和连接异常导致的发送失败,可以通过以下方式处理:
方案1:封装安全发送方法
扩展WebSocketSession,捕获CancellationException并检查通道状态,重新抛出明确的异常:
import io.ktor.websocket.* import kotlinx.coroutines.channels.ChannelClosedException suspend fun WebSocketSession.sendSafe(frame: Frame) { try { send(frame) } catch (e: CancellationException) { // 检查发送通道是否因连接关闭而关闭 if (outgoing.isClosedForSend) { throw ChannelClosedException("WebSocket connection closed during send", e) } else { // 确认为用户主动取消,重新抛出原异常 throw e } } }
方案2:修改异常处理逻辑
在你的测试代码中,使用封装后的sendSafe,即可明确区分两种场景:
@Test fun testChannelSendCatchCancellationException(): Unit = runBlocking { val server = MockWebServer() server.start() val serverUrl = server.url("/").toString().replaceFirst("http", "ws") println(serverUrl) val response = MockResponse().withWebSocketUpgrade(object : WebSocketListener() { override fun onOpen(webSocket: WebSocket, response: Response) { val request = server.takeRequest() println(request) webSocket.close(3000, "goodbye") } }) server.enqueue(response) val session = HttpClient { install(WebSockets) }.webSocketSession(serverUrl) launch { try { delay(500L) try { session.sendSafe(Frame.Text("Hello, world")) println("Send success") // 发送成功后的业务逻辑 } catch (e: ChannelClosedException) { println("Send failed due to connection closed: $e") // 连接关闭的故障处理逻辑 } catch (e: CancellationException) { println("Coroutine cancelled by user: $e") // 用户主动取消的处理逻辑 throw e } catch (e: Exception) { println("Unexpected send error: $e") } } catch (e: CancellationException) { println("Coroutine cancelled at outer scope: $e") throw e } } delay(200L) server.close() delay(200L) }
方案3:利用协程取消原因(可选)
如果不需要严格的异常类型区分,可以通过检查CancellationException的message判断,但这种方式依赖框架的message格式,存在兼容性风险:
catch (e: CancellationException) { if (e.message?.contains("Channel was cancelled") == true) { // 连接关闭导致的发送失败 } else { // 用户主动取消 } }
内容的提问来源于stack exchange,提问作者progquester

