当前位置:网站首页>面试突击70:什么是粘包和半包?怎么解决?
面试突击70:什么是粘包和半包?怎么解决?
2022-08-01 19:42: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:自定义请求协议

- 编写一个消息封装类
- 编写客户端
- 编写服务器端
① 消息封装类
/**
* 消息封装类
*/
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();
}
}
}
}

优缺点分析
总结
边栏推荐
猜你喜欢
第55章 业务逻辑之订单、支付实体定义
图文详述Eureka的缓存机制/三级缓存
From ordinary advanced to excellent test/development programmer, all the way through
为什么限制了Oracle的SGA和PGA,OS仍然会用到SWAP?
OSPO 五阶段成熟度模型解析
#yyds dry goods inventory# Interview must brush TOP101: the last k nodes in the linked list
因斯布鲁克大学团队量子计算硬件突破了二进制
kubernetes - deploy nfs storage class
57:第五章:开发admin管理服务:10:开发【从MongoDB的GridFS中,获取文件,接口】;(从GridFS中,获取文件的SOP)(不使用MongoDB的服务,可以排除其自动加载类)
XSS靶场中级绕过
随机推荐
把 Oracle 数据库从 RAC 集群迁移到单机环境
Choosing the right DevOps tool starts with understanding DevOps
为什么限制了Oracle的SGA和PGA,OS仍然会用到SWAP?
数据库系统原理与应用教程(072)—— MySQL 练习题:操作题 121-130(十六):综合练习
ssh & scp
Win11怎么安装语音包?Win11语音包安装教程
部署zabbix
Source code analysis of GZIPOutputStream class
从普通进阶成优秀的测试/开发程序员,一路过关斩将
开源视界 | StreamNative 盛宇帆:和浪漫的人一起做最浪漫的事
Oracle排序某个字段, 如果这个varchar2类型的字段有数字也有文字 , 怎么按照数字大小排序?
Win11校园网无法连接怎么办?Win11连接不到校园网的解决方法
The solution to the vtk volume rendering code error (the code can run in vtk7, 8, 9), and the VTK dataset website
Screenshot of Selenium in Remote
An implementation of an ordered doubly linked list.
openresty 动态黑白名单
常用命令备查
MySQL开发技巧——并发控制
1个小时!从零制作一个! AI图片识别WEB应用!
datax - 艰难debug路