Netty线程模型

4 篇文章 0 订阅
2 篇文章 0 订阅

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的介绍,将会放在下一章节。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值