Netty(七):EventLoop学习前导——Reactor模式
了解Netty的人多少都会知道Netty的高性能的一个原因就是它是基于事件驱动的,而这一事件的原型就是Reactor模式。
所以在学习EventLoop前,很有必要先搞懂Reactor模式。
本文目录:
- 传统的服务器设计
- Basic Reactor(单线程模式)
- MultiThreadReactor(多线程模式)
- 主从多线程模型
传统的服务器设计模式:
先来简单的介绍下传统的服务器设计模式。
看从图例了解:
传统的服务器设计模式是基于IO实现的。服务器在等待连接,及IO准备就绪前都会被阻塞。
代码示例如下:
class Server implements Runnable {
public void run() {
try {
ServerSocket ss = new ServerSocket(PORT);
while (!Thread.interrupted())
new Thread(new Handler(ss.accept())).start();
} catch (IOException ex) { /* ... */ }
} static class Handler implements Runnable {
final Socket socket;
Handler(Socket s) { socket = s; }
public void run() {
try {
byte[] input = new byte[MAX_INPUT];
socket.getInputStream().read(input);
byte[] output = process(input);
socket.getOutputStream().write(output);
} catch (IOException ex) { /* ... */ }
}
private byte[] process(byte[] cmd) { /* ... */ }
}
}
传统的服务器模式的优势在于实现简便,相对于NIO的服务器,它的代码量更少,更直接。但它最大的缺点就是IO阻塞导致运行效率低下。
Reactor模式:
Reactor模式是利用NIO的多路复用而设计的一种基于事件驱动的服务器模式。主要的设计目的是通过分而治之的思想让服务器实现可扩容的目标。
Basic Reactor(单线程版本):
Basic Reactor是Reactor模式最基础的版本,可以说是定义了整个Reactor模式的大骨架,其他复杂的版本也是在此基础上演变而来。
深入了解Basic Reactor是掌握Reactor模式的基本,因此我们会用最多的内容去理解Basic Reactor。
无论是Reactor模式的哪些变化,基本上都离不开下列三种角色:
Reactor(反应堆):服务器启动的主入口
Acceptor(接收器):主要负责处理IO连接事件
Handler(处理器):负责处理IO读写以及业务逻辑处理等
先结合图例来了解下Reactor:
图中已经明显画出了Reactor和Acceptor的角色,而未画出的Handler部分就是黄色圆圈的部分(read,decode, compute, encode, send 构成了一个Handler的基本职能)
在通过代码来分析下:
package com.insaneXs.netty.reactor.basic; import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set; /**
* @Author: insaneXs
* @Description:
* @Date: Create at 2018-12-19
*/
public class Reactor implements Runnable{ final Selector selector; final ServerSocketChannel serverSocket; Reactor(int port) throws Exception{ //创建ServerSocketChannel,绑定端口,设置为非阻塞,选择器上注册ACCEPT事件
selector = Selector.open();
serverSocket = ServerSocketChannel.open(); serverSocket.bind(new InetSocketAddress(port));
serverSocket.configureBlocking(false);
SelectionKey sk = serverSocket.register(selector, SelectionKey.OP_ACCEPT); sk.attach(new Acceptor());
} @Override
public void run() {
try {
while (!Thread.interrupted()) {
//阻塞,直到注册的事件发生
selector.select();
Set selected = selector.selectedKeys();
Iterator it = selected.iterator();
while (it.hasNext()){
//任务派发
dispatch((SelectionKey)(it.next()));
}
selected.clear();
}
} catch (IOException ex) {
ex.printStackTrace();
} } void dispatch(SelectionKey k) {
//通过将不同的附件绑定到SelectionKey上,实现dispatch统一派发Acceptor和Handler的逻辑
Runnable r = (Runnable)(k.attachment());
if (r != null)
r.run();
} class Acceptor implements Runnable{
@Override
public void run() {
try {
//ACCEPT负责接收链接
SocketChannel sc = serverSocket.accept();
if(sc != null)
new Handler(selector, sc);
} catch (IOException e) {
e.printStackTrace();
}
}
} class Handler implements Runnable{
final SocketChannel socket; final SelectionKey sk; ByteBuffer input = ByteBuffer.allocate(1024);
ByteBuffer output = ByteBuffer.allocate(1024); static final int READING = 0, SENDING = 1;
int state = READING; Handler(Selector sel, SocketChannel c) throws IOException{
socket = c;
c.configureBlocking(false);
// Optionally try first read now
//返回了新的SelectionKey,将Handler添加为SelectionKey的附件,先注册READ事件
sk = socket.register(sel, 0);
sk.attach(this);
sk.interestOps(SelectionKey.OP_READ);
sel.wakeup();
} boolean inputIsComplete() {
return true;
}
boolean outputIsComplete() {
return true;
}
void process() {
//DO SOME THING
} @Override
public void run() {
try {
if (state == READING) read();
else if (state == SENDING) send();
} catch (IOException ex) {
ex.printStackTrace();
}
} void read() throws IOException {
socket.read(input);
if (inputIsComplete()) {
process();
state = SENDING;
// Normally also do first write now
sk.interestOps(SelectionKey.OP_WRITE);
}
} void send() throws IOException {
socket.write(output);
if (outputIsComplete()) sk.cancel();
} }
}
了解完Reactor中的角色分工,再看代码其实并不复杂。代码关键的部分也都加上了注释。
每个角色的业务处理逻辑都是以run方法为入口,
Reactor中run方法处理的主要逻辑就是监听NIO的多路复用,并通过dispatch方法分发任务。
Acceptor中run方法处理的主要逻辑就是接收连接,并为处理读写做准备。
Handler中run方法处理的主要逻辑就是读写和业务逻辑的处理。
有几点值得注意的:
第一,这段代码最关键的地方就是在Reactor进行任务分发时,利用SelectionKey的Attach添加附件的方法实现了用同一入口分发给Acceptor和Handler(这是设计的比较巧妙的部分)。
第二,无论是哪个角色都实现了Runnable,这也保证了即使是其他多线程版本,只需要修改部分代码,而不用动整个Reactor模式的骨架。
第三,我们可以看到上面的代码都是直接调用run方法,而不是通过Thread.start方法来运行,说明Basic Reactor的处理过程确实是单线程下的。
另外提到一点就是Handler的构造函数中先是register的0,然后再设置SelectionKey的interestOps为OP_READ。这点在之前的Netty源码分析中,我们也了解到,Netty正是这样的过程。
将代码转换成时序图,加深对代码的印象:
Basic Reactor优点与不足:
优点:利用了NIO的特性,可以仅用一条线程处理多个通道的连接处理。相较于传统的服务器模式,这样对资源的消耗更少。
不足:我们可以看到不仅IO的部分由Reactor的线程处理,连业务处理的逻辑同样是放在Reactor的线程中处理,这样可能就会导致Reactor线程积累越来越多的请求,导致效率下降。
MultiThreads版本的Reactor模型,正是为了解决上述的问题。
同样先通过图例来了解这个模式下,各个角色的关系:
这个图和Basic Reactor的区别是什么?我们又该如何理解呢?
我们可以看到之前的Handler处理的角色被一分为二,read,send(也就是IO的读写)和Basic Reactor中的模式不变,但是decode,compute,encode(也就是业务处理的逻辑)被拆出来,提交给ThreadPool运行。
新的Reactor模式对比Basic Reactor,其他代码不变,只是我们修改了Handler,增加了一个新的角色,叫做Processor,作为负责处理业务逻辑的单元:
public class ThreadPooledHandler implements Runnable{
final SocketChannel socket;
final SelectionKey sk;
ByteBuffer input = ByteBuffer.allocate(1024);
ByteBuffer output = ByteBuffer.allocate(1024);
static final int READING = 0, SENDING = 1;
static final int PROCESSING = 3;
int state = READING; // uses util.concurrent thread pool
static ExecutorService pool = Executors.newFixedThreadPool(4); ThreadPooledHandler(Selector sel, SocketChannel c) throws IOException {
socket = c;
c.configureBlocking(false);
// Optionally try first read now
//返回了新的SelectionKey,将Handler添加为SelectionKey的附件,先注册READ事件
sk = socket.register(sel, 0);
sk.attach(this);
sk.interestOps(SelectionKey.OP_READ);
sel.wakeup();
} boolean inputIsComplete() {
return true;
}
boolean outputIsComplete() {
return true;
} void process() {
//DO SOME THING
} @Override
public void run() {
try {
if (state == READING) read();
else if (state == SENDING) send();
} catch (IOException ex) {
ex.printStackTrace();
}
} synchronized void read() throws IOException { // ...
socket.read(input);
if (inputIsComplete()) {
state = PROCESSING;
pool.execute(new Processer());
}
} void send() throws IOException {
socket.write(output);
if (outputIsComplete()) sk.cancel();
}
synchronized void processAndHandOff() {
process();
state = SENDING; // or rebind attachment
sk.interestOps(SelectionKey.OP_WRITE);
} //增加Processer角色,处理业务逻辑
class Processer implements Runnable {
public void run() { processAndHandOff(); }
} }
为了方便看出变化,我将两个版本的代码放在一起,做了对比图:
最大的区别就是原先Handler中process方法被交给了Processer执行,并且在执行时,是提交给线程池去执行。而Handler负责的IO读写逻辑仍然在Reactor的线程中执行(只是非网络IO的业务逻辑部分在新的线程中执行)。
相对于BasicReactor,这个版本的Reactor能更好的利用现代多核CPU的性能。让一条线程负责处理IO,而其他线程执行业务逻辑。多路复用上监听的阻塞,并不会阻塞业务逻辑的执行。
主从复合的Reactor模型
多线程的Reactor模型处理能力已经非常的高效,但是IO的连接过程仍然可能是个耗时的过程(比如SSL认证)。因此引出了一个新的变化——主从复合的Reactor模型。
先看图例:
和上一个版本比较,这个版本的Reactor区别主要是将Reactor拆分一个MainReactor(负责处理Accept事件)和多个SubReactor(负责处理IO读写事件)。
而MainReactor和SubReactor的关联只要是通过Acceptor。
我们知道Reactor和Selector的关系是一对一的关系。通常一个Reactor由一条独立的线程执行。该线程在Reactor关联的Selector是监听事件。
因此这个模式下,当Accept在为连接进来的SocketChannel绑定Selector时,不再是绑定到MainReactor对应的Selector中,而是绑定到其他Reactor对应的Selector上(对应其他线程)。
这也因此让MainReactor只负责执行ACCEPT,而SubReactor负责IO读写。也使得ACCEPT上费时的操作将不会影响IO读写和业务逻辑处理。
贴上代码:
增加SubReactor:
public class SubReactor implements Runnable{
private final Selector selector; public SubReactor() throws IOException {
selector = Selector.open();
} @Override
public void run() {
while(!Thread.interrupted()){
try {
selector.select();
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while(iter.hasNext()){
SelectionKey sk = iter.next();
((Runnable)sk.attachment()).run();
}
} catch (IOException e) {
e.printStackTrace();
}
}
} public Selector getSelector(){
return selector;
}
}
SubReactor的代码和Basic Reactor中Reactor的代码很相似,因为不处理连接部分,所以没有ServerSocketChannel和绑定监听端口的操作。
接下来看MainReactor和Acceptor的代码:
package com.insaneXs.netty.reactor.multiple; import com.insaneXs.netty.reactor.threadpooled.ThreadPooledHandler; import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set; /**
* @Author: insaneXs
* @Description:
* @Date: Create at 2018-12-21
*/
public class MainReactor implements Runnable{
final Selector selector; final ServerSocketChannel serverSocket; private final static int SUB_REACTOR_COUNT = 3; private final Selector[] selectors = new Selector[SUB_REACTOR_COUNT]; MainReactor(int port) throws Exception{ //创建ServerSocketChannel,绑定端口,设置为非阻塞,选择器上注册ACCEPT事件
selector = Selector.open();
serverSocket = ServerSocketChannel.open(); serverSocket.bind(new InetSocketAddress(port));
serverSocket.configureBlocking(false);
SelectionKey sk = serverSocket.register(selector, SelectionKey.OP_ACCEPT); for(int i=0; i<selectors.length; i++){ //创建SUB-REACTOR,并保存对应的Selector对象
SubReactor subReactor = new SubReactor();
selectors[i] = subReactor.getSelector();
//为SUB-REACTOR启动独立的线程
new Thread(subReactor).start();
} sk.attach(new Acceptor());
} @Override
public void run() {
try {
while (!Thread.interrupted()) {
//阻塞,直到注册的事件发生
selector.select();
Set selected = selector.selectedKeys();
Iterator it = selected.iterator();
while (it.hasNext()){
//任务派发
dispatch((SelectionKey)(it.next()));
}
selected.clear();
}
} catch (IOException ex) {
ex.printStackTrace();
} } void dispatch(SelectionKey k) {
//通过将不同的附件绑定到SelectionKey上,实现dispatch统一派发Acceptor和Handler的逻辑
Runnable r = (Runnable)(k.attachment());
if (r != null)
r.run();
} class Acceptor implements Runnable{
private int idx = 0;
@Override
public void run() {
try {
//ACCEPT负责接收链接
SocketChannel sc = serverSocket.accept();
if(sc != null)//将SocketChannel与SubReactor的Selector均匀绑定
new ThreadPooledHandler(selectors[idx], sc); idx++;
if(idx == SUB_REACTOR_COUNT)
idx = 0;
} catch (IOException e) {
e.printStackTrace();
}
}
} }
做个对比图,比较下和之前的版本的差异:
MainReactor:
区别主要在MainReactor内部保存了一些SubReactor,在MainReactor被创建时,同时创建了几个SubReactor。并且创建线程独立的运行SubReactor。
再看看Acceptor:
二者Acceptor的区别就是当把Handler提交给线程池时,非主从复合结构的版本仍然是用一个Selector。而主从复合结构的Handler在处理时,用的多路复用器是SubReactor中的。因此分离出了ACCEPT和IO读写。
本文参考:Scalable IO in Java
本文代码:Github
Netty(七):EventLoop学习前导——Reactor模式的更多相关文章
- (一)Netty源码学习笔记之概念解读
尊重原创,转载注明出处,原文地址:http://www.cnblogs.com/cishengchongyan/p/6121065.html 博主最近在做网络相关的项目,因此有契机学习netty,先 ...
- Reactor模式详解
转自:http://www.blogjava.net/DLevin/archive/2015/09/02/427045.html 前记 第一次听到Reactor模式是三年前的某个晚上,一个室友突然跑过 ...
- 转一篇:Reactor模式
转载自:http://www.blogjava.net/DLevin/archive/2015/09/02/427045.html 前记 第一次听到Reactor模式是三年前的某个晚上,一个室友突然跑 ...
- (转)reactor模式
转自: http://www.blogjava.net/DLevin/archive/2015/09/02/427045.html Reactor模式详解 前记 第一次听到Reactor模式是三年前的 ...
- EDA风格与Reactor模式
本文将探讨如下几个问题: Event-Driven架构风格的约束 EDA风格对架构属性的影响 Reactor架构模式 Reactor所解决的问题 redis中的EventDriven 从观察者模式到E ...
- 【转】Netty那点事(四)Netty与Reactor模式
[原文]https://github.com/code4craft/netty-learning/blob/master/posts/ch4-reactor.md 一:Netty.NIO.多线程? 时 ...
- Netty如何实现Reactor模式
在前面的文章中(Reactor模型详解),我们讲解了Reactor模式的各种演变形式,本文主要讲解的则是Netty是如何实现Reactor模式的.这里关于Netty实现的Reactor模式,需要说明的 ...
- Netty之Reactor模式
无论是C++还是Java编写的网络框架,大多数都是基于Reactor模式进行设计和开发,Reactor模式基于事件驱动,特别适合处理海量的I/O事件. 1. 单线程模型 Reactor单线程模型,指的 ...
- Reactor 模式在Netty中的应用
Reactor 模式在Netty中的应用 典型的Rector模式 mainReactor 服务端创建成功后,会监听Accept操作,其中ServerSocketchannel中的PipeLine中现在 ...
随机推荐
- 了解一下mock
1.mock简介: mock测试就是在测试过程中,对于某些不容易构成或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法,mock是在测试过程中,对于一些不容易构造/获取的对象,创建一个mo ...
- 2019-07-31【机器学习】无监督学习之降维NMF算法 (人脸特征提取)
代码 from numpy.random import RandomState #加载RandomState用于创建随机种子 import matplotlib.pyplot as plt from ...
- 背景知识+监督和无监督学习辨析+预备知识(1-1—1-4/用时4h)
1-1/1.2,基本上都是一些基础知识,机器学习的背景,发展,概念,用途 1-3,监督学习: 数据集类型已知,数据信息为已知正解--由已知正解推测趋势(拟合分布函数)-- 给出的模型例子--基本类似于 ...
- 【Java】【常用类】Object 基类 源码学习
源码总览: 有好些都是native本地方法,背后是C++写的 没有关于构造器的描述,默认编译器提供的无参构造 https://blog.csdn.net/dmw412724/article/detai ...
- redis 浅谈事务
写在前面的话 之前在某个网站上看到一个问题:redis在什么情况下出现事务不会滚的情况,以此为由并结合redis官方文档整理这边笔记.不足之处,请指出,谢谢. 事务 redis支持事务,提供两条重要的 ...
- 关于 System.IO.File.Exists 需要注意的事项
各位: .NET Framework 本省在设计的时候,他对于异常没有完全做到抛出,这样可能会有很多意想不到的问题. 比如 你在asp.net 应用程序中判断文件是否存在,这个文件可能是一个共 ...
- E2. Send Boxes to Alice (Hard Version)
秒的有点难以理解:https://blog.csdn.net/weixin_42868863/article/details/103200132 #include<bits/stdc++.h&g ...
- Salesforce LWC学习(十六) Validity 在form中的使用浅谈
本篇参考: https://developer.salesforce.com/docs/component-library/bundle/lightning-input/documentation h ...
- Windows安装Tesseract-OCR 4.00并配置环境变量
一.前言 Tesseract-OCR 是一款由HP实验室开发由Google维护的开源OCR(Optical Character Recognition , 光学字符识别)引擎.与Microsoft O ...
- 论JDK5/7/8版本都做出了哪些革新
在Java发展的里程碑上,有三个版本做出的改动,是革命性的 为什么说是革命性的呢? 因为这三个版本所推出的有些新机制,在之后的Java框架开发.新类的产生等等中, 都被广泛使用了. 那么,这三个版本的 ...