粘包问题的引入
基于tcp协议远程执行命令的代码
服务端:
from socket import * import subprocess server = socket(AF_INET, SOCK_STREAM) server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) server.bind(('127.0.0.1', 6666)) server.listen(5) while True: conn, client_addr = server.accept() while True: try: cmd = conn.recv(1024) if len(cmd) == 0: break # subprocess.Popen类用于在一个新进程中执行一个子程序 res = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) stdout_res = res.stdout.read() stderr_res = res.stdout.read() conn.send(stdout_res) conn.send(stderr_res) except Exception: break conn.close()
客户端:
from socket import * client = socket(AF_INET, SOCK_STREAM) client.connect(('127.0.0.1', 6666)) while True: msg = input('请输入命令 >>: ').strip() if len(msg) == 0: continue client.send(msg.encode()) cmd_res = client.recv(1024) # 本次接收, 最大接收1024Bytes print(cmd_res.decode())
可以发现, 服务端发送的消息, 在客户端接收消息中, 主要是通过recv()函数接收消息, 该函数接收一个参数, 该参数指定接收消息的大小, 单位为字节。
如果服务端第一次发送的消息超过了客户端接收消息的规定的字节数, 那么第一次客户端也只会接收该字节数大小的消息, 那么剩下的服务端发送的消息去哪了?
服务端第二次发送消息的时候, 客户端接收消息会接收到第一次剩下的消息和第二次的消息(不超过规定字节数的消息)。
这就产生了网络通信中的粘包问题
粘包问题的产生
socket收发消息的原理如下:
发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,
TCP协议是面向流的协议,这也是容易出现粘包问题的原因。
而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。
怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。
所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。
此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
- TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
- UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
- tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头
udp的recvfrom是阻塞的,一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠
tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
粘包问题主要是在两种情况下产生:
- 发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)
- 接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)
粘包问题的解决
接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据
解决问题的思路为:
1. 服务端先发头信息(固定长度的bytes): 对数据的描述 2. 客户端先收固定长度的头信息: 解析出数据的描述信息, 拿到数据的总大小total_size 3. 服务端再发真实的数据 4. 客户端循环接收, 直到接收完固定长度的信息
实现方法一: 使用固定长度
服务端
from socket import * import subprocess import struct server = socket(AF_INET, SOCK_STREAM) server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) server.bind(('127.0.0.1', 6666)) server.listen(5) while True: conn, client_addr = server.accept() while True: try: cmd = conn.recv(1024) if len(cmd) == 0: break # subprocess.Popen类用于在一个新进程中执行一个子程序 res = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) stdout_res = res.stdout.read() stderr_res = res.stdout.read() total_size = len(stdout_res) + len(stderr_res) # 1. 先发头信息(固定长度的bytes): 对数据的描述 header = struct.pack('i', total_size) conn.send(header) # 2. 再发真实的数据 conn.send(stdout_res) conn.send(stderr_res) except Exception: break conn.close()
客户端
from socket import * import struct client = socket(AF_INET, SOCK_STREAM) client.connect(('127.0.0.1', 6666)) while True: msg = input('请输入命令 >>: ').strip() if len(msg) == 0: continue client.send(msg.encode()) # 1. 先收固定长度的头信息: 解析出数据的描述信息, 拿到数据的总大小total_size header = client.recv(4) total_size = struct.unpack('i', header)[0] # 2. recv_size=0, 循环接收, 每接收一次, recv_size+=接收长度 # 3. 直到recv_size=total_size recv_size = 0 cmd_res = b'' while recv_size < total_size: recv_data = client.recv(1024) recv_size += len(recv_data) print(recv_data.decode('utf-8'), end='') else: print(' ') print(cmd_res.decode())
实现方法二: 使用truct模块
服务端
from socket import * import subprocess import struct import json server = socket(AF_INET, SOCK_STREAM) server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) server.bind(('127.0.0.1', 6666)) server.listen(5) while True: conn, client_addr = server.accept() while True: try: cmd = conn.recv(1024) if len(cmd) == 0: break # subprocess.Popen类用于在一个新进程中执行一个子程序 res = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) stdout_res = res.stdout.read() stderr_res = res.stdout.read() total_size = len(stdout_res) + len(stderr_res) # 1. 制作头信息 header_dic = { 'filename': 'aaa.text', 'total_size': total_size, 'md5': '123123123' } json_str = json.dumps(header_dic) json_str_bytes = json_str.encode('utf-8') # 2. 先把头信息的长度发过去 header_size = struct.pack('i', len(json_str_bytes)) conn.send(header_size) # 3. 发送头信息 conn.send(json_str_bytes) # 4. 再发真实的数据 conn.send(stdout_res) conn.send(stderr_res) except Exception: break conn.close()
客户端
from socket import * import struct import json client = socket(AF_INET, SOCK_STREAM) client.connect(('127.0.0.1', 6666)) while True: msg = input('请输入命令 >>: ').strip() if len(msg) == 0: continue client.send(msg.encode()) # 先接收4个字节, 从中提取接下来要接收的头的长度 header = client.recv(4) header_len = struct.unpack('i', header)[0] # 然后接收头信息, 并进行解析 json_str_bytes = client.recv(header_len) json_str = json_str_bytes.decode('utf-8') header_dict = json.loads(json_str) print(header_dict) total_size = header_dict['total_size'] recv_size = 0 cmd_res = b'' while recv_size < total_size: recv_data = client.recv(1024) recv_size += len(recv_data) print(recv_data.decode('utf-8'), end='') else: print(' ') print(cmd_res.decode())