一、背景
在做机器人与上位机通信调试时,我遇到一个典型的异常:
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 通信中,都应该建立一层消息边界感知 机制。
只有这样,通信层才真正做到"稳如老狗"。