粘包及其解决方法

本文深入探讨了TCP协议中的粘包问题,包括其产生的原因、两种常见情况及拆包情形。通过对比UDP协议,阐述了面向流与面向消息通信的区别。并提供了解决粘包问题的具体方案,涉及报头设计、数据发送与接收流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、粘包

1.1 粘包现象只有在tcp协议中会出现,在udp协议中永远不会出现

tcp协议是面向流的协议,这也是容易产生粘包问题的原因
所谓粘包问题只要你还是因为接受方不知道消息之间的界限,不知道一次性提取多少字节的数据造成的
此外,发送方引起的粘包是由于TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。如果连续几次需要发送的数据都很少,通常TCP会根据优化算法将这些数据合成一个TCP段后一次发送出去,这样接受方就收到了粘包数据

TCP是面向连接,面向流的,提供高可靠性服务。收发两端都要有一一成对的socket,
因此,发送端为了将多个发往接受端的包,更有效的发到对方,使用了Nagle算法,
将多次间隔较小、数据量小的数据 合并成一个大的数据库块,然后进行封包。
这样,接受端就难于分辨出来了,必须提供科学的拆包机制。
即面向流的通信是无消息保护边界的。

UDP协议是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,
由于	UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用的是
链式的结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息)
这样,对于接收端来说,就容易区分了。
即面向消息的通信是有消息保护边界的。

TCP是基于数据的,于是发送的消息不能为空,
这就需要在客户端和服务端都添加空消息处理机制防止程序卡住;
而UDP是基于数据报的,即便数据内容为空,UDP协议也会帮助封装上消息头

1.2 两种粘包的情况

① 发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据很小,会合到一起,产生粘包)
② 接受方不及时接受缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只接收了一小部分,服务端下次再收的时候还是从缓冲区拿走上次遗留的数据,产生粘包)

1.3 拆包发生的情况

当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去
补充:send(字节流)、 recv(1024) 、sendall
recv(1024)指的是从缓存里一次拿出1024个字节的数据
send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果待发送的字节流大小大于缓存剩余空间,那么剩余数据丢失,用sendall就会循环调用send,数据不会丢失

二、解决粘包问题的方案

我们可以将报头做成字典,字典里面包含将要发送的真实数据的详细信息,使用json模块序列化字典后转成bytes类型,然后使用 struct模块将bytes数据的长度打包成4个字节("i"模式)
发送时:

先发送报头长度(struct打包后的数据)
再编码报头内容(bytes类型)然后发送
最后将真实的数据内容(转成bytes类型)发送出去

接收时:

先收报头长度,用struct.unpack取出数值(报头的字节个数)
根据取出来的长度接收指定长度的字节数(即取到报头内容),然后解码、反序列化
从反序列化的结果中取出数据的详细数据(字典),再根据字典中的信息(真实数据的长度)取指定长度的字节解码得到真实的数据

示例:连接服务端,发送指定让服务端执行,并将执行的结果返回
服务端:

import struct,json
import subprocess
from socket import *
server = socket(AF_INET, SOCK_STREAM)
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server.bind(('127.0.0.1', 8080))
server.listen(5)
while True:
	conn, client_addr = server.accept()
	while True:
		cmd = conn.recv(1024)
		if len(cmd) == 0: break
		print(f"cmd : {cmd.decode('utf-8')}")
		obj = subprocess.Popen(cmd.decode('utf-8'),
							   shell = True,
							   stdout = subprocess.PIPE,
							   stderr = subprocess.PIPE)
		res1 = obj.stdout.read()
		res2 = obj.stderr.read()
		res = (res1 if res1 else res2)
		header = {'data_size': len(res)}
		header_json = json.dumps(header)
		header_json_bytes = header_json.encode('utf-8')
		
		conn.send(struct.pack('i', len(header_json_bytes)))  # 发送报头长度
		conn.send(header_json_bytes)  # 发送报头内容(bytes类型)
		conn.send(res.encode('utf-8'))  # 发送真实的内容
	conn.close()	

客户端:

import json, struct
from socket import *
client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 8080))
while True:
	cmd = input('>>>:').strip()
	if not cmd: continue
	client.send(cmd.encode('utf-8'))

	header = client.recv(4)
	header_json_length = struct.unpack('i', header)[0]
	header_json = recv(header_json_length)
	data_length = header_json['data_size']
	recv_size = 0
	data_recv = b''
	while recv_size < data_length:
		data = client.recv(1024)
		data_recv += data
		recv_size += len(data)
	
	print(recv_data.decode('gbk'))  # windows默认返回gbk编码的结果	
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值