网络编程 黏包
分类:
IT文章
•
2022-04-15 23:49:11
一,黏包现象
同时执行多条命令之后,得到的结果很可能只有一部分,在执行其他命令的时候又接收到之前执行的另外一部分结果,这种显现就是黏包。
我们通过一段简单程序来看看黏包现象:
服务器:
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',9090))
sk.listen()
conn,addr = sk.accept()
conn.send(b'hello')
conn.send(b'world')
conn.close()
sk.close()
View Code
客户端:
import socket
sk = socket.socket()
sk.connect_ex(('127.0.0.1',9090))
msg1 = sk.recv(1024)
msg2 = sk.recv(1024)
print(msg1)
print(msg2)
sk.close()
View Code
黏包例子2:
服务器:
import socket
sk=socket.socket()
sk.bind(('127.0.0.1',8090))
sk.listen()
conn,addr=sk.accept()
while True:
cmd=input(">>>")
if cmd=='q':
conn.send(b'q')
break
conn.send(cmd.encode('gbk'))
res=conn.recv(1024).decode('gbk')
print(res)
conn.close()
sk.close()
客户端:
import socket
import subprocess
sk=socket.socket()
sk.connect(('127.0.0.1',8090))
while True:
cmd=sk.recv(1024).decode('gbk')
if cmd=='q':
break
res=subprocess.Popen(cmd,shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
sk.send(res.stdout.read())
sk.send(res.stderr.read())
sk.close()
View Code
同时执行多条命令之后,得到的结果很可能只有一部分,在执行其他命令时又会接到之前执行的另外一部分结果,这种就是黏包。
只有tcp有黏包现象,udp不会黏包。
server端
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8898)) #把地址绑定到套接字
sk.listen() #监听链接
conn,addr = sk.accept() #接受客户端链接
ret = conn.recv(1024) #接收客户端信息
print(ret) #打印客户端信息
conn.send(b'hi') #向客户端发送信息
conn.close() #关闭客户端套接字
sk.close() #关闭服务器套接字(可选)
tcp_server
client端
import socket
sk = socket.socket() # 创建客户套接字
sk.connect(('127.0.0.1',8898)) # 尝试连接服务器
sk.send(b'hello!')
ret = sk.recv(1024) # 对话(发送/接收)
print(ret)
sk.close() # 关闭客户套接字
tcp_client
服务器:
import socket
import subprocess
sk = socket.socket()
sk.bind(('127.0.0.1',9090))
sk.listen()
conn,addr = sk.accept()
while 1:
cmd = conn.recv(1024).decode('utf-8')
res = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
std_out = res.stdout.read() # 读取正确的返回信息
std_err = res.stderr.read() # 读取错误的返回信息
if std_out:
conn.send(std_out)
else:
conn.send(std_err)
conn.close()
sk.close()
tcp_server
客户端;
import socket
sk = socket.socket()
sk.connect_ex(('127.0.0.1',9090))
while 1:
cmd = input('请输入一个命令>>>') #输入windows命令 比如:dir ipconfig
sk.send(cmd.encode('utf-8'))
print(sk.recv(204800000).decode('gbk'))
sk.close()
tcp_client
黏包成因
TCP协议中的数据传递
tcp协议的拆包机制
当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。
当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。
MTU是Maximum Transmission Unit的缩写。意思是网络上传送的最大数据包。MTU的单位是字节。 大部分网络设备的MTU都是1500。
如果本机的MTU比网关的MTU大,大的数据包就会被拆开来传送,
这样会产生很多数据包碎片,增加丢包率,降低网络速度。
面向流的通信特点和Nagle算法
TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。
收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。
这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。
可靠黏包的tcp协议:tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
View Code
基于tcp协议特点的黏包现象成因
发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据。
也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。
而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。
怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
socket数据传输过程中的用户态与内核态说明
例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
UDP不会发生黏包
UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。
不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。
不可靠不黏包的udp协议:udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y;x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠。
View Code
补充说明:
用UDP协议发送时,用sendto函数最大能发送数据的长度为:65535- IP头(20) – UDP头(8)=65507字节。用sendto函数发送数据时,如果发送数据长度大于该值,则函数会返回错误。(丢弃这个包,不进行发送)
用TCP协议发送时,由于TCP是数据流协议,因此不存在包大小的限制(暂不考虑缓冲区的大小),这是指在用send函数时,数据长度参数不受限制。而实际上,所指定的这段数据并不一定会一次性发送出去,如果这段数据比较长,会被分段发送,如果比较短,可能会等待和下一次数据一起发送。
View Code
三,会发生黏包的两种情况
1,发送方的缓存机制:发送端需要等缓冲区满才发送出去,造成黏包(发送数据时间间隔很短,数据很小,会合到一起,产生黏包)
例:连续send两次且数据很小
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8090))
sk.send(b'hello')
sk.send(b'egg')
sk.close()
发送端
接收端:
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8090))
sk.listen()
conn,addr = sk.accept()
ret2 = conn.recv(10)
print(ret2)
conn.close()
sk.close()
接收端
2,接收方的缓存机制:接收不及时接收缓冲区的包,造成多个包接收(客户端发送一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿走上次剩余的数据,产生黏包。)
例:连续recv两次且第一个recv接收的数据小
发送端:
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8090))
sk.send(b'hello,egg')
sk.close()
发送端
接收端:
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8090))
sk.listen()
conn,addr = sk.accept()
ret = conn.recv(2)
ret2 = conn.recv(10)
print(ret)
print(ret2)
conn.close()
sk.close()
接收端
总结:
1,表面上看,黏包问题主要是因为发送端和接收端的缓存机制、tcp协议面向流通信的特点。
2,实际上,主要还是因为接收端不知道消息之间的界限,不知道一次性提取多少次字节的数据所造成的。
四,黏包的解决
1,将要发送的字节流总大小发给接收端,然后接收端来一个循环接收完所有数据。
服务端:
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8080))
sk.listen()
conn,addr = sk.accept()
while True:
cmd = input('>>>')
if cmd == 'q':
conn.send(b'q')
break
conn.send(cmd.encode('gbk'))
num = conn.recv(1024).decode('utf-8') # 2048
conn.send(b'ok')
res = conn.recv(int(num)).decode('gbk')
print(res)
conn.close()
sk.close()
服务器
客户端:
import socket
import subprocess
sk = socket.socket()
sk.connect(('127.0.0.1',8080))
while True:
cmd = sk.recv(1024).decode('gbk')
if cmd == 'q':
break
res = subprocess.Popen(cmd,shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
std_out = res.stdout.read()
std_err = res.stderr.read()
sk.send(str(len(std_out)+len(std_err)).encode('utf-8')) #2000
sk.recv(1024) # ok
sk.send(std_out)
sk.send(std_err)
sk.close()
客户端
问题:程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网路延迟带来的性能损耗。send sendto在超过一定范围的时候会报错
2,进阶方案
struck模块,该模块可以把一个类型,例,数字,转换成固定的长度bytes类型
import struct
s=struct.pack('i',23) # i ,整数类型,不管数字有多大 ,都转换成4个字节长度
print(s) #b'x84x1ax00x00'
s1=struct.unpack('i',s) #取出数字
print(s1) #元组(23,)
import json,struct
#假设通过客户端上传1T:1073741824000的文件a.txt
#为避免粘包,必须自定制报头
header={'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T数据,文件路径和md5值
#为了该报头能传送,需要序列化并且转为bytes
head_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化并转成bytes,用于传输
#为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节
head_len_bytes=struct.pack('i',len(head_bytes)) #这4个字节里只包含了一个数字,该数字是报头的长度
#客户端开始发送
conn.send(head_len_bytes) #先发报头的长度,4个bytes
conn.send(head_bytes) #再发报头的字节格式
conn.sendall(文件内容) #然后发真实内容的字节格式
#服务端开始接收
head_len_bytes=s.recv(4) #先收报头4个bytes,得到报头长度的字节格式
x=struct.unpack('i',head_len_bytes)[0] #提取报头的长度
head_bytes=s.recv(x) #按照报头长度x,收取报头的bytes格式
header=json.loads(json.dumps(header)) #提取报头
#最后根据报头的内容提取真实的数据,比如
real_data_len=s.recv(header['file_size'])
s.recv(real_data_len)
View Code
使用struck解决黏包:
把报头做成字典,字典里要包含将要发送真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节。
发送时:先发报头长度,再编码报头内容然后发送,最后发真实内容。
接收时:先收报头长度,用struct取出来,根据取出的长度收取报头内容,解码,反序列化,从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容
# 发送端
import os
import json
import struct
import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8090))
buffer = 1024
# 发送文件
head = {'filepath':r'F:Program FilesfeiqRecv Filesday23',
'filename':r'04 python fullstack s9day23 组合2.mp4',
'filesize':None}
file_path = os.path.join(head['filepath'],head['filename'])
filesize = os.path.getsize(file_path)
head['filesize'] = filesize
json_head = json.dumps(head) # 字典转成了字符串
bytes_head = json_head.encode('utf-8') # 字符串转bytes
# 计算head的长度
head_len = len(bytes_head) # 报头的长度
pack_len = struct.pack('i',head_len)
sk.send(pack_len) # 先发报头的长度
sk.send(bytes_head) # 再发送bytes类型的报头
with open(file_path,'rb') as f:
while filesize:
print(filesize)
if filesize >= buffer:
content = f.read(buffer) # 每次读出来的内容
sk.send(content)
filesize -= buffer
else:
content = f.read(filesize)
sk.send(content)
break
sk.close()
发送端
接收端:
import json
import socket
import struct
sk = socket.socket()
sk.bind(('127.0.0.1',8090))
sk.listen()
buffer = 1024
conn,addr = sk.accept()
# 接收
head_len = conn.recv(4)
head_len = struct.unpack('i',head_len)[0]
json_head = conn.recv(head_len).decode('utf-8')
head = json.loads(json_head)
filesize = head['filesize']
with open(head['filename'],'wb') as f:
while filesize:
print(filesize)
if filesize >= buffer:
content = conn.recv(buffer)
f.write(content)
filesize -= buffer
else:
content = conn.recv(filesize)
f.write(content)
break
conn.close()
sk.close()
接收端
服务器:
import socket
import json
sk = socket.socket()
sk.bind(('127.0.0.1',9090))
sk.listen()
conn,addr = sk.accept()
dic_str = conn.recv(1024).decode('utf-8')
dic = json.loads(dic_str) # 反序列化 得到字典 opt filename content
if dic['opt'] == 'upload':
'''接收文件'''
filename = '1'+dic['filename'] # 将文件名字修改,防止重名
with open(filename,'w',encoding='utf-8') as f:
f.write(dic['content'])
elif dic['opt'] == 'download':
'''给客户端传输文件'''
conn.close()
sk.close()
服务器
客户端:
import socket
import os
import json
sk = socket.socket()
sk.connect(('127.0.0.1',9090))
l = ['upload','download']
for i,v in enumerate(l):
print(i+1,v)
dic = {'opt':None,'filename':None,'content':None}
while 1:
opt = input("请输入功能选项>>>") # 客户要执行的操作选项
if opt == '1':
'''upload'''
file_dir = input('请输入文件路径>>>') # 'E:/sylar/python_workspace/day34/作业/时间同步机制_client.py'
file_name = os.path.basename(file_dir) # 获取文件名字
with open(file_dir,'r',encoding='utf-8') as f:
content = f.read() # 文件内容
dic['opt'] = l[int(opt)-1]
dic['filename'] = file_name
dic['content'] = content
dic_str = json.dumps(dic) # 将字典序列化成一个字符串形式的字典
sk.send(dic_str.encode('utf-8')) # 发送给服务器
elif opt == '2':
'''download'''
pass
else:
print('有误')
客户端
服务器:
import socket
import json
sk = socket.socket()
sk.bind(('127.0.0.1',9090))
sk.listen()
conn,addr = sk.accept()
dic_str = conn.recv(1024).decode('utf-8')
dic = json.loads(dic_str)# 反序列化 得到字典 opt filename filesize
if dic['opt'] == 'upload':
'''接收文件'''
filename = '1'+dic['filename'] # 将文件名字修改,防止重名
with open(filename,'wb') as f:
while dic['filesize']:
content = conn.recv(1024)
f.write(content)
dic['filesize'] -= len(content)
elif dic['opt'] == 'download':
'''给客户端传输文件'''
conn.close()
sk.close()
服务器
客户端:
import socket
import os
import json
import time
sk = socket.socket()
sk.connect(('127.0.0.1',9090))
l = ['upload','download']
for i,v in enumerate(l):
print(i+1,v)
dic = {'opt':None,'filename':None,'filesize':None}
while 1:
opt = input("请输入功能选项>>>") # 客户要执行的操作选项
if opt == '1':
'''upload'''
file_dir = input('请输入文件路径>>>')# 'E:/sylar/python_workspace/day34/作业/时间同步机制_client.py'
file_name = os.path.basename(file_dir)# 获取文件名字
file_size = os.path.getsize(file_dir)# 获取文件大小
dic['opt'] = l[int(opt)-1]
dic['filename'] = file_name
dic['filesize'] = file_size
dic_str = json.dumps(dic) # 将字典序列化成一个字符串形式的字典
time.sleep(1)
sk.send(68 + dic_str.encode('utf-8')) # 发送给服务器
with open(file_dir,'rb') as f:
while file_size:
content = f.read(1024) # 文件内容
sk.send(content)
file_size -= len(content)
elif opt == '2':
'''download'''
pass
else:
print('有误')
客户端
服务器:
import socket
import json
import struct
sk = socket.socket()
sk.bind(('127.0.0.1',9090))
sk.listen()
conn,addr = sk.accept()
dic_size = conn.recv(4) # 先接受4字节长度的一个bytes, 代表字典的大小
dic_size = struct.unpack('i',dic_size)[0] # 将这个特殊的bytes转变成原数字
dic_str = conn.recv(dic_size).decode('utf-8') # 根据字典大小去获取字典,以免和底下获取文件内容发生粘包
dic = json.loads(dic_str) # 反序列化 得到字典 opt filename filesize
if dic['opt'] == 'upload':
'''接收文件'''
filename = '1'+dic['filename'] # 将文件名字修改,防止重名
with open(filename,'wb') as f:
while dic['filesize']:
content = conn.recv(1024)
f.write(content)
dic['filesize'] -= len(content)
elif dic['opt'] == 'download':
'''给客户端传输文件'''
conn.close()
sk.close()
服务器
客户端:
import socket
import os
import json
import struct
sk = socket.socket()
sk.connect(('127.0.0.1',9090))
l = ['upload','download']
for i,v in enumerate(l):
print(i+1,v)
dic = {'opt':None,'filename':None,'filesize':None}
while 1:
opt = input("请输入功能选项>>>") # 客户要执行的操作选项
if opt == '1':
'''upload'''
file_dir = input('请输入文件路径>>>')# 'E:/sylar/python_workspace/day34/作业/时间同步机制_client.py'
file_name = os.path.basename(file_dir)# 获取文件名字
file_size = os.path.getsize(file_dir)# 获取文件大小
dic['opt'] = l[int(opt)-1]
dic['filename'] = file_name
dic['filesize'] = file_size
dic_str = json.dumps(dic)# 将字典序列化成一个字符串形式的字典
dic_size = len(dic_str)# 获取字典的大小
ds = struct.pack('i',dic_size) # 把一个小于21.3E的一个数字转变成一个4字节长度的bytes
sk.send(ds + dic_str.encode('utf-8')) # 发送给服务器
with open(file_dir,'rb') as f:
while file_size:
content = f.read(1024) # 文件内容
sk.send(content)
file_size -= len(content)
elif opt == '2':
'''download'''
pass
else:
print('有误')
sk.close()
客户端
使用struct解决黏包
借助struct模块,我们知道长度数字可以被转换成一个标准大小的4字节数字。因此可以利用这个特点来预先发送数据长度。
发送时 |
接收时 |
先发送struct转换好的数据长度4字节 |
先接受4个字节使用struct转换成数字来获取要接收的数据长度 |
再发送数据 |
再按照长度接收数据 |
import socket,struct,json
import subprocess
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加
phone.bind(('127.0.0.1',8080))
phone.listen(5)
while True:
conn,addr=phone.accept()
while True:
cmd=conn.recv(1024)
if not cmd:break
print('cmd: %s' %cmd)
res=subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
err=res.stderr.read()
print(err)
if err:
back_msg=err
else:
back_msg=res.stdout.read()
conn.send(struct.pack('i',len(back_msg))) #先发back_msg的长度
conn.sendall(back_msg) #在发真实的内容
conn.close()
服务端
import socket,time,struct
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(('127.0.0.1',8080))
while True:
msg=input('>>: ').strip()
if len(msg) == 0:continue
if msg == 'quit':break
s.send(msg.encode('utf-8'))
l=s.recv(4)
x=struct.unpack('i',l)[0]
print(type(x),x)
# print(struct.unpack('I',l))
r_s=0
data=b''
while r_s < x:
r_d=s.recv(1024)
data+=r_d
r_s+=len(r_d)
# print(data.decode('utf-8'))
print(data.decode('gbk')) #windows默认gbk编码
客户端
我们还可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节(4个自己足够用了)
发送时 |
接收时 |
先发报头长度
|
先收报头长度,用struct取出来 |
再编码报头内容然后发送 |
根据取出的长度收取报头内容,然后解码,反序列化 |
最后发真实内容 |
从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容
|
import socket,struct,json
import subprocess
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加
phone.bind(('127.0.0.1',8080))
phone.listen(5)
while True:
conn,addr=phone.accept()
while True:
cmd=conn.recv(1024)
if not cmd:break
print('cmd: %s' %cmd)
res=subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
err=res.stderr.read()
print(err)
if err:
back_msg=err
else:
back_msg=res.stdout.read()
headers={'data_size':len(back_msg)}
head_json=json.dumps(headers)
head_json_bytes=bytes(head_json,encoding='utf-8')
conn.send(struct.pack('i',len(head_json_bytes))) #先发报头的长度
conn.send(head_json_bytes) #再发报头
conn.sendall(back_msg) #在发真实的内容
conn.close()
服务器
from socket import *
import struct,json
ip_port=('127.0.0.1',8080)
client=socket(AF_INET,SOCK_STREAM)
client.connect(ip_port)
while True:
cmd=input('>>: ')
if not cmd:continue
client.send(bytes(cmd,encoding='utf-8'))
head=client.recv(4)
head_json_len=struct.unpack('i',head)[0]
head_json=json.loads(client.recv(head_json_len).decode('utf-8'))
data_len=head_json['data_size']
recv_size=0
recv_data=b''
while recv_size < data_len:
recv_data+=client.recv(1024)
recv_size+=len(recv_data)
print(recv_data.decode('utf-8'))
#print(recv_data.decode('gbk')) #windows默认gbk编码
客户端
新模块: subprocess
执行系统命令
r = subprocess.Popen('ls',shell=True,stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
subprocess.Popen(a,b,c,d)
a: 要执行的系统命令(str)
b: shell = True 表示确定我当前执行的命令为系统命令
c: 表示正确信息的输出管道
d: 表示错误信息的输出管道
tcp协议:面向数据流形式的特点
tcp协议会发生粘包,因为两个机制,一个拆包机制,一个合包机制(nagle算法)
udp协议不会发生粘包,因为udp协议是面向数据包形式的通信
***黏包:就是因为接收端不知道如何接收数据,造成接收数据的混乱的问题,只发生在tcp协议上,因为tcp协议的特点是面向数据流形式的传输
黏包的发生主要是因为tcp协议有两个机制:合包机制(nagle算法),拆包机制
subprocess 模块 有一个方法可以执行系统命令 Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
struct 模块 有一个方法可以将21.3E以内的数据,打包成4个长度的bytes
r = struct.pack('i',num)
struct.unpack('i',r)
import os
import subprocess
# r = os.popen('ls')
# print(r.read())
r = subprocess.Popen('dir',shell=True,stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
print('stdout:',r.stdout.read().decode('gbk'))
print('stderr:',r.stderr.read().decode('gbk'))
print('stdout:',r.stdout.read().decode('gbk'))
#操作windows系统返回的bytes类型解码用
file_size = os.path.getsize('时间同步机制_client.py')
print(file_size) #获取文件大小
with open('时间同步机制_client.py','rb') as f:
while file_size:
content = f.read(1024) # 文件内容
file_size -= len(content)
print(file_size)
import os
import json
dic = {1:2}
s = 'E:/sylar/python_workspace/day34/作业/__init__.py'
dic_str = json.dumps(dic)
print(type(dic_str))
print(os.path.basename(s)) #通过文件路径找文件名
E:/sylar/python_workspace/day33/udp_时间同步机制_client.py
struct模块:
有一个方法,可以将一个21.3E以内的数字,转变成一个固定长度的bytes数据,长度为4b
res = struct.pack('i',num)
'i' : 表示的是int类型的数据
num : 表示要转换的数据
re = struct.unpack('i',res)将bytes数据转变回原数据
re是一个元组,原数据保存在元组的下标0的地方
import struct
s = struct.pack('i',2)
print(s,len(s))
s = struct.unpack('i',s)
print(s)
print(b'hi' + b'wo')
FTP作业:上传下载文件
import socket
import struct
import json
import subprocess
import os
class MYTCPServer:
address_family = socket.AF_INET
socket_type = socket.SOCK_STREAM
allow_reuse_address = False
max_packet_size = 8192
coding='utf-8'
request_queue_size = 5
server_dir='file_upload'
def __init__(self, server_address, bind_and_activate=True):
"""Constructor. May be extended, do not override."""
self.server_address=server_address
self.socket = socket.socket(self.address_family,
self.socket_type)
if bind_and_activate:
try:
self.server_bind()
self.server_activate()
except:
self.server_close()
raise
def server_bind(self):
"""Called by constructor to bind the socket.
"""
if self.allow_reuse_address:
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.bind(self.server_address)
self.server_address = self.socket.getsockname()
def server_activate(self):
"""Called by constructor to activate the server.
"""
self.socket.listen(self.request_queue_size)
def server_close(self):
"""Called to clean-up the server.
"""
self.socket.close()
def get_request(self):
"""Get the request and client address from the socket.
"""
return self.socket.accept()
def close_request(self, request):
"""Called to clean up an individual request."""
request.close()
def run(self):
while True:
self.conn,self.client_addr=self.get_request()
print('from client ',self.client_addr)
while True:
try:
head_struct = self.conn.recv(4)
if not head_struct:break
head_len = struct.unpack('i', head_struct)[0]
head_json = self.conn.recv(head_len).decode(self.coding)
head_dic = json.loads(head_json)
print(head_dic)
#head_dic={'cmd':'put','filename':'a.txt','filesize':123123}
cmd=head_dic['cmd']
if hasattr(self,cmd):
func=getattr(self,cmd)
func(head_dic)
except Exception:
break
def put(self,args):
file_path=os.path.normpath(os.path.join(
self.server_dir,
args['filename']
))
filesize=args['filesize']
recv_size=0
print('----->',file_path)
with open(file_path,'wb') as f:
while recv_size < filesize:
recv_data=self.conn.recv(self.max_packet_size)
f.write(recv_data)
recv_size+=len(recv_data)
print('recvsize:%s filesize:%s' %(recv_size,filesize))
tcpserver1=MYTCPServer(('127.0.0.1',8080))
tcpserver1.run()
服务器
import socket
import struct
import json
import os
class MYTCPClient:
address_family = socket.AF_INET
socket_type = socket.SOCK_STREAM
allow_reuse_address = False
max_packet_size = 8192
coding='utf-8'
request_queue_size = 5
def __init__(self, server_address, connect=True):
self.server_address=server_address
self.socket = socket.socket(self.address_family,
self.socket_type)
if connect:
try:
self.client_connect()
except:
self.client_close()
raise
def client_connect(self):
self.socket.connect(self.server_address)
def client_close(self):
self.socket.close()
def run(self):
while True:
inp=input(">>: ").strip()
if not inp:continue
l=inp.split()
cmd=l[0]
if hasattr(self,cmd):
func=getattr(self,cmd)
func(l)
def put(self,args):
cmd=args[0]
filename=args[1]
if not os.path.isfile(filename):
print('file:%s is not exists' %filename)
return
else:
filesize=os.path.getsize(filename)
head_dic={'cmd':cmd,'filename':os.path.basename(filename),'filesize':filesize}
print(head_dic)
head_json=json.dumps(head_dic)
head_json_bytes=bytes(head_json,encoding=self.coding)
head_struct=struct.pack('i',len(head_json_bytes))
self.socket.send(head_struct)
self.socket.send(head_json_bytes)
send_size=0
with open(filename,'rb') as f:
for line in f:
self.socket.send(line)
send_size+=len(line)
print(send_size)
else:
print('upload successful')
client=MYTCPClient(('127.0.0.1',8080))
client.run()
客户端
总结:
一、TCP黏包问题
TCP黏包问题是因为发送方把若干数据发送,接收方收到数据时候黏在一包,从接受缓冲区来看,后一包的数据黏在前一包的尾部。
二、黏包出现的原因
TCP黏包问题主要出现在两个方面
(1)发送方问题
首先TCP会默认使用Nagle算法,Nagle算法主要做两件事。
第一:上一包分组得到确认,才会发送下一分组。
第二:收集多个小组,在一个确认到来时一起发送。
由此可见Nagle算法会使得数据在发送方造成黏包问题 。
(2)接收方问题
TCP接收方接收到分组的时候,并不会立刻提交到应用层处理,收到的数据放在接收缓存里面,然后应用程序会主动从接受缓存里读取接收的分组,这样以来,如果TCP接收分组的速度大于应用读取分组的速度,多个包的数据会存至缓存区里面,应用读取数据就可能会产生黏包问题。
三、什么时候处理黏包问题
(1)如果每次利用TCP发送数据,就与对方建立连接,然后发送完数据就关闭连接这样就不会出现黏包问题(大家都知道只发送一个数据包)
(2)如果发送的数据无结构,比如文件的传输,只要发送方一直发送,接收方只管接收到储存的数据,此时也不用考虑黏包问题。
(3)如果在连接的一段时间内发送的数据毫无关系,我们就要考虑黏包问题了。
比如:你要发送一段话
I love you.
I want play.
如果产生了黏包问题,接收方可能会傻眼,你让我干啥?所以一般会在数据前加一个长度之类的包,确保接收。
四、处理黏包现象
(1)发送方
发送方产生黏包问题的主要原因在于Nagle算法,我们可以通过关闭Nagle算法来解决,使用TCP_NODELAY选项来关闭Nagle算法。
(2)接收方
由于TCP没有处理接收方黏包现象的机制,我们只能在应用层进行处理。
(3)应用层处理
解决方法就是循环处理:应用程序在处理从缓存读来的分组时,读完一条数据时,就应该循环读下一条数据,直到所有的数据都被处理;但是如何判断每条数据的长度呢?
两种途径:
1)格式化数据:每条数据有固定的格式(开始符、结束符),这种方法简单易行,但选择开始符和结束符的时候一定要注意每条数据的内部一定不能出现开始符或结束符。
2)发送长度:发送每条数据的时候,将数据的长度一并发送,比如可以选择每条数据的前4位是数据的长度,应用层处理时可以根据长度来判断每条数据的开始和结束。