python的socket.recv函数陷阱
目录
前言
惯例练习历史实验,在编写tcp
数据流粘包实验的时候,发现一个奇怪的现象。当远程执行的命令返回结果很短的时候可以正常执行,但返回结果很长时,就会发生json
解码错误,故将排错和解决方法记录下来。
一个粘包实验
服务端(用函数):
import socket import json import struct import subprocess import sys from concurrent.futures import ThreadPoolExecutor def init_socket(): addr = ('127.0.0.1', 8080) server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind(addr) server.listen(5) print('start listening...') return server def handle(request): command = request.decode('utf-8') obj = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) result = obj.stdout.read() + obj.stderr.read() # 如果是win还需要转换编码 if sys.platform == 'win32': result = result.decode('gbk').encode('utf-8') return result def build_header(data_len): dic = { 'cmd_type': 'shell', 'data_len': data_len, } return json.dumps(dic).encode('utf-8') def send(conn, response): data_len = len(response) header = build_header(data_len) header_len = len(header) struct_bytes = struct.pack('i', header_len) # 粘包发送 conn.send(struct_bytes) conn.send(header) conn.send(response) def task(conn): try: while True: # 消息循环 request = conn.recv(1024) if not request: # 链接失效 raise ConnectionResetError response = handle(request) send(conn, response) except ConnectionResetError: msg = f'链接-{conn.getpeername()}失效' conn.close() return msg def show_res(future): result = future.result() print(result) if __name__ == '__main__': max_thread = 5 futures = [] server = init_socket() with ThreadPoolExecutor(max_thread) as pool: while True: # 链接循环 conn, addr = server.accept() print(f'一个客户端上线{addr}') future = pool.submit(task, conn) future.add_done_callback(show_res) futures.append(future)
客户端(用类):
import socket import struct import time import json class Client(object): addr = ('127.0.0.1', 8080) def __init__(self): self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket.connect(self.addr) print('连接上服务器') def get_request(self): while True: request = input('>>>').strip() if not request: continue return request def recv(self): # 拆包接收 struct_bytes = self.socket.recv(4) header_len = struct.unpack('i', struct_bytes)[0] header_bytes = self.socket.recv(header_len) header = json.loads(header_bytes.decode('utf-8')) data_len = header['data_len'] gap_abs = data_len % 1024 count = data_len // 1024 recv_data = b'' for i in range(count): data = self.socket.recv(1024) recv_data += data recv_data += self.socket.recv(gap_abs) print('recv data len is:', len(recv_data)) return recv_data def run(self): while True: # 消息循环 request = self.get_request() self.socket.send(request.encode('utf-8')) response = self.recv() print(response.decode('utf-8')) if __name__ == '__main__': client = Client() client.run()
执行结果
在执行dir/ipconfig
等命令时可以正常获取结果,但是在执行tasklist
命令时,发现没有获取完整的执行结果,而且下一条命令将发生报错:
Traceback (most recent call last): File "F:/projects/hello/world.py", line 62, in <module> client.run() File "F:/projects/hello/world.py", line 57, in run response = self.recv() File "F:/projects/hello/world.py", line 35, in recv header = json.loads(header_bytes.decode('utf-8')) File "C:\Users\zouliwei\AppData\Local\Programs\Python\Python36\lib\json\__init__.py", line 354, in loads return _default_decoder.decode(s) File "C:\Users\zouliwei\AppData\Local\Programs\Python\Python36\lib\json\decoder.py", line 339, in decode obj, end = self.raw_decode(s, idx=_w(s, 0).end()) File "C:\Users\zouliwei\AppData\Local\Programs\Python\Python36\lib\json\decoder.py", line 357, in raw_decode raise JSONDecodeError("Expecting value", s, err.value) from None json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
排错思路
1、错误明确指示是json
的解码发生了错误,解码错误应该是来自于解码的数据编码不正确或者读取的数据不完整。
2、发生错误的函数在客户端,错误在第6
行,摘出如下:
def recv(self): # 拆包接收 struct_bytes = self.socket.recv(4) header_len = struct.unpack('i', struct_bytes)[0] header_bytes = self.socket.recv(header_len) header = json.loads(header_bytes.decode('utf-8')) # 此行发生错误 data_len = header['data_len'] gap_abs = data_len % 1024 count = data_len // 1024 recv_data = b'' for i in range(count): data = self.socket.recv(1024) recv_data += data recv_data += self.socket.recv(gap_abs) print('recv data len is:', len(recv_data)) return recv_data
3、继续思考,第6
行尝试对接收到的头部二进制数据进行json
解码,而头部二进制在服务器是通过UTF-8
编码的,查看服务器端编码代码发现没有错误,所以编码错误被排除。剩下的应该就是接收的数据不完整问题。
4、按理说,通过struct
和header
来控制每一次读取的字节流可以保证每次收取的时候是准确完整的收取一个消息的数据,但是这里却发生了错误,我通过在下方的for
函数增加print
看一下依次循环读取时的长度数据:
for i in range(count): data = self.socket.recv(1024) print('recv接收的长度是:', len(data)) # 增加此行查看每次循环读取的长度是多少,按理应该是1024 recv_data += data
结果令我意外:
recv接收的长度是: 1024 recv接收的长度是: 1024 recv接收的长度是: 1024 recv接收的长度是: 1024 recv接收的长度是: 400 # 错误 recv接收的长度是: 1024 recv接收的长度是: 1024 recv接收的长度是: 1024 recv接收的长度是: 400 # 错误 recv接收的长度是: 1024 recv接收的长度是: 1024 recv接收的长度是: 1024 recv接收的长度是: 400 # 错误 recv接收的长度是: 1024 recv接收的长度是: 1024 recv data len is: 14121
按照逻辑,每一次循环应该都收取1024
字节,却发现有3
次收取并不完整(每次执行时错误不完全一样,但是都会发生错误),这就是导致最终数据不完整的原因。
因为执行tasklist
返回的结果很长,导致接收数据不完整,于是下一条执行命令就发生了粘包,json
解码的数据就不是一个正常的数据,故报错。
解决和总结
1、之所以会发生这种情况,我猜测应该是recv
函数的接收机制原因,recv
函数一旦被调用,就会尝试获取缓冲中的数据,只要有数据,就会直接返回,如果缓冲中的数据大于1024
,最多返回1024
字节,不过如果缓冲只有400
,也只会返回400
,这是recv
函数的读取机制。
2、当客户端需要读取大量数据(执行tasklist
命令的返回就达到1w
字节以上)时,需要多次recv
,每一次recv
时,客户端并不能保证缓冲中的数据量已经达到1024
字节(这可能有服务器和客户端发送和接收速度不适配的问题),有可能某次缓冲只有400
字节,但是recv
依然读取并返回。
3、最初尝试解决的方法是,在recv
之前增加time.sleep(0.1)
来使得每次recv
之前都有一个充足的时间来等待缓冲区的数据大于1024
,此方法可以解决问题,不过这方法不是很好,因为如果服务器在远程,就很难控制sleep
的秒数,因为你不知道网络IO
会发生多长时间,一旦sleep
时间过长,就会长期阻塞线程浪费cpu
时间。
4、查看recv
函数源码,发现是c
写的,不过recv
的接口好像除了size
之外,还有一个flag
参数。翻看《python参考手册》
查找recv
函数的说明,recv
函数的flag
参数可以有一个选项是:MSG_WAITALL
,书上说,这表示在接收的时候,函数一定会等待接收到指定size
之后才会返回。
5、最终使用如下方法解决:
for i in range(count): # time.sleep(0.1) data = self.socket.recv(1024, socket.MSG_WAITALL) print('recv接收的长度是:', len(data)) recv_data += data
接收结果:
recv接收的长度是: 1024 recv接收的长度是: 1024 recv接收的长度是: 1024 recv接收的长度是: 1024 recv接收的长度是: 1024 recv接收的长度是: 1024 recv接收的长度是: 1024 recv接收的长度是: 1024 recv接收的长度是: 1024 recv接收的长度是: 1024 recv接收的长度是: 1024 recv接收的长度是: 1024 recv接收的长度是: 1024 recv接收的长度是: 1024 recv接收的长度是: 1024 recv data len is: 16039
6、以后应该还会学习到更好的解决方法,努力学习。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Java并发基础:了解无锁CAS就从源码分析
CAS的全称为Compare And Swap,直译就是比较交换。是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值,其实现方式是基于硬件平台的汇编指令,在intel的CPU中,使用的是cmpxchg指令,就是说CAS是靠硬件实现的,从而在硬件层面提升效率。 CSA 原理 利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法,其它原子操作都是利用类似的特性完成的。在 java.util.concurrent 下面的源码中,Atomic, ReentrantLock 都使用了Unsafe类中的方法来保证并发的安全性。 CAS操作是原子性的,所以多线程并发使用CAS更新数据时,可以不使用锁,JDK中大量使用了CAS来更新数据而防止加锁来保持原子更新。 CAS 操作包含三个操作数 :内存偏移量位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。 源码分析 下面来看一下 java.util.concurrent.atomic.AtomicInteger...
- 下一篇
在Ubuntu14.04中如何安装Py3和切换Py2和Py3环境
前几天小编给大家分享了如何安装Ubuntu14.04系统,感兴趣的小伙伴可以戳这篇文章:手把手教你在VMware虚拟机中安装Ubuntu14.04系统。今天小编给大家分享一下在Ubuntu14.04系统中如何安装Python3的简单教程,并且实现Python2和Python3直接的切换,具体的教程如下。 1、在Ubuntu系统中,关于Python2和Python3的安装其实很简单,比Windows下的安装要简单的多。一般来说,Python2都是Ubuntu系统自带的,默认的版本是Python2.7,正常情况下是无需安装的。直接在命令行中输入python2就可以进入Python2的环境了,如下图所示。 2、这里以Python3的安装为例,直接在Ubuntu14.04系统命令行中输入安装命令:sudo apt-get install python3.4。务必要指定Python的版本,如果想安装Python3.5、Python3.6的话,就把安装命令换为sudo apt-get install python3.5、sudo apt-get install python3.6即可。安装过程一般...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS8编译安装MySQL8.0.19
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- Hadoop3单机部署,实现最简伪集群
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- Linux系统CentOS6、CentOS7手动修改IP地址
- CentOS7,8上快速安装Gitea,搭建Git服务器
- CentOS关闭SELinux安全模块