NIO Buffer带给我们的好处是在进行I/O数据读写时可以用块模式进行了,这相比以往使用字节的模式效率提升不小。首先我们来看下NIO 给我们提供了什么样的Buffer,他们都有些啥子特点,我们在应用应该怎样使用他们以扬长避短达到最佳的效率。
NIO提供了2种类型的Buffer,Direct Buffer 和 Heap Buffer。 Direct Buffer是创建在非java heap上的系统级缓冲区,创建和销毁的开销大;但与I/O数据交换非常快,可以直接与I/O进数据交换。Heap Buffer创建于java heap 上,相当于一个byte[]的封装,创建和销毁开销小。但不能直接与I/O进行数据交换,必须通过一个临时的Direct Buffer来中转数据,所以与I/O的数据交换速度慢。
个人比较认同以下3点意见:
1、 劣势:创建和释放Direct Buffer的代价比Heap Buffer得要高;
2、 区别:Direct Buffer不是分配在堆上的,它不被GC直接管理(但Direct Buffer的JAVA对象是归GC管理的,只要GC回收了它的JAVA对象,操作系统才会释放Direct Buffer所申请的空间),它似乎给人感觉是“内核缓冲区(buffer in kernel)”。Heap Buffer则是分配在堆上的,或者我们可以简单理解为Heap Buffer就是byte[]数组的一种封装形式,查看JAVA源代码实现,Heap Buffer也的确是这样。
3、 优势:当我们把一个Direct Buffer写入Channel的时候,就好比是“内核缓冲区”的内容直接写入了Channel,这样显然快了,减少了数据拷贝(因为我们平时的read/write都是需要在I/O设备与应用程序空间之间的“内核缓冲区”中转一下的)。而当我们把一个Heap Buffer写入Channel的时候,实际上底层实现会先构建一个临时的Direct Buffer,然后把Heap Buffer的内容复制到这个临时的Direct Buffer上,再把这个Direct Buffer写出去。当然,如果我们多次调用write方法,把一个Heap Buffer写入Channel,底层实现可以重复使用临时的Direct Buffer,这样不至于因为频繁地创建和销毁Direct Buffer影响性能。
下面也摘录了一些网上的总总结:
好,前面说了这么多。那CshBBrain到底采用了怎样的使用策略呢。下面我就详细介绍下。
CshBBrain中全部使用Direct Buffer来与I/O进行数据交换,而且只有与I/O有进行数据交换时才使用Buffer.任何一个网络服务器要使用Buffer与I/O进行数据交换的地方无非是读取客户端的数据和向客户端发送数据的地方,CshBBrain中也不例外。
首先来看数据读取的实现,在CshBBrain中当有客户端发送数据到服务器时,服务器会给分配一个工作线程Worker来进行数据读取,协议解码,业务处理和协议编码。所以在每个Worker中都有创建一个Direct Buffer,这个Buffer 在工作线程创建时就已经创建了,每次把任务分配给该线程的时候该线程就重复利用此Buffer来从I/O读取数据,读取完数据后将Buffer重置以便后面的连接重复使用。
Worker类中的相关代码:
private ByteBuffer charBuffer = null;// 读取缓冲区 定义缓冲区
this.charBuffer = ByteBuffer.allocateDirect(1024 * bufferSize);// 创建读取缓冲区
public void process(SelectionKey selectionKey){
Client key = Client.getSockector(selectionKey);
if(key.getMessages(charBuffer)){ // 已完成握手,从客户端读取报刊,重复利用
key.process();// 进行业务处理
}
}
Client类中使用worker中的buffer读取数据:
do{// 读取客户端请求的数据并解码
dataLength = socketChannel.read(byteBuffer);
if(dataLength > 0){
byteBuffer.flip();
this.decoderHandler.process(byteBuffer,this);// 解码处理
byteBuffer.clear();
readSuccess = true;
this.readDataFlag = true;// 将读取数据标识设置为真
this.preBlank = false;// 上次不为空读
}else{
if(this.preBlank){
++this.readCount;
}
this.preBlank = true;
break;
}
}while(dataLength > 0);
最后来看CshBBrain中数据发送的实现,CshBBrain将处理好的内容放入Client对象的responseMsgs队列中,然后由sendMsgs方法进行数据发送。发送数据给I/O时并不创建Direct Buffer,而是从BufferPool中获取一个Direct Buffer.BufferPool是CshBBrain中的Direct Buffer缓存池,服务器启动时根据系统参数minBufferPoolSize、maxBufferPoolSize和writeBuffer创建好指定数量指定大小的Direct Buffer,CshBBrain每次想I/O写数据时,从BufferPool缓冲池中获取一个Direct Buffer使用,使用完毕将Direct Buffer 重置放回BufferPool中供其他线程使用。这样就避免每次写数据都重复创建缓存的情况。
发送数据到I/O的Response类中相关代码:
if(bb == null){// 如果缓冲区为空,则从缓冲池中获取一个缓冲区
bb = BufferPool.getInstance().getBuffer();
}
写完数据重置缓冲区,放回缓冲池
buffer.clear();
BufferPool.getInstance().releaseBuffer(buffer);// 将用完的缓冲区放回缓冲区池
总之在开源Websocket服务器CshBBrain中,全部使用Direct Buffer.在worker线程和BufferPool中维护Direct Buffer,并实现最大限度的重用这些Direct Buffer来达到最好一个效果。Direct Buffer 和Heap Buffer的选用上本来都比较麻烦,作者也还在不断探索,如果你有好的方案望不吝赐教。