webIM系列之二——Netty的Reactor模型分析

747 阅读6分钟

Netty这个框架相信大家都很熟悉,Java世界中网络IO处理的集大成者,很多我们用到的框架的底层通信都是Netty实现的,例如Dubbo、Hadoop等等,但凡是Java体系的即时通信系统,都无法绕开Netty,我自己也还在不断的学习,今天先看一下Netty的基本使用,后面会将Netty的各个组件深入的去分析,个人水平有限,如有不正确的,希望大家可以指正。

使用SpringBoot集成Netty的方式,创建一个Netty服务端程度:

package com.example.nettysourcedemo;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.concurrent.DefaultThreadFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class ServerRunner implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        /**
         * NioEventLoopGroup本身是一个eventLoopExecutor,里面包含一个childGroup=EventExecutor[]
         * 数组中是NioEventLoop,每一个NioEventLoop中都包含一个selector,还包含一个executor
         * 这个executor可以用来执行任务,在使用executor.execute执行任务时,会使用ThreadPerTaskExecutor.execute()方法执行
         * 这个方法又会调用DefaultThreadFactory创建一个线程来启动任务的执行
         * 重点关注NioEventLoopGroup构造器中执行的父类构造器
         */
        NioEventLoopGroup boss = new NioEventLoopGroup(new DefaultThreadFactory("boss"));
        NioEventLoopGroup worker = new NioEventLoopGroup(new DefaultThreadFactory("worker"));
        NioEventLoopGroup business = new NioEventLoopGroup(new DefaultThreadFactory("business"));

        ServerBootstrap b = new ServerBootstrap();
        b.group(boss,worker)
                //调用ReflectiveChannelFactory的构造器,创建NioServerSocketChannel
                .channel(NioServerSocketChannel.class)
                //设置boss线程的属性
                .option(ChannelOption.SO_BACKLOG,1024)
                //设置worker线程的属性,参考Linux tcp参数设置
                .childOption(ChannelOption.TCP_NODELAY,true)
                //ChannelInitializer也是一个ChannelInboundHandler,主要作用是往pipeline中添加handler,添加完handler之后就会从pipeline中移除
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline();
                        //使用单独的业务线程池执行业务handler
                        pipeline.addLast(business,"stringDecoder",new StringDecoder());
                        pipeline.addLast(business,"stringEncoder",new StringEncoder());
                        pipeline.addLast(business,"simpleHandler",new SimpleHandler());
                        pipeline.addLast(business,"realHandler",new RealHandler());
                    }
                });
        ChannelFuture future = b.bind(9080).sync();
        log.info("netty server started");
        future.channel().closeFuture().sync();
        boss.shutdownGracefully();
        worker.shutdownGracefully();
        business.shutdownGracefully();
    }
}

ServerRunner类实现CommandLineRunner接口,显示的告知Spring容器,需要运行这个类。当然@Component注解也是必不可少的,需要告知Spring容器,这也是个JavaBean,需要容器去管理。

一个比较简单的Netty服务端程序主要包括设置parentGroup线程池(boss)、childGroup线程池(worker),设置使用的channel类型(NioServerSocketChannel),设置parentGroup线程的属性(#option()方法),设置childGroup线程的属性(#childOption()方法),设置流水线上的处理器(#childHandler()方法),绑定端口(#bind()方法)。

我写的这个程序比上面介绍的多了一个business的线程池,这个线程池主要用来处理比较耗时的业务流程。

Netty很贴心的提供了ServerBootstrap这个启动类,只要简单的几行代码就能启动一个功能强大的NettyServer。

为什么要设置一个parentGroup线程池,一个childGroup线程池呢?这就不得不提Netty大名鼎鼎的Reactor(反应堆)模型了,两个线程池的参数不同,Reactor的模式也有所不同。

  • 只设置parentGroup(1)

    NioEventLoopGroup boss = new NioEventLoopGroup(1);

  • 只设置parentGroup(默认值)

    NioEventLoopGroup boss = new NioEventLoopGroup();

  • 设置parentGroup和childGroup

    NioEventLoopGroup boss = new NioEventLoopGroup();

    NioEventLoopGroup worker = new NioEventLoopGroup();

1.只设置parentGroup且参数为1 Reactor单线程模型 这种模式,整个反应堆中只有一个线程,这个线程既当爹又当妈,这个线程既要轮询客户端的连接状态,又要处理连接的IO操作,除此之外,非IO的操作,也要在这个线程上处理,这可能会大大延迟IO请求的响应,可以说这个线程是非常累了。这种模式只适用于连接数非常少的系统,像互联网这种动辄千万甚至过亿的并发,完全吃不消。

如下图,应用启动时,只有一个boss线程,当有多个客户端连接时,依然只有一个boss线程。 Reactor单线程-客户端连接 2.只设置parentGroup且参数大于1(默认值为当前服务器逻辑核心数的2倍) 当只设置parentGroup时,一个线程专门用来轮询客户端的连接事件,其他线程用来处理客户端连接后的IO事件的处理。 Reactor多线程模型 parentGroup设置为默认值时,应用启动,只有一个boss线程。 启动应用时只有一个boss线程 有一个客户端连接时,有两个boss线程。 有一个客户端连接时,有两个boss线程 有两个客户端连接时,有三个boss线程。 有两个客户端连接时,有三个boss线程 3.parentGroup和childGroup同时设置 当parentGroup和childGroup都设置为默认值时,parentGroup中只有一个boss线程用于轮询客户端连接事件,当有客户端连接上来后,所有的IO处理都会交给childGroup中的线程来处理。 主从Reactor模型 应用启动时,只有一个boss线程 只有一个boss线程 有一个客户端连接时,有一个boss线程,一个worker线程 一个boss线程,一个worker线程 有两个客户端连接时,有一个boss线程,两个worker线程 一个boss线程,两个worker线程 可以看出,当parentGroup和childGroup都设置时,parentGroup只有一个线程在运行中,但是,这时候服务端只监听了一个端口,让我们看一下服务端启动多个监听端口的情况,这时候程序运行后,会有两个boss线程。 多端口 有两个客户端分别连接不同端口的时候,两个boss,两个worker线程 两个端口,两个客户端连接,两个boss,两个worker线程 因此,parentGroup中的线程,只会用于监听端口的连接事件,IO操作都会交给childGroup线程来处理。

以上的几种情况,都是在没有启用business线程的情况下测试的结果,代码需做如下修改:

pipeline.addLast("stringDecoder",new StringDecoder());
pipeline.addLast("stringEncoder",new StringEncoder());
pipeline.addLast("simpleHandler",new SimpleHandler());
pipeline.addLast("realHandler",new RealHandler());

那如果启用了business线程会是什么情况呢?将代码恢复之后测试看一下结果: 除了boss线程外,worker线程和business线程都出现了。这种情况,parentGroup中的线程负责监听端口的连接事件,childGroup中的线程负责read()/write()等IO操作,业务流程处理的handler,交给business线程池处理。 多端口、business线程 上一个10年,计算机界还在为C10K(单机1万连接)问题而抓脑袋,现在C100K(单机10万连接)已经能够实现,人们已经开始展望C10M(单机千万连接)的问题,虽然现在分布式架构、集群架构都已经非常成熟了,能够足以支撑千万甚至上亿的连接数,但是计算机科学界还是在不遗余力的想要提高单机并发连接数,我认为,谁能最快的实现C10M,谁就能成就超越腾讯的伟大公司。

要想做好即时通信产品,除了Java端的知识之外,还要对Linux服务器网络、IO、文件等有非常深的理解。

这一篇文章主要介绍了NettyServer中boss线程和worker线程的配置引发的Reactor模型的差异,下一篇介绍一下NioEventLoopGroup的创建过程(非常烧脑)。