微信号:imooc-com

介绍:慕课网是一个超酷的互联网、IT技术免费学习平台,创新的网络一站式学习、实践体验;服务及时贴心,内容专业、有趣易学。专注服务互联网工程师快速成为技术高手!

Netty源码分析之服务端启动全解析

2018-06-07 20:15 闪电侠



JavaCoder如果没有研究过Netty,那么你对Java语言的使用和理解仅仅停留在表面水平。 




background



netty 是一个异步事件驱动的网络通信层框架,其官方文档的解释为


Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming such as TCP and UDP socket server.



也就是说,Netty 是一个基于NIO的客户、服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户、服务端应用。Netty相当于简化和流线化了网络应用的编程开发过程,例如:基于TCP和UDP的socket服务开发。


我们在新美大消息推送系统sailfish(日均推送消息量50亿),新美大移动端代理优化系统shark(日均吞吐量30亿)中,均选择了netty作为底层网络通信框架。


既然两大如此重要的系统底层都使用到了netty,所以必然要对netty的机制,甚至源码了若指掌,于是,便催生了netty源码系列文章。后面,我会通过一系列的主题把我从netty源码里所学到的毫无保留地介绍给你,源码基于4.1.6.Final。



why netty



netty底层基于jdk的NIO,我们为什么不直接基于jdk的nio或者其他nio框架?下面是我总结出来的原因:


1.使用jdk自带的nio需要了解太多的概念,编程复杂 

2.netty底层IO模型随意切换,而这一切只需要做微小的改动 

3.netty自带的拆包解包,异常检测等机制让你从nio的繁重细节中脱离出来,让你只需要关心业务逻辑 

4.netty解决了jdk的很多包括空轮训在内的bug 

5.netty底层对线程,selector做了很多细小的优化,精心设计的reactor线程做到非常高效的并发处理 

6.自带各种协议栈让你处理任何一种通用协议都几乎不用亲自动手 

7.netty社区活跃,遇到问题随时邮件列表或者issue 

8.netty已经历各大rpc框架,消息中间件,分布式通信中间件线上的广泛验证,健壮性无比强大



dive into netty



了解了这么多,今天我们就从一个例子出来,开始我们的netty源码之旅。


本篇主要讲述的是netty是如何绑定端口,启动服务。启动服务的过程中,你将会了解到netty各大核心组件,我先不会细讲这些组件,而是会告诉你各大组件是怎么串起来组成netty的核心


example


下面是一个非常简单的服务端启动代码


 
           
  1. public final class SimpleServer {

  2.  public static void main(String[] args) throws Exception {

  3.  EventLoopGroup bossGroup = new NioEventLoopGroup(1);

  4.  EventLoopGroup workerGroup = new NioEventLoopGroup();

  5.  try {

  6.  ServerBootstrap b = new ServerBootstrap();

  7.  b.group(bossGroup, workerGroup)

  8.  .channel(NioServerSocketChannel.class)

  9.  .handler(new SimpleServerHandler())

  10.  .childHandler(new ChannelInitializer<SocketChannel>() {

  11.  @Override

  12.  public void initChannel(SocketChannel ch) throws Exception {

  13.  }

  14.  });

  15.  ChannelFuture f = b.bind(8888).sync();

  16.  f.channel().closeFuture().sync();

  17.  } finally {

  18.  bossGroup.shutdownGracefully();

  19.  workerGroup.shutdownGracefully();

  20.  }

  21.  }

  22.  private static class SimpleServerHandler extends ChannelInboundHandlerAdapter {

  23.  @Override

  24.  public void channelActive(ChannelHandlerContext ctx) throws Exception {

  25.  System.out.println("channelActive");

  26.  }

  27.  @Override

  28.  public void channelRegistered(ChannelHandlerContext ctx) throws Exception {

  29.  System.out.println("channelRegistered");

  30.  }

  31.  @Override

  32.  public void handlerAdded(ChannelHandlerContext ctx) throws Exception {

  33.  System.out.println("handlerAdded");

  34.  }

  35.  }

  36. }

简单的几行代码就能开启一个服务端,端口绑定在8888,使用nio模式,下面讲下每一个步骤的处理细节

EventLoopGroup 已经在我的其他文章中详细剖析过,说白了,就是一个死循环,不停地检测IO事件,处理IO事件,执行任务

ServerBootstrap 是服务端的一个启动辅助类,通过给他设置一系列参数来绑定端口启动服务

group(bossGroup, workerGroup) 我们需要两种类型的人干活,一个是老板,一个是工人,老板负责从外面接活,接到的活分配给工人干,放到这里, bossGroup的作用就是不断地accept到新的连接,将新的连接丢给 workerGroup来处理

.channel(NioServerSocketChannel.class) 表示服务端启动的是nio相关的channel,channel在netty里面是一大核心概念,可以理解为一条channel就是一个连接或者一个服务端bind动作,后面会细说

.handler(new SimpleServerHandler() 表示服务器启动过程中,需要经过哪些流程,这里 SimpleServerHandler最终的顶层接口为 ChannelHander,是netty的一大核心概念,表示数据流经过的处理器,可以理解为流水线上的每一道关卡

childHandler(new ChannelInitializer<SocketChannel>)...表示一条新的连接进来之后,该怎么处理,也就是上面所说的,老板如何给工人配活

ChannelFuture f = b.bind(8888).sync(); 这里就是真正的启动过程了,绑定8888端口,等待服务器启动完毕,才会进入下行代码

f.channel().closeFuture().sync(); 等待服务端关闭socket

bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); 关闭两组死循环

上述代码可以很轻松地再本地跑起来,最终控制台的输出为:

 
           
  1. handlerAdded

  2. channelRegistered

  3. channelActive

关于为什么会顺序输出这些,深入分析之后其实很easy



在线学Netty教程

点击下图,即可订阅


想写出好代码,先看好代码怎么写 




深入细节

ServerBootstrap 一系列的参数配置其实没啥好讲的,无非就是使用method chaining的方式将启动服务器需要的参数保存到filed。我们的重点落入到下面这段代码

 
           
  1. b.bind(8888).sync();


这里说一句:我们刚开始看源码,对细节没那么清楚的情况下可以借助IDE的debug功能,step by step,one step one test或者二分test的方式,来确定哪行代码是最终启动服务的入口,在这里,我们已经确定了bind方法是入口,我们跟进去,分析


 
           
  1. public ChannelFuture bind(int inetPort) {

  2.  return bind(new InetSocketAddress(inetPort));

  3. }

通过端口号创建一个 InetSocketAddress,然后继续bind

 
           
  1. public ChannelFuture bind(SocketAddress localAddress) {

  2.  validate();

  3.  if (localAddress == null) {

  4.  throw new NullPointerException("localAddress");

  5.  }

  6.  return doBind(localAddress);

  7. }

validate() 验证服务启动需要的必要参数,然后调用 doBind()

 
           
  1. private ChannelFuture doBind(final SocketAddress localAddress) {

  2.  //...

  3.  final ChannelFuture regFuture = initAndRegister();

  4.  //...

  5.  final Channel channel = regFuture.channel();

  6.  //...

  7.  doBind0(regFuture, channel, localAddress, promise);

  8.  //...

  9.  return promise;

  10. }

这里,我去掉了细枝末节,让我们专注于核心方法,其实就两大核心一个是 initAndRegister(),以及 doBind0()


其实,从方法名上面我们已经可以略窥一二,init->初始化,register->注册,那么到底要注册到什么呢?联系到nio里面轮询器的注册,可能是把某个东西初始化好了之后注册到selector上面去,最后bind,像是在本地绑定端口号,带着这些猜测,我们深入下去


initAndRegister()


 
           
  1. final ChannelFuture initAndRegister() {

  2.  Channel channel = null;

  3.  // ...

  4.  channel = channelFactory.newChannel();

  5.  //...

  6.  init(channel);

  7.  //...

  8.  ChannelFuture regFuture = config().group().register(channel);

  9.  //...

  10.  return regFuture;

  11. }

我们还是专注于核心代码,抛开边角料,我们看到 initAndRegister() 做了几件事情 1.new一个channel 2.init这个channel 3.将这个channel register到某个对象


我们逐步分析这三件事情↓


1.new一个channel


我们首先要搞懂channel的定义,netty官方对channel的描述如下


A nexus to a network socket or a component which is capable of I/O operations such as read, write, connect, and bind


这里的channel,由于是在服务启动的时候创建,我们可以和普通Socket编程中的ServerSocket对应上,表示服务端绑定的时候经过的一条流水线


我们发现这条channel是通过一个 channelFactory new出来的, channelFactory 的接口很简单


 
           
  1. public interface ChannelFactory<T extends Channelextends io.netty.bootstrap.ChannelFactory<T> {

  2.  /**

  3.  * Creates a new channel.

  4.  */

  5.  @Override

  6.  T newChannel();

  7. }

就一个方法,我们查看channelFactory被赋值的地方

AbstractBootstrap.java



 
           
  1. public B channelFactory(ChannelFactory<? extends C> channelFactory) {

  2.  if (channelFactory == null) {

  3.  throw new NullPointerException("channelFactory");

  4.  }

  5.  if (this.channelFactory != null) {

  6.  throw new IllegalStateException("channelFactory set already");

  7.  }

  8.  this.channelFactory = channelFactory;

  9.  return (B) this;

  10. }


在这里被赋值,我们层层回溯,查看该函数被调用的地方,发现最终是在这个函数中,ChannelFactory被new出


 
           
  1. public B channel(Class<? extends C> channelClass) {

  2.  if (channelClass == null) {

  3.  throw new NullPointerException("channelClass");

  4.  }

  5.  return channelFactory(new ReflectiveChannelFactory<C>(channelClass));

  6. }


这里,我们的demo程序调用 channel(channelClass)方法的时候,将 channelClass作为 ReflectiveChannelFactory的构造函数创建出一个 ReflectiveChannelFactory

demo端的代码如下:

 
           
  1. .channel(NioServerSocketChannel.class);

然后回到本节最开始

 
           
  1. channelFactory.newChannel();


我们就可以推断出,最终是调用到 ReflectiveChannelFactory.newChannel() 方法,跟进

 
           
  1. public class ReflectiveChannelFactory<T extends Channelimplements ChannelFactory<T> {

  2.  private final Class<? extends T> clazz;

  3.  public ReflectiveChannelFactory(Class<? extends T> clazz) {

  4.  if (clazz == null) {

  5.  throw new NullPointerException("clazz");

  6.  }

  7.  this.clazz = clazz;

  8.  }

  9.  @Override

  10.  public T newChannel() {

  11.  try {

  12.  return clazz.newInstance();

  13.  } catch (Throwable t) {

  14.  throw new ChannelException("Unable to create Channel from class " + clazz, t);

  15.  }

  16.  }

  17. }


看到 clazz.newInstance();,我们明白了,原来是通过反射的方式来创建一个对象,而这个class就是我们在 ServerBootstrap中传入的 NioServerSocketChannel.class


结果,绕了一圈,最终创建channel相当于调用默认构造函数new出一个 NioServerSocketChannel对象


这里提一下,读源码细节,有两种读的方式,一种是回溯,比如用到某个对象的时候可以逐层追溯,一定会找到该对象的最开始被创建的代码区块,还有一种方式就是自顶向下,逐层分析,一般用在分析某个具体的方法,庖丁解牛,最后拼接出完整的流程。


接下来我们就可以将重心放到 NioServerSocketChannel的默认构造函数


 
           
  1. private static final SelectorProvider DEFAULT_SELECTOR_PROVIDER = SelectorProvider.provider();

  2. public NioServerSocketChannel() {

  3.  this(newSocket(DEFAULT_SELECTOR_PROVIDER));

  4. }

 
           
  1. private static ServerSocketChannel newSocket(SelectorProvider provider) {

  2.  //...

  3.  return provider.openServerSocketChannel();

  4. }

通过 SelectorProvider.openServerSocketChannel()创建一条server端channel,然后进入到以下方法

 
           
  1. public NioServerSocketChannel(ServerSocketChannel channel) {

  2.  super(null, channel, SelectionKey.OP_ACCEPT);

  3.  config = new NioServerSocketChannelConfig(this, javaChannel().socket());

  4. }


这里第一行代码就跑到父类里面去了,第二行,new出来一个 NioServerSocketChannelConfig,其顶层接口为 ChannelConfig,netty官方的描述如下

A set of configuration properties of a Channel.

基本可以判定, ChannelConfig 也是netty里面的一大核心模块,初次看源码,看到这里,我们大可不必深挖这个对象,而是在用到的时候再回来深究,只要记住,这个对象在创建 NioServerSocketChannel对象的时候被创建即可


我们继续追踪到 NioServerSocketChannel 的父类

AbstractNioMessageChannel.java

 
           
  1. protected AbstractNioMessageChannel(Channel parent, SelectableChannel ch, int readInterestOp) {

  2.  super(parent, ch, readInterestOp);

  3. }

继续往上追

AbstractNioChannel.java

 
           
  1. protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {

  2.  super(parent);

  3.  this.ch = ch;

  4.  this.readInterestOp = readInterestOp;

  5.  //...

  6.  ch.configureBlocking(false);

  7.  //...

  8. }


这里,简单地将前面 provider.openServerSocketChannel(); 创建出来的 ServerSocketChannel 保存到成员变量,然后调用 ch.configureBlocking(false);设置该channel为非阻塞模式,标准的jdk nio编程的玩法


这里的 readInterestOp 即前面层层传入的 SelectionKey.OP_ACCEPT,接下来重点分析 super(parent);(这里的parent其实是null,由前面写死传入)


AbstractChannel.java


 
           
  1. protected AbstractChannel(Channel parent) {

  2.  this.parent = parent;

  3.  id = newId();

  4.  unsafe = newUnsafe();

  5.  pipeline = newChannelPipeline();

  6. }

到了这里,又new出来三大组件,赋值到成员变量,分别为

 
           
  1. id = newId();

  2. protected ChannelId newId() {

  3.  return DefaultChannelId.newInstance();

  4. }

id是netty中每条channel的唯一标识,这里不细展开,接着

 
           
  1. unsafe = newUnsafe();

  2. protected abstract AbstractUnsafe newUnsafe();

查看Unsafe的定义

Unsafe operations that should never be called from user-code. These methods are only provided to implement the actual transport, and must be invoked from an I/O thread


成功捕捉netty的又一大组件,我们可以先不用管TA是干嘛的,只需要知道这里的 newUnsafe方法最终属于类 NioServerSocketChannel


最后

 
           
  1. pipeline = newChannelPipeline();

  2. protected DefaultChannelPipeline newChannelPipeline() {

  3.  return new DefaultChannelPipeline(this);

  4. }

  5. protected DefaultChannelPipeline(Channel channel) {

  6.  this.channel = ObjectUtil.checkNotNull(channel, "channel");

  7.  succeededFuture = new SucceededChannelFuture(channel, null);

  8.  voidPromise = new VoidChannelPromise(channel, true);

  9.  tail = new TailContext(this);

  10.  head = new HeadContext(this);

  11.  head.next = tail;

  12.  tail.prev = head;

  13. }


初次看这段代码,可能并不知道 DefaultChannelPipeline 是干嘛用的,我们仍然使用上面的方式,查看顶层接口 ChannelPipeline的定义

A list of ChannelHandlers which handles or intercepts inbound events and outbound operations of a Channel


从该类的文档中可以看出,该接口基本上又是netty的一大核心模块

到了这里,我们总算把一个服务端channel创建完毕了,将这些细节串起来的时候,我们顺带提取出netty的几大基本组件,先总结如下

  • Channel

  • ChannelConfig

  • ChannelId

  • Unsafe

  • Pipeline

  • ChannelHander

初次看代码的时候,我们的目标是跟到服务器启动的那一行代码,我们先把以上这几个组件记下来,等代码跟完,我们就可以自顶向下,逐层分析,我会放到后面源码系列中去深入到每个组件


总结一下,用户调用方法 Bootstrap.bind(port) 第一步就是通过反射的方式new一个 NioServerSocketChannel对象,并且在new的过程中创建了一系列的核心组件,仅此而已,并无他,真正的启动我们还需要继续跟

2、init这个channel

到了这里,你最好跳到文章最开始的地方回忆一下,第一步newChannel完毕,这里就对这个channel做init,init方法具体干啥,我们深入

 
           
  1. @Override

  2. void init(Channel channel) throws Exception {

  3.  final Map<ChannelOption<?>, Object> options = options0();

  4.  synchronized (options) {

  5.  channel.config().setOptions(options);

  6.  }

  7.  final Map<AttributeKey<?>, Object> attrs = attrs0();

  8.  synchronized (attrs) {

  9.  for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {

  10.  @SuppressWarnings("unchecked")

  11.  AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();

  12.  channel.attr(key).set(e.getValue());

  13.  }

  14.  }

  15.  ChannelPipeline p = channel.pipeline();

  16.  final EventLoopGroup currentChildGroup = childGroup;

  17.  final ChannelHandler currentChildHandler = childHandler;

  18.  final Entry<ChannelOption<?>, Object>[] currentChildOptions;

  19.  final Entry<AttributeKey<?>, Object>[] currentChildAttrs;

  20.  synchronized (childOptions) {

  21.  currentChildOptions = childOptions.entrySet().toArray(newOptionArray(childOptions.size()));

  22.  }

  23.  synchronized (childAttrs) {

  24.  currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(childAttrs.size()));

  25.  }

  26.  p.addLast(new ChannelInitializer<Channel>() {

  27.  @Override

  28.  public void initChannel(Channel ch) throws Exception {

  29.  final ChannelPipeline pipeline = ch.pipeline();

  30.  ChannelHandler handler = config.handler();

  31.  if (handler != null) {

  32.  pipeline.addLast(handler);

  33.  }

  34.  ch.eventLoop().execute(new Runnable() {

  35.  @Override

  36.  public void run() {

  37.  pipeline.addLast(new ServerBootstrapAcceptor(

  38.  currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));

  39.  }

  40.  });

  41.  }

  42.  });

  43. }

初次看到这个方法,可能会觉得,哇塞,老长了,这可这么看?还记得我们前面所说的吗,庖丁解牛,逐步拆解,最后归一,下面是我的拆解步骤


1.设置option和attr

 
           
  1. final Map<ChannelOption<?>, Object> options = options0();

  2.  synchronized (options) {

  3.  channel.config().setOptions(options);

  4.  }

  5.  final Map<AttributeKey<?>, Object> attrs = attrs0();

  6.  synchronized (attrs) {

  7.  for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {

  8.  @SuppressWarnings("unchecked")

  9.  AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();

  10.  channel.attr(key).set(e.getValue());

  11.  }

  12.  }


通过这里我们可以看到,这里先调用 options0()以及 attrs0(),然后将得到的options和attrs注入到channelConfig或者channel中,关于option和attr是干嘛用的,其实你现在不用了解得那么深入,只需要查看最顶层接口 ChannelOption以及查看一下channel的具体继承关系,就可以了解,我把这两个也放到后面的源码分析系列再讲


2.设置新接入channel的option和attr

 
           
  1. final EventLoopGroup currentChildGroup = childGroup;

  2. final ChannelHandler currentChildHandler = childHandler;

  3. final Entry<ChannelOption<?>, Object>[] currentChildOptions;

  4. final Entry<AttributeKey<?>, Object>[] currentChildAttrs;

  5. synchronized (childOptions) {

  6.  currentChildOptions = childOptions.entrySet().toArray(newOptionArray(childOptions.size()));

  7. }

  8. synchronized (childAttrs) {

  9.  currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(childAttrs.size()));

  10. }

这里,和上面类似,只不过不是设置当前channel的这两个属性,而是对应到新进来连接对应的channel,由于我们这篇文章只关心到server如何启动,接入连接放到下一篇文章中详细剖析


3.加入新连接处理器

 
           
  1. p.addLast(new ChannelInitializer<Channel>() {

  2.  @Override

  3.  public void initChannel(Channel ch) throws Exception {

  4.  final ChannelPipeline pipeline = ch.pipeline();

  5.  ChannelHandler handler = config.handler();

  6.  if (handler != null) {

  7.  pipeline.addLast(handler);

  8.  }

  9.  ch.eventLoop().execute(new Runnable() {

  10.  @Override

  11.  public void run() {

  12.  pipeline.addLast(new ServerBootstrapAcceptor(

  13.  currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));

  14.  }

  15.  });

  16.  }

  17.  });

到了最后一步, p.addLast()向serverChannel的流水线处理器中加入了一个 ServerBootstrapAcceptor,从名字上就可以看出来,这是一个接入器,专门接受新请求,把新的请求扔给某个事件循环器,我们先不做过多分析

来,我们总结一下,我们发现其实init也没有启动服务,只是初始化了一些基本的配置和属性,以及在pipeline上加入了一个接入器,用来专门接受新连接,我们还得继续往下跟

3、将这个channel register到某个对象

这一步,我们是分析如下方法

 
           
  1. ChannelFuture regFuture = config().group().register(channel);

调用到 NioEventLoop 中的 register

 
           
  1. @Override

  2. public ChannelFuture register(Channel channel) {

  3.  return register(new DefaultChannelPromise(channel, this));

  4. }

 
           
  1. @Override

  2. public ChannelFuture register(final ChannelPromise promise) {

  3.  ObjectUtil.checkNotNull(promise, "promise");

  4.  promise.channel().unsafe().register(this, promise);

  5.  return promise;

  6. }


好了,到了这一步,还记得这里的 unsafe()返回的应该是什么对象吗?不记得的话可以看下前面关于unsafe的描述,或者最快的方式就是debug到这边,跟到register方法里面,看看是哪种类型的unsafe

我们跟进去之后发现是

AbstractUnsafe.java

 
           
  1. @Override

  2. public final void register(EventLoop eventLoop, final ChannelPromise promise) {

  3.  // ...

  4.  AbstractChannel.this.eventLoop = eventLoop;

  5.  // ...

  6.  register0(promise);

  7. }


这里我们依然只需要focus重点,先将EventLoop事件循环器绑定到该NioServerSocketChannel上,然后调用 register0()

 
           
  1. private void register0(ChannelPromise promise) {

  2.  try {

  3.  boolean firstRegistration = neverRegistered;

  4.  doRegister();

  5.  neverRegistered = false;

  6.  registered = true;

  7.  pipeline.invokeHandlerAddedIfNeeded();

  8.  safeSetSuccess(promise);

  9.  pipeline.fireChannelRegistered();

  10.  if (isActive()) {

  11.  if (firstRegistration) {

  12.  pipeline.fireChannelActive();

  13.  } else if (config().isAutoRead()) {

  14.  beginRead();

  15.  }

  16.  }

  17.  } catch (Throwable t) {

  18.  closeForcibly();

  19.  closeFuture.setClosed();

  20.  safeSetFailure(promise, t);

  21.  }

  22. }


这一段其实也很清晰,先调用 doRegister();,具体干啥待会再讲,然后调用 invokeHandlerAddedIfNeeded(), 于是乎,控制台第一行打印出来的就是

 
           
  1. handlerAdded


关于最终是如何调用到的,我们后面详细剖析pipeline的时候再讲

然后调用 pipeline.fireChannelRegistered(); 调用之后,控制台的显示为

 
           
  1. handlerAdded

  2. channelRegistered

继续往下跟

 
           
  1. if (isActive()) {

  2.  if (firstRegistration) {

  3.  pipeline.fireChannelActive();

  4.  } else if (config().isAutoRead()) {

  5.  beginRead();

  6.  }

  7. }

读到这,你可能会想当然地以为,控制台最后一行

 
           
  1. pipeline.fireChannelActive();

由这行代码输出,我们不妨先看一下 isActive() 方法

 
           
  1. @Override

  2. public boolean isActive() {

  3.  return javaChannel().socket().isBound();

  4. }

最终调用到jdk中

ServerSocket.java

 
           
  1.  /**

  2.  * Returns the binding state of the ServerSocket.

  3.  *

  4.  * @return true if the ServerSocket succesfuly bound to an address

  5.  * @since 1.4

  6.  */

  7.  public boolean isBound() {

  8.  // Before 1.3 ServerSockets were always bound during creation

  9.  return bound || oldImpl;

  10.  }


这里 isBound()返回false,但是从目前我们跟下来的流程看,我们并没有将一个ServerSocket绑定到一个address,所以 isActive() 返回false,我们没有成功进入到 pipeline.fireChannelActive();方法,那么最后一行到底是谁输出的呢,我们有点抓狂,其实,只要熟练运用IDE,要定位函数调用栈,无比简单


下面是我用intellij定位函数调用的具体方法



Intellij函数调用定位

我们先在最终输出文字的这一行代码处打一个断点,然后debug,运行到这一行,intellij自动给我们拉起了调用栈,我们唯一要做的事,就是移动方向键,就能看到函数的完整的调用链


如果你看到方法的最近的发起端是一个线程Runnable的run方法,那么就在提交Runnable对象方法的地方打一个断点,去掉其他断点,重新debug,比如我们首次debug发现调用栈中的最近的一个Runnable如下

 
           
  1. if (!wasActive && isActive()) {

  2.  invokeLater(new Runnable() {

  3.  @Override

  4.  public void run() {

  5.  pipeline.fireChannelActive();

  6.  }

  7.  });

  8. }


我们停在了这一行 pipeline.fireChannelActive();, 我们想看最初始的调用,就得跳出来,断点打到 if (!wasActive && isActive()),因为netty里面很多任务执行都是异步线程即reactor线程调用的,如果我们要查看最先发起的方法调用,我们必须得查看Runnable被提交的地方,逐次递归下去,就能找到那行"消失的代码"

最终,通过这种方式,终于找到了 pipeline.fireChannelActive(); 的发起调用的代码,不巧,刚好就是下面的 doBind0()方法

doBind0()

 
           
  1. private static void doBind0(

  2.  final ChannelFuture regFuture, final Channel channel,

  3.  final SocketAddress localAddress, final ChannelPromise promise) {

  4.  channel.eventLoop().execute(new Runnable() {

  5.  @Override

  6.  public void run() {

  7.  if (regFuture.isSuccess()) {

  8.  channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);

  9.  } else {

  10.  promise.setFailure(regFuture.cause());

  11.  }

  12.  }

  13.  });

  14.  }

我们发现,在调用 doBind0(...)方法的时候,是通过包装一个Runnable进行异步化的。

好,接下来我们进入 channel.bind()方法

AbstractChannel.java

 
           
  1. @Override

  2. public ChannelFuture bind(SocketAddress localAddress) {

  3.  return pipeline.bind(localAddress);

  4. }

发现是调用pipeline的bind方法

 
           
  1. @Override

  2. public final ChannelFuture bind(SocketAddress localAddress) {

  3.  return tail.bind(localAddress);

  4. }


相信你对tail是什么不是很了解,可以翻到最开始,tail在创建pipeline的时候出现过,关于pipeline和tail对应的类,我后面源码系列会详细解说,这里,你要想知道接下来代码的走向,唯一一个比较好的方式就是debug 单步进入,篇幅原因,我就不详细展开


最后,我们来到了如下区域

HeadContext.java

 
           
  1. @Override

  2. public void bind(

  3.  ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise)

  4.  throws Exception {

  5.  unsafe.bind(localAddress, promise);

  6. }

这里的unsafe就是前面提到的 AbstractUnsafe, 准确点,应该是 NioMessageUnsafe


我们进入到它的bind方法

 
           
  1. @Override

  2. public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {

  3.  // ...

  4.  boolean wasActive = isActive();

  5.  // ...

  6.  doBind(localAddress);

  7.  if (!wasActive && isActive()) {

  8.  invokeLater(new Runnable() {

  9.  @Override

  10.  public void run() {

  11.  pipeline.fireChannelActive();

  12.  }

  13.  });

  14.  }

  15.  safeSetSuccess(promise);

  16. }


显然按照正常流程,我们前面已经分析到 isActive(); 方法返回false,进入到 doBind()之后,如果channel被激活了,就发起 pipeline.fireChannelActive();调用,最终调用到用户方法,在控制台打印出了最后一行,所以到了这里,你应该清楚为什么最终会在控制台按顺序打印出那三行字了吧


doBind()方法也很简单

 
           
  1. protected void doBind(SocketAddress localAddress) throws Exception {

  2.  if (PlatformDependent.javaVersion() >= 7) {

  3.  //noinspection Since15

  4.  javaChannel().bind(localAddress, config.getBacklog());

  5.  } else {

  6.  javaChannel().socket().bind(localAddress, config.getBacklog());

  7.  }

  8. }

最终调到了jdk里面的bind方法,这行代码过后,正常情况下,就真正进行了端口的绑定。


另外,通过自顶向下的方式分析,在调用 pipeline.fireChannelActive();方法的时候,会调用到如下方法

HeadContext.java

 
           
  1. public void channelActive(ChannelHandlerContext ctx) throws Exception {

  2.  ctx.fireChannelActive();

  3.  readIfIsAutoRead();

  4. }

进入 readIfIsAutoRead

 
           
  1. private void readIfIsAutoRead() {

  2.  if (channel.config().isAutoRead()) {

  3.  channel.read();

  4.  }

  5. }

分析 isAutoRead方法

 
           
  1. private volatile int autoRead = 1;

  2. public boolean isAutoRead() {

  3.  return autoRead == 1;

  4. }

由此可见, isAutoRead方法默认返回true,于是进入到以下方法

 
           
  1. public Channel read() {

  2.  pipeline.read();

  3.  return this;

  4. }

最终调用到

AbstractNioUnsafe.java

 
           
  1. protected void doBeginRead() throws Exception {

  2.  final SelectionKey selectionKey = this.selectionKey;

  3.  if (!selectionKey.isValid()) {

  4.  return;

  5.  }

  6.  readPending = true;

  7.  final int interestOps = selectionKey.interestOps();

  8.  if ((interestOps & readInterestOp) == 0) {

  9.  selectionKey.interestOps(interestOps | readInterestOp);

  10.  }

  11. }

这里的 this.selectionKey就是我们在前面register步骤返回的对象,前面我们在register的时候,注册测ops是0


回忆一下注册

AbstractNioChannel

 
           
  1. selectionKey = javaChannel().register(eventLoop().selector, 0this)

这里相当于把注册过的ops取出来,通过了if条件,然后调用

 
           
  1. selectionKey.interestOps(interestOps | readInterestOp);

而这里的 readInterestOp 就是前面newChannel的时候传入 SelectionKey.OP_ACCEPT,又是标准的jdk nio的玩法,到此,你需要了解的细节基本已经差不多了,就这样结束吧!




Summary



最后,我们来做下总结,netty启动一个服务所经过的流程 

1.设置启动类参数,最重要的就是设置channel 

2.创建server对应的channel,创建各大组件,包括ChannelConfig,ChannelId,ChannelPipeline,ChannelHandler,Unsafe等 

3.初始化server对应的channel,设置一些attr,option,以及设置子channel的attr,option,给server的channel添加新channel接入器,并出发addHandler,register等事件 

4.调用到jdk底层做端口绑定,并触发active事件,active触发的时候,真正做服务端口绑定

另外,文章中阅读源码的思路详细或许也可以给你带来一些帮助。




最后推荐一门《Java读源码之Netty深入剖析》教程,带你从一个Socket例子入手,一步步深入探究Netty各个模块的源码,深入剖析Netty的工作流程和源码设计,让你不但“真懂”也要“会用“。



戳下图,永久订阅





点击下列关键词,了解技术热词


美团mpvue   |   程序员面试  |  区块链


大前端  | Python3入门  |  微服务


     

长按识别二维码关注我们


   慕课网,程序员的梦工厂  




 
慕课网 更多文章 第一门Android实战课火爆上线!丨【新课速递】 学习之道 | 学习不是枯燥的劳作,是精彩的冒险(评论区留言送书啦!) 抽奖 | 双11实战大狂欢,“肾7”、iPad都给你准备好了! 新课速递 | Vue.js高仿饿了么外卖App 新课速递 | Android专项及自动化测试 Unittest测试 Fiddler抓包
猜您喜欢 聊聊代码规范 SQL优化案例精彩连载(一) 腾讯TMQ在线沙龙|性能测试(活动即将开始,报名马上截止) Swift教程第三部分:元组,协议,委托和表格视图 Python那么火,到底可以用来做什么