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

Java NIO SocketChannel服务器向客户端发送第二个对象失败

解决NIO SocketChannel发送多个Java对象时的StreamCorruptedException

这个java.io.StreamCorruptedException: invalid type code: AC异常我太熟了——本质是你没处理好序列化对象的字节边界,或者在序列化/反序列化的流程上踩了NIO和Java序列化的坑。咱们一步步拆解问题和解决办法:

为什么第一个对象正常,第二个就报错?

Java的ObjectInputStream和ObjectOutputStream是基于连续的字节流工作的:ObjectOutputStream会在第一次创建时写入一个特定的序列化头部,之后每次writeObject只写入对象数据;而ObjectInputStream会先读取这个头部,然后依次解析后续的对象数据。

但NIO的SocketChannel是字节流传输,没有天然的“消息边界”:

  • 第一次发送时,完整的头部+对象数据被客户端完整接收,ObjectInputStream能正常解析;
  • 第二次发送时,要么你重复创建了ObjectOutputStream(导致多写了一个头部),要么TCP的粘包/拆包让客户端把两次发送的字节混在一起,ObjectInputStream解析时遇到了不符合预期的字节(比如多余的头部、不完整的对象数据),就抛出了类型码错误(AC是十六进制的错误字节标记)。

靠谱的解决方案

方案1:给每个对象加长度前缀(最适合NIO场景)

这是NIO下处理消息边界的标准做法:发送对象前,先把对象序列化成字节数组,发送数组的长度(用固定4字节的int),再发送字节数组。客户端先读长度,再读对应长度的字节,最后反序列化。

服务器端代码示例

// 序列化任务对象到字节数组
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 注意:如果是多次发送,不要每次都新建ObjectOutputStream!否则会重复写入头部
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(yourTaskObject);
oos.flush();
byte[] taskBytes = baos.toByteArray();
oos.close();

// 第一步:发送字节数组长度(4字节int,大端序)
ByteBuffer lenBuffer = ByteBuffer.allocate(4);
lenBuffer.putInt(taskBytes.length);
lenBuffer.flip();
// NIO非阻塞模式下要循环写,确保全部发送
while (lenBuffer.hasRemaining()) {
    socketChannel.write(lenBuffer);
}

// 第二步:发送对象字节数组
ByteBuffer dataBuffer = ByteBuffer.wrap(taskBytes);
while (dataBuffer.hasRemaining()) {
    socketChannel.write(dataBuffer);
}

客户端代码示例

// 第一步:读取长度(必须读够4字节)
ByteBuffer lenBuffer = ByteBuffer.allocate(4);
int readLen = 0;
while (readLen < 4) {
    int r = socketChannel.read(lenBuffer);
    if (r == -1) {
        // 连接断开,处理异常
        break;
    }
    readLen += r;
}
lenBuffer.flip();
int dataLength = lenBuffer.getInt();

// 第二步:读取对应长度的对象字节
ByteBuffer dataBuffer = ByteBuffer.allocate(dataLength);
readLen = 0;
while (readLen < dataLength) {
    int r = socketChannel.read(dataBuffer);
    if (r == -1) {
        // 连接断开,处理异常
        break;
    }
    readLen += r;
}
dataBuffer.flip();

// 第三步:反序列化对象
ByteArrayInputStream bais = new ByteArrayInputStream(dataBuffer.array());
ObjectInputStream ois = new ObjectInputStream(bais);
Object task = ois.readObject();
ois.close();

方案2:复用ObjectOutputStream/ObjectInputStream(适合阻塞NIO或BIO)

如果你的服务器用的是阻塞模式的NIO,或者愿意处理流的适配,可以直接用Channels工具类把SocketChannel包装成流,然后复用同一个ObjectOutputStream和ObjectInputStream:

服务器端代码

// 只创建一次ObjectOutputStream,不要每次发送都新建!
OutputStream channelOut = Channels.newOutputStream(socketChannel);
ObjectOutputStream oos = new ObjectOutputStream(channelOut);

// 发送第一个任务
oos.writeObject(task1);
oos.flush(); // 必须flush,确保数据写入Channel

// 发送第二个任务
oos.writeObject(task2);
oos.flush();

客户端代码

// 只创建一次ObjectInputStream
InputStream channelIn = Channels.newInputStream(socketChannel);
ObjectInputStream ois = new ObjectInputStream(channelIn);

// 依次读取对象
Object task1 = ois.readObject();
Object task2 = ois.readObject();

⚠️ 注意:这种方式的坑是绝对不能重复创建ObjectOutputStream,否则每次创建都会写入一个序列化头部,客户端的ObjectInputStream读到多余的头部就会抛出异常。另外,非阻塞NIO下用这种方式要特别小心,因为flush()不一定能立即把数据写入Channel,可能需要配合NIO的缓冲区处理。

额外注意事项

  • 确保所有要发送的对象(包括其内部引用的对象)都实现了Serializable接口,并且serialVersionUID保持一致(虽然这个异常不是因为版本号,但也是序列化常见坑);
  • NIO下读写数据必须循环操作,因为read()/write()方法不一定会一次性处理完所有字节;
  • 如果用非阻塞模式,要结合Selector来监听通道的可读/可写事件,避免空轮询或数据丢失。

内容的提问来源于stack exchange,提问作者Naveen Kumar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.21 06:55:05