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 ...
随机推荐
- Web前后端数据交换技术和规范发展史:Form、Ajax、Comet、Websocket
第一阶段:Form web应用想要与服务器交互,必须提交一个表单(form).服务器接收并处理该表单,然后返回一个全新的页面. 缺点:前后两个页面需要更新的数据可能很少,这个过程可能传输了很多之前那个 ...
- 看图说说Sun HotSpot虚拟机对象
- MFC中的多线程
程序是计算机指令的几何,以文件的形式存在磁盘上.进程被定义为正在运行的程序的实例,是在进行地址空间中的一次执行活动.一个程序可以对应多个进程,如可以通过打开多个Word程序,每个word的应用就是一个 ...
- Low-level Thinking in High-level Shading Languages
因为要反汇编shader代码,所以google了数学函数_sat的知识,发现了一些高级着色语言的优化相关的问题.Low-level Thinking in High-level Shading Lan ...
- python中GUI使用小结
1 先来个简单的 import wx app = wx.App() frm = wx.Frame(None, title="Hello World") frm.Show() app ...
- C# Using 开发随录
Using 关键字有2个主要用途: 1.做为语句 用于定义一个范围,在此范围的末尾将释放对象 2.做为指令 用于为命名空间创建别名或导入其他命名空间中定义的类型 C# 通过 .NET Framew ...
- linux下使用supervisor启动.net core mvc website的配置
发布好的asp.net core mvc项目, 如果想在window或linux下的以控制台程序启动的话,可以用下面的命令 dotnet MyProject.dll --urls="http ...
- winform app.cpnfig 文件的引用
1.app.config配置文件修改 <?xml version="1.0" encoding="utf-8"?> <configuratio ...
- 如何使用jQuery实现根据不同IP显示不同的内容
一些SEM的投放页会针对不同地域做针对性的内容推广,下面我把实现方法分享出来. 一.引用新浪提供的IP查询的js库 <script src="http://int.dpool.sina ...
- 部署网络存储ISCSI
1.什么是ISCSIInternet Small Computer System Interface 互联网小型计算机接口技术,是一种将SCS存储与以太网技术相结合,可以用来在互联网中传输SCSI接口 ...