TCP粘包拆包问题

- TCP 全称是 Transmission Control Protocol(传输控制协议),它由 IETF 的 RFC 793 定义,是一种面向连接的点对点的传输层通信协议。
- 粘包拆包问题是处于⽹络⽐较底层的问题,在数据链路层、⽹络层以及传输层都有可能发⽣;
- TCP会发生粘包问题;TCP⽆消息保护边界,需要在接收端处理消息边界问题,也就是我们所说的粘包、拆包问题;
- UDP不会发生粘包问题;UDP具有保护消息边界,在每个UDP包中就有了消息头(UDP长度、源端口、目的端口、校验和)。

什么是粘包 - 拆包问题

  • 粘包问题
- 粘包问题指,当发送方发送了数据包 `消息1 - ABC` 和 `消息2 - DEF` 时,但接收方接收到的数据包却是 `消息 -  ABCDEF`,像这种一次性读取了两条数据包的数据粘连在一起的情况就叫做粘包(正常情况应该是一条一条读取的)。

  • 拆包问题
- 拆包问题是指,当发送方发送了数据包 ` 消息1 -  ABC ` 和 `消息2 - DEF` 时,接收方接收到数据包经拆分后获得了 `ABCD` 和 `EF` 两个数据包信息的情况,像这种情况有时候也叫做半包。

为什么存在粘包 - 拆包问题

- TCP 是面向连接的传输协议,TCP 传输的数据是以流的形式,而流数据是没有明确的开始结尾边界,所以 TCP 也没办法判断哪一段流属于一个消息;

- TCP 协议是流式协议;所谓流式协议,即协议的内容是像流水一样的字节流,内容与内容之间没有明确的分界标志,需要认为手动地去给这些协议划分边界。
  • 粘包主要原因
- 发送方每次写入数据 < 接收方套接字(Socket)缓冲区大小;
- 接收方读取套接字(Socket)缓冲区数据不够及时。
  • 拆包问题
- 发送方每次写入数据 > 接收方套接字(Socket)缓冲区大小;
- 发送的数据大于协议的 MTU (Maximum Transmission Unit,最大传输单元),既TCP报⽂⻓度-TCP头部⻓度>MSS时发生拆包问题。

粘包 - 拆包 演示

PasteServer.java: 服务端;

PasteClient.java: 客户端;

  • PasteServer.java
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket; /**
* @author hosystem
* @version 1.0
*/
public class PasteServer {
// 字节数组的长度
private static final int BYTE_LENGTH = 20;
public static void main(String[] args) throws IOException {
// 创建 Socket 服务器
ServerSocket serverSocket = new ServerSocket(9999);
// 获取客户端连接
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;
}
}
}
}
  • PasteClient.java
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket; /**
* @author hosystem
* @version 1.0
*/
public class PasteClient { public static void main(String[] args) throws IOException {
// 创建 Socket 客户端并尝试连接服务器端
Socket socket = new Socket("127.0.0.1", 9999);
// 发送的消息内容
final String message = "hello.java";
// 使用输出流发送消息
try (OutputStream outputStream = socket.getOutputStream()) {
// 给服务器端发送 10 次消息
for (int i = 0; i < 10; i++) {
// 发送消息
outputStream.write(message.getBytes());
}
outputStream.close();
}finally {
socket.close();
}
}
}

通过上述结果我们可以看出,服务器端发生了粘包和拆包问题,因为客户端发送了 10 次固定的“hello.java.”的消息;正常的结果应该是服务器端也接收到了 10 次固定的消息才对,但结果并非如此。

粘包 - 拆包 解决方案

# 解决方案
- 方案一: 设置定⻓消息,服务端每次读取既定⻓度的内容作为⼀条完整消息(固定缓冲区大小); - 方式二: 使⽤⾃定义协议+编解码器(封装请求协议); - 方案三: 设置消息边界,服务端从⽹络流中按消息编辑分离出消息内容(特殊字符结尾,按行读取)。 # 优缺点
- 方案一: 从以上代码可以看出,虽然这种方式可以解决粘包和拆包的问题,但这种固定缓冲区大小的方式增加了不必要的数据传输;当这种方式当发送的数据比较小时会使用空字符来弥补,所以这种方式就大大的增加了网络传输的负担,所以它也不是最佳的解决方案。 - 方案二: 实现较为复杂,更多情况下使用该种实现;Dubbo实现自定义的传输协议,使用Netty来实现可降低编码复杂程度,netty框架对于粘包有专门encoder和decoder接口来处理。 - 方案三: 特殊字符的方案其实是最不可取的;TCP是面向流的;所以应该认为TCP传输的是字节流,任何一个字节都可能被传输;在这种情况下,特殊字符也不特殊了,没法和正常数据区分。

方式一: 固定缓冲区大小

固定缓冲区大小的实现方案,只需要控制服务器端和客户端发送和接收字节的(数组)长度相同即可。

  • PasteServer.java
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket; /**
* @author hosystem
* @version 1.1
*/
public class PasteServer { // 字节数组的长度
private static final int BYTE_LENGTH = 1024;
public static void main(String[] args) throws IOException {
// 创建 Socket 服务器
ServerSocket serverSocket = new ServerSocket(9999);
// 获取客户端连接
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;
}
}
}
}
  • PasteClient.java
mport java.io.IOException;
import java.io.OutputStream;
import java.net.Socket; /**
* @author hosystem
* @version 1.1
*/
public class PasteClient { // 字节数组的长度
private static final int BYTE_LENGTH = 1024; public static void main(String[] args) throws IOException {
// 创建 Socket 客户端并尝试连接服务器端
Socket socket = new Socket("127.0.0.1", 9999);
// 发送的消息内容
final String message = "hello.java";
// 使用输出流发送消息
OutputStream outputStream = socket.getOutputStream();
try { //将数组装成定长字节数组
byte[] bytes = new byte[BYTE_LENGTH];
int index = 0;
for (byte b : message.getBytes()) {
bytes[index++] = b;
} // 给服务器端发送 10 次消息
for (int i = 0; i < 10; i++) {
// 发送消息
outputStream.write(bytes,0,BYTE_LENGTH);
}
}finally {
socket.close();
outputStream.close();
}
}
}

方式二: 封装请求协议

将请求的数据封装为两部分:数据头+数据正文,在数据头中存储数据正文的大小,当读取的数据小于数据头中的大小时,继续读取数据,直到读取的数据长度等于数据头中的长度时才停止。

实现起来较为复杂,这里不给出代码,可以使用netty完成方式二。

方式三: 特殊字符结尾 - 按行读取

使用 Java 中自带的 BufferedReader 和 BufferedWriter,也就是带缓冲区的输入字符流和输出字符流,通过写入的时候加上 \n 来结尾,读取的时候使用readLine 按行来读取数据,通过遇到结束标志 \n来结束行的读取。

  • PasteServer.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket; /**
* @author hosystem
* @version 1.3
*/
public class PasteServer { public static void main(String[] args) throws IOException {
// 创建 Socket 服务器
ServerSocket serverSocket = new ServerSocket(9999);
// 获取客户端连接
Socket clientSocket = serverSocket.accept();
// 得到客户端发送的流对象
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) {
while (true) {
String msg = bufferedReader.readLine();
if (msg != null) {
// 成功接收到客户端的消息并打印
System.out.println("接收到客户端的信息:" + msg);
}
}
}
}
}
  • PasteClient.java
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.Socket; public class PasteClient { public static void main(String[] args) throws IOException {
// 创建 Socket 客户端并尝试连接服务器端
Socket socket = new Socket("127.0.0.1", 9999);
// 发送的消息内容
final String message = "hello.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();
}
}finally {
socket.close();
}
}
}

TCP 粘包 - 拆包问题及解决方案的更多相关文章

  1. Netty(三)TCP粘包拆包处理

    tcp是一个“流”的协议,一个完整的包可能会被TCP拆分成多个包进行发送,也可能把小的封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题. 粘包.拆包问题说明 假设客户端分别发送数据包D1和D ...

  2. TCP粘包/拆包问题

    无论是服务端还是客户端,当我们读取或者发送消息的时候,都需要考虑TCP底层的粘包/拆包机制. TCP粘包/拆包 TCP是个"流"协议,所谓流,就是没有界限的一串数据.大家可以想想河 ...

  3. TCP 粘包/拆包问题

    简介    TCP 是一个’流’协议,所谓流,就是没有界限的一串数据. 大家可以想想河里的流水,是连成一片的.期间并没有分界线, TCP 底层并不了解上层业务数据的具体含义 ,它会根据 TCP 缓冲区 ...

  4. Netty(二)——TCP粘包/拆包

    转载请注明出处:http://www.cnblogs.com/Joanna-Yan/p/7814644.html 前面讲到:Netty(一)--Netty入门程序 主要内容: TCP粘包/拆包的基础知 ...

  5. Java网络编程基础之TCP粘包拆包

    TCP是个"流"协议,所谓流,就是没有界限的一串数据.大家可以想象河里的流水,他们是连成一片的,其间并没有分界线.TCP底层并不了解上层业务数据的具体含义,他会根据TCP缓冲区的实 ...

  6. Netty使用LineBasedFrameDecoder解决TCP粘包/拆包

    TCP粘包/拆包 TCP是个”流”协议,所谓流,就是没有界限的一串数据.TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TC ...

  7. TCP粘包/拆包(Netty权威指南)

    无论是服务端还是客户端,当我们读取或者发送消息的时候,都需要考虑TCP底层的粘包/拆包机制. TCP粘包/拆包 TCP是个“流”协议,所谓流,就是没有界限的一串数据.大家可以想想河里的流水,是连成一片 ...

  8. 《精通并发与Netty》学习笔记(13 - 解决TCP粘包拆包(一)概念及实例演示)

    一.粘包/拆包概念 TCP是一个“流”协议,所谓流,就是没有界限的一长串二进制数据.TCP作为传输层协议并不不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行数据包的划分,所以在业务上认 ...

  9. TCP粘包/拆包问题的解决

    TCP粘包拆包问题 一个完整的包可能被TCP拆分成多个包,或多个小包封装成一个大的数据包发送. 解决策略 消息定长,如果不够,空位补空格 在包尾增加回车换行符进行分割,例如FTP协议 将消息分为消息头 ...

随机推荐

  1. MySQL密码重置方法

    MySQL数据库的安装和配置,配置Mysql按照bin目录到Path中 使用命令行窗口连接MYSQL数据库:mysql –u用户名 –p密码 对于密码的重置有以下两种方法(卸载重新安装当然也可以): ...

  2. python 文件批量改名重命名 rename

    path = '/Volumes/Seagate/dev/imgs/' os.chdir(path) print('cwd: ', os.getcwd()) for f in os.listdir(' ...

  3. MySQL(一)——入门

    一.安装 二.配置环境变量 https://www.cnblogs.com/wzk153/category/1934516.html https://www.cnblogs.com/wzk153/ca ...

  4. 基本ServletWEB项目

    项目搭建 项目链接https://gitee.com/zhangjzm/smbms.git 前置知识,Servlet JSP 结构图 搭建maven web项目 1.搭建一个maven web项目 2 ...

  5. 安全系列之:跨域资源共享CORS

    目录 简介 CORS举例 CORS protocol HTTP request headers HTTP response headers 基本CORS Preflighted requests 带认 ...

  6. 用Java实现红黑树

    红黑树是众多"平衡的"搜索树模式中的一种,在最坏情况下,它相关操作的时间复杂度为O(log n). 1.红黑树的属性 红黑树是一种二分查找树,与普通的二分查找树不同的一点是,红黑树 ...

  7. python 爬虫新手入门教程

    python 爬虫新手教程 一.什么是爬虫 爬虫就是把一个网站里的内容读取下来 这里我们就要学习一个知识 我们看到的网页是有一种叫HTML的语言编写的 他可以给文字显示不同的样式 如:<p> ...

  8. 关于在.H文件中定义变量

    KEIL中,在".H"文件定义变量. 如果该".H"文件同时被两个".C"文件调用,则会出现重复定义错误(*** ERROR L104: M ...

  9. php-抽象工厂

    目标:创建有依赖关系的实例;(套餐) <?php //抽象类 食物 interface IAllayFood { function Allay(); } interface IDrinkFood ...

  10. 【译】.NET Core 3.0 发布小尺寸 self-contained 单体可执行程序

    .NET Core 提供的发布应用程序选项 self-contained 是共享应用程序的好方法,因为应用程序的发布目录包含所有组件.运行时和框架.您只需要告诉使用者应用程序的入口 exe 文件,就可 ...