[Netty] 基础-java.nio

satjd

2018/10/17

Categories: Java Netty Tags: netty

Netty是以Java nio为基础的一个高性能网络框架。要想深入的探究netty的实现机理,对于java nio体系的了解是及其必要的。 这里记录了我在阅读Java nio tutorial 这部分内容时的一些收获和心得。

Channel和Buffer

Channel顾名思义,是程序和所需数据之间的媒介。类似于bio中中stream的概念,但是和stream也有以下几点差别

  1. 一个Channel可读可写,而一个stream只能负责读和写中的一个
  2. Channel可以进行异步读写,而stream不行
  3. Channel必须要和一个Buffer配合才能读写数据,而stream不用显式和buffer配合

Channel和Buffer的关系:

Buffer的操作方式

Buffer中有几个重要的字段:

针对Buffer的读写操作实际上是写数据和修改上述几个字段的过程。Buffer有两种状态:读模式和写模式。一般来说,读Buffer中的数据时需要让Buffer置为读模式,向Buffer写数据时则需要让Buffer置为写模式。

但是,实际上,读模式写模式只是position,limit,capacity的不同状态,并没有一个字段来标识Buffer的实际模式,我们用两种模式进行区分实际上是避免我们因为对于上面三个字段控制不当而读到错误的数据

Buffer的字段含义如下图所示:

Buffer的几个重要方法:

  1. allocate
  2. get和put
  3. flip
  4. rewind
  5. clear和compact
  6. mark和reset
  7. equals和compareTo

allocate

是Buffer的类方法,调用后可得到一个Buffer对象。 常见的Buffer类有:

可以通过如下方式创建一个CharBuffer

CharBuffer buf = CharBuffer.allocate(1024);

get put

get: 从Buffer中读写数据的方法。get读取数据并对position++

buf.get();

除此之外,也可以通过Channel的read和write方法向某个Buffer读写数据。以FileChannel为例:

RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();

int bytesRead = inChannel.read(buf); //向buf写入inChannel的数据(写buffer)

int bytesWritten = inChannel.write(buf); //向inChannel写buf中的数据(读buffer)

put: 实际上包含了一系列方法,可以向buffer写数据并相应地对position增加对应size的字节

flip()

将position设置为0,并将limit设置为position的值。从上面的操作可以看出,实质上是将buffer从写模式切换为读模式

rewind()

将position设置为0,不改变limit。使用场景一般为对一个读模式的buffer重新从头开始读,或者写模式下重新从头写。

clear()和compact()

clear用于将buffer中的数据清空,实质上是将buffer置为初始写状态(position=0,limit=capacity) compact用于在读模式的buffer,将buffer中的剩余未读数据拷贝到开头,将position设置成第一个空位,limit设置为capacity,那么该buffer自然也被设置成写模式了。正如函数名compact的含义,这个方法将未读数据“压缩”到buffer开头,并切到写模式,在保留剩余数据的基础上可以对buffer继续写入

mark()和reset()

mark可以对buffer当前的position设置一个标记,之后可以通过reset将mark对应的标记赋值给position。相当于临时保存position,后续可恢复。

buffer.mark();

//call buffer.get() a couple of times, e.g. during parsing.

buffer.reset();  //set position back to mark. 

equals()和compareTo()

对position到limit之间的数据进行判等和比较 equals判等规则:

  1. 两个Buffer有相同的type,如ByteBuffer CharBuffer 等
  2. 两个Buffer的remaining的数据量相等(也就是position到limit的距离)相同
  3. 这些数据相等

compareTo比较规则:

  1. 先按位比较,有一位不同则判断出大小关系
  2. 若所有位均相等,比较remaining的数据量,数据量大的更大

NIO中的分散聚合(Scatter/Gather)

分散:Channel中的数据写入到多个buffer中

聚合:多个buffer中的数据聚合到一个Channel中

应用场景:从Channel中读取header和body数据,放到两个buffer中,从两个buffer中聚合成一份数据写入Channel.要注意的是,在分散的场景下,header的长度需要固定,而在聚合的场景下,header和body的长度都可变。

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);

ByteBuffer[] bufferArray = { header, body };

channel.read(bufferArray);
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);

//write data into buffers

ByteBuffer[] bufferArray = { header, body };

channel.write(bufferArray);

使用Channel的transferFrom或transferTo实现zero-copy

回顾一下java中实现文件拷贝的几种方式

1.新建两个stream(InputStream和OutputStream),然后读取InputS,写入OutputS

InputStream is = new FileInputStream("/home/songao/tale.txt");
OutputStream os = new FileOutputStream("/home/songao/taleCopy3.txt");

s = System.currentTimeMillis();
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) > 0) {
  os.write(buffer, 0, length);
}
System.out.println("IO Stream copy dur: " + (System.currentTimeMillis() - s));

2.使用Files.copy库函数

Path srcPath = FileSystems.getDefault().getPath("/home/songao/tale.txt");
Path dstPath = FileSystems.getDefault().getPath("/home/songao/taleCopy2.txt");

s = System.currentTimeMillis();
Files.copy(srcPath,dstPath);
System.out.println("Files.copy dur: " + (System.currentTimeMillis() - s));

3.使用nio Channel提供的transferFrom/transferTo方法

RandomAccessFile srcFile = new RandomAccessFile("/home/songao/tale.txt", "rw");
FileChannel srcChannel = srcFile.getChannel();

RandomAccessFile dstFile = new RandomAccessFile("/home/songao/taleCopy.txt", "rw");
FileChannel dstChannel = dstFile.getChannel();

long s = System.currentTimeMillis();
dstChannel.transferFrom(srcChannel,0,srcChannel.size());
System.out.println("zero copy dur: " + (System.currentTimeMillis() - s));

经过测试(文件大小350M),效率 3>2>1

zero copy dur: 932
Files.copy dur: 1444
IO Stream copy dur: 2397

为什么快?原因在于transferFrom/transferTo在内核态进行数据传输,省去了将buffer内数据拷贝到用户态的时间,这也叫做zero-copy,zero指的没有内核态到用户态的copy数据过程