NIO原理解析

NIO中主要包括几大组件,selector、channel、buffer。selector后面介绍,channel则类似于BIO中的流,但是流的读取是单向的,例如只能读,或只能写,但是channel则是双向的。数据可以从channel读到buffer中,也可以从buffer中写入到channel中。

NIO原理解析

针对于客户端请求服务端的场景,NIO实现的结构图如下:

NIO原理解析

 禁止盗图,画了好久。。。

一、channel

channel主要包括以下几类

  • FileChannel-------------------------->从文件中读写数据
  • DatagramChannel------------------>通过UDP读写网络中的数据
  • SocketChannel----------------------->通过TCP读写网络中的数据
  • ServerSocketChannel--------------->监听新进来的TCP连接,并生成一个SocketChannel对象

基本示例:

//创建能访问任意位置的file文件
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
//获取fileChannel对象
FileChannel inChannel = aFile.getChannel();
//分配48字节大小的byteBuffer
ByteBuffer buf = ByteBuffer.allocate(48);
//读取channel中的数据到buffer中
int bytesRead = inChannel.read(buf); while (bytesRead != -1) {
System.out.println("Read " + bytesRead);
//将buffer从写模式切换到读模式
buf.flip();
while(buf.hasRemaining()){
System.out.print((char) buf.get());
}
  //清空整个缓冲区
buf.clear();
  //缓冲区已经全部读完,返回-1退出循环
bytesRead = inChannel.read(buf);
} aFile.close();

另外channel不仅可以读取数据到buffer中,当存在多个channel并且其中有一个channel为fileChannel时,channel之间可以互相传输数据

transferFrom() :可以将数据从源通道传输到FileChannel中

RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
toChannel.transferFrom(position, count, fromChannel);

transferTo()

RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
fromChannel.transferTo(position, count, toChannel);

二、Buffer

1、buffer主要包括如下几类:

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

2、使用buffer的一般步骤:

  • 写入数据到Buffer
  • 调用flip()方法
  • 从Buffer中读取数据
  • 调用clear()方法或者compact()方法------------>clear()方法会清空整个缓冲区,compact()只会清除已经读过的数据

3、buffer中的概念

为了理解Buffer的工作原理,需要熟悉它的三个属性:

  • capacity
  • position
  • limit

position和limit的含义取决于Buffer处在读模式还是写模式。不管Buffer处在什么模式,capacity的含义总是一样的。

NIO原理解析

capacity

作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。

position

当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1.

当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0. 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。

limit

在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。

当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)

4、常用方法

a、分配大小

//分配48字节大小的byteBuffer
ByteBuffer buf = ByteBuffer.allocate(48);
//分配2014字符大小的charBuffer
CharBuffer buf = CharBuffer.allocate(1024);

b、写数据到buffer中

包括两种方式:一是从channel中读数据到buffer中,另外一种就是调用buffer的put()方法

//一、channel读取数据到buffer
int bytesRead = inChannel.read(buf);
//二、调用channel的put()方法
buf.put(127);

c、读取数据

同样包括两种方式:一是写入数据到channel中,另外一种就是调用buffer的get()方法

//一、将数据写入到channel中
int bytesWritten = inChannel.write(buf);
//二、调用buffer的get()方法读取数据
byte aByte = buf.get();

d、读写模式切换

buffer.flip()

e、清除buffer

一旦读完Buffer中的数据,需要让Buffer准备好再次被写入。可以通过clear()或compact()方法来完成。

如果调用的是clear()方法,position将被设回0,limit被设置成 capacity的值。换句话说,Buffer 被清空了。Buffer中的数据并未清除,只是这些标记告诉我们可以从哪里开始往Buffer里写数据。

如果Buffer中有一些未读的数据,调用clear()方法,数据将“被遗忘”,意味着不再有任何标记会告诉你哪些数据被读过,哪些还没有。

如果Buffer中仍有未读的数据,且后续还需要这些数据,但是此时想要先先写些数据,那么使用compact()方法。

compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。

另外NIO支持scatter/gather,说白了,就是一个channel可以读取数据到多个buffer中去,或者多个buffer可以写入到channel中。当第一个buffer写满之后,会紧接着读取到第二个buffer中去,如下图:

channel-->buffer

NIO原理解析

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray);

buffer-->channel

NIO原理解析

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);

三、selector

NIO原理解析

为啥使用selector?在传统的BIO当中,监听每个客户端的请求都需要一个线程去处理,线程数的上升会涉及到大量的上下文切换的操作,这也是非常浪费性能的。NIO中基于事件驱动的理念,使用selector监听各种事件的发生,可以实现只开启一个线程

就可以管理所有的请求,当然实际情况合理的增加线程数可以大大提高性能。

注意:与Selector一起使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。

1、注册channel到selector中

channel.configureBlocking(false);
SelectionKey key = channel.register(selector,Selectionkey.OP_READ);

selectionKey用来描述事件,包括事件类型,以及对应的selector与channel等等。

第二个入参为事件类型,主要包括四种:

  • Connect
  • Accept
  • Read
  • Write

分别用常量表示为:

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE

当然selector监听channel时,可以对多个事件感兴趣,写法如下:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

可能对selector、channel、事件三者的关系有点乱,用故事来总结一下:selector是父母,channel是孩子,父母监督孩子学习,孩子有很多同学,有些同学学习好,有些同学学习差,父母欢迎学习好的学生来家里玩,排斥成绩差的。那么特定的事件就可以理解

成那些成绩差的同学来家中,父母监听到了,开始行动,将他们赶走。

2、selectionKey

当channel注册到selector中时,会返回一个selectionKey对象,可以理解成事件的描述或是对注册的描述,主要包括这几个部分:

a、interest集合----------------------->感兴趣的事件集合。

int interestSet = selectionKey.interestOps();
//判断事件是否在集合中
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;

b、ready集合-------------------------->通道已经准备就绪的操作的集合

int readySet = selectionKey.readyOps();
//事件是否在已准备就绪的集合中,selectionKey提供了如下方法
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();

c、Channel

Channel  channel  = selectionKey.channel();

d、Selector

Selector selector = selectionKey.selector();

e、附加的对象(可选)

用户也可以将buffer等其他对象加到selectionKey上,方便后续操作

添加对象:

//添加对象到selectionKey有两种方式
selectionKey.attach(theObject);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
//获取此附加对象
Object attachedObj = selectionKey.attachment();

3、select()

当把channel注册到selector中去之后,可以通过select()方法来监听对应channel的特定事件。主要有三种select方法:

  • int select()-------------------------------------->阻塞到被监听的channel至少有一个事件发生。
  • int select(long timeout)---------------------->在timeout(ms)的时间内阻塞,直到被监听的channel至少有一个事件发生。
  • int selectNow()--------------------------------->非阻塞,不管有没有事件发生,都立马返回。

4、selectedKeys()

Set selectedKeys = selector.selectedKeys();

完整的示例:

Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
while(true) {
int readyChannels = selector.select();
if(readyChannels == 0) continue;
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.remove();
}
}

四、DatagramChannel

前面的示例都是基于TCP连接,现在讲述一下UDP的示例.

DatagramChannel是一个能收发UDP包的通道。因为UDP是无连接的网络协议,所以不能像其它通道那样读取和写入。它发送和接收的是数据包。

1、打开DatagramChannel

DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(9999));

2、接收数据

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
channel.receive(buf);

3、发送数据

String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
int bytesSent = channel.send(buf, new InetSocketAddress("jenkov.com", 80));
上一篇:Java线程间通信方式剖析——Java进阶(四)


下一篇:当我们说线程安全时,到底在说什么——Java进阶系列(二)