TCP协议粘包问题详解
前言在本章节中,我们将探讨TCP协议基于流式传输的最大一个问题,即粘包问题。本章主要介绍TCP粘包的原理与其三种解决粘包的方案。并且还会介绍为什么UDP协议不会产生粘包。 基于TCP协议的socket实现远程命令输入 我们准备做一个可以在Client端远程执行Server端 Server端代码如下: #!/usr/bin/env python3 # -*- coding:utf-8 -*- ==== 基于TCP协议的socket实现远程命令输入之Server ==== import subprocess from socket import * server = socket(AF_INET,SOCK_STREAM) server.bind(("0.0.0.0",6666)) 放在远程填入0.0.0.0,放在本地填入127.0.0.1 server.listen(5) while 1: 链接循环 conn,client_addr = server.accept() 通信循环 try: 防止Windows平台下Client端异常关闭导致双向链接崩塌Server端异常的情况发生 cmd = conn.recv(1024) if not cmd: 防止类Unix平台下Client端异常关闭导致双向链接崩塌Server端异常的情况发生 break res = subprocess.Popen(cmd.decode(utf-8"),shell=True,stdout=subprocess.PIPE,stderr= 正确结果 stderr_res = res.stderr.read() 错误结果 subprocess模块拿到的是bytes类型,所以直接发送即可 cmd_res = stdout_res if stdout_res else stderr_res 因为两个结果只有一个有信息,所以我们只拿到有结果的那个 conn.send(cmd_res) except Exception: conn.close() 由于client端链接异常,故关闭链接循环 Client端代码如下: ==== 基于TCP协议的socket实现远程命令输入之Client ==== client =xxx.xxx.xxx.xxx 填入Server端公网IP while 1: cmd = input(请输入命令>>>:).strip() not cmd: continue if cmd == quit: client.send(cmd.encode()) cmd_res = client.recv(1024) 本次接收1024字节数据 print(cmd_res.decode(")) 如果Server端是Windows则用gbk解码,类Unix用utf-8解码 client.close() 测试结果: 粘包问题及其原理上面的测试一切看起来都非常完美,但是是有一个BUG的。当我们如果读取一条非常长的命令实际上是会出问题的,比如: 这种现象被称之为粘包,那么为何会产生这样的现象呢?
我们来解析一下这种现象产生的原因: 由于我们的 这里我还画了一幅图,可以方便读者理解: 那么我们可以通过不断的增大 Nagle算法与粘包
TCP协议的发送方有一个特征。他会进行组包,如果一次发送的数据量很小,比如第一次发送10个字节,第二次发生2个字节,第三次发生3个字节。他可能会将这15个字节凑到一块发送出去,这是采用了 如下图组所示 发送方: ip_port = (127.0.0.1) buffer_size = 1024 back_log = 5 server.accept() conn.send(hello,".encode( 第一次发送是6Bytes的数据 conn.send(world,1)">")) 第二次也是6Bytes的数据 conn.send(yunyaGG!! 第三次是9Bytes的数据 接收方: 我们读取数据时统一用设定的 buffer_size 来读取 print(这是第一次的数据包:)) data_2 = client.recv(buffer_size) 这是第二次的数据包:)) data_3 =这是第三次的数据包:")) 接收结果: ==== 执行结果 ==== """ 这是第一次的数据包: hello,这是第二次的数据包: world,yunyaGG!! 这是第三次的数据包: """ 和预想的有点不太一样哈,居然把第二次和第三次组成了一个大的数据包发送过来了。这就是 现在思考一下粘包的思路,我们的发送方需要将切分解包的规则告诉给接收方。 我们尝试改一下每一次的 接收方: 我们手动的按照对方发送时的规则来进行拆包 )) data_2 = client.recv(6) )) data_3 = client.recv(9")) 接收结果: """
粘包被我们手动的计算字节数来精确的分割数据接受量的大小给解决了,但是这样做是不现实的..我们不可能知道对方发送的数据到底是怎么样的,更不用说手动计算。所以有没有更好的解决方案呢? 解决方案1:预先发送消息长度好了,其实上面关于解决粘包的思路已经出来了。我们需要做的就是让接收方知道本次发送内容的大小,接收方才能够精确的将所有数据全部提取出来不产生遗漏。其实实现方式很简单,可以尝试以下思路:
Server端: "0.0.0.0 放在远程填入0.0.0.0 放在本地测试填入127.0.0.1 server.listen(5 因为两个结果只有一个有信息,所以我们只拿到有结果的那个 msg_length = len(cmd_res) 本次数据的长度 conn.send(str(msg_length).encode( 先将要发的整体内容长度发送过去 if conn.recv(1024) == bready": 如果接收方回应了ready则开始发送真正的数据体 conn.send(cmd_res) 由于client端链接异常,故关闭链接循环 Client端: )) msg_length = int(client.recv(1024).decode( 接收到此次发送内容的整体长度 recv_length = 0 代表已接收的内容长度 cmd_res = b"" client.send(b") 发送给Server端,代表自己已经接收到此次内容长度,可以发送真正的数据啦 while recv_length < msg_length: cmd_res += client.recv(1024) 本次接收1024字节数据,可能是一小节数据 recv_length += len(cmd_res) 添加上本次读取的长度,当全部读取完后应该 recv_length == msg_length else client.close() 结果如下: 解决方案2:json+struct方案 其实上面的解决方案还是有一些弊端,因为Server端是发送了2次 所以我们需要一个更加完美的解决方案,即Server端发送一次
使用演示: >>> struct >>> b1 = struct.pack(i 尝试将 int类型的12进行序列化,得到一个4字节的对象 >>> b1 b'x0cx00x00x00' >>> struct.unpack( 尝试将12的序列化对象字节进行反解,得出元组,第1位就是需要的数据。 (12,) >>> 好了,了解到这里我们就可以开始进行改写了。 Server端代码如下: json struct 因为两个结果只有一个有信息,所以我们只拿到有结果的那个 解决粘包:构建字典,包含数据主体长度,这个就相当于其头部信息 head_msg = { msg_length": len(cmd_res), 包含数据主体部分的长度 如果是文件,还可以添加file_name,file_size等属性。 } 序列化成json格式,并且统计其头部的长度 head_data = json.dumps(head_msg).encode() head_length = struct.pack( 得到4字节的头部信息,里面包含头部的长度 发送头部长度信息,头部数据,与真实数据部分 conn.send(head_length + head_data + cmd_res) 由于client端链接异常,故关闭链接循环 Client端代码如下: 发送终端命令 解决粘包 head_length = struct.unpack( 接收到头部的长度信息 head_data = json.loads(client.recv(head_length)) 接收到真实的头部信息 msg_length = head_data["] 获取到数据主体的长度信息 recv_length = 0 "" 开始获取真正的数据主体信息 client.close()
解决方案3:iter()与偏函数(失败案例)上面那么做看似完美但还是美中不足。因为内存缓冲区本来就是只能取一次值,和迭代器很像,只能迭代一次便不能继续迭代了。基于这一点我们来做一个终极优化: 还记得 def iter(source,sentinel=None): known special case of iter iter(iterable) -> iterator iter(callable,sentinel) -> iterator Get an iterator from an object. In the first form,the argument must supply its own iterator,or be a sequence. In the second form,the callable is called until it returns the sentinel. """ pass 我们来试试这个参数做什么用的。 li = [1,2,3,4] def my_iter(): return li.pop() res = iter(my_iter,2) 代表这个迭代器没__next__一下就会执行my_iter函数,并且该函数返回值如果是2则终止迭代 print(res.__next__()) 4 3 StopIteration 第二个参数看来可以设置迭代的终点。 那么偏函数是什么呢?偏函数可以设定一个固定的参数给第一个位置的值 效果如下: from functools import partial 导入偏函数 add(x,y): return x + y func = partial(add,1) 设置辨寒暑绑定的第一个参数的值 print(func(1)) 2 print(func(5)) 6 现在我们仔细回想,当缓冲区的消息接收完毕后为空的状态是会变成 可以使用 我们尝试用函数来查看一下效果: 导入偏函数 li = [b"",12345 模拟内核缓冲区 test(buffer_size): if buffer_size: 模拟recv的数据大小 li.pop() buffer_size必须为一个int类型的值) res = "".join(iter(partial(test,1024),b)) print(res) 54321 join()方法会不断的调用iter()下的__next__,每调用一次就执行一次偏函数。知道出现b""停止 最后我们发现,这样的做法是会产生 测试的Server端代码如下: import * struct ip_port=(',8080) back_log=5 buffer_size=1024 tcp_server=socket(AF_INET,SOCK_STREAM) tcp_server.bind(ip_port) tcp_server.listen(back_log) while True: conn,addr=tcp_server.accept() 新的Client链接' True: 收 try: cmd=conn.recv(buffer_size) not cmd:break 收到Client的命令执行命令,得到命令的运行结果cmd_res res=subprocess.Popen(cmd.decode('),shell=subprocess.PIPE) err=res.stderr.read() if err: cmd_res=err : cmd_res=res.stdout.read() 发 cmd_res: cmd_res=执行成功'.encode(gbk) length=len(cmd_res) data_length=struct.pack( Exception as e: print(e) break 测试的Client代码如下: import partial 偏函数 ip_port=( tcp_client= True: cmd=input(>>: ': tcp_client.send(cmd.encode()) 解决粘包 length_data=tcp_client.recv(4) length=struct.unpack(第一种方法 recv_size=0 recv_msg=b'' while recv_size < length: 为何recv里是buffer_size,不是length,因为length如果为24G,系统内存没有那么大 所以每次buffer_size,当recv_size < length时,循环接收,直到recv_size =length,退出循环 recv_msg += tcp_client.recv(buffer_size) recv_size=len(recv_msg) 1024 第二种方法 失败版本,会引发recv()的阻塞,而不会终止迭代。因为join()方法会不断的调用其iter()方法产生的迭代器,也就是调用其__next__方法,所以第二次没消息的recv()会阻塞住。 recv_msg=''.join(iter(partial(tcp_client.recv,buffer_size),b'')) 命令的执行结果是 )) tcp_client.close() UDP协议为何不会产生粘包
UDP是面向消息的协议,每个UDP段都是一条消息,每 并且每一次 我们还是用一个快递员的那个图来进行演示: 还有一点需要注意一下。使用UDP协议进行通信的时候不管首先启动哪一方都不会报错,因为它只管发,不管有没有人接收。 所以,这也是我称UDP协议比较随便的原因。
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |