TCP 粘包 - 拆包问题及解决方案
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 粘包 - 拆包问题及解决方案的更多相关文章
- Netty(三)TCP粘包拆包处理
tcp是一个“流”的协议,一个完整的包可能会被TCP拆分成多个包进行发送,也可能把小的封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题. 粘包.拆包问题说明 假设客户端分别发送数据包D1和D ...
- TCP粘包/拆包问题
无论是服务端还是客户端,当我们读取或者发送消息的时候,都需要考虑TCP底层的粘包/拆包机制. TCP粘包/拆包 TCP是个"流"协议,所谓流,就是没有界限的一串数据.大家可以想想河 ...
- TCP 粘包/拆包问题
简介 TCP 是一个’流’协议,所谓流,就是没有界限的一串数据. 大家可以想想河里的流水,是连成一片的.期间并没有分界线, TCP 底层并不了解上层业务数据的具体含义 ,它会根据 TCP 缓冲区 ...
- Netty(二)——TCP粘包/拆包
转载请注明出处:http://www.cnblogs.com/Joanna-Yan/p/7814644.html 前面讲到:Netty(一)--Netty入门程序 主要内容: TCP粘包/拆包的基础知 ...
- Java网络编程基础之TCP粘包拆包
TCP是个"流"协议,所谓流,就是没有界限的一串数据.大家可以想象河里的流水,他们是连成一片的,其间并没有分界线.TCP底层并不了解上层业务数据的具体含义,他会根据TCP缓冲区的实 ...
- Netty使用LineBasedFrameDecoder解决TCP粘包/拆包
TCP粘包/拆包 TCP是个”流”协议,所谓流,就是没有界限的一串数据.TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TC ...
- TCP粘包/拆包(Netty权威指南)
无论是服务端还是客户端,当我们读取或者发送消息的时候,都需要考虑TCP底层的粘包/拆包机制. TCP粘包/拆包 TCP是个“流”协议,所谓流,就是没有界限的一串数据.大家可以想想河里的流水,是连成一片 ...
- 《精通并发与Netty》学习笔记(13 - 解决TCP粘包拆包(一)概念及实例演示)
一.粘包/拆包概念 TCP是一个“流”协议,所谓流,就是没有界限的一长串二进制数据.TCP作为传输层协议并不不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行数据包的划分,所以在业务上认 ...
- TCP粘包/拆包问题的解决
TCP粘包拆包问题 一个完整的包可能被TCP拆分成多个包,或多个小包封装成一个大的数据包发送. 解决策略 消息定长,如果不够,空位补空格 在包尾增加回车换行符进行分割,例如FTP协议 将消息分为消息头 ...
随机推荐
- Python - 面向对象编程 - 多继承
继承的详解 https://www.cnblogs.com/poloyy/p/15216652.html 这篇文章讲的都是单继承,Python 中还有多继承 Python 多继承的背景 大部分面向对象 ...
- noip模拟测试50
考试过程:开题顺序1,2,3,做T1的时候我想到了要求的东西,就是分成尽量少的段使得每段之和>=k,但是我不会求,就打了个暴力走了,然后看T2,这题我觉得和之前做过的一道题比较像,因为我觉得\( ...
- Element 对话框简单使用
官方文档介绍的是页内对话框,但没有基于组件的对话框,这里记录一下,原理就是父子传值是否显示 父页导入组件 <template> <div class="home" ...
- Postman调试Abp API
在swagger中查看登录需要用post方式访问,Abp需要用application/json方式调用 请求体 Postman调用方式 例:访问所有用户,调用Api地址为http://localhos ...
- DHCP的原理和配置
前言 在大型企业网络中,会有大量的主机或设备需要获取IP地址等网络参数.如果采用手工配置,工作量大且不好管理,如果有用户擅自修改网络参数,还有可能会造成 IP地址冲突等问题.使用动态主机配置协议DHC ...
- kubectl apply部署时可以用 --record 方便记录版本 和回退
1.部署时正常时下面的 kubectl apply -f http.yaml 2.如果修改文件文件重新部署或者之前有上一个版本的 想回退上一个的 可以无感知的回退回去 不影响业务 其中http-de ...
- HDU 6170 FFF at Valentine(强联通缩点+拓扑排序)
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=6165 题意:给你一个无环,无重边的有向图,问你任意两点,是否存在路径使得其中一点能到达另一点 解析:强 ...
- PHP设计模式之模板方法模式
模板方法模式,也是我们经常会在不经意间有会用到的模式之一.这个模式是对继承的最好诠释.当子类中有重复的动作时,将他们提取出来,放在父类中进行统一的处理,这就是模板方法模式的最简单通俗的解释.就像我们平 ...
- Java基础系列(38)- 数组的使用
数组的使用 For-Each循环 数组作方法入参 数组作返回值 For-Each循环 普通型 package array; import sun.security.util.Length; publi ...
- fillder 抓包工具详解
一.安装详解 直接点击.exe可执行文件,一直下一步直到安装完成即可.打开主要为5个部分: 二.安装jmeter插件详解 三.工具详解 3.1:工具条:,可以给指定的请求添加备注信息,在导出后可以查看 ...