粘包
黏包现象
让我们基于tcp先制作一个远程执行命令的程序(命令ls -l ; lllllll ; pwd)
执行远程命令的模块
需要用到模块subprocess
subprocess通过子进程来执行外部指令,并通过input/output/error管道,获取子进程的执行的返回信息。
import os import subprocess ret = os.popen('dir').read() print(ret) print('*'*50) ret = subprocess.Popen('dir',shell=True,stdout=subprocess.PIPE, stderr=subprocess.PIPE) print(ret.stdout) print(ret.stderr)
shell= True 可以执行一个普通系统命令
stdout 表示一个容器,返回正常的信息
stderr 存放错误信息的容器
执行输出:
驱动器 E 中的卷是 file 卷的序列号是 8077-D7B9 E:python_scriptday30黏包 的目录 2018/05/07 14:54 <DIR> . 2018/05/07 14:54 <DIR> .. 2018/05/07 14:54 236 a.py 1 个文件 236 字节 2 个目录 183,394,840,576 可用字节 ************************************************** <_io.BufferedReader name=3> <_io.BufferedReader name=4>
执行一个错误的命令
import os import subprocess ret = os.popen('ls').read() print(ret) print('*'*50) ret = subprocess.Popen('ls',shell=True,stdout=subprocess.PIPE, stderr=subprocess.PIPE) print('out:',ret.stdout.read().decode('gbk')) print('err:',ret.stderr.read().decode('gbk'))
执行输出:
os.popen() 执行一个错误的命令,显示乱码
而subprocess则不会,它还是比较完善的。
基于tcp协议实现的黏包
用server端,让客户端执行一个命令
server.py
import socket sk = socket.socket() sk.bind(('127.0.0.1',9000)) sk.listen() conn,addr = sk.accept() while True: cmd = input('>>>') conn.send(cmd.encode('utf-8')) if cmd == 'q': break ret1 = conn.recv(1024) print('stdout : ', ret1.decode('gbk')) ret2 = conn.recv(1024) print('stderr : ',ret2.decode('gbk')) conn.close() sk.close()
client.py
import socket import subprocess sk = socket.socket() sk.connect(('127.0.0.1',9000)) while True: cmd = sk.recv(1024).decode('utf-8') print(cmd) if cmd == 'q':break ret = subprocess.Popen(cmd,shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out = ret.stdout.read() err = ret.stderr.read() print(out,'***** ',err) sk.send(b'out :'+out) sk.send(b'error :'+err) sk.close()
先执行server.py,再执行client.py,执行效果如下:
首先是执行了help命令,再执行dir命令
但是为什么都是显示help的命令结果呢?
这就是黏包现象
因为每次执行,固定为1024字节。它只能接收到1024字节,那么超出部分怎么办?
等待下一次执行命令dir时,优先执行上一次,还没有传完的信息。传完之后,再执行dir命令
总结:
发送过来的一整条信息
由于server端没有及时接受
后来发送的数据和之前没有接收完的数据黏在了一起
这就是著名的黏包现象
那么udp会发现黏包现象吗?实践一下,就知道了
基于udp协议实现的黏包
server.py
#_*_coding:utf-8_*_ from socket import * import subprocess ip_port=('127.0.0.1',9000) bufsize=1024 udp_server=socket(AF_INET,SOCK_DGRAM) udp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) udp_server.bind(ip_port) while True: #收消息 cmd,addr=udp_server.recvfrom(bufsize) print('用户命令----->',cmd) #逻辑处理 res=subprocess.Popen(cmd.decode('utf-8'),shell=True,stderr=subprocess.PIPE,stdin=subprocess.PIPE,stdout=subprocess.PIPE) stderr=res.stderr.read() stdout=res.stdout.read() #发消息 udp_server.sendto(stderr,addr) udp_server.sendto(stdout,addr) udp_server.close()
client.py
from socket import * ip_port=('127.0.0.1',9000) bufsize=1024 udp_client=socket(AF_INET,SOCK_DGRAM) while True: msg=input('>>: ').strip() udp_client.sendto(msg.encode('utf-8'),ip_port) err,addr=udp_client.recvfrom(bufsize) out,addr=udp_client.recvfrom(bufsize) print(err) if err: print('error : %s'%err.decode('gbk'),end='') if out: print(out.decode('gbk'), end='')
先执行server.py,再执行client.py,执行效果如下:
>>: ipconfig Traceback (most recent call last): File "E:/python_script/day30/黏包/client.py", line 11, in <module> out,addr=udp_client.recvfrom(bufsize) OSError: [WinError 10040] 一个在数据报套接字上发送的消息大于内部消息缓冲区或其他一些网络限制,或该用户用于接收数据报的缓冲区比数据报小。
在客户端执行ipconfig,就报错了,提示缓冲区过大。所以说udp不会出现黏包
总结:
只有TCP有粘包现象,UDP永远不会粘包
subprocess不能运行windows help命令,不是因为udp问题,而是subprocess问题。
一,粘包问题详情
1.只有TCP有粘包现象,UDP永远不会粘包
你的程序实际上无权直接操作网卡的,你操作网卡都是通过操作系统给用户程序暴露出来的接口,那每次你的程序要给远程发数据时,其实是先把数据从用户态copy到内核态,这样的操作是耗资源和时间的,频繁的在内核态和用户态之前交换数据势必会导致发送效率降低, 因此socket 为提高传输效率,发送方往往要收集到足够多的数据后才发送一次数据给对方。若连续几次需要send的数据都很少,通常TCP socket 会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
2,首先需要掌握一个socket收发消息的原理
发送端可以是1k,1k的发送数据。而接受端的应用程序可以2k,2k的提取数据,当然也有可能是3k或者多k提取数据,也就是说,应用程序是不可见的,因此TCP协议是面来那个流的协议,
这也是容易出现粘包的原因。而UDP是面向无连接的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任一字节的数据,这一点和TCP是很同的。
怎样定义消息呢?
认为对方一次性write/send的数据为一个消息,需要命的是当对方send一条信息的时候,无论底层怎么样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
例如基于TCP的套接字客户端往服务器端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看来根本不知道文件的字节流从何初开始,在何处结束。
二、黏包成因
直接原因:所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的
根本原因
发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
tcp协议的拆包机制
当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。 MTU是Maximum Transmission Unit的缩写。意思是网络上传送的最大数据包。MTU的单位是字节。
大部分网络设备的MTU都是1500。如果本机的MTU比网关的MTU大,大的数据包就会被拆开来传送,这样会产生很多数据包碎片,增加丢包率,降低网络速度。
面向流的通信特点和Nagle算法
1. TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket, 因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块, 然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。 2.UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式, 所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样, 对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。 3.对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的, 即便是你输入的是空内容(直接回车),那也不是空消息,也可以被发送,udp协议会帮你封装上消息头发送过去 4.udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包, 但是会丢数据,不可靠 5.可靠黏包的tcp协议:tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
基于tcp协议特点的黏包现象成因
socket数据传输过程中用户态和内核态说明:
发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据。
也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,
这也是容易出现粘包问题的原因。
而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。
怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,
TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区
例如基于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根本不会粘包,但是会丢数据,不可靠。
补充说明:
用UDP协议发送时,用sendto函数最大能发送数据的长度为:65535- IP头(20) – UDP头(8)=65507字节。用sendto函数发送数据时,如果发送数据长度大于该值,
则函数会返回错误。(丢弃这个包,不进行发送)
用TCP协议发送时,由于TCP是数据流协议,因此不存在包大小的限制(暂不考虑缓冲区的大小),这是指在用send函数时,数据长度参数不受限制。
而实际上,所指定的这段数据并不一定会一次性发送出去,如果这段数据比较长,会被分段发送,如果比较短,可能会等待和下一次数据一起发送。
三,两种情况下会发生粘包:
情况一 发送方的缓存机制
1,发送端需要等到本机的缓冲区满了以后才发出去,造成粘包(发送数据时间间隔很短,数据很小,python使用了优化算法,合在一起,产生粘包)
发送端内核态
如果数据包过小,不会立即发送。先缓存了一小下,通过优化算法,将2次或者多次数据包,一次发送。
如果数据包过大,分配发送。
如果这个时候,再来一个大的数据包,也会拆分包。那么就发生黏包了。
server.py
from socket import * ip_port = ('127.0.0.1', 8080) tcp_socket_server = socket(AF_INET, SOCK_STREAM) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(5) conn, addr = tcp_socket_server.accept() data1 = conn.recv(10) data2 = conn.recv(10) print('----->', data1.decode('utf-8')) print('----->', data2.decode('utf-8')) conn.close()
client.py
import socket BUFSIZE = 1024 ip_port = ('127.0.0.1', 8080) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) res = s.connect_ex(ip_port) s.send('hello'.encode('utf-8')) s.send('egg'.encode('utf-8'))
先执行server.py,再执行client.py
server.py输出:
-----> helloegg
----->
从代码中,可以看出。client发送了2次,第一次发送hello,第二次发送egg
服务端接收时了2次,但是第一次接收,直接是helloegg。第二次接收内容为空。
为什么呢?这个是因为发送端的优化机制,导致的黏包
情况二 接收方的缓存机制
接收端不及时接受缓冲区的包,造成多个包接受(客户端发送一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,就产生粘包)
server.py
from socket import * ip_port = ('127.0.0.1', 8080) tcp_socket_server = socket(AF_INET, SOCK_STREAM) tcp_socket_server.bind(ip_port) tcp_socket_server.listen(5) conn, addr = tcp_socket_server.accept() data1 = conn.recv(2) # 一次没有收完整 data2 = conn.recv(10) # 下次收的时候,会先取旧的数据,然后取新的 print('----->', data1.decode('utf-8')) print('----->', data2.decode('utf-8')) conn.close()
client.py
import socket BUFSIZE = 1024 ip_port = ('127.0.0.1', 8080) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) res = s.connect_ex(ip_port) s.send('hello egg'.encode('utf-8'))
先执行server.py,再执行client.py
server.py输出:
-----> he
-----> llo egg
从代码上来,client发送了2次数据给server端
server端,第一次接收2字节,第二次接收10字节。
所以第一次返回he,第二次,接收剩余的,返回llo egg
注意:conn永远不会接收到空数据,conn断开连接的时候recv收到一个空,那么连接就会等待
总结
黏包现象只发生在tcp协议中:
1.从表面上看,黏包问题主要是因为发送方和接收方的缓存机制、tcp协议面向流通信的特点。
2.实际上,主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的
四,拆包的发生情况
当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送过去
补充问题一:为何tcp是可靠传输,udp是不可靠传输
tcp在数据传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以tcp是可靠的
而udp发送数据,对端是不会返回确认信息的,因此不可靠
补充问题二:send(字节流)和recv(1024)及sendall是什么意思?
recv里指定的1024意思是从缓存里一次拿出1024个字节的数据
send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失。
五,粘包问题如何解决?
问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。
5-1 简单的解决方法(从表面解决):
在客户端发送下边添加一个时间睡眠,就可以避免粘包现象。在服务端接收的时候也要进行时间睡眠,才能有效的避免粘包情况。
客户端:
#!/usr/bin/env python
# coding:utf-8
import socket
import time
import subprocess
din=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
ip_port=('127.0.0.1',8080)
din.connect(ip_port)
din.send('hello world'.encode('utf-8'))
time.sleep(3)
din.send('sb'.encode('utf-8'))
服务端:
#!/usr/bin/env python
# coding:utf-8
import socket
import time
import subprocess
din=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
ip_port=('127.0.0.1',8080)
din.bind(ip_port)
din.listen(5)
conn,deer=din.accept()
data1=conn.recv(1024)
time.sleep(4)
data2=conn.recv(1024)
print(data1)
print(data2)
上面解决方法肯定会出现很多纰漏,因为你不知道什么时候传输完,时间暂停的长短都会有问题,长的话效率低,短的话不合适,所以这种方法是不合适的。
例子2:
客户端:
##客户端 import socket import struct phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(('127.0.0.1',9909)) while True: #1、发命令 cmd=input('>>: ').strip() #ls /etc if not cmd:continue phone.send(cmd.encode('utf-8')) #2、拿命令的结果,并打印 #第一步:先收报头(先拿到数据的长度) header=phone.recv(4) #只收4个,收报头 #第二步:从报头中解析出对真实数据的描述信息(数据的长度) total_size=struct.unpack('i',header)[0] #反解,i格式,从报头从拿出对我有用的信息 #第三步:接收真实的数据 ##循环来取 recv_size=0 recv_data=b'' #拼接操作,接收的都是bytes类型 while recv_size < total_size: ##收完while循环才会让你结束进入下次循环输入input res=phone.recv(1024) #1024是一个坑 recv_data+=res ###每收一次就做一次拼接; recv_size+=len(res) ##+收的数据长度 print(recv_data.decode('gbk')) phone.close()
服务端:
##服务端 import socket import subprocess import struct phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) # phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) phone.bind(('127.0.0.1',9909)) #0-65535:0-1024给操作系统使用 phone.listen(5) print('starting...') while True: # 链接循环 conn,client_addr=phone.accept() print(client_addr) while True: #通信循环 try: #1、收命令 cmd=conn.recv(8096) if not cmd:break #适用于linux操作系统 #2、执行命令,拿到结果 obj = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout=obj.stdout.read() stderr=obj.stderr.read() #3、把命令的结果返回给客户端 #第一步:制作固定长度的报头 真实数据的信息 #一定得是固定长度,这样接收端才知道接收多少个数据,才能提取出报头来,才能提取出有用的信息来 total_size = len(stdout) + len(stderr) #数据的长度 header=struct.pack('i',total_size) #制作报头 i就是4个bytes #第二步:把报头发送给客户端 ###三个send就是三个包,有可能会粘在一起;头要是固定标准固定长度,因为即使粘到一起了,我接收固定的长度,粘包也影响不到了 conn.send(header) #header本身就是bytes类型 #第三步:再发送真实的数据 #有头有数据才是互联网协议 conn.send(stdout) ##省去了那个+号,因为它们本身会产生粘包,优化出来了 conn.send(stderr) except ConnectionResetError: #适用于windows操作系统 break conn.close() phone.close()
这个解决方案只包含数据的长度,报头应该是对真实数据的描述信息,报头不仅包含数据长度的信息;命令的长度可能超过这个范围,用i模式就不行了
5-2 普通的解决方法(从根本看问题):
问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据
为字节流加上自定义固定长度报头,报头中包含字节流长度,然后依次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据。
struct模块
使用struct模块可以用于将Python的值根据格式符,转换为字符串(byte类型)
struct模块中最重要的三个函数是pack(), unpack(), calcsize()
pack(fmt, v1, v2, ...) 按照给定的格式(fmt),把数据封装成字符串(实际上是类似于c结构体的字节流) unpack(fmt, string) 按照给定的格式(fmt)解析字节流string,返回解析出来的tuple calcsize(fmt) 计算给定的格式(fmt)占用多少字节的内存
struct中支持的格式如下表:
Format | C Type | Python | 字节数 |
---|---|---|---|
x | pad byte | no value | 1 |
c | char | string of length 1 | 1 |
b | signed char | integer | 1 |
B | unsigned char | integer | 1 |
? | _Bool | bool | 1 |
h | short | integer | 2 |
H | unsigned short | integer | 2 |
i | int | integer | 4 |
I | unsigned int | integer or long | 4 |
l | long | integer | 4 |
L | unsigned long | long | 4 |
q | long long | long | 8 |
Q | unsigned long long | long | 8 |
f | float | float | 4 |
d | double | float | 8 |
s | char[] | string | 1 |
使用案例
#!/usr/bin/env python # coding:utf-8 import struct res = struct.pack('i', 123) printres, type(res), len(res)) # b'{x00x00x00' <class 'bytes'> 4 封装一个4个字节的包 res1 = struct.pack('q', 11122232323) print(res1, type(res1), len(res1)) # b'x03xccxefx96x02x00x00x00' <class 'bytes'> 8 封装一个8个字节的包 print(struct.unpack('i', res)[0]) # 拆包 print(struct.unpack('q', res1)[0]) # # 输出 # b'{x00x00x00' <class 'bytes'> 4 # b'x03xccxefx96x02x00x00x00' <class 'bytes'> 8 # (123,) # (11122232323,)
普通的客户端
在Windows下的Python3实现
# _*_ coding: utf-8 _*_
import socket
import struct
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',8880)) #连接服
while True:
# 发收消息
cmd = input('请你输入命令>>:').strip()
if not cmd:continue
phone.send(cmd.encode('utf-8')) #发送
#先收报头
header_struct = phone.recv(4) #收四个
unpack_res = struct.unpack('i',header_struct)
total_size = unpack_res[0] #总长度
#后收数据
recv_size = 0
total_data=b''
while recv_size<total_size: #循环的收
recv_data = phone.recv(1024) #1024只是一个最大的限制
recv_size+=len(recv_data) #
total_data+=recv_data #
print('返回的消息:%s'%total_data.decode('gbk'))
phone.close()
普通的服务端
#!/usr/bin/env python # coding:utf-8 import socket import subprocess import struct phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #买手机 phone.bind(('127.0.0.1',8880)) #绑定手机卡 phone.listen(5) #阻塞的最大数 print('start runing.....') while True: #链接循环 coon,addr = phone.accept()# 等待接电话 print(coon,addr) while True: #通信循环 # 收发消息 cmd = coon.recv(1024) #接收的最大数 print('接收的是:%s'%cmd.decode('utf-8')) #处理过程 res = subprocess.Popen(cmd.decode('utf-8'),shell = True, stdout=subprocess.PIPE, #标准输出 stderr=subprocess.PIPE #标准错误 ) stdout = res.stdout.read() stderr = res.stderr.read() #先发报头(转成固定长度的bytes类型,那么怎么转呢?就用到了struct模块) #len(stdout) + len(stderr)#统计数据的长度 header = struct.pack('i',len(stdout)+len(stderr))#制作报头 coon.send(header) #再发命令的结果 coon.send(stdout) coon.send(stderr) coon.close() phone.close()
客户端执行结果:
C:Python36python3.exe G:/PycharmProject/test/test/cli.py 请你输入命令>>:dir 返回的消息: Volume in drive G is ??? Volume Serial Number is 8E43-8056 Directory of G:PycharmProject est est 2019/08/16 15:55 <DIR> . 2019/08/16 15:55 <DIR> .. 2019/08/01 16:50 0 abc 2019/08/13 10:49 29 b.txt 2019/08/16 15:55 793 cli.py 2019/07/29 13:19 <DIR> crm 2019/07/29 11:50 36 manage.py 2019/07/29 13:17 <DIR> proj 2019/08/16 15:55 1,269 ser.py 2019/08/16 15:49 529 test3.py 2019/08/16 15:45 564 test4.py 2019/07/29 11:48 36 __init__.py 2019/08/09 15:38 <DIR> __pycache__ 8 File(s) 3,256 bytes 5 Dir(s) 505,991,168 bytes free 请你输入命令>>:ipconfig 返回的消息: Windows IP Configuration Ethernet adapter ???: Media State . . . . . . . . . . . : Media disconnected Connection-specific DNS Suffix . : Wireless LAN adapter ????* 1: Media State . . . . . . . . . . . : Media disconnected Connection-specific DNS Suffix . : Wireless LAN adapter ????* 2: Media State . . . . . . . . . . . : Media disconnected Connection-specific DNS Suffix . : Ethernet adapter ??? 3: Connection-specific DNS Suffix . : Link-local IPv6 Address . . . . . : fe80::e426:cc63:c654:f906%11 IPv4 Address. . . . . . . . . . . : 10.0.0.30 Subnet Mask . . . . . . . . . . . : 255.255.255.0 Default Gateway . . . . . . . . . : Wireless LAN adapter WLAN: Connection-specific DNS Suffix . : IPv4 Address. . . . . . . . . . . : 192.168.9.196 Subnet Mask . . . . . . . . . . . : 255.255.255.0 Default Gateway . . . . . . . . . : 192.168.9.1 Ethernet adapter ??? 2: Media State . . . . . . . . . . . : Media disconnected Connection-specific DNS Suffix . : Ethernet adapter ??? 4: Media State . . . . . . . . . . . : Media disconnected Connection-specific DNS Suffix . : Ethernet adapter ??????: Media State . . . . . . . . . . . : Media disconnected Connection-specific DNS Suffix . : 请你输入命令>>:
服务器端执行结果:
C:Python36python3.exe G:/PycharmProject/test/test/ser.py start runing..... <socket.socket fd=532, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8880), raddr=('127.0.0.1', 65471)> ('127.0.0.1', 65471) 接收的是:dir 接收的是:ipconfig
例子2:
服务端:
##服务端 import subprocess import struct import json phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) # phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) phone.bind(('127.0.0.1',9909)) #0-65535:0-1024给操作系统使用 phone.listen(5) print('starting...') while True: # 链接循环 conn,client_addr=phone.accept() print(client_addr) while True: #通信循环 try: #1、收命令 cmd=conn.recv(8096) if not cmd:break #适用于linux操作系统 #2、执行命令,拿到结果 obj = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout=obj.stdout.read() stderr=obj.stderr.read() #3、把命令的结果返回给客户端 #第一步:制作固定长度的报头 header_dic={ 'filename':'a.txt', 'md5':'xxdxxx', 'total_size': len(stdout) + len(stderr) } #字典不能直接转成bytes类型,可以转成字符串;反解出字典类型json header_json=json.dumps(header_dic) #json格式的字符串 header_bytes=header_json.encode('utf-8') #bytes类型 #第二步:先发送报头的长度 conn.send(struct.pack('i',len(header_bytes))) #4个bytes #第三步:再发报头 conn.send(header_bytes) #第四步:再发送真实的数据 conn.send(stdout) conn.send(stderr) except ConnectionResetError: #适用于windows操作系统 break conn.close() phone.close()
客户端:
##客户端 import socket import struct import json phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) phone.connect(('127.0.0.1',9909)) while True: #1、发命令 cmd=input('>>: ').strip() #ls /etc if not cmd:continue phone.send(cmd.encode('utf-8')) #2、拿命令的结果,并打印 #第一步:先收报头的长度 obj=phone.recv(4) header_size=struct.unpack('i',obj)[0] #第二步:再收报头 header_bytes=phone.recv(header_size) #第三步:从报头中解析出对真实数据的描述信息 header_json=header_bytes.decode('utf-8') header_dic=json.loads(header_json) print(header_dic) total_size=header_dic['total_size'] #第四步:接收真实的数据 recv_size=0 recv_data=b'' while recv_size < total_size: res=phone.recv(1024) #1024是一个坑 recv_data+=res recv_size+=len(res) print(recv_data.decode('gbk')) phone.close()
发送时:
发送时先发报头长度再编码报头内容然后发送最后发真实内容
接收时
接收时先收报头长度,用struct取出来
根据取出的长度收取报头内容,然后解码,反序列化
从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容
5-3 优化版的解决方法(从根本解决问题)
优化的解决粘包问题的思路就是服务端将报头信息进行优化,对要发送的内容用字典进行描述,首先字典不能直接进行网络传输,需要进行序列化转成json格式化字符串,然后转成bytes格式服务端进行发送,因为bytes格式的json字符串长度不是固定的,所以要用struct模块将bytes格式的json字符串长度压缩成固定长度,发送给客户端,客户端进行接受,反解就会得到完整的数据包。
在Windows下的Python3实现
终极版的客户端
# _*_ coding: utf-8 _*_
import socket
import struct
import json
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.connect(('127.0.0.1',8080)) #连接服务器
while True:
# 发收消息
cmd = input('请你输入命令>>:').strip()
if not cmd:continue
phone.send(cmd.encode('utf-8')) #发送
#先收报头的长度
header_len = struct.unpack('i',phone.recv(4))[0] #吧bytes类型的反解
#在收报头
header_bytes = phone.recv(header_len) #收过来的也是bytes类型
header_json = header_bytes.decode('utf-8') #拿到json格式的字典
header_dic = json.loads(header_json) #反序列化拿到字典了
total_size = header_dic['total_size'] #就拿到数据的总长度了
#最后收数据
recv_size = 0
total_data=b''
while recv_size<total_size: #循环的收
recv_data = phone.recv(1024) #1024只是一个最大的限制
recv_size+=len(recv_data) #有可能接收的不是1024个字节,或许比1024多呢,
# 那么接收的时候就接收不全,所以还要加上接收的那个长度
total_data+=recv_data #最终的结果
print('返回的消息:%s'%total_data.decode('gbk'))
phone.close()
终极版的服务端
# _*_ coding: utf-8 _*_
import socket
import subprocess
import struct
import json
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #买手机
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(('127.0.0.1',8080)) #绑定手机卡
phone.listen(5) #阻塞的最大数
print('start runing.....')
while True: #链接循环
coon,addr = phone.accept()# 等待接电话
print(coon,addr)
while True: #通信循环
# 收发消息
cmd = coon.recv(1024) #接收的最大数
print('接收的是:%s'%cmd.decode('utf-8'))
#处理过程
res = subprocess.Popen(cmd.decode('utf-8'),shell = True,
stdout=subprocess.PIPE, #标准输出
stderr=subprocess.PIPE #标准错误
)
stdout = res.stdout.read()
stderr = res.stderr.read()
# 制作报头
header_dic = {
'total_size': len(stdout)+len(stderr), # 总共的大小
'filename': None,
'md5': None
}
header_json = json.dumps(header_dic) #字符串类型
header_bytes = header_json.encode('utf-8') #转成bytes类型(但是长度是可变的)
#先发报头的长度
coon.send(struct.pack('i',len(header_bytes))) #发送固定长度的报头
#再发报头
coon.send(header_bytes)
#最后发命令的结果
coon.send(stdout)
coon.send(stderr)
coon.close()
phone.close()
服务器端执行结果:
C:Python36python3.exe G:/PycharmProject/test/test/ser.py start runing..... <socket.socket fd=556, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 49286)> ('127.0.0.1', 49286) 接收的是:dir 接收的是:ipconfig
解决粘包问题简单版(适用于传输字节较小)
针对Windows下的python3
服务器端:
import socket import subprocess import struct def cmd_exec(cmd): """ 执行shell命令 :param cmd: :return: """ p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = p.communicate() if p.returncode != 0: return stderr return stdout sock_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 重用地址端口 sock_server.bind(('127.0.0.1', 8088)) sock_server.listen(1) # 开始监听,1代表在允许有一个连接排队,更多的新连接连进来时就会被拒绝 print('starting...') while True: conn, client_addr = sock_server.accept() # 阻塞直到有连接为止,有了一个新连接进来后,就会为这个请求生成一个连接对象 print(client_addr) while True: try: data = conn.recv(1024) # 接收1024个字节 if not data: break # 适用于linux操作系统,防止客户端断开连接后死循环 print('客户端的命令', data.decode('gbk')) res = cmd_exec(data.decode('gbk')) # 执行cmd命令 # 第一步:制作固定长度的报头4bytes total_size = len(res) header = struct.pack('i', total_size) # 第二步:把报头发送给客户端 conn.send(header) # 第三步:再发送真实的数据 conn.sendall(res) except ConnectionResetError: # 适用于windows操作系统,防止客户端断开连接后死循环 break conn.close() server.close()
客户端:
import socket import struct client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) print(client) client.connect(('127.0.0.1', 8088)) while True: data = input('input >>>') if not data: # 如果数据为空,继续输入 continue client.send(data.encode('GBK')) # 发送数据 # 第一步:先收报头 header = client.recv(4) # 第二步:从报头中解析出对真实数据的描述信息(数据的长度) total_size = struct.unpack('i', header)[0] print('收到数据长度=', total_size) # 第三步:接收真实的数据 recv_size = 0 recv_data = b'' while recv_size < total_size: data = client.recv(1024) # 接收数据 recv_data += data recv_size += len(data) # 不能加1024,如果加进度条,会计算有误 print('接收数据 =', recv_data.decode('gbk', 'ignore')) # 如果设置为ignore,则会忽略非法字符; client.close() # 关闭
服务器端执行结果:
C:Python36python3.exe G:/PycharmProject/test/test/ser.py starting... ('127.0.0.1', 49360) 客户端的命令 dir 客户端的命令 ipconfig
客户端执行结果:
C:Python36python3.exe G:/PycharmProject/test/test/cli.py
<socket.socket fd=568, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0>
input >>>dir
收到数据长度= 780
接收数据 = Volume in drive G is ???
Volume Serial Number is 8E43-8056
Directory of G:PycharmProject est est
2019/08/16 16:08 <DIR> .
2019/08/16 16:08 <DIR> ..
2019/08/01 16:50 0 abc
2019/08/13 10:49 29 b.txt
2019/08/16 16:08 999 cli.py
2019/07/29 13:19 <DIR> crm
2019/07/29 11:50 36 manage.py
2019/07/29 13:17 <DIR> proj
2019/08/16 16:08 1,747 ser.py
2019/08/16 15:49 529 test3.py
2019/08/16 15:45 564 test4.py
2019/07/29 11:48 36 __init__.py
2019/08/09 15:38 <DIR> __pycache__
8 File(s) 3,940 bytes
5 Dir(s) 505,991,168 bytes free
input >>>ipconfig
收到数据长度= 1332
接收数据 =
Windows IP Configuration
Ethernet adapter ???:
Media State . . . . . . . . . . . : Media disconnected
Connection-specific DNS Suffix . :
Wireless LAN adapter ????* 1:
Media State . . . . . . . . . . . : Media disconnected
Connection-specific DNS Suffix . :
Wireless LAN adapter ????* 2:
Media State . . . . . . . . . . . : Media disconnected
Connection-specific DNS Suffix . :
Ethernet adapter ??? 3:
Connection-specific DNS Suffix . :
Link-local IPv6 Address . . . . . : fe80::e426:cc63:c654:f906%11
IPv4 Address. . . . . . . . . . . : 10.0.0.30
Subnet Mask . . . . . . . . . . . : 255.255.255.0
Default Gateway . . . . . . . . . :
Wireless LAN adapter WLAN:
Connection-specific DNS Suffix . :
IPv4 Address. . . . . . . . . . . : 192.168.9.196
Subnet Mask . . . . . . . . . . . : 255.255.255.0
Default Gateway . . . . . . . . . : 192.168.9.1
Ethernet adapter ??? 2:
Media State . . . . . . . . . . . : Media disconnected
Connection-specific DNS Suffix . :
Ethernet adapter ??? 4:
Media State . . . . . . . . . . . : Media disconnected
Connection-specific DNS Suffix . :
Ethernet adapter ??????:
Media State . . . . . . . . . . . : Media disconnected
Connection-specific DNS Suffix . :
input >>>
解决粘包问题优化版(适用于传输字节很大)
server端
import socket import subprocess import struct import json def cmd_exec(cmd): """ 执行shell命令 :param cmd: :return: """ p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = p.communicate() if p.returncode != 0: return stderr return stdout sock_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 重用地址端口 sock_server.bind(('127.0.0.1', 8088)) sock_server.listen(1) # 开始监听,1代表在允许有一个连接排队,更多的新连接连进来时就会被拒绝 print('starting...') while True: conn, client_addr = sock_server.accept() # 阻塞直到有连接为止,有了一个新连接进来后,就会为这个请求生成一个连接对象 print(client_addr) while True: try: data = conn.recv(1024) # 接收1024个字节 if not data: break # 适用于linux操作系统,防止客户端断开连接后死循环 print('客户端的命令', data.decode('gbk')) res = cmd_exec(data.decode('gbk')) # 执行cmd命令 # 第一步:制作固定长度的报头dict header_dict = { 'filename': '文件名', 'md5': 'md5值', 'total_size': len(res) } header_json = json.dumps(header_dict, ensure_ascii='False', indent=2) # 序列化json print(header_json) header_bytes = header_json.encode('utf-8') header = struct.pack('i', len(header_bytes)) # 第二步:把报头长度发送给客户端 conn.send(header) # 第三步:把报头内容发送给客户端 conn.send(header_bytes) # 第四步:再发送真实的数据 conn.sendall(res) except ConnectionResetError: # 适用于windows操作系统,防止客户端断开连接后死循环 break conn.close() server.close()
client端
import socket import struct import json client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) print(client) client.connect(('127.0.0.1', 8088)) while True: data = input('input >>>') if not data: # 如果数据为空,继续输入 continue client.send(data.encode('GBK')) # 发送数据 # 第一步:先收报头 header = client.recv(4) # 第二步:从报头中解析(header数据的长度) header_size = struct.unpack('i', header)[0] print('收到报头长度=', header_size) # 第三步:收到报头解析出对真实数据的描述信息 header_json = client.recv(header_size) header_dict = json.loads(header_json) print('收到报头内容=', header_dict) total_size = header_dict['total_size'] # 第四步:接收真实的数据 recv_size = 0 recv_data = b'' while recv_size < total_size: data = client.recv(1024) # 接收数据 recv_data += data recv_size += len(data) # 不能加1024,如果加进度条,会计算有误 print('接收数据 =', recv_data.decode('gbk', 'ignore')) # 如果设置为ignore,则会忽略非法字符; client.close() # 关闭
服务器端执行结果:
C:Python36python3.exe G:/PycharmProject/test/test/ser.py starting... ('127.0.0.1', 49486) 客户端的命令 dir { "filename": "u6587u4ef6u540d", "md5": "md5u503c", "total_size": 780 } 客户端的命令 ipconfig { "filename": "u6587u4ef6u540d", "md5": "md5u503c", "total_size": 1332 }
客户端执行结果:
C:Python36python3.exe G:/PycharmProject/test/test/cli.py <socket.socket fd=532, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0> input >>>dir 收到报头长度= 81 收到报头内容= {'filename': '文件名', 'md5': 'md5值', 'total_size': 780} 接收数据 = Volume in drive G is ??? Volume Serial Number is 8E43-8056 Directory of G:PycharmProject est est 2019/08/16 16:19 <DIR> . 2019/08/16 16:19 <DIR> .. 2019/08/01 16:50 0 abc 2019/08/13 10:49 29 b.txt 2019/08/16 16:19 1,238 cli.py 2019/07/29 13:19 <DIR> crm 2019/07/29 11:50 36 manage.py 2019/07/29 13:17 <DIR> proj 2019/08/16 16:18 2,189 ser.py 2019/08/16 15:49 529 test3.py 2019/08/16 15:45 564 test4.py 2019/07/29 11:48 36 __init__.py 2019/08/09 15:38 <DIR> __pycache__ 8 File(s) 4,621 bytes 5 Dir(s) 505,991,168 bytes free input >>>ipconfig 收到报头长度= 82 收到报头内容= {'filename': '文件名', 'md5': 'md5值', 'total_size': 1332} 接收数据 = Windows IP Configuration Ethernet adapter ???: Media State . . . . . . . . . . . : Media disconnected Connection-specific DNS Suffix . : Wireless LAN adapter ????* 1: Media State . . . . . . . . . . . : Media disconnected Connection-specific DNS Suffix . : Wireless LAN adapter ????* 2: Media State . . . . . . . . . . . : Media disconnected Connection-specific DNS Suffix . : Ethernet adapter ??? 3: Connection-specific DNS Suffix . : Link-local IPv6 Address . . . . . : fe80::e426:cc63:c654:f906%11 IPv4 Address. . . . . . . . . . . : 10.0.0.30 Subnet Mask . . . . . . . . . . . : 255.255.255.0 Default Gateway . . . . . . . . . : Wireless LAN adapter WLAN: Connection-specific DNS Suffix . : IPv4 Address. . . . . . . . . . . : 192.168.9.196 Subnet Mask . . . . . . . . . . . : 255.255.255.0 Default Gateway . . . . . . . . . : 192.168.9.1 Ethernet adapter ??? 2: Media State . . . . . . . . . . . : Media disconnected Connection-specific DNS Suffix . : Ethernet adapter ??? 4: Media State . . . . . . . . . . . : Media disconnected Connection-specific DNS Suffix . : Ethernet adapter ??????: Media State . . . . . . . . . . . : Media disconnected Connection-specific DNS Suffix . : input >>>
简单的文件传送 :
文件的上传和下载
需要文件的名字,文件的大小,文件的内容
自定义一个文件传输协议:
1
|
{ 'filesize' : 000 , 'filename' : 'XXXX' } |
使用server.py将一个文件传给client
server.py
import os import json import struct import socket # E:BaiduYunDownloadAppleEthernet-master.zip sk = socket.socket() sk.bind(('127.0.0.1',9000)) sk.listen() conn,addr = sk.accept() print(addr) dic = {'filename':'AppleEthernet-master.zip', 'filesize':os.path.getsize(r'E:BaiduYunDownloadAppleEthernet-master.zip')} str_dic = json.dumps(dic).encode('utf-8') dic_len = struct.pack('i',len(str_dic)) conn.send(dic_len) conn.send(str_dic) with open(r'E:BaiduYunDownloadAppleEthernet-master.zip','rb') as f: content = f.read() conn.send(content) conn.close() sk.close()
client.py
import json import struct import socket sk = socket.socket() sk.connect(('127.0.0.1',9000)) dic_len = sk.recv(4) dic_len = struct.unpack('i',dic_len)[0] str_dic = sk.recv(dic_len).decode('utf-8') dic = json.loads(str_dic) with open(dic['filename'],'wb') as f: content = sk.recv(dic['filesize']) f.write(content) sk.close()
先运行server.py,再运行client.py
那么在当前目录中,就会多出一个文件AppleEthernet-master.zip
server的IP为127.0.0.1。如果改成本机的IP,比如192.168.11.27
那么别的电脑,开启客户端,就可以接收到。
注意:
大文件的传输,不能一次性读到内存里
今日作业:
上传一个视频,几台电脑之间能互相传,视频要3个G左右。
进阶需求,加一个登陆功能
server.py
import os import json import struct import socket import hashlib sk = socket.socket() sk.bind(('127.0.0.1',9999)) sk.listen() conn,addr = sk.accept() print(addr) filename = '[电影天堂www.dy2018.com]移动迷宫3:死亡解药BD国英双语中英双字.mp4' # 文件名 absolute_path = os.path.join('E:BaiduYunDownload',filename) # 文件绝对路径 buffer_size = 1024*1024 # 缓冲大小,这里表示1MB md5obj = hashlib.md5() with open(absolute_path, 'rb') as f: while True: content = f.read(buffer_size) # 每次读取指定字节 if content: md5obj.update(content) else: break # 当内容为空时,终止循环 md5 = md5obj.hexdigest() print(md5) # 打印md5值 dic = {'filename':filename, 'filename_md5':str(md5),'buffer_size':buffer_size, 'filesize':os.path.getsize(absolute_path)} str_dic = json.dumps(dic).encode('utf-8') # 将字典转换为json dic_len = struct.pack('i', len(str_dic)) # 获取字典长度,转换为struct conn.send(dic_len) # 发送字典长度 conn.send(str_dic) # 发送字典 with open(absolute_path, 'rb') as f: # 打开文件 while True: content = f.read(buffer_size) # 每次读取指定大小的字节 if content: # 判断内容不为空 conn.send(content) # 每次读取指定大小的字节 else: break conn.close() # 关闭连接 sk.close() # 关闭套接字
client.py
import json import struct import socket import hashlib import time start_time = time.time() sk = socket.socket() sk.connect(('127.0.0.1',9999)) dic_len = sk.recv(4) # 接收4字节,因为struct的int为4字节 dic_len = struct.unpack('i',dic_len)[0] # 反解struct得到元组,获取元组第一个元素 #print(dic_len) # 返回一个数字 str_dic = sk.recv(dic_len).decode('utf-8') # 接收指定长度,获取完整的字典,并解码 #print(str_dic) # json类型的字典 dic = json.loads(str_dic) # 反序列化得到真正的字典 #print(dic) # 返回字典 md5 = hashlib.md5() with open(dic['filename'],'wb') as f: while True: content = sk.recv(dic['buffer_size']) if not content: break md5.update(content) md5 = md5.hexdigest() print(md5) # 打印md5值 if dic['filename_md5'] == str(md5): f.write(content) print('md5校验正确--下载成功') else: print('文件验证失败') sk.close() end_time = time.time() print('本次下载花费了{}秒'.format(end_time-start_time))
先执行server.py,再执行client.py
server输出:
('127.0.0.1', 54230) 30e63a254cf081e8e93c036b21057347
client输出:
30e63a254cf081e8e93c036b21057347 md5校验正确--下载成功 本次下载花费了25.687340021133423秒
参考:https://www.cnblogs.com/xiao-apple36/p/9276777.html
https://www.cnblogs.com/wj-1314/p/8953148.html
https://www.cnblogs.com/shengyang17/p/8766507.html
https://www.cnblogs.com/xiao987334176/p/9002737.html