首先来简单的看一下netty的简介:
Netty 是一个 Java 开源通讯框架,是一个异步的,基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络 IO 程序。
这里主要和大家分享的就是事件驱动模型。
首先我们看下百度百科是如何描述“事件驱动模型”的:鼠标的一个点击,移动,键盘的按键按下等等操作,都是对应操作系统的一个事件,然后应用程序接受你的操作进行处理。
em...好像没有什么不对的地方,但是基本上我们看完后也还是不懂什么是事件驱动模型。只能说百度百科凝结的都是精华(敷衍~)。
1. 事件驱动模型简介
实际上,事件驱动并不是计算机编程领域的专业词汇,它是一种比较古老的响应事件的模型,在计算机编程,公共关系,经济活动等领域内均有很广泛的应用。顾名思义,事件驱动就是持续事件管理过程中,由当前时间点上出现的事件引发可用资源的调动执行相关任务,解决出现的问题,防止事务堆积的一种策略。
在计算机编程领域中,事件驱动模型对应一种程序设计方式,即事件驱动程序设计(Event-driven programming);
事件驱动模型一般是由事件收集器
,事件发送器
,事件处理器
三部分基本单元组成。
其中, 事件收集器
专门负责收集所有的事件,包括来自用户的(如鼠标单击事件、键盘输入事件等)、来自硬件的(如时钟事件等)和来自软件的(如操作系统、应用程序本身等)。
事件发送器
负责将收集器收集到的事件分发到目标对象中。目标对象就是事件处理器所处的位置。
事件处理器主要负责具体事件的响应工作,它往往要到实现阶段才完全确定。
事件驱动程序可以由任何编程语言来实现,只是难易程度有别。如果一个系统是以事件驱动程序模型作为编程基础的,那么,它的架构基本上是这样的:预先设计一个事件循环所形成的程序,这个事件循环程序构成了“事件收集器”,它不断地检查目前要处理的事件信息,然后使用“事件发送器”传递给“事件处理器”。“事件处理器”一般运用虚函数机制来实现。
我们下面用图片来更加详细的展示以下事件驱动模型:
主要包括4个基本组件:
1. 事件队列(event queue):接收事件的入口,存储待处理事件。
2. 分发器(event mediator):将不同的事件分发到不同的业务逻辑单元。
3. 事件通道(event channel):分发器与处理器之间的联系渠道。
4. 事件处理器(event processor):实现业务逻辑,处理完成后会发出事件,触发下一步操作
2. Nginx中的事件驱动模型
Nginx服务响应和处理Web请求的过程,就是基于事件驱动模型的。它也包含事件收集器,事件发送器和事件处理器等三部分基本单元。Nginx的“事件收集器”和“事件发生器”的实现没有太大的特点,重点介绍以下它的“事件处理器”。
通常,我们在编写服务器处理模型的程序时,基于事件驱动模型,“目标对象”中的“事件处理器”可以有以下几种实现办法:
- “事件发送器”每传递过来一个请求,“目标对象”就创建一个新的进程,调用“事件处理器”来处理该请求。
- “事件发送器”每传递过来一个请求,“目标对象”就创建一个新的线程,调用“事件处理器”来处理该请求。
- “事件发送器”每传递过来一个请求,“目标对象”就将其放入一个待处理事件的列表,使用非阻塞I/O方式调用“事件处理器”来处理该请求。
以上的三种处理方式,各有特点,第一种方式,由于创建新的进程的开销比较大,会导致服务器性能比较差,但其实现相对来说比较简单。
第二种方式,由于要涉及到线程的同步,故可能会面临死锁、同步等一系列问题,编码比较复杂。
第三种方式,在编写程序代码时,逻辑比前面两种都复杂。大多数网络服务器采用了第三种方式,逐渐形成了所谓的“事件驱动处理库”。
事件驱动处理库又被称为多路IO复用方法,最常见的包括以下三种:select模型,poll模型和epoll模型。
2.1 seletct库
select库,是各个版本的Linux和Windows平台都支持的基本事件驱动模型库,并且在接口的定义上也基本相同,只是部分参数的含义略有差异。使用select库的步骤一般是:
首先,创建所关注事件的描述符集合。对于一个描述符,可以关注其上面的(Read)事件、写(Write)事件以及异常发送(Exception)事件,所以要创建三类事件描述符集合,分别用来收集读事件的描述符、写事件的描述符和异常事件的描述符。
其次,调用底层提供的select()函数,等待事件发生。这里需要注意的一点是,select的阻塞与是否设置非阻塞I/O是没有关系的。
然后,轮询所有事件描述符集合中的每一个事件描述符,检查是否有相应的事件发生,如果有,就进行处理。
Nginx服务器在编译过程中如果没有为其指定其他高性能事件驱动模型库,它将自动编译该库。我们可以使用--with-select_module和--without-select_module两个参数强制Nginx是否编译该库。
2.2 poll库
poll库,作为Linux平台上的基本事件驱动模型,实在Linux2.1.23中引入的。Windows平台不支持poll库。
poll与select的基本工作方式是相同的,都是现创建一个关注事件的描述符集合,再去等待这些事件发生,然后在轮询描述符集合,检查有没有事件发生,如果有,就进行处理。
poll库与select库的主要区别在于,select库需要为读事件、写事件和异常事件分别创建一个描述符集合,因此在最后轮询的时候,需要分别轮询这三个集合。而poll库只需要创建一个集合,在每个描述符对应的结构上分别设置读事件、写事件或者异常事件,最后轮询的时候,可以同时检查这三种事件是否发生。可以说,poll库是select库的优化实现。
Nginx服务器在编译过程中如果没有为其制定其他高性能事件驱动模型库,它将自动编译该库。我们可以使用--with-poll_module和--without-poll_module两个参数强制Nginx是否编译该库。
2.3 epoll库
epoll库是Nginx服务器支持的高性能事件驱动库之一,它是公认的非常优秀的事件驱动模型,和poll库及select库有很大的不同。epoll属于poll库的一个变种,是在Linux 2.5.44中引入的,在Linux 2.6以上的版本都可以使用它。poll库和select库在实际工作中,最大的区别在于效率。
从前面的介绍我们知道,它们的处理方式都是创建一个待处理事件列表,然后把这个列表发给内核,返回的时候,再去轮询检查这个列表,以判断事件是否发生。这样在描述符比较多的应用中,效率就显得比较低下了。一种比较好的做法是,把描述符列表的管理交给内核负责,一旦有某种事件发生,内核把发生事件的描述符列表通知给进程,这样就避免了轮询整个描述符列表。epoll库就是这样一种模型。
首先,epoll库通过相关调用通知内核创建一个由N个描述符的事件列表。然后,给这些描述符设置所关注的事件,并把它添加到内核的事件列表中去,在具体的编码过程中也可以通过相关调用对事件列表中的描述符进行修改和删除。
完成设置之后,epoll库就开始等待内核通知事件发生了。某一事件发生后,内核将发生事件的描述符列表上报给epoll库。得到事件列表的epoll库,就可以进行事件处理了。
epoll库在Linux平台上是最高效的。它支持一个进程打开大数目的事件描述符,上限是系统可以打开文件的最大数目。同时,epoll库的IO效率不随描述符数目增加而线性下降,因为它只会对内核上报的“活跃”的描述符进行操作。
3. Netty中事件驱动模型
Netty借鉴了Reactor设计模式来实现事件驱动。
Reactor设计模式是一种事件处理模式,在该模式下,来自一个或多个输入源的请求会被并发的投递到一个服务处理器中。服务处理器随后对传入请求进行I/O多路复用,并将它们同步分发给相关的请求处理程序。
Reactor设计模式中有一些核心概念必须提前了解:
-
Event:来自Client端的请求会被抽象为可连接、可写、可读等事件,以便交给相应的处理器来处理事件。
-
Demultiplexer:Demultiplexer为多路复用的含义。在Java NIO中,Selector扮演了Demultiplexer的角色。在多路复用技术下,不再像BIO中那样每一个客户端都需要打开一个Channel,再交给一个独立的线程处理。在NIO中,客户端的请求会共用一个SelectableChannel,来自客户端的所有请求被看做是事件,由Selector监听并转发给Dispatcher进行后续处理。
-
Dispatcher:每个事件都会有一个特定的Handler进行处理,这个映射关系需要提前绑定(或注册),当事件通过多路复用被转过来后,在分发器中转交给绑定的Handler。
-
Handler:负责处理这个事件。
Reactor设计模式的实现思路有三种:
1. Reactor模型之单线程模型
2. Reactor模型之多线程模型
3. Reactor模型之主从多线程模型