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是否就绪的间隔,用户线程可以处理别的任务,其优势在于

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

  2. 线程可以复用

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操作定制化的多线程时序控制逻辑。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容