庄周梦蝶

生活、程序、未来
   :: 首页 ::  ::  :: 聚合  :: 管理

Kilim的小BUG

Posted on 2010-11-03 23:22 dennis 阅读(2798) 评论(2)  编辑  收藏 所属分类: java

    我最近在实现一个基于Kilim的HttpClient,在处理响应body特别大的情形下遇到了kilim的一个BUG,有必要记录下。
    问题是这样,Kilim将连接封装为EndPoint对象,EndPoint有个方法fill用于从管道读数据到缓冲区,并且可以指定希望至少读到多少个字节(atLeastN)才返回。那么在进入此方法的时候会判断缓冲区是否有足够空间容纳atLeastN个字节,如果没有,则创建一个更大的缓冲区,并将“老”的缓冲区的数据拷贝到新缓冲区,这部分代码是这样实现:
public ByteBuffer fill(ByteBuffer buf, int atleastN) throws IOException, Pausable {
        
if (buf.remaining() < atleastN) {
            ByteBuffer newbb 
= ByteBuffer.allocate(Math.max(buf.capacity() * 3 / 2, buf.position() + atleastN));
            buf.rewind();
            newbb.put(buf);
            buf 
= newbb;
        }
        ……
}

    后面的代码我省略了,这个BUG就出现在这段代码里。这段代码的逻辑很简单,先是创建一个新的更大的缓冲区,然后将老的缓冲区的数据put到新的缓冲区,在put之前调用rewind方法将老的缓冲区的position设置为0。查看rewind干了什么:
 public final Buffer rewind() {
    position 
= 0;
    mark 
= -1;
    
return this;
    }

    仅仅是将position设置为0,并让mark失效。position指向下一个读或者写的位置,这里在写入到新缓冲区之前确实需要将position设置为0,以便写入从老的缓冲区第一个位置开始。问题是什么?问题是position仅仅指定了下一个读取数据的位置,却没有指定有效数据的大小,换句话说,没有指定老的缓冲区的limit。因此这里造成的后果是老的缓冲区整个被写入到新的老缓冲区,包括有效数据和无效数据,默认情况下缓冲区的limit等于capacity。

   这个bug可以通过下面程序看出来:
        ByteBuffer old = ByteBuffer.allocate(8);
        old.putInt(
99);
        ByteBuffer newBuf 
= ByteBuffer.allocate(16);
        old.rewind();
        newBuf.put(old);
        newBuf.putInt(
100);

        newBuf.flip();
        System.out.println(newBuf.remaining());
        System.out.println(newBuf.getInt());
        System.out.println(newBuf.getInt());
        System.out.println(newBuf.getInt());

    先往old写入一个整数99,然后创建newBuf并写入old数据,并再写入一个整数100,最后从newBuf读数据。本来我们预期只应该读到两个整数99和100,但是中间却插入一个0,输出如下:

12
99
0
100

    12表示缓冲区可读的数据,本来应该是8个字节,却多了4个字节的无效数据。

     这个BUG解决很简单,将rewind修改为flip方法即可,flip不仅将position设置为0,也将limit设置为当前位置:
  public final Buffer flip() {
    limit 
= position;
    position 
= 0;
    mark 
= -1;
    
return this;
    }


    修改上面的测试程序,符合我们的预期了:
        ByteBuffer old = ByteBuffer.allocate(8);
        old.putInt(
99);
        ByteBuffer newBuf 
= ByteBuffer.allocate(16);
        old.flip();
        newBuf.put(old);
        newBuf.putInt(
100);

        newBuf.flip();
        System.out.println(newBuf.remaining());
        System.out.println(newBuf.getInt());
        System.out.println(newBuf.getInt());;

    输出:
8
99
100

    总结,使用rewind的前提是limit已经正确设置,例如你将buffer写入成功并想记录这个buffer,可以使用rewind:
while (buffer.hasRemaining()) //发送数据
    networkChannel.write(buffer);
buffer.rewind(); 
// 重置buffer,准备写入日志管道
while (buffer.hasRemaining()) // 写入日志
    loggerChannel.write(buffer);

   而flip用于缓冲区发送或者读取之前,也就是将缓冲区设置为等待传出状态。

评论

# re: Kilim的小BUG  回复  更多评论   

2010-12-14 07:30 by zz69
这里应该注意到原始设计中 的使用场景是当oldbuffer 不够大的时候,也就是oldbuffer有一个隐含的意思就是已被写满,那么limit == capacity 使用flip抑或是rewind效果是一样的,甚至这个地方rewind更能表现出代码原始的使用企图

个人理解,还请多多指教

# re: Kilim的小BUG  回复  更多评论   

2010-12-14 10:48 by dennis

@zz69
这里并没有什么隐含的意思,判断条件是
buf.remaining() < atleastN

而不是

!buf.hasRemaining()

只有注册用户登录后才能发表评论。


网站导航: