Netty源码分析主要分两部分:
基础知识
Netty中的Reactor主从模型
首先需要了解一下基本概念
一、同步、异步、阻塞、非阻塞(网上很多说比较乱,还是自己总结一下吧):
首先同步也异步是对与用户空间来说的,java上通常作用与线程;阻塞和非阻塞是对于kernel内核来说的,通常作用与socket。阻塞和非阻塞都属于同步。
同步(Synchronous communication): 需要阻塞等待消息执行返回结果,则为同步,例如线程池的submit()提交方式
异步(Asynchronous communication): 无需等待等待,通过callback回调接受消息则为异步。例如线程池的execute()提交方式。
BIO(blocking I/O): 阻塞IO,其实就是socket中设置为blocking状态
- 用户进程调用recvfrom向kernl发送system call
- 如果kernel层数据还没准备好,则处于wait_for_data阻塞等待状态
- kernel层数据准备好后,执行copy data from kernel to user操作,返回消息给用户;
NIO(non-blocking I/O): 非阻塞IO,其实就是将socket设置为non-blocking状态
- 用户进程调用recvfrom向kernl发送system call
- 如果kernel层数据还没准备好,会立刻返回消息/状态码
- 用户需要主动轮询发送消息,判断kernel层数据是否准备好
- kernel层数据准备好后,执行copy data from kernel to uer操作,返回消息给用户;
二、IO多路复用机制
多路复用机制如select/poll/epoll,都是IO的对路复用机制,其实就是同步非阻塞IO;
工作原理:
在一个线程中创建一个selector,然后将多个socket注册到selector中去,然后通过轮询socket,当某个socket有消息到达时,就会通知用户。
Select的缺点:
- 单线程监听文件描述符有限,一般默认是1024,数量越大性能越差
- 每次轮询需要将大量的fd从用户空间拷贝到内核空间,而且需要遍历整个列表
- 轮询的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符的IO操作,那么以后每次轮询都会把这个文件描述符通知给用户进程。
Poll的缺点:
虽然将select的文件描述符改成了链表形式,但是select的三大缺点依然存在。
Poll的缺点就是每次轮询都要将fd文件描述符集合从用户空间copy到内核空间,然后完全遍历,当fd数量多的时候,很消耗性能。
Epoll的多路复用机制:
epll_create:创建epoll对象,每个epoll对象都有一个eventpoll:红黑树+链表
epoll_ctl:将用户事件添加到红黑树中,事件有消息时,会通过callback添加到链表中
epoll_wait:只需要定时轮询检查链表中有没有epItem元素就行了,链表不为空就返回给用户,对于百万计注册用户来说,只需要监听活跃用户就行,因此遍历量很少,想读速度就非常快
Reactor模型:
以下源码摘自:https://blog.csdn.net/prestigeding/article/details/55100075
netty是基于Reactor模型设计的,它有多种结构模型,简单来说就是用来监听端口连接和处理io数据两个作用。Reactor是一个模型,他是基于IO多路复用的机制设计的。
lee大神提到的Reactor三个要素:
- Reactor:相当于一个路由,做一些统筹分配工作,负责监听和分配事件,将IO时间分派给对应的Handler。一般主从Reactor模型会将连接和数据处理分开
- Acceptor:处理客户端新连接,并分派请求到处理器链中
- Handler:将自身与事件绑定,执行非阻塞IO任务,完成channel读入,完成处理业务逻辑,负责将结果写入channel。
单Reactor单线程模型。
Reactor负责多路分离套接字,Accepter处理新连接,然后通过Handler处理数据。这些操作都在一个线程中完成,实现思路就是:
- 首先在Reactor中open一个ServerSocketChannel(对应netty中就是NioServerSocketChannel)和一个Selector,然后将socket设置成非阻塞并bind一个端口,通知注册到select中。
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress("127.0.0.1", 9080));
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
- 然后select开启while循环select()阻塞遍历获取socket中的感兴趣的事件,如SelectionKey.OP_ACCEPT接受客户端链接请求,SelectionKey.OP_WRITE写事件,SelectionKey.OP_READ读事件等等。如果有,通过Set ops = selector.selectedKeys();获取接收到兴趣点的所有socket集合。
while(true) {
selector.select(); //如果没有感兴趣的事件到达,阻塞等待
Set<SelectionKey> ops = selector.selectedKeys();
........for循环处理消息.............
- 然后for循环中处理相关事件,处理一件移除一件,一面重复:
(1)如果是客户端链接事件,接受并将socket设置为非阻塞并重新注册为read事件
(2)如果是读事件,通过clientChannel.read读取流并重新注册为write事件
(3)如果是写事件,通过clientChannel.write向客户端写数据并重新注册为read事件
for (Iterator<SelectionKey> it = ops.iterator(); it.hasNext();) {
SelectionKey key = it.next();
it.remove();
try {
if(key.isAcceptable()) { //客户端建立连接
ServerSocketChannel serverSc = (ServerSocketChannel) key.channel();//这里其实,可以直接使用ssl这个变量
SocketChannel clientChannel = serverSc.accept();//接受链接请求
clientChannel.configureBlocking(false);//设置为非阻塞
clientChannel.register(selector, SelectionKey.OP_READ);//准备读取客户端数据
} else if (key.isWritable()) { //向客户端发送请求
SocketChannel clientChannel = (SocketChannel)key.channel();
ByteBuffer buf = (ByteBuffer)key.attachment();
buf.flip();
clientChannel.write(buf);//服务端向客户端发送数据
clientChannel.register(selector, SelectionKey.OP_READ);
} else if(key.isReadable()) { //处理客户端发送的数据
ByteBuffer buf = ByteBuffer.allocate(1024);
System.out.println(buf.capacity());
clientChannel.read(buf);
buf.put(b);
clientChannel.register(selector, SelectionKey.OP_WRITE, buf);//注册写事件
}
} catch(Throwable e) {
e.printStackTrace();
System.out.println("客户端主动断开连接。。。。。。。");
ssc.register(selector, SelectionKey.OP_ACCEPT);
}
}
就好比餐厅门口一个接待员,接到客户后,自己又是服务员,去服务客户。因此缺点很明显,单线程无法处理大量数据,容易造成消息加压,处理延迟,也无法满足高并发。(就像门口客户排长队处理不过来)。
单Reactor多线程模型
这个模型是在单线程基础上,将handler处理数据的部分分离出来,然后通过线程池的方式处理,自身只负责链接请求的处理。
- 第一步跟单线程一样,open一个ServerSocketChannel以及一个Selector,并将socket设置成非阻塞并注册到Selector中去。区别就是同时创建一个线程池
在Reactor中创建一个线程池对象。
NioReactorThreadGroup nioReactorThreadGroup = new NioReactorThreadGroup();
线程池对象构造的时候,创建多个线程并start,对应netty中就是在EventExecutor数组中new了很多的NioEventLoop。
public NioReactorThreadGroup(int threadCount) {
this.nioThreadCount = threadCount;
this.nioThreads = new NioReactorThread[threadCount];
for(int i = 0; i < threadCount; i ++ ) {
this.nioThreads[i] = new NioReactorThread();
this.nioThreads[i].start(); //构造方法中启动线程,由于nioThreads不会对外暴露,故不会引起线程逃逸
}
}
然后通过dispatch方法,将链接发送过来的channel添加到线程中去
public void dispatch(SocketChannel socketChannel) {
if(socketChannel != null ) {
next().register(socketChannel);这里next就是为了获取一个线程
}
}
- 开启while循环,selector通过阻塞遍历select()方法获取感兴趣事件。但是区别是在Reactor中只处理接受请求。将读写请求抛到多线程中去处理。
while (true) {
try {
selector.select();
ops = selector.selectedKeys();
} catch (Throwable e) {
e.printStackTrace();
break;
}
for (Iterator<SelectionKey> it = ops.iterator(); it.hasNext();) {
SelectionKey key = it.next();
it.remove();
try {
if (key.isAcceptable()) { // 客户端建立连接
ServerSocketChannel serverSc = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = serverSc.accept();
clientChannel.configureBlocking(false);
nioReactorThreadGroup.dispatch(clientChannel); // 将建立链接的socket抛给线程池去处理
}
} catch (Throwable e) {
}
}
}
- 最后在子线程中开启while循环,不断处理Reactor抛出来的channel事件
while循环不断执行注册和读写事件
while(true) {
Set<SelectionKey> ops = null;
try {
selector.select(1000);
ops = selector.selectedKeys();
} catch (IOException e) {
}
//处理相关事件
for (Iterator<SelectionKey> it = ops.iterator(); it.hasNext();) {
SelectionKey key = it.next();
it.remove();
try {
if (key.isWritable()) {
SocketChannel clientChannel = (SocketChannel)key.channel();
ByteBuffer buf = (ByteBuffer)key.attachment();
buf.flip();
clientChannel.write(buf);
clientChannel.register(selector, SelectionKey.OP_READ);
} else if(key.isReadable()) {
SocketChannel clientChannel = (SocketChannel)key.channel();
ByteBuffer buf = ByteBuffer.allocate(1024);
System.out.println(buf.capacity());
clientChannel.read(buf);
buf.put(b);
clientChannel.register(selector, SelectionKey.OP_WRITE, buf);
}
} catch(Throwable e) {
}
}
//注册事件
if(!waitRegisterList.isEmpty()) {
....这里简单来说就是从waitRegisterList中获取所有的客户端链接channel并注册到selector中去,同时移除避免重复注册.....
sc.register(selector, SelectionKey.OP_READ);
it.remove();
.......
}
}
就好比,餐厅门口,一个接待员,接到客户后,然后交给一帮服务员专门去服务。这个相比与第一种,Reactor能专门处理连接也事件响应分发工作,线程池提高业务处理效率。缺点就是,连接和事务响应工作还是在主线程,线程池消息处理完毕后,也是返回到主Reactor去send,如果连接请求太多,百万级别,一个接待员还是接待不过来的,如果再有安全认证,就像客户进门吃饭还得做个防控疫情量体温,那就废了。
多Reactor多线程模型(主从Reactor模型)
这个模型是在单Reactor多线程模型基础上进行改进的:将Reactor分为主main和从sub,相当与netty中的NioEventLoopGroup分为boss和worker一样。boss中只负责处理链接事件请求但是通过线程池处理的,并将链接成功的事件抛到worker中去执行,worker部分跟单Reactor多线程是一样的了。
- 在MainReactor中设置开启一个线程池,有线程池来处理客户端的链接请求事件。
// main Reactor 线程池,用于处理客户端的连接请求
private static ExecutorService mainReactor = Executors.newSingleThreadExecutor();
public void run() {
ServerSocketChannel ssc = null;
try {
ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(DEFAULT_PORT));
dispatch(ssc);//连接工作交给线程池去处理
} catch (IOException e) {
}
}
private void dispatch(ServerSocketChannel ssc) {
mainReactor.submit(new MainReactor(ssc));
}
- 在MainReactor中首先打开selector并将channel注册进去,然后创建subReactor,最后在run方法中开启while循环处理客户端的链接请求并将成功的链接抛到subReactor的线程池中处理
public MainReactor(SelectableChannel channel) {
selector = Selector.open();
channel.register(selector, SelectionKey.OP_ACCEPT);
subReactorThreadGroup = new SubReactorThreadGroup(4);
}
public void run() {
while (!Thread.interrupted()) {
Set<SelectionKey> ops = null;
selector.select(1000);
ops = selector.selectedKeys();
// 处理相关事件
for (Iterator<SelectionKey> it = ops.iterator(); it.hasNext();) {
SelectionKey key = it.next();
it.remove();
if (key.isAcceptable()) { // 客户端建立连接
ServerSocketChannel serverSc = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = serverSc.accept();
clientChannel.configureBlocking(false);
subReactorThreadGroup.dispatch(clientChannel); // 转发该请求
}
}
}
}
- 之后在subReactor中线程池中执行处理客户端发来的读写消息,跟单Reactor多线程中的处理方式都是一样的了
优点就是当大量客户发来请求链接同时还要做一些安全校验时,就可以通过主Reactor的线程池去完成,链接成功后又交给SubReactor的线程池去处理读写操作,这样效率和性能都将大大提高。