在netty的线程模型中,bossGroup只负责请求的转发,workerGroup是具体的数据处理,其实netty使用的是Reactor(响应器)的设计模式。
一篇文章对这种模式做了非常细致的介绍,《Scalable IO in Java》 ,这篇文章的作者是 Doug Lea!!!
采用分治是应对拓展的好方法:
将一个处理过程分成多个小任务,每个任务都是非阻塞的;
当任务可执行时再调用;下图中IO事件通常充作触发开关
底层利用java.nio 的非阻塞的读与写操作,一个Selector管理多个channel。
来看看Reactor是怎么做的。
使用异步非阻塞 I/O,一个线程独立处理所有 I/O相关操作。通过 Acceptor 类接收客户端连接请求,建立链路,dispatch 将对应的信息 ByteBuffer 分发到响应 handler,进而对消息进行解析处理。
通过代码来理解:
class Reactor implements Runnable {
final Selector selector;
final ServerSocketChannel serverSocket;
Reactor(int port) throws IOException {
selector = Selector.open();
serverSocket = ServerSocketChannel.open();
serverSocket.socket().bind(new InetSocketAddress(port));
serverSocket.configureBlocking(false);
SelectionKey sk =serverSocket.register(selector,SelectionKey.OP_ACCEPT);
sk.attach(new Acceptor());
}
/*
Alternatively, use explicit SPI provider:
SelectorProvider p = SelectorProvider.provider();
selector = p.openSelector();
serverSocket = p.openServerSocketChannel();
*/
// class Reactor continued
public void run() { // normally in a newThread
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) { /* ... */ }
}
}
void dispatch(SelectionKey k) {
Runnable r = (Runnable)(k.attachment());
if (r != null){
r.run();
}
}
Reactor 负责监听连接请求,初始化时绑定一个Acceptor,连接到来后遍历selectedKeys,调用dispatch分发请求,在dispatch里边通过selectedKey得到绑定的Acceptor,调用其run方法
看一下Acceptor :
// class Reactor continued
class Acceptor implements Runnable { // inner
public void run() {
try {
SocketChannel c = serverSocket.accept();
if (c != null)
new Handler(selector, c);
}catch(IOException ex) { /* ... */ }
}
}
}
构造了一个SocketChannel与该客户端通信,创建了处理该请求的handler。
Handler有SocketChannel 和SelectionKey的引用,Handler的构造器将当前类(Handler)加入到绑定里边,并且对READ感兴趣,之后调sel.wakeup()意思是让select( )方法立刻返回,如果当前没有select()方法阻塞的话,那么下一次调用select()会立即返回,然后执行run()方法,是通过判断状态的方式来决定是写还是读 ,这个在Netty3中就是需要这样实现handler代码的,需要自己判断状态来决定业务逻辑。Netty4已经改成各种回调了,比如channelRead,channelActive等。
Handler:
final class Handler implements Runnable {
final SocketChannel socket;
final SelectionKey sk;
ByteBuffer input = ByteBuffer.allocate(MAXIN);
ByteBuffer output = ByteBuffer.allocate(MAXOUT);
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
sk = socket.register(sel, 0);
sk.attach(this);
sk.interestOps(SelectionKey.OP_READ);
sel.wakeup();//注册OP_READ兴趣之后,让select()方法返回,接受要读取的数据
}
boolean inputIsComplete() { /* ... */ }
boolean outputIsComplete() { /* ... */ }
void process() { /* ... */ }
// class Handler continued
public void run() {
try {
if (state == READING) read();
else if (state == SENDING) send();
} catch (IOException ex) { /* ... */ }
}
void read() throws IOException {
socket.read(input);
if (inputIsComplete()) {
process();
state = SENDING;
// Normally also do first write now
sk.interestOps(SelectionKey.OP_WRITE);//将状态变为SENDING之后,接下来就是往外写数据,对写感兴趣。
}
}
void send() throws IOException {
socket.write(output);
if (outputIsComplete()) sk.cancel();
}
}
单线程模型适合一些小容量场景,无法应对高负载,高并发需求。
handler 优化,引入handler 线程池,也就是使用一组线程来处理 I/O操作。
有一个专门监听客户端连接请求的 Acceptor 线程;线程池包含多个可用线程,由这些线程来负责消息的处理;一个线程可以同时处理多条链路,但一条链路只对应一个线程。
服务端用于接收客户端的连接是一个独立的线程池 mainReactor,Acceptor 接收到客户端的链接请求处理后,将新创建的SocketChannel 注册到 subReactor 线程池的某个线程上,由他来负责 SocketChannel。这也是Netty 推荐使用的线程模型。
Netty同时支持多种线程模型,你可以根据需要来配置。
图片来自《Netty权威指南》
这张图结合源码看会由更好的理解,Netty源码解析-NioEventLoop