I/O多路复用技术
I/O多路复用技术通过把多个I/O的阻塞复用到同一个 select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求,不需要创建新的额外进程或者线程
I/O多路复用的主要应用场景如下: ◎服务器需要同时处理多个处于监听状态或者多个连接状态的套接字; ◎服务器需要同时处理多种网络协议的套接字
Epoll作出的重大改进:
- 支持一个进程打开的 socket 描述符(FD)数量不受限制(仅受限于操作系统的最大文 件句柄数)。
- .l/O效率不会随着 FD 数目的增加而线性下降
- 使用 mmap 加速内核与用户空间的消息传递,避免不必要的内存复制
- epoll的 API 更加简单
NIO入门
认识BIO、NIO、NIO2.0
BIO:通常有一个独立的Acceptor线程负责监听客户端的连接
缺点:缺乏弹性伸缩能力,当线程数膨胀后,系统性能急剧下降
若没有客户端接入,主线程会阻塞在server socket的accept操作上
每当有一个新的客户端请求接入时,服务端必须创建一个新的线程处理新接入的客户端,一个线程只能处理一个客户端连接
伪异步IO
当有新的客户端接入时,将客户端的 Socket 封装成一个 Task(该任务实现 java.lang. Runnable 接口)投递到后端的线程池中进行处理(调用线程池的execute方法执行),JDK 的线程池维护一个消息队列和 X 个活跃线程,对消息队列中的任务进行处理。由于线程池可以设置消息队列的大小和最大 线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的 耗尽和岩机。
优点:避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题
缺点:在某些场景下,读取一方的线程也会被阻塞60s,写操作也可能会被阻塞,并不是真正的实现了无阻塞,只是使用了线程池
NIO:
概念简介:
- 缓冲区Buffer:实质上是一个数组,通常是一个字节数组,也可以使用其它类型的数组
- 通道channel:像自来水管一样,网络数据通过channel读取和写入,通道可以用于读写或二者同时进行。主要有用于文件读写的filechannel和网络读写的selectablechannel(子类有server socketchannel和socket channel)
- 多路复用器selector:多路复用器提供选择已经就绪的任务的能力。selector会不断地轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件, 这个Channel 就处于就绪状态,会被Selector 轮询出来,然后通过 SelectionKey 可以获取 就绪Channel 的集合,进行后续的I/O操作。由于JDK 使用了 epoll)代替传 统的 select 实现,所以它并没有最大连接句柄 1024/2048 的限制。这也就意味着只需要一 个线程负责Selector的轮询,就可以接入成千上万的客户端
NIO服务端主要创建过程:
步骤一: 打开ServerSocketChannel,用于监听客户端的连接,它是所有客户端连接的父管道,示例代码如下: ServerSocketChannel acceptorSvr = ServerSocketChannel.open();
步骤二:绑定监听端口,设置连接为非阻塞模式,示例代码如下 acceptorSvr.socket().bind(new Inetsocketaddress(Inetaddress.getByName(”工P“),port))
accetorSvr.configureBlocking(false);
步骤三:创建Reactor 线程,创建多路复用器并启动线程,示例代码如下。 ,Select○r selector = Selector.○pen();
New Thread(new ReactorTask()).start();
步骤四:将 ServerSocketChannel 注册到 Reactor 线程的多路复用器 Selector 上,监听 ACCEPT事件,示例代码如下
SelectionKey key=acceptorSvr.register(selector,SelectionKey.OP_ACCEPT, ioHardler);
步骤五:多路复用器在线程run方法的无限循环体内轮询准备就绪的Key,示例代码 如下
int num = selector.select();
Set selectedKeys = selector.selectedKeys()
Iterator it = selectedKeys.iterator();
while(it.hasNext()) selectionKegKeg =(SelectionKey)it.next()﹒ ,//·..dealwith I/O event··
步骤六:多路复用器监听到有新的客户端接入,处理新的接入请求,完成 TCP 三次 握手,建立物理链路,示例代码如下
socketChannel channel= svrChannel.accept()
步骤七:设置客户端链路为非阻塞模式,示例代码如下 ,channel.confiqureBlocking(false); channel.socket().setReuseAddress(true); 步骤八:将新接入的客户端连接注册到 Reactor 线程的多路复用器上,监听读操作, 读取客户端发送的网络消息,示例代码如下
SelectionKey key= socketChannel.register(selector,SelectionKeg.0P_READ, ioHandler);
步骤九:异步读取客户端请求消息到缓冲区,示例代码如下
Int readNumber_=channel.read(receivedBuffer);
步骤十:对 ByteBufer进行编解码,如果有半包消息指针reset,继续读取后续的报文, 将解码成功的消息封装成 Task,投递到业务线程池中,进行业务逻辑编排
步骤十一:将消息异步发送给客户端
SocketChannel.write(buffer)
注意:selectionkey的实例key用来保存用以判断网络事件类型的操作位
NIO客户端主要创建过程:
步骤一:打开socketchannel,绑定客户端本地地址,
步骤二:设置socketchannel为非阻塞模式,同时设置TCP参数,
步骤三:异步连接服务器,
步骤四:判断是否连接成功,如果连接成功,则直接注册读状态位到多路复用器中, 如果当前没有连接成功(异步连接,返回 false,说明客户端已经发送 sync 包,服务端没 有返回 ack 包,物理链路还没有建立)
步骤五:向 Reactor 线程的多路复用器注册OP_CONNECT状态位,监听服务端的 TCP ACK应答
步骤六:创建 Reactor 线程,创建多路复用器并启动线程
步骤七:多路复用器在线程run方法的无限循环体内轮询准备就绪的 Key
此处注意key是什么
步骤九:判断连接结果,如果连接成功,注册读事件到多路复用器
步骤十:注册读事件到多路复用器
步骤十一:异步读客户端请求消息到缓冲区
步骤十二:对 ByteBuffer 进行编解码,如果有半包消息接收缓冲区 Reset,继续读取 后续的报文,将解码成功的消息封装成 Task,投递到业务线程池中,进行业务逻辑编排
步骤十三:将 POJO 对象 encode 成 ByteBuffer,调用SocketChannel 的异步 write接口, 将消息异步发送给客户端
NIO优点:不会同步阻塞、一个selector线程可以处理成千上万个客户端连接、读写操作是异步的
AIO编程
◎通过java.util.concurrent.Future 类来表示异步操作的结果;
◎在执行异步操作的时候传入一个 java.nio.channels。 CompletionHandler 接口的实现类作为操作完成的回调
不同IO模型的对比: