多用户通信系统 - Socket 编程使用对象流进行通信的踩坑记录

在多用户通信系统项目中,客户端 Socket 与服务端 Socket 的所有通信均使用对象流的方式来完成。一切很好,直到在完成离线发送消息/发送文件功能时,我才踩到在 Socket 编程中使用对象流通信的坑

java.io.StreamCorruptedException: invalid stream header: 7371007E

在数据通道某一端的 Socket 使用同一个 ObjectOutputStream 写多个对象,但在另外一端的 Socket 使用不同ObjectInputStream 来读数据通道内的多个对象,就会产生异常:java.io.StreamCorruptedException: invalid stream header: 7371007E

参考:StackOverflow - java.io.StreamCorruptedException: invalid stream header: 7371007E 高赞回答可知该异常发生的原因(虽然提问者问得不太好):

The error you get is because the objectOutputStream writes a header, which is expected by objectIutputStream. As you are not writing multiple streams, but simply multiple objects, then the next objectInputStream created on the socket input fails to find a second header, and throws an exception.

简单来说就是:ObjectOutputStream 写(多个)对象数据时,会在最开始写一个 header,而该 headerObjectInputStream 期待获取到的,获取到后才进行读(多个)对象数据的操作,若获取不到,就会抛出该异常。

对于同一个 ObjectOutputStream 写出的多个对象数据,第一个 ObjectInputStream 在读取时还能读到 header,因此可以正常 readObject,但第二个 ObjectInputStream 就读不到期待的 header 了,因此抛出异常。

java.io.StreamCorruptedException: invalid type code: AC

在数据通道某一端的 Socket 使用不同ObjectOutputStream 写多个对象,但在另外一端的 Socket 使用同一个 ObjectInputStream 来读数据通道内的多个对象,就会产生异常:java.io.StreamCorruptedException: invalid type code: AC

参考:StackOverflow - StreamCorruptedException: invalid type code: AC 告知回答可知该异常发生的原因:

The underlying problem is that you are using a new ObjectOutputStream to write to a stream that you have already used a prior ObjectOutputStream to write to. These streams have headers which are written and read by the respective constructors, so if you create another ObjectOutputStream you will write a new header, which starts with - guess what? - 0xAC, and the existing ObjectInputStream isn’t expecting another header at this point so it barfs.

问题依然出在对象流的 header 上面。
对于使用不同的 ObjectOutputStream 分别单独写出的多个对象数据,每个对象数据都有一个 header 开头,此时如果只用同一个 ObjectInputStream 来全部读取,那么 ObjectInputStream 只会在最开始读取一次 header,并认为后面的都是对象数据,所以在读到第二个 ObjectOutputStream 写出的 header 时,就会抛出异常

如何避坑

对于一个数据连接通道的两端的 Socket,保证一端的 Socket 所绑定的某个 ObjectOutputStream 写出的所有对象数据,总是被另外一段的 Socket 所绑定的某个 ObjectInputStream 全部读取,就可以避免上面的两个异常发生!

StackOverflow - StreamCorruptedException: invalid type code: AC 也给出了最佳实践:

Use a single OOS and OIS for the life of the socket, and don’t use any other streams on the socket.
And don’t use any other streams or Readers or Writers on the same socket. The object stream APIs can handle all Java primitive datatypes and all Serializable classes.

Socket 的整个生命周期中,从始至终只使用一个 ObjectInputStream 和一个 ObjectOutputStream 来进行网络通信。另外,不要使用其他的流,建议只使用对象流,因为对象流可以处理所有 Java 原生类型以及所有序列化类。

多用户通信系统项目中解决以上异常

虽然最佳实践是建议在 Socket 的整个生命周期中,只使用一个 ObjectInputStream 和一个 ObjectOutputStream,但是多用户通信系统项目做到离线发送消息/文件这个功能时,已经接近尾声,此前每次使用对象流通信,都是在数据通道两端的 Socket 对象上都创建一个新的对象流,要遵循最佳实践,改动较大。

因此,这里就不遵循最佳实践,而是每次通信,都在两端的 Socket 对象上创建新的对象流。

QQServer 项目中 com.hspedu.qqserver.service.QQServer.java
服务端将多个离线私聊/文件消息,转发给刚上线登录的用户时,确保写每个 Message 对象时,都创建一个新的 ObjectOutputStream

1
2
3
4
5
6
7
8
9
10
11
12
13
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
// 省略无关代码...

ArrayList<Message> offlineMessages =
OfflineMessageService.getMessagesByUserId(user.getUserId());
if (offlineMessages != null) {
for (Message offlineMessage : offlineMessages) {
// 添加下面一行:每次 writeObject 写对象前,都创建新的对象流
oos = new ObjectOutputStream((socket.getOutputStream()));
oos.writeObject(offlineMessage);
}
OfflineMessageService.removeMessagesByUserId(user.getUserId());
}

QQClient 项目中 com.hspedu.qqclient.service.ClientConnectServerThread.java
客户端持有 Socket 的线程不断运行尝试读取数据,每次读取 Message 对象数据,都会创建新的 ObjectInputStream 来读取,这里不需要更改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public void run() {
try {
while (true) {
// 每次都创建新的对象流,来读取服务端回送的消息
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());

// 如果服务端没有回送消息,则在 ois.readObject() 处阻塞等待
Message msg = (Message) ois.readObject();

// 省略无关代码...
}
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}