标签:ast 多个 取数据 read 字段 产生 recv 字节 完成
前提:只有TCP会发生粘包现象,UDP永远不会粘包。
粘包问题本质上就是接收方不知道消息的边界,不知道一次性该提取多少字节流用于解析消息,造成的消息解析错误问题。
?
发送端可以是1K1K的发送数据,而接收端的应用程序可以是两K两K地提取数据,也可以一次性全部提走,或者一次只提取几个字节地数据,也就是说,应用程序所看到的数据是一个整体,或者说是一个流(stream) ,一条消息有多少个字节对应用程序时不可见的,因此TCP协议是"""**面向流的协议**""",这也是容易出现粘包问题的原因。而UDP协议是面向消息的协议,每个UDP字段都是一条消息,应用程序必须以消息为单位提取数据,不能一次性提取任意字节的数据,这和TCP很不相同。TCP协议下,一条消息的发送,无论底层如何分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
例如:基于TCP的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看来,根部不知道该文件的字节流是从何处开始,在何处结束。
# **所谓的粘包问题,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节流的数据造成的。**
此外,发送放引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要手机足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据整合成一个TCP段后一次性发出,这样接收方就收到了粘包数据。
### 1.TCP(transport control protocol,传输控制协议) 下的消息边界
该协议是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务端) 都要有一一成对的socket,因此,发送端为了将多个法网接收端的包,更有效的发到对方,使用了优化算法(Nagle算法) ,将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端就难于分辨出来了,必须提供科学的拆包机制。**即面向流的通信是无消息保护边界的。**
### 2.UDP(user datagram protocal,用户数据报协议) 下的消息边界
该协议是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区) 采用了链式结构来记录每一个到达的UDP包,在每个UDP包中都有消息头(消息来源地址,端口等消息) ,这样,对于接收端来说,就容易进行区分处理了。**即面向消息的通信是有消息保护边界的。**
### 3.总结
由于TCP协议是基于数据流的,于是收发消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即使是你输入的是空内容(直接回车) ,那也不是空消息,udp协议会帮你封装上消息头。
udp的recvfrom是阻塞的,一个recvfrom(x) 必须对唯一一个sendinto(y) ,收完了x 个字节的数据就算完成,若是y > x 数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠。
tcp协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
1.发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据流很小,会河道一起,产生粘包) 。
2.接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再接收的时候,还是从缓冲区拿上次一六的数据,产生粘包) 。
当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。
### 1.为何tcp是可靠传输,udp是不可靠传输
tcp在传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发送往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以tcp是可靠的。
而udp发送数据,对端是不会返回确认信息的,因此不可靠。
### 2.send(字节流) 和recv(1024) 及sendall
recv里指定的1024意思是从缓存里一次拿出了1024个字节的数据。
send的字节流是先存放入己端缓存,然后由协议控制将缓存内容法网对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失。
发送字节之前,先发送一段该字节流的长度,然后接收字节流长度的数据。
缺陷:由于程序的运行速度远快于网络传输速度,会因网络延迟造成性能损耗。
? 为字节流加入自定义固定长度报头,报头中包含字节流长度,然后send一次到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据。使用struct模块,可以辅助实现此功能。
# 该模块可以把一个类型,如数字,转成固定长度的bytes。
struct.pack(fmt,v1,v2,…)
返回的是一个字符串,是参数按照fmt数据格式组合而成
struct.unpack(fmt,string)
按照给定数据格式解开(通常都是由struct.pack进行打包)数据,返回值是一个tuple
? 服务端:
##——————————————————————————————————————server端远程执行指令解决粘包问题
import subprocess
import struct
from socket import *
server = socket(AF_INET, SOCK_STREAM)
server.setsockopt(SOL_SOCKET, SO_REUSEADDR)
server.bind((‘127.0.0.1‘, 8080))
server.listen(5)
while True:
conn, client_addr = server.accept()
while True:
try:
cmd = conn.recv(1024)
obj = subprocess.Popen(cmd.decode(‘utf-8‘),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout = obj.stdout.read()
stderr = obj.stderr.read()
total_size = len(stdout) + len(stderr)
# 先发送数据大小
conn.send(struct.pack(‘i‘, total_size))
# 再发送真正的数据
conn.send(stdout)
conn.send(stderr)
except Exception:
break
conn.close()
server.close()
? 客户端:
##——————————————————————————————————————client端远程执行指令解决粘包问题
import struct
from socket import *
client = socket(AF_INET, SOCK_STREAM)
client.connect((‘127.0.0.1‘, 8080)) # connect__连接
while True:
cmd = input(‘>>>:‘).strip()
if len(cmd) == 0: # 禁止发送空,规避可能的粘包问题
continue
client.send(cmd.encode(‘utf-8‘))
# 先接受数据长度(接收固定字节的数据)
n = 0
header = b‘‘
while n < 4:
data = client.recv(1)
header += data
n += 1
total_size = struct.unpack(‘i‘, header)[0] # unpack出是一个元组,取第一个数据
# 收真正的数据
recv_size = 0
res = b‘‘
while recv_size < total_size:
data = client.recv(1024)
res += data
recv_size += len(data)
print(res.decode(‘gbk‘)) # windows下的系统命令,要以gbkg格式解码
client.close()
客户端:
##——————————————————————————————————————server端————定制复杂的报头
import os
import struct
import json
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()
print(conn)
print(client_addr)
while True:
try:
msg = conn.recv(1024).decode(‘utf-8‘)
cmd, file_path = msg.split()
print(cmd, type(cmd), file_path)
if cmd == ‘get‘:
# 一、制作报头
print(os.path.getsize(file_path))
print(os.path.basename(file_path))
header_dic = {
‘total_size‘: os.path.getsize(file_path),
‘filename‘: os.path.basename(file_path),
‘md5‘: ‘123123123123‘}
print(header_dic)
header_json = json.dumps(header_dic) # 报头字典使用json序列化
header_json_bytes = header_json.encode(‘utf-8‘) # 序列化的字符串,转为bytes类型
# 二、发送数据
# 1、先发送报头长度
header_size = len(header_json_bytes)
conn.send(struct.pack(‘i‘, header_size))
# 2、再发送报头
conn.send(header_json_bytes)
# 3、最后发送真是的数据
with open(r‘%s‘ % file_path, mode=‘rb‘) as f:
for line in f:
conn.send(line)
except Exception:
break
conn.close()
server.close()
服务端:
##——————————————————————————————————————client端----定制复杂的报头import structimport jsonfrom socket import *client = socket(AF_INET, SOCK_STREAM)client.connect((‘127.0.0.1‘, 8080))while True: cmd = input(‘>>>:‘).strip() # get 文件路径 if len(cmd) == 0: continue client.send(cmd.encode(‘utf-8‘)) # 1.先接收报头的长度 res = client.recv(4) # 我们已知报头长度定长为4 header_size = struct.unpack(‘i‘, res)[0] # 2.再接收报头 header_json_bytes = client.recv(header_size) header_json = header_json_bytes.decode(‘utf-8‘) header_dic = json.loads(header_json) print(header_dic) # 3.最后接收真实的数据 total_size = header_dic[‘total_size‘] filename = header_dic[‘filename‘] recv_size = 0 with open(r‘D:\%s‘ % filename, mode=‘wb‘) as f: while recv_size < total_size: data = client.recv(1024) f.write(data) recv_size += len(data)client.close()
标签:ast 多个 取数据 read 字段 产生 recv 字节 完成
原文地址:https://www.cnblogs.com/chaochaofan/p/14930262.html