TCP协议中的粘包问题
1.粘包现象
基于TCP实现一个简易远程cmd功能
#服务端 import socket import subprocess sever = socket.socket() sever.bind(('127.0.0.1', 33521)) sever.listen() while True: client, address = sever.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() client.send(data) client.send(err_data) except ConnectionResetError: print('connect broken') client.close() break sever.close() #客户端 import socket client = socket.socket() client.connect(('127.0.0.1', 33521)) while True: cmd = input('请输入指令(Q\q退出)>>:').strip().lower() if cmd == 'q': break client.send(cmd.encode('utf-8')) data = client.recv(1024) print(data.decode('gbk')) client.close()
上述是基于TCP协议的远程cmd简单功能,在运行时会发生粘包。
2、什么是粘包?
只有TCP会发生粘包现象,UDP协议永远不会发生粘包;
TCP:(transport control protocol,传输控制协议)流式协议。在socket中TCP协议是按照字节数进行数据的收发,数据的发送方发出的数据往往接收方不知道数据到底长度是多长,而TCP协议由于本身为了提高传输的效率,发送方往往需要收集到足够的数据才会进行发送。使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
UDP:(user datagram protocol,用户数据报协议)数据报协议。在socket中udp协议收发数据是以数据报为单位,服务端和客户端收发数据是以一个单位,所以不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
TCP协议不会丢失数据,UDP协议会丢失数据。
udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠。
tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
3、什么情况下会发生粘包?
1.由于TCP协议的优化算法,当单个数据包较小的时候,会等到缓冲区满才会发生数据包前后数据叠加在一起的情况。然后取的时候就分不清了到底是哪段数据,这是第一种粘包。
2.当发送的单个数据包较大超过缓冲区时,收数据方一次就只能取一部分的数据,下次再收数据方再收数据将会延续上次为接收数据。这是第二种粘包。
粘包的本质问题就是接收方不知道发送数据方一次到底发送了多少数据,解决问题的方向也是从控制数据长度着手,也就是如何设置缓冲区的问题
4、如何解决粘包问题?
解决问题思路:上述已经明确粘包的产生是因为接收数据时不知道数据的具体长度。所以我们应该先发送一段数据表明我们发送的数据长度,那么就不会产生数据没有发送或者没有收取完全的情况。
1.struct 模块(结构体)
struct模块的功能可以将python中的数据类型转换成C语言中的结构体(bytes类型)
import struct s = 123456789 res = struct.pack('i', s) print(res) res2 = struct.unpack('i', res) print(res2) print(res2[0])
2.粘包的解决方案基本版
既然我们拿到了一个可以固定长度的办法,那么应用struct模块,可以固定长度了。
为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据
#服务器端 import socket import subprocess import struct sever = socket.socket() sever.bind(('127.0.0.1', 33520)) sever.listen() while True: client, address = sever.accept() while True: try: cmd = client.recv(1024).decode('utf-8') #利用子进程模块启动程序 p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) #管道输出的信息有正确和错误的 data = p.stdout.read() err_data = p.stderr.read() #先将数据的长度发送给客户端 length = len(data)+len(err_data) #利用struct模块将数据的长度信息转化成固定的字节 len_data = struct.pack('i', length) #以下将信息传输给客户端 #1.数据的长度 client.send(len_data) #2.正确的数据 client.send(data) #2.错误管道的数据 client.send(err_data) except Exception as e: client.close() print('连接中断。。。。') break #客户端 import socket import struct client = socket.socket() client.connect(('127.0.0.1', 33520)) while True: cmd = input('请输入指令>>:').strip().encode('utf-8') client.send(cmd) #1.先接收传过来数据的长度是多少,我们通过struct模块固定了字节长度为4 length = client.recv(4) #将struct的字节再转回去整型数字 len_data = struct.unpack('i', length) print(len_data) len_data = len_data[0] print('数据长度为%s:' % len_data) all_data = b'' recv_size = 0 #2.接收真实的数据 #循环接收直到接收到数据的长度等于数据的真实长度(总长度) while recv_size < len_data: data = client.recv(1024) recv_size += len(data) all_data += data print('接收长度%s' % recv_size) print(all_data.decode('gbk'))
#总结:
服务器端:
1.在服务器端先收到命令,打开子进程,然后计算返回的数据的长度
2.先利用struct模块将数据长度转成固定4个字节传给客户端
3.再向客户端发送真实的数据。
客户端(两次接收):
1.第一次只接受4个字节,因为长度数据就是4个字节。这样防止了数据粘包。解码得到长度数据