Java NIO 随笔

前言

最近在看Java NIO, 做下记录。操作平台皆基于Windows 10, JDK8, TCP Socket.
记录里面把 普通IO 成为 BIO。

记录提纲:

  • Java NIO的设计模式
  • Selector的阻塞原理
  • ServerSocketChannel的TCP通讯流程

Java NIO 和 BIO的区别

编辑方式的区别

Java BIO是基于流的,一旦收到消息就会立马处理,但是存在等到流收取事件。 NIO是基于channel的,操作系统内核协助获取完毕数据流之后才会交给业务代码处理。

以Socket为例:

普通IO:

//1、创建一个服务器端Socket,即ServerSocket,指定绑定的端口,并监听此端口
ServerSocket serverSocket =newServerSocket(10086);//1024-65535的某个端口
//2、调用accept()方法开始监听,等待客户端的连接
Socket socket = serverSocket.accept();
//3、获取输入流,并读取客户端信息
InputStream is = socket.getInputStream();
...
socket.shutdownInput();//关闭输入流
//4、获取输出流,响应客户端的请求
OutputStream os = socket.getOutputStream();
...
//5、关闭资源
...
serverSocket.close();

NIO:

Selector selector = Selector.open();
// 打开监听信道 
ServerSocketChannel listenerChannel = ServerSocketChannel.open();
// 与本地端口绑定
listenerChannel.socket().bind(new InetSocketAddress(12345));
// 设置为非阻塞模式
listenerChannel.configureBlocking(false);
// 将选择器绑定到监听信道,只有非阻塞信道才可以注册选择器.并在注册过程中指出该信道可以进行Accept操作
listenerChannel.register(selector, SelectionKey.OP_ACCEPT);

// 反复循环,等待IO
while (true) {
    // 等待某信道就绪(或超时)
    if (selector.select(TIME_OUT) == 0) {
        continue;
    }
    // 取得迭代器.selectedKeys()中包含了每个准备好某一I/O操作的信道的SelectionKey
    Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator();
    while (keyIter.hasNext()) {
        SelectionKey key = keyIter.next();
        // 处理信息
        dealWith(key);
        // 移除处理过的键
        keyIter.remove();
    }
}

可以看出, BIO需要获取到输入输出流,阻塞性的进行输入输出操作。
而NIO则使用了Selector,这个Selector可以绑定多个 Chanel, 例如实现TCP、UDP、File的操作,灵活性更高。

BIO的通讯模型

对于BIO而已,不同的IO类型处理的事件是不一样的,比如写TCP处理就必须使用ServerSocket, 读写文件处理则必须使用InputStream/OutputStream等。

由于BIO的通讯模型中所有的操作都是阻塞式的,因此为了实现对多客户端的处理,则必须频繁的创建大量的线程来针对每一个连接进行处理。

Java Socket编程----通信是这样炼成的
图片来源: Java Socket编程—-通信是这样炼成的

BIO通讯模型在 少量请求、高数据流 的情况下处理非常有效。 但是针对于大量请求、低数据流的情况, 就面临着这样的问题:

  • 大量客户端连接,则需要建立大量的线程处理。线程的创建、销毁、上下文切换开销巨大。
  • 流式读取, 每次从内核空间将字节拷贝到线程空间,处理完毕之后再交给内核空间取发送,有IO开销。
  • 阻塞式IO, 在网络环境较差的情况下,线程的read()/write()方法是阻塞的,线程周期长,线程调度会占据较多时间。
  • 如上述,如果在流式读取中,每次 read()或者write()还做一些其它的业务操作,线程调度将会占据更多的时间。
  • 在使用线程池的情况下,高并发,低数据量的IO请求,可能导致内核空间大量请求堆积。

NIO的通讯模型刚好就解决掉了上述的问题

NIO的通讯模型

NIO面向事件而设计。由 Selector进行监听和分发。 每一个处理Chanel在向Selector注册的时候必须选定监听的事件。
Java NIO:浅析I/O模型

图片来源:Java NIO:浅析I/O模型

具体流程可以浅析为:

  1. 各种Chanel(TCP/UDP/File)向Selector注册(一般只会写一种类型,否则代码的可编辑性很低)。
  2. Selector根据不同的内核空间事件通知对不同的Channel进行调度。
  3. Channel的处理面向缓冲区,通过建立堆外内存将待处理数据写入缓冲区内。
  4. 缓冲区写结束了之后Channel才会继续处理。Selector在这一系列操作中并不阻塞。

Selector 的工作模式

Selector的工作流程图

Selector除了在获取内核通知的时候,其它时候都不阻塞。见文章:Java NIO——Selector机制源码分析—转

下图是Selector调用内部类 SubSelector 的调用时序图。其中 poll0() 方法是一个本地方法。它的作用是在一个等待时间段里面,如果有注册好的Channel的IO事件则立即返回,否则等到等待时间之后返回。

Created with Raphaël 2.1.0 Selector Selector SubSelector SubSelector Selector的实现: windows:WindowsSelectorImpl Linux:KQueueSelectorImpl select(); select(long); doSelect(long); poll(); poll0(long, int, int[], int[], int[], long);

如上所述, 在poll0() 中会将当前等待时间段里面所监听到的所有的IO句柄进行归类分集,并根据数量的不同生成不同数量的线程,生成SelectKey再交给业务代码处理。需要注意的是, 在没有使用 独立线程/线程池 去处理每个SelectKey的情况下, NIO是依然同步的

通用的NIO Socket的编写方式

通常的NIO Socket会这么编写:

{
    Selector selector = Selector.open();
    // 打开监听信道 目前只做TCP支持
    ServerSocketChannel listenerChannel = ServerSocketChannel.open();
    // 与本地端口绑定
    listenerChannel.socket().bind(new InetSocketAddress(ListenPort));
    // 设置为非阻塞模式
    listenerChannel.configureBlocking(false);
    // 将选择器绑定到监听信道,只有非阻塞信道才可以注册选择器.并在注册过程中指出该信道可以进行Accept操作
    listenerChannel.register(selector, SelectionKey.OP_ACCEPT);
    // 反复循环,等待IO
    while (true) {
        // 等待某信道就绪(或超时)
        if (selector.select(TIME_OUT) == 0) {
            continue;
        }
        // 取得迭代器.selectedKeys()中包含了每个准备好某一I/O操作的信道的SelectionKey
        Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator();

        while (keyIter.hasNext()) {
            SelectionKey key = keyIter.next();
            try {
                if (key.isAcceptable()) {
                    /*clientChannel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufferSize));*/
                    protocol.handleAccept(key);
                }

                if (key.isReadable()) {
                    /*key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);*/
                    protocol.handleRead(key);
                }

                if (key.isValid() && key.isWritable()) {
                    protocol.handleWrite(key);
                }
            } catch (IOException ex) {
            }
            // 移除处理过的键
            keyIter.remove();
        }
    }
}

可是, 这么编写的原因是什么呢?

首先, ServerSocketChannel是只支持SelectionKey.OP_ACCEPT的(见ServerSocketChannel#validOps)。这样的设计就是专为TCP连接而设定的。

其次,对于SocketChannel,它支持SelectionKey.OP_READ | SelectionKey.OP_WRITE | SelectionKey.OP_CONNECT。 这种设计就表示它专门用于处理ServerSocketChannel产生的连接信息,然后对其进行读写操作。

再次,因为Selector在最初注册的时候, 根本不知道有没有链接会到来,会在什么时候到来。 因此必须由代码手动注册SocketChannelSelector, 它才知道对每个不同的SocketChannel进行处理(注意Selector本身是没有显式的提供移除Channel功能的)。

继续,如果在key.isAcceptable()代码块里没有做SelectionKey.OP_READ, 将会导致key.isReadable()返回值为false。 同样的,对于一个既定的Channel而言,想要修改它在Selector上注册的操作类型,需要调用key.interestOps()方法去修改。它会让Selector重新生成一条事件通知。

最后,NIO Servlet用于处理事件处理的线程只有一个。其包含不限个数(一般是3~5个)的子线程,专用于从操作系统获取事件信息及数据发送。

如上。 普通NIO的编写模式大多大同小异。都是通过ServerSocketChannel获取SocketChannel,并向Selector注册。 然后由独立的业务代码进行处理。 可以理解为用一个线程去处理所有的请求。 如果想要使用多线程去处理, 可以使用多个Selector, 再配合线程池。 这样效率会有大幅度的提升(需要注意每个Selector都会向自己注册的Channel发起通知,业务代码需要判断“SocketChannel是否已经被其它代码处理了)。

Selector 在 SocketChannel 上的通讯流程

NIO 与 BIO 的比较

NIO和BIO是同源的,且NIO也是基于BIO的。

BIO的几个问题,NIO在技术层面上做了规避。可以把 NIO理解为BIO的聚合,把IO操作交给了操作系统去做。

阻塞性对比:
NIO的不阻塞在于: NIO是等到数据拷贝已经完毕之后才会对数据进行处理。
BIO的阻塞在于:BIO对于每次的数据请求都会立即处理。

由此,NIO可以更快的处理更多的数据,之所以说它IO不阻塞,是因为它处理IO的时候IO都还没有完成,而操作系统的IO性能是高于JVM的; BIO可以处理少量但是更大的数据,因为它是流式的。

同异步对比:
二者皆支持同步/异步模式。 默认没有使用 线程/线程池 的情况下, 二者的编写方式都是同步的。

内存消耗对比:
BIO使用的堆内存,只要每次buffer不要设置太大,更多的是IO上、线程调度的性能考量。
NIO可以使用堆外内存,分批次将数据读取至缓冲区,需要注意连接的数量和每次缓冲区的大小。

NIO的Socket通讯时序图

依据以上描述, 补充时序图如下:

Created with Raphaël 2.1.0 客户端 客户端 服务器操作系统 服务器操作系统 NIO进程 NIO进程 业务线程 业务线程 请求报文 操作系统建立TCP连接 Selector获取到Accept事件 生成ServerSocketChannel SelectKey 逐个处理SelectorKey(Accept) 通过ServerSocketChannel 获取到SocketChannel 注册SocketChannel 读事件 逐个处理SelectorKey(Read) 将ByteBuffer数据获取 业务处理 注册SocketChannel 写事件 逐个处理SelectorKey(Write) 请求处理结果 响应报文
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值