Ref: https://javadoop.com/post/netty-part-1

2019-04-08 更新:为了增强该文章的可读性,我重新梳理了一遍,更新了少量内容,并且将原来长长的文章拆分为多篇文章,这样有利于读者阅读,也方便读者进行互动。
我将原来的文章拆分为了以下九篇文章,基本上每篇都很短,很快就可以看完一篇了。希望大家不要再将该文章放到收藏夹了,从现在开始阅读吧。
当前 => Netty 源码解析(一): 开始
Netty 源码解析(二): Netty 的 Channel
Netty 源码解析(三): Netty 的 Future 和 Promise
Netty 源码解析(四): Netty 的 ChannelPipeline
Netty 源码解析(五): Netty 的线程池分析
Netty 源码解析(六): Channel 的 register 操作
Netty 源码解析(七): NioEventLoop 工作流程
Netty 源码解析(八): 回到 Channel 的 register 操作
Netty 源码解析(九): connect 过程和 bind 过程分析


本文又是一篇源码分析文章,其实除了 Doug Lea 的并发包源码,我是真不太爱写源码分析。因为要花非常多的时间,而且很多地方需要反复组织语言。
本文将介绍 Netty,Java 平台上使用最广泛的 NIO 包,它是对 JDK 中的 NIO 实现的一层封装,让我们能更方便地开发 NIO 程序。其实,Netty 不仅仅是 NIO 吧,但是,基本上大家都冲着 NIO 来的。
个人感觉国内对于 Netty 的吹嘘是有点过了,主要是很多人靠它吃饭,要么是搞培训的,要么是出书的,恨不得把 Netty 吹上天去,这种现象也是挺不好的,反而使得初学者觉得 Netty 是什么高深的技术一样。
Netty 的源码不是很简单,因为它比较多,而且各个类之间的关系错综复杂,很多人说它的源码很好,这点我觉得一般,真要说好代码,还得 Doug Lea 的并发源码比较漂亮,一行行都是精华,不过它们是不同类型的,也没什么好对比的。Netty 源码好就好在它的接口使用比较灵活,往往接口好用的框架,源码都不会太简单。

本文将立足于源码分析,所以读者需要先掌握 NIO 的基础知识,至少我之前写的 《Java NIO:Buffer、Channel 和 Selector》 中介绍的基础知识要清楚,如果读者已经对 Netty 有些了解,或者使用过,那就更好了。

  • 本文只介绍 TCP 相关的内容,Netty 对于其他协议的支持,不在本文的讨论范围内。
  • 和并发包的源码分析不一样,我不可能一行一行源码说,所以有些异常分支是会直接略过,除非我觉得需要介绍。
  • Netty 源码一直在更新,各版本之间有些差异,我是按照 2018-09-06 的最新版本 4.1.25.Final 来进行介绍的。

建议初学者在看完本文以后,可以去翻翻《Netty In Action》,网上也可以找到中文文字版的。

准备

学习源码,一开始肯定是准备环境。
我喜欢用 maven,也喜欢 Spring Boot,所以我一般先到 https://start.spring.io/ 准备一个最简单的脚手架。
10 秒搞定脚手架,然后就是导入到 Intellij 中,如果用新版本的 Spring Boot,可能还需要等待下载依赖,期间打开 https://mvnrepository.com/ 搜索马上要用到的 maven 依赖。
Netty 分为好些模块,有 netty-handlernetty-buffernetty-transportnetty-common 等等,也有一个 netty-all,它包含了所有的模块。
既然我们是源码分析,那么自然是用一个最简单的。netty-all 不是最好的选择,netty-example 才是:

  1. <dependency>
  2. <groupId>io.netty</groupId>
  3. <artifactId>netty-example</artifactId>
  4. <version>4.1.25.Final</version>
  5. </dependency>

它不仅可以解决我们的依赖,而且 example 里面的示例非常适合我们学习使用。

Echo 例子

Netty 作为 NIO 的库,自然既可以作为服务端接受请求,也可以作为客户端发起请求。使用 Netty 开发客户端或服务端都是非常简单的,Netty 做了很好的封装,我们通常只要开发一个或多个 handler 用来处理我们的自定义逻辑就可以了。
下面,我们来看一个经常会见到的例子,它叫 Echo,也就是回声,客户端传过去什么值,服务端原样返回什么值。
打开 netty-example 的源码,把 echo 包下面的代码复制出来玩一玩。
image.png
左边是服务端代码,右边是客户端代码。
上面的代码基本就是模板代码,每次使用都是这一个套路,唯一需要我们开发的部分是 handler(…) 和 childHandler(…) 方法中指定的各个 handler,如 EchoServerHandlerEchoClientHandler,当然 Netty 源码也给我们提供了很多的 handler,比如上面的 LoggingHandler,它就是 Netty 源码中为我们提供的,需要的时候直接拿过来用就好了。
我们先来看一下上述代码中涉及到的一些内容:

  • ServerBootstrap 类用于创建服务端实例,Bootstrap 用于创建客户端实例。
  • 两个 EventLoopGroup:bossGroup 和 workerGroup,它们涉及的是 Netty 的线程模型,可以看到服务端有两个 group,而客户端只有一个,它们就是 Netty 中的线程池。
  • Netty 中的 Channel,没有直接使用 Java 原生的 ServerSocketChannel 和 SocketChannel,而是包装了 NioServerSocketChannel 和 NioSocketChannel 与之对应。当然,也有对其他协议的支持,如支持 UDP 协议的 NioDatagramChannel,本文只关心 TCP 相关的。
  • 左边 handler(…) 方法指定了一个 handler(LoggingHandler),这个 handler 是给服务端收到新的请求的时候处理用的。右边 handler(…) 方法指定了客户端处理请求过程中需要使用的 handlers。如果你想在 EchoServer 中也指定多个 handler,也可以像右边的 EchoClient 一样使用 ChannelInitializer
  • 左边 childHandler(…) 指定了 childHandler,这边的 handlers 是给新创建的连接用的,我们知道服务端 ServerSocketChannel 在 accept 一个连接以后,需要创建 SocketChannel 的实例,childHandler(…) 中设置的 handler 就是用于处理新创建的 SocketChannel 的,而不是用来处理 ServerSocketChannel 实例的。
  • pipeline:handler 可以指定多个(需要上面的 ChannelInitializer 类辅助),它们会组成了一个 pipeline,它们其实就类似拦截器的概念,现在只要记住一点,每个 NioSocketChannel 或 NioServerSocketChannel 实例内部都会有一个 pipeline 实例。pipeline 中还涉及到 handler 的执行顺序。
  • ChannelFuture:这个涉及到 Netty 中的异步编程,和 JDK 中的 Future 接口类似。

对于不了解 Netty 的读者,也不要有什么压力,我会一一介绍它们,本文主要面向新手,我觉得比较难理解或比较重要的部分,会花比较大的篇幅来介绍清楚。
上面的源码中没有展示消息发送和消息接收的处理,此部分我会在介绍完上面的这些内容以后再进行介绍。


当前 => Netty 源码解析(二): Netty 的 Channel

Netty 中的 Channel

这节我们来看看 NioSocketChannel 是怎么和 JDK 底层的 SocketChannel 联系在一起的,它们是一对一的关系。NioServerSocketChannel 和 ServerSocketChannel 同理,也是一对一的关系。
image.png
在 Bootstrap(客户端) 和 ServerBootstrap(服务端) 的启动过程中都会调用 channel(…) 方法:
image.png
下面,我们来看 channel(…) 方法的源码:

  1. // AbstractBootstrap
  2. public B channel(Class<? extends C> channelClass) {
  3. if (channelClass == null) {
  4. throw new NullPointerException("channelClass");
  5. }
  6. return channelFactory(new ReflectiveChannelFactory<C>(channelClass));
  7. }

我们可以看到,这个方法只是设置了 channelFactory 为 ReflectiveChannelFactory 的一个实例,然后我们看下这里的 ReflectiveChannelFactory 到底是什么:
image.png
newChannel() 方法是 ChannelFactory 接口中的唯一方法,工厂模式大家都很熟悉。我们可以看到,ReflectiveChannelFactory#newChannel() 方法中使用了反射调用 Channel 的无参构造方法来创建 Channel,我们只要知道,ChannelFactory 的 newChannel() 方法什么时候会被调用就可以了。

  • 对于 NioSocketChannel,由于它充当客户端的功能,它的创建时机在 connect(…) 的时候;
  • 对于 NioServerSocketChannel 来说,它充当服务端功能,它的创建时机在绑定端口 bind(…) 的时候。

接下来,我们来简单追踪下充当客户端的 Bootstrap 中 NioSocketChannel 的创建过程,看看 NioSocketChannel 是怎么和 JDK 中的 SocketChannel 关联在一起的:

// Bootstrap
public ChannelFuture connect(String inetHost, int inetPort) {
    return connect(InetSocketAddress.createUnresolved(inetHost, inetPort));
}

然后再往里看,到这个方法:

public ChannelFuture connect(SocketAddress remoteAddress) {
    if (remoteAddress == null) {
        throw new NullPointerException("remoteAddress");
    // validate 只是校验一下各个参数是不是正确设置了
    validate();
    return doResolveAndConnect(remoteAddress, config.localAddress());
}

继续:

// 再往里就到这里了
private ChannelFuture doResolveAndConnect(final SocketAddress remoteAddress, final SocketAddress localAddress) {
    // 我们要说的部分在这里
    final ChannelFuture regFuture = initAndRegister();
    final Channel channel = regFuture.channel();
    ......
}

然后,我们看 initAndRegister() 方法:

final ChannelFuture initAndRegister() {
    Channel channel = null;
    try {
        // 前面我们说过,这里会进行 Channel 的实例化
        channel = channelFactory.newChannel();
        init(channel);
    } catch (Throwable t) {
        ...
    }
    ...
    return regFuture;
}

我们找到了 channel = channelFactory.newChannel() 这行代码,根据前面说的,这里会调用相应 Channel 的无参构造方法。
然后我们就可以去看 NioSocketChannel 的构造方法了:

public NioSocketChannel() {
    // SelectorProvider 实例用于创建 JDK 的 SocketChannel 实例
    this(DEFAULT_SELECTOR_PROVIDER);
}

public NioSocketChannel(SelectorProvider provider) {
    // 看这里,newSocket(provider) 方法会创建 JDK 的 SocketChannel
    this(newSocket(provider));
}

我们可以看到,在调用 newSocket(provider) 的时候,会创建 JDK NIO 的一个 SocketChannel 实例:

private static SocketChannel newSocket(SelectorProvider provider) {
    try {
        // 创建 SocketChannel 实例
        return provider.openSocketChannel();
    } catch (IOException e) {
        throw new ChannelException("Failed to open a socket.", e);
    }

NioServerSocketChannel 同理,也非常简单,从 ServerBootstrap#bind(…) 方法一路点进去就清楚了。
所以我们知道了,NioSocketChannel 在实例化过程中,会先实例化 JDK 底层的 SocketChannel,NioServerSocketChannel 也一样,会先实例化 ServerSocketChannel 实例:
image.png
说到这里,我们顺便再继续往里看一下 NioSocketChannel 的构造方法:

public NioSocketChannel(SelectorProvider provider) {
    this(newSocket(provider));
}

刚才我们看到这里,newSocket(provider) 创建了底层的 SocketChannel 实例,我们继续往下看构造方法:

public NioSocketChannel(Channel parent, SocketChannel socket) {
    super(parent, socket);
    config = new NioSocketChannelConfig(this, socket.socket());
}

上面有两行代码,第二行代码很简单,实例化了内部的 NioSocketChannelConfig 实例,它用于保存 channel 的配置信息,这里没有我们现在需要关心的内容,直接跳过。
第一行调用父类构造器,除了设置属性外,还设置了 SocketChannel 的非阻塞模式:

protected AbstractNioByteChannel(Channel parent, SelectableChannel ch) {
    // 毫无疑问,客户端关心的是 OP_READ 事件,等待读取服务端返回数据
    super(parent, ch, SelectionKey.OP_READ);
}

// 然后是到这里
protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
    super(parent);
    this.ch = ch;
    // 我们看到这里只是保存了 SelectionKey.OP_READ 这个信息,在后面的时候会用到
    this.readInterestOp = readInterestOp;
    try {
        // ******设置 channel 的非阻塞模式******
        ch.configureBlocking(false);
    } catch (IOException e) {
        ......
    }
}

NioServerSocketChannel 的构造方法类似,也设置了非阻塞,然后设置服务端关心的 SelectionKey.OP_ACCEPT 事件:

public NioServerSocketChannel(ServerSocketChannel channel) {
    // 对于服务端来说,关心的是 SelectionKey.OP_ACCEPT 事件,等待客户端连接
    super(null, channel, SelectionKey.OP_ACCEPT);
    config = new NioServerSocketChannelConfig(this, javaChannel().socket());
}

这节关于 Channel 的内容我们先介绍这么多,主要就是实例化了 JDK 层的 SocketChannel 或 ServerSocketChannel,然后设置了非阻塞模式,我们后面再继续深入下去。