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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 01:35:15