3

Netty学习笔记

 2 years ago
source link: https://codeshellme.github.io/2022/02/netty-learn/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

公号:码农充电站pro

主页:https://codeshellme.github.io

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

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
  • 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 编程中常见易错点

  • LengthFieldBasedFrameDecoderinitialBytesToStrip 未考虑设置
  • 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);

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK