网络编程中,特别是自定义协议基于TCP/IP时候,会出现黏包和半包现象,学习nio过程中,目前学习buffer时候,简单模拟下黏包,半包拆包现象。
-
黏包(Packet Sticking)
- 定义:黏包是指发送方(客户端)发送的多个数据包被接收方(服务端)当成一个数据包进行接收的情况。在基于 TCP 协议通信时,由于 TCP 是面向字节流的协议,它没有消息边界的概念。例如,发送方连续发送了两个数据包 “数据包 A” 和 “数据包 B”,接收方可能将它们当作一个整体接收,就好像这两个数据包 “黏” 在了一起。
- 产生原因:TCP 为了提高传输效率,会使用一个缓冲区(发送缓冲区和接收缓冲区)来处理数据。当发送的数据量较小且发送频率较高时,这些小的数据可能会被 TCP 协议栈缓存起来,积累到一定程度后一起发送,从而导致接收方收到的数据出现黏包现象。
- 示例:假设发送方发送了两个消息,消息 1 是 “Hello”,消息 2 是 “World”。如果出现黏包,接收方可能收到 “HelloWorld”,而不是期望的两个独立的消息 “Hello” 和 “World”。
-
半包(Packet Fragmentation)
- 定义:半包是指发送方发送的一个数据包在接收方被分成多个部分接收。这是因为网络环境复杂,数据包在传输过程中可能会受到网络带宽、路由器缓存等因素的影响。
- 产生原因:当数据包的大小超过了网络链路层的最大传输单元(MTU)时,数据包就会在传输过程中被拆分。例如,以太网的 MTU 一般是 1500 字节,如果发送的数据包大小为 2000 字节,那么这个数据包就可能会被分成两个或多个部分进行传输。
- 示例:发送方发送一个长度为 1600 字节的数据包,由于 MTU 限制,接收方可能先收到一个 1500 字节的部分,然后再收到剩下的 100 字节部分。
-
拆包(Packet Unpacking)
- 定义:拆包和半包有相似之处,但拆包更侧重于在接收端从底层接收到的字节流中正确地提取出完整的应用层数据包。它是接收方根据预先定义的协议规则,将接收到的字节流分割成一个个完整的数据包的过程。
- 产生原因:当发送方按照一定的协议格式发送数据,接收方需要按照相同的协议格式来解析数据。在解析过程中,就需要从字节流中把一个个完整的数据包拆分出来。
- 示例:如果协议规定每个数据包以一个特定的字节序列(如
0xAA 0xBB)开头,以另一个字节序列(如0xCC 0xDD)结尾。接收方就需要在收到的字节流中寻找这些标志来进行拆包操作。
public class NioBufferMain {
public static void main(String[] args) {
handleStickPackage();
System.out.println("\n====================");
handleHalfPackage();
}
// 处理黏包现象
private static void handleStickPackage() {
System.out.println("===处理黏包现象===");
ByteBuffer buffer = ByteBuffer.allocate(32);
StringBuilder stickPackageBuilder = new StringBuilder();
// 模拟发送方发送的多个消息
String message1 = "Hello,World\n";
String message2 = "Hi,NIO\n";
String message3 = "Welcome!\n";
// 模拟黏包:将多个消息写入buffer
buffer.put(message1.getBytes());
buffer.put(message2.getBytes());
buffer.put(message3.getBytes());
// 切换到读模式
buffer.flip();
// 模拟服务端按照分隔符进行消息解析
while (buffer.hasRemaining()) {
char c = (char) buffer.get();
if (c == '\n') {
System.out.println("解析出一条消息: " + stickPackageBuilder.toString());
stickPackageBuilder.setLength(0);
} else {
stickPackageBuilder.append(c);
}
}
}
// 处理半包现象
private static void handleHalfPackage() {
System.out.println("===处理半包现象===");
ByteBuffer buffer = ByteBuffer.allocate(32);
StringBuilder halfPackageBuilder = new StringBuilder();
// 第一次接收到不完整消息
String incompleteMsg = "Hello,Wor";
buffer.put(incompleteMsg.getBytes());
buffer.flip();
while (buffer.hasRemaining()) {
halfPackageBuilder.append((char) buffer.get());
}
System.out.println("接收到不完整消息: " + halfPackageBuilder.toString());
// 模拟后续数据到达
buffer.clear();
String remainingMsg = "ld\n";
buffer.put(remainingMsg.getBytes());
buffer.flip();
while (buffer.hasRemaining()) {
char c = (char) buffer.get();
if (c == '\n') {
System.out.println("解析出完整消息: " + halfPackageBuilder.toString());
halfPackageBuilder.setLength(0);
} else {
halfPackageBuilder.append(c);
}
}
}
}