Day 6-3 粘包现象

时间:2021-08-15 03:47:57

服务端:

 import socket
import subprocess phone = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
phone.bind(("127.0.0.1", 8990)) phone.listen(10) print("运行中...")
while True:
conn, client_ipaddr = phone.accept()
print("客户端IP:%s,端口:%s" % (client_ipaddr[0], client_ipaddr[1]))
while True: # 通信循环
try:
# 1,接收客户端发送的命令
cmd = conn.recv(1024)
if not cmd: break
# 2,在服务器上执行客户端发过来的命令
cmd = subprocess.Popen(cmd.decode("utf-8"), shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout = cmd.stdout.read()
stderr=cmd.stderr.read()
# 3,把执行结果发送给客户端
conn.send(stdout+stderr)
except ConnectionResetError: # 针对windows系统,客户端强制断开后,会报这个错误.
break
conn.close()
phone.close()

客户端:

 import socket
import os
if os.name =="nt":
code = "GBK"
else:
code="utf-8" phone1 = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) phone1.connect(("127.0.0.1", 8990)) while True:
#1,发送命令给服务器
cmd = input("请输入你要发送的信息:").strip()
if not cmd:continue
phone1.send(cmd.encode("utf-8"))
#2,接收服务器执行命令后的结果.
data = phone1.recv(1024)
print(data.decode(code))
phone1.close()

我们分别启动服务端和客户端.然后在客户端上执行一个名 tree c:\ (windows系统).服务端返回的结果如下:

 C:\
├─e_object
├─GeePlayerDownload
├─Intel
│ └─Logs
├─Program Files
│ ├─Common Files
│ │ ├─Microsoft Shared
│ │ │ ├─Filters
│ │ │ ├─ink
│ │ │ │ ├─ar-SA
│ │ │ │ ├─bg-BG
│ │ │ │ ├─cs-CZ
│ │ │ │ ├─da-DK
│ │ │ │ ├─de-DE
│ │ │ │ ├─el-GR
│ │ │ │ ├─en-US
│ │ │ │ ├─es-ES
│ │ │ │ ├─et-EE
│ │ │ │ ├─fi-FI
│ │ │ │ ├─fr-FR
│ │ │ │ ├─fsdefinitions
│ │ │ │ │ ├─auxpad
│ │ │ │ │ ├─keypad
│ │ │ │ │ ├─main
│ │ │ │ │ ├─numbers
│ │ │ │ │ ├─oskmenu
│ │ │ │ │ ├─osknumpad
│ │ │ │ │ ├─oskpred
│ │ │ │ │ ├─symbols
│ │ │ │ │ └─web
│ │ │ │ ├─he-IL
│ │ │ │ ├─hr-HR
│ │ │ │ ├─hu-HU
│ │ │ │ ├─HWRCustomization
│ │ │ │ ├─it-IT
│ │ │ │ ├─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的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束

所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

总结

  1. 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
"""
 #!_*_ coding:utf-8 _*_
import socket
import subprocess
import struct phone = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
phone.bind(("127.0.0.1", 8990)) phone.listen(10) print("运行中...")
while True:
conn, client_ipaddr = phone.accept()
print("客户端IP:%s,端口:%s" % (client_ipaddr[0], client_ipaddr[1]))
while True: # 通信循环
try:
# 1,接收客户端发送的命令
cmd = conn.recv(1024)
if not cmd: break
# 2,在服务器上执行客户端发过来的命令
cmd = subprocess.Popen(cmd.decode("utf-8"), shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout = cmd.stdout.read()
stderr=cmd.stderr.read()
# 3,把执行结果发送给客户端
#3-1 把报头(固定长度)发送给客户端
total_size = len(stdout+stderr)
print(total_size)
header = struct.pack("i",total_size) # i是类型,total_size是值.这个命令会把total_size打包成一个4个字节长度的字节数据类型
conn.send(header) # 把报头发送给客户端
#302 发送数据给客户端 conn.send(stdout)
conn.send(stderr)
except ConnectionResetError: # 针对windows系统,客户端强制断开后,会报这个错误.
break
conn.close()
phone.close()

粘包解决服务端

 #!_*_ coding:utf-8 _*_
import socket
import os
import struct if os.name == "nt":
code = "GBK"
else:
code = "utf-8" phone1 = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) phone1.connect(("127.0.0.1", 8990)) while True:
# 1,发送命令给服务器
cmd = input("请输入你要发送的信息:").strip()
if not cmd: continue
phone1.send(cmd.encode("utf-8"))
# 2,接收服务器执行命令后的结果.
# 2-1 接收服务器发过来的报头
header = phone1.recv(4) # 收报头
total_size = struct.unpack("i", header)[0] #解包,并取出报头中数据 # 2-2 循环接收数据
recv_size = 0
recv_data = b""
while recv_size < total_size:
data = phone1.recv(1024) # 接收数据
recv_data += data # 拼接数据
recv_size += len(data) # 设置已接收数据的大小
print(recv_data.decode(code))
phone1.close()

粘包解决客户端

上面粘包解决办法中存在着一些问题:

1,struct制作报头的时候,不管是i还是l模式,total_size都有可能超出它们俩的范围.程序就会报错.

   total_size = len(stdout+stderr)
print(total_size)
header = struct.pack("i",total_size)

2,报头信息不应该只有文件大小信息.还应该包含其他文件信息.

新思路:

   设置一个字典,字典中包含了文件的信息,(大小,名称,md5等).然后通过json.dumps转换成字符串格式,再把转换后的数据转成bytes类型(便于网络传输)
.再然后,通过struct模块,把bytes类型的制作成一个报头(报头长度依然是4bytes),发给客户端.然后客户端接收后,反序列化,获取字典中文件的大小.然后开始接收文件.

服务端:

 #!_*_ coding:utf-8 _*_
import socket
import subprocess
import struct
import json phone = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
phone.bind(("127.0.0.1", 8990)) phone.listen(10) print("运行中...")
while True:
conn, client_ipaddr = phone.accept()
print("客户端IP:%s,端口:%s" % (client_ipaddr[0], client_ipaddr[1]))
while True: # 通信循环
try:
# 1,接收客户端发送的命令
cmd = conn.recv(1024)
if not cmd: break
# 2,在服务器上执行客户端发过来的命令
cmd = subprocess.Popen(cmd.decode("utf-8"), shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout = cmd.stdout.read()
stderr=cmd.stderr.read()
# 3,把执行结果发送给客户端
#3-1 把报头(固定长度)发送给客户端
header_dict ={"filename":"a.txt",
"md5":"a0id2ndnk23nmnm1bazi23",
"total_size":len(stdout+stderr)
}
#3-1-1,把字典序列化为字符串
header_json = json.dumps(header_dict)
#3-1-2,把序列化后的数据转成bytes类型,便于网络传输
header_bytes = header_json.encode("utf-8")
#3-1-3,把bytes类型的数据做成一个报头
struct.pack("i",len(header_bytes)) # 对应客户端的 obj = phone1.recv(4)
#3-1-4,发送报头给客户端
conn.send(struct.pack("i",len(header_bytes)))
#3-1-5.把报头信息发给客户端
conn.send(header_bytes) #对应客户端的header_bytes = phone1.recv(header_size)
#302 发送数据给客户端
conn.send(stdout)
conn.send(stderr)
except ConnectionResetError: # 针对windows系统,客户端强制断开后,会报这个错误.
break
conn.close()
phone.close()

客户端:

 #!_*_ coding:utf-8 _*_
import socket
import os
import struct
import json
if os.name == "nt":
code = "GBK"
else:
code = "utf-8" phone1 = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) phone1.connect(("127.0.0.1", 8990)) while True:
# 1,发送命令给服务器
cmd = input("请输入你要发送的信息:").strip()
if not cmd: continue
phone1.send(cmd.encode("utf-8"))
# 2,接收服务器执行命令后的结果.
# 2-1 接收服务器发过来的报头
obj = phone1.recv(4) # 收报头
header_size= struct.unpack("i",obj)[0] #获取报头长度
header_bytes = phone1.recv(header_size) # 收取报头信息(bytes格式)
header_json = header_bytes.decode("utf-8") #解码报头信息
header_dict=json.loads(header_json) # 反序列化,获取字典内容
print(header_dict)
total_size = header_dict["total_size"] # 获取total_size的值 # 2-2 循环接收数据
recv_size = 0
recv_data = b""
while recv_size < total_size:
data = phone1.recv(1024) # 接收数据
recv_data += data # 拼接数据
recv_size += len(data) # 设置已接收数据的大小
print(recv_data.decode(code))
phone1.close()

制作报头的流程:

Day 6-3 粘包现象