Netty源码篇(一) 服务器启动源码分析

Netty是什么就不过多介绍了,源码分析的代码来自于netty源码包下的案例。
在这里插入图片描述
在这里插入图片描述

今天分析的代码就是上面这一段,现在我们就开始分析服务端启动的源码流程。(在看源码之前希望大家能够了解Nio、三种Reactor模型等前置知识,这些可以帮助你进行源码理解。源码这个东西自己看的越多就越容易理解,大家坚持下去就一定能掌握

1.创建EventLoopGroup

1.1 关于线程数的设置

我们Netty服务端需要创建两个EventLoopGroup(事件循环组),分别用来监听Accept事件和其他事件,因为创建流程基本大同小异,这里以创建bossGroup()的流程为例。

EventLoopGroup bossGroup = new NioEventLoopGroup(1);

构造器,一路的this(…)、this(…)一直到MultithreadEventLoopGroup类的构造方法时对我们参数中的线程数有了操作。
在这里插入图片描述
判断我们的传入的线程数是否为0(没传入默认是0),如果是0的话,那么采用常量DEFAULT_EVENT_LOOP_THREADS的值,否则就是我们自己设置的值。那么DEFAULT_EVENT_LOOP_THREADS的值是多少呢,我们看到了是在类的构造器中进行初始化的。
在这里插入图片描述
DEFAULT_EVENT_LOOP_THREADS的值是 1 与NettyRuntime.availableProcessors() * 2这个表达式的较大者,我们一直跟进这个方法,发现其实是通过Runtime类获取我们操作系统可用的处理器内核数,那么说明如果我们不设置线程数的话,默认是我们操作系统内核数的两倍。比如8核的cpu对应默认就是16个线程。
在这里插入图片描述

1.2 构造方法主体内容

上面分析完了线程数的设定,我们继续回到我们的构造方法,继续this(…)、super(…)的跟进,最终来到了一个有真正内容的方法。
在这里插入图片描述

将方法的主题内容复制在下面,为了节省篇幅,一些验证性的代码就不贴了。

		第一部分代码
 		children = new EventExecutor[nThreads];

        for (int i = 0; i < nThreads; i ++) {
            boolean success = false;
            try {
                children[i] = newChild(executor, args);
                success = true;
            } catch (Exception e) {
                // TODO: Think about if this is a good exception type
                throw new IllegalStateException("failed to create a child event loop", e);
            } finally {
                if (!success) {
                    for (int j = 0; j < i; j ++) {
                        children[j].shutdownGracefully();
                    }

                    for (int j = 0; j < i; j ++) {
                        EventExecutor e = children[j];
                        try {
                            while (!e.isTerminated()) {
                                e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
                            }
                        } catch (InterruptedException interrupted) {
                            // Let the caller handle the interruption.
                            Thread.currentThread().interrupt();
                            break;
                        }
                    }
                }
            }
        }
        
		第二部分代码
		chooser = chooserFactory.newChooser(children);

		第三部分代码
        final FutureListener<Object> terminationListener = new FutureListener<Object>() {
            @Override
            public void operationComplete(Future<Object> future) throws Exception {
                if (terminatedChildren.incrementAndGet() == children.length) {
                    terminationFuture.setSuccess(null);
                }
            }
        };
		for (EventExecutor e: children) {
            e.terminationFuture().addListener(terminationListener);
        }

        Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length);
        Collections.addAll(childrenSet, children);
        readonlyChildren = Collections.unmodifiableSet(childrenSet);

我将上述代码按作用分为了三个部分,我们一个部分一个部分进行解读。

第一部分:初始化事件循环组

children = new EventExecutor[nThreads];

首先创建了一个EventExecutor的数组,EventExecutor是我们事件循环NioEventLoop的父接口,这个操作说明了我们底层用了数组存放我们的EventLoop。

		boolean success = false;
        try {
           children[i] = newChild(executor, args);
           success = true;
        }

然后在循环体内有这么一段代码,我们调用newChild()方法给每一个数组元素进行赋值,我们看看我们是如何创建出NioEventLoop的。
在这里插入图片描述
返回的是直接new的NioEventLoop对象,并且在里面对我们的任务队列,选择器等进行了初始化,这里就部继续跟下去了。
在这里插入图片描述
try语句里面的看完了,因为我们是循环创建的,所以创建中出现异常,我们会将之前创建的那些事件循环进行关闭,这里没什么好说的,我们看第二部分代码。
在这里插入图片描述

第二部分:创建选择器

chooser = chooserFactory.newChooser(children);

第二部分代码只有一句话,就是创建出我们事件循环组工作时候的选择选择器,主要想说的是这个选择器如何选择我们的EventLoop的算法算得上是奇淫巧计。
在这里插入图片描述
跟进newChooser()方法,发现创建的是PowerOfTwoEventExecutorChooser类型的选择器,我们看看这个选择器的next()方法。
在这里插入图片描述
相信有些同学看到这里就有些蒙了,这是怎么选择的,首先我们将变量 idx看作是一个int 类型的变量 index ,然后这个next实际上可以翻译为下面这条语句

return executors[++index & executors.length - 1];

那么 ++index & executors.length - 1到底有什么用呢,这就涉及到我们与操作的特殊含义了,如果两个数相与,表面上是两个数按位与,同一为1,不同为0,实际上与A & B的另一个含义是,二者中的较大者对另一个数+1取模。
假设 A >= B ,那么 A & B = A%(B + 1)。不信的同学可以验证一下 ,假设 A = 7 (111)
B = 3(011) 结果为 res = 011(3)实际上就是 7 % 4。有能力的同学可以在数学上证明一下这个结论,这里就不过多叙述了。然后我们选取下一个EventLoop的原理实际上是索引一直自增,然后对数组长度取模,类似于轮询地进行选取。

第三部分:收尾操作
第三部分主要做的就是给EventLoop加上关闭监听,创建只读集合等等,没什么好说的,不是核心源码,大家有兴趣的可以看一下。

2.创建ServerBootstrap

		ServerBootstrap b = new ServerBootstrap();
        b.group(bossGroup, workerGroup)
        .channel(NioServerSocketChannel.class)
        .option(ChannelOption.SO_BACKLOG, 100)
        .handler(new LoggingHandler(LogLevel.INFO))
        .childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            public void initChannel(SocketChannel ch) throws Exception {
               ChannelPipeline p = ch.pipeline();
               if (sslCtx != null) {
                    p.addLast(sslCtx.newHandler(ch.alloc()));
               }
               p.addLast(new EchoServerHandler());
            }
        });

第一行是一个空参构造器没什么好说的我们就不看了,大家可以自行点进去看看。其实这部分内容也很少,主要就是给我们的bossGroup和workerGroup进行一些参数的设置,需要注意的一点是workerGroup的主体内容大都保存在ServerBootstrap本类的属性中,bossGroup的主体内容都保存在ServerBootstrap父类AbstractBootstrap的属性中。
在这里插入图片描述
在这里插入图片描述

3.服务器启动

 ChannelFuture f = b.bind(PORT).sync();

重头戏来了,bind()方法中的内容决定了我们的服务器是如何启动起来的。sync()方法只是同步等待创建结果的而已,我们跟进bind()方法。
在这里插入图片描述
验证一下EventLoop等组件的正确性后,我们进入doBind()方法。这部分代码核心的有两个initAndRegister()和doBind0(),我们接下来对他们进行展开讲解。

 private ChannelFuture doBind(final SocketAddress localAddress) {
        final ChannelFuture regFuture = initAndRegister();
        final Channel channel = regFuture.channel();
        if (regFuture.cause() != null) {
            return regFuture;
        }

        if (regFuture.isDone()) {
            // At this point we know that the registration was complete and successful.
            ChannelPromise promise = channel.newPromise();
            doBind0(regFuture, channel, localAddress, promise);
            return promise;
        } else {
            // Registration future is almost always fulfilled already, but just in case it's not.
            final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
            regFuture.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    Throwable cause = future.cause();
                    if (cause != null) {
                        // Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an
                        // IllegalStateException once we try to access the EventLoop of the Channel.
                        promise.setFailure(cause);
                    } else {
                        // Registration was successful, so set the correct executor to use.
                        // See https://github.com/netty/netty/issues/2586
                        promise.registered();

                        doBind0(regFuture, channel, localAddress, promise);
                    }
                }
            });
            return promise;
        }
    }

3.1 initAndRegister()

    final ChannelFuture initAndRegister() {
        Channel channel = null;
        try {
            channel = channelFactory.newChannel();
            init(channel);
        } catch (Throwable t) {
            if (channel != null) {
                // channel can be null if newChannel crashed (eg SocketException("too many open files"))
                channel.unsafe().closeForcibly();
                // as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor
                return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t);
            }
            // as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor
            return new DefaultChannelPromise(new FailedChannel(), GlobalEventExecutor.INSTANCE).setFailure(t);
        }

        ChannelFuture regFuture = config().group().register(channel);
        if (regFuture.cause() != null) {
            if (channel.isRegistered()) {
                channel.close();
            } else {
                channel.unsafe().closeForcibly();
            }
        }


        return regFuture;
    }

跟进initAndRegister()方法,我们发现主要做了三件事情

  1. channel = channelFactory.newChannel();
  2. init(channel);
  3. ChannelFuture regFuture = config().group().register(channel);

我们一个一个来看
1.channel = channelFactory.newChannel();
在这里插入图片描述

跟进方法内部发现是通过反射创建clazz对象实例,clazz实际上是在我们第二步中channel方法进行指定的。newChannel()方法的作用就是就是创建出我们的NioServerSocketChannel实例对象。
在这里插入图片描述

2.init(channel);

void init(Channel channel) throws Exception {
        final Map<ChannelOption<?>, Object> options = options0();
        synchronized (options) {
            setChannelOptions(channel, options, logger);
        }

        final Map<AttributeKey<?>, Object> attrs = attrs0();
        synchronized (attrs) {
            for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
                @SuppressWarnings("unchecked")
                AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
                channel.attr(key).set(e.getValue());
            }
        }

        ChannelPipeline p = channel.pipeline();

        final EventLoopGroup currentChildGroup = childGroup;
        final ChannelHandler currentChildHandler = childHandler;
        final Entry<ChannelOption<?>, Object>[] currentChildOptions;
        final Entry<AttributeKey<?>, Object>[] currentChildAttrs;
        synchronized (childOptions) {
            currentChildOptions = childOptions.entrySet().toArray(newOptionArray(childOptions.size()));
        }
        synchronized (childAttrs) {
            currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(childAttrs.size()));
        }

        p.addLast(new ChannelInitializer<Channel>() {
            @Override
            public void initChannel(final Channel ch) throws Exception {
                final ChannelPipeline pipeline = ch.pipeline();
                ChannelHandler handler = config.handler();
                if (handler != null) {
                    pipeline.addLast(handler);
                }

                ch.eventLoop().execute(new Runnable() {
                    @Override
                    public void run() {
                        pipeline.addLast(new ServerBootstrapAcceptor(
                                ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
                    }
                });
            }
        });
    }

这个方法前大半部分的操作都是将我们给ServerBootStrap()注册的那些属性(options、attrs等等)赋值给我们的ServerSocketChannel,但是最后部分有一段代码很重要。
在这里插入图片描述
该部分的initChannel在我们的initChannel连接初始化时会执行,往我们的pipeline管道里加入了一个ServerBootstrapAcceptor,这个类是我们bossGroup实现监听到accpect事件,获取对应的socketChannel然后注册在workerGroup上的主要实现类。我们可以先看一看这个类的对应代码
在这里插入图片描述
在 channelRead方法中,将我们传入的channel注册到了childGroup(workerGroup)中,这部分具体执行流程在我们下一篇服务器接收请求的源码分析中会详细分析,这里就不细说了。
综上init()方法除了对一些TCP参数的设置之外,还将我们的ServerBootstrapAcceptor加入到了管道之中。

3.config().group().register(channel);
这条语句前两个方法返回值实际上是我们的BossGroup,然后调用了对应的register方法,我们跟进register()方法中去。

在这里插入图片描述
调用了我们的next()的register,选择一个EventLoop进行注册,因为我们的bossGroup一般只有一个EventLoop,所以这里不用太过在意,继续跟进方法。
在这里插入图片描述
最终调到了这个方法,我们直接跟进第二行的register()方法
在这里插入图片描述
来到了下面的方法。

public final void register(EventLoop eventLoop, final ChannelPromise promise) {
            if (eventLoop == null) {
                throw new NullPointerException("eventLoop");
            }
            if (isRegistered()) {
                promise.setFailure(new IllegalStateException("registered to an event loop already"));
                return;
            }
            if (!isCompatible(eventLoop)) {
                promise.setFailure(
                        new IllegalStateException("incompatible event loop type: " + eventLoop.getClass().getName()));
                return;
            }

            AbstractChannel.this.eventLoop = eventLoop;

            if (eventLoop.inEventLoop()) {
                register0(promise);
            } else {
                try {
                    eventLoop.execute(new Runnable() {
                        @Override
                        public void run() {
                            register0(promise);
                        }
                    });
                } catch (Throwable t) {
                    logger.warn(
                            "Force-closing a channel whose registration task was not accepted by an event loop: {}",
                            AbstractChannel.this, t);
                    closeForcibly();
                    closeFuture.setClosed();
                    safeSetFailure(promise, t);
                }
            }
        }

前三个判断都是一些验证性的操作,我们无需在意,我们直接看下面这段代码
在这里插入图片描述
eventLoop.inEventLoop()的方法实际上是判断当前的eventLoop绑定的线程是不是正在执行的线程
在这里插入图片描述
注意我们的bind()方法是主方法调用的,所以肯定不是我们bossGroup绑定的线程。我们进入else的判断。
在这里插入图片描述
这段代码很重要,很重要
我们跟进eventLoop的execute()方法
在这里插入图片描述
依旧是判断当前线程是不是绑定的线程,**不是的话就startThread();!startThread();!startThread();!**重要的事情说三遍,这就是我们服务器线程的开启代码,我们跟进去看看。
在这里插入图片描述
防止多线程同时操作的问题,使用CAS算法控制同步,调用了doStartThread();方法,继续跟进去。
在这里插入图片描述
executor相当于线程池,对应的excute方法完成的操作就是开启线程。
在这里插入图片描述
然后在run方法内调用了我们的 **SingleThreadEventExecutor.this.run();**这是我们的eventLoop的核心代码,一个死循环,不断执行以下三步
在这里插入图片描述
在这里插入图片描述

  1. selector.select() 监听channel事件
  2. processSelectedKeys(); 处理事件
  3. runAllTasks(); 执行任务队列中的任务
    这部分内容属于我们下一篇服务器接收客户端请求的核心分析代码,这里就不反客为主了,大家在当前流程需要知道的就是,服务器在register()代码中开启了我们的线程,并将我们的注册任务放到了任务队列中。至此我们的initAndRegister()方法就分析完毕了。

3.2 doBind0()

上一步我们的服务器线程启动完毕之后,下一步需要做的就是端口绑定了。
在这里插入图片描述
如果我们的异步执行已经做完了,我们就直接调用doBind0(),否则我们就将doBind0()加入到任务队列中去。
在这里插入图片描述
doBind0实际上就是给我们所有的channel绑定好端口地址,然后注册关闭的监听事件。
至此我们的doBInd方法就执行完毕了,我们的服务器也真正的启动起来了。

后续应该还会更新三篇netty的源码,应该分别是服务器接收请求源码分析handler调用链源码分析和netty心跳源码分析。(应该会更新吧、、、、如果我不是太懒的话,不过写博客真的很耗时间。。。。。尤其是分析源码的博客,动不动就三四个小时,已经分析的很简略了,但是还是需要花很多时间,希望能对大家有帮助。。)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Netty5.0 架构剖析和源码解读 作者:李林锋 版权所有 email neu_lilinfeng@ © Netty5.0 架构剖析和源码解读1 1. 概述2 1.1. JAVA 的IO演进2 1.1.1. 传统BIO通信的弊端2 1.1.2. Linux 的网络IO模型简介4 1.1.3. IO复用技术介绍7 1.1.4. JAVA的异步IO8 1.1.5. 业界主流的NIO框架介绍10 2.NIO入门10 2.1. NIO服务端10 2.2. NIO客户端13 3.Netty源码分析16 3.1. 服务端创建16 3.1.1. 服务端启动辅助类ServerBootstrap16 3.1.2. NioServerSocketChannel 的注册21 3.1.3. 新的客户端接入25 3.2. 客户端创建28 3.2.1. 客户端连接辅助类Bootstrap28 3.2.2. 服务端返回ACK应答,客户端连接成功32 3.3. 读操作33 3.3.1. 异步读取消息33 3.4. 写操作39 3.4.1. 异步消息发送39 3.4.2. Flush操作42 4.Netty架构50 4.1. 逻辑架构50 5. 附录51 5.1. 作者简介51 5.2. 使用声明51 1. 概述 1.1.JAVA 的IO演进 1.1.1. 传统BIO通信的弊端 在JDK 1.4推出JAVANIO1.0之前,基于JAVA 的所有Socket通信都采用 BIO 了同步阻塞模式( ),这种一请求一应答的通信模型简化了上层的应用开发, 但是在可靠性和性能方面存在巨大的弊端。所以,在很长一段时间,大型的应 C C++ 用服务器都采用 或者 开发。当并发访问量增大、响应时间延迟变大后, 采用JAVABIO作为服务端的软件只有通过硬件不断的扩容来满足访问量的激 增,它大大增加了企业的成本,随着集群的膨胀,系统的可维护性也面临巨大 的挑战,解决这个问题已经刻不容缓。 首先,我们通过下面这幅图来看下采用BIO 的服务端通信模型:采用BIO 通信模型的 1connect NewThread1 WebBrowse 2connect 2handle(Req) WebBrowse 3connect Acceptor NewThread2 WebBrowse WebBrowse 4connect NewThread3 3sendResponsetopeer NewThread4 图1.1.1-1 BIO通信模型图 服务端,通常由一个独立的Accepto 线程负责监听客户端的连接,接收到客户 端连接之后为客户端连接创建一个新的线程处理请求消息

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值