1. 问题说明
1.1 系统需求
某设备管理系统需兼容安卓智能设备与嵌入式RTOS设备,两类设备均支持 HTTP 和 TCP 协议。受现场物联网卡限制,所有设备只能通过单一地址和端口接入。系统使用 HAProxy 进行流量分发,对外统一暴露一个端口,内部按协议类型分流至不同服务。
1.2 问题描述
在生产环境中,安卓设备调用接口 A 可正常收到响应并执行业务流程;而 RTOS 设备调用同样的接口 A 时,仅收到响应码00且响应体为空(date字段为空)。使用 Postman 模拟 RTOS 设备的请求却能正常收到响应。
2. 问题分析
由于服务端未对两类设备做差异化处理,且 Postman 测试正常,初步判断问题源于 RTOS 设备侧的 HTTP 客户端实现。安卓设备使用标准 HTTP 框架(如 OkHttp),而 RTOS 设备采用自研的轻量级 Socket 实现,可能在 HTTP 协议头部处理、连接管理或响应解析等方面存在兼容性问题。
2.1 RTOS设备与Android设备实现差异分析
| 对比维度 | Android设备 | RTOS设备 | 问题影响 |
|---|---|---|---|
| HTTP库 | OkHttp/HttpURLConnection | 手动Socket实现 | 头部完整性差异 |
| 协议支持 | HTTP/1.1完整支持 | 可能简化实现 | 缺少必需头部 |
| 连接管理 | Keep-Alive自动处理 | 可能固定短连接 | HAProxy检测失败 |
| 头部格式 | RFC规范格式 | 可能格式不规范 | 协议检测不通过 |
| 编码处理 | UTF-8自动处理 | 可能编码错误 | 响应解析失败 |
RTOS设备端的请求
POST /serxxxxs/xxxs/xxxxxxe HTTP/1.1 Content-Type: application/json; charset=UTF-8 Connection: Keep-Alive Accept:application/json Host: 192.168.xx.xx Content-Length: 44 { "pn": "xxxxx", "sn": "xxxxxxxxxxxx" }2.2 HTTP协议请求和响应规范
2.2.1 HTTP请求头(Request Headers)
2.2.1.1通用头部(General Headers)
| 头部字段 | 说明 | 示例 |
|---|---|---|
| Connection | 控制连接状态 | Connection: keep-aliveConnection: close |
| Cache-Control | 缓存控制指令 | Cache-Control: no-cacheCache-Control: max-age=3600 |
| Pragma | HTTP/1.0的缓存控制 | Pragma: no-cache |
| Date | 消息生成的日期时间 | Date: Tue, 15 Nov 2024 08:12:31 GMT |
2.2.1.2请求头部(Request Headers)
| 头部字段 | 说明 | 示例 |
|---|---|---|
| Host | 服务器的域名和端口(HTTP/1.1必需) | Host: api.example.com:8443 |
| User-Agent | 客户端信息 | User-Agent: Mozilla/5.0 (Android 10) |
| Accept | 可接受的响应内容类型 | Accept: application/json, text/plain, */* |
| Accept-Encoding | 可接受的编码方式 | Accept-Encoding: gzip, deflate, br |
| Accept-Language | 可接受的语言 | Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 |
| Authorization | 认证信息 | Authorization: Bearer token123 |
| Cookie | 发送的Cookie | Cookie: sessionId=abc123; userId=456 |
| Referer | 来源页面URL | Referer: https://www.example.com/page |
| Origin | 跨域请求来源 | Origin: https://www.example.com |
2.2.1.3实体头部(Entity Headers)
| 头部字段 | 说明 | 示例 |
|---|---|---|
| Content-Type | 请求体的MIME类型 | Content-Type: application/json; charset=UTF-8 |
| Content-Length | 请求体的字节长度 | Content-Length: 348 |
| Content-Encoding | 请求体的编码方式 | Content-Encoding: gzip |
2.2.2 HTTP响应头(Response Headers)
2.2.2.1通用头部
| 头部字段 | 说明 | 示例 |
|---|---|---|
| Connection | 连接状态 | Connection: keep-alive |
| Cache-Control | 响应缓存策略 | Cache-Control: public, max-age=3600 |
| Date | 响应生成时间 | Date: Tue, 15 Nov 2024 08:15:00 GMT |
2.2.2.2响应头部
| 头部字段 | 说明 | 示例 |
|---|---|---|
| Server | 服务器软件信息 | Server: nginx/1.18.0 |
| Set-Cookie | 设置Cookie | Set-Cookie: sessionId=xyz789; Path=/; HttpOnly |
| Location | 重定向目标URL | Location: https://new.example.com/resource |
| WWW-Authenticate | 认证要求 | WWW-Authenticate: Basic realm="Access to site" |
2.2.2.3实体头部
| 头部字段 | 说明 | 示例 |
|---|---|---|
| Content-Type | 响应体的MIME类型 | Content-Type: application/json; charset=UTF-8 |
| Content-Length | 响应体的字节长度 | Content-Length: 1024 |
| Content-Encoding | 响应体的编码方式 | Content-Encoding: gzip |
| Transfer-Encoding | 传输编码方式 | Transfer-Encoding: chunked |
| Content-Disposition | 内容处理方式 | Content-Disposition: attachment; filename="file.zip" |
2.3 使用python模拟RTOS设备请求
2.3.1 请求及结果分析
import socket import json import time def debug_device_request(): """带详细调试信息的模拟""" host = "192.168.XXXX.XXXX" port = XXXXXXX path = "/serXXXXX/XXX/XXXXXXXXile" # 准备数据 payload = {"pn": "XXXXXXXXXXXX", "sn": "XXXXXXX"} # json.dumps() 的作用:将Python对象转换为JSON格式字符串 # 参数1:要转换的Python对象(字典、列表、字符串等) # 参数2:separators - 控制JSON字符串的格式 body = json.dumps(payload, separators=(',', ':')) # 构建请求 headers = [ f"POST {path} HTTP/1.1", f"Content-Type: application/json; charset=UTF-8", f"Connection: Keep-Alive", f"Accept: application/json", f"Host: {host}", f"Content-Length: {len(body)}", "", # 空行 body ] request = "\r\n".join(headers) print("=" * 50) print("模拟设备请求:") print("=" * 50) print(request) print("=" * 50) # 创建socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 关键配置 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) sock.settimeout(10.0) try: print(f"\n1. 连接到 {host}:{port}...") start_connect = time.time() sock.connect((host, port)) connect_time = time.time() - start_connect print(f" 连接成功,耗时: {connect_time:.3f}秒") print("\n2. 发送请求...") start_send = time.time() # 分步发送以便调试 request_bytes = request.encode('utf-8') total_sent = 0 chunk_size = 1024 while total_sent < len(request_bytes): sent = sock.send(request_bytes[total_sent:total_sent+chunk_size]) if sent == 0: raise RuntimeError("Socket连接已断开") total_sent += sent print(f" 已发送 {total_sent}/{len(request_bytes)} 字节") send_time = time.time() - start_send print(f" 发送完成,耗时: {send_time:.3f}秒") # 等待响应 print("\n3. 等待响应...") time.sleep(0.1) # 给服务器处理时间 # 接收响应 response = b"" start_recv = time.time() # 设置接收超时 sock.settimeout(5.0) try: while True: chunk = sock.recv(4096) if not chunk: break response += chunk print(f" 收到 {len(chunk)} 字节,累计 {len(response)} 字节") # 如果收到完整HTTP响应,可以提前退出 if b"\r\n\r\n" in response: # 检查是否有Content-Length headers_end = response.find(b"\r\n\r\n") headers = response[:headers_end].decode('utf-8', errors='ignore') if "Content-Length:" in headers: # 解析内容长度 import re match = re.search(r'Content-Length:\s*(\d+)', headers) if match: content_length = int(match.group(1)) body_start = headers_end + 4 if len(response) >= body_start + content_length: print(" 收到完整响应体") break else: # 没有Content-Length,可能是chunked或连接关闭 print(" 没有Content-Length头,继续接收...") except socket.timeout: print(" 接收超时") recv_time = time.time() - start_recv print(f"\n4. 响应统计:") print(f" 总耗时: {recv_time:.3f}秒") print(f" 总接收: {len(response)} 字节") if response: print("\n5. 响应内容:") try: response_text = response.decode('utf-8', errors='ignore') print(response_text[:2000]) # 显示前2000字符 except: print(" (无法解码为UTF-8,显示前500字节的hex)") print(response[:500].hex()) return response except socket.timeout as e: print(f"\n错误: 连接或接收超时 - {e}") except ConnectionRefusedError as e: print(f"\n错误: 连接被拒绝 - {e}") except Exception as e: print(f"\n错误: {e}") import traceback traceback.print_exc() finally: sock.close() print("\n连接已关闭") # 执行 debug_device_request()执行结果如下:
发现请求的响应中,没有Content-Length和Transfer-Encoding: chunked,且有Connection: close。设备端网络层接收到后,默认连接断了,就不会再处理后续date。
2.3.2 RFC规范要求
根据RFC 7230 Section 3.3.3,HTTP/1.1响应必须满足以下条件之一:
包含有效的
Content-Length头部使用
Transfer-Encoding: chunked使用
Connection: close(但这是针对HTTP/1.0的兼容)
实际上,对于HTTP/1.1:
如果同时没有
Content-Length和Transfer-Encoding: chunked即使有
Connection: close,也应该被视为格式错误
2.3.3 问题发生的具体流程
RTOS设备发送请求 → HAProxy接收 → 后端处理 → 返回响应
↓
Connection: close
无Content-Length
无Transfer-Encoding
↓
RTOS设备收到头部后等待...
↓
服务器关闭连接(Socket EOF)
↓
RTOS设备应该读到底,但在收到close前就返回了
↓
应用层只收到空的响应体
3. 问题解决
使用的是HAProxy进行的流量分流
3.1 修复HAProxy配置
# haproxy_fix.cfg - 确保响应格式正确 backend app_backend mode http # 1. 如果后端没有Content-Length,添加它 http-after-response set-header Content-Length %[res.len] if { res.hdr_cnt(Content-Length) eq 0 } # 2. 或者强制使用chunked编码 # http-response set-header Transfer-Encoding chunked if { res.hdr_cnt(Content-Length) eq 0 } # 3. 清理连接头部 http-response del-header Connection http-response set-header Connection close # 4. 确保Date头部存在 http-response set-header Date %[date()] server app1 192.168.1.10:8080 check3.2 修复RTOS设备客户端代码
// rtos_client_fix.c - 修复RTOS客户端响应处理 #include <stdbool.h> #include <string.h> #include <sys/socket.h> // 修复的响应读取函数 int read_http_response_fixed(int sock, char* buffer, int buf_size, int timeout_sec) { int total_received = 0; bool headers_complete = false; int content_length = -1; // -1表示未知 bool connection_close = false; // 设置超时 struct timeval tv; tv.tv_sec = timeout_sec; tv.tv_usec = 0; setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); while (total_received < buf_size - 1) { // 接收数据 int received = recv(sock, buffer + total_received, buf_size - total_received - 1, 0); if (received <= 0) { // 连接关闭或超时 break; } total_received += received; buffer[total_received] = '\0'; // 检查头部是否完整 if (!headers_complete) { char* headers_end = strstr(buffer, "\r\n\r\n"); if (headers_end) { headers_complete = true; // 解析头部 char* header_start = buffer; while (header_start < headers_end) { char* line_end = strstr(header_start, "\r\n"); if (!line_end || line_end > headers_end) break; // 检查Content-Length if (strncasecmp(header_start, "Content-Length:", 15) == 0) { sscanf(header_start + 15, "%d", &content_length); } // 检查Connection if (strncasecmp(header_start, "Connection:", 11) == 0) { if (strstr(header_start, "close")) { connection_close = true; } } header_start = line_end + 2; } // 关键修复:如果没有Content-Length但有Connection: close if (content_length == -1 && connection_close) { // 将剩余所有数据读到底 content_length = INT_MAX; // 设置为最大值,读到底 } } } // 检查是否接收完整 if (headers_complete) { int headers_len = strstr(buffer, "\r\n\r\n") - buffer + 4; int body_received = total_received - headers_len; // 如果有明确的Content-Length if (content_length >= 0 && content_length != INT_MAX) { if (body_received >= content_length) { break; // 接收完整 } } // 如果是Connection: close且无长度,继续读取直到失败 else if (content_length == INT_MAX) { // 继续读取,直到recv返回0或错误 continue; } } } buffer[total_received] = '\0'; return total_received; } // 使用示例 void process_http_response_fixed(int sock) { char response_buffer[8192]; int received = read_http_response_fixed(sock, response_buffer, sizeof(response_buffer), 30); if (received > 0) { printf("收到完整响应: %d 字节\n", received); // 查找body开始位置 char* body_start = strstr(response_buffer, "\r\n\r\n"); if (body_start) { body_start += 4; // 跳过\r\n\r\n printf("响应体: %s\n", body_start); } } else { printf("接收响应失败或超时\n"); } }