1. 目的
我们知道Netty优点很多以及netty原理的底层机制,但是是否真正理解呢?如果真的理解了那么自然回答出一下几个问题:
- 1.你能看的懂netty源码吗
- 2.你能讲出netty源码的架构吗
- 3.netty中的NioEventLoop对应一个线程,并绑定了一个selector,为何主Reactor中的NioEventLoop可以负责接收请求,而从Reactor中的NioEventLoop可以负责消息的读取呢?同一个client发起的连接请求及后续的消息可以被2个不同的NioEventLoop处理?
其实netty源码的核心就是问题3,搞清楚了问题3,netty源码就很简单了,看完本篇文章,就会为你揭晓答案。
2 手写netty架构的nio代码
本篇基于《java NIO编程示例以及流程详解》改造,在该篇文章中,我们的服务端代码负责接收请求,由于采用nio架构,让我们能非阻塞的处理消息连接和消息处理。
并且是单线程的,对应【Netty源码分析摘录】(二)Netty源码分析系列之Reactor线程模型中”2.1 Reactor单线程模型“。
我的思路是将这个例子先扩展为多线程的,保证先能理解多个selector之间的协同关系,后面的主从架构就更容易理解了。
2.1 单线程的nio架构
NIO原生语法结构图,这样对比,可以更好的理解netty的架构:
我们看下单线程的nio架构特点:
-
只有一个selector,服务端通道和客户端通道都注册到同一个selector上
-
一旦某个channel和selector绑定后,通过SelectionKey,可以获得对应的channel
2.2 改写为多线程架构
我们重写一个服务器端代码ServerSocketChannels2 ,继承了ServerSocketChannels,仅仅覆写了doAccept():
import java.io.IOException;
import java.nio.channels.SelectionKey;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class ServerSocketChannels2 extends ServerSocketChannels implements Runnable {
public ServerSocketChannels2(int port ){
super(port);
}
/**
* 覆写doAccept方法,保证新启动一个线程去负责处理连接的请求
* @param key
* @throws IOException
*/
@Override
public void doAccept(SelectionKey key) throws IOException {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = (SocketChannel) ssc.accept();
new Thread(new SockertChannel(sc),"subclient")
.start();
}
public static void main(String[] args) {
int port = 8010;
ServerSocketChannels2 server = new ServerSocketChannels2(port);
// ServerSocketChannels2 server = new ServerSocketChannels2(port);
new Thread(server,"timeserver-002").start();
}
}
在覆写的doAccept()方法中,我们没有在当前的线程中处理连接事件,而是新起了一个线程去负责处理连接事件:new Thread(server,"timeserver-002").start()
,这里对应Netty从workerGroup
中取出一个NioEventLoop,并由该NioEventLoop处理后续的消息。
ServerSocketChannels 和ServerSocketChannels2 对应
BossGroup
中的一个NioEventLoop,并处理客户端的连接事件。
SockertChannel:
import java.io.IOException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
/**
* 当客户端发起连接后,负责生成客户端channel
*/
public class SockertChannel extends ServerSocketChannels {
private SocketChannel sc;
public SockertChannel(SocketChannel sc) {
this.sc = sc;
try {
//新创建的selector
selector = Selector.open();
//设置客户端链路为非阻塞模式
this.sc.configureBlocking(false);
this.sc.register(selector, SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace();
}
}
}
SockertChannel继承了ServerSocketChannels是为了复用run(),处理接收的消息。在构造函数中,新建了一个selector,并且将其注册到新的selector上,并设置关心的消息类型为OP_READ
。
至此,我们基本上模拟了netty的实现原理,实现了一个多线程的nio架构,我们来看下执行结果:
服务端:
打印的是子线程,说明子线程处理了客户端发给服务端的消息。
3. 原理分析
多线程结构示意图:
我们在ServerSocketChannels2中创建了一个服务端通道,ServerSocketChannel,并将其注册到selector A上,当某个客户端C1发起连接后,利用ServerSocketChannel生成一个客户端通道SocketChannel SC1,并将SC1注册到新的selector A1上。
依此类推,当某个客户端C2发起连接后,利用ServerSocketChannel生成一个客户端通道SocketChannel SC2,并将SC2注册到新的selector A2上。
由上得知以下结论:SC1和SC2虽然是由ServerSocketChannel创建的,但是将其各自绑定到 selector A1和selector A2上后,算上selector A,总共有3个selector,但3者没有任何关系,各自由的消息由各自的selector 负责通知。
基本上Netty也是这样实现的,只不过不是每次新创建一个线程,而是有个workerGroup,对应一个线程池,线程池内已经预准备好了线程,每次有连接后,每个新产生的客户端通道SocketChannel 分配到某个线程池内的线程,该线程对应具体的NioEventLoop,一个NioEventLoop可以对应多个SocketChannel ,而一个SocketChannel 只会对应一个NioEventLoop,不会变化。
NioEventLoop内部有个selector,也就是说存在多个selector,只要在各自的selector上设置相应的关心事件的类型,那么就会出现分组的概念,各组负责各自组内成员的事件通知,不会跨组通知。
主从结构示意图,与多线程的区别是可以有多个线程负责Accept:
4. 总结
通过本篇文章,你可以了解多个selector之间是如何协同工作的,据此,知道了netty底层实现,这样,你就可以轻松的去阅读netty源码了。