2008年12月11日星期四

关于字节流的一个争论(一)

最近一段时间,和一个英国人争论了很久,其实来回也没几封邮件,但是对方每次回都要隔一两天,所以就拖了很久,而且最终也没有解决,看来我的影响力(Influence)还是很需要提高的。

问题其实很简单,我们的代码里需要序列化一个数据对象,序列化前它是很多个字节数组(byte array),这个是因为这个对象类似ByteArrayOutputStream,它是慢慢变大的,一开始的时候我们并不知道它有多大,如果用单个数组,在变大的过程中需要不断申请新的大块内存,可能会导致内存不够的错误。因为我们关心的是数据,反序列化之后则只需要恢复成一个数组就好了。

所以我们的主要逻辑就类似于下面这样的伪代码:
class ValueObject {
private transient List<byte[]> values;
private transient byte[] data;
private void writeObject(ObjectOutputStream output) {
output.writeInt(totalLength); // write out the total length of bytes
for (byte[] value : values) {
output.write(value, 0, value.length);
}
}

private void readObject(ObjectInputStream input) {
int length = input.readInt();
data = new byte[length];
input.read(data, 0, length);
}
}

换句话说,我们的主要想法就是写的时候分开写,读的时候一次读,这个逻辑本来是没有问题的,因为Java里面的流(stream)指的就是字节流,比如OutputStream里面明确申明:
This abstract class is the superclass of all classes representing an output stream of bytes.
既然是流,我怎么往里面写数据,另外一头怎么读应该任意,只要我读写的总字节数对上了就OK,而且大部分I/O类也都是这么实现的,所以这段代码一直都能很好地工作,似乎从来没有问题。

然而,我们还是碰到了一个问题,当我们的对象通过RMI-IIOP传输的时候,竟然导致了一个错误。经过测试,发现了一个奇怪的现象,那就是IIOP的I/O流和普通的流的行为是不一致的,具体就是当数组比较大的时候,当你写出一个字节数组,那么对应地必须读一个字节数组,写两次就得读两次,而且这个问题只在字节数组比较大的时候才会出现问题,小数组是没有问题的。

为什么会有这么奇怪的问题呢?仔细读过这方面的实现代码CDRInputStream后发现,IIOP是把数据分区块(block)传输的,比如这一头做了很多次写,如果数据不多的话(小于等于区块大小),但是传输是一个区块过去的,而对于字节数组,如果大于区块大小,就会是一个单独的区块。问题在于,区块是有边界标志的,读的时候,应该检查边界标志,事实上CDRInputStream也是这么做了,然而不幸的是,当用户读一个字节数组的时候,它假定用户指定的长度刚好就是区块的大小,并不判断是否超出当前区块,问题就出现了。

IT民工看到这儿,基本上立刻意识到,这不就是一个简单的错误么?检查一下边界就好了。于是我立刻给我们公司负责JDK ORB的组开了一个bug,要求他们解决这个问题,我以为他们应该很容易改掉,可是没有想到,这个问题竟然也成了一个扯皮的问题,最后竟然没法解决了!

具体过程有点复杂,下次再写。

没有评论: