当前位置:网站首页>面试突击:什么是粘包和半包?怎么解决?
面试突击:什么是粘包和半包?怎么解决?
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();
}
}
}
}
优缺点分析
总结
边栏推荐
- C专家编程 第3章 分析C语言的声明 3.5 typedef可以成为你的朋友
- 使用 PowerShell 将 Windows 转发事件导入 SQL Server
- SwinIR实战:如何使用SwinIR和预训练模型实现图片的超分
- 高效的组织信息共享知识库是一种宝贵的资源
- uniapp的webview滑动缩放
- 如何在 DataWorks 中 写SQL语句监控数据的变化到达一定的值 进行提示
- 详谈RDMA技术原理和三种实现方式
- C专家编程 第3章 分析C语言的声明 3.6 typedef int x[10]和#define x int[10]的区别
- 新版本 MaxCompute 的SQL 中支持的 EXTRACT 函数有什么作用?
- CPU个数_核心数_线程数之间的关系
猜你喜欢
#夏日挑战赛# HarmonyOS 实现一个绘画板
C专家编程 第1章 C:穿越时空的迷雾 1.9 阅读ANSI C标准,寻找乐趣和裨益
deepstresam的插件配置说明,通过配置osd,设置字体的背景为透明
vector类
Huawei, Lenovo, BAIC, etc. were selected as the first batch of training bases for "Enterprise Digital Transformation and Security Capability Improvement" by the Ministry of Industry and Information Te
TiKV & TiFlash 加速复杂业务查询丨TiFlash 应用实践
[Unity Getting Started Plan] Basic Concepts (8) - Tile Map TileMap 02
Yuan xiaolin: Volvo focus on travel security, and put it perfectly
C专家编程 第1章 C:穿越时空的迷雾 1.10 “安静的改变”究竟有多少安静
Auto Scaling 弹性伸缩(运维释放人力)
随机推荐
J9数字虚拟论:元宇宙的潜力:一股推动社会进步的力量
将 Windows 事件日志错误加载到 SQL 表中
生产环境如何删除表呢?只能在SQL脚本里执行 drop table 吗
机器人开发--Universal Scene Description(USD)
[Deep Learning] Today's bug (August 2)
Windows 事件转发到 SQL 数据库
When mobile applications go overseas, is your "network optimization" holding back?
C语言02、语句、函数
滑环安装注意事项
DataGrip数据仓库工具
视频人脸识别和图片人脸识别的关系
MarkDown常用代码片段和工具
正向代理与反向代理
TiKV & TiFlash accelerate complex business queries丨TiFlash application practice
leetcode SVM
高效的组织信息共享知识库是一种宝贵的资源
面试不再被吊打!这才是Redis分布式锁的七种方案的正确打开方式
Looking at the ecological potential of Hongmeng OS from the evolution of MatePad Pro
MySQL窗口函数
#夏日挑战赛# HarmonyOS 实现一个绘画板