在NIO中,数据的读写操作始终是与缓冲区相关联的.读取时信道(SocketChannel)将数据读入缓冲区,写入时首先要将发送的数据按顺序填入缓冲区.缓冲区是定长的,基本上它只是一个列表,它的所有元素都是基本数据类型.ByteBuffer是最常用的缓冲区,它提供了读写其他数据类型的方法,且信道的读写方法只接收ByteBuffer.因此ByteBuffer的用法是有必要牢固掌握的.
1.创建ByteBuffer
1.1 使用allocate()静态方法
ByteBuffer buffer=ByteBuffer.allocate(256);
以上方法将创建一个容量为256字节的ByteBuffer,如果发现创建的缓冲区容量太小,唯一的选择就是重新创建一个大小合适的缓冲区.
1.2 通过包装一个已有的数组来创建
如下,通过包装的方法创建的缓冲区保留了被包装数组内保存的数据.
ByteBuffer buffer=ByteBuffer.wrap(byteArray);
如果要将一个字符串存入ByteBuffer,可以如下操作:
String sendString="你好,服务器. ";
ByteBuffer sendBuffer=ByteBuffer.wrap(sendString.getBytes("UTF-16"));
2.回绕缓冲区
buffer.flip();
这个方法用来将缓冲区准备为数据传出状态,执行以上方法后,输出通道会从数据的开头而不是末尾开始.回绕保持缓冲区中的数据不变,只是准备写入而不是读取.
3.清除缓冲区
buffer.clear();
这个方法实际上也不会改变缓冲区的数据,而只是简单的重置了缓冲区的主要索引值.不必为了每次读写都创建新的缓冲区,那样做会降低性能.相反,要重用现在的缓冲区,在再次读取之前要清除缓冲区.
4.从套接字通道(信道)读取数据
int bytesReaded=socketChannel.read(buffer);
执行以上方法后,通道会从socket读取的数据填充此缓冲区,它返回成功读取并存储在缓冲区的字节数.在默认情况下,这至少会读取一个字节,或者返回-1指示数据结束.
5.向套接字通道(信道)写入数据
socketChannel.write(buffer);
此方法以一个ByteBuffer为参数,试图将该缓冲区中剩余的字节写入信道.
# 字节缓冲区
ByteBuffer其实就是一个字节缓冲区, 在这里你可以对缓冲区的数据进行 字节级的操作. 这样的好处在于你可以比较方便的获取到底层的字节操作和字节数据. 比如你有一个int型数据, 而你想获取他的byte[]数组, 你可以这样做:
ByteBuffer buffer = ByteBuffer.allocate(1024); //分配一定的空间,1024
int i = 90;
buffer.putInt(i);
byte[] array = buffer.array(); //获取该buffer的数组,这个数组是跟该buffer一一对应的
for(int j =0; j <4;j++){
System.out.println(Integer.toBinaryString(array[j] & 0xFF));
}
# public static ByteBuffer wrap(byte[] array)
将array中的数据包装到ByteBuffer中
byte[] array = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap(array);
等效于
ByteBuffer buffer = ByteBuffer.allocate(1024); //分配一定的空间,1024
# flip
public final Buffer flip()
反转此缓冲区。首先对当前位置设置限制,然后将该位置设置为零。如果已定义了标记,则丢弃该标记。
当将数据从一个地方传输到另一个地方时,经常将此方法与 compact 方法一起使用。
我最终的理解是: 文档翻译得太差了, 把不应该翻译的内容也译成了中文, 所以反而不容易理解.
关键就在以下 2 处:
当前位置: 这个可以直观地理解为缓冲区中的当前数据指针, 或是 SQL 中的游标, 记为 curPointer.
限制: 这个可以理解成实际操作的缓冲区段的结束标记, 记为 endPointer.
反转: 这个完全是对 flip 这个词不负责的翻译, 如果参照 DirectX 里的 flip() 而译为翻转/翻页, 那就好理解得多, 就像写信/看信, 写/看完一页后, 翻到下一页, 眼睛/笔从页底重新移回页首.
这个翻转背后的操作其实就是 "把 endPointer 定位到 curPointer 处, 并把 curPointer 设为 0".
关于标记, 在这里不涉及. 下一句说到常与 compact 方法一起使用, 是可以想像的, 因为 compact 方法对数据进
行了压缩, 有效数据的真实长度发生了变化, 肯定需要用 flip 重新定位结束标记.
在填充, 压缩等数据操作时, curPointer 估计都是自动更新了位置的, 总是指向最后一个有效数据, 所以每次调
用 flip() 后, endPointer 就指向了有效数据的结尾, 而 curPointer 指向了 0 (缓冲起始处).
举个图例:
(c 和 e 分别代表 curPointer 和 endPointer 两个指针)
* 先是一个空的 ByteBuffer (大小为 10 字节)
-------------------
-------------------
c
e
* 然后填充 5 字节数据
-------------------
0 1 2 3 4
-------------------
e c
此时, endPointer 尚在 0 处, curPointer 移到了数据结尾.
经测试, 此时若取数据, 将得到 5 个字节, 内容通常为 0 (也有可能是未知), 因为实际上取到的是从 c 处到缓冲
区实际结束处的 5 个未初始化的字节.
(QZone 字体处理不正确, 此处 c 是在 4 的下面, e 在 0 的下面)
* 调用一次 flip() 后
-------------------
0 1 2 3 4
-------------------
c e
此时, endPointer 先被移到 curPointer, 然后 curPointer 移到 0.
通过测试可见, ByteBuffer 取数据时, 是从 curPointer 起, 到 endPointer 止, 若 curPointer > endPointer, 则取到缓冲区结束.
(QZone 字体处理不正确, 此处 c 是在 0 的下面, e 在 4 的下面)
再看上面代码的关键片段, 行 8 处调用 flip() 即有两个作用, 一是将 curPointer 移到 0, 二是将 endPointer 移到有效数据结尾.
此行可由以下两行代替:
buff.limit( buff.position());
buff.position( 0 );
可见对其工作原理的理解, 应该是正确的