Day 6-3 粘包现象
服务端:
1 import socket 2 import subprocess 3 4 phone = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) 5 phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 6 phone.bind(("127.0.0.1", 8990)) 7 8 phone.listen(10) 9 10 print("运行中...") 11 while True: 12 conn, client_ipaddr = phone.accept() 13 print("客户端IP:%s,端口:%s" % (client_ipaddr[0], client_ipaddr[1])) 14 while True: # 通信循环 15 try: 16 # 1,接收客户端发送的命令 17 cmd = conn.recv(1024) 18 if not cmd: break 19 # 2,在服务器上执行客户端发过来的命令 20 cmd = subprocess.Popen(cmd.decode("utf-8"), shell=True, 21 stdout=subprocess.PIPE, 22 stderr=subprocess.PIPE) 23 stdout = cmd.stdout.read() 24 stderr=cmd.stderr.read() 25 # 3,把执行结果发送给客户端 26 conn.send(stdout+stderr) 27 except ConnectionResetError: # 针对windows系统,客户端强制断开后,会报这个错误. 28 break 29 conn.close() 30 phone.close()
客户端:
1 import socket 2 import os 3 if os.name =="nt": 4 code = "GBK" 5 else: 6 code="utf-8" 7 8 phone1 = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) 9 10 phone1.connect(("127.0.0.1", 8990)) 11 12 while True: 13 #1,发送命令给服务器 14 cmd = input("请输入你要发送的信息:").strip() 15 if not cmd:continue 16 phone1.send(cmd.encode("utf-8")) 17 #2,接收服务器执行命令后的结果. 18 data = phone1.recv(1024) 19 print(data.decode(code)) 20 phone1.close()
我们分别启动服务端和客户端.然后在客户端上执行一个名 tree c:\ (windows系统).服务端返回的结果如下:
1 C:\ 2 ├─e_object 3 ├─GeePlayerDownload 4 ├─Intel 5 │ └─Logs 6 ├─Program Files 7 │ ├─Common Files 8 │ │ ├─Microsoft Shared 9 │ │ │ ├─Filters 10 │ │ │ ├─ink 11 │ │ │ │ ├─ar-SA 12 │ │ │ │ ├─bg-BG 13 │ │ │ │ ├─cs-CZ 14 │ │ │ │ ├─da-DK 15 │ │ │ │ ├─de-DE 16 │ │ │ │ ├─el-GR 17 │ │ │ │ ├─en-US 18 │ │ │ │ ├─es-ES 19 │ │ │ │ ├─et-EE 20 │ │ │ │ ├─fi-FI 21 │ │ │ │ ├─fr-FR 22 │ │ │ │ ├─fsdefinitions 23 │ │ │ │ │ ├─auxpad 24 │ │ │ │ │ ├─keypad 25 │ │ │ │ │ ├─main 26 │ │ │ │ │ ├─numbers 27 │ │ │ │ │ ├─oskmenu 28 │ │ │ │ │ ├─osknumpad 29 │ │ │ │ │ ├─oskpred 30 │ │ │ │ │ ├─symbols 31 │ │ │ │ │ └─web 32 │ │ │ │ ├─he-IL 33 │ │ │ │ ├─hr-HR 34 │ │ │ │ ├─hu-HU 35 │ │ │ │ ├─HWRCustomization 36 │ │ │ │ ├─it-IT 37 │ │ │ │ ├─ja-
我们此时,在客户端继续输入ifconfig 命令,发现返回的数据依然是上次tree c:\的结果.这是为什么呢?
这是因为,客户端一次只能接收1024个字节的数据,如果超过1024个字节,那么这些数据就会在服务器的IO缓存区里暂存下来.如果现在在客户端输入ipconfg命令后,在服务端返回数据给客户端时,因为IO缓存区还有上次tree命令存留的信息,所以会先把上次的信息返回给客户端.等tree命令所有的数据都返回给客户端后,才会返回ipconfig的数据.就造成了两条命令的结果都在某一次的返回数据中.这种现象就叫做粘包.
粘包发生需要满足的条件:
一,在客户端:
由于TCP协议使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。如果连续发送2个2bytes的包,这时候在客户端就已经发生了粘包现象.但是此时在服务端不一定会发生粘包.
二,服务端:
如果这2个包没有超出服务器接收的最大字节数(1024),就不会发生粘包.如果服务器每次只接收1bytes,那么在服务端也会发生粘包.
怎么解决粘包这种现象呢?有人说把客户端接收的最大字节值改成其他更大的数字,不就可以了吗?一般情况下,最大接收字节数的值不超过8192.超过这个数,会影响接收的稳定性和速度.
send和recv对比:
1.不管是send还是recv,都不是直接把数据发送给对方,而是通过系统发送.然后从系统内存中读取返回的数据.
2.send和recv不是一一对应的.
3.send工作流程:把数据发送给操作系统,让系统调用网卡进行发送.send就完成了工作
recv工作流程,等待客户端发送过来的数据.这个时间比较长.接收到数据后,再从系统内存中调用数据.
粘包问题只存在于TCP中,Not UDP
还是看上图,发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
总结
- TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
2. UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
3. tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头
解决粘包现象的思路:
通过上述的实验和例子,我们知道,粘包现象的产生,主要是客户端不知道要接收多少数据(或者说多大的数据).那么,按照这个思路,那么我们知道,在服务端执行完命令后,我们可以在服务端获取结果的大小.再发送给客户端,让客户端知道被接收数据的大小,然后再通过一个循环,来接收数据即可.这时我们需要用一个新的模块,struct来制作报头信息.发送给客户端.
import struct pack = struct.pack("i",10000) # 定义格式 print(pack,len(pack),type(pack)) # pack的类型是bytes,传输的时候,就不用encode了. t = struct.unpack("i",pack) #解包, print(t) # 获取元组形式的数据. t = struct.unpack("i",pack)[0] # 直接获取数据的值. """ b"\x10'\x00\x00" 4 <class 'bytes'> (10000,) 直接获取: 10000 """
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #!_*_ coding:utf-8 _*_ 2 import socket 3 import subprocess 4 import struct 5 6 phone = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) 7 phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 8 phone.bind(("127.0.0.1", 8990)) 9 10 phone.listen(10) 11 12 print("运行中...") 13 while True: 14 conn, client_ipaddr = phone.accept() 15 print("客户端IP:%s,端口:%s" % (client_ipaddr[0], client_ipaddr[1])) 16 while True: # 通信循环 17 try: 18 # 1,接收客户端发送的命令 19 cmd = conn.recv(1024) 20 if not cmd: break 21 # 2,在服务器上执行客户端发过来的命令 22 cmd = subprocess.Popen(cmd.decode("utf-8"), shell=True, 23 stdout=subprocess.PIPE, 24 stderr=subprocess.PIPE) 25 stdout = cmd.stdout.read() 26 stderr=cmd.stderr.read() 27 # 3,把执行结果发送给客户端 28 #3-1 把报头(固定长度)发送给客户端 29 total_size = len(stdout+stderr) 30 print(total_size) 31 header = struct.pack("i",total_size) # i是类型,total_size是值.这个命令会把total_size打包成一个4个字节长度的字节数据类型 32 conn.send(header) # 把报头发送给客户端 33 #302 发送数据给客户端 34 35 conn.send(stdout) 36 conn.send(stderr) 37 except ConnectionResetError: # 针对windows系统,客户端强制断开后,会报这个错误. 38 break 39 conn.close() 40 phone.close()
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #!_*_ coding:utf-8 _*_ 2 import socket 3 import os 4 import struct 5 6 if os.name == "nt": 7 code = "GBK" 8 else: 9 code = "utf-8" 10 11 phone1 = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) 12 13 phone1.connect(("127.0.0.1", 8990)) 14 15 while True: 16 # 1,发送命令给服务器 17 cmd = input("请输入你要发送的信息:").strip() 18 if not cmd: continue 19 phone1.send(cmd.encode("utf-8")) 20 # 2,接收服务器执行命令后的结果. 21 # 2-1 接收服务器发过来的报头 22 header = phone1.recv(4) # 收报头 23 total_size = struct.unpack("i", header)[0] #解包,并取出报头中数据 24 25 # 2-2 循环接收数据 26 recv_size = 0 27 recv_data = b"" 28 while recv_size < total_size: 29 data = phone1.recv(1024) # 接收数据 30 recv_data += data # 拼接数据 31 recv_size += len(data) # 设置已接收数据的大小 32 print(recv_data.decode(code)) 33 phone1.close()
上面粘包解决办法中存在着一些问题:
1,struct制作报头的时候,不管是i还是l模式,total_size都有可能超出它们俩的范围.程序就会报错.
1 total_size = len(stdout+stderr) 2 print(total_size) 3 header = struct.pack("i",total_size)
2,报头信息不应该只有文件大小信息.还应该包含其他文件信息.
新思路:
设置一个字典,字典中包含了文件的信息,(大小,名称,md5等).然后通过json.dumps转换成字符串格式,再把转换后的数据转成bytes类型(便于网络传输)
.再然后,通过struct模块,把bytes类型的制作成一个报头(报头长度依然是4bytes),发给客户端.然后客户端接收后,反序列化,获取字典中文件的大小.然后开始接收文件.
服务端:
1 #!_*_ coding:utf-8 _*_ 2 import socket 3 import subprocess 4 import struct 5 import json 6 7 phone = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) 8 phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 9 phone.bind(("127.0.0.1", 8990)) 10 11 phone.listen(10) 12 13 print("运行中...") 14 while True: 15 conn, client_ipaddr = phone.accept() 16 print("客户端IP:%s,端口:%s" % (client_ipaddr[0], client_ipaddr[1])) 17 while True: # 通信循环 18 try: 19 # 1,接收客户端发送的命令 20 cmd = conn.recv(1024) 21 if not cmd: break 22 # 2,在服务器上执行客户端发过来的命令 23 cmd = subprocess.Popen(cmd.decode("utf-8"), shell=True, 24 stdout=subprocess.PIPE, 25 stderr=subprocess.PIPE) 26 stdout = cmd.stdout.read() 27 stderr=cmd.stderr.read() 28 # 3,把执行结果发送给客户端 29 #3-1 把报头(固定长度)发送给客户端 30 header_dict ={"filename":"a.txt", 31 "md5":"a0id2ndnk23nmnm1bazi23", 32 "total_size":len(stdout+stderr) 33 } 34 #3-1-1,把字典序列化为字符串 35 header_json = json.dumps(header_dict) 36 #3-1-2,把序列化后的数据转成bytes类型,便于网络传输 37 header_bytes = header_json.encode("utf-8") 38 #3-1-3,把bytes类型的数据做成一个报头 39 struct.pack("i",len(header_bytes)) # 对应客户端的 obj = phone1.recv(4) 40 #3-1-4,发送报头给客户端 41 conn.send(struct.pack("i",len(header_bytes))) 42 #3-1-5.把报头信息发给客户端 43 conn.send(header_bytes) #对应客户端的header_bytes = phone1.recv(header_size) 44 #302 发送数据给客户端 45 conn.send(stdout) 46 conn.send(stderr) 47 except ConnectionResetError: # 针对windows系统,客户端强制断开后,会报这个错误. 48 break 49 conn.close() 50 phone.close()
客户端:
1 #!_*_ coding:utf-8 _*_ 2 import socket 3 import os 4 import struct 5 import json 6 if os.name == "nt": 7 code = "GBK" 8 else: 9 code = "utf-8" 10 11 phone1 = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) 12 13 phone1.connect(("127.0.0.1", 8990)) 14 15 while True: 16 # 1,发送命令给服务器 17 cmd = input("请输入你要发送的信息:").strip() 18 if not cmd: continue 19 phone1.send(cmd.encode("utf-8")) 20 # 2,接收服务器执行命令后的结果. 21 # 2-1 接收服务器发过来的报头 22 obj = phone1.recv(4) # 收报头 23 header_size= struct.unpack("i",obj)[0] #获取报头长度 24 header_bytes = phone1.recv(header_size) # 收取报头信息(bytes格式) 25 header_json = header_bytes.decode("utf-8") #解码报头信息 26 header_dict=json.loads(header_json) # 反序列化,获取字典内容 27 print(header_dict) 28 total_size = header_dict["total_size"] # 获取total_size的值 29 30 # 2-2 循环接收数据 31 recv_size = 0 32 recv_data = b"" 33 while recv_size < total_size: 34 data = phone1.recv(1024) # 接收数据 35 recv_data += data # 拼接数据 36 recv_size += len(data) # 设置已接收数据的大小 37 print(recv_data.decode(code)) 38 phone1.close()
制作报头的流程: