1. 背景

1.1 基础知识

1.1.1 BIO 与 IO 多路复用

  • 同步阻塞 BIO(图例为多线程模式对应的 Acceptor 模型)

Netty - 图2Netty - 图3

  • IO 多路复用

    • select、poll、epoll 详见
      • 其中 select 与 poll 是无差别轮询,select 有连接数限制 1024,而 poll 支持无限连接;
      • epoll 为最小轮询,是 select 与 poll 的加强版,支持无限连接;

        1.1.2 TCP/IP 协议

        Netty - 图4Netty - 图5

        1.1.3 Socket

  • Socket 与 TCP/IP 协议模型的关系

Netty - 图6

  • Socket 通信原理(左服务器端,右客户端)

Netty - 图7 Netty - 图8

2. 发展过程

2.1 传统 Apache 服务器

  • 传统服务器根据编码解码协议不同可分为不同类型,如: 协议是 redis ,则为 redis 服务器
  • 传统的 HTTP 服务器原理 :
    • 创建一个 ServerSocket,监听并绑定一个端口;
    • 一系列客户端来请求这个端口;
    • 服务器使用 Accept,获得一个来自客户端的Socket连接对象;
    • 启动一个新线程处理连接:
      • 读Socket,得到字节流;
      • 解码协议,得到Http请求对象 ;
      • 处理Http请求,得到一个结果,封装成一个HttpResponse对象 ;
      • 编码协议,将结果序列化字节流 写Socket,将字节流发给客户端 ;
    • 继续循环步骤 3
  • 优点:具有很高的响应速度,控制简单;
  • 缺点:传统的多线程服务器为 BlockingIO 模式,占用操作系统的调度资源,且连接数较多会造成资源浪费;
  • 阻塞式 Socket

    Netty - 图9

    • Accept 是阻塞的,只有新连接来了,Accept 才会返回,主线程才能继续;
    • Read 是阻塞的,只有请求消息来了,Read 才能返回,子线程才能继续处理;
    • Write 是阻塞的,只有客户端把消息收了,Write 才能返回,子线程才能继续读取下一个请求;

      2.2 分布式服务与 Java NIO

      2.2.1 概要

  • 产生背景

    • 高并发环境下,线程数量可能会创建太多,操作系统的任务调度压力大,系统负载也会比较高;
  • 特点:同步非阻塞,使用一个线程把Accept、读写操作、请求处理的逻辑全部实现,伪码如下:

    1. while true {
    2. events = takeEvents(fds) // 获取事件,如果没有事件,线程就休眠
    3. for event in events {
    4. if event.isAcceptable {
    5. doAccept() // 新链接来了
    6. } elif event.isReadable {
    7. request = doRead() // 读消息
    8. if request.isComplete() {
    9. doProcess()
    10. }
    11. } elif event.isWriteable {
    12. doWrite() // 写消息
    13. }
    14. }
    15. }
  • 优点

    • 充分利用多核
    • 当 IO 阻塞系统,但是 CPU 空闲时,可利用多线程使用 CPU 资源;
  • 缺点

    • 类库和 API 较为复杂,如 Buffer 的使用、Selector 编写等;
    • 而且若对某个事件注册后,业务代码将过于耦合;
    • 对多线程了解要求较高;
    • 熟悉网络编程,面对断连重连、保丢失、粘包等,处理复杂;
    • 存在 BUG,selector 空轮训导致CPU飙升;

      2.2.2 NIO 模型原理

      Netty - 图10Netty - 图11

    • Reacotr 单线程模型,适合处理器链中业务处理组件能快速完成的场景,不常用

image.png

  • Reactor 多线程模型,在处理链部分采用多线程(线程池),后端程序常用模型

image.png

  • Reactor 主从模型,多个 Acceptor 的 NIO 线程池用于接受客户端的连接;

Netty - 图14

2.2.3 buffer 与 channel

Netty - 图15

  • Channel 是 NIO 中的数据通道,与流类似,区别在于 Channel 既可从中读取数据,又可以从写数据到通道中,但是流的读写通常是单向的;
  • Channel 可以异步的读写;
  • Channel 类型包括:FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel;
  • Channel 中的数据总是要先读到一个 Buffer 中,或者从缓冲区中将数据写到通道中; 

    2. Netty

    2.1 概要介绍

  • 定义

    • Netty是一个异步、事件驱动的用来做高性能、高可靠性的网络应用框架;
  • 本质:基于原生Java NIO技术封装的一套处理 socket 远程通信的框架,是用 JBoss 做的一个Jar包,有完整的 ioc 容器支持;
  • 优点
    • 框架设计优雅,底层模型随意切换适应不同的网络协议要求;
    • 提供很多标准的协议、安全、编码解码的支持;
    • 解决了很多 NIO 不易用的问题;
    • 社区更为活跃,在很多开源框架中使用,如 Dubbo、RocketMQ、Spark 等;
    • 支持自定义协议服务器开发;
    • 作为异步高性能的通信框架,往往作为基础通信组件被其他 RPC 框架使用;
  • 支持的功能及特性
    • 设计统一的API,适用于不同的协议(阻塞和非阻塞),基于灵活、可扩展的事件驱动模型(三种 Reactor 模型)高度可定制的线程模型可靠的无连接数据Socket支持(UDP);
    • 性能更好的吞吐量,低延迟更省资源尽量减少不必要的内存拷贝;
    • 安全完整的 SSL/TLS和STARTTLS 的支持,能在 Applet 与 Android 的限制环境运行良好;
    • 健壮性,不再因过快、过慢或超负载连接导致 OutOfMemoryError ,不再有在高速网络环境下 NIO 读写频率不一致的问题;
    • 易用完善的 JavaDoc,用户指南和样例简洁简单仅信赖于 JDK1.5;

Netty - 图16

  • 实现原理
    • 在Netty里面,Accept连接可以使用单独的线程池去处理,读写操作又是另外的线程池来处理。Accept连接和读写操作也可以使用同一个线程池来进行处理。而请求处理逻辑既可以使用单独的线程池进行处理,也可以跟放在读写线程一块处理。线程池中的每一个线程都是NIO线程。用户可以根据实际情况进行组装,构造出满足系统需求的并发模型。
  • 应用
    • 阿里分布式服务框架 Dubbo 的 RPC 框架使用 Dubbo 协议进行节点间通信,Dubbo 协议默认使用 Netty 作为基础通信组件,用于实现各进程节点之间的内部通信。它的架构图如下:

Netty - 图17

  • 淘宝的消息中间件 RocketMQ 的消息生产者和消息消费者之间,也采用 Netty 进行高性能、异步通信;
  • 经典的 Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架,默认采用 Netty 进行跨节点通信,它的 Netty Service 基于 Netty 框架二次封装实现;

    2.2 具体实现

    2.2.1 主要的组件:

  • Bootstrap:netty的组件容器,用于把其他各个部分连接起来;如果是TCP的Server端,则为ServerBootstrap.
  • Channel:代表一个Socket的连接
  • EventLoopGroup:一个Group包含多个EventLoop,可以理解为线程池
  • EventLoop:处理具体的Channel,一个EventLoop可以处理多个Channel
  • ChannelPipeline:每个Channel绑定一个pipeline,在上面注册处理逻辑handler
  • Handler:具体的对消息或连接的处理,有两种类型,Inbound和Outbound。分别代表消息接收的处理和消息发送的处理,当接收消息的时候,会从链表的表头开始遍历,如果是inbound就调用对应的方法;如果发送消息则从链表的尾巴开始遍历;

Netty - 图18



  • ChannelFuture:注解回调方法

    2.2.2 解决TCP 粘包与拆包

  • 产生原因:ByteBuf 为 单位来发送数据,server按照Bytebuf读取,但是到了底层操作系统仍然是按照字节流发送数据,因此,数据到了服务端,也是按照字节流的方式读入,然后到了 Netty 应用层面,重新拼装成 ByteBuf,而这里的 ByteBuf 与客户端按顺序发送的 ByteBuf 可能是不对等的

解决:Netty - 图19

2.2.3 netty 零拷贝

传统意义的拷贝

是在发送数据的时候,传统的实现方式是:
1. File.read(bytes)
2. Socket.send(bytes)
这种方式需要四次数据拷贝和四次上下文切换:
1. 数据从磁盘读取到内核的read buffer
2. 数据从内核缓冲区拷贝到用户缓冲区
3. 数据从用户缓冲区拷贝到内核的socket buffer
4. 数据从内核的socket buffer拷贝到网卡接口(硬件)的缓冲区

零拷贝的概念

明显上面的第二步和第三步是没有必要的,通过java的FileChannel.transferTo方法,可以避免上面两次多余的拷贝(当然这需要底层操作系统支持)
1. 调用transferTo,数据从文件由DMA引擎拷贝到内核read buffer
2. 接着DMA从内核read buffer将数据拷贝到网卡接口buffer
上面的两次操作都不需要CPU参与,所以就达到了零拷贝。

Netty中的零拷贝

主要体现在三个方面:
1、bytebuffer
Netty发送和接收消息主要使用bytebuffer,bytebuffer使用对外内存(DirectMemory)直接进行Socket读写。
原因:如果使用传统的堆内存进行Socket读写,JVM会将堆内存buffer拷贝一份到直接内存中然后再写入socket,多了一次缓冲区的内存拷贝。DirectMemory中可以直接通过DMA发送到网卡接口
2、Composite Buffers
传统的ByteBuffer,如果需要将两个ByteBuffer中的数据组合到一起,我们需要首先创建一个size=size1+size2大小的新的数组,然后将两个数组中的数据拷贝到新的数组中。但是使用Netty提供的组合ByteBuf,就可以避免这样的操作,因为CompositeByteBuf并没有真正将多个Buffer组合起来,而是保存了它们的引用,从而避免了数据的拷贝,实现了零拷贝。
3、对于FileChannel.transferTo的使用
Netty中使用了FileChannel的transferTo方法,该方法依赖于操作系统实现零拷贝。

2.2.4 Netty 内部执行流程

服务端:
Netty - 图20
Netty - 图21
1、创建ServerBootStrap实例
2、设置并绑定Reactor线程池:EventLoopGroup,EventLoop就是处理所有注册到本线程的Selector上面的Channel
3、设置并绑定服务端的channel
4、5、创建处理网络事件的ChannelPipeline和handler,网络时间以流的形式在其中流转,handler完成多数的功能定制:比如编解码 SSl安全认证
6、绑定并启动监听端口
7、当轮训到准备就绪的channel后,由Reactor线程:NioEventLoop执行pipline中的方法,最终调度并执行channelHandler
客户端
Netty - 图22

Netty - 图23

3. 补充

3.1 由 BIO、NIO 到 AIO

  • BIO 同步阻塞 IO;
  • NIO 同步非阻塞 IO,基于 Reactor 模型实现;
  • AIO 异步非阻塞 IO,基于 Proactor 模型实现,其非阻塞通过一个 selector 线程不断轮询其他 socket 连接实现,若事件发生就产生通知,然后便启动一个线程处理一个请求即可,其同步体现在仍需主动地读写数据;
  • AIO 发送读写消息后不会阻塞,操作系统完成操作会将执行结果放入缓冲 buffer 中

    Netty - 图24

    3.2 异步 IO 模型

    Netty - 图25

    参考

  1. 知乎-通俗地讲,Netty 能做什么?
  2. 网络编程之socket异步编程
  3. 彻底理解Netty,这一篇文章就够了
  4. 漫谈 java io 之 netty 与 nio 服务
  5. 最高频的Java NIO面试题剖析
  6. Java NIO 浅析