nio的应用 java_NIO应用浅析

作者:范平

上海华瑞银行数字银行开发中心软件工程师

目前负责华瑞银行移动银行、融资中台开发工作。

本篇文章对NIO非阻塞IO在日常web容器中的使用分析,会从IO模型、Java的NIO包、Socket网络访问原理和web容器的常见核心NIO模型Reactor几方面循序渐进的进行一个讲解,阅读本篇文章需要对linux操作系统和网络有一定了解。

1. IO模型

同步阻塞IO

用户线程发起IO请求到IO操作结束,用户线程会被一直挂起,下面是linux recvfrom的函数接口。

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

recvfrom函数是阻塞的,其作用是从指定的socket fd中,将数据读入到buffer里;在recvfrom的调用过程中,完成了kenel准备数据和从kernel复制数据到用户空间的过程。

image

同步非阻塞

此场景中socket设置为非阻塞的(通过fcntl方法),调用recvfrom函数会返回EWOULDBLOCK,暗示kernel仍在准备数据中

用户线程发起IO请求后会立即获得数据是否就绪的状态返回;在查询IO是否就绪的间隔,用户线程可以处理别的任务,其优势在于

使用较少的线程管理多个连接,减少内存管理和上下文切换所带来的开销

线程可以复用

image

IO复用模型

常用函数:select(), poll(), epoll()

在一个线程中使用select()或 poll()或epoll()函数,来轮询多个fd(e.g. socket)的就绪情况,如果有fd准备就绪,则返回,否则该线程会阻塞等待有fd就绪直到超时。就绪的fd,既可以放在当前现场处理,也可以创建线程池来处理。

这种使用单线程来检测多个fd就绪情况的机制就是多路复用,其优势在于

1)减少了线程数

2)减少了内存开销

3)减少了多线程之间上下文切换的开销

image

2. Java.NIO简介

Channel

Channel对象提供了对多种fd(socket, file等)写入/读取Buffer的实现

image

Buffer

主要属性:capacity, limit, position, mark

属性之间的关系:mark <= position <= limit <= capacity

// 构造函数

Buffer(int mark, int pos, int lim, int cap) {

if (cap < 0)

throw new IllegalArgumentException("Negative capacity: " + cap);

this.capacity = cap;

limit(lim);

position(pos);

if (mark >= 0) {

if (mark > pos)

throw new IllegalArgumentException("mark > position: ("

+ mark + " > " + pos + ")");

this.mark = mark;

}

}

Selector

Selector selector = Selector.open(); // 创建selector

channel.configureBlocking(false); // channel需要设置为非阻塞

SelectionKey key = channel.register(selector, SelectionKey.OP_ACCEPT|SelectionKey.OP_CONNE);

image

selector会轮询已注册的channel,更具注册时登记的SelectionKey判断是否触发对应事件

key

功能

OP_ACCEPT

请求在接受新连接并创建Channel时获得通知

OP_CONNECT

请求在建立一个连接时获得通知

OP_READ

数据已经继续,请求可从Channel中读取数据时获得通知

OP_WRITE

可以继续向Channel中写数据时,请求获取通知

// e.g.

socketChannle.register(selector, SelectionKey.OP_ACCEPT|SelectionKey.OP_CONNECT)

方法

功能

open()

新建一个selector

keys()

返回selectionKeys

select()

阻塞select操作

selectNow()

非阻塞select操作

wakeup()

在另外一个线程调用wakeup,被阻塞与select方法的线程就会立刻返回

3. Socket是如何工作的?

了解socket的工作原理,会有助于我们了解接下来的话题。

在服务端创建一个socketfd(socket函数),绑定该服务的socket地址信息(bind函数),再将该socketfd转化为listenfd(listen函数)。这三个步骤是一般socket编程中所常见的。

image

当一个connection建立后,会获得一个connfd(accept函数),主进程创建子进程来处理该connfd的请求,伪代码如下:

pid_t pid;

int listenfd, connfd;

listenfd = socket(...); // 创建socket

bind(listenfd, ...); // 将该服务的socket地址信息绑定到listenfd上

listen(listenfd, ...); // 转化为listenfd

for ( ; ; ) {

connfd = accept(listenfd, ...); // 阻塞至连接建立

if ( (pid = fork()) == 0 ) { // 子进程部分

close(listenfd); // 关闭listenfd

/*** 处理请求 ***/

close(connfd); // 子进程关闭connfd,该fd引用计数减1

exit(0); // 关闭子进程

}

close(connfd); // 父进程关闭connfd,该fd应用计数减1

}

}

更近一步去优化这个过程。我们更倾向使用线程的方式来管理和调度。

master线程专注于监听连接请求;buffer用于缓存那些tcp三次握手已经established的socketfd;每个worker线程将其需要处理的socketfd从buffer中取出(移除),调用accept函数生成对应的connfd和clientfd这对fd,即打通了一条两端均为socketfd的网络连接通道。每个work线程通过这条通道来实现对各自client的服务。

image

4. Reactor模式

Reactor单线程模型

Reactor单线程模型提供了一种解决思路。

步骤一:开启Reactor主线程监听服务端端口;创建serverSocketChannel,并注册该channel到Selector中,关注OP_ACCEPT事件;selector的注册函数返回一个selectionKey, 设置selectionKey的attachment为Acceptor对象;

步骤二:Selector轮询已注册的channel(目前只有一个serverSocketChannel),当listenfd(即serverSocketChannel的endpoint)准备好接收一个新的连接时,selector便会轮询到该channel所绑定的key,这个时候Reactor线程会调起之前绑定在这个key上的attachment(即Acceptor对象)

步骤三:Accept对象将socketChannel注册到selector中,一般在这个阶段关注的事件为OP_READ或OP_WRITE事件,即当读或写就绪时,即可唤起一个线程来处理之

image

其中Reactor线程专注于listenfd用于监听客户端的connection请求;Acceptor线程被调起来之后会创建connfd(用于服务端和客户端搭建connection)。需要注意的是Acceptor线程是被动创建的,当有serverSocketChannel触发了OP_ACCEPT事件或socketChannel触发了OP_READ/OP_WRITE事件之后,才会由Reactor线程调用。Acceptor线程通过创建线程来处理对应请求,e.g.读取报文 => 处理 => 响应请求。

Reactor线程池模型

Reactor线程池模型和单线程模型的主要区别在于前者将对非IO的处理交给了线程池,其优势在于加快了React线程的处理速度。因为在单线程模型中从socket读取数据之后,必须等处理完成之后,才可以将该channel的interestSet从OP_READ变更为OP_WRITE;而在多线程模型中处理非IO的过程被丢给了thread pool,当处理结束之后由thread pool分配的线程变更interestSet即可,从而缩短了阻塞的时间。

image

Reactor主从模型

在高并发的场景下,Reactor主从模型可以充分利用cpu核心数提升并发能力。Main Reactor线程专注于监听客户端的连接请求,并通过Main Acceptor线程分发OP_ACCEPT就绪的连接。

比较来看,之前的两个模型都只有一个Selector,所有的OP_ACCEPT, OP_CONNECT, OP_READ和OP_WRITE都归这一个Selector去轮询;而主从模型中,Main Reactor线程持有的Selector中只关注OP_ACCEPT这一种selectionKey,其他SubReactor线程持有的Selector则管理注册到各自的serverChannel的interestSet事件的触发。因此Main Acceptor需要维护各个Sub Reactor与所持有Selector的关系。

image

总结

Reactor模型将IO的处理和非IO的处理剥离开来,使IO事件可以更加及时得注册和分派,提升了事件驱动的效率。按照网络连接工作的特性:在接受请求阶段,将OP_ACCEPT单独监听并配合selector的使用,使单个线程不用阻塞在accept()中,而是线程直接被分配一个就绪的socketf;在处理请求阶段,IO线程在OP_READ触发后,通过channel将都就绪的socketfd指向的信息读入buffer,将报文信息交给线程池分配的一个线程来处理一些非IO的操作,此时刚才的IO线程又可以去处理别的IO事件了,当处理非IO的线程完成任务后,注册OP_WRITE给selector并由selector指派另一个空闲的IO线程将响应报文写给socketfd。

总体来说Reactor模式为web服务提供了一套按照网络连接、IO操作定制化的多线程时序控制逻辑。

你可能感兴趣的:(nio的应用,java)