当前位置:网站首页>面试突击:什么是粘包和半包?怎么解决?
面试突击:什么是粘包和半包?怎么解决?
2022-08-03 16:31:00 【InfoQ】


1.为什么会有粘包问题?
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());
}
}
}
}

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:特殊字符结尾
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();
}
}
}
/**
* 客户端,改进版三(只负责发送消息)
*/
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();
}
}
}
}

优缺点分析
总结
边栏推荐
- WordPress 5.2.3 更新,升级出现请求超时的解决方法
- 20. Valid Parentheses
- 虹科分享 | 如何测试与验证复杂的FPGA设计(3)——硬件测试
- 使用Stream多年,collect还有这些“骚操作”?
- 如何在 DataWorks 中 写SQL语句监控数据的变化到达一定的值 进行提示
- Yuan xiaolin: Volvo focus on travel security, and put it perfectly
- Kubernetes 笔记 / 目录
- 《社会企业开展应聘文职人员培训规范》团体标准在新华书店上架
- Windows 事件查看器记录到 MYSQL
- DAYU200 OpenHarmony标准系统HDMI全屏显示
猜你喜欢
leetcode:202. 快乐数
如何使用MATLAB绘制极坐标堆叠柱状图
元宇宙系列--Value creation in the metaverse
C专家编程 第1章 C:穿越时空的迷雾 1.9 阅读ANSI C标准,寻找乐趣和裨益
Detailed explanation of setting HiSilicon MMZ memory and OS memory
Understand the recommendation system in one article: Outline 02: The link of the recommendation system, from recalling rough sorting, to fine sorting, to rearranging, and finally showing the recommend
视频人脸识别和图片人脸识别的关系
MATLAB | 一种简易的随机曼陀罗图形生成函数
B站回应HR称核心用户是Loser;微博回应宕机原因;Go 1.19 正式发布|极客头条
自动化部署+整合SSM项目
随机推荐
中小微企业如何简单便捷、低成本实现数字化?360视觉云有妙招
面试突击71:GET 和 POST 有什么区别?
Hannah荣获第六季完美童模全球总决赛全球人气总冠军
高效的组织信息共享知识库是一种宝贵的资源
[Unity Getting Started Plan] Basic Concepts (7) - Input Manager & Input Class
C专家编程 第3章 分析C语言的声明 3.7 typedef struct foo{... foo;}的含义
deepstresam的插件配置说明,通过配置osd,设置字体的背景为透明
Looking at the ecological potential of Hongmeng OS from the evolution of MatePad Pro
WordPress建站技术笔记
元宇宙系列--Value creation in the metaverse
纯纯粹粹纯纯粹粹
《社会企业开展应聘文职人员培训规范》团体标准在新华书店上架
phoenix创建映射表和创建索引、删除索引
C专家编程 第3章 分析C语言的声明 3.3 优先级规则
protobuf 反射使用总结
详谈RDMA技术原理和三种实现方式
When mobile applications go overseas, is your "network optimization" holding back?
如何使用MATLAB绘制极坐标堆叠柱状图
实时渲染流程操作复杂吗,如何实现?
Detailed ReentrantLock