美文网首页
一文搞定NIO的三大组件

一文搞定NIO的三大组件

作者: 千淘萬漉 | 来源:发表于2020-03-31 22:48 被阅读0次

提到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参数,就像下面这张图的样子。

Buffer结构

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

ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
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方法去往堆外空间来分配对象数组。

二、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选择器


1.Selector原理

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

具体的工作流程:Selecor通过一种类似于事件的机制来解决这个问题。首先你需要把你的连接,也就是 Channel 绑定到 Selector 上,然后你可以在接收数据的线程来调用 Selector.select() 方法来等待数据到来。这个 select 方法是一个阻塞方法,这个线程会一直卡在这儿,直到这些 Channel 中的任意一个有数据到来,就会结束等待返回数据。它的返回值是一个迭代器,你可以从这个迭代器里面获取所有 Channel 收到的数据,然后来执行你的数据接收的业务逻辑。既可以选择直接在这个线程里面来执行接收数据的业务逻辑,也可以将任务分发给其他的线程来执行,如何选择完全可以由你的代码来控制。

引用geektime.《消息队列高手课》

大致流程如下代码,也可以说是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<SelectionKey> keys = selector.selectedKeys();
    //遍历事件
    Iterator<SelectionKey> 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 线程组 EventLoopGroupServerBootstrap
  • 设置 ServerBootstrap的属性:线程组、SO_BACKLOG 选项,设置 NioServerSocketChannelChannel,设置业务处理 Handler
  • 绑定端口,启动服务器程序。
  • 在业务处理 Handler处理器 中,读取客户端发送的数据,并给出响应。

参考列表


1.不二程序.Java NIO三剑客详细剖析
2.玉刚说.面试官:什么是NIO?NIO的原理是什么?
3.占小狼的博客.深入浅出NIO之Selector实现原理
4.TheLudlows.NIO之终极Selctor源码分析

![关注公众号获取更多学习资料](https://img.haomeiwen.com/i8926909/acbea8e304b57e43.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/124

相关文章

  • 一文搞定NIO的三大组件

    提到NIO网络编程,就不得不提一下NIO中的三大组件: Buffer、Channel、Selector,JDK源码...

  • Android 网络编程3 Java NIO

    Android网络编程 目录 1、Java NIO 的核心组件 Java NIO的核心组件包括:Channel(通...

  • Netty之NIO

    ------NIO简介(1)-------- NIO组件 channel,buffer,selector,pip,...

  • Java NIO概览

    Java NIO 包含下列几个核心组件: ChannelsBuffersSelectors    Java NIO...

  • Overview

    Java NIO包括以下几个核心的组件: Channels Buffers Selectors Java NIO还...

  • Java NIO 概述

    Java NIO 包括以下核心组件: Channels Buffers Selectors Java NIO 中除...

  • Java NIO 概述

    Java NIO 主要包括一下核心组件: Channels Buffers Selectors Java NIO的...

  • 2. Java NIO 概述

    Java NIO由下面几个核心组件组成: Channel Buffer Selector Java NIO有更多的...

  • Netty快速入门(05)Java NIO 介绍-Selecto

    Java NIO Selector Selector是Java NIO中的一个组件,用于检查一个或多个NIO Ch...

  • NIO java编程

    NIO 同步式非阻塞式IO NIO组件:Buffer channel selector Buffer 缓冲区 1....

网友评论

      本文标题:一文搞定NIO的三大组件

      本文链接:https://www.haomeiwen.com/subject/vvubuhtx.html