【Netty系列_1】Netty简介与I/O&线程模型

370 阅读9分钟

【前言】

netty作为Java领域中优秀的网络编程框架,其设计思想代码实现都非常值的我们去学习,同时,更好的了解netty概念设计细节后,我们使用起来也就会更踏实更称手,从而让netty这件神兵利器在我们手中发挥出最大的威力!!!


netty简介

What is netty?

  1. Netty 是一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端

netty的优势

  1. API使用简单,使用&开发 门槛低
  2. 预置多种编解码器,支持多种主流协议,亦可定制私有协议。
  3. 基于灵活且可扩展的事件模型
  4. 高度可定制的线程模型,真正的无连接数据报套接字支持(自 3.1 起)
  5. 解决了Java中NIO使用起来繁琐,空轮询等等问题
  6. 社区活跃,经历无数个商业&开源项目验证,稳定性好
  7. 完整的 SSL/TLSStartTLS 支持
  8. 高吞吐,低时延,更少的资源,减少不必要的内存拷贝
  9. ...... 总之一句话: 牛x。

netty架构

image.png

  1. 图中可以了解到netty 包含了事件模型,统一的API零拷贝,以及其丰富的协议支持

netty绕不开的知识点之 IO模型

  1. 然而,说起netty的IO模型,我们就不得不说到 UnixIO模型

Unix系统五种I/O模型

  1. 下面我们以UDPsocket 读为例来介绍一下五种I/O模型(但是在介绍I/O模型之前,我们需要了解下Unix系统函数recvfrom)

recvfrom系统函数介绍

  1. 说明: recvfrom函数用来从一个套接口(有的文献称为套接字)接收消息,也可以用来在一个面向连接(如TCP)或非连接(UDP)的套接口上接收数据(但是大部分时候都是在UDP协议中使用)

  2. recvfrom函数在Java中的应用 (可以看到Java中是通过调用c语言来调用的recvfrom函数),感兴趣可以看下系统函数accept在Java中都有哪几个地方使用到了。 image.png image.png

  3. recvfrom函数的执行时机 image.png

  • 好了,介绍完recvfrom函数,我们紧接着看下 Unix 的5类I/O模型吧!

阻塞式I/O

image.png

  1. 进程调用 recvfrom系统函数,直到数据包到达且被复制到应用进程的缓冲区中或 中间发生异常时才返回,在这个期间进程会一直等待。进程从调用 recvfrom 开始到它返回的整段时间内都是被阻塞的,因此,被称为阻塞 式I/O 模型

所以,阻塞式I/O的特点就是在IO执行的两个阶段都被 “阻塞”了

非阻塞式I/O

image.png

  1. recvfrom 调用到达内核时,如果该缓冲区没有数据,就会直接返回 EWOULDBLOCK 错误,一般都会对非阻塞 I/O 模型进行轮询以检查该状态,看看内核是不是有数据报到来, recvform 调用之后,进程并没有被阻塞,而是内核马上返回给进程,从而达到非阻塞的效果

  2. 如果数据还没准备好,此时会返回一个 EWOULDBLOCK。进程在返回之后,可以干点别的事情,然后再发起 recvform 系统调用。重复上面的过程,循环往复的进行 recvform 系统调用,这个过程通常被称之为轮询

  3. 如果某次轮询发现数据已经准备好了,那么拷贝数据到用户空间,然后返回成功指示,随后进程进行数据报处理。需要注意: 拷贝数据整个过程,进程仍然是属于阻塞的状态(图中的阶段2)

  4. 值的注意的是,非阻塞I/O模型往往会消耗大量的cpu,因为其轮询方式是一个很耗cpu的操作

I/O复用(select pool)

image.png

  1. IO 复用模型:进程发起一个 IO 请求,然后把自己注册到一个 select 的系统进程,select 会监听数据是否准备好,若准备好 select 会通知进程,进程发起系统调用,CPU 会复制内核空间缓冲区数据到用户空间,同样,复制数据过程也是阻塞的。
  2. 为什么叫 IO 多路复用呢?因为 select 可以接受多个进程注册,可以监听多个进程的 IO 请求,不像非阻塞式 IO 模型需要进程不断轮询,多路复用模型把询问的任务交给了 select,让多个进程都可以共享 select

信号驱动方式I/O

image.png

  1. 若使用该I/O模型,需要开启套接字的信号驱动式I/O功能,步骤如下:
    • 要求进程建立SIGON信号的信号处理函数
    • 设置该套接字的属主,通常使用fcntlF_SETOWN设置
    • 开启该套接字的信号驱动式I/O,通常通过fcntlF_SETFL命令打开O_ASYNC标志

image.png

  1. 通过sigaction系统调用安装一个信号处理函数,该系统调用将会立即返回,进程继续做其他事情,当有数据报准备就绪时,内核就为该进程产生一个SIGIO信号,之后我们的进程发起recvfrom函数调用,当数据从内核复制到用户空间完成后(需要注意: 拷贝数据整个过程,进程仍然是属于阻塞的状态),返回成功指示,进程开始处理数据报

异步I/O (POSIX的aio系列函数)

image.png

  1. 可以看到,进程发起系统调用后,直接返回,不需要阻塞,直到数据从内核空间复制用户空间完成后,系统通知进程 I/O 完成
  2. 注意异步I/O和信号驱动I/O的主要区别是: 信号驱动I/O是由内核通知我们何时可以启动一个I/O操作,而异步I/O是通知我们I/O操作何时完成

五种I/O模型对比

image.png

  1. 根据上图我们可以看出,阻塞,非阻塞,复用,信号驱动这四种,本质上都是同步I/O,只有第五种异步I/O才是真正的异步处理方式

  • 然后说完I/O模型还不够,还需要学习一下线程模型,才能为我们学习netty打下更好的基础!

netty绕不开的知识点之 线程模型

经典/传统 线程模型

image.png

  1. 这个传统线程模型不用多说相信大家都明白,就是为每个连接都创建一个线程,只要量稍微一上来,系统必然会gg

  2. 大师们为了改变这糟糕的情况,于是催生了Reactor线程模型

Reactor线程模型

模型特点

  1. Reactor 模型中有 3 个关键组成:

    • ReactorReactor 在一个单独的线程中运行,负责监听分发事件,分发给相应的处理程序来对 IO 事件做出反应。 它就像公司的电话接线员,它接听来自客户的电话并将线路转移到对应的联系人
    • Handlers:实际上,你可以把他理解为处理器,即执行读取数据报执行业务逻辑的地方
    • ThreadPool:(严格讲,单Reactor 单线程的模型并没有线程池,所以线程池只是针对后两种Reactor线程模型来说的)线程池负责处理(decode => compute => encode) 这些业务操作
  2. 根据 Reactor 的数量和线程的数量不同Reactor又分为有 3 种典型的实现

    • 单 Reactor 单线程
    • 单 Reactor 多线程
    • 主从 Reactor 多线程

下面我们分别来看一下这三种线程模型都是什么,以及他们有什么优劣

单 Reactor 单线程

image.png

执行流程简述

  1. Reactor: 对象通过 Select 监控客户端请求事件,收到事件后通过 Dispatch 进行分发
  2. 如果是建立连接请求事件:则由 Acceptor 通过 Accept 处理连接请求,然后创建一个 Handler 对象处理连接完成后的后续业务逻辑
  3. 如果不是建立连接事件: (如read ,write),则 Reactor 会分发并调用对应的 Handler 来响应
  4. Handler: 会完成 read => (decode => compute => encode) => send 的完整业务流程

该线程模型的优缺

  1. 优点:模型简单没有线程安全问题,因为都在一个线程中完成。
  2. 缺点性能问题: 只有一个线程,无法充分发挥多核 CPU 的优势。Handler 在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈;系统可靠性不能保证,如线程进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障

使用场景:客户端的数量有限,业务处理非常快速,比如 Redis

单Reactor 多线程

image.png

  • 从上图可以看出,这种模型和单Reactor 单线程模型的主要区别是把业务处理从之前的单一线程,换成线程池处理

执行流程简述

  1. Reactor线程:通过select监听客户请求,如果是连接建立的事件,通过accept接受连接,并创建一个Handler来处理连接后续的读写事件。这里的Handler只负责响应事件、readwrite事件,会将具体的业务处理交由Worker线程池处理

  2. Worker线程池:负责处理(decode => compute => encode) 这三个操作

该线程模型的优缺

  1. 优点: 业务处理充分利用多核机器的资源,提高性能
  2. 缺点: 由于Reactor仍然是单线程,所以在极个别特殊场景中(如大并发情况下--->双十一春运抢票等)Reactor线程负责监听和处理所有的客户端连接可能会存在性能问题

主从Reactor 多线程

image.png

执行流程简述

  1. 主 Reactor: 单独监听server socketaccept新连接,然后将建立的 SocketChannel 注册给指定的 从Reactor
  2. 从Reactor: 将连接加入到连接队列进行监听,并创建handler进行事件处理。执行事件的读写、分发,然后把剩余的业务处理扔给worker线程池
  3. Worker线程池:负责处理(decode => compute => encode) 这些操作

该模型优缺

  1. 优点:充分利用多核机器的资源,提高性能,轻松处理百万并发
  2. 缺点:实现起来相对复杂一些

  • 到这里,我们说了netty的优点架构以及I/O模型线程模型,那么实际编程中,我们需要自己去实现Reactor线程模型吗?答案是不需要的,netty已经帮我们做好了! 是不是想说: 哎嘛,真香!!!

【结语】

本来我想把netty的核心组件也放到此文,但是鉴于让文章更清晰明了,不那么臃肿,所以我将在下一篇文章,讲述netty组件以及netty的使用。到这里,此文完~

参考:
gee.cs.oswego.edu/dl/cpjslide…
developer.51cto.com/article/666…
netty.io/index.html
《UNIX网络编程:卷一》