公号:码农充电站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);