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、以后应该还会学习到更好的解决方法,努力学习。
python的socket.recv函数陷阱的更多相关文章
- python的socket的学习
一.Socket相关知识 1.socket是什么: socket是应用层与TCP/IP协议族通信的中间软件抽象层,他是一组接口.在设计模式中,Socket其实就是一个门面模式. 它把复杂的TCP/IP ...
- [转]Socket send函数和recv函数详解
1.send 函数 int send( SOCKET s, const char FAR *buf, int len, int flags ); 不论是客户还是服务器应用程序都用send函数来向TCP ...
- Linux下tcp协议socket的recv函数返回时机分析(粘包)
http://www.vckbase.com/index.php/wv/10http://blog.csdn.net/zlzlei/article/details/7689409 文章一: 当前在网络 ...
- socket使用TCP协议时,send、recv函数解析以及TCP连接关闭的问题
Tcp协议本身是可靠的,并不等于应用程序用tcp发送数据就一定是可靠的.不管是否阻塞,send发送的大小,并不代表对端recv到多少的数据. 在阻塞模式下, send函数的过程是将应用程序请求发送的数 ...
- Socket send函数和recv函数详解
1.send 函数 int send( SOCKET s, const char FAR *buf, int len, int flags ); 不论是客户还是服务器应用程序都用send函数来向TCP ...
- socket函数send和recv函数
转自:http://www.cppblog.com/aaxron/archive/2012/04/27/172891.html 在发送端,一次发送4092个字节,在接收端,一次接收4092个字节,但是 ...
- linux Socket send与recv函数详解
转自:http://www.cnblogs.com/blankqdb/archive/2012/08/30/2663859.html linux send与recv函数详解 1 #include ...
- [转]socket使用TCP协议时,send、recv函数解析以及TCP连接关闭的问题
Tcp协议本身是可靠的,并不等于应用程序用tcp发送数据就一定是可靠的.不管是否阻塞,send发送的大小,并不代表对端recv到多少的数据. 在阻塞模式下, send函数的过程是将应用程序请求发送的数 ...
- socket中send和recv函数
Socket一次Recv接受的字节有限制么? 从套接字接收数据. 返回值是表示接收数据的字符串. 一次接收的最大数据量由bufsize指定.它默认为零. 注意为了最好地匹配硬件和网络现实,bufsiz ...
随机推荐
- linux 下 WebSphere日志中文乱码
管理控制台--->服务器--->应用程序服务器--->server1--->java和进程管理--->进程定义--->java虚拟机--->将通用jvm参数设 ...
- 《Forward团队-爬虫豆瓣top250项目-开发文档》
码云地址:https://github.com/xyhcq/top250 模块功能:获取豆瓣top250网页的源代码,并分析. def getHTMLText(url,k): # 获取网页源代码 tr ...
- Linux 基础教程 38-文件下载
什么是wget wget用原始帮助里面的英文来讲就是:The non-interactive network downloader,非交互式网络下载器.它支持HTTP.HTTPS.FTP等协议 ...
- GPU的历史:从固定管线到可编程管线再到通用计算平台
开始的时候GPU不能编程,也叫固定管线的,就是把数据按照固定的通路走完. 和CPU同样作为计算处理器,顺理成章就出来了可编程的GPU,但是那时候想在GPU上编程可不是容易的事,你只能使用GPU汇编来写 ...
- solr特点三: QueryElevation(编辑结果排序)
在理想的情况下,搜索引擎只返回与用户查询相关的文档.而在现实的查询中,编辑(没发现更合适的表达)通常需要指定特定文档在搜索结果中的特定位置.这样做有很多原因.或许 “置顶” 的文档就是最好的查询结果. ...
- pycharm中安装可以贴图片的Markdown插件
方法一:(测试成功) 先安装官方推荐的Markdown support插件,再安装Paste images into MarkDown 如果Paste images into MarkDown插件在线 ...
- sonar扫描多模块的maven项目
SonarQube Scanner,作为代码扫描的工具,通过它,将项目的代码读取并发送至SonarQube服务器中,才能让SonarQube进行代码分析. 可以认为SonarQube Scanner就 ...
- 正则表达式的Wed验证应用(40)
电子邮件地址的校验 <?php /* 校验邮件地址*/ function checkMail($email) { //用户名,由“\w”格式字符.“-”或“.”组成 $email_name= & ...
- 【OCP-052】052最新考试题库分析整理-第7题
7.Which is true about external tables? A) The ORACLE_DATAPUMP access driver can be used to write dat ...
- 洛谷P2179 [NOI2012]骑行川藏(拉格朗日乘数法)
题面 传送门 题解 看\(mashirosky\)大佬的题解吧--这里 //minamoto #include<bits/stdc++.h> #define R register #def ...