前几章我们了解了nio以及epoll,在我们了解netty之前,我们还需要了解什么是Reactor编程模型,netty则是这个模型的实现.
1.事件驱动
在很久以前我们使用JWT的时候,我首先需要把点击或者滚动事件注册在按钮上,然后当我鼠标点击或者滚动的时候会触发事件,会告诉监听者,这个时候我的监听者就会进行一系列的逻辑操作.
2.观察者设计模式
观察者设计模式其实就是事件驱动,两者其实本质上可以认为是一致的.
简单来说就是我有一个Subject主题,这是被观察者,这个对象里面维护着我们的Observer观察者的一个集合,Subject对象里面有新增,删除观察者的方法,以及Subject的一些状态改变的方法,例如update方法.当我们Subject调用了update方法,证明有事件发生,他就会遍历自己维护的观察者Observer,去告诉观察者有事件发生.
观察者设计:
- 定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并自动更新,也叫做发布订阅模式Publish/Subscribe,属于行为型模式
应用场景:
-
消息通知里面:邮件通知、广播通知、微信朋友圈、微博私信等,就是监听观察事件
-
当一个对象的改变需要同时改变其它对象,且它不知道具体有多少对象有待改变的时候,考虑使用观察者模式
角色
- Subject主题:持有多个观察者对象的引用,抽象主题提供了一个接口可以增加和删除观察者对象;有一个观察者数组,并实现增、删及通知操作
- Observer抽象观察者:为具体观察者定义一个接口,在得到主题的通知时更新自己
- ConcreteSubject具体主题:将有关状态存入具体观察者对象,在具体主题内部状态改变时,给所有登记过的观察者发出通知
- ConcreteObserver具体观察者:实现抽象观察者角色所要求的更新接口,以便使本身的状态与主题的状态保持一致
代码实现:
老王,技术比较厉害,因此上班不想那么辛苦,领导又在周围,所以选了个好位置,方便监听老板的到来,
当领导即将出现时老王可以立马观察到,赶紧工作。
用观察者模式帮助老王实现这个需求
//使用
public static void main(String[] args) {
//创建一个主题,老板
BossConcreteSubject subject = new BossConcreteSubject();
//创建观察者,就是摸鱼的同事
Observer lwObserver = new LWConcreteObserver();
//创建观察者,就是摸鱼的同事
Observer annaObserver = new AnnaConcreteObserver();
//建立对应的关系,老板这个主题被同事进行观察
subject.addObserver(lwObserver);
subject.addObserver(annaObserver);
//主题开始活动,里面会通知观察者(相当于发布消息)
subject.doSomething();
}
public class Subject {
private List<Observer> observerList = new ArrayList<>();
/**
* 新增观察者
* @param observer
*/
public void addObserver(Observer observer){
this.observerList.add(observer);
}
/**
*删除观察者
* @param observer
*/
public void deleteObserver(Observer observer){
this.observerList.remove(observer);
}
public void notifyAllObserver(){
for(Observer observer:this.observerList){
observer.update();
}
}
}
public interface Observer {
/**
* 观察到消息后进行的操作,就是响应
*/
void update();
}
public class BossConcreteSubject extends Subject {
public void doSomething(){
System.out.println("老板完成自己的工作");
//还有其他操作
System.out.println("视察公司工作情况");
super.notifyAllObserver();
}
}
public class LWConcreteObserver implements Observer {
@Override
public void update() {
System.out.println("老王发现领导到来,暂停在线摸鱼,回归工作");
}
}
public class AnnaConcreteObserver implements Observer {
@Override
public void update() {
System.out.println("Anna小姐姐发现领导到来,暂停在线摸鱼,回归工作");
}
}
3.在web服务中,处理web请求通常有两种体系结构,分别为:
thread-based architecture(基于线程的架构),
event-driven architecture(事件驱动模型)
我们看一下他是怎么演变到reactor模型的
3.1thread-based architecture(基于线程的架构)
就是传统的BIO模式,我来一个client,我就new一个Thread,去处理.
下图中handler里面read代表从缓冲区读取消息,decode代表解码读取的消息因为是二进制,compute代表拿到消息后我们的逻辑处理,encode代表处理后我们需要把发送给client的消息编码成二进制,send就是发送给client
这种模式一定程度上极大地提高了服务器的吞吐量,由于在不同线程中,之前的请求在read阻塞以后,不会影响到后续的请求。但是,仅适用于于并发量不大的场景,因为:
- 线程需要占用一定的内存资源
- 创建和销毁线程也需一定的代价
- 操作系统在切换线程也需要一定的开销
- 线程处理I/O,在等待输入或输出的这段时间处于空闲的状态,同样也会造成cpu资源的浪费
如果连接数太高,系统将无法承受
3.2 event-driven architecture Reactor模式-单线程模式
第一种模型,他的缺点很明显,所以我们衍生了单线程版本的reacotor,类似我们的NIO,下图的reactor我们可以看成我们的selector.当client连接的时候,我们触发ON_ACCEPT事件,当有读事件的时候触发我们的READ事件去执行对应的逻辑,我们的单线程程序不断在轮训这些事件(包括连接事件和读事件)然后处理,极致的压榨我们线程.
Reactor的单线程模式的单线程主要是针对于I/O操作而言,也就是所以的I/O的accept()、read()、write()以及connect()操作都在一个线程上完成的。
但在目前的单线程Reactor模式中,不仅I/O操作在该Reactor线程上,连非I/O的业务操作也在该线程上进行处理了,这可能会大大延迟I/O请求的响应。所以我们应该将非I/O的业务逻辑操作从Reactor线程上卸载,以此来加速Reactor线程对I/O请求的响应。
3.3多线程reactor模型
与单线程模式不同的是,添加了一个工作者线程池,并将非I/O操作从Reactor线程中移出转交给工作者线程池(Thread Pool)来执行。这样能够提高Reactor线程的I/O响应,不至于因为一些耗时的业务逻辑而延迟对后面I/O请求的处理。
在工作者线程池模式中,虽然非I/O操作交给了线程池来处理,但是所有的I/O操作依然由Reactor单线程执行,在高负载、高并发或大数据量的应用场景,依然较容易成为瓶颈。所以,对于Reactor的优化,又产生出下面的多线程模式
3.4主从reactor模式
对于上面多线程reacotor还有一些缺点,假如我有10W个连接过来的时候,我需要一直在分发任务给线程池,而导致延迟I/O请求的响应.对于多个CPU的机器,为充分利用系统资源,将Reactor拆分为两部分:mainReactor和subReactor.mainReactor负责连接事件,获取到对应的channel之后,将channel的读写事件注册到subReactor.这样,逻辑的处理由subReactor来处理.
mainReactor负责监听server socket,用来处理网络新连接的建立,将建立的socketChannel指定注册给subReactor,通常一个线程就可以处理 ;
subReactor维护自己的selector, 基于mainReactor 注册的socketChannel多路分离I/O读写事件,读写网络数据,通常使用多线程;
对非I/O的操作,依然转交给工作者线程池(Thread Pool)执行。
此种模型中,每个模块的工作更加专一,耦合度更低,性能和稳定性也大量的提升,支持的可并发客户端数量可达到上百万级别。关于此种模型的应用,目前有很多优秀的框架已经在应用了,比如mina和netty 等。Reactor模式-多线程模式下去掉工作者线程池(Thread Pool),则是Netty中NIO的默认模式。
4.Netty工作模型
看完了上面的主从reactor模型.我们简单来看下netty的工作模型,他也是用的主从reactor模型,详细的下一章介绍
1) Netty 抽象出两组线程池BossGroup(主reactor)和WorkerGroup(从reactor),BossGroup专门负责接收客户端的连接, WorkerGroup专门负责网络的读写
2) BossGroup和WorkerGroup类型都是NioEventLoopGroup
5.reactor官方概念
看完上面的介绍,我们再来看reactor的概念
Reactor模式首先是事件驱动的,有一个或多个并发输入源,有一个Service Handler,有多个Request Handlers;这个Service Handler会同步的将输入的请求(Event)多路复用的分发给相应的Request Handler
Reactor模式的角色构成(Reactor模式一共有5中角色构成):
- Handle(句柄或描述符,在Windows下称为句柄,在Linux下称为描述符):本质上表示一种资源(比如说文件描述符,或是针对网络编程中的socket描述符),是由操作系统提供的;该资源用于表示一个个的事件,事件既可以来自于外部,也可以来自于内部;外部事件比如说客户端的连接请求,客户端发送过来的数据等;内部事件比如说操作系统产生的定时事件等。它本质上就是一个文件描述符,Handle是事件产生的发源地。
- Synchronous Event Demultiplexer(同步事件分离器):它本身是一个系统调用,用于等待事件的发生(事件可能是一个,也可能是多个)。调用方在调用它的时候会被阻塞,一直阻塞到同步事件分离器上有事件产生为止。对于Linux来说,同步事件分离器指的就是常用的I/O多路复用机制,比如说select、poll、epoll等。在Java NIO领域中,同步事件分离器对应的组件就是Selector;对应的阻塞方法就是select方法。
- Event Handler(事件处理器):本身由多个回调方法构成,这些回调方法构成了与应用相关的对于某个事件的反馈机制。在Java NIO领域中并没有提供事件处理器机制让我们调用或去进行回调,是由我们自己编写代码完成的。Netty相比于Java NIO来说,在事件处理器这个角色上进行了一个升级,它为我们开发者提供了大量的回调方法,供我们在特定事件产生时实现相应的回调方法进行业务逻辑的处理,即,ChannelHandler。ChannelHandler中的方法对应的都是一个个事件的回调。
- Concrete Event Handler(具体事件处理器):是事件处理器的实现。它本身实现了事件处理器所提供的各种回调方法,从而实现了特定于业务的逻辑。它本质上就是我们所编写的一个个的处理器实现。
- Initiation Dispatcher(初始分发器):实际上就是Reactor角色。它本身定义了一些规范,这些规范用于控制事件的调度方式,同时又提供了应用进行事件处理器的注册、删除等设施。它本身是整个事件处理器的核心所在,Initiation Dispatcher会通过Synchronous Event Demultiplexer来等待事件的发生。一旦事件发生,Initiation Dispatcher首先会分离出每一个事件,然后调用事件处理器,最后调用相关的回调方法来处理这些事件。Netty中ChannelHandler里的一个个回调方法都是由bossGroup或workGroup中的某个EventLoop来调用的。
Reactor模式流程
① 初始化Initiation Dispatcher,然后将若干个Concrete Event Handler注册到Initiation Dispatcher中。当应用向Initiation Dispatcher注册Concrete Event Handler时,会在注册的同时指定感兴趣的事件,即,应用会标识出该事件处理器希望Initiation Dispatcher在某些事件发生时向其发出通知,事件通过Handle来标识,而Concrete Event Handler又持有该Handle。这样,事件 ————> Handle ————> Concrete Event Handler 就关联起来了。
② Initiation Dispatcher 会要求每个事件处理器向其传递内部的Handle。该Handle向操作系统标识了事件处理器。
③ 当所有的Concrete Event Handler都注册完毕后,应用会调用handle_events方法来启动Initiation Dispatcher的事件循环。这是,Initiation Dispatcher会将每个注册的Concrete Event Handler的Handle合并起来,并使用Synchronous Event Demultiplexer(同步事件分离器)同步阻塞的等待事件的发生。比如说,TCP协议层会使用select同步事件分离器操作来等待客户端发送的数据到达连接的socket handler上。
比如,在Java中通过Selector的select()方法来实现这个同步阻塞等待事件发生的操作。在Linux操作系统下,select()的实现中 a)会将已经注册到Initiation Dispatcher的事件调用epollCtl(epfd, opcode, fd, events)注册到linux系统中,这里fd表示Handle,events表示我们所感兴趣的Handle的事件;b)通过调用epollWait方法同步阻塞的等待已经注册的事件的发生。不同事件源上的事件可能同时发生,一旦有事件被触发了,epollWait方法就会返回;c)最后通过发生的事件找到相关联的SelectorKeyImpl对象,并设置其发生的事件为就绪状态,然后将SelectorKeyImpl放入selectedSet中。这样一来我们就可以通过Selector.selectedKeys()方法得到事件就绪的SelectorKeyImpl集合了。
④ 当与某个事件源对应的Handle变为ready状态时(比如说,TCP socket变为等待读状态时),Synchronous Event Demultiplexer就会通知Initiation Dispatcher。
⑤ Initiation Dispatcher会触发事件处理器的回调方法,从而响应这个处于ready状态的Handle。当事件发生时,Initiation Dispatcher会将被事件源激活的Handle作为『key』来寻找并分发恰当的事件处理器回调方法。
⑥ Initiation Dispatcher会回调事件处理器的handle_event(type)回调方法来执行特定于应用的功能(开发者自己所编写的功能),从而相应这个事件。所发生的事件类型可以作为该方法参数并被该方法内部使用来执行额外的特定于服务的分离与分发。
6.总结
Reactor实现相对简单,对于链接多,但耗时短的处理场景高效; 操作系统可以在多个事件源上等待,并且避免了线程切换的性能开销和编程复杂性; 事件的串行化对应用是透明的,可以顺序的同步执行而不需要加锁; 事务分离:将与应用无关的多路复用、分配机制和与应用相关的回调函数分离开来。