Python socket 收取POST协议内容时没有payload

2023-09-08
update 2024-01-04

使用Python搭建简单服务器的时候,只能收到headers,收不到POST内容的情况

最近做了一个简单的http服务器,用于接收IoT端上传的json格式的日志。

服务端使用socket中的方法接收原始数据,再对原始数据单独进行处理,根据报文头部的GET或者POST等方法进行具体的操作。

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind((self.host, self.port))
sock.listen(128)

(client, address) = self.sock.accept()
th = Thread(target=self.accept_request, args=(client, address))

def accept_request(client, address):
	data = client_sock.recv(8192)
	req = data.decode("utf-8")
	# 处理数据的具体方法
    response = process_response(req, client_addr)
    client_sock.send(response)
 
    # clean up
    client_sock.shutdown(1)
    client_sock.close()

上面的代码是大概的处理流程。在这个流程中,我发现处理POST的数据的时候,经常只能收到headers部分,payload部分的数据不管发几次都是空的。而且接收缓冲区已经扩大到了8192字节,headers部分的长度只有200多字节,IoT端上传的payload也不过上百字节,不应该是超出缓冲区的问题。

=====2024.01.04更新=======

下面的方法突然某一天就失效了,我不清楚是不是python的环境变化导致的。

其实用flask接收和处理数据会更加方便快捷:

from flask import Flask, request

# 实例化flask
app = Flask(__name__)

# 接收和处理post的方法
@app.route('/test', methods=['POST'])
def recv_file_www():
    # post的传入参数
    print(request.args.to_dict())
    # post所承载的数据
    print(request.data)
    return 'OK'
    
if __name__ == '__main__':
    app.run(port=31408, host="0.0.0.0",debug=True)

上面就是一个完整的post接收和处理程序了,不需要调试socket,也不需要自己处理ResponseBuilder,最重要的是能保证获取到post数据。

======以下是原来的内容==========

经过网上搜索与自己尝试,发现socket.recv返回的时候是分段返回的,貌似会按照实际内容切割返回,必须不断的recv才能完整接收数据。有一种解决方案是POST传送之前先确定报文的完整擦换个年度,然后利用while循环,接收到对应长度才结束。

但是POST发送的一个不定长的数据,如果每次都通过http报文先传送post完整长度的话,有些浪费资源。于是我想到了这么一个方案:

POST的报文头会包含一个Content-length的数据,记录的是payload的长度。那我在接收阶段就对headers部分进行解码和处理,获取到content-length后,在加上本次获取headers的长度就是POST的完整长度了。

那么将上面代码的client_sock.recv()部分改写成下面的方法:

def recv(self, client_sock):
    lrecved = 8192          # 记录每次接收的长度
    lrecv_all=0             # 记录数据总长度 
    data_body = bytes()     # 接收的所有数据存入这里
    payload_len = 0         # 解析的payload长度
    payload_flag = 0        # 用于记录是否接收到了payload,以及实时记录接收到payload的长度
    while True :
        part_body= client_sock.recv(8192)   # 循环接收数据
        data_body +=  part_body             # 将接收的数据写入data_body
        lrecved = len(part_body)            # 获取本次接收长度
        lrecv_all += lrecved                # 计算总长度

        if payload_flag == 0:               # 没有接收到payload部分时,才进入下面的判断流程
            if data_body[0:3].decode() == 'GET':    # 由于我get方法没出现过断开的问题,因此直接返回即可。
                break
            elif data_body[0:4].decode() == 'POST':
                payload_len = count_payload_len(data_body)      # 解析头部,计算payload部分的长度

                if lrecved < payload_len: 
                # 偶尔会出现能接收到payload的情况,所以多加了一个判断。
                # 能接收payload肯定总长度大于payload_len的
                    print("no payloads, keep recving...")
                    # 计算post报文的总长度,借用payload_flag变量
                    payload_flag = lrecved + payload_len
        # 当接收到的总长度 >= POST的计算长度,表示所有数据接收完成,退出
        if payload_flag <= lrecv_all:
            break
                 
    return data_body

里面解析headers的代码如下:

def count_payload_len(data):
    for d in data.split(b'\r\n'):                   # 通过\r\n来分隔报文头
        if "LENGTH" in d.upper().decode():          # 如果报文头部有length关键词,当然可以写完整,我的heasers没有其他的length就简化了
            return int(d.decode().split(":")[1])    # 分隔出payload长度并返回
    return 0

上面的代码其实写的很复杂,因为我解析POST整体报文的功能已经写完并且通过第三方http报文调试工具验证过了,上IoT设备实验的时候才发现了问题,本着少改一点是一点的原则,就把这一部分复杂化了,但确实解决了我的问题,就OK了。

对了,这篇博客就是利用这个这个方案改造的新服务端进行上传同步的,之前用GitHub Actions进行同步博客,每次运营商都会发一条高危访问的短信和邮件通知我,太烦了。

Avatar
Kushidou 什么都学,却什么都不精通的一个小白:)