Netty学习笔记
目录
公号:码农充电站pro
1,初始 Netty
Netty 是一个异步事件驱动的网络应用程序框架,可用于快速开发可维护的高性能网络服务器和客户端。
Netty 项目地址:
- 官网:
https://netty.io/
- Github:
https://github.com/netty/netty
Netty 结构图:
Netty 的版本迭代
Netty 版本迭代:
- 2004 年 6 月 Netty2 发布
- 2008 年 10 月 Netty3 发布
- 2013 年 7 月 Netty4 发布
- 2013 年 12 月 Netty 5.0Alphal 发布
- 2015 年 11 月 Netty 5.0 废弃
- 2016 年 6 月 Netty 3.10.6.Final 发布
- 2018 年 2 月 Netty 4.0.56.Final 发布
- 2019 年 8 月 Netty 4.1.39.Final 发布
Netty5.0 已不在被官网支持,其废弃原因是:
- 太过复杂
- 没有明显性能优势
- 维护不过来
Netty 的使用者 https://netty.io/wiki/related-projects.html
。
Netty 学习资料:
2,经典的三种 I/O 模式
三种 I/O 模式:
- BIO:同步阻塞 IO(JDK1.4 之前)
- 如果 Socket 上没有数据可读,就一直等待
- NIO:同步非阻塞 IO(JDK1.4 2002年,java.nio 包)
- 如果 Socket 上没有数据可读,不会等待,当 Socket 上有数据时,会接到通知,再去读数据
- AIO:异步非阻塞 IO(JDK1.7 2011年)
- 在 Socket 上注册回调函数,当有数据时,让回调函数去处理
1,Java NIO 概念
BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多。
BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel 和 Buffer 进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中,Buffer 和Channel 之间的数据流向是双向的(BIO 中要么是输入流,或者是输出流,不能双向)。
Selector 用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。
Java NIO 三大概念:
-
Channels(通道):每个 Channel 都会对应一个 Buffer
- Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer
- 常用的 Channel 类有:
- FileChannel:用于本地文件的数据读写
- DatagramChannel:用于 UDP 的数据读写
- ServerSocketChannel:类似 ServerSocket
- SocketChannel:类似 Socket
-
Buffers(缓冲区):Buffer 是一个内存块,底层是一个容器(数组)
-
Selectors(选择器):一个 Selector 监听多个 Channel(连接),Selector 会根据不同的 Event (事件),在各个通道上切换。
- SelectionKey 中的事件类型:
OP_READ,OP_WRITE,OP_CONNECT, OP_ACCEPT
- SelectionKey 中的事件类型:
Java NIO 学习资料:
原生 NIO 存在的问题:
- NIO 的类库和 API 繁杂,使用麻烦:需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等
- 需要具备其他的额外技能:要熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的 NIO 程序
- 开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等等
- JDK NIO 的 Bug:例如臭名昭著的 Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU100%。直到 JDK1.7 版本该问题仍旧存在,没有被根本解决
2,零拷贝 DMA(直接内存访问,不经过CPU)
常用的零拷贝有 mmap
(内存映射)和 sendFile
。
mmap 和 sendFile 的区别:
- mmap 适合小数据量读写,sendFile 适合大文件传输。
- mmap 需要 4 次上下文切换,3 次数据拷贝;sendFile 需要 3 次上下文切换,最少 2 次数据拷贝。
- sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket缓冲区)。
Java NIO 中零拷贝方式通过 transferTo
方法实现。
3,Netty 中的 IO 模式
Netty 对三种 IO 模式的支付:
- BIO:
不建议使用
- ThreadPerChannelEventLoopGroup
- ThreadPerChannelEventLoop
- OioServerSocketChannel
- OioSocketChannel
- NIO:
- COMMON
- NioEventLoopGroup
- NioEventLoop
- NioServerSocketChannel
- NioSocketChannel
- Linux
- EpollEventLoopGroup
- EpollEventLoop
- EpollServerSocketChannel
- EpollSocketChannel
- 通用的 NIO 实现(Common)在 Linux 下也是使用 epoll,但是 Netty 更好,例如:
- JDK 的 NIO 默认实现是水平触发
- Netty 是边缘触发(默认)和水平触发可切换
- Netty 实现的垃圾回收更少、性能更好
- macOS/BSD
- KQueueEventLoopGroup
- KQueueEventLoop
- KQueueServerSocketChannel
- KQueueSocketChannel
- COMMON
- AIO:
已移除
- AioEventLoopGroup
- AioEventLoop
- AioServerSocketChannel
- AioSocketChannel
为什么删掉已经做好的 AIO 支持?
- Windows 实现成熟,但是很少用来做服务器
- Linux 常用来做服务器,但是 AIO 实现不够成熟
- Linux 下 AIO 相比较 NIO 的性能提升不明显
4,Netty 异步模型
异步的概念和同步相对:
- 当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的组件在完成后,通过状态、通知和回调来通知调用者
- Netty 中的 I/O 操作是异步的,包括
Bind、Write、Connect
等操作会先返回一个 ChannelFuture - 调用者并不能立刻获得结果,而是通过 Future-Listener 机制,用户可以方便的主动获取或者通过通知机制获得 IO 操作结果
ChannelFuture.sync()
,等待异步操作执行完毕
3,Reactor 模式
Reactor 的中文是“反应堆”,其实就是 I/O 多路复用结合线程池。
Reactor 模式的核心组成部分包括 Reactor 和处理资源池(进程池或线程池):
- Reactor 负责监听和分配事件
- 处理资源池负责处理事件
Reactor 的三种实现:
-
单线程,所有的处理都由一个线程完成
-
多线程
-
主从多线程:最高效的模式
Netty Reactor 工作架构图
在 Netty 中使用 Reactor 模式
Reactor 单线程模式:
EventLoopGroup eventGroup = new NioEventLoopGroup(1);
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(eventGroup);
非主从 Reactor 多线程模式:
EventLoopGroup eventGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(eventGroup);
主从 Reactor 多线程模式:
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup);
4,TCP Keepalive
TCP Keepalive 机制一般用在 Server 端,用于确认 Client 是否存活。
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 次)
HTTP 协议中的 Keep-Alive 与 TCP 中的不是一回事, HTTP Keep-Alive 指的是对长连接和短连接的选择:
- Connection : Keep-Alive 长连接(HTTP/1.1 默认长连接,不需要带这个 header)
- Connection : Close 短连接
Netty 中开启 Keepalive 的方法:
// Server 端开启 TCP keepalive 有两种方式
// 第一种方式
bootstrap.childOption(ChannelOption.SO_KEEPALIVE,true)
// 第二种方式
bootstrap.childOption(NioChannelOption.of(StandardSocketOptions.SO_KEEPALIVE), true)
// 注意该方式无效
bootstrap.option(ChannelOption.SO_KEEPALIVE,true)
5,Java 中的锁
锁的分类:
- 对竞争的态度:乐观锁(java.util.concurrent 包中的原子类)与悲观锁(Synchronized)
- 等待锁的人是否公平而言:公平锁 new ReentrantLock (true)与非公平锁 new ReentrantLock ()
- 是否可以共享:共享锁与独享锁:ReadWriteLock ,其读锁是共享锁,其写锁是独享锁
6,Unpooled 类
该类是 Netty 提供的一个专门用来操作缓冲区(ByteBuf
)的工具类。
// 创建一个 10 字节的缓冲区
ByteBuf buffer = Unpooled.buffer(10);
// 创建 ByteBuf
ByteBuf byteBuf = Unpooled.copiedBuffer("hello,world!", Charset.forName("utf-8"));
7,编码与解码
编写网络应用程序时,因为数据在网络中传输的都是二进制字节码数据,在发送数据时就需要编码,接收数据时就需要解码。
codec(编解码器)的组成部分有两个:
- encoder(编码器):把业务数据转换成字节码数据
- decoder(解码器):把字节码数据转换成业务数据
Netty 中提供的编解码器:
StringEncoder/StringDecoder
:对字符串数据进行编解码ObjectEncoder/ObjectDecoder
:对Java对象进行编解码- 底层了使用了 Java 序列化技术
Java 序列化技术存在的问题:
- 无法跨语言
- 序列化后的体积太大,是二进制编码的5倍多
- 序列化性能太低
Protobuf 是 Google 发布的开源项目,全称 Google Protocol Buffers,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。
- 以 message 的方式来管理数据
- 支持跨平台、跨语言
- 高性能,高可靠性
8,Netty 主要组件
Netty 的主要组件有:
EventLoop
Channel
ChannelPipe
ChannelHandler
ChannelFuture
ChannelHandler 充当了处理入站和出站数据的应用程序逻辑的容器。
- 例如,实现 ChannelInboundHandler 接口(或 ChannelInboundHandlerAdapter),就可以接收入站事件和数据,这些数据会被业务逻辑处理。
- 当要给客户端发送响应时,也可以从 ChannelInboundHandler 冲刷数据。业务逻辑通常写在一个或者多个 ChannelInboundHandler 中。
- ChannelOutboundHandler 原理一样,只不过它是用来处理出站数据的
9,TCP 拆包和粘包
TCP 是面向连接的,面向流的,提供高可靠性服务。TCP 发送端为了将多个数据包,更有效的发给接收端,使用了优化方法(Nagle 算法),将多个间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。
这样做虽然提高了效率,但是接收端就难于分辨出完整的数据包了,因为面向流的通信是无消息保护边界的。由于 TCP 无消息保护边界,需要在接收端处理消息边界问题,也就是我们所说的粘包、拆包问题。
TCP 出现粘包和半包现象的原因:
- 粘包的主要原因:
- 发送方每次写入数据 < 套接字缓冲区大小
- 接收方读取套接字缓冲区数据不够及时
- 半包的主要原因:
- 发送方写入数据 > 套接字缓冲区大小
- 发送的数据大于协议的 MTU(最大传输单元),必须拆包
- 根本原因:TCP 是流式协议,消息无边界
- 因此应用层的处理办法是让消息有边界
- 从不同的角度看
- 消息的接收和发送:一次发送可能被多次接收,多次发送可能被一次接收
- 消息的传输:一个发送可能占用多个传输包,多个发送可能公用一个传输包
注意 UDP 协议是有边界的,所以不会出现粘包和半包问题。
1,Netty 处理粘包半包问题 ByteToMessageDecoder
处理办法:
Netty 对三种常用封帧方式的支持(ByteToMessageDecoder
):
- 固定长度方式
- FixedLengthFrameDecoder:解码
- 不内置编码
- 分隔符方式
- DelimiterBasedFrameDecoder:解码
- 不内置编码
- 固定长度字段存个内容的长度信息
- LengthFieldBasedFrameDecoder:解码
- LengthFieldPrepender:编码
它们都是
ByteToMessageDecoder
的子类
2,二次解码器 MessageToMessageDecoder
如果把解决半包粘包问题的常用三种解码器叫一次解码器,一次解码的结 果是字节;还需要和项目中所使用的对象做转化,这层解码器可以称为二次解 码器,对应的编码器是为了将 Java 对象转化成字节流方便存储或传输。
解码器:
- 一次解码器:ByteToMessageDecoder
- io.netty.buffer.ByteBuf (原始数据流)-> io.netty.buffer.ByteBuf (用户数据)
- 二次解码器:MessageToMessageDecoder
- io.netty.buffer.ByteBuf (用户数据)-> Java Object
10,Netty 源码剖析
Netty 中的一些概念:
- channel::就是连接
- eventloop:为连接服务的执行器
- 它是一个死循环(loop)轮训、处理 channel上发生的事件(event)。
- 一个channel只会绑定到一个eventloop,但是一个eventloop一般服务于多个channel
- eventloopgroup: 假设就一个eventloop服务于所有channel,肯定会有瓶颈,所以搞一个组,相当于多线程了
1,启动服务
11,Netty 实例
编写网络应用程序基本步骤:
数据包格式:
消息处理流程:
1,Netty 编程中常见易错点
LengthFieldBasedFrameDecoder
中initialBytesToStrip
未考虑设置ChannelHandler
顺序不正确ChannelHandler
该共享不共享,不该共享却共享- 分配 ByteBuf :分配器直接用
ByteBufAllocator.DEFAULT
等,而不是采用ChannelHandlerContext.alloc()
- 未考虑 ByteBuf 的释放
- 错以为
ChannelHandlerContext.write(msg)
就写出数据了 - 乱用
ChannelHandlerContext.channel().writeAndFlush(msg)
- 应该使用
ChannelHandlerContext.writeAndFlush(msg)
- 应该使用
12,Netty 编程之参数优化
1,tcp_keepalive_time
Linux 系统参数
/proc/sys/net/ipv4/tcp_keepalive_time
2,SO_BACKLOG
serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024);
SocketChannel -> .childOption
ServerSocketChannel -> .option
3,Server 端需要调的参数
serverBootstrap.option(NioChannelOption.SO_BACKLOG, 1024);
serverBootstrap.childOption(NioChannelOption.TCP_NODELAY, true);
4,Client 端需要调的参数
bootstrap.option(NioChannelOption.CONNECT_TIMEOUT_MILLIS, 10 * 1000);
文章作者 @码农加油站
上次更改 2022-02-10