TCP(transport control protocol,传输控制协议):面向连接的,面向流的,提供可靠的服务。为了高效的发送包,使用了Nagle 算法,将多次间隔较小且数据量小的数据,合并成一个大的数据块进行封包,因此面向流的通信是无消息保护边界的。
UDP(user datagram protocol,用户数据报协议):无连接,面型消息的,提供高效率的服务。UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构记录每一个到达的UDP包(包含消息来源地址,端口等信息),对于接收端来说可以进行区分。面向消息是有消息保护边界的
黏包: 接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的
TCP协议为了提高传输效率,发送方往往要手机到足够多的数据后才会发送一个TCP段,若连续几次的数据量都少,TCP会根据Nagle算法将数据合成一个TCP段后一次发送出去,接收方就收到了黏包数据
黏包产生原因: 1 、发送数据时间间隔短,数据量小,合在一起发送,产生粘包
2 、接收方不及时接收缓冲区的包,造成多个包接收
拆包发生原因: 当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。
解决粘包的方法: (发送端在发送数据是,把自己将要发送的字节流总大小让接收端知晓,然后接收端用循环接收所有的数据)
# 基于TCP的CMD服务端 # 思路:想统计数据长度发给对方,待对方确认后,在发送数据 import socket import subprocess server = socket.socket() socket.bind(("127.0.0.1",9999)) socket.listen() while True: client,address = socket.accept() while True: try: cmd = client.recv(1024).decode('utf-8') if not data: client.close() break p1 = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr = subprocess.PIPE) data = p1.stdout.read() err_data = p1.stderr.read() # 先发送数据长度,然后发送数据 length = str(len(data)+len(err_data)) client.send(length.encode('utf-8')) data1 = server.recv(1024).decode('utf-8') # 确认对方已经收到长度信息 if data == 'recv_ready': client.send(err_data.encode('utf-8')) client.send(data.encode('utf-8')) except ConnectionResetError: print('客户端断开了连接!') server.close()
# CMD客户端 <strong>import socket client = socket.socket() client.connect(('127.0.0.1',8888)) while True: msg = input("请输入指令: ").strip()</strong> # 判断指令是否正确,q 退出 <strong>client.send(msg.encode('utf-8'))</strong> # 这个是数据的长度 <strong>length = int(client.recv(1024).decode('utf-8'))</strong> # 收到数据后要给服务端回复'recv_ready' <strong>client.send('recv_ready'.encode('utf-8'))</strong> <strong>total_data = b"" recv_size = 0</strong> # 每次接收一部分,分批次接收完 <strong>while recv_size< length: data += client.recv(1024) recv_size+=len(data) print(data.decode('utf-8')) </strong>
以上方法虽然解决了粘包,但是 程序的运行速度远快于网络传输速度,在发送一次字节前,要先把字节的长度发给对方,这种方式放大网络延迟甙类的性能损耗
解决办法: 为字节流加上自定义的固定长度的报头(报头中包含字节流长度),一次send到对端,对端在接收时,先从缓存中取出订场的报头,然后在取出只是数据
struct模块:把一个类型,如整数,转成固定长度的bytes
struct.pack("i", 11111) ------> 转成固定长度的二进制
# CMD 服务端 # 思路:发送真正的数据之前,先添加一个报头(包括数据的长度,时间,文件名)计算报头长度 # 先发送报头(4 个字节)对方接收后反解出报头长度,我方发送报头数据,对方接收,然后反解出真正数据的长度,然后接收数据 import socket import subprocess import datetime import struct import json server = socket.socket() server.bind(("127.0.0.1",8888)) server.listen() while True: client,address = server.accept() while True: try: cmd = client.recv(1024).decode('utf-8') p1 = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) data = p1.stdout.read() err_data = p1.stderr.read() if data: res = data else: res = err_data length = len(res) # 发送数据之前发送额外的信息(时间,数据长度,文件名) t = {} t['time']=str(datetime.datetime.now()) t['size'] = length t['filename']='a.txt' # 将字典转成json格式,算出字典长度,用struct变成固定长度的二进制 t_json = json.dumps(t) #得到json格式的字符串 t_data = t_json.encode('utf-8') t_length = struct.pack("i",len(t_data)) #第一步,发送额外信息的长度 client.send(t_length) #第二步,发送额外信息内容 client.send(t_data) #第三步,发送真正的数据 client.send(res) except ConnectionResetError: print('连接中断。。。') client.close() break server.close()
# CMD 的客户端 import socket import struct import json client = socket.socket() client.connect(("127.0.0.1",8888)) while True: msg = input('输入指令:').strip() #判断输入不能为空 if not msg: print('命令不能为空!') continue client.send(msg.encode('utf-8')) #第一次: 收到对方发来的报头长度 t_length1 = client.recv(4) # 反解出长度 t_length = struct.unpack("i",t_length1)[0] # struck 反解出的是元组形式 # 第二次:接收报头内容 head_data1 = client.recv(t_length).decode('utf-8') # 解析报头内容(时间,文件名,数据长度),得到数据长度 head_data = json.loads(head_data1) print('执行时间:%s'%head_data['time']) data_length = head_data['size'] # 第三次: 接收真正的数据 all_data = b"" recv_length = 0 while recv_length < data_length: all_data+=client.recv(1024) recv_length += len(all_data) print('接收长度为:%s'%recv_length) print(all_data.decode('gbk')) # 注意解码为GBK client.close()
总结: TCP发生粘包的三种情况
1. 当单个数据包较小时接收方可能一次性读取多个包的数据
2. 当整体数据较大时接收方可能一次仅能读取一个包的一部分内容
3. 另外TCP协议为了提高效率,增加一种优化机制,会将数据较小且发送间隔较短的数据合并发送,该机制也会导致发送方将两个数据包粘在一起发送。
产生黏包的原因:
接收方不知道发送方发送了多少数据
struck模块: 将python中的类型,例如整型,转化成固定长度的二进制
a=struct.pack("i",89007) print(a) #结果: b'xaf[x01x00' b = struct.unpack("i",a) # 结果为元组形式 print(b) # (89007,)
练习:
1.完成文件上传下载
上传
给一个文件路径 判断是否存在
打开文件 读取文件 内容 文件名 文件大小
把文件信息 发送给服务器
发文件数据
服务器 接收文件信息
打开文件 写入收到的数据
下载
给一个文件路径 判断是否存在
打开文件 读取文件 内容 文件名 文件大小
把文件信息 发送给客户端
发文件数据
客户端 接收文件信息
打开文件 写入文件数据
server: import socket import struct import json import os server = socket.socket() server.bind(("127.0.0.1",9090)) server.listen() def run(): while True: client,addr = server.accept() while True: try: # 接收报头 len_data = client.recv(4) if not len_data: print("客户端已断开....") client.close() break head_len = struct.unpack("i",len_data)[0] head = json.loads(client.recv(head_len).decode("utf-8")) # 从报头中获取用户要执行的操作 if head["opt"] == "upload": upload(head,client) elif head["opt"] == "download": download(head,client) else: print("请求错误!") except ConnectionResetError: print("客户端异常断开...") client.close() break def upload(head,c): dir_path = r"E:1212.27粘包upload" path = os.path.join(dir_path,head["filename"]) recv_size = 0 with open(path,"wb") as f: while recv_size < head["size"]: data = c.recv(1024) f.write(data) recv_size += len(data) print("接收完成....!") def download(head,c): path = r"E:1212.27粘包一个文件" # 制作文件报头 file_info = {"filename":os.path.basename(path),"size":os.path.getsize(path)} info_data = json.dumps(file_info).encode("utf-8") info_len = struct.pack("i",len(info_data)) c.send(info_len) c.send(info_data) with open(path,"rb") as f: while True: data = f.read(1024) if not data: break c.send(data) print("发送完成...") run() client: import socket,os, struct, json c = socket.socket() c.connect(("127.0.0.1",9090)) def run(): print(""" 1.上传 2.下载 3.退出""") res = input("请选择功能:") if res == "3": return elif res == "1": upload() elif res == "2": download() else: print("输入错误!") def upload(): path = r"E:1212.27粘包download一个文件" # 制作文件报头 file_info = {"filename": os.path.basename(path), "size": os.path.getsize(path),"opt":"upload"} info_data = json.dumps(file_info).encode("utf-8") info_len = struct.pack("i", len(info_data)) c.send(info_len) c.send(info_data) with open(path, "rb") as f: while True: data = f.read(1024) if not data: break c.send(data) print("发送完成...") def download(): # 制作报头 head = {"opt": "download"} head_data = json.dumps(head).encode("utf-8") head_len = struct.pack("i", len(head_data)) c.send(head_len) c.send(head_data) len_data = c.recv(4) head_len = struct.unpack("i", len_data)[0] head = json.loads(c.recv(head_len).decode("utf-8")) dir_path = r"E:1212.27粘包download" path = os.path.join(dir_path, head["filename"]) recv_size = 0 with open(path, "wb") as f: while recv_size < head["size"]: data = c.recv(1024) f.write(data) recv_size += len(data) print("接收完成....!") run()
2.基于TCP的登录注册
客户端提交注册数据 服务器判断是否重复 并返回注册结果 用户数据写入文件 客户端提交都登录数据 服务器判断是否得了成功 并返回登录结果 数据交互直接用json即可 1.发送json长度 2.取出json进行处理
server: import socket import struct import json import os server = socket.socket() server.bind(("127.0.0.1",9090)) server.listen() def run(): while True: client,addr = server.accept() while True: try: # 接收报头 len_data = client.recv(4) if not len_data: print("客户端已断开....") client.close() break head_len = struct.unpack("i",len_data)[0] head = json.loads(client.recv(head_len).decode("utf-8")) # 从报头中获取用户要执行的操作 if head["opt"] == "login": res = login(head,client) # 调用登录 无论成功失败 都会得到一个结果 send_response(res,client) # 将结果发送给客户端 elif head["opt"] == "register": res = register(head,client)# 调用注册 无论成功失败 都会得到一个结果 send_response(res,client)# 将结果发送给客户端 else: print("请求错误!") except ConnectionResetError: print("客户端异常断开...") client.close() break def login(head,client): dir_path = r"E:1212.27粘包一个文件" path = os.path.join(dir_path, head["name"]) if not os.path.exists(path): response = {"status": "200", "msg": "用户名不存在!"} return response with open(path,"rt",encoding="utf-8") as f: user_dic = json.load(f) if user_dic["pwd"] == head["pwd"]: response = {"status": "200", "msg": "登录成功!"} return response else: response = {"status": "200", "msg": "密码错误!"} return response def register(head,client): user_dic = {"name":head["name"],"pwd":head["pwd"]} dir_path = r"E:1212.27粘包一个文件" path = os.path.join(dir_path,head["name"]) if os.path.exists(path): print("用户名已存在!") response = {"status": "200", "msg": "用户名已存在!"} return response with open(path,"wt",encoding="utf-8") as f: json.dump(user_dic,f) response = {"status":"200","msg":"注册成功"} return response def send_response(resp,client): response_data = json.dumps(resp).encode("utf-8") client.send(struct.pack("i", len(response_data))) client.send(response_data) run() client: import socket import struct import json c = socket.socket() c.connect(("127.0.0.1",9090)) def run(): print(""" 1.登录 2.注册 3.退出 """) res = input("请选择功能:") if res == "3": return elif res == "1": login() elif res == "2": register() else: print("输入错误!") def login(): name = input("name:") pwd = input("password:") dic = {"name":name,"pwd":pwd,"opt":"login"} res = send_request(dic) print(res) def register(): name = input("name:") pwd = input("password:") dic = {"name":name,"pwd":pwd,"opt":"register"} res = send_request(dic) print(res) def send_request(dic): dic_data = json.dumps(dic).encode("utf-8") c.send(struct.pack("i",len(dic_data))) c.send(dic_data) # 接收返回结果 res_len = struct.unpack("i",c.recv(4))[0] res = json.loads(c.recv(res_len).decode("utf-8")) return res run()