Netty粘包&半包支持&Protobuf
TCP粘包、半包、拆包
客户端发送数据包给服务端,因服务端一次读取到的字节数是不确定的,有好几种情况:
1、服务端分两次读取到了两个独立的数据包,没有粘包和拆包
2、服务端一次接收到了两个数据包,粘合在一起,被称为 TCP 粘包
3、服务端分两次读取到了两个数据包,第一次读取到了完整的包和另外一个包的部分内容,第二次读取到了另一个包的剩余内容,这被称为 TCP 拆包
4、服务端分两次读取到了两个数据包,第一次读取到了包的部分内容,第二次读取到了之前未读完的包剩余内容和另一个包,发生了拆包和粘包;第二次读取的剩余的包称为半包
5、服务端 TCP 接收滑动窗口很小,数据包比较大,即服务端分多次才能将 包接收完全,发生多次拆包
提醒:UDP 像邮寄的包裹,虽然一次运输多个,但每个包裹都有“界限”,一个一个签收, 所以无粘包、半包问题
TCP出现粘包和半包的原因:
根本原因:TCP是流式协议,消息无边界
1、发送角度
粘包:
①:发送方每次写入数据 < 套接字缓冲区大小 (合并发送导致粘包)
②:接收方读取套接字缓冲区数据不够及时
半包:
①:发送方写入数据 > 套接字缓冲区大小
②:发送的数据大于协议的 MTU(Maximum Transmission Unit,最大传输单元),必须拆包(IPv4最大传输单元64KB)
2、接收角度
多个发送可能被一次接收(粘包)
一个发送可能被多次接收(半包)
3、传输角度
多个发送可能公用一个传输包(粘包)
一个发送可能占用多个传输包(半包)
解决问题的根本手段——找出消息的边界:
方式 |
寻找消息边界方式 |
优点 |
缺点 |
推荐度 |
|
封装成帧 (Framing) |
固定长度 |
满足固定长度即可 |
简单 |
空间浪费 |
不推荐 |
分割符 |
分隔符之间 |
空间不浪费,也比较简单 |
内容本身出现分隔符时需转义,所以需要扫描内容 |
推荐 |
|
固定长度字段存每个内容的长度信息 |
先解析固定长度的字段获取长度,然后读取后续内容 |
精确定位用户数据,内容也不用转义 |
长度理论上有限制,需提前预知可能的最大长度从而定义长度占用字节数 |
推荐+ |
|
其他方式 |
每种都不同,例如 JSON 可以看{}是否应 已经成对 |
衡量实际场景,很多是对现有协议的支持 |
Netty对三种常用封帧方式的支持:
方式\支持 |
解码 |
编码 |
|
封装成帧 (Framing) |
固定长度 |
FixedLengthFrameDecoder |
不内置:几乎没人用 |
分割符 |
DelimiterBasedFrameDecoder |
不内置:太简单 |
|
固定长度字段存每个内 |
LengthFieldBasedFrameDecoder | LengthFieldPrepender |
二次编解码
假设我们把上面解决半包粘包问题的常用三种解码器叫一次解码器
我们在项目中,除了可选的的压缩解压缩之外,还需要一层解码(反序列化),因为一次解码的结果是字节,需要和项目中所使用的对象做转化,方便使用,这层解码器可以称为“二次解
码器”,相应的,对应的编码器是为了将 Java 对象转化成字节流方便存储或传输(序列化)
一次解码器:ByteToMessageDecoder
io.netty.buffer.ByteBuf (原始数据流)-> io.netty.buffer.ByteBuf (用户数据)
二次解码器:MessageToMessageDecoder<I>
io.netty.buffer.ByteBuf (用户数据)-> Java Object
常见的“二次”编解码方式
即序列化和反序列化方式
Java序列化、XML,JSON文本序列化、Protobuf
Protobuf
Github:https://github.com/protocolbuffers/protobuf
官网:https://developers.google.com/protocol-buffers/
什么是Protobuf?
Protobuf是Protocol Buffers的简称,它是Google公司开发的一种数据描述语言,用于描述一种轻便高效的结构化数据存储格式
Protobuf是一种与语言无关、与平台无关的可扩展机制,用于序列化结构化数据
Protobuf是 Google 用于序列化结构化数据的语言无关、平台平台、可扩展机制——对比 XML,但更小、更快、更简单。您只需定义一次数据的结构化方式,然后就可以使用特殊生成的源代码轻松地在各种数据流中写入和读取结构化数据,并使用多种语言。
开发者可以通过Protobuf附带的工具生成代码并实现将结构化数据序列化的功能。
简介:
Protobuf是一个灵活的、高效、自动化机制的结构数据序列化方法
Protobuf相比较XML和JSON格式,Protobuf更小、更快、更便捷。比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单
Protobuf是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等
Protobuf自带了编译器(protoc),只需用它进行编译,可以自动生成Java、Python、C++等代码,不需要再写其他代码
简单来讲, ProtoBuf 是结构数据序列化方法,可简单类比于 XML和JSON,其具有以下特点:
序列化:将结构数据或对象转换成能够被存储和传输(例如网络传输)的格式,同时应当要保证这个序列化结果在之后(可能在另一个计算环境中)能够被重建回原来的结构数据或对象
①:语言无关、平台无关。即 ProtoBuf 支持 Java、C++、Python 等多种语言,支持多个平台
②:高效。即比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单
③:扩展性、兼容性好。你可以更新数据结构,而不影响和破坏原有的旧程序
Protobuf使用:
①:定义proto文件
②:生成代码
③:序列化和反序列化
序列化:byte[] bytes=xxx.toByteArray();
反序列化:xxx.parseFrom(bytes);
proto类型对应关系:
.proto Type | Notes | C++ Type | Java Type | Python Type |
Go Type |
---|---|---|---|---|---|
double | double | double | float | *float64 | |
float | float | float | float | *float32 | |
int32 | 使用可变长度编码。对负数进行编码效率低下——如果您的字段可能有负值,请改用 sint32 | int32 | int | int | *int32 |
int64 | 使用可变长度编码。编码负数效率低 - 如果您的字段可能有负值,请改用 sint64 | int64 | long | int/long |
*int64 |
uint32 | 使用可变长度编码 | uint32 | int |
int/long |
*uint32 |
uint64 | 使用可变长度编码 | uint64 | long |
int/long |
*uint64 |
sint32 | 使用可变长度编码。带符号的 int 值。这些比常规 int32 更有效地编码负数. | int32 | int | int | *int32 |
sint64 | 使用可变长度编码。带符号的 int 值。这些比常规 int64 更有效地编码负数 | int64 | long | int/long |
*int64 |
fixed32 | 总是4个字节。如果值通常大于 2^28,则比 uint32 更有效. | uint32 | int |
int/long |
*uint32 |
fixed64 | 总是8个字节。如果值通常大于 2^56,则比 uint64 更有效 | uint64 | long |
int/long |
*uint64 |
sfixed32 | 总是4个字节 | int32 | int | int | *int32 |
sfixed64 | 总是8个字节 | int64 | long | int/long |
*int64 |
bool | bool | boolean | bool | *bool | |
string | 字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本 | string | String | unicode (Python 2) or str (Python 3) | *string |
bytes |
可能包含任意字节序列 |
string | ByteString | bytes | []byte |
定义proto格式文件:
①:为要序列化的每个数据结构添加一条message
②:然后为message中的每个字段指定名称和类型
③:编译Proto文件
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/xxx.proto
如:
syntax = "proto3"; message SearchRequest { string query = 1; int32 page_number = 2; int32 result_per_page = 3; }
字段修饰符
每个字段都必须使用以下修饰符之一进行注释:
①:optional: 该字段可以设置也可以不设置。如果未设置可选字段值,则使用默认值。对于简单类型,您可以指定自己的默认值,就像我们在示例中为电话号码所做的那样type。否则,使用系统默认值:数字类型为零,字符串为空字符串,布尔值为 false。对于嵌入式消息,默认值始终是消息的“默认实例”或“原型”,没有设置任何字段。调用访问器以获取未显式设置的可选(或必需)字段的值始终返回该字段的默认值。
②:repeated:该字段可以重复任意次数(包括零次)。重复值的顺序将保存在Protobuf中。将重复字段视为动态大小的数组。
③:required:必须提供该字段的值,否则该消息将被视为“未初始化”。尝试构建未初始化的消息将抛出RuntimeException. 解析未初始化的消息将抛出IOException. 除此之外,必填字段的行为与可选字段完全相同(proto3不支持此字段)
Netty中对protobuf使用和支持
示例:
public class WorldClockClientInitializer extends ChannelInitializer<SocketChannel> {
private final SslContext sslCtx;
public WorldClockClientInitializer(SslContext sslCtx) {
this.sslCtx = sslCtx;
}
@Override
public void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
if (sslCtx != null) {
p.addLast(sslCtx.newHandler(ch.alloc(), WorldClockClient.HOST, WorldClockClient.PORT));
}
// 一对解码器
p.addLast(new ProtobufVarint32FrameDecoder());
p.addLast(new ProtobufDecoder(WorldClockProtocol.LocalTimes.getDefaultInstance()));
// 一对编码器
p.addLast(new ProtobufVarint32LengthFieldPrepender());
p.addLast(new ProtobufEncoder());
p.addLast(new WorldClockClientHandler());
}
}
附录:
Netty源码:https://github.com/netty/netty
END.