Netty线程模型

来源:互联网 发布:收快递的软件 编辑:程序博客网 时间:2024/05/28 15:38

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线程监听服务端*/@overrideChannel createChannel(){    EventLoop eventLoop = group().next();    return channelFactory().newChannel(eventLoop,childGroup);}

其中,group()组返回的就是bossGroup,它的next方法用于从线程组中获取可用线程,代码如下:

/*** 选择Acceptor线程* /@overridepublic 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 loopif((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 clientEventLoopGroup 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操作都是异步的,因此不需要创建单独的连接线程。相关代码如下:

//绑定客户端连接线程@overrideChannel createChannel(){    EventLoop eventLoop = group.next();    return ChannelFactory().newChannel(EventLoop);}

当前的group()就是之前传入的EventLoopGroup,从中获取可用的IO线程EventLoop,然后作为参数设置到新创建的NioChannel中。

第二步,发起连接操作,判断连接结果,代码如下:

/*** 连接操作* /@overrideprotected 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:

/*** 判断连接操作结果@overridepublic 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的介绍,将会放在下一章节。