关于TCP粘包
首先,TCP(传输控制协议)没有粘包问题,但是为什么总是有人喜欢问如何解决TCP粘包分包问题呢?实际上问这个问题是在问如何设计一种应用层协议(TCP/IP四层模型),来解决使用TCP协议时,数据在经过多个中转节点后导致在目的地出现包合并或者拆分的问题。
在问这个问题之前,我想大部分人使用的都是基于socket的通信,socket是对传输层的封装,如下图
TCP是一种面向流传输的协议,只负责数据能完整不丢失的到达目的地,不保证数据在中间转发单元的合并与拆分,因此说TCP粘包本身就是一种误解,应该是说如何设计应用层协议来保证在目的地能正确的识别并拆分数据包。
协议设计
通常应用层对于在使用TCP时出现多个数据包合并拆分的解决办法有如下三种:
- 固定长度:每个消息固定为特定长度,不足的部分可以用空白或特定字符填充。
-
分隔符:使用特定字符或字符串作为消息的结束标志,如HTTP协议中的
\r\n
。 -
长度字段:消息的开始部分包含一个表示消息总长度的字段,这样接收方就可以根据这个长度信息确定消息的结束位置。(本文接下来的代码即使用这种方法)
通过这些方法,即使在使用TCP协议时,也可以有效地处理粘包和分包的问题。
例如应用层的HTTP协议的解决思路是:
- 使用
Content-Length
头部: 这是最常见的方法,其中Content-Length
头部字段明确指定了消息体的长度。通过这个长度值,接收方可以确定何时一个HTTP消息体结束,并正确地将这个消息与后续的消息区分开。 - 使用分块传输编码(Chunked Transfer Encoding): 在这种情况下,消息体被分割成一系列的块,每个块都以其长度开始(以十六进制格式表示),后面跟着实际的数据内容,每个块结束时使用CRLF(即
\r\n
)。 - 多部分类型(Multipart types): 特别是在需要发送多种类型数据(如文件上传)的情况下,HTTP消息体可以包含多个部分,每个部分都由边界符分隔,这些边界符在HTTP头部中定义。每一部分都可以有自己的
Content-Type
和Content-Length
等头部字段。
这样,即使在数据是流式传输的情况下,接收方也能正确地解析和重建原始的HTTP消息。
为什么没有UDP粘包问题?
UDP(用户数据报协议)没有粘包问题,主要是因为它的工作机制和TCP不同。这些差异包括:
- 面向消息的协议:
UDP是面向消息的协议,它保留了消息的边界。当应用层发送一个消息时,UDP会将该消息封装成一个数据报,并以这种形式在网络中传输。接收端收到数据报后,就能清楚地识别出消息的边界。因此,每个UDP数据报都是独立的,接收方接收和处理的就是发送方发送的单个消息,而不会将多个消息合并在一起。 -
无连接:
UDP是无连接的协议,不像TCP那样在发送数据前需要建立连接。每个UDP数据报独立发送,与其他数据报无关,不存在数据流的概念,因此也就没有粘包的问题。 -
不保证可靠性:
UDP不提供数据到达的保证,也不保证数据报的顺序和完整性。丢包或乱序是UDP网络环境中常见的现象。因为它不像TCP那样维护一个顺序的、可靠的字节流,所以UDP不需要处理数据的重新组装,从而也就不存在粘包的问题。
总的来说,UDP的这些特性(面向消息、无连接、不保证可靠性)使得它在传输数据时不会出现TCP那样的粘包问题。UDP更适合于对实时性要求高、可以容忍一定丢包的应用,如实时视频或音频传输。
封装socket
对socket的send和recv函数封装,即设计应用层协议,解决TCP不区分数据包的情况。这里使用文章开头协议设计的第三个方法。
协议规则流程:
- 先发送整个数据包的大小
- 发送数据包分片(最后一个分片大小 = 总大小 – n * 固定分片大小)
#include <iostream>
#include <string>
#include <netinet/in.h>
#include <unistd.h>
const size_t MAX_CHUNK_SIZE = 1024; // 最大分片大小
// 发送数据,支持分片
bool sendData(int sockfd, const std::string& data) {
size_t totalSize = data.size();
// 发送总数据长度信息
int32_t totalSizeNet = htonl(totalSize);
if (send(sockfd, &totalSizeNet, sizeof(totalSizeNet), 0) != sizeof(totalSizeNet)) {
std::cerr << "Failed to send total data length" << std::endl;
return false;
}
size_t offset = 0;
while (offset < totalSize) {
size_t chunkSize = std::min(MAX_CHUNK_SIZE, totalSize - offset);
std::string chunk = data.substr(offset, chunkSize);
// 直接发送分片数据
if (send(sockfd, chunk.c_str(), chunkSize, 0) != static_cast<ssize_t>(chunkSize)) {
std::cerr << "Failed to send data chunk" << std::endl;
return false;
}
offset += chunkSize;
}
return true;
}
// 接收数据,分片接收
bool receiveData(int sockfd, std::string& data) {
int32_t totalSizeNet;
// 接收总数据长度信息
if (recv(sockfd, &totalSizeNet, sizeof(totalSizeNet), 0) != sizeof(totalSizeNet)) {
std::cerr << "Failed to receive total data length" << std::endl;
return false;
}
size_t totalSize = ntohl(totalSizeNet);
data.reserve(totalSize);
size_t received = 0;
while (received < totalSize) {
size_t chunkSize = std::min(MAX_CHUNK_SIZE, totalSize - received);
std::string chunk(chunkSize, '\0');
// 直接接收分片数据
if (recv(sockfd, &chunk[0], chunkSize, 0) != static_cast<ssize_t>(chunkSize)) {
std::cerr << "Failed to receive data chunk" << std::endl;
return false;
}
data.append(chunk);
received += chunkSize;
}
return true;
}
断点续传
// send.cpp
#include <iostream>
#include <string>
#include <netinet/in.h>
#include <unistd.h>
const size_t MAX_CHUNK_SIZE = 1024; // 最大分片大小
// 发送数据,支持分片和断点续传
bool sendData(int sockfd, const std::string& data, size_t &offset) {
size_t totalSize = data.size();
// 只在首次发送时传送总数据长度信息
if (offset == 0) {
int32_t totalSizeNet = htonl(totalSize);
if (send(sockfd, &totalSizeNet, sizeof(totalSizeNet), 0) != sizeof(totalSizeNet)) {
std::cerr << "Failed to send total data length" << std::endl;
return false;
}
}
while (offset < totalSize) {
size_t chunkSize = std::min(MAX_CHUNK_SIZE, totalSize - offset);
std::string chunk = data.substr(offset, chunkSize);
// 发送分片数据
if (send(sockfd, chunk.c_str(), chunkSize, 0) != static_cast<ssize_t>(chunkSize)) {
std::cerr << "Failed to send data chunk at offset " << offset << std::endl;
return false; // 发生错误,返回false
}
offset += chunkSize; // 更新偏移量
}
return true; // 数据发送完毕
}
int main() {
// socket初始化等操作
string data = "数据";
size_t offset = 0;
// 如果因为某些原因(如网络问题)未发送完毕,可以继续发送剩余部分
while (offset < data.size()) {
if (!sendData(sockfd, data, offset)) {
std::cerr << "Error occurred, data sent up to offset " << offset << std::endl;
}
}
// 关闭socket等后续操作
return 0;
}
// receiver.cpp
#include <iostream>
#include <string>
#include <netinet/in.h>
#include <unistd.h>
const size_t MAX_CHUNK_SIZE = 1024; // 最大分片大小
// 接收数据,支持分片和断点续传
bool receiveData(int sockfd, std::string& data, size_t &offset) {
size_t totalSize;
// 只在首次接收时获取总数据长度信息
if (offset == 0) {
int32_t totalSizeNet;
if (recv(sockfd, &totalSizeNet, sizeof(totalSizeNet), 0) != sizeof(totalSizeNet)) {
std::cerr << "Failed to receive total data length" << std::endl;
return false;
}
totalSize = ntohl(totalSizeNet);
data.reserve(totalSize);
} else {
totalSize = data.capacity(); // 假设data已经预留了足够的空间
}
while (offset < totalSize) {
size_t chunkSize = std::min(MAX_CHUNK_SIZE, totalSize - offset);
std::string chunk(chunkSize, '\0');
ssize_t numReceived = recv(sockfd, &chunk[0], chunkSize, 0);
if (numReceived < 0) {
std::cerr << "Failed to receive data chunk" << std::endl;
return false; // 发生错误,返回false
}
data.append(chunk, 0, numReceived);
offset += numReceived; // 更新偏移量
if (numReceived < chunkSize) {
// 没有更多的数据可接收,可能是因为发送方的数据发送完毕或者连接中断
break;
}
}
return true; // 数据接收完毕或者接收到一部分
}
int main() {
// socket初始化等操作
string data;
size_t offset = 0;
// 如果数据未接收完全,可以继续接收剩余部分
while (offset < data.capacity()) {
if (!receiveData(sockfd, data, offset)) {
std::cerr << "Error occurred, data received up to offset " << offset << std::endl;
}
}
// 数据处理等后续操作
return 0;
}
关于文件的传输
对于文件的传输,也可以使用如上的协议,不过在此之前,需要先发送一个文件元数据,文件元数据包含:
- 文件名
- 文件大小
- 修改时间
- 文件权限
- 等等
发送文件元数据之后,再发送文件的二进制数据即可