NIO三大组件

目录

一、Buffer缓冲区

二、Channel通道

1.Channel和Stream的区别

2.Socketchannel

3.ServerSocketChannel

三、Selector选择器

四、NIO三大件的工作流程


提到NIO网络编程,就不得不提一下NIO中的三大组件:Buffer、Channel、Selector,JDK源码开发者在这里用了很通用形象的三个词语,去分别赋予三个类的抽象含义,Buffer即缓冲,Channel即管道,Selector为选择器,因为底层实现机制的复杂, 便于开发

者的理解就用了这三个词语去定义NIO的核心类,但是除了表面上的这层含义,Java开发者还是需要去花点时间,真正深入了解其背后的原理实现。

一、Buffer缓冲区

读NIO源码会发现Buffer最终是实现了一个在内存中的字节数组,所以一个 Buffer 本质上是内存中的一块,我们可以将数据写入这块内存,之后从这块内存获取数据。这一点改变了原有的IO方式,回忆一下原来的IO方式,我们的编程范式是从Stream流里去

读取数据,一个一个字节的读取,读完以后就放在自己提前定义的数组对象里面,读取的时候只能顺序移动下标、且不能回溯,但是NIO的buffer不一样了,内部使用字节数组存储数据,并维护几个特殊变量,实现数据的反复利用。

首先用mark来备份当前的position,然后用position表示当前可以写入或读取数据的位置,再用capacity来表示缓存数组大小,最后还有一个表示剩余容量的limit参数,就像下面这张图的样子。

NIO三大组件_第1张图片

Buffer提供了八种类型的Buffer,覆盖了能从 IO 中传输的所有的 Java 基本数据类型,但是在网络编程的场景下用的最多的还是ByteBuffer。

  1. ByteBuffer

  2. CharBuffer

  3. ShortBuffer

  4. IntBuffer

  5. LongBuffer

  6. FloatBuffer

  7. DoubleBuffer

JDK NIO原生的ByteBuffer功能虽然强大,但是在直接用于开发是有些费劲的,这时候经过Netty封装的ByteBuf则显得简单多了,ByteBuffer的实现类包括"HeapByteBuffer"和"DirectByteBuffer"两种,前者的Heap方式是基于堆内存使用的缓冲字节数组,而后

者的Direct则是使用的堆外内存(系统内存)。

HeapByteBuf最终往下追溯allocateArray()方法,可以发现这个是直接实例化New出来的字节数组,毫无疑问就是交给JVM托管的对象了。

protected byte[] allocateArray(int initialCapacity){
        return new byte[initialCapacity];
}

而DirectByteBuffer追溯到最终的allocatDirect方法里,可以看到此处调用JDK的native方法去往堆外空间来分配对象数组。

NIO三大组件_第2张图片

二、Channel通道

1.Channel和Stream的区别

通道是对原来 IO 包中的Stream流的模拟,通过Channel这个对象,我们可以读取和写入数据,并且通过Channel的所有发送数据都要读到缓冲区Buffer中,所有要接收的数据都要写到缓冲区Buffer里。NIO中的Channel和IO中的Stream最显著的区别如下:

  • 流是单向的,通道是双向的,可读可写。

  • 流读写是阻塞的,通道可以异步读写。

  • 流中的数据可以选择性的先读到缓存中,通道的数据总是要先读到一个缓存中,或从缓存中写入

解释一下为啥Stream流是单向的?之前用IO的方式去读写数据的时候,读写是要分离的,即必须要明确是InputStream还是OutputStream,而在Channel这里,一条连接客户端和服务端的Channel是共用的,NIO开发中可以利用 channel.read()方法去读取

socket缓冲区的数据,也可以通过 channel.write()去刷出数据到客户端。Channel的实现类很多,这里需要重点了解的就是 SocketchannelServerSocketChannel

再解释一下为何流是阻塞的而通道不是?流的read和write都是同步操作,在Stream中调用读写方法时,必须要等IO操作完成以后才能执行下一步,需要顺序执行而没有异步的方式可以用。而NIO中Channel的读写是可以设置为非阻塞的,非阻塞模式下,

write()方法在尚未写出任何内容时可能就返回了,这种模式下须得在while循环中判断来调用write()。

String newData = "New String to write to file..." + System.currentTimeMillis();

ByteBuffer buf = ByteBuffer.allocate(48);

buf.clear();

buf.put(newData.getBytes());

buf.flip();

while(buf.hasRemaining()) {

channel.write(buf);

}

2.Socketchannel

SocketChannel 暂时可以理解成一个 TCP 客户端(其实SocketChannel还可以作为服务端中Worker线程组要处理的TCP长连接),打开一个 TCP 连接的姿势如下:

// 打开一个通道

SocketChannel socketChannel = SocketChannel.open();

// 发起连接

socketChannel.connect(new InetSocketAddress("localhost", 80));

而读写数据的方式也很方便,读时read到缓冲buffer,写时刷出缓冲buffer即可:

// 读取数据

socketChannel.read(buffer);

// 写入数据到网络连接中

while(buffer.hasRemaining()) {

socketChannel.write(buffer);

}

3.ServerSocketChannel

ServerSocketChannel 可以理解为服务端,ServerSocketChannel 用于监听机器端口,管理从这个端口进来的 TCP 连接。

// 实例化

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

// 监听端口

serverSocketChannel.socket().bind(new InetSocketAddress(80));

while (true) {

// 一旦有一个 TCP 连接进来,就对应创建一个 SocketChannel 进行处理

SocketChannel socketChannel = serverSocketChannel.accept();

}

ServerSocketChannel 不和 Buffer 打交道了,因为它并不实际处理数据,它一旦接收到请求后,实例化 SocketChannel,之后在这个连接通道上的数据传递它就不管了,因为它需要继续监听端口,等待下一个连接;每一个TCP连接都分配给一个

SocketChannel来处理了,读写都基于后面的SocketChannel,这部分其实也是网络编程中经典的Reactor设计模式。

三、Selector选择器

Selector是三大组件中的最C位的对象,Selector建立在非阻塞的基础之上,IO多路复用在Java中实现就是它,它做到了一个线程管理多个Channel,可以向Selector注册感兴趣的事件,当事件就绪,通过 Selector.select()方法获取注册的事件,进行相应的

操作。

具体的工作流程:Selecor通过一种类似于事件的机制来解决这个问题。首先你需要把你的连接,也就是 Channel 绑定到 Selector 上,然后你可以在接收数据的线程来调用 Selector.select() 方法来等待数据到来。这个 select 方法是一个阻塞方法,这个线程会

一直卡在这儿,直到这些 Channel 中的任意一个有数据到来,就会结束等待返回数据。它的返回值是一个迭代器,你可以从这个迭代器里面获取所有 Channel 收到的数据,然后来执行你的数据接收的业务逻辑。既可以选择直接在这个线程里面来执行接收数

据的业务逻辑,也可以将任务分发给其他的线程来执行,如何选择完全可以由你的代码来控制。

NIO三大组件_第3张图片

大致流程如下代码,也可以说是NIO编程的模板代码:

Selector selector = Selector.open();

// 实例化一个服务端的ServerSocketChannel

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

//开启非阻塞

serverSocketChannel.configureBlocking(false);

//绑定端口

serverSocketChannel.socket().bind(new InetSocketAddress(1234));

//ServerSocketChannel注册selector,并表示关心连接事件

serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

//没有socket就绪,select方法会被阻塞一段时间,并返回0

while (selector.select() > 0) {

//socket就绪则会返回具体的事件类型和数目

Set keys = selector.selectedKeys();

//遍历事件

Iterator iterator = keys.iterator();

//根据事件类型,进行不同的处理逻辑;

while (iterator.hasNext()) {

SelectionKey key = iterator.next();

iterator.remove();

if (key.isAcceptable()) {

...

} else if (key.isReadable() && key.isValid()) {

...

}

keys.remove(key);

}

}

一个服务端程序启动一个Selector,在Netty中一个NioEventLoop对应一个Selector,Netty在解决JDK NIO的epoll空轮询bug时,采用的策略是废弃原来的有问题的Selector,然后重建一个Selector。因此在Reactor的主从反应堆这里,不同的反应堆可以取不同

的Selector事件来选择关心,可以用注册的事件有如下四种:

  • SelectionKey.OP_CONNECT

  • SelectionKey.OP_ACCEPT

  • SelectionKey.OP_READ

  • SelectionKey.OP_WRITE

例如主Reactor通常就是设计为关心的OP_ACCEPT,而从Reactor就更关心其余的读写以及连接状态事件。Selector中的select是实现多路复用的关键,这个方法会一直阻塞直到至少一个channel被选择(即该channel注册的事件发生了为止,除非当前线程发生

中断或者selector的wakeup方法被调用。

Selector.select方法最终调用的是EPollSelectorImpl的doSelect方法,在深入远吗就会发现其中的subSelector.poll() ,这里是select的核心,由native函数poll0实现了(不忍心贴代码,看懂要花挺多的时间了)。

四、NIO三大件的工作流程

所以将上面NIO的三大组件串起来,并结合Reactor设计模式用于网络编程开发的,基本模板代码思路就是如下:

  • 首先创建 ServerSocketChannel对象,和真正处理业务的线程池

  • 然后对上述 ServerSocketChannel对象进行绑定一个对应的端口,并设置为非阻塞

  • 紧接着创建 Selector对象并打开,然后把这 Selector对象注册到 ServerSocketChannel中,并设置好监听的事件,监听 SelectionKey.OP_ACCEPT

  • 接着就是 Selector对象进行死循环监听每一个 Channel通道的事件,循环执行 Selector.select()方法,轮询就绪的 Channel

  • 从 Selector中获取所有的 SelectorKey(这个就可以看成是不同的事件),如果 SelectorKey是处于 OP_ACCEPT状态,说明是新的客户端接入,调用 ServerSocketChannel.accept接收新的客户端。

  • 然后对这个把这个接受的新客户端的 Channel通道注册到 ServerSocketChannel上,并且把之前的 OP_ACCEPT状态改为 SelectionKey.OP_READ读取事件状态,并且设置为非阻塞的,然后把当前的这个 SelectorKey给移除掉,说明这个事件完成了

  • 如果第5步的时候过来的事件不是 OP_ACCEPT状态,那就是 OP_READ读取数据的事件状态,然后调用本文章的上面的那个读取数据的机制就可以了。

当然Netty的网络编程风格上要优化了许多,它的工作流程步骤:

  • 创建 NIO 线程组 EventLoopGroup 和 ServerBootstrap

  • 设置 ServerBootstrap的属性:线程组、SO_BACKLOG 选项,设置 NioServerSocketChannel 为 Channel,设置业务处理 Handler

  • 绑定端口,启动服务器程序。

  • 在业务处理 Handler处理器 中,读取客户端发送的数据,并给出响应。

你可能感兴趣的:(网络编程篇,netty,nio,socket,java)