数据的载体
这一篇,我们将学习Netty提供提供的核心功能以及它如何基于此构建出的完整的网络程序开发栈。要牢记上面这幅图。
1. Rich Byte Buffer
如果直接翻译过来就是富字节数组,你如果觉得这个名字很陌生,应该听过html叫做富文本吧。那么html为什么叫做富文本呢?
这个富字节数组在Netty中是有专门的抽象表示的,叫ByteBuf
。它是Java NIO中的ByteBuffer
的升级版。所以它叫Rich Byte Buffer。下面,我们先来看一看Java NIO中的ByteBuffer
。
1.1 ByteBuffer
我们知道在IO程序中,如果一堆数据一个一个传输效率太低了,把它们集中起来一起传输效率会提高很多,所以应该用一种数据结构承载这些数据。很容易想到数组,再通用一些,就是字节数组Byte Array。那么Java NIO为什么又弄出来个ByteBuffer
呢。这再一次体现了Java的一切接对象的设计哲学。把原生Byte Array这种面向过程的数据结构,抽象出来成为面向对象的ByteBuffer
。就可以往里面加入许多功能,把一些buffer相关的操作以及操作后的状态聚合在一起,需要替换另外一个缓存技术时,整体直接替换。这不就是高内聚,低耦合的设计原则吗。那么这种抽象的数据结构长什么样子呢?
ByteBuffer
里面,定义了4个属性,记录各种操作后的状态,比如在写日志文件时,第一次写完,第二次要接着第一次写,内部要有指针指向第一次结束的位置,要不然每次都从开头写,日志只有最新的记录,那日志就没有意义了。
- Capacity: Buffer 可以承载的最大数据量
- Limit: 读写的界限
- Position: 当前读写到的位置
- Mark: 可选地,可以记录一个位置,Position 以后可以恢复到这个位置
详细的用法参考ByteBuffer官方文档
那么这个对比普通的字节数据有什么好处呢?
- 支持比较
它既然抽象出来成为类,所以它可以重写equals
方法,自然就可以支持比较了。 - 任意字节顺序(大端和小段)
ByteBuffer
允许你以任意的字节顺序读写数据,像大端和小端。网络中的字节顺序就是大端。 - 读写有类型的数据
ByteBuffer
提供了可以直接读写指定类型数据的方法。例如putInt()
、putDouble
、getInt()
等。 - 动态扩容
ByteBuffer
在创建之后,不行传统数组,容量就固定了,还可以扩容。 - 线程安全
ByteBuffer
提供了线程安全的操作方法。 - 可以访问堆外内存
java程序是运行在JVM上的,JVM也是一个程序,它运行时用的空间是向操作系统申请的,java程序运行时用的空间一般都是是向JVM申请的,受JVM控制的。有一般就有特殊,java程序可以直接向操作系统申请空间,从而对该空间的利用不收JVN的管理。当然,这样的话,这块内存的回收应该就只能由程序员自己管理了,否则就会导致内存泄露。
1.2 ByteBuf
Netty使用它自己的ByteBuffer-ByteBuf,而不用Java NIO的ByteBuffer,因为它更具优势。
- 你可以根据需要定义自己的buffer类型
- 可以实现零拷贝传输
- 它可以动态扩容
- 不再需要调用
flip()
- 它比
ByteBuffer
速度快
更多的信息参考netty api doc
1.2.1 包的拆分和合并
在网络通信层之间,数据要进行包的拆分和合并。例如,如果到来的数据是被分成多个包的话,使用前,需要合并数据包。传统的方式,是先创建一个新的buffer,然后把每个buffer的数据拷贝到新的buffer中。
Netty支持零拷贝,它内部用指针指向需要拷贝的数据,从而避免拷贝数据。
requestPart1 = buffer1.slice(OFFSET_PAYLOAD, buffer1.readableBytes() - OFFSET_PAYLOAD);
requestPart2 = buffer2.slice(OFFSET_PAYLOAD, buffer2.readableBytes() - OFFSET_PAYLOAD);
request = unpooled.wrappedBuffer(requestPart1, requestPart2);