Socket粘包问题

这两天看csdn有一些关于socket粘包,socket缓冲区设置的问题,发现自己不是很清楚,所以查资料了解记录一下:

 

一两个简单概念长连接与短连接:
1.长连接

    Client方与Server方先建立通讯连接,连接建立后不断开, 然后再进行报文发送和接收。

2.短连接

    Client方与Server每进行一次报文收发交易时才进行通讯连接,交易完毕后立即断开连接。此种方式常用于一点对多点
通讯,比如多个Client连接一个Server.

 

二 什么时候需要考虑粘包问题?

1:如果利用tcp每次发送数据,就与对方建立连接,然后双方发送完一段数据后,就关闭连接,这样就不会出现粘包问题(因为只有一种包结构,类似于http协议)。关闭连接主要要双方都发送close连接(参考tcp关闭协议)。如:A需要发送一段字符串给B,那么A与B建立连接,然后发送双方都默认好的协议字符如"hello give me sth abour yourself",然后B收到报文后,就将缓冲区数据接收,然后关闭连接,这样粘包问题不用考虑到,因为大家都知道是发送一段字符。
2:如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包
3:如果双方建立连接,需要在连接后一段时间内发送不同结构数据,如连接后,有好几种结构:
 1)"hello give me sth abour yourself"
 2)"Don't give me sth abour yourself"
   那这样的话,如果发送方连续发送这个两个包出去,接收方一次接收可能会是"hello give me sth abour yourselfDon't give me sth abour yourself" 这样接收方就傻了,到底是要干嘛?不知道,因为协议没有规定这么诡异的字符串,所以要处理把它分包,怎么分也需要双方组织一个比较好的包结构,所以一般可能会在头加一个数据长度之类的包,以确保接收。
 

三 粘包出现原因:在流传输中出现,UDP不会出现粘包,因为它有消息边界(参考Windows 网络编程)
1 发送端需要等缓冲区满才发送出去,造成粘包
2 接收方不及时接收缓冲区的包,造成多个包接收

解决办法:
为了避免粘包现象,可采取以下几种措施。一是对于发送方引起的粘包现象,用户可通过编程设置来避免,TCP提供了强制数据立即传送的操作指令push,TCP软件收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满;二是对于接收方引起的粘包,则可通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施,使其及时接收数据,从而尽量避免出现粘包现象;三是由接收方控制,将一包数据按结构字段,人为控制分多次接收,然后合并,通过这种手段来避免粘包。

以上提到的三种措施,都有其不足之处。第一种编程设置方法虽然可以避免发送方引起的粘包,但它关闭了优化算法,降低了网络发送效率,影响应用程序的性能,一般不建议使用。第二种方法只能减少出现粘包的可能性,但并不能完全避免粘包,当发送频率较高时,或由于网络突发可能使某个时间段数据包到达接收方较快,接收方还是有可能来不及接收,从而导致粘包。第三种方法虽然避免了粘包,但应用程序的效率较低,对实时应用的场合不适合。


相关文章截取:

一个包没有固定长度,以太网限制在46-1500字节,1500就是以太网的MTU,超过这个量,TCP会为IP数据报设置偏移量进行分片传输,现在一般可允许应用层设置8k(NTFS系)的缓冲区,8k的数据由底层分片,而应用看来只是一次发送。windows的缓冲区经验值是4k,Socket本身分为两种,流(TCP)和数据报(UDP),你的问题针对这两种不同使用而结论不一

样。甚至还和你是用阻塞、还是非阻塞Socket来编程有关。

1、通信长度,这个是你自己决定的,没有系统强迫你要发多大的包,实际应该根据需求和网络状况来决定。对于TCP,这个长度可以大点,但要知道,Socket内部默认的收发缓冲区大小大概是8K,你可以用SetSockOpt来改变。但对于UDP,就不要太大,一般在1024至10K。注意一点,你无论发多大的包,IP层和链路层都会把你的包进行分片发送,一般局域网就是1500左右,广域网就只有几十字节。分片后的包将经过不同的路由到达接收方,对于UDP而言,要是其中一个分片丢失,那么接收方的IP层将把整个发送包丢弃,这就形成丢包。显然,要是一个UDP发包佷大,它被分片后,链路层丢失分片的几率就佷大,你这个UDP包,就佷容易丢失,但是太小又影响效率。最好可以配置这个值,以根据不同的环境来调整到最佳状态。

send()函数返回了实际发送的长度,在网络不断的情况下,它绝不会返回(发送失败的)错误,最多就是返回0。对于TCP你可以字节写一个循环发送。当send函数返回SOCKET_ERROR时,才标志着有错误。但对于UDP,你不要写循环发送,否则将给你的接收带来极大的麻烦。所以UDP需要用SetSockOpt来改变Socket内部Buffer的大小,以能容纳你的发包。明确一点,TCP作为流,发包是不会整包到达的,而是源源不断的到,那接收方就必须组包。而UDP作为消息或数据报,它一定是整包到达接收方。

2、关于接收,一般的发包都有包边界,首要的就是你这个包的长度要让接收方知道,于是就有个包头信息,对于TCP,接收方先收这个包头信息,然后再收包数据。一次收齐整个包也可以,可要对结果是否收齐进行验证。这也就完成了组包过程。UDP,那你只能整包接收了。要是你提供的接收Buffer过小,TCP将返回实际接收的长度,余下的还可以收,而UDP不同的是,余下的数据被丢弃并返回WSAEMSGSIZE错误。注意TCP,要是你提供的Buffer佷大,那么可能收到的就是多个发包,你必须分离它们,还有就是当Buffer太小,而一次收不完Socket内部的数据,那么Socket接收事件(OnReceive),可能不会再触发,使用事件方式进行接收时,密切注意这点。这些特性就是体现了流和数据包的区别。


相关参考文章:
http://www.cnblogs.com/alon/archive/2009/04/16/1437600.html

 

 

.版本 2 .支持库 spec .支持库 sock .程序集 窗口程序集_启动窗口 .子程序 _按钮2_被单击 客户1.发送数据 (取重复字节集 (10000, 到字节集 (“1”)) + 到字节集 (“分隔符”)) 客户1.发送数据 (取重复字节集 (20000, 到字节集 (“2”)) + 到字节集 (“分隔符”)) 客户1.发送数据 (取重复字节集 (30000, 到字节集 (“3”)) + 到字节集 (“分隔符”)) 客户1.发送数据 (取重复字节集 (40000, 到字节集 (“4”)) + 到字节集 (“分隔符”)) 客户1.发送数据 (取重复字节集 (50000, 到字节集 (“5”)) + 到字节集 (“分隔符”)) 客户1.发送数据 (取重复字节集 (60000, 到字节集 (“6”)) + 到字节集 (“分隔符”)) 客户1.发送数据 (取重复字节集 (70000, 到字节集 (“7”)) + 到字节集 (“分隔符”)) 客户1.发送数据 (取重复字节集 (80000, 到字节集 (“8”)) + 到字节集 (“分隔符”)) 客户1.发送数据 (取重复字节集 (90000, 到字节集 (“9”)) + 到字节集 (“分隔符”)) 客户1.发送数据 (取重复字节集 (100000, 到字节集 (“0”)) + 到字节集 (“结尾符”)) .子程序 _服务器1_数据到达 .局部变量 取回数据, 字节集 .局部变量 数据数组, 文本型, , "0" .局部变量 次数, 整数型 .局部变量 临时数据, 字节集, 静态 .局部变量 得到的封, 文本型 取回数据 = 服务器1.取回数据 () .判断开始 (取字节集右边 (取回数据, 6) ≠ 到字节集 (“结尾符”))  ' 6为结尾符的长度;这里检测封是否全部发送完毕,如果没有发送完毕就会把发送来的封数据进行累加     临时数据 = 临时数据 + 取回数据 .默认     临时数据 = 临时数据 + 取回数据  ' 检测到结尾符出现,说明数据已经发送完毕,这里把最后发送来的带有结尾符的数据加上就OK了。     临时数据 = 子字节集替换 (临时数据, 到字节集 (“结尾符”), , 取字节集长度 (临时数据) - 6, 6)  ' 6为结尾符的长度;这里把结尾符替换尾空,剩余数据尾完整的纯净数据。     数据数组 = 分割文本 (到文本 (临时数据), “分隔符”, )  ' 这里把收到的数据进行分割处理,无论服务器发送了多少次,都统一按分隔符分割     调试输出 (“封数量:” + 到文本 (取数组成员数 (数据数组)))     .计次循环首 (取数组成员数 (数据数组), 次数)         得到的封 = 数据数组 [次数]  ' 这里得到分割后的封文本。         调试输出 (“第” + 到文本 (次数) + “个封的大小:” + 到文本 (取文本长度 (得到的封)))  ' 这里的大小和上面发送封的大小相同,可以看到封的分割次数。     .计次循环尾 ()     临时数据 = {  } .判断结束 .子程序 __启动窗口_创建完毕 客户1.连接 (取本机名 (), 8888)
<think>嗯,用户现在想了解TCP Socket中的问题及其解决方案。首先,我需要回顾一下之前和用户的对话历史。用户之前询问了如何使用QTcpSocket传输数组,当时我提到了问题以及如何通过先发送数据长度来解决。现在用户进一步深入,想详细理解问题的本质和解决方法。 首先,要确认用户的需求。他们可能在进行网络编程时遇到了数据传输完整或混乱的情况,或者在学习过程中遇到了相关概念。用户可能已经知道的存在,但需要更系统的解释和解决方案。此外,用户可能希望了解编程语言或框架下的处理方法,比如他们之前提到的C#和Golang的例子。 接下来,我需要根据提供的引用内容来整合信息。引用[1]解释了问题的原因,即TCP的流式传输导致数据连,并提到几种解决方法。引用[2]和[3]讨论了Golang中的处理方式,如短连接和头+体的方法。引用[4]展示了C#中使用消息长度前缀的示例。 用户可能希望知道这些方法在同场景下的应用,以及如何选择最合适的解决方案。此外,他们可能想知道除了提到的几种方法外,是否有其他处理策略,或者在实际项目中如何结合使用这些方法。 需要确保回答的结构清晰,分点解释问题原因和解决方案,同时结合之前的回答内容,比如在QTcpSocket中如何处理。还要注意用户可能存在的误区,比如认为TCP协议的缺陷,而实际上它是设计特性,需要应用层处理。 可能用户还想了解和拆的区别,或者如何测试和验证解决方案的有效性。此外,错误处理、性能影响以及编程语言的具体实现差异也是需要考虑的方面。 最后,应该总结同方法的优缺点,帮助用户根据具体应用场景选择最合适的解决方案,并提醒在实际开发中需要注意的细节,比如字节序、缓冲区管理、异常处理等。同时,结合之前的QTcpSocket例子,说明如何将理论应用到实践中,以加强用户的理解。</think>### TCP Socket 问题详解与解决方案 --- #### **一、问题本质** 1. **产生原因** - **TCP流式传输特性**:TCP协议将数据视为无结构的字节流,保留应用层消息边界,多个数据可能在传输层合并或拆分(如Nagle算法优化)[^1][^2] - **接收缓冲区机制**:接收端可能一次性读取多个数据的累积内容,导致无法区分消息边界 - **网络传输特性**:路由分片、拥塞控制等可能导致数据到达顺序与发送顺序一致 2. **典型表现** - **数据连**:两个独立消息合并为单个接收数据块 ```plaintext 发送: [A][B][C] → 接收: [ABC] ``` - **数据拆分**:单个消息被拆分为多次接收 ```plaintext 发送: [ABCD] → 接收: [AB][CD] ``` --- #### **二、核心解决方案** (参考QTcpSocket实现和引用[1][4]) | 方法 | 实现原理 | 适用场景 | 代码示例片段 | |------|---------|----------|-------------| | **1. 固定长度法** | 约定固定长度数据 | 实时音视频流 | `QByteArray buffer = socket->read(1024);` | | **2. 分隔符法** | 用特殊符号分隔消息 | 文本协议(如HTTP) | `QString data = buffer.split('\n');` | | **3. 头+体** | 含数据长度信息 | 结构化数据传输(推荐) | 见下方代码实现 | | **4. 短连接法** | 每次发送后断开连接 | 低频请求(如HTTP) | 引用[3]的Golang示例 | --- #### **三、最佳实践:头+体方案** (结合QTcpSocket和引用[4]的C#示例) **发送端逻辑**: ```cpp // 发送数组数据(QT示例) void sendData(QTcpSocket* socket, QByteArray payload) { QByteArray header; QDataStream headerStream(&header, QIODevice::WriteOnly); headerStream << (quint32)payload.size(); // 4字节头 // 先发头,再发数据 socket->write(header); socket->write(payload); } ``` **接收端处理**: ```cpp // 接收状态机(关键代码) enum RecvState { Header, Payload }; RecvState state = Header; quint32 payloadSize = 0; void processData(QTcpSocket* socket) { switch(state) { case Header: if(socket->bytesAvailable() >= 4) { QDataStream(socket) >> payloadSize; state = Payload; } break; case Payload: if(socket->bytesAvailable() >= payloadSize) { QByteArray data = socket->read(payloadSize); processPayload(data); // 处理实际数据 state = Header; // 重置状态 } break; } } ``` --- #### **四、进阶处理技巧** 1. **协议设计优化** - 增加校验码(如CRC32)验证数据完整性 ```cpp quint32 crc = qChecksum(payload.constData(), payload.size()); headerStream << crc; // 在头中加入校验码 ``` - 使用TLV(Type-Length-Value)格式支持多数据类型 2. **性能优化** - **缓冲区管理**:使用环形缓冲区减少内存拷贝 - **异步处理**:通过`QIODevice::readyRead`信号触发处理逻辑 3. **跨语言兼容** - **字节序处理**:统一使用网络字节序(大端) ```cpp headerStream.setByteOrder(QDataStream::BigEndian); ``` - **数据类型对齐**:避免同平台数据类型差异 --- #### **五、同语言对比** | 语言 | 典型处理方式 | 特点 | |---------|--------------------------|-------------------------------| | C++/Qt | QDataStream序列化 | 类型安全,自动处理字节序 | | Golang | binary.Read/binary.Write | 需要手动处理结构体标签 |[^3] | C# | BitConverter类 | 需配合MemoryStream使用 |[^4] | Python | struct模块 | 需要格式字符串指定数据类型 | --- #### **六、选择方案的考量维度** 1. **数据特征** - 定长/变长数据 - 文本/二进制格式 2. **性能需求** - 高吞吐场景:优先头+体方案 - 低延迟场景:可关闭Nagle算法(`socket->setSocketOption(QAbstractSocket::LowDelayOption, 1)`) 3. **协议扩展性** - 预留版本字段 - 支持嵌套数据结构 ---
评论 11
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值