言成言成啊 | Kit Chen's Blog

Netty大白进阶教程

发布于2023-04-29 10:54:07,更新于2023-05-03 22:14:09,标签:java netty  文章会持续修订,转载请注明来源地址:https://meethigher.top/blog

部分基础概念,参考网络编程NIO

一、粘包与拆包

1.1 概念

粘包

  • 发送方容量<接收方容量

  • 现象,发送两条消息abcdef,接收到一条消息 abcdef

  • 导致原因

    • 应用层:接收方 ByteBuf 设置太大(Netty 默认 1024)
    • 滑动窗口:假设发送方 256 bytes 表示一个完整报文,但由于接收方处理不及时且窗口大小足够大,这 256 bytes 字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包
    • Nagle 算法:会造成粘包

拆包

  • 发送方容量>接收方容量

  • 现象,发送一条消息abcdef,接收到两条消息 abcdef

  • 导致原因

    • 应用层:接收方 ByteBuf 小于实际发送数据量
    • 滑动窗口:假设接收方的窗口只剩了 128 bytes,发送方的报文大小是 256 bytes,这时放不下了,只能先发送前 128 bytes,等待 ack 后才能发送剩余部分,这就造成了拆包
    • MSS 限制:当发送的数据超过 MSS 限制后,会将数据切分发送,就会造成拆包

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

1.2 问题复现

写个demo,客户端每次向服务端发送16字节,但是服务端会直接收到了160字节,这种现象就是粘包

服务端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class HelloWorldServer {

public static void main(String[] args) throws Exception {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();

ChannelFuture future = new ServerBootstrap()
.channel(NioServerSocketChannel.class)
.group(boss, worker)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new LoggingHandler(LogLevel.INFO));
}
})
.bind(8080)
.sync();
//同步阻塞等待关闭。close()会触发关闭
future.channel().closeFuture().sync();
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}

客户端代码

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
29
30
31
public class HelloWorldClient {
public static void main(String[] args) throws Exception {
NioEventLoopGroup worker = new NioEventLoopGroup();
new Bootstrap()
.channel(NioSocketChannel.class)
.group(worker)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new LoggingHandler(LogLevel.INFO))
.addLast(new ChannelInboundHandlerAdapter() {
//channel建立连接成功后,会触发active事件
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i = 0; i < 10; i++) {
ByteBuf buf = ctx.alloc().buffer(16);
buf.writeBytes(new byte[]{
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16
});
ctx.writeAndFlush(buf);
}
}
});
}
})
.connect("127.0.0.1", 8080)
.sync();
worker.shutdownGracefully();
}
}

验证拆包,只需要在原有代码,添加SO_RCVBUF参数

1
2
3
4
5
6
7
8
9
10
11
12
13
ChannelFuture future = new ServerBootstrap()
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_RCVBUF, 10)//控制套接字接收缓冲区的大小
.group(boss, worker)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new LoggingHandler(LogLevel.INFO));
}
})
.bind(8080)
.sync();

客户端保持不变,一次消息16个字节,但是收到4个字节。这时候就出现了拆包现象。

1.2 产生原因

1.2.1 滑动窗口

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

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

流量控制

窗口实际就起到一个缓冲区的作用

  • 窗口内的数据才会允许被发送,当应答未到达前,窗口必须停止滑动
  • 如果窗口内的数据 ack(确认应答) 回来了,窗口就可以向前滑动
  • 发送方/接收方都会维护一个窗口,只有落在窗口内的数据才能允许发送/接收

同时,窗口又能起到流量控制的作用:发送方的发送频率不要过快,要让接收方来得及接收。

rwnd表示receiver window,表示接收窗口,TCP的窗口单位是字节。

解决死锁-持续计时器

死锁问题的呈现

  1. B给A发送零窗口通知,此时A就不再发送数据了。
  2. B给A发送非零窗口通知,然而这个通知在传输过程中丢失了。
  3. 这时就会出现死锁现象:A等B的非零窗口通知,B等A的发送消息。

那么TCP如何解决死锁呢?

TCP为每个连接设有一个持续计时器,只要收到对方的零窗口通知,就启动该持续计时器。

  1. 持续计时器到期,A向B获取现在的窗口值
  2. 若窗口仍然是零或者没收到,计时器仍然执行。
  3. 若窗口不是零,移除计时器,死锁问题解决。

1.2.2 Nagle算法

Nagle 算法

  • 即使发送一个字节,也需要加入 tcp 头和 ip 头(两者通常均为20bytes),也就是总字节数会使用 41 bytes,非常不经济。因此为了提高网络利用率,tcp 希望尽可能发送足够大的数据,这就是 Nagle 算法产生的缘由
  • 该算法是指发送端即使还有应该发送的数据,但如果这部分数据很少的话,则进行延迟发送
    • 如果 SO_SNDBUF 的数据达到 MSS,则需要发送
    • 如果 SO_SNDBUF 中含有 FIN(表示需要连接关闭)这时将剩余数据发送,再关闭
    • 如果 TCP_NODELAY = true,则需要发送
    • 如果已发送的数据都收到 ack 时,则需要发送
    • 上述条件不满足,但发生超时(一般为 200ms)则需要发送
    • 除上述情况,延迟发送

1.2.3 MSS限制

MSS(Maximum Segment Size)是TCP协议中一个重要的参数,它表示在一个TCP段(即TCP协议中的数据包)中所能承载的最大数据量。MSS的大小通常由网络中最小的MTU(Maximum Transmission Unit,即最大传输单元)和TCP头部的大小来决定,具体计算公式为MSS = MTU - TCP头部大小。

具体来说,当发送端发送一个TCP段时,它会根据自己的MSS大小将数据分割为多个TCP段进行传输。接收端在接收这些TCP段时,如果其MSS大小小于发送端的MSS大小,则接收端需要将接收到的TCP段重新分割为更小的TCP段进行传输,这会导致传输效率降低。

为了解决这个问题,TCP协议中提供了一种MSS限制机制。具体来说,当建立一个TCP连接时,双方会通过TCP选项交换各自的MSS大小。其实就是木桶短板理论,以小的为准,从而提高TCP连接的传输效率。

需要注意的是,MSS限制只会影响TCP连接的数据传输,而不会影响TCP连接的建立和关闭过程。

MSS 限制

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

    • 以太网的 MTU 是 1500
    • FDDI(光纤分布式数据接口)的 MTU 是 4352
    • 本地回环地址的 MTU 是 65535(本地测试不走网卡)
  • MSS = MTU - TCP头部大小

    • ipv4 tcp 头占用 20 bytes,ip 头占用 20 bytes,因此以太网 MSS 的值为 1500 - 40 = 1460
    • TCP 在传递大量数据时,会按照 MSS 大小将数据进行分割发送
    • MSS 的值在三次握手时通知对方自己 MSS 的值,然后在两者之间选择一个小值作为 MSS

1.3 解决方案

1.3.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
29
30
31
32
33
34
35
36
37
public class HelloWorldClient {
public static void main(String[] args) throws Exception {
for (int i = 0; i < 10; i++) {
send();
}
}

private static void send() throws Exception {
NioEventLoopGroup worker = new NioEventLoopGroup();
new Bootstrap()
.channel(NioSocketChannel.class)
.group(worker)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new LoggingHandler(LogLevel.INFO))
.addLast(new ChannelInboundHandlerAdapter() {
//channel建立连接成功后,会触发active事件
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf buf = ctx.alloc().buffer(16);
buf.writeBytes(new byte[]{
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16
});
ctx.writeAndFlush(buf);
//关闭连接
ctx.channel().close();
}
});
}
})
.connect("127.0.0.1", 8080)
.sync();
worker.shutdownGracefully();
}
}

服务端代码修改option配置即可

1
2
3
4
5
6
//option针对全局
//调整系统的接收缓冲区
.option(ChannelOption.SO_RCVBUF, 10)//控制套接字接收缓冲区的大小
//childOption针对单个连接
//调整netty的bytebuf接收缓冲区
.childOption(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(16, 16, 16))

短连接的做法,可以避免粘包,但拆包问题依然存在

  • 当发送方容量<接收方容量时,也就解决了粘包问题。因为我一个连接发完一条消息就结束了,所以只要接收方容量过大,就不会发生粘包。
  • 当发送方容量>接收方容量时,拆包问题,仍然存在。

1.3.2 定长

1.3.3 固定分隔符

1.3.4 预设长度

二、协议设计与解析

三、聊天室案例

发布:2023-04-29 10:54:07
修改:2023-05-03 22:14:09
链接:https://meethigher.top/blog/2023/netty02/
标签:java netty 
付款码 打赏 分享
Shift+Ctrl+1 可控制工具栏