网络编程基础:粘包现象、基于UDP协议的套接字

粘包现象:

如上篇博客中最后的示例,客户端有个 phone.recv(2014) , 当服务端发送给客户端的数据大于1024个字节时, 多于1024的数据就会残留在管道中,下次客户端再给服务端发命令时,残留在管道的数据会先发送给客户端,新命令产生的数据会排在上次命令残留数据的后面发送到客户端,即两次结果的数据粘在一起了, 这个就是粘包现象。

粘包现象的原理分析:

# 运行一个软件或程序需要的硬件:CPU、内存、硬盘
# CPU负责执行;CPU执行需要数据,而数据从内存中取,最后可以把数据存到硬盘(内存速度会比硬盘快很多)
# 操作系统所占的内存空间和应用程序所占的内存空间互相隔离 # 客户端的 phone.send(数据) send是应用软件的代码,是发送给操作系统的命令,应用软件先把要发送的数据copy给操作系统的内存然后让操作系统把该数据发送出去
# 应用软件把数据复制给操作系统后,操作系统怎么发这个数据应用软件控制不了
# recv的完成需要2步:1.recv对应的操作系统等待接收对方传过来的数据;(耗时长);2.recv的操作系统将接收的数据复制给应用软件(耗时短,因为是本地copy)
# send的完成只需要1步: 将数据从自己(应用软件)的内存空间复制到操作系统的内存空间。 # send和recv对比
# 1. 不管是recv还是send都不是在直接接收对方的数据,而是在操作自己的操作系统内存---> so,不是一个send就要对应一个recv
# 2. recv:
# wait data 耗时非常长
# copy data
# send:
# copy data
# 3. TCP协议的特点:发送端为了将多个发往接收端的包能有效的发到对方,会将多次间隔较小且数据量小的数据,合并成一个大的数据包,然后进行封装;这样接收端就难以分辨出来了,即面向流的通信是无消息保护边界的。

粘包解决方法普通版(制作自己的“报头”):

补充知识点struct模块:

import struct

# 制作报头
res = struct.pack("i",123498654) # 输出结果是bytes格式 # i 代表整型,如果是整型,res这个bytes就是固定长度4(跟后面整数具体的大小无关); #有两个参数:第一个是格式("i"代表整型),第二个是值
print(res,type(res),len(res))
"""
struct.pack可用于制作报头,把描述信息传入第二个参数value
"""
# 打印结果
# b'\x9ep\\\x07' <class 'bytes'> 4 # 解析报头
unpack_res = struct.unpack("i",res) # 对res这个bytes格式的字符串进行解包 # 解包结果为元祖形式 # 也有两个参数: 1. 格式("i") 2. bytes格式的字符串
print(unpack_res) # 打印结果
# (123498654,)

粘包解决方法普通版客户端代码如下:

import socket
import struct client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",9901))
while True:
# 1. 发命令
cmd = input(">>>").strip()
if not cmd:continue
client.send(cmd.encode("utf-8")) # 2. 拿到命令结果并打印
# 第一步:先收“报头”
head = client.recv(4) # head为bytes格式; # 由于报头的长度固定为4,所以recv应该为4
# 第二步:从报头解析出对真实数据的描述信息(真实数据的长度)
total_size = struct.unpack("i",head)[0] # 对head这个报头解析 # struct.unpack()的结果是元祖的形式 # 第三步:接收真实的数据
"""开始循环接收服务端发来的真实数据"""
recv_size = 0 # 用于计算接收到的bytes数
total_recv_res = b"" # 设置一个bytes格式的空字符串,用于拼接、接收服务端发来的真实数据
while recv_size < total_size: # 已经接收的bytes数小于服务端发送的全部字节数
recv_res = client.recv(1024)
total_recv_res += recv_res # 把每次从服务端接收到的真实数据添加到total_recv_res 里面
recv_size += len(recv_res) # 每次从服务端接收到的数据的bytes数加到 recv_size里面; 不要用 recv_size += 1024,这种方法不能准确计算出bytes数
print(total_recv_res.decode("gbk")) phone.close()

服务端代码:

import subprocess
import struct
import socket phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(("127.0.0.1",9901))
phone.listen(5) while True:
conn,client_addr = phone.accept() while True:
try:
# 1. 收命令
cmd = conn.recv(1024) # 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. 把命令结果返回给客户端
# 第一步:制定固定长度的“报头”(报头一定需要是固定长度)
head = struct.pack("i",len(stdout)+len(stderr)) # head 是bytes格式,长度固定为4 # 第二步: 把报头发送给客户端
conn.send(head) # 第三步:再发送真实的结果数据
# conn.send(stdout+stderr) 解决的方法如下所示:
conn.send(stdout)
conn.send(stderr) # 不需要再用“+”,因为这种形式的发送TCP协议就会把数据粘在一起
except ConnectionResetError:
break conn.close()
phone.close()

粘包解决最终版(利用字典制作自己的报头)

服务端代码:

import socket
import subprocess
import json
import struct server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(("127.0.0.1",8080))
server.listen(5) while True:
conn,addr = server.accept() while True:
try:
# 1. 收命令
cmd = conn.recv(1024) # 2. 处理命令
obj = subprocess.Popen(cmd.decode("utf-8"),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
stdout = obj.stdout.read() # bytes格式
stderr = obj.stderr.read() # 3. 把处理结果发送给客户端
"""
报头应该包含多种信息,而不只是只包含真实信息的长度,所以考虑利用字典去制定报头
"""
# 3.1 用字典形式制作报头
header_dict = {
"filename":"cmd处理",
"md5":"xxxxxx",
"total_size":len(stdout)+len(stderr)
} # 字典不能用于send(),只有bytes才可以
header_json = json.dumps(header_dict) # 将报头字典转化成json格式的字符串
header_bytes = header_json.encode("utf-8") # 将json形式的字符串转化成bytes格式
"""
字典是报头,报头转化成bytes之后你并不能确定bytes的个数,但报头又需要是固定长度,
所以先把head_bytes(报头的bytes格式)的长度利用struct模块打包成固定长度发送给客户端,然后再把header_bytes发送给客户端,
对应的,客户端先收报头长度,然后再接收报头长度个数的bytes,那么客户端第二次接收的bytes就是报头的完整信息
"""
# 3.2 发送报头bytes的长度
header_length = struct.pack("i",len(header_bytes)) # 把报头bytes的个数利用struct.pack()打包、制定成固定长度(4)
conn.send(header_length)
# 3.3 发送报头bytes的真实信息
conn.send(header_bytes)
# 3.4 发送处理结果的真实数据
conn.send(stdout)
conn.send(stderr)
except ConnectionResetError:
break
conn.close()
server.close()

客户端代码:

import socket
import json
import struct client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",8080)) while True:
cmd = input(">>>").strip()
if not cmd: continue
client.send(cmd.encode("utf-8")) # 1. 接收报头的长度
obj_contain_length = client.recv(4) # 因为报头bytes的长度已经经过struct.pack()的打包,长度固定为4
header_bytes_length = struct.unpack("i",obj_contain_length)[0] # 报头bytes的长度
# 2. 接收报头的数据
header_bytes = client.recv(header_bytes_length) # 接收报头bytes个数的bytes数,就是完整的报头bytes信息
header_json = header_bytes.decode("utf-8") # 将bytes格式解码成json字符串格式
header_dict = json.loads(header_json) # 将报头的json字符串格式反序列化得到报头的字典形式
total_size = header_dict.get("total_size") # 得到报头字典里的处理结果的bytes数
# 3. 接收处理结果的数据
recv_size = 0
total_recv_bytes = b""
while recv_size < total_size:
recv_bytes = client.recv(1024)
total_recv_bytes += recv_bytes
recv_size += len(recv_bytes) print(total_recv_bytes.decode("gbk")) client.close()

文件传输功能:

文件的目录结构如下:

网络编程基础:粘包现象、基于UDP协议的套接字

服务端代码:

import socket
import json
import struct
import os
import sys BASE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(BASE_DIR) server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(("127.0.0.1",8089))
server.listen(5) while True:
conn,addr = server.accept() while True:
try:
# 1. 收命令
res = conn.recv(1024) # b"get a.txt" # 2. 解析命令,提取相应命令参数
cmd,file_name = res.decode("utf-8").split() # ["get","a.txt"] # 3. 以读的模式打开文件,读取文件内容发送给客户端
# 第一步:用字典形式制作报头
file = os.path.join(BASE_DIR,"share",file_name)
header_dict = {
"filename":file_name,
"md5":"xxxxxx",
"total_size": os.path.getsize(file)
}
header_json = json.dumps(header_dict)
header_bytes = header_json.encode("utf-8") # 第二步:发送报头bytes的长度
header_length = struct.pack("i",len(header_bytes))
conn.send(header_length)
# 第三步:发送报头bytes的真实信息
conn.send(header_bytes) # 4. 发送处理结果的真实数据
with open(file,"rb") as f:
for line in f:
conn.send(line) # 单行发送跟一下全部发送效果上没有区别,因为单行发送也是粘在一起 except ConnectionResetError:
break
conn.close()
server.close()

客户端代码:

import socket
import json
import struct
import os,sys BASE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(BASE_DIR) client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",8089)) while True:
# 1. 发命令
cmd = input(">>>").strip()
if not cmd: continue
client.send(cmd.encode("utf-8")) # 2. 以写的模式打开一个新文件,把服务端发来的文件内容写入新文件
# 第一步: 接收报头的长度
obj_contain_length = client.recv(4)
header_bytes_length = struct.unpack("i",obj_contain_length)[0]
# 第二步:再收报头,从报头中解析出对真实信息的描述信息
header_bytes = client.recv(header_bytes_length)
header_json = header_bytes.decode("utf-8")
header_dict = json.loads(header_json)
total_size = header_dict.get("total_size")
file_name = header_dict["filename"]
# 3. 接收真实的数据
with open(os.path.join(BASE_DIR,"download",file_name),"wb") as f:
recv_size = 0
while recv_size < total_size:
line = client.recv(1024)
f.write(line)
recv_size += len(line)
print("文件总大小:%s;已下载:%s;已下载比例:%s"%(total_size,recv_size,(recv_size/total_size))) client.close()

文件传输功能函数版:

客户端代码:

import socket
import json
import struct
import os,sys BASE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(BASE_DIR) """下载功能:即从服务端接收文件"""
def get(client,cmd):
# 2. 以写的模式打开一个新文件,把服务端发来的文件内容写入新文件
# 第一步: 接收报头的长度
file_name = cmd[1]
obj_contain_length = client.recv(4)
header_bytes_length = struct.unpack("i", obj_contain_length)[0]
# 第二步:再收报头,从报头中解析出对真实信息的描述信息
header_bytes = client.recv(header_bytes_length)
header_json = header_bytes.decode("utf-8")
header_dict = json.loads(header_json)
total_size = header_dict.get("total_size")
# file_name = header_dict["filename"]
# 3. 接收真实的数据
with open(os.path.join(BASE_DIR, "download", file_name), "wb") as f:
recv_size = 0
while recv_size < total_size:
line = client.recv(1024)
f.write(line)
recv_size += len(line)
print("文件总大小:%s;已下载:%s;已下载比例:%s" % (total_size, recv_size, (recv_size / total_size))) """上传功能:即发送文件给服务端"""
def put(client,cmd):
file_name = cmd[1]
file = os.path.join(BASE_DIR,"download",file_name)
# 1. 制定报头
file_size = os.path.getsize(file)
head_dict ={
"filename":file_name,
"dm5": "xxxxxxx",
"total_size":file_size
}
head_json = json.dumps(head_dict)
head_bytes = head_json.encode("utf-8")
# 2. 发送报头
head_bytes_length = struct.pack("i",len(head_bytes))
client.send(head_bytes_length)
client.send(head_bytes)
# 3. 发送真实数据
with open(file,"rb") as f:
send_size = 0
for line in f:
client.send(line)
send_size += len(line)
print("文件总大小:%s;已上传:%s;上传比例:%s"%(file_size,send_size,(send_size/file_size))) def run():
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(("127.0.0.1",8089)) while True:
# 1. 发命令
cmd = input(">>>").strip()
if not cmd: continue
client.send(cmd.encode("utf-8"))
cmd_list = cmd.split()
cmd_dict = {
"get":get,
"put":put
}
for k,v in cmd_dict.items():
if cmd_list[0] == k:
v(client,cmd_list) client.close() if __name__ == "__main__":
run()

服务端代码:

import socket
import json
import struct
import os
import sys BASE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(BASE_DIR) """下载功能:即发送数据给客户端"""
def get(conn,cmd):
file_name = cmd[1]
file = os.path.join(BASE_DIR, "share", file_name)
header_dict = {
"filename": file_name,
"md5": "xxxxxx",
"total_size": os.path.getsize(file)
}
header_json = json.dumps(header_dict)
header_bytes = header_json.encode("utf-8") header_length = struct.pack("i", len(header_bytes))
conn.send(header_length)
conn.send(header_bytes) # 发送处理结果的真实数据
with open(file, "rb") as f:
for line in f:
conn.send(line) """上传功能:即接收客户端发来的数据"""
def put(conn,cmd):
file_name = cmd[1]
# 1. 先接收报头长度
bytes_header_length = conn.recv(4)
header_length = struct.unpack("i",bytes_header_length)[0]
# 2. 接收报头数据
header_bytes = conn.recv(header_length) # bytes格式的字符串
header_json = header_bytes.decode("utf-8")
header = json.loads(header_json)
total_size = header["total_size"] # 要上传数据的总大小
# 3. 接收真实的数据
with open(os.path.join(BASE_DIR,"share",file_name),"wb") as f:
recv_size = 0
while recv_size < total_size:
recv_bytes = conn.recv(1024)
f.write(recv_bytes)
recv_size += len(recv_bytes) def run():
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(("127.0.0.1",8089))
server.listen(5) while True:
conn,addr = server.accept() while True:
try:
# 1. 收命令
res = conn.recv(1024) # b"get a.txt"
# 2. 解析命令,提取相应命令参数
cmd = res.decode("utf-8").split() # ["get","a.txt"] cmd_dict = {
"get": get,
"put": put
}
for k,v in cmd_dict.items():
if cmd[0] == k:
v(conn,cmd) except ConnectionResetError:
break
conn.close()
server.close() if __name__ == "__main__":
run()

基于UDP协议的套接字:

客户端代码:

from socket import *

client = socket(AF_INET,SOCK_DGRAM)

"""
UTP不需要建链接(通道)。所以不需要 connect
""" while True:
msg = input(">>>").strip()
client.sendto(msg.encode("utf-8"),("127.0.0.1",8080)) # sendto传入两个参数:数据和接收端的IP和端口 # msg也是bytes格式 data = client.recvfrom(1024)
print(data) client.close() # 运行结果:
# (b'HELLO', ('127.0.0.1', 8080)) """
UTP协议不会粘包
UTP协议能够发送空消息,所以不需要写 if not msg: continue
UTP协议一定是一个sendto对应一个 recvfrom
对于recvfrom(1024),在Windows上,如果接收的数据大于1024个bytes,会报错;在Linux上,如果接收的数据大于1024个bytes,程序只接收1024个,多余的数据就丢失了
"""

客户端代码:

from socket import *  # 导入socket模块时可以利用 import *

server = socket(AF_INET,SOCK_DGRAM)  # DGRAM 是UDP协议,即“数据报协议”
server.bind(("127.0.0.1",8080)) # UDP协议也需要bind """
UTP协议没有 listen和accept;因为UTP不需要建通道,而TCP中的listen和accept是为了建通道
"""
while True:
data,addr = server.recvfrom(1) # 接收数据;收到的也是bytes格式 # 接收到的数据是元祖形式:第一个元素是数据信息,第二个是发送端的IP和端口 # 数据信息也是bytes格式
print(data,addr) server.sendto(data.upper(),addr) # recvfrom中包含发送端的IP和端口,还通过这个IP端口发发送端回数据
server.close() # 运行结果:
# b'hello' ('127.0.0.1', 53729)
上一篇:基于udp协议的套接字通信


下一篇:网络编程(基于udp协议的套接字/socketserver模块/进程简介)