Java NIO 随笔
来源:互联网 发布:电子相册制作软件 知乎 编辑:程序博客网 时间:2024/06/04 19:05
- 前言
- Java NIO 和 BIO的区别
- 编辑方式的区别
- BIO的通讯模型
- NIO的通讯模型
- Selector 的工作模式
- Selector的工作流程图
- 通用的NIO Socket的编写方式
- Selector 在 SocketChannel 上的通讯流程
- NIO 与 BIO 的比较
- NIO的Socket通讯时序图
前言
最近在看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);// 反复循环,等待IOwhile (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编程—-通信是这样炼成的
BIO通讯模型在 少量请求、高数据流 的情况下处理非常有效。 但是针对于大量请求、低数据流的情况, 就面临着这样的问题:
- 大量客户端连接,则需要建立大量的线程处理。线程的创建、销毁、上下文切换开销巨大。
- 流式读取, 每次从内核空间将字节拷贝到线程空间,处理完毕之后再交给内核空间取发送,有IO开销。
- 阻塞式IO, 在网络环境较差的情况下,线程的
read()
/write()
方法是阻塞的,线程周期长,线程调度会占据较多时间。 - 如上述,如果在流式读取中,每次
read()
或者write()
还做一些其它的业务操作,线程调度将会占据更多的时间。 - 在使用线程池的情况下,高并发,低数据量的IO请求,可能导致内核空间大量请求堆积。
NIO的通讯模型刚好就解决掉了上述的问题。
NIO的通讯模型
NIO面向事件而设计。由 Selector
进行监听和分发。 每一个处理Chanel在向Selector
注册的时候必须选定监听的事件。
图片来源:Java NIO:浅析I/O模型
具体流程可以浅析为:
- 各种Chanel(TCP/UDP/File)向
Selector
注册(一般只会写一种类型,否则代码的可编辑性很低)。 Selector
根据不同的内核空间事件通知对不同的Channel进行调度。- Channel的处理面向缓冲区,通过建立堆外内存将待处理数据写入缓冲区内。
- 缓冲区写结束了之后Channel才会继续处理。
Selector
在这一系列操作中并不阻塞。
Selector 的工作模式
Selector的工作流程图
Selector除了在获取内核通知的时候,其它时候都不阻塞。见文章:Java NIO——Selector机制源码分析—转
下图是Selector调用内部类
SubSelector
的调用时序图。其中poll0()
方法是一个本地方法。它的作用是在一个等待时间段里面,如果有注册好的Channel的IO事件则立即返回,否则等到等待时间之后返回。
如上所述, 在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
在最初注册的时候, 根本不知道有没有链接会到来,会在什么时候到来。 因此必须由代码手动注册SocketChannel
给Selector
, 它才知道对每个不同的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通讯时序图
依据以上描述, 补充时序图如下:
- Java NIO 随笔
- Java NIO 随笔(二)
- Java NIO 随笔
- NIO随笔
- Java NIO: NIO概述
- Java NIO:NIO概述
- Java NIO:NIO概述
- Java NIO:NIO概述
- Java NIO:NIO概述
- Java NIO:NIO概述
- Java NIO:NIO概述
- Java NIO:NIO概述
- NIO--JAVA NIO 入门
- Java NIO:NIO概述
- Java NIO:NIO概述
- Java NIO:NIO概述
- Java NIO:NIO概述
- Java NIO:NIO概述
- springMVC 实现的增删查(没有数据库,用session代替)
- oracle表的分区本地索引以及全局索引
- openGl oom的解决方法
- 第8次C练习
- Fiddler抓取手机HTTP/HTTPS请求(3)
- Java NIO 随笔
- mysql group by获取第一组数据
- AltCoin
- linux解压、解压缩文件
- 使用li标签布局
- Android酷炫实用的开源框架(UI框架)
- linux下通过V4L2驱动USB摄像头
- “+[SomeClass initialize] may have been in progress in another thread when fork() was called”
- Log4j学习汇总(二)