Ktor服务端关闭WebSocket的正确方式及客户端未收Close帧排查
WebSocket无效Token时关闭连接的正确方式及客户端收不到Close帧的原因
一、正确的关闭连接方式(基于Ktor服务端)
针对无效Token场景,优先在握手阶段完成认证,避免建立WebSocket连接后再关闭;若必须在会话内认证,需确保Close帧能正确发送并等待会话结束:
1. 握手阶段直接拦截(推荐)
WebSocket握手本质是HTTP请求,可在握手时验证Token,无效则直接返回HTTP 401,不建立WebSocket连接:
fun Application.configureWebSocketRouting(messageProcessor: MessageProcessor) { routing { webSocket(path = CHAT_WEBSOCKET_PATH) { // 从请求头获取Token并验证 val authHeader = call.request.header(HttpHeaders.Authorization) val isValid = validateToken(authHeader) // 实现你的Token校验逻辑 if (!isValid) { // 握手阶段拒绝,返回401,不会建立WebSocket连接 call.respond(HttpStatusCode.Unauthorized) return@webSocket } // 认证通过后处理会话 val user = getUserFromToken(authHeader) launch { messageProcessor.process(user, this@webSocket) } // 挂起保持会话活跃,避免路由提前结束 incoming.consumeEach {} } } }
2. 会话内认证后的正确关闭
若使用authenticate块,需确保异常抛出时会话已建立,且等待Close帧发送完成:
fun Application.configureWebSocketRouting(messageProcessor: MessageProcessor) { routing { webSocket(path = CHAT_WEBSOCKET_PATH) { val session = this try { authenticate("access") { val user = call.principal<User>() ?: throw IllegalArgumentException("用户未找到") launch { messageProcessor.process(user, session) } // 挂起直到会话自然结束 incoming.consumeEach {} } } catch (e: Exception) { // 尝试发送Close帧并等待会话关闭 runCatching { session.close(CloseReason(CloseReason.Codes.VIOLATED_POLICY, "未认证用户")) session.incoming.cancelAndJoin() } } } } }
二、客户端收不到Close帧的核心原因
结合你提供的代码,问题主要集中在以下几点:
1. 服务端在WebSocket会话未建立时关闭
authenticate块可能在握手完成前抛出异常,此时WebSocket连接未完成HTTP升级,服务端直接断开TCP连接,不会发送WebSocket协议层面的Close帧,客户端只会收到连接异常。
2. 服务端未等待Close帧发送完成
调用session.close()后,若协程立即结束,Close帧可能还未发送到客户端,TCP连接就被强制关闭,客户端无法接收Close帧。
3. 客户端异常逻辑覆盖Close帧接收
若服务端在握手阶段返回401,client.webSocketSession会直接抛出异常进入catch块,session.incoming从未被消费,自然不会触发Close帧的处理逻辑。
4. 客户端重试逻辑未区分场景
当前客户端无论是否收到Close帧都会重试:若握手失败(Token无效),reconnected为false会累加重试次数;若收到Close帧,代码也未设置停止重试的标记,导致无限重试。
三、客户端代码优化方案
针对收不到Close帧和无限重试问题,调整如下:
override fun initialize(retryCount: Int, shouldRetry: Boolean) { messageScope.launch { var reconnected = false var receivedCloseFrame = false logger.d("Attempting websocket connection...") try { session = client.webSocketSession { url(urlString) } reconnected = true SocketFlows._connectivityFlow.emit(ConnectivityState.Connected()) logger.d("Connected to websocket") session.incoming.consumeAsFlow() .onEach { frame -> when (frame) { is Frame.Close -> { receivedCloseFrame = true logger.d("收到Close帧: ${frame.reason}") handleCloseFrame(frame) } is Frame.Text -> handleTextFrame(frame) else -> {} } } .filterIsInstance<SocketEvent.ChatMessageEvent>() .collect { SocketFlows._messageFlow.emit(it) } } catch (e: Exception) { logger.e("Websocket异常", e) } logger.d("Disconnected from websocket") // 仅在未收到Close帧且允许重试时继续 if (!receivedCloseFrame && shouldRetry) { val currentDelay = (minRetryDelayMs * retryCount).coerceAtMost(maxRetryDelayMs) SocketFlows._connectivityFlow.emit(ConnectivityState.Disconnected(currentDelay)) delay(currentDelay) val nextCount = if (reconnected) 0 else retryCount + 1 initialize(nextCount, shouldRetry) } else { SocketFlows._connectivityFlow.emit(ConnectivityState.Disconnected(0)) } } }
内容的提问来源于stack exchange,提问作者Vinnie
相关产品推荐
相关产品推荐

