原创

readNBytes 少读了几个字节,为什么没有抛 EOFException

协议说接下来有四个字节,readNBytes(4) 却只给了两个,调用没有报错。若直接把这两个字节交给后续解析器,错误会被拖到更远的位置,最后看起来像业务字段缺失。

readNBytes中的N是希望读取的上限,不是遇到EOF也必须凑齐的承诺。它会尝试读取请求数量,但流在此前结束时可以返回更短结果。方法比普通read更方便,并没有自动替输入协议验证定长字段。

下面用Java 17对比短读取、严格定长读取和一个两字节长度头的小协议。保存为 ExactReads.java,执行 java ExactReads.java。

import java.io.*;
import java.nio.charset.StandardCharsets;
public class ExactReads {
    static byte[] frame(DataInputStream in) throws IOException {
        int length = in.readUnsignedShort();
        if (length > 1024) throw new IOException("declared length exceeds budget");
        byte[] payload = in.readNBytes(length);
        if (payload.length != length) throw new EOFException("truncated frame");
        return payload;
    }
    static DataInputStream input(byte... bytes) {
        return new DataInputStream(new ByteArrayInputStream(bytes));
    }
    public static void main(String[] args) throws Exception {
        System.out.println("short_length=" + input((byte)1, (byte)2).readNBytes(4).length);
        try (DataInputStream in = input((byte)1, (byte)2)) {
            try { in.readFully(new byte[4]); throw new AssertionError(); }
            catch (EOFException expected) { System.out.println("exact=EOF"); }
        }
        try (DataInputStream in = input((byte)0, (byte)2, (byte)'O', (byte)'K')) {
            System.out.println("frame=" + new String(frame(in), StandardCharsets.US_ASCII));
        }
        try (DataInputStream in = input((byte)0, (byte)3, (byte)'A')) {
            try { frame(in); throw new AssertionError(); }
            catch (EOFException expected) { System.out.println("truncated=EOF"); }
        }
        try (DataInputStream in = input((byte)8, (byte)0)) {
            try { frame(in); throw new AssertionError(); }
            catch (IOException expected) { System.out.println("oversized=REJECTED"); }
        }
    }
}

输出:

short_length=2
exact=EOF
frame=OK
truncated=EOF
oversized=REJECTED

判断“正常读完”,要先知道正常到底有多长

第一行表明四字节请求只返回两个字节。对“最多预览四字节”的功能,这很正常;对“完整读取一个四字节整数”,它就代表截断。是否应该失败取决于调用方约定,不能只看方法有没有抛异常。

DataInputStream.readFully把后一种需要写得更明确:没有足够输入就抛EOFException。它也不保证异常时目标数组未被写入,前面已经收到的内容可能留在其中。不要捕获以后再把旧数组当完整字段继续用。

若采用readNBytes,严格路径可以检查返回数组长度与声明长度相同,再交给下一层。检查必须发生在解析之前;让解码器“尽量读”只会把截断语义混进业务字段默认值里。

长度头先校验,再分配和读取

例子规定长度头为两个字节无符号整数,最大允许1024字节。程序在获得声明长度以后立即核对上限,再创建并读取正文。长度来自远端时,不能先按它分配内存,分配完才检查“有没有超限”。

长度头为零是一份合法空正文;连长度头都没有读完整则不是同一件事。两者都可能让最终正文看起来没有内容,但协议含义不同。程序用readUnsignedShort让不完整头部直接失败,正文不足也单独判为EOF。

真实协议还要明确字节序、版本、压缩前后长度以及校验字段。本例只选定大端无符号短整型作为长度头,不能因为Java两端都能通信,就认为其他语言可以猜出同样规则。对接文档里应给出具体字节样本,而不是只写“先发一个short”。

网络暂时没数据,与 EOF 不是一回事

这些例子使用确定的内存输入,因此EOF容易观察。套接字暂时没有新数据时,阻塞读取可能继续等待,既没有返回短数组,也没有宣布结束。readNBytes不是一种读取超时机制,等到EOF的语义与允许等多久要分别设计。

若从缓冲区构造一份有限长度的InputStream,再用它解析网络帧,也要确保外层已经正确完成分帧。否则某一层把暂时可用字节误当成完整输入,内层就会看到人为制造的EOF。available()不适合拿来决定整帧有多长。

循环读取多帧时,更不能把任意EOFException都当正常断开。完整上一帧以后、下一帧尚未开始的EOF可以按协议结束;长度头只到一半或正文到一半的EOF通常代表截断。错误处理需要知道当前解析阶段,不能靠一个宽泛catch把两者吞成一样。

重试也不能从剩余位置猜开始

短读取已经推进了输入位置。若期待从头再读一帧,必须有可靠的回放或重新建立请求能力;在当前流上再次读取四个字节,只会从后续位置继续。数据协议需要明确是否允许恢复,以及如何找到下一帧边界。

这个问题的修正通常只有几行:先核对声明长度,读完核对实际长度,只有完整字段才能进入解析。它们看起来不如一个方便方法简洁,却保留了最重要的信息:这是一份合法空数据、一份完整数据,还是一份不完整数据。后续业务不该替传输层猜这个答案。

正文到此结束
评论插件初始化中...
Loading...
本文目录