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 通信中,都应该建立一层消息边界感知 机制。

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

相关推荐
Danny_姜9 分钟前
Android Studio Quail 4 稳定版发布:本地 Gemma 4 + Android Skills
android·ide·android studio
数据狐(Datafox)38 分钟前
京东商品列表API接口解析(附 JSON 样例)
数据库·爬虫·json
平头哥AI10 小时前
Day 22 _ 包装错误别丢链_%w、errors.Is 与 errors.As
android·服务器·学习·golang·go
聚美智数11 小时前
图片广告检测-图片审核-图片广告识别-图像广告检测
android
mengzhi啊11 小时前
缓存类CacheDataManager把临时数据存入json。做成插件·
qt·缓存·json
早睡早起身体好12313 小时前
用 XGrammar 约束大模型工具调用:解决参数为空的问题
android·人工智能·神经网络·机器学习·自然语言处理·vllm
golang学习记15 小时前
Go 1.27新特性: json/v2使用有趣指南
开发语言·golang·json
大佐不会说日语~15 小时前
Android 16 下 uni-app APP 提示“网络连接失败”的排障与修复
android·uni-app
2501_9151063216 小时前
苹果App Store上架费用及流程全面解析
android·ios·小程序·https·uni-app·iphone·webview
YF021118 小时前
Android 16系统App开机自启动方案
android