--- title: Python socket 收取POST协议内容时没有payload date: 2023-09-08T00:00:00+08:00 lastmod: 2024-01-04T00:00:00+08:00 draft: false description: '' authors: - Kushidou tags: - python - http - server categories: [] toc: true comments: true --- 使用Python搭建简单服务器的时候,只能收到headers,收不到POST内容的情况 最近做了一个简单的http服务器,用于接收IoT端上传的json格式的日志。 服务端使用socket中的方法接收原始数据,再对原始数据单独进行处理,根据报文头部的GET或者POST等方法进行具体的操作。 ```python 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接收和处理数据会更加方便快捷: ```python 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()`部分改写成下面的方法: ```python 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的代码如下: ```python 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进行同步博客,每次运营商都会发一条高危访问的短信和邮件通知我,太烦了。