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

Android使用StompClient实现WebSocket连接即断并抛EOFException求助

Android中StompClient连接WebSocket后立即断开并抛出EOFException问题排查

问题背景

服务器拥有CA签名的证书,首次在Android中使用StompClient实现WebSocket功能,连接建立后立即断开,抛出如下异常:

java.io.EOFException
    at okio.RealBufferedSource.require(RealBufferedSource.kt:199)
    at okio.RealBufferedSource.readByte(RealBufferedSource.kt:209)
    at okhttp3.internal.ws.WebSocketReader.readHeader(WebSocketReader.kt:119)
    at okhttp3.internal.ws.WebSocketReader.processNextFrame(WebSocketReader.kt:102)
    at okhttp3.internal.ws.RealWebSocket.loopReader(RealWebSocket.kt:293)
    at okhttp3.internal.ws.RealWebSocket$connect$1.onResponse(RealWebSocket.kt:195)
    at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
    at java.lang.Thread.run(Thread.java:920)

相关代码与依赖

依赖

com.github.NaikSoftware:StompProtocolAndroid:1.6.6

业务逻辑代码

private fun startWebSocketConnection() {
    lifecycleScope.launch( defaultDispatcher) {
        compositeDisposable.clear()
        // Web Socket //
        val url: String = "https://url.ca/guard-ws"

        val rawClient = OkHttpClient.Builder().build()
        val rawRequest = Request.Builder()
            .url(url)
            .addHeader("Authorization", "Bearer ${tokenInterceptor.token}")
            .build()
        rawClient.newWebSocket(rawRequest, object : WebSocketListener() {
            override fun onOpen(ws: WebSocket, response: Response) {
                Log.d("MyTag", "✅ onOpen: ${response.code}")
                ws.send("CONNECT\naccept-version:1.1,1.2\nheart-beat:0,0\n\n\u0000")
            }
            override fun onMessage(ws: WebSocket, text: String) {
                Log.d("MyTag", "📨 onMessage: $text")
            }
            override fun onClosing(ws: WebSocket, code: Int, reason: String) {
                Log.d("MyTag", "🔴 onClosing: $code / $reason")
            }
            override fun onFailure(ws: WebSocket, t: Throwable, response: Response?) {
                Log.e("MyTag", "❌ onFailure: ${t.message}", t)
            }
        })


        webSocketVM.stompClient = Stomp
            .over(
                Stomp.ConnectionProvider.OKHTTP,
                url,
                null,
                webSocketVM.getOkHttpClient()
            )
            .withServerHeartbeat(30000)
            .withClientHeartbeat(30000)
        val list = listOf(StompHeader("Authorization", "Bearer ${tokenInterceptor.token}"))
        webSocketVM.stompClient?.let {
            it.connect( list)
            val main: Disposable = it.lifecycle()
                .subscribe({ lifecycleEvent ->
                    when (lifecycleEvent.type) {
                        Type.OPENED -> {
                            Log.d("MyTag", "intentando con: $url")
                            Log.d("MyTag", "open")
                        }
                        Type.ERROR -> {
                            Log.d("MyTag", "Error: ${lifecycleEvent.exception}")
                        }
                        Type.CLOSED -> {
                            Log.d("MyTag", "closed")
                        }
                        Type.FAILED_SERVER_HEARTBEAT -> {
                            Log.d("MyTag", "failed server heartbeat")
                        }
                        else -> {}
                    }
                }){ error: Throwable? -> Log.e( "MyTag", "error: ${error?.message} : Connection")}
            compositeDisposable.add( main)
        }
    }
}

WebSocket所用OkHttpClient配置

fun getOkHttpClient(): OkHttpClient {
    return OkHttpClient.Builder()
        .addNetworkInterceptor(
            HttpLoggingInterceptor().apply {
                level = HttpLoggingInterceptor.Level.HEADERS
            }
        ).build()
}

问题排查与解决方案

核心问题分析

  1. 重复WebSocket连接:代码中同时创建了两个独立的WebSocket连接(原始OkHttpClient连接 + StompClient连接),服务器端会因连接冲突直接断开连接,这是引发EOFException的主要原因。
  2. 心跳配置不匹配:原始连接发送heart-beat:0,0,但StompClient设置30秒心跳间隔,两者不一致导致服务器无法识别心跳规则,触发断开。
  3. OkHttpClient缺少超时配置:未设置连接、读写超时,可能导致连接过程中因超时被强制断开。
  4. 日志级别不足:当前仅打印Headers,无法查看完整Stomp帧交互细节,难以定位协议层面问题。

具体修复步骤

  1. 删除重复的原始WebSocket连接代码:保留StompClient连接逻辑即可,StompProtocolAndroid会自动处理底层WebSocket握手和Stomp帧发送。
  2. 统一心跳配置:如果服务器要求禁用心跳(heart-beat:0,0),修改StompClient配置:
    webSocketVM.stompClient = Stomp
        .over(
            Stomp.ConnectionProvider.OKHTTP,
            url,
            null,
            webSocketVM.getOkHttpClient()
        )
        .withServerHeartbeat(0)
        .withClientHeartbeat(0)
    
  3. 增强OkHttpClient配置:添加超时设置并提升日志级别:
    fun getOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .addNetworkInterceptor(
                HttpLoggingInterceptor().apply {
                    level = HttpLoggingInterceptor.Level.BODY // 打印完整请求响应,包括Stomp帧
                }
            )
            .connectTimeout(10, TimeUnit.SECONDS)
            .readTimeout(30, TimeUnit.SECONDS)
            .writeTimeout(30, TimeUnit.SECONDS)
            .build()
    }
    
  4. 验证Authorization头传递:通过日志确认StompHeader中的Bearer Token有效且格式正确。
  5. 添加订阅逻辑:连接成功后添加Stomp订阅,避免服务器因无活跃订阅断开连接:
    // 在Type.OPENED分支中添加订阅
    Type.OPENED -> {
        Log.d("MyTag", "intentando con: $url")
        Log.d("MyTag", "open")
        // 替换为实际需要订阅的主题
        val subscription = it.topic("/your/topic")
            .subscribe { message ->
                Log.d("MyTag", "Received message: ${message.payload}")
            }
        compositeDisposable.add(subscription)
    }
    

验证方法

查看HttpLoggingInterceptor输出的日志,确认:

  • WebSocket握手响应状态码为101 Switching Protocols
  • Stomp CONNECT帧发送后,收到服务器返回的CONNECTED帧
  • 无异常断开帧或错误响应

内容的提问来源于stack exchange,提问作者Cristian Alexis Torres Zavala

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.03 15:14:50