当前位置:网站首页>Interview Blitz: What Are Sticky Packs and Half Packs?How to deal with it?
Interview Blitz: What Are Sticky Packs and Half Packs?How to deal with it?
2022-08-03 19:12:00 【Shum technology curtilage】
粘包和半包问题是数据传输中比较常见的问题,所谓的粘包问题是指数据在传输时,在一条消息中读取到了另一条消息的部分数据,这种现象就叫做粘包.比如发送了两条消息,分别为“ABC”和“DEF”,那么正常情况下接收端也应该收到两条消息“ABC”和“DEF”,但接收端却收到的是“ABCD”,像这种情况就叫做粘包,如下图所示:
半包问题是指接收端只收到了部分数据,而非完整的数据的情况就叫做半包.比如发送了一条消息是“ABC”,而接收端却收到的是“AB”和“C”两条信息,这种情况就叫做半包,如下图所示:
PS:大部分情况下我们都把粘包问题和半包问题看成同一个问题,所以下文就用“粘包”问题来替代“粘包”和“半包”问题.
1.为什么会有粘包问题?
粘包问题发生在 TCP/IP 协议中,因为 TCP 是面向连接的传输协议,它是以“流”的形式传输数据的,而“流”数据是没有明确的开始和结尾边界的,所以就会出现粘包问题.
2.粘包问题代码演示
接下来我们用代码来演示一下粘包和半包问题,为了演示的直观性,我会设置两个角色:
服务器端用来接收消息;
客户端用来发送一段固定的消息.
然后通过打印服务器端接收到的信息来观察粘包问题.服务器端代码实现如下:
/**
* 服务器端(只负责接收消息)
*/
class ServSocket {
// 字节数组的长度
private static final int BYTE_LENGTH = 20;
public static void main(String[] args) throws IOException {
// 创建 Socket 服务器
ServerSocket serverSocket = new ServerSocket(8888);
// 获取客户端连接
Socket clientSocket = serverSocket.accept();
// 得到客户端发送的流对象
try (InputStream inputStream = clientSocket.getInputStream()) {
while (true) {
// 循环获取客户端发送的信息
byte[] bytes = new byte[BYTE_LENGTH];
// 读取客户端发送的信息
int count = inputStream.read(bytes, 0, BYTE_LENGTH);
if (count > 0) {
// 成功接收到有效消息并打印
System.out.println("接收到客户端的信息是:" + new String(bytes));
}
count = 0;
}
}
}
}
客户端实现代码如下:
/**
* 客户端(只负责发送消息)
*/
static class ClientSocket {
public static void main(String[] args) throws IOException {
// 创建 Socket 客户端并尝试连接服务器端
Socket socket = new Socket("127.0.0.1", 8888);
// 发送的消息内容
final String message = "Hi,Java.";
// 使用输出流发送消息
try (OutputStream outputStream = socket.getOutputStream()) {
// 给服务器端发送 10 次消息
for (int i = 0; i < 10; i++) {
// 发送消息
outputStream.write(message.getBytes());
}
}
}
}
以上程序的执行结果如下图所示:
通过上述结果我们可以看出,服务器端发生了粘包问题,因为客户端发送了 10 次固定的“Hi,Java.”的消息,正确的结果应该是服务器端也接收到了 10 次固定消息“Hi,Java.”才对,但实际执行结果并非如此.
3.解决方案
粘包问题的常见解决方案有以下 3 种:
发送方和接收方固定发送数据的大小,当字符长度不够时用空字符弥补,有了固定大小之后就知道每条消息的具体边界了,这样就没有粘包的问题了;
在 TCP 协议的基础上封装一层自定义数据协议,在自定义数据协议中,包含数据头(存储数据的大小)和 数据的具体内容,这样服务端得到数据之后,通过解析数据头就可以知道数据的具体长度了,也就没有粘包的问题了;
以特殊的字符结尾,比如以“\n”结尾,这样我们就知道数据的具体边界了,从而避免了粘包问题(推荐方案).
解决方案1:固定数据大小
收、发固定大小的数据,服务器端的实现代码如下:
/** * 服务器端,改进版本一(只负责接收消息) */ static class ServSocketV1 { private static final int BYTE_LENGTH = 1024; // 字节数组长度(收消息用) public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(9091); // 获取到连接 Socket clientSocket = serverSocket.accept(); try (InputStream inputStream = clientSocket.getInputStream()) { while (true) { byte[] bytes = new byte[BYTE_LENGTH]; // 读取客户端发送的信息 int count = inputStream.read(bytes, 0, BYTE_LENGTH); if (count > 0) { // 接收到消息打印 System.out.println("接收到客户端的信息是:" + new String(bytes).trim()); } count = 0; } } } }
客户端的实现代码如下:
/** * 客户端,改进版一(只负责接收消息) */ static class ClientSocketV1 { private static final int BYTE_LENGTH = 1024; // 字节长度 public static void main(String[] args) throws IOException { Socket socket = new Socket("127.0.0.1", 9091); final String message = "Hi,Java."; // 发送消息 try (OutputStream outputStream = socket.getOutputStream()) { // 将数据组装成定长字节数组 byte[] bytes = new byte[BYTE_LENGTH]; int idx = 0; for (byte b : message.getBytes()) { bytes[idx] = b; idx++; } // 给服务器端发送 10 次消息 for (int i = 0; i < 10; i++) { outputStream.write(bytes, 0, BYTE_LENGTH); } } } }
以上代码的执行结果如下图所示:
优缺点分析
从以上代码可以看出,虽然这种方式可以解决粘包问题,但这种固定数据大小的传输方式,当数据量比较小时会使用空字符来填充,所以会额外的增加网络传输的负担,因此不是理想的解决方案.
解决方案2:自定义请求协议
这种解决方案的实现思路是将请求的数据封装为两部分:消息头(发送的数据大小)+消息体(发送的具体数据),它的格式如下图所示:
此解决方案的实现分为以下 3 部分:
编写一个消息封装类
编写客户端
编写服务器端
接下来我们一一来实现.
① 消息封装类
消息的封装类中提供了两个方法:一个是将消息转换成消息头 + 消息体的方法,另一个是读取消息头的方法,具体实现代码如下:
/**
* 消息封装类
*/
class SocketPacket {
// 消息头存储的长度(占 8 字节)
static final int HEAD_SIZE = 8;
/**
* 将协议封装为:协议头 + 协议体
* @param context 消息体(String 类型)
* @return byte[]
*/
public byte[] toBytes(String context) {
// 协议体 byte 数组
byte[] bodyByte = context.getBytes();
int bodyByteLength = bodyByte.length;
// 最终封装对象
byte[] result = new byte[HEAD_SIZE + bodyByteLength];
// 借助 NumberFormat 将 int 转换为 byte[]
NumberFormat numberFormat = NumberFormat.getNumberInstance();
numberFormat.setMinimumIntegerDigits(HEAD_SIZE);
numberFormat.setGroupingUsed(false);
// 协议头 byte 数组
byte[] headByte = numberFormat.format(bodyByteLength).getBytes();
// 封装协议头
System.arraycopy(headByte, 0, result, 0, HEAD_SIZE);
// 封装协议体
System.arraycopy(bodyByte, 0, result, HEAD_SIZE, bodyByteLength);
return result;
}
/**
* 获取消息头的内容(也就是消息体的长度)
* @param inputStream
* @return
*/
public int getHeader(InputStream inputStream) throws IOException {
int result = 0;
byte[] bytes = new byte[HEAD_SIZE];
inputStream.read(bytes, 0, HEAD_SIZE);
// 得到消息体的字节长度
result = Integer.valueOf(new String(bytes));
return result;
}
}
② 客户端
客户端中我们添加一组待发送的消息,随机给服务器端发送一个消息,实现代码如下:
/**
* 客户端
*/
class MySocketClient {
public static void main(String[] args) throws IOException {
// 启动 Socket 并尝试连接服务器
Socket socket = new Socket("127.0.0.1", 9093);
// 发送消息合集(随机发送一条消息)
final String[] message = {"Hi,Java.", "Hi,SQL~", "关注公众号|Java中文社群."};
// 创建协议封装对象
SocketPacket socketPacket = new SocketPacket();
try (OutputStream outputStream = socket.getOutputStream()) {
// 给服务器端发送 10 次消息
for (int i = 0; i < 10; i++) {
// 随机发送一条消息
String msg = message[new Random().nextInt(message.length)];
// 将内容封装为:协议头+协议体
byte[] bytes = socketPacket.toBytes(msg);
// 发送消息
outputStream.write(bytes, 0, bytes.length);
outputStream.flush();
}
}
}
}
③ 服务器端
服务器端使用线程池来处理每个客户端的业务请求,实现代码如下:
/**
* 服务器端
*/
class MySocketServer {
public static void main(String[] args) throws IOException {
// 创建 Socket 服务器端
ServerSocket serverSocket = new ServerSocket(9093);
// 获取客户端连接
Socket clientSocket = serverSocket.accept();
// 使用线程池处理更多的客户端
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(100, 150, 100,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
threadPool.submit(() -> {
// 客户端消息处理
processMessage(clientSocket);
});
}
/**
* 客户端消息处理
* @param clientSocket
*/
private static void processMessage(Socket clientSocket) {
// Socket 封装对象
SocketPacket socketPacket = new SocketPacket();
// 获取客户端发送的消息对象
try (InputStream inputStream = clientSocket.getInputStream()) {
while (true) {
// 获取消息头(也就是消息体的长度)
int bodyLength = socketPacket.getHeader(inputStream);
// 消息体 byte 数组
byte[] bodyByte = new byte[bodyLength];
// 每次实际读取字节数
int readCount = 0;
// 消息体赋值下标
int bodyIndex = 0;
// 循环接收消息头中定义的长度
while (bodyIndex <= (bodyLength - 1) &&
(readCount = inputStream.read(bodyByte, bodyIndex, bodyLength)) != -1) {
bodyIndex += readCount;
}
bodyIndex = 0;
// 成功接收到客户端的消息并打印
System.out.println("接收到客户端的信息:" + new String(bodyByte));
}
} catch (IOException ioException) {
System.out.println(ioException.getMessage());
}
}
}
以上程序的执行结果如下:
从上述结果可以看出,消息通讯正常,客户端和服务器端的交互中并没有出现粘包问题.
优缺点分析
此解决方案虽然可以解决粘包问题,但消息的设计和代码的实现复杂度比较高,所以也不是理想的解决方案.
解决方案3:特殊字符结尾
以特殊字符结尾就可以知道流的边界了,它的具体实现是:使用 Java 中自带的 BufferedReader
和 BufferedWriter
,也就是带缓冲区的输入字符流和输出字符流,通过写入的时候加上 \n
来结尾,读取的时候使用 readLine
按行来读取数据,这样就知道流的边界了,从而解决了粘包的问题.服务器端实现代码如下:
/**
* 服务器端,改进版三(只负责收消息)
*/
static class ServSocketV3 {
public static void main(String[] args) throws IOException {
// 创建 Socket 服务器端
ServerSocket serverSocket = new ServerSocket(9092);
// 获取客户端连接
Socket clientSocket = serverSocket.accept();
// 使用线程池处理更多的客户端
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(100, 150, 100,
TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
threadPool.submit(() -> {
// 消息处理
processMessage(clientSocket);
});
}
/**
* 消息处理
* @param clientSocket
*/
private static void processMessage(Socket clientSocket) {
// 获取客户端发送的消息流对象
try (BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()))) {
while (true) {
// 按行读取客户端发送的消息
String msg = bufferedReader.readLine();
if (msg != null) {
// 成功接收到客户端的消息并打印
System.out.println("接收到客户端的信息:" + msg);
}
}
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
PS:上述代码使用了线程池来解决多个客户端同时访问服务器端的问题,从而实现了一对多的服务器响应.
客户端的实现代码如下:
/**
* 客户端,改进版三(只负责发送消息)
*/
static class ClientSocketV3 {
public static void main(String[] args) throws IOException {
// 启动 Socket 并尝试连接服务器
Socket socket = new Socket("127.0.0.1", 9092);
final String message = "Hi,Java."; // 发送消息
try (BufferedWriter bufferedWriter = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream()))) {
// 给服务器端发送 10 次消息
for (int i = 0; i < 10; i++) {
// 注意:结尾的 \n 不能省略,它表示按行写入
bufferedWriter.write(message + "\n");
// 刷新缓冲区(此步骤不能省略)
bufferedWriter.flush();
}
}
}
}
以上代码的执行结果如下图所示:
优缺点分析
以特殊符号作为粘包的解决方案的最大优点是实现简单,但存在一定的局限性,比如当一条消息中间如果出现了结束符就会造成半包的问题,所以如果是复杂的字符串要对内容进行编码和解码处理,这样才能保证结束符的正确性.
总结
粘包和半包问题是数据传输中比较常见的问题,它的解决方案有很多,比较常见的解决方案有:设置固定的数据传输大小、自定义请求协议的封装,在请求头中加入传输数据的长度、使用特殊符号作为结束符等.
边栏推荐
- 力扣刷题之移动零
- idea——同一项目开启多个实例(不同端口)
- 京东云发布新一代分布式数据库StarDB 5.0
- OneNote 教程,如何在 OneNote 中设置页面格式?
- ScrollView嵌套RV,滑动有阻力不顺滑怎么办?
- Postgresql source code (64) Query execution - data structure and execution process before submodule Executor (2) execution
- 阿里巴巴政委体系-第七章、阿里政委培育
- MVC vs MVP
- InnoDB 中不同SQL语句设置的锁
- [Dataset][VOC] Rat dataset voc format 3001 sheets
猜你喜欢
随机推荐
Postgresql snapshot optimization Globalvis new system analysis (performance greatly enhanced)
软件测试回归案例,什么是回归测试?
Postgresql快照优化Globalvis新体系分析(性能大幅增强)
MySQL超详细安装教程 手把手教你安装MySQL到使用MySQL 最简单的MySQL安装方式,这种方式装,卸载也简单
微信小程序分享功能
多线程和并发编程(四)
MySQL基础
首届MogDB征文活动开启啦!
要想成为黑客,离不开这十大基础知识
WEB 渗透之CSRF
力扣刷题之合并两个有序数组
Postgresql-xl global snapshot and GTM code walking (branch line)
Postgresql source code (64) Query execution - data structure and execution process before submodule Executor (2) execution
Climbing Stairs (7/30)
MySQL【变量、流程控制与游标】
普通用户如何利用小红书赚钱呢?小红书的流量是真的吗?
阿里巴巴政委体系-第五章、阿里政委体系建设
阿里二面:多线程间的通信方式有几种?举例说明
学弟:我适不适合转行做软件测试?
Force is brushed buckle problem for the sum of two Numbers