跳至内容
`CTRL K`

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

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

2023/09/08· Kushidou

使用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进行同步博客，每次运营商都会发一条高危访问的短信和邮件通知我，太烦了。

最后更新于 2024/01/04
讨论在Debian系操作系统中重新提取已安装的软件并重新打包 Linux安装VNC服务端