Python TCP Socket接收JSON解析报'length'键错误问题排查
TCP转HTTP转发程序
'length'键错误排查方案 你遇到的Other error occurred: 'length'是KeyError异常,本质是代码尝试读取不存在的字典键,叠加TCP流处理、逻辑写法的多个bug共同导致,具体问题点和修复方式如下:
已确认的代码问题
- 无意义的数据重复逻辑:接收TCP数据后写的
data += data会将刚收到的分片列表复制拼接一份,直接产生大量重复无效内容,是触发解析异常的核心诱因之一。 - TCP流处理逻辑完全不可靠:TCP是面向流的协议,单次
recv()调用无法保证刚好拿到完整、边界对齐的JSON对象,靠替换}{为},{拼接JSON的写法容错性为0——只要JSON字段值中包含}{字符串、或者单次recv拿到半包/粘包数据,JSON解析就会直接失败。 - 键读取缺少前置判断:协议中
length字段只存在于长度包头中,业务数据包(比如你示例里的agent_hello结构)没有这个字段,代码默认解析结果的最后一个元素一定带length键,处理到业务包时必然触发键不存在的错误。 - HTTP头读取缺少容错:GET请求默认无请求体,多数客户端不会携带
Content-Length头,代码直接读取self.headers['Content-Length']会额外触发KeyError。 - 连接生命周期管理错误:TCP连接在全局作用域初始化,只要一次接收出错就直接关闭连接,后续所有请求都会因为连接失效直接失败。
修复步骤
- 删掉
data += data这行无效逻辑。 - 替换字符串拼接JSON的处理方式,用标准库
json.JSONDecoder的raw_decode方法做增量解析,维护一个持久的接收缓冲区:每次收到新数据就追加到缓冲区,循环从缓冲区头部解析完整JSON对象,解析成功就截断缓冲区对应长度,解析失败说明数据不全,继续等待下一批TCP数据即可,从根本上解决粘包、半包问题。 - 读取
length键前先做存在性判断:只有解析出的JSON对象包含length字段时,才更新下一次接收的缓冲区大小,普通业务数据包直接存入待转发队列即可,不要强行读取length字段。 - 读取
Content-Length头前先判断键是否存在,不存在则默认内容长度为0,避免触发头字段缺失错误。 - 增加TCP连接状态检查逻辑,连接断开时自动重连,避免单次错误导致整个服务不可用。
参考修复代码
#!/usr/bin/python3 import http.server import socketserver import json import socket PORT = 8000 INIT_BUFF_SIZE = 4096 TCP_TARGET_ADDR = ('10.10.10.10', 2100) tcp_conn = None recv_buf = "" json_decoder = json.JSONDecoder() def get_alive_tcp_conn(): global tcp_conn if tcp_conn is None or tcp_conn.fileno() == -1: tcp_conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) tcp_conn.connect(TCP_TARGET_ADDR) return tcp_conn def get_forward_data(): global recv_buf conn = get_alive_tcp_conn() forward_list = [] current_buf_size = INIT_BUFF_SIZE while True: # 先从已有缓冲区解析完整JSON recv_buf = recv_buf.strip() while recv_buf: try: json_obj, parse_end_pos = json_decoder.raw_decode(recv_buf) recv_buf = recv_buf[parse_end_pos:].lstrip() # 区分长度包和业务数据包 if "length" in json_obj: current_buf_size = json_obj["length"] + 16 else: forward_list.append(json_obj) # 按需调整返回逻辑:示例为拿到1个完整业务包即返回 if len(forward_list) >= 1: return forward_list except json.JSONDecodeError: # 剩余数据不完整,跳出解析继续收数据 break # 从TCP连接读取新数据 new_chunk = conn.recv(current_buf_size).decode("utf-8") if not new_chunk: conn.close() raise ConnectionError("上游TCP连接已断开") recv_buf += new_chunk class MyHttpHandler(http.server.SimpleHTTPRequestHandler): def do_GET(self): # 拉取上游TCP数据 try: resp_data = get_forward_data() except Exception as e: self.send_response(502) self.end_headers() self.wfile.write(f"拉取上游数据失败: {str(e)}".encode("utf-8")) return # 解析请求参数 content_len = 0 if "Content-Length" in self.headers: try: content_len = int(self.headers["Content-Length"]) except ValueError: content_len = 0 req_data = None if content_len > 0: try: req_data = json.loads(self.rfile.read(content_len)) except json.JSONDecodeError: req_data = None print("收到请求参数:", req_data) # 返回响应 self.send_response(200) self.send_header("Content-Type", "application/json") self.end_headers() self.wfile.write(json.dumps(resp_data).encode("utf-8")) if __name__ == "__main__": socketserver.TCPServer.allow_reuse_address = True with socketserver.TCPServer(("", PORT), MyHttpHandler) as httpd: print(f"HTTP服务启动成功,监听0.0.0.0:{PORT}") try: httpd.serve_forever() except KeyboardInterrupt: print("收到终止信号,正在关闭服务") if tcp_conn and tcp_conn.fileno() != -1: tcp_conn.close() httpd.server_close()
可以根据实际业务需求调整
get_forward_data里的返回逻辑,比如需要一次返回多个业务包、或者匹配特定类型的包再返回,基于增量解析的逻辑不会再出现JSON解析错位、键读取错误的问题。
内容的提问来源于stack exchange,提问作者Aleksa Markovic
相关产品推荐
相关产品推荐

