Netty学习笔记 - 李林锋

第一章:初识Netty:背景、现状与趋势

揭开 Netty 面纱

  • 作者
  • 概述
  • 代码模块
  • helloworld

为什么舍近求远:不直接用 JDK NIO

  • 做的更多
    • 支持常用应用层协议;
    • 解决传输问题:粘包、半包现象;
    • 支持流量整形;
    • 完善的断连、Idle 等异常处理等
  • 做的更好
    • 避免jdk nio 的bug
    • API更好更强大
    • 隔离变化、屏蔽细节
  • 避免花大量时间造轮子
  • netty社区活跃,发展情景明朗

为什么孤注一掷:独选 Netty ?

  • 相比mina:同作者推荐
  • 相比grizzy:更新多、文档多用的多
  • 为什么不选 Apple SwfitNIO 、ACE 等:其他语言
  • 为什么不选 Cindy 等:生命周期不长。
  • 为什么不选 Tomcat、Jetty :还没有独立出来。

Netty 的前尘往事

  • 从归属组织上看发展
  • 从版本演变上看发展
  • 社区现状
  • 最新版本

Netty 的现状与趋势

  • 应用现状
  • 一些典型项目
  • 趋势

第二章:Netty 源码:从“点”(领域知识)的角度剖析

1. Netty 怎么切换三种 I/O 模式

  • 什么是经典的三种 I/O 模式
    • 一个吃饭的例子
  • Netty 对三种 I/O 模式的支持
    • BIO:过时
    • NIO:common、linux、bsd
    • AIO:移除
  • 为什么 Netty 仅支持 NIO 了?
    • BIO
    • AIO
  • 为什么 Netty 有多种 NIO 实现?
    • 更多功能
    • 更好性能
  • NIO 一定优于 BIO 么?
    • 仅在高并发时
  • 源码解读 Netty 怎么切换 I/O 模式?
    • EventLoopGroup
      • 线程池
      • 工厂模式+泛型+反射实现
    • ServerSocketChannel
      • 如何处理链接
    • 为什么服务器开发并不需要切换客户端对应NioSocketChannel ?
      • ServerSocketChannel 负责创建对应的 SocketChannel 。

2. Netty 如何支持三种 Reactor

2.1. 什么是Reactor模式?

2.2. Reactor模式的图示

  • Thread-Per-Connection模式

  • Reactor模式v1:单线程

  • Reactor模式v2:多线程

  • Reactore模式v3:主从多线程

2.3. 如何使用三种Reactor模式

2.4. Reactor相关源码分析

  • Netty 如何支持主从 Reactor 模式的?
  • 为什么说 Netty 的 main reactor 大多并不能用到一个线程组,只能线程组里面的一个?
  • Netty 给 Channel 分配 NIO event loop 的规则是什么
  • 通用模式的 NIO 实现多路复用器是怎么跨平台的

3. TCP 粘包/半包 Netty 全搞定

3.1. 什么是粘包和半包?

3.2. 为什么 TCP 应用中会出现粘包和半包现象?

粘包的主要原因:

  • 发送方每次写入数据 < 套接字缓冲区大小
  • 接收方读取套接字缓冲区数据不够及时

半包的主要原因:

  • 发送方写入数据 > 套接字缓冲区大小
  • 发送的数据大于协议的 MTU(Maximum Transmission Unit,最大传输单元),必须拆包

换个角度看:

  • 收发
    • 一个发送可能被多次接收,多个发送可能被一次接收
  • 传输
    • 一个发送可能占用多个传输包,多个发送可能公用一个传输包

根本原因:

  • TCP 是流式协议,消息无边界。

3.3. 解决粘包和半包问题的几种常用方法

3.4. Netty 对三种常用封帧方式的支持

3.5. 解读 Netty 处理粘包、半包的源码

4. 常用的“二次”编解码方式

4.1. 为什么要“二次”解码?

Java对象和字节流之间相互转换。

4.2. 常用的“二次编解码”方式

  • Java序列化
  • Marshaling
  • XML
  • JSON
  • MessagePack
  • Protobuf
  • 其他

4.3. 选择编解码方式的要点

  • 大小
  • 速度
  • 可读性

4.4. Protobuf简介与适用

  • 工具生成对应相等代码

4.5. 源码解读:Netty对二次编解码的支持

ch.pipeline().addLast(new ProtobufVarint32FrameDecoder());
ch.pipeline().addLast(new ProtobufDecoder(PersonOuterClass.Person.getDefaultInstance())); ch.pipeline().addLast(new ProtobufVarint32LengthFieldPrepender()); ch.pipeline().addLast(new ProtobufEncoder());

5. keepalive 与 Idle 监测

5.1. 为什么需要keepalive?

生活中的例子:
如果打电话中别人忽然不说话了,你会问一句“你还在吗?”,如果没有回复,就挂断。

好处:
避免长时间占用线路,别人就打不进来了

5.2. 怎么设计keepalive?以TCP keepalive 为例

TCP keepalive 核心参数:

# sysctl -a|grep tcp_keepalive 
net.ipv4.tcp_keepalive_time = 7200 
net.ipv4.tcp_keepalive_intvl = 75 
net.ipv4.tcp_keepalive_probes = 9

当启用(默认关闭)keepalive 时,TCP 在连接没有数据
通过的7200秒后发送 keepalive 消息,当探测没有确认时, 按75秒的重试频率重发,一直发 9 个探测包都没有确认,就认定 连接失效。

所以总耗时一般为:2 小时 11 分钟 (7200 秒 + 75 秒* 9 次)

5.3. 为什么还需要应用层keepalive?

  • 协议分层,各层关注点不同:
    传输层关注是否“通”,应用层关注是否可服务? 类比前面的电话订餐例子,电话能通, 不代表有人接;服务器连接在,但是不定可以服务(例如服务不过来等)。
  • TCP 层的 keepalive 默认关闭,且经过路由等中转设备 keepalive 包可能会被丢弃。
  • TCP 层的 keepalive 时间太长:
    默认 > 2 小时,虽然可改,但属于系统参数,改动影响所有应用。

5.4. idle监测是什么?

生活例子:
在打电话时,如果别人忽然不讲讲话了,隔一段时间后,你会问“你还在吗?”

总结:
Idle监测用做诊断,配合keepalive,减少keepalive消息。

Idle监测的演进:
v1:定时发送监测消息。
v2:有其他数据传输时,不发送监测消息。无数据传输时,定时发送监测消息。

5.5. 如何在Netty中开启TCP keepalive 和Idle监测?

  • Server 端开启 TCP keepalive
bootstrap.childOption(ChannelOption.SO_KEEPALIVE,true) bootstrap.childOption(NioChannelOption.of(StandardSocketOptions.SO_KEEPALIVE), true)

提示:.option(ChannelOption.SO_KEEPALIVE,true) 存在但是无效

  • 开启不同的 Idle Check:
ch.pipeline().addLast(“idleCheckHandler", new IdleStateHandler(0, 20, 0, TimeUnit.SECONDS));

5.6. 源码解读 Netty 对 TCP keepalive 和三种 Idle 检测的支持

源码解读:

  • 设置 TCP keepalive 怎么生效的?
    • 调用jdk接口,设置chennel的option
  • 两种设置 keepalive 的方式有什么区别?
    • 没啥区别,一个是netty抽象的,一个是nio的
  • Idle 检测类包(io.netty.handler.timeout)的功能浏览
  • 读 Idle 检测的原理
  • 写 Idle 检测原理和参数 observeOutput 用途?

6. Netty 的那些“锁”事

6.1. 分析同步问题的核心三要素

  • 原子性
  • 可见性
  • 有序性

6.2. 锁的分类

  • 对竞争的态度:乐观锁(java.util.concurrent 包中的原子类)与悲观锁(Synchronized)
  • 等待锁的人是否公平而言:公平锁 new ReentrantLock (true)与非公平锁 new ReentrantLock ()
  • 是否可以共享:共享锁与独享锁:ReadWriteLock ,其读锁是共享锁,其写锁是独享锁

6.3. Netty玩转锁的五个关键点:

  • 在意锁的对象和范围 -> 减少粒度
  • 注意所得对象本身大小 -> 减少空间占用
  • 注意锁的速度 -> 提高速度
  • 不同场景选择不同的并发类 -> 因需而变
  • 衡量好锁的价值 -> 能不能则不用

7. Netty 如何玩转内存使用

7.1. 内存使用技巧的目标

目标:

  • 内存占用少(空间)
  • 应用速度快(时间)
    对 Java 而言:减少 Full GC 的 STW(Stop the world)时间

7.2. Netty 内存使用技巧 - 减少对像本身大小

例 1:用基本类型就不要用包装类:
例 2: 应该定义成类变量的不要定义为实例变量:
例 3: Netty 中结合前两者:
io.netty.channel.ChannelOutboundBuffer#incrementPendingOutboundBytes(long, boolean) 统计待写的请求的字节数

7.3. Netty 内存使用技巧 - 对分配内存进行预估

例 1:对于已经可以预知固定 size 的 HashMap避免扩容
可以提前计算好初始size或者直接使用 com.google.common.collect.Maps#newHashMapWithExpectedSize

例 2:Netty 根据接受到的数据动态调整(guess)下个要分配的 Buffer 的大小。可参考 io.netty.channel.AdaptiveRecvByteBufAllocator

7.4. Netty 内存使用技巧 - Zero-Copy

例 1:使用逻辑组合,代替实际复制。
例如 CompositeByteBuf: io.netty.handler.codec.ByteToMessageDecoder#COMPOSITE_CUMULATOR

例 2:使用包装,代替实际复制。
byte[] bytes = data.getBytes();
ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);

例 3:调用 JDK 的 Zero-Copy 接口。
Netty 中也通过在 DefaultFileRegion 中包装了 NIO 的 FileChannel.transferTo() 方法实 现了零拷贝:io.netty.channel.DefaultFileRegion#transferTo

7.5. Netty 内存使用技巧 - 堆外内存

优点:

  • 更广阔的“空间 ”,缓解店铺内压力 -> 破除堆空间限制,减轻 GC 压力
  • 减少“冗余”细节(假设烧烤过程为了气氛在室外进行:烤好直接上桌:vs 烤好还 要进店内)-> 避免复制

缺点:

  • 需要搬桌子 -> 创建速度稍慢
  • 受城管管、风险大 -> 堆外内存受操作系统管理

7.6. Netty 内存使用技巧 - 内存池

为什么引入对象池:

  • 创建对象开销大
  • 对象高频率创建且可复用
  • 支持并发又能保护系统
  • 维护、共享有限的资源

如何实现对象池?

  • 开源实现:Apache Commons Pool
  • Netty 轻量级对象池实现 io.netty.util.Recycler

7.7. 源码解读Netty 内存使用

源码解读:

  • 怎么从堆外内存切换堆内使用?以UnpooledByteBufAllocator为例
  • 堆外内存的分配本质?
  • 内存池/非内存池的默认选择及切换方式?
    io.netty.channel.DefaultChannelConfig#allocator
  • 内存池实现(以 PooledDirectByteBuf 为例)
    io.netty.buffer.PooledDirectByteBuf
/** * RECOMMENDED CONFIGURATION VARIABLES: EDIT AND UNCOMMENT THE SECTION BELOW TO INSERT DYNAMIC VALUES FROM YOUR PLATFORM OR CMS. * LEARN WHY DEFINING THESE VARIABLES IS IMPORTANT: https://disqus.com/admin/universalcode/#configuration-variables*/ /* var disqus_config = function () { this.page.url = PAGE_URL; // Replace PAGE_URL with your page's canonical URL variable this.page.identifier = PAGE_IDENTIFIER; // Replace PAGE_IDENTIFIER with your page's unique identifier variable }; */ (function() { // DON'T EDIT BELOW THIS LINE var d = document, s = d.createElement('script'); s.src = 'https://chenzz.disqus.com/embed.js'; s.setAttribute('data-timestamp', +new Date()); (d.head || d.body).appendChild(s); })();