什么是TCP粘包和半包问题?如何解决? 我会带着你远行 2024-05-06 09:50 16阅读 0赞 ### 什么是TCP粘包问题?如何解决? ### TCP粘包和半包是数据传输中比较常见的问题。所谓的**粘包问题就是指在数据传输的时候,在一条消息中读取到了另一条消息的部分数据**,如下图:![4cb7bc5586b741a9b4f1d7ccabee5e53.png][] **半包是指接收端只收到了部分的数据,而非完整的数据的情况**,如下图:![ffda30a2c1264972bcd355b178932936.png][] 大部分情况下我们都把粘包问题和半包问题看成同一个问题,所以下文就用粘包问题来替代粘包和半包问题 #### 为什么会有粘包问题 #### 粘包问题发生在TCP/IP协议中,因为TCP是面向连接的传输协议,它是以流的形式传输数据的,而流数据是没有明确开始和结尾的边界的,所以就会出现粘包问题 #### 粘包问题演示 #### 接下来我们用代码来演示一下粘包和半包问题,为了演示的直观性,我会设置两个角色: * 服务器端用来接收消息 * 客户端用来发送一段固定的消息 服务端代码实现: import java.io.*; import java.net.*; public class Server { 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, 0, count)); } } } } } 客户端代码实现: import java.io.*; import java.net.*; import java.nio.charset.StandardCharsets; public class Client { public static void main(String[] args) throws IOException { String serverAddress = "127.0.0.1"; int port = 8888; try (Socket socket = new Socket(serverAddress, port); PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) { String message = "hello,world"; OutputStream outputStream= socket.getOutputStream(); for (int i = 0; i < 10; i++) { outputStream.write(message.getBytes()); } } } } 程序执行结果: ![c207f3ffde1642ce8dc30bcaab0114fe.png][] 此时我们发现出现了粘包问题,正常应该是直接输出10次hello world 才对 #### 解决方案 #### 粘包问题的常见解决方案有以下三种: 1. **固定数据大小**:发送方和接收方固定发送消息的大小,当字符长度不够的时候用空字符弥补,有了固定大小就知道每条消息的边界了 2. **自定义数据协议(定义数据边界)**:在TCP协议的基础上封装上一层自定义数据协议,在自定义的数据协议中,包含数据头(存储数据的大小)和数据的具体内容,这样服务端的得到的数据头就可以知道数据的具体长度,也就没有粘包问题 3. **以特殊字符结尾**:比如以“/n”字符结尾,这样就可以直到数据的具体边界,可以避免粘包问题(推荐使用) ##### **解决方案一:固定数据大小** ##### 收、发固定大小的数据,**服务端实现代码**: import java.io.*; import java.net.*; public class Server { private static final int BYTE_LENGTH = 1024; 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); if (count > 0) { // 成功接收到有效消息并打印 System.out.println("接收到客户端的信息是:" + new String(bytes, 0, count)); } } } } } 客户端实现代码: import java.io.*; import java.net.*; import java.nio.charset.StandardCharsets; public class Client { private static final int BYTE_LENGTH = 1024; public static void main(String[] args) throws IOException, InterruptedException { String serverAddress = "127.0.0.1"; int port = 8888; String message = "hello,world"; try (Socket socket = new Socket(serverAddress, port)) { OutputStream outputStream= socket.getOutputStream(); byte[] bytes = new byte[BYTE_LENGTH]; int idx= 0; for(byte b:message.getBytes()){ bytes[idx]= b; idx++; } for (int i = 0; i < 10; i++) { outputStream.write(bytes,0,BYTE_LENGTH); } } } } 运行结果: ![19155c2277aa46e0836046ec9c09f693.png][] 后面是字符编码的问题 **优缺点分析** 从以上代码可以看出,虽然这种方式可以解决粘包问题,但这种固定数据大小的传输方式,当数据量比较小时会使用空字符来填充,所以会额外的增加网络传输的负担,因此不是理想的解决方案 ##### **解决方案二:自定义请求协议** ##### 这种解决方案的实现思路是将请求的数据封装成两部分:消息头(发送的数据大小)+消息体(发送的具体数据),如下图:![fae77545ac7e48b5bfd5ef37e19f9708.png][] 此解决方案的实现为以下三部分: 1. 编写一个消息的封装类 2. 编写客户端 3. 编写服务器端 **消息的封装类:** import java.nio.charset.StandardCharsets; public class CustomProtocol { private static final int HEAD_SIZE = 8; // 假设消息头固定为8个字节 public static byte[] toBytes(String context) { // 协议体 byte 数组 byte[] bodyByte = context.getBytes(StandardCharsets.UTF_8); 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; } 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; } } **客户端代码:** import java.io.IOException; import java.io.OutputStream; import java.net.Socket; import java.util.Random; public class MySocketClient { public static void main(String[] args) throws IOException { // 启动 Socket 并尝试连接服务器 Socket socket = new Socket("127.0.0.1", 9093); // 发送消息合集(随机发送一条消息) final String[] messages = {"hello world"}; // 创建协议封装对象 SocketPacket socketPacket = new SocketPacket(); try (OutputStream outputStream = socket.getOutputStream()) { // 给服务器端发送 10 次消息 for (int i = 0; i < 10; i++) { // 随机发送一条消息 String msg = messages[new Random().nextInt(messages.length)]; // 将内容封装为:协议头+协议体 byte[] bytes = socketPacket.toBytes(msg); // 发送消息 outputStream.write(bytes, 0, bytes.length); outputStream.flush(); } } } } **服务器端代码:** import java.io.IOException; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class MySocketServer { public static void main(String[] args) throws IOException { // 创建 Socket 服务器端 ServerSocket serverSocket = new ServerSocket(9093); // 使用线程池处理更多的客户端 ThreadPoolExecutor threadPool = new ThreadPoolExecutor(100, 150, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000)); while (true) { // 获取客户端连接 Socket clientSocket = serverSocket.accept(); // 客户端消息处理 threadPool.submit(() -> { processMessage(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 e) { e.printStackTrace(); } } } **运行结果:** ![ec8e74dbda944a82a4ed6cdff8da1d38.png][] **优缺点分析:** 此解决方案虽然可以解决粘包问题,但消息的设计和代码的实现复杂度比较高,所以也不是理想的解决方案 ##### 解决方案三:特殊字符结尾 ##### 以特殊字符结尾就可以知道流的边界了,它的具体实现是:使用Java 中自带的 BufferedReader 和Bufferedwriter ,也就是带缓冲区的输入字符流和输出字符流,通过写入的时候加上 \\n 来结尾,读取的时候使用 readLine按行来读取数据,这样就知道流的边界了,从而解决了粘包的问题 **服务器端代码:** import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ServerSocketV3 { public static void main(String[] args) throws IOException { // 创建 Socket 服务器端 ServerSocket serverSocket = new ServerSocket(9092); // 使用线程池处理更多的客户端 ThreadPoolExecutor threadPool = new ThreadPoolExecutor(100, 150, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000)); while (true) { // 获取客户端连接 Socket clientSocket = serverSocket.accept(); // 消息处理 threadPool.submit(() -> { processMessage(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(); } } } **客户端代码:** import java.io.BufferedWriter; import java.io.IOException; import java.io.OutputStreamWriter; import java.net.Socket; public class ClientSocketV3 { public static void main(String[] args) throws IOException { // 启动 Socket 并尝试连接服务器 Socket socket = new Socket("127.0.0.1", 9092); final String message = "hello world"; // 发送消息 try (BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))) { // 给服务器端发送 10 次消息 for (int i = 0; i < 10; i++) { // 注意:结尾的\n 不能省略,它表示按行写入 bufferedWriter.write(message + "\n"); // 刷新缓冲区(此步骤不能省略) bufferedWriter.flush(); } } } } **运行结果:** ![fdf9f5c7f46c441aad6f8fea2173c72a.png][] **优缺点分析:** 以特殊符号作为粘包的解决方案的最大优点是实现简单,但存在一定的局限性,比如当一条消息中间如果出现了结束符就会造成半包的问题,所以如果是复杂的字符串要对内容进行编码和解码处理,这样才能保证结束符的正确性 [4cb7bc5586b741a9b4f1d7ccabee5e53.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/05/06/9e799f78c5e147be8a99f0971637ab29.png [ffda30a2c1264972bcd355b178932936.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/05/06/b40da1dee3eb4043940fc96db5c0a5be.png [c207f3ffde1642ce8dc30bcaab0114fe.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/05/06/c5b3e0f264a540da9378c4ffca38a114.png [19155c2277aa46e0836046ec9c09f693.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/05/06/c31f4e8ad50c4b3687e185eef8230d4f.png [fae77545ac7e48b5bfd5ef37e19f9708.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/05/06/209c1fcbcc324804b85e7218b4966e96.png [ec8e74dbda944a82a4ed6cdff8da1d38.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/05/06/cfb8c84d45da491c84b37d548e658ade.png [fdf9f5c7f46c441aad6f8fea2173c72a.png]: https://image.dandelioncloud.cn/pgy_files/images/2024/05/06/cce382792cc7433c8a854b5d5c148b26.png
还没有评论,来说两句吧...