目录
Github地址:https://github.com/netty/netty
参考课程:Netty源码剖析与实战 https://time.geekbang.org/course/intro/237
第一章、初识Netty:背景、现状与趋势
学习后做到以下四点:
-
掌握Java网络编程基础知识和原理
-
使用Netty构建一个能推向产品线的Java网络服务器
-
熟悉Netty核心源码以及其深层原理
-
能够熟练诊断、分析并排除Netty使用中的各种故障
解开Netty面纱
Netty由 Trustin Lee(韩国,Line公司)2004年开发
-
本质:网络应用程序框架
-
实现:异步、时间驱动
-
特性:高性能、可维护、快速开发
-
用途:开发服务器和客户端
三部分
-
最底层的核心层
-
零复制的功能丰富的Byte Buffer
-
通用的通信层API
-
可扩展的事件模型
-
-
左上:支持的传输层,包括TCP的socket、UDP的Datagtam、Http的Tunnel和In-VM的Pipe
-
右上:支持的各种各样协议
Netty本质上是一个jar包
为什么不使用JDK NIO?
-
Netty做的更多
-
支持常用应用层协议;
-
解决传输问题:粘包、半包现象;
-
支持流量整形
-
完整的断连、Idle等异常处理等。
-
Netty做的更好
-
规避JDK NIO bug
-
经典的epoll bug:异常唤醒空转导致CPU 100%
-
IP_TOS参数(IP包的优先级和QoS选项)使用时的抛出异常 --> java.lang.Assertion:Option not found
-
-
API更友好强大
-
JDK的NIO一些API不够友好,功能薄弱,例如:ByteBuffer --> Netty's ByteBuf
-
其他一些增强:Threadlocal --> Netty's FastThreadLocal
-
-
隔离变化、屏蔽细节
-
隔离JDK NIO的实现变化:nio -> nio2(aio) -> ...
-
屏蔽JDK NIO的实现细节
-
-
为什么独选Netty
Apache Mina | 同一作者,推荐Netty |
Sun Grizzly | 三少:用的少、文档少、更新少 |
Apple SwiftNIO、ACE等 | 其他语言,不考虑 |
Cindy | 生命周期不长 |
Tomcat、Jetty | 还没独立出来 |
Netty的前尘往事
-
废弃5.0原因
复杂、没有证明明显性能优势、维护不过来
-
与Apache Mina关系
同一作者开发,都处于维护阶段(2004.06 Netty2发布 2005.05 Mina发布)
Github地址:https://github.com/netty/netty
第二章: Netty 源码:从“点”(领域知识)的角度剖析
Netty 怎么切换三种 I/O 模式
-
经典的三种I/O模式
排队打饭模式 | BIO (阻塞 I/O) | JDK1.4 之前 |
点单、等待被叫模式 | NIO (非阻塞 I/O) | JDK1.4(2002 年,java.nio 包) |
包厢模式 | AIO(异步 I/O) | JDK1.7 (2011 年) |
-
阻塞与非阻塞
-
菜没好,要不要死等 -> 数据就绪前要不要等待?
-
阻塞:没有数据传过来时,读会阻塞直到有数据;缓冲区满时,写操作也会阻塞。
非阻塞遇到这些情况,都是直接返回。
-
-
同步与异步
-
菜好了,谁端 -> 数据就绪后,数据操作谁完成?
-
数据就绪后需要自己去读是同步,数据就绪直接读好再回调给程序是异步。
-
Netty 对三种 I/O 模式的支持
-
为什么 Netty 仅支持 NIO 了?
-
为什么不建议(deprecate)阻塞 I/O(BIO/OIO)?
连接数高的情况下:阻塞 -> 耗资源、效率低
-
为什么删掉已经做好的 AIO 支持?
Windows 实现成熟,但是很少用来做服务器。
Linux 常用来做服务器,但是 AIO 实现不够成熟。
Linux 下 AIO 相比较 NIO 的性能提升不明显 。
-
为什么 Netty 有多种 NIO 实现?
通用的 NIO 实现(Common)在 Linux 下也是使用 epoll,为什么自己单独实现?
实现得更好!
• Netty 暴露了更多的可控参数,例如:
• JDK 的 NIO 默认实现是水平触发
• Netty 是边缘触发(默认)和水平触发可切换
• Netty 实现的垃圾回收更少、性能更好
-
NIO 一定优于 BIO 么?
BIO 代码简单;特定场景:连接数少,并发度低,BIO 性能不输 NIO。
-
Netty 怎么切换 I/O 模式的?
-
怎么切换? 例如对于服务器开发:从 NIO 切换到 OIO
NIO | OIO |
---|---|
NioEventLoopGroup | OioEventLoopGroup |
NioServerSocketChannel | OioServerSocketChannel |
-
原理是什么? 例如对于 ServerSocketChannel:工厂模式+泛型+反射实现
-
为什么服务器开发并不需要切换客户端对应NioSocketChannel ?
ServerSocketChannel 负责创建对应的 SocketChannel 。
-
Netty 如何支持三种 Reactor
-
什么是 Reactor 及三种版本
1. 一个人包揽所有:迎宾、点菜、做饭、上菜、送客等 --> Reactor 单线程
2. 多招几个伙计:大家一起做上面的事情 --> Reactor 多线程模式
3. 进一步分工:搞一个或者多个人专门做迎宾 --> 主从 Reactor 多线程模式
-
Reactor 是一种开发模式,模式的核心流程:
注册感兴趣的事件 -> 扫描是否有感兴趣的事件发生 -> 事件发生后做出相应的处理。
BIO | NIO | AIO |
---|---|---|
Thread-Per-Connection | Reactor | Proactor |
模式 | 模型 | Netty中如何使用 |
---|---|---|
Thread-Per-Connection | ![]()
|
|
Reactor模式v1:单线程 | ![]()
| EventLoopGroup eventGroup = new NioEventLoopGroup(1); ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(eventGroup); |
Reactor模式v2:多线程 | ![]()
| EventLoopGroup eventGroup = new NioEventLoopGroup(); ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(eventGroup); |
Reactor模式v3:主从多线程 | ![]()
| EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup, workerGroup); |
TCP 粘包、半包 Netty 全搞定
-
什么是粘包和半包
ABC DEF --> ABCDEF? AB CD EF?
粘包的主要原因:
-
发送方每次写入数据 < 套接字缓冲区大小
-
接收方读取套接字缓冲区数据不够及时
半包的主要原因:
-
发送方写入数据 > 套接字缓冲区大小
-
发送的数据大于协议的 MTU(Maximum Transmission Unit,最大传输单元),必须拆包
换个角度看:
-
收发 一个发送可能被多次接收,多个发送可能被一次接收
-
传输 一个发送可能占用多个传输包,多个发送可能公用一个传输包
根本原因:
TCP是流式协议,消息无边界。
提醒:UDP 像邮寄的包裹,虽然一次运输多个,但每个包裹都有“界限”,一个一个签收,所以无粘包、半包问题
解决问题的根本手段:找出消息的边界
方式\比较 | 寻找消息边界方式 | 优点 | 缺点 | Netty封帧方式 | 推荐度 | ||
---|---|---|---|---|---|---|---|
解码 | 编码 | ||||||
TCP连接改为短连接,一个请求一个短连接 | 建立连接到释放连接之间的信息即为传输信息 | 简单 | 效率低下 |
|
| 不推荐 | |
封装成帧 (Framing) | 固定长度
| 满足固定长度即可 | 简单 | 空间浪费 | FixedLengthFrameDecoder | 简单 | 不推荐 |
分割符
| 分割符之间 | 空间不浪费,也比较简单 | 内容本身出现分隔符时需转义,所以需要扫描内容 | DelimiterBasedFrameDecoder | 推荐 | ||
固定长度字段存个内容的长度信息
| 先解析固定长度的字段获取长度,然后读取后续内容 | 精确定位用户数据,内容也不用转义 | 长度理论上有限制,需提前预知可能的最大长度从而定义长度占用字节数 |
LengthFieldBasedFrameDecoder | LengthFieldPrepender | 推荐+ | |
其他方式 | 每种都不同,例如JSON 可以看{}是否应已经成对 | 衡量实际场景,很多是对现有协议的支持 |
为什么需要“二次”编解码
假设我们把解决半包粘包问题的常用三种解码器叫一次解码器,那么我们在项目中,除了可选的的压缩解压缩之外,还需要一层解码,因为一次解码的结果是字节,需要和项目中所使用的对象做转化,方便使用,这层解码器可以称为“二次解码器”,相应的,对应的编码器是为了将 Java 对象转化成字节流方便存储或传输。
-
一次解码器:ByteToMessageDecoder
io.netty.buffer.ByteBuf (原始数据流)--> io.netty.buffer.ByteBuf (用户数据)
-
二次解码器:MessageToMessageDecoder<I>
io.netty.buffer.ByteBuf (用户数据)--> Java Object
-
是否一步到位,合并一次和二次解码?
可以,但不建议;没有分层,不够清晰;耦合性高,不容易置换方案。
常见的“二次”编解码方式
Java序列化、Marshaling、XML、JSON、MessagePack、Protobuf、其他
选择编码方式的要点
- 空间
编码后的占用空间,需要比较不同的数据大小情况
- 时间
编解码速度,需要比较不同的数据大小情况
- 是否追求可读性
- 多语言(Java、C、Python 等)的支持:例如 msgpack 的多语言支持
Google Protobuf 简介
-
Protobuf 是一个灵活的、高效的用于序列化数据的协议。
-
相比较 XML 和 JSON 格式,Protobuf 更小、更快、更便捷。
-
Protobuf 是跨语言的,并且自带了一个编译器(protoc),只需要用它进行编译,可以自动生成 Java、python、C++ 等代码,不需要再写其他代码。
Netty 对二次编解码的支持
ch.pipeline().addLast(new ProtobufVarint32FrameDecoder());
ch.pipeline().addLast(new ProtobufDecoder(PersonOuterClass.Person.getDefaultInstance()));
ch.pipeline().addLast(new ProtobufVarint32LengthFieldPrepender());
ch.pipeline().addLast(new ProtobufEncoder());
keepalive 与 Idle 监测
为什么需要keepalive
为什么还需要应用层keepalive?
-
协议分层,各层关注点不同:
传输层关注是否“通”,应用层关注是否可服务? 类比前面的电话订餐例子,电话能通,不代表有人接;服务器连接在,但是不定可以服务(例如服务不过来等)。
-
TCP 层的 keepalive 默认关闭,且经过路由等中转设备 keepalive 包可能会被丢弃。
-
TCP 层的 keepalive 时间太长:
默认 > 2 小时,虽然可改,但属于系统参数,改动影响所有应用。
-
提示:
HTTP 属于应用层协议,但是常常听到名词“ HTTP Keep-Alive ”指的是对长连接和短连接的选择:
• Connection : Keep-Alive 长连接(HTTP/1.1 默认长连接,不需要带这个 header)
• Connection : Close 短连接
什么是idle检测
Idle 监测,只是负责诊断,诊断后,做出不同的行为,决定 Idle 监测的最终用途:
-
发送 keepalive :一般用来配合 keepalive ,减少 keepalive 消息。
Keepalive 设计演进:V1 定时 keepalive 消息 -> V2 空闲监测 + 判定为 Idle 时才发keepalive。
-
V1:keepalive 消息与服务器正常消息交换完全不关联,定时就发送;
-
V2:有其他数据传输的时候,不发送 keepalive ,无数据传输超过一定时间,判定为 Idle,再发 keepalive 。
-
直接关闭连接:
-
快速释放损坏的、恶意的、很久不用的连接,让系统时刻保持最好的状态。
-
简单粗暴,客户端可能需要重连。
实际应用中:结合起来使用。按需 keepalive ,保证不会空闲,如果空闲,关闭连接。
开启keepalive
-
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));
/**
* @see #IdleStateHandler(boolean, long, long, long, TimeUnit)
*/
public IdleStateHandler(long readerIdleTime, long writerIdleTime, long allIdleTime,TimeUnit unit) {
this(false, readerIdleTime, writerIdleTime, allIdleTime, unit);
}
Netty 的那些“锁”事
分析同步问题的核心三要素
-
原子性:“并无一气呵成,岂能无懈可击”
-
可见性:“你做的改变,别人看不见”
-
有序性:“不按套路出牌”
锁的分类
-
对竞争的态度:乐观锁(java.util.concurrent 包中的原子类)与悲观锁(Synchronized)
-
等待锁的人是否公平而言:公平锁 new ReentrantLock (true)与非公平锁 new ReentrantLock ()
-
是否可以共享:共享锁与独享锁:ReadWriteLock ,其读锁是共享锁,其写锁是独享锁
Netty 玩转锁的五个关键点
- 在意锁的对象和范围 -> 减少粒度
初始化 channel (io.netty.bootstrap.ServerBootstrap#init)
Synchronized method -> Synchronized block
- 注意锁的对象本身大小 -> 减少空间占用
统计待发送的字节数(io.netty.channel.ChannelOutboundBuffer
AtomicLong -> Volatile long + AtomicLongFieldUpdater
Atomic long VS long:
前者是一个对象,包含对象头(object header)以用来保存 hashcode、lock 等信息,32 位系统占
用8字节;64 位系统占 16 字节,所以在 64 位系统情况下:
• volatile long = 8 bytes
• AtomicLong = 8 bytes (volatile long)+ 16bytes (对象头)+ 8 bytes (引用) = 32 bytes
至少节约 24 字节!
结论:Atomic* objects -> Volatile primary type + Static Atomic*FieldUpdater
- 注意锁的速度 -> 提高并发性
1)记录内存分配字节数等功能用到的LongCounter io.netty.util.internal.PlatformDependent#newLongCounter()
高并发时:java.util.concurrent.atomic.AtomicLong ->java.util.concurrent.atomic.LongAdder (JDK1.8)
结论: 及时衡量、使用 JDK 最新的功能
2)曾经根据不同情况,选择不同的并发包实现:JDK < 1.8 考虑ConcurrentHashMapV8(ConcurrentHashMap 在 JDK8 中的版本)
- 不同场景选择不同的并发类 -> 因需而变
1)关闭和等待关闭事件执行器(Event Executor):
Object.wait/notify --> CountDownLatch
io.netty.util.concurrent.SingleThreadEventExecutor#threadLock
2)Nio Event loop中负责存储task的Queue
Jdk’s LinkedBlockingQueue (MPMC) -> jctools’ MPSC
io.netty.util.internal.PlatformDependent.Mpsc#newMpscQueue(int)
- 衡量好锁的价值 -> 能不用则不用
1)局部串行:Channel 的 I/O 请求处理 Pipeline 是串行的
2)整体并行:多个串行化的线程(NioEventLoop)
Netty 应用场景下:局部串行 + 整体并行 > 一个队列 + 多个线程模式:
-
降低用户开发难度、逻辑简单、提升处理性能
-
避免锁带来的上下文切换和并发保护等额外开销
3)避免用锁:用 ThreadLocal 来避免资源争用,例如 Netty 轻量级的线程池实现
io.netty.util.Recycler#threadLocal
Netty 如何玩转内存使用
内存使用技巧的目标
目标:
• 内存占用少(空间)
• 应用速度快(时间)
对 Java 而言:减少 Full GC 的 STW(Stop the world)时间
Netty内存使用技巧
减少对象本身大小 | 1)用基本类型就不要用包装类型 2)应该定义成类变量的不要定义为实例变量:
3)Netty 中结合前两者: io.netty.channel.ChannelOutboundBuffer#incrementPendingOutboundBytes(long, boolean) 统计待写的请求的字节数 |
对分配内存进行预估 | 1)对于已经可以预知固定 size 的 HashMap避免扩容,可以提前计算好初始size或者直接使用 com.google.common.collect.Maps#newHashMapWithExpectedSize 2)Netty 根据接受到的数据动态调整(guess)下个要分配的 Buffer 的大小。可参考 io.netty.channel.AdaptiveRecvByteBufAllocator |
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 |
堆外内存 |
|
内存池 | 为什么引入对象池:
如何实现对象池?
|
源码解读Netty内存使用
怎么从堆外内存切换堆内使用?
-
方法 1:参数设置
io.netty.noPreferDirect = true;
-
方法 2:传入构造参数false
ServerBootstrap serverBootStrap = new ServerBootstrap();
UnpooledByteBufAllocator unpooledByteBufAllocator = new UnpooledByteBufAllocator(false);
serverBootStrap.childOption(ChannelOption.ALLOCATOR, unpooledByteBufAllocator)
堆外内存的分配?
-
ByteBuffer.allocateDirect(initialCapacity)
内存池/非内存池的默认选择及切换方式?
默认选择:安卓平台 -> 非 pooled 实现,其他 -> pooled 实现。
-
参数设置:io.netty.allocator.type = unpooled;
-
显示指定:serverBootStrap.childOption(ChannelOption.ALLOCATOR, UnpooledByteBufAllocator.DEFAULT)
内存池实现?
核心要点:有借有还,避免遗忘