TCP 流通信中的 EOFException 与 JSON 半包问题解析

一、背景

在做机器人与上位机通信调试时,我遇到一个典型的异常:

复制代码
Caused by: java.io.EOFException: End of input at line 1 column 2697 path $.results.charge_point_3F_1.pose.orientation.y

表面看似 JSON 格式错误,实际上是------收到的 JSON 不完整(半包)

二、问题重现

在 Android 端通过 TCP 接收上位机发来的 JSON 数据,直接用 Gson 解析:

Kotlin 复制代码
val json = String(bytes, Charsets.UTF_8)
val obj = Gson().fromJson(json, RobotMarkers::class.java)

第一段刚好停在 "y": 后面。

Gson 在解析时发现缺少值和右括号,于是抛出 EOFException。


三、TCP 是流,不是消息队列

这其实是一个经典误区:

"一次 read() 读取的数据,就是一条完整消息。"

错!

TCP 是"流式传输",它只保证字节顺序,不保证消息边界。

因此可能出现:

情况 描述 示例
半包 一条消息被拆成多次到达 { "a":1, "b": + 2 }
粘包 多条消息连在一起 { "a":1 }{ "b":2 }

四、为什么会抛 EOFException?

Gson.fromJson() 会逐字符读取输入流:

  • 它期望 JSON 结构完整闭合({}[])。

  • 当还没读完对象,却到达字符串结尾(EOF)→ 抛异常。

也就是说:

EOFException = "输入流已结束,但我还没看到该有的 } 或 ]。"


五、解决方案

✅ 方案一:协议层加"定界符"

最根本的办法是:

让每条消息"有边界"。

常见两种协议形式:

1. 长度前缀

发送端先写入 4 字节长度,再写入 JSON 内容。

复制代码

[4字节长度][JSON字节...]

接收端先读长度,再循环读取指定字节数 → 这就是完整 JSON。

2. 行分隔(NDJSON)

每条 JSON 以换行 \n 结尾。

接收端用 BufferedReader.readLine() 读取一整行后解析。


✅ 方案二:客户端做"拼包器"

如果短期内无法改协议,可以在客户端拼接:

Kotlin 复制代码
class JsonFramer {
    private val sb = StringBuilder()

    fun feed(chunk: String): List<String> {
        sb.append(chunk)
        val out = mutableListOf<String>()
        var i = 0; var depth = 0; var inStr = false; var esc = false; var start = -1
        while (i < sb.length) {
            val c = sb[i]
            if (inStr) {
                if (esc) esc = false else if (c == '\\') esc = true else if (c == '"') inStr = false
            } else when (c) {
                '"' -> inStr = true
                '{' -> if (depth++ == 0) start = i
                '}' -> if (--depth == 0 && start >= 0) {
                    out += sb.substring(start, i + 1)
                    start = -1
                }
            }
            i++
        }
        if (out.isNotEmpty()) sb.delete(0, sb.lastIndexOf('}') + 1)
        return out
    }
}

每次 read() 到的数据都 feed() 一次,
feed() 会在 JSON 结构完整时返回一条完整字符串。

✅ 方案三:解析前清洗数据

有时候日志里包含前缀,比如:

收到 TCP 消息: {...}

要裁掉这些内容:

Kotlin 复制代码
fun clean(raw: String): String {
    val start = raw.indexOf('{')
    val end = raw.lastIndexOf('}')
    return raw.substring(start, end + 1)
}

六、lenient 模式能解决吗?

JsonReader.setLenient(true) 只是在语法层放宽规则:

  • 允许单引号、注释、NaN 等;

  • 不会修复"半包"或"粘包"问题

七、最佳实践:稳健 TCP 接收逻辑

Kotlin 复制代码
val buf = ByteArray(16 * 1024)
val framer = JsonFramer()

while (true) {
    val n = input.read(buf)
    if (n == -1) break
    val chunk = String(buf, 0, n, Charsets.UTF_8)
    val frames = framer.feed(chunk)
    for (json in frames) {
        val clean = clean(json)
        val obj = Gson().fromJson(clean, Response::class.java)
        process(obj)
    }
}

八、常见排查 Checklist ✅

检查项 说明
是否分多次 read() 打印每次字节数确认
是否有日志前缀 清洗字符串
缓冲大小是否过小 建议 ≥ 16KB
是否拼包 必须有!
是否多条粘在一起 用 Framer 逐条拆分
是否 lenient 可用于调试,但不治本

九、总结

核心问题 原因 解决方案
EOFException JSON 被截断(半包) 拼包或协议定界
MalformedJsonException 粘包或脏前缀 拆包 + 清洗
偶发成功偶发失败 TCP 分段不固定 循环读 + 拼包

⚙️ 记住一句话:
TCP 是流,JSON 是消息。要想稳定,必须"定界"。

十、结语

EOFException 并不可怕,

可怕的是我们误以为"读一次就是一条消息"。

在任何基于 TCP 的 JSON 通信中,都应该建立一层消息边界感知 机制。

只有这样,通信层才真正做到"稳如老狗"。

相关推荐
阿巴斯甜1 天前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker1 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95271 天前
Andorid Google 登录接入文档
android
黄林晴1 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab2 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇2 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android