rookie-Netty

1. 概述

1.1. 什么是Netty

1
2
Netty is an asynchronous event-driven network application framework
for rapid development of maintainable high performance protocol servers & clients.Copy

Netty 是一个异步的、基于事件驱动的网络应用框架,用于快速开发可维护、高性能的网络服务器和客户端

注意netty的异步还是基于多路复用的,并没有实现真正意义上的异步IO

1.2. Netty的优势

如果使用传统NIO,其工作量大,bug 多

  • 需要自己构建协议

  • 解决 TCP 传输问题,如粘包、半包

  • 因为bug的存在,epoll 空轮询导致 CPU 100%

Netty 对 API 进行增强,使之更易用,如

  • FastThreadLocal -> ThreadLocal

  • ByteBuf -> ByteBuffer

1.3. Netty 的地位

Netty 在 Java 网络应用框架中的地位就好比:Spring 框架在 JavaEE 开发中的地位

以下的框架都使用了 Netty,因为它们有网络通信需求!

  • Cassandra - nosql 数据库

  • Spark - 大数据分布式计算框架

  • Hadoop - 大数据分布式存储框架

  • RocketMQ - ali 开源的消息队列

  • ElasticSearch - 搜索引擎

  • gRPC - rpc 框架

  • Dubbo - rpc 框架

  • Spring 5.x - flux api 完全抛弃了 tomcat ,使用 netty 作为服务器端

  • Zookeeper - 分布式协调框架

2. 入门案例

2.1. 服务器端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class HelloServer {
public static void main(String[] args) {
// 1. 启动器 负责装配netty组件 启动服务器
new ServerBootstrap()
// 2. 创建 NioEventLoopGroup 可以简单理解为 线程池 + Selector
.group(new NioEventLoopGroup())
// 3. 选择 服务器的 ServerSocketChannel 实现
.channel(NioServerSocketChannel.class)
// 4. child 负责处理读写 该方法决定了 child 执行哪些操作
// ChannelInitializer 处理器(仅执行一次)
// 它的作用是待客户端 SocketChannel 建立连接后 执行 initChannel 以便添加更多的处理器
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
// 5. SocketChannel 的处理器 使用 StringDecoder 解码 ByteBuf -> String
ch.pipeline().addLast(new StringDecoder());
// 6. SocketChannel 的业务处理 使用上一个处理器的处理结果
ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println(msg);
}
});
}
// 7. ServerSocketChannel 绑定 8080 端口
}).bind(8080);
}
}

2.2. 客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class HelloClient {
public static void main(String[] args) throws InterruptedException {
new Bootstrap()
.group(new NioEventLoopGroup())
// 1. 选择 客户端 Socket 实现类 NioSocketChannel 表示 基于 NIO 的客户端实现
.channel(NioSocketChannel.class)
// 2. ChannelInitializer 处理器(仅执行一次)
// 它的作用是在客户端 SocketChannel 建立连接之后 执行 initChannel 添加更多的处理器
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
// 3. 消息会经过管道中的 handler 处理 这里是将 String -> ByteBuf 编码发出
ch.pipeline().addLast(new StringEncoder());
}
// 4. 指定要连接的服务器和端口
}).connect(new InetSocketAddress("127.0.0.1", 8080))
// 5. Netty 中很多方法都是异步的 如 connect
// 这时需要使用 sync 方法等待 connect 建立连接完毕
.sync()
// 6. 获取 channel 对象 它即为通道抽象 可以进行数据读写操作
.channel()
// 7. 写入消息并清空缓冲区
.writeAndFlush("hey!");
}
}

2.3. 组件解释

  • channel 可以理解为数据的通道

  • msg 理解为流动的数据,最开始输入是 ByteBuf,经过 pipeline 中的各个 handler 加工,会变成其它类型对象,最后输出又变成 ByteBuf

  • handler 可以理解为数据的处理工序

    • 工序有多道,合在一起就是 pipeline(传递途径),pipeline 负责发布事件(读、读取完成…)传播给每个 handler,handler 对自己感兴趣的事件进行处理(重写了相应事件处理方法)

      • pipeline 中有多个 handler,处理时会依次调用其中的 handler
    • handler 分 Inbound 和 Outbound 两类

      • Inbound 入站

      • Outbound 出站

  • eventLoop 可以理解为处理数据的工人

    • eventLoop 可以管理多个 channel 的 io 操作,并一旦 eventLoop 负责了某个 channel,就会将其与channel进行绑定,以后该 channel 中的 io 操作都由该 eventLoop 负责

    • eventLoop 既可以执行 io 操作,也可以进行任务处理,每个 eventLoop 有自己的任务队列,队列里可以堆放多个 channel 的待处理任务,任务分为普通任务、定时任务

    • eventLoop 按照 pipeline 顺序,依次按照 handler 的规划(代码)处理数据,可以为每个 handler 指定不同的 eventLoop

3. 粘包与半包

3.1. 现象分析

3.1.1. 粘包

  • 现象,发送 abc def,接收 abcdef

  • 原因

    • 应用层:接收方 ByteBuf 设置太大(Netty 默认 1024)

    • 滑动窗口:假设发送方 256 bytes 表示一个完整报文,但由于接收方处理不及时且窗口大小足够大,这 256 bytes 字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包

    • Nagle 算法:会造成粘包

3.1.2. 半包

  • 现象,发送 abcdef,接收 abc def

  • 原因

    • 应用层:接收方 ByteBuf 小于实际发送数据量

    • 滑动窗口:假设接收方的窗口只剩了 128 bytes,发送方的报文大小是 256 bytes,这时放不下了,只能先发送前 128 bytes,等待 ack 后才能发送剩余部分,这就造成了半包

    • MSS 限制:当发送的数据超过 MSS 限制后,会将数据切分发送,就会造成半包

本质是因为 TCP 是流式协议,消息无边界

3.2. 原理分析

3.2.1. 滑动窗口

  • TCP 以一个段(segment)为单位,每发送一个段就需要进行一次确认应答(ack)处理,但如果这么做,缺点是包的往返时间越长性能就越差

    0049

  • 为了解决此问题,引入了窗口概念,窗口大小即决定了无需等待应答而可以继续发送的数据最大值

0051

  • 窗口实际就起到一个缓冲区的作用,同时也能起到流量控制的作用

    • 图中深色的部分即要发送的数据,高亮的部分即窗口

    • 窗口内的数据才允许被发送,当应答未到达前,窗口必须停止滑动

    • 如果 1001~2000 这个段的数据 ack 回来了,窗口就可以向前滑动

    • 接收方也会维护一个窗口,只有落在窗口内的数据才能允许接收

3.2.2. MSS 限制

  • 链路层对一次能够发送的最大数据有限制,这个限制称之为 MTU(maximum transmission unit),不同的链路设备的 MTU 值也有所不同,例如

  • 以太网的 MTU 是 1500

  • FDDI(光纤分布式数据接口)的 MTU 是 4352

  • 本地回环地址的 MTU 是 65535 - 本地测试不走网卡

  • MSS 是最大段长度(maximum segment size),它是 MTU 刨去 TCP 头和 IP 头后剩余能够作为数据传输的字节数

  • ipv4 TCP 头占用 20 bytes,IP 头占用 20 bytes,因此以太网 MSS 的值为 1500 - 40 = 1460

  • TCP 在传递大量数据时,会按照 MSS 大小将数据进行分割发送

  • MSS 的值在三次握手时通知对方自己 MSS 的值,然后在两者之间选择一个小值作为 MSS

3.2.3. Nagle 算法

  • 即使发送一个字节,也需要加入 TCP 头和 IP 头,也就是总字节数会使用 41 bytes,非常不经济。因此为了提高网络利用率,TCP 希望尽可能发送足够大的数据,这就是 Nagle 算法产生的缘由

  • 该算法是指发送端即使还有应该发送的数据,但如果这部分数据很少的话,则进行延迟发送

    • 如果 SO_SNDBUF 的数据达到 MSS,则需要发送

    • 如果 SO_SNDBUF 中含有 FIN(表示需要连接关闭)这时将剩余数据发送,再关闭

    • 如果 TCP_NODELAY = true,则需要发送

    • 已发送的数据都收到 ack 时,则需要发送

    • 上述条件不满足,但发生超时(一般为 200ms)则需要发送

    • 除上述情况,延迟发送

3.3. 解决方案

  1. 短链接,发一个包建立一次连接,这样连接建立到连接断开之间就是消息的边界,缺点效率太低

  2. 每一条消息采用固定长度,缺点浪费空间

  3. 每一条消息采用分隔符,例如 \n,缺点需要转义

  4. 每一条消息分为 head 和 body,head 中包含 body 的长度


rookie-Netty
https://arloyee.github.io/2023/09/01/rookie-Netty/
作者
YaYee
发布于
2023年9月1日
许可协议