粘包现象:
如上篇博客中最后的示例,客户端有个 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()
文件传输功能:
文件的目录结构如下:
服务端代码:
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)