Post

Netty ByteBuf

关于ByteBuf的部分主要翻译的Netty官方提供的文档

在Java的网络编程体系中,时常会提到一点,堵塞式的IO,以Socket为例是面向Stream构建的,而非堵塞式的NIO,以SocketChannel为代表,则是基于Buffer作为基础。两者之间的区别在与,Stream类似连接在自来水管道上,只能顺序从Stream中读取或者写入数据,并且Stream只能是单向的,InputStream或者OutputStream 两者有严格的区分,必须按照顺序获取流中的所有数据,在需要跳过部分数据的时候,也需要将数据全部读取,再进行额外的处理;而NIO模式则是将数据先读/写到Buffer中,Buffer本身只是一个缓存区,SocketChannel是一个双向通道,在进行读写的时候再从Buffer中获取想要的数据,并可以通过下表索引来控制获取全部或者部分数据。

NIO中提供了多种Buffer支持,其中使用最多的为ByteBuffer,用来表示低级别的二进制和文本消息的数据结构,一个电信的ByteBuffer的使用代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ByteBuffer buffer = ByteBuffer.allocate(1024);
while(true) {
  Set<SelectionKey> selectionKeys = selector.selectedKeys();
  Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
  while(keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if(key.isAcceptable()) {
      ServerSocketChannel sChannel = (ServerSocketChannel) key.channel();
      SocketChannel channel = sChannel.accept();
      channel.configureBlocking(false);
      //写入
      channel.write(ByteBuffer.wrap("from server to client".getBytes()));
      channel.register(selector,SelectionKey.OP_READ);
    } else if (key.isReadable()) {
      SocketChannel channel = (SocketChannel)key.channel();
      //充值position为0,limit为capacity,避免重复读取之前遗留的buffer数据
      buffer.clear();
      //读取,
      channel.read(buffer);
      //设置limit为position,为下一次从buffer中读取做准备
      buffer.flip();
      channel.write(buffer);
    }
    keyIterator.remove();
  }
}

整体而言,对ByteBuffer的使用还是略显繁琐,特别需要手工管理markpositionlimitcapacity这四个下标索引,容易出现遗漏调用flip()clear()导致一些异常情况的出现。

正是因为ByteBuffer的一些缺点,以及在NIO变成中的重要性,Netty使用独有的Buffer API代替NIOByteBuffer来表示字节序列。这种方式相比使用ByteBuffer具有明显的优势。Netty的新Buffer类型,ByteBuf(在Netty3中为ChannelBuffer),被设计用来彻底解决ByteBuffer存在的问题,并满足网络应用开发人员常见的需求。下面是一些很酷的特性:

  • 允许自定义缓存类型
  • 复合缓存类型中,内建透明的zero copy实现
  • 开箱即用的动态Buffer类型,提供类似StringBuffer的按需拓展能力
  • 不再需要手工调用flip()
  • 一般情况下会比ByteBuffer更快

可拓展性

ByteBuf有一组丰富并经过优化的操作,由于快速实现自定义协议。举一个例子,ByteBuf提供多种操作和访问无符号值,字符串并在缓冲中搜索一定的byte序列。你可以通过拓展或者包装 已经存在的buffer类型来提供便捷的访问。自定义的Buffer类型依然实现了ByteBuf的接口,而不是引入一个不兼容的类型。

透明的Zero Copy

为了让网络应用的性能提高到极致,你需要减少内存拷贝操作的次数。你可能有一组Buffer需要经过切分,组合形成完整的消息,Netty提供一个组合Buffer,允许你通过任意数量的既有Buffer创建一个新的buffer,并且不需要进行内存拷贝。举个例子,一个消息由两部分组成,消息头和消息体,在一个模块化的应用,这两部分的数据可能分别由两个模块产生并在消息发送之前进行组合。

如果使用ByteBuffer,你必须创建一个大的Buffer,并把header和body两部分拷贝到新的buffer。或者,你可以使用NIO来执行收集写操作,但是这限制你需要将组合后的Buffer表示为一个数组,而不是单一的Buffer。并且,这种方法破坏了原本的抽象,并引入复杂的状态管理,更重要的是,如果你不准备使用NIOChannel来进行读写,这个方法是不可用的。

1
2
//组合对象不兼容组件对象
ByteBuffer[] message = new ByteBuffer[] {header,body};

作为比较,ByteBuf并没有类型ByteBuffer这么多的限制,ByteBuf是完全可拓展的,并且拥有一个内建的组合Buffer类型。

1
2
3
4
5
6
7
8
//组合对象和组件对象都是ByteBuf
ByteBuf message = Unpooled.wrappedBuffer(header,body);

//因此你也可以把组合后的ByteBuf当做一个普通的ByteBuf对象和其他原生的ByteBuf类型进行组合
ByteBuf messageWithFooter = Unpooled.wrappedBuffer(message,footer);

//因为组合对象依然是一个ByteBuf,你可以便捷地访问他的内容,并且访问方法也表现的像一个简单的Buffer,你甚至可以跨越原来的组件长度,进行跨多个组件对象的操作。
messageWithFooter.getunsignedInt(messageWithFooter.readableBytes() - footer.readableBytes() -1);

容量自动扩展

许多协议定义可变长度的消息,这代表着你没有办法确定消息的长度,直到你完成消息的构建,如果消息没有完全构建完毕,则很难有便捷的方法获取精确的消息长度。就像你创建一个String,经常需要估算最后的String长度,让StringBuffer按照自身的需求拓展,

1
2
3
4
5
6
7
8
9
//一个动态Buffer 创建,在内部,实际Buffer的创建是延迟创建的,用于避免潜在的内存空间浪费
ByteBuf b = Unpooled.buffer(4);
//当第一次尝试写入的时候,内部的Buffer使用指定的初始化容量
b.writeByte('1');
b.writeByte('2');
b.writeByte('3');
b.writeByte('4');
//当写入的字节操作了容量之后,内部buffer会自动进行容量的拓展
b.writeByte('5');

更好的性能

大部分高频率使用的Buffer实现,ByteBuf是一个 byte 数组 ( byte[] ) 非常轻量级的封装,不像ByteBufferByteBuf没有错综复杂的边界检查和索引补偿,因此JVM可以更容易地优化buffer访问,更复杂的Buffer实现,仅用于拆分和组合bufferByteBuf的性能表现和ByteBuffer一致。

This post is licensed under CC BY 4.0 by the author.