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客户端 import socket client = socket.socket() client.connect(('127.0.0.1',8888)) while True: msg = input("请输入指令: ").strip() # 判断指令是否正确,q 退出 client.send(msg.encode('utf-8')) # 这个是数据的长度 length = int(client.recv(1024).decode('utf-8')) # 收到数据后要给服务端回复'recv_ready' client.send('recv_ready'.encode('utf-8')) total_data = b"" recv_size = 0 # 每次接收一部分,分批次接收完 while recv_size< length: data += client.recv(1024) recv_size+=len(data) print(data.decode('utf-8'))
小结: 以上方法虽然解决了粘包,但是 程序的运行速度远快于网络传输速度,在发送一次字节前,要先把字节的长度发给对方,这种方式放大网络延迟甙类的性能损耗
解决办法: 为字节流加上自定义的固定长度的报头(报头中包含字节流长度),一次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[\x01\x00' b = struct.unpack("i",a) # 结果为元组形式 print(b) # (89007,)