1.Netty线程模型分类
事实上,Netty线程模型与与Reactor线程模型(之前有介绍)相似,下面我们通过Netty服务端和客户端的线程处理流程来介绍Netty的线程模型。
1.1服务端线程模型
一种比较流行的做法是服务端监听线程和IO线程分离,类似于Reactor的多线程模型,它的工作原理图如下:
Netty服务端线程工作流程
下面结合Netty的源码,对服务端创建线程工作流程进行介绍:
第一步,从用户线程发起创建服务端操作,代码如下:
public void run() throws Exception
{
//config the server
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try
{
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup,workerGroup).
option(ChannelOption.SO_BACKLOG,100).
handler(new LoggingHandler(LogLevel.INFO)).
childHandler(new ChannelInitial<SocketChannel>()
{
@override
public void initChannel(SocketChannel ch) throws Exception
{
ch.pipeline().addlast(
//new LoggingHandler(LogLevel.INFO)
new EchoSeverHandler());
}
});
//Start the server.
ChannelFuture f = b.bind(port).sync();
}
}
通常情况下,服务端的创建是在用户进程启动的时候进行,因此一般由Main函数或者启动类负责创建,服务端的创建由业务线程负责完成。在创建服务端的时候实例化了2个EventLoopGroup,1个EventLoopGroup实际就是一个EventLoop线程组,负责管理EventLoop的申请与释放。
EventLoopGroup管理的线程数可以通过构造函数设置,如果没有设置,默认取-Dio.netty.eventLoopThreads,如果该系统参数也没有设定,则为可用的CPU内核数 x 2.
private static final int DEFAULT_EVENT_LOOP_THREADS;
static {
DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
"io.netty.eventLoopThreads", Runtime.getRuntime().availableProcessors() * 2));
if (logger.isDebugEnabled()) {
logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS);
}
}
bossGroup线程组实际就是Acceptor线程池,负责处理客户端的TCP连接请求,如果系统只有一个服务端端口需要监听,则建议bossGroup线程组线程数设置为1。
workerGroup线程是真正负责IO读写操作的线程组,通过SeverBootstrap的group方法进行设置,用于后续的Channel绑定。
第二步,Acceptor线程绑定监听端口,启动NIO服务,相关代码如下:
/**
* 从bossGroup中选择一个Acceptor线程监听服务端
*/
@override
Channel createChannel()
{
EventLoop eventLoop = group().next();
return channelFactory().newChannel(eventLoop,childGroup);
}
其中,group()组返回的就是bossGroup,它的next方法用于从线程组中获取可用线程,代码如下:
/**
* 选择Acceptor线程
* /
@override
public EventExecutor next()
{
return children[Math.abs(childIndex.getAndIncrement()%children.length)];
}
服务端Channel创建完成之后,将其注册到多路复用器Selector上,用于接收客户端的TCP连接,核心代码如下:
/**
* Create a new instance,注册ServerSocketChannel到Selector
* /
public NioServerSocketChannel(EventLoop eventLoop, EventLoopGroup childGroup)
{ super(null,eventLoop,childGroup,newSocket(),SelectionKey.OP_ACCEPT);
config = new DefaultServerSocketChannelConfig(this,javaChannel().socket());
}
第三步,如果监听到客户端连接,则创建客户端SocketChannel连接,重新注册到workerGroup的IO线程上。首先看Acceptor如何处理客户端的接入:
/**
* 处理读或者连接事件
* /
try
{
int readyOps = k.readyOps();
//Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead
//to a spin loop
if(readyOps&(SelectionKey.OP_READ|SelectionKey.OP_ACCEPT)!=0||readyOps==0)
unsafe.read();
if(!ch.isOpen())
{
//connection already closed - no need to handle write.
return;
}
}
调用unsafe的read()方法,对于NioServerSocketChannel,它调用了NioMesageUnsafe的read()方法,代码如下:
/**
* NioServerSocketChannel的read()方法
Throwable exception = null;
try
{
for(;;)
{
int localRead = doReadMessage(readBuf);
if(localRead == 0)
{
break;
}
if(localRead<0)
{
closed = true;
break;
}
}
}
最终它会调用NioServerSocketChannel的doReadMessage()方法,代码如下:
/**
* 创建客户端连接SocketChannel的read()方法
protected int doReadMesage(List<Object> buf) throws Exception
{
SocketChannel ch = javaChannel().accept();
try
{
if(ch!=null)
{
buf.add(new NioSocektChannel(this,childEventLoopGroup().next(),ch));
return 1;
}
}
}
其中childEventLoopGroup就是之前的workerGroup,从中选择一个IO线程负责网络消息的读写。
第四步,选择IO线程后,将SocketChannel注册到多路复用器上,监听Read操作。
/**
*监听网络读事件
*/
protected AbstractNioByteChannel(Channel parent,EventLoop eventLoop,SelectableChannel ch)
{
super(parent,eventLoop,ch,SelectionKey.OP_READ);
}
第五步,处理网络的IO读写事件,核心代码如下:
/**
* 处理读写事件
int readyOps = k.readyOps();
//Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead to a spin loop
if((readyOps&(SelectionKey.OP_READ|Selection.OP_ACCEPT))!=0||readyOps==0)
{
unsafe.read();
if(!ch.isOpen())
{
return;
}
if(readyOps&SelectinKey.OP_WRITE!=0)
{
ch.unsafe().forceFlush();
}
}
1.2客户端线程模型
Netty 客户端线程模型
相比于服务端,客户端的线程模型简单一些,它的工作原理如下:
第一步,由用户线程发起客户端连接,示例代码如下:
/**
* Netty 客户端创建代码
//Configure the client
EventLoopGroup group = new NioEventLoopGroup();
try
{
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY,ture)
.handler(new ChannelInitializer<SocketChannel>()
{
@override
public void initChannel(SocketChannel ch) throws Exception
{
ch.pipeline().addLast
(
//new LoggingHandler(LogLevel.INFO)
new EchoClientHandler(firstMessageSize)
);
}
});
//Start the Client
ChannelFuture f = b.connect(host,port).sync();
}
相比于服务端,客户端只需要创建一个EventLoopGroup,因为它不需要独立的线程去监听客户端连接,也没有必要通过一个单独的客户端线程去连接服务端。Netty是异步事件驱动的NIO框架,它的连接和所有的IO操作都是异步的,因此不需要创建单独的连接线程。相关代码如下:
//绑定客户端连接线程
@override
Channel createChannel()
{
EventLoop eventLoop = group.next();
return ChannelFactory().newChannel(EventLoop);
}
当前的group()就是之前传入的EventLoopGroup,从中获取可用的IO线程EventLoop,然后作为参数设置到新创建的NioChannel中。
第二步,发起连接操作,判断连接结果,代码如下:
/**
* 连接操作
* /
@override
protected boolean doConnect(SocketAddress remoteAddress,SocketAddress localAddress)
{
if(localAddress!=null)
{
javaChannel().socket().bind(localAddress);
}
boolean success = false;
try
{
boolean connected = javaChannel().connect(remoteAddress);
if(!connected)
{
selectionKey().interestOps(SelectionKey.OP_CONNECT);
}
success = true;
return connected;
}finally
{
if(!success)
{
doClose();
}
}
}
判断连接结果,如果没有连接成功,则监听连接网络操作位SelectionKey.OP_CONNECT。如果连接成功,则调用pipeline().fireChannelActive()将监听位修改为READ。
第三步,由NioEventLoop的多路复用轮询连接操作结果,代码如下:
/**
*Selector 发起轮询操作
*/
if(readyOps&SelectionKey.OP_CONNECT!=0)
{
int ops = k.interestOps();
ops&=~SelectionKey.OP_CONNECT;
k.interestOps(ops);
unsafe.finishConnect();
}
判断连接结果,如果连接成功,重新设置监听位为READ:
/**
* 判断连接操作结果
@override
public void finishConnect()
{
assert eventLoop().inEventLoop();
assert connectPromise !=null;
try
{
boolean wasActive = isActive();
doFinishConnect();
FulfillConnectPromise(connectPromise,wasActive);
}
}
/**
* 设置操作位为READ
* /
@override
protected void doBeginRead() throws Exception
{
if(inputShutDown)
return;
final SelectionKey selectionKey = this.selectionKey;
if(!selectionKey.isValid())
{
return;
}
final int interestOps = selectionKey.interestOps();
if(interestOps&readinterestOp==0)
{
selectionKey.interestOps(interestOps|readInterestOp);
}
}
第四步,由NioEventLoop线程负责IO读写,同服务端。
总结:客户端创建,线程模型如下:
1.由用户线程负责初始化客户端资源,发起连接操作;
2.如果连接成功,将SocketChannel注册到IO线程组的NioEventLoop线程中,监听读操作位;
3.如果没有立即连接成功,将SocketChannel注册大NioEventLoop线程中,监听连接操作位;
4.连接成功后,修改监听位为READ,但是不需要切换线程。
注:NioEventLoop的介绍,将会放在下一章节。