尧图精选

Pico W MicroPython HTTP客户端实战:urequests稳定调用指南

🕒 发布时间:2026/9/10 6:27:30 📁 来源:尧图网络
1. 这不是“又一篇教程”而是Pico上真正能跑通的HTTP客户端实战手记MicroPython、urequests、Pico、HTTP客户端——这几个词堆在一起看起来像极了某篇被转发过百次的“入门指南”。但如果你真在树莓派Pico上敲过import urequests然后卡在OSError: [Errno 113] EHOSTUNREACH里反复重启或者发现POST请求发出去后Wireshark抓包里连SYN都没看到你就知道网上90%的“一篇搞懂”根本没跑通过真实硬件。我用Pico W带WiFi和Pico 2USB Host固件版实测了27种组合场景从最基础的GET天气API到带JSON BodyBearer Token的RESTful调用再到断网重试连接复用超时熔断的工业级封装——这篇不是讲原理的是讲怎么让Pico在真实车间环境里稳稳当当地把数据发出去、把响应拿回来。核心就三点urequests不是requests的精简版它是为资源极度受限设备重新设计的轻量协议栈Pico的WiFi驱动和TCP/IP栈有隐藏坑点HTTP客户端的本质不是发包而是状态机管理。适合正在做物联网终端开发、想用Pico替代ESP32做轻量级网关、或者被学校项目逼着用MicroPython交作业的同学。你不需要先看《HTTP权威指南》只要能写print(Hello)就能跟着本文把Pico变成一个可信赖的网络节点。2. 为什么urequests不能照搬requests底层机制与Pico硬件约束的硬核拆解2.1 urequests的“减法哲学”砍掉什么留下什么urequests库只有不到400行Python代码但它不是requests的阉割版而是针对MicroPython运行时特性的重构。关键差异点在于无连接池requests默认复用TCP连接keep-aliveurequests每次请求都新建socket发完立刻close。这不是bug是设计选择——Pico RAM仅264KB维护连接池状态需要额外内存开销而多数IoT场景本就是短连接。无自动重定向urequests.get(http://example.com)遇到301/302直接返回响应对象不会自动跳转。因为重定向意味着二次DNS解析二次TCP握手在Pico上耗时可能超过5秒且需额外内存缓存Location头。无Cookie持久化urequests.session()在urequests中不存在。MicroPython没有全局状态管理器每次请求都是干净上下文。若需会话维持必须手动提取Set-Cookie头并拼入下一次请求的headers参数。SSL/TLS支持有限urequests依赖底层ussl模块而Pico W的MicroPython固件默认不启用TLS 1.2完整套件。实测发现访问https://httpbin.org成功但访问https://api.github.com常报OSError: [Errno -1] EIO——根源是GitHub要求SNIServer Name Indication扩展而旧版MicroPython固件未实现。提示Pico W官方固件截至2024.08的ussl模块仅支持TLS 1.0/1.1且不验证证书链。生产环境必须升级到支持TLS 1.2的第三方固件如pimoroni的micropython-pico-w否则HTTPS请求成功率低于60%。2.2 Pico硬件层的真实瓶颈WiFi驱动与TCP栈的隐性消耗Pico W的RP2040芯片本身不集成WiFi靠CYW43439芯片通过SDIO接口通信。这带来三个关键约束SDIO带宽瓶颈WiFi芯片与主控间最大传输速率为25Mbps但实际可用带宽受固件调度影响。实测连续发送1KB JSON数据平均耗时320ms其中210ms花在SDIO数据搬运上而非网络传输。TCP窗口大小固定为512字节MicroPython TCP栈未实现滑动窗口动态调整。当服务器响应体大于512字节时urequests会分多次recv()每次间隔约15ms——这导致大响应体如天气API返回的JSON解析延迟显著增加。DNS解析阻塞主线程urequests.get()内部调用socket.getaddrinfo()该函数在Pico上是同步阻塞的。若DNS服务器响应慢如国内运营商DNS平均耗时800ms整个MicroPython解释器会卡死无法响应按键或传感器中断。我做过对比测试同一段代码在ESP32上DNS解析平均120ms在Pico W上达780ms。解决方案不是换DNS而是预解析——把域名提前转成IP地址硬编码绕过DNS查询。例如urequests.get(http://104.18.24.245/ip)比urequests.get(http://httpbin.org/ip)快6.3倍。2.3 HTTP客户端的本质状态机而非函数调用很多开发者把urequests.get()当成黑盒函数但Pico上的HTTP客户端必须自己管理状态。典型状态包括连接建立态CONNECTING调用socket.connect()后需轮询socket.getsockopt(0, 3, 4)检查SO_ERROR而非等待阻塞返回。Pico WiFi驱动在弱信号下connect()可能挂起3秒以上必须设超时。请求发送态SENDINGHTTP请求头Body需分块发送。实测发现若一次性send()超过256字节CYW43439芯片偶发丢包。正确做法是分片发送每片≤128字节片间加time.sleep_ms(1)。响应接收态RECEIVINGrecv()返回数据长度不确定。urequests默认recv(1024)但Pico TCP栈实际每次最多返回512字节。若响应头跨包需循环接收直到\r\n\r\n出现否则response.text会解析失败。这就是为什么简单复制粘贴的代码在模拟器里跑得飞快一烧进Pico就超时——你没在管理状态硬件在替你做不可控的决策。3. 从零搭建稳定HTTP客户端Pico W实操全流程与关键参数详解3.1 环境准备固件选择与依赖确认Pico W必须使用支持WiFi的MicroPython固件。官方固件地址为https://micropython.org/download/rp2-pico-w/但注意版本号推荐固件rp2-pico-w-20240602-unstable-378-gb4e5f4a7d.uf22024年6月不稳定版。此版本修复了CYW43439芯片在AP模式下的内存泄漏且ussl模块已启用TLS 1.2 SNI支持。禁用固件所有2023.x系列固件。实测其urequests在HTTPS请求中随机崩溃错误码为MemoryError根源是TLS握手时SSL_CTX结构体分配失败。烧录后通过Thonny连接执行以下验证import network, urequests, ussl # 检查WiFi驱动 wlan network.WLAN(network.STA_IF) wlan.active(True) print(WiFi driver OK:, wlan.isconnected()) # 应返回False未连接 # 检查SSL支持 try: ussl.wrap_socket(None) print(SSL OK) except OSError as e: print(SSL broken:, e) # 若报错[Errno 1] EPERM说明固件不支持TLS注意Pico 2RP2350需刷入micropython-pico2固件并启用USB Host功能。此时urequests无法直接使用WiFi必须通过USB连接的4G模块如SIM7600实现HTTP需额外加载usbhost驱动。本文以Pico W为主Pico 2方案见第4.3节。3.2 最小可行GET请求绕过DNS、控制超时、解析响应以下代码是经过200次压力测试的稳定版本非网上流传的简化版import network, urequests, time from machine import Pin # 1. 预连接WiFi避免在HTTP请求中处理连接 def connect_wifi(ssid, password): wlan network.WLAN(network.STA_IF) wlan.active(True) if not wlan.isconnected(): wlan.connect(ssid, password) max_wait 10 while max_wait 0 and not wlan.isconnected(): time.sleep(1) max_wait - 1 if not wlan.isconnected(): raise RuntimeError(WiFi connect timeout) return wlan # 2. 构建HTTP GET请求关键硬编码IP、显式超时、状态检查 def http_get(ip, path, timeout_ms3000): try: # 创建socket设置超时单位毫秒 s socket.socket() s.settimeout(timeout_ms / 1000) # urequests不支持毫秒级超时需底层控制 # 直接连接IP跳过DNS addr (ip, 80) s.connect(addr) # 构造HTTP请求头必须包含Host否则部分CDN拒绝 request GET {} HTTP/1.1\r\nHost: httpbin.org\r\nConnection: close\r\n\r\n.format(path) s.send(request.encode()) # 接收响应循环读取直到\r\n\r\n response b start_time time.ticks_ms() while time.ticks_diff(time.ticks_ms(), start_time) timeout_ms: try: chunk s.recv(512) if not chunk: break response chunk # 检查响应头结束标记 if b\r\n\r\n in response: break except OSError as e: if e.args[0] 110: # ETIMEDOUT raise TimeoutError(Response timeout) raise s.close() # 解析响应分离头和体 header_end response.find(b\r\n\r\n) if header_end -1: raise ValueError(Invalid HTTP response: no header end) headers response[:header_end].decode() body response[header_end 4:] # 提取状态码 status_line headers.split(\r\n)[0] status_code int(status_line.split()[1]) return { status_code: status_code, headers: headers, text: body.decode(), raw: response } except Exception as e: s.close() raise e # 使用示例 try: connect_wifi(Your_SSID, Your_Pass) result http_get(104.18.24.245, /ip) # httpbin.org的IP print(Status:, result[status_code]) print(IP:, result[text].strip()) except Exception as e: print(HTTP error:, e)参数详解timeout_ms3000总超时设为3秒。Pico W在良好信号下GET请求平均耗时850ms3秒覆盖99.7%场景。s.settimeout()必须在socket层面设超时urequests的timeout参数在Pico上无效。Host: httpbin.org即使硬编码IPHTTP/1.1要求Host头否则Cloudflare等CDN返回400。Connection: close显式关闭连接避免Pico TCP栈因keep-alive未处理而内存泄漏。3.3 POST请求进阶JSON Body、Token认证与错误重试工业场景中POST比GET更常见。以下是带Bearer Token和JSON Body的稳定实现import ujson, urequests def http_post_json(ip, path, json_data, token, timeout_ms5000): try: # 构造JSON Body body ujson.dumps(json_data) # 设置Headers关键Content-Type和Authorization headers { Content-Type: application/json, Authorization: Bearer {}.format(token), User-Agent: PicoW/1.0 # 避免被WAF拦截 } # 执行POSTurequests支持但需注意Body长度 response urequests.post( http://{}{}.format(ip, path), databody, headersheaders, timeouttimeout_ms // 1000 # urequests只接受秒级超时 ) # 检查HTTP状态码 if response.status_code not in [200, 201]: raise RuntimeError(HTTP {} {}.format( response.status_code, response.text[:100] )) return response.json() # 自动解析JSON except ValueError as e: # JSON解析失败如返回HTML错误页 raise ValueError(Invalid JSON response: {}.format(e)) except Exception as e: raise e # 使用示例向私有API提交传感器数据 sensor_data { device_id: pico-001, temperature: 23.5, humidity: 45.2, timestamp: time.time() } token your-jwt-token-here result http_post_json(192.168.1.100, /api/v1/sensors, sensor_data, token) print(Upload success:, result)关键细节ujson.dumps()比json.dumps()快3倍内存占用少40%专为MicroPython优化。User-Agent头部分API如AWS IoT Core要求非空User-Agent否则返回403。timeout_ms // 1000urequests的timeout参数只接受整数秒因此5秒超时需传5而非5000。response.json()内部调用ujson.loads()若响应非JSON格式会抛ValueError必须捕获。3.4 连接复用与资源回收解决Pico内存泄漏的核心技巧Pico RAM有限频繁创建socket会导致内存碎片。实测连续100次HTTP请求后gc.mem_free()从230KB降至142KB第101次请求直接OOM。解决方案是连接复用class HTTPClient: def __init__(self, ip, port80): self.ip ip self.port port self.sock None self.connected False def _ensure_connection(self): if not self.connected: self.sock socket.socket() self.sock.settimeout(3) # 3秒连接超时 self.sock.connect((self.ip, self.port)) self.connected True def get(self, path): self._ensure_connection() try: # 复用socket发送GET请求 request GET {} HTTP/1.1\r\nHost: {}\r\nConnection: keep-alive\r\n\r\n.format( path, self.ip ) self.sock.send(request.encode()) # 接收响应同前略 response b while True: chunk self.sock.recv(512) if not chunk or b\r\n\r\n in response chunk: response chunk break # 解析响应... return self._parse_response(response) except Exception as e: self._close_connection() raise e def _close_connection(self): if self.sock: self.sock.close() self.sock None self.connected False def _parse_response(self, raw): # 解析逻辑同前略 pass # 使用 client HTTPClient(104.18.24.245) for i in range(10): res client.get(/uuid) print(res[uuid]) client._close_connection() # 手动关闭释放资源实操心得Connection: keep-alive头必须显式添加否则服务器默认关闭连接。Pico TCP栈不支持HTTP/1.1管道化pipelining每次请求仍需等待前一个响应结束。必须在异常时调用_close_connection()否则socket句柄泄露5次后系统无可用socket。4. 生产级增强断网重试、HTTPS支持与Pico 2 USB Host方案4.1 断网自愈机制三重重试策略与退避算法工厂环境中WiFi可能瞬时中断。以下策略经72小时压力测试验证import random, time def robust_http_get(ip, path, max_retries3, base_delay_ms1000): for attempt in range(max_retries): try: # 检查WiFi连接状态 wlan network.WLAN(network.STA_IF) if not wlan.isconnected(): print(WiFi disconnected, reconnecting...) connect_wifi(SSID, PASS) # 重连WiFi # 执行HTTP请求 result http_get(ip, path, timeout_ms3000) if result[status_code] 200: print(Success on attempt {}.format(attempt 1)) return result else: raise RuntimeError(HTTP {}.format(result[status_code])) except (OSError, RuntimeError, TimeoutError) as e: print(Attempt {} failed: {}.format(attempt 1, e)) if attempt max_retries - 1: # 指数退避 随机抖动 delay min(base_delay_ms * (2 ** attempt), 30000) # 最大30秒 jitter random.randint(0, 500) # ±500ms抖动 time.sleep_ms(delay jitter) else: raise e raise RuntimeError(All retries exhausted) # 使用 try: result robust_http_get(104.18.24.245, /delay/2) print(Data:, result[text]) except Exception as e: print(Final failure:, e)策略解析指数退避第1次失败后等1秒第2次等2秒第3次等4秒避免网络雪崩。随机抖动防止多台Pico同时重试造成AP拥塞。WiFi状态预检在每次重试前检查连接避免无效重试。4.2 HTTPS终极方案固件升级与证书处理Pico W原生HTTPS支持率低必须组合方案固件升级刷入pimoroni-micropython-pico-w固件下载地址https://github.com/pimoroni/pimoroni-pico/releases该固件启用完整TLS 1.2。证书嵌入将根证书如Lets Encrypt ISRG Root X1转换为PEM格式存入Pico文件系统# 在PC上执行 openssl x509 -in isrgrootx1.pem -outform DER -out isrgrootx1.der # 用Thonny上传isrgrootx1.der到PicoHTTPS请求代码import ussl, socket def https_get(host, path): # 加载证书 with open(isrgrootx1.der, rb) as f: cert f.read() # 创建SSL socket sock socket.socket() sock.connect((host, 443)) ssl_sock ussl.wrap_socket(sock, server_hostnamehost, certcert) # 发送HTTPS请求 request GET {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n.format(path, host) ssl_sock.write(request.encode()) # 接收响应同HTTP略 response ssl_sock.read(1024) ssl_sock.close() sock.close() return response注意ussl.wrap_socket()的cert参数必须是DER格式二进制证书PEM格式会报错OSError: [Errno -1] EIO。4.3 Pico 2 USB Host方案摆脱WiFi依赖的工业级选择Pico 2RP2350支持USB Host可外接4G模块如SIM7600EC-H实现蜂窝网络HTTP硬件连接SIM7600通过USB线接入Pico 2的USB Host口天线接好SIM卡装入。固件要求刷入micropython-pico2-usbhost固件官方地址https://micropython.org/download/rp2-pico2/。AT指令初始化import usbhost, time # 初始化USB Host usb usbhost.USBHost() modem usb.device(0) # 获取第一个USB设备 # 发送AT指令需实现串口通信 def send_at(cmd): modem.write({}\r\n.format(cmd).encode()) time.sleep_ms(100) return modem.read(1024) # 激活4G模块 send_at(ATCGATT1) # 附着网络 send_at(ATCGACT1,1) # 激活PDP上下文HTTP请求通过ATHTTPCMD指令发起无需urequests库直接控制模块。此方案优势4G信号覆盖远超WiFi适合野外监测缺点功耗高待机电流15mA需外接电源。5. 常见问题排查手册从“OSError 113”到“HTTP 400”的实战解法5.1 网络层错误速查表错误码错误信息根本原因解决方案OSError: [Errno 113] EHOSTUNREACH目标主机不可达WiFi未连接或目标IP路由不通执行wlan.isconnected()检查用ping命令验证网络连通性OSError: [Errno 110] ETIMEDOUT连接超时DNS解析慢或服务器无响应改用IP直连增加超时时间至5000msOSError: [Errno -1] EIOI/O错误TLS握手失败证书不匹配或SNI缺失升级固件嵌入正确根证书检查server_hostname参数OSError: [Errno 23] ENFILE打开文件过多socket未关闭句柄耗尽每次请求后调用socket.close()使用连接池类5.2 HTTP协议层典型问题问题POST请求返回400 Bad Request原因Content-Type头缺失或Body格式错误。解法用Wireshark抓包对比确认请求头是否含Content-Type: application/json用ujson.dumps()确保Body为合法JSON字符串。问题GET请求返回403 Forbidden原因缺少User-Agent头被WAF拦截。解法在headers中添加User-Agent: MicroPython/1.22。问题响应体乱码中文显示为原因服务器返回UTF-8编码但response.text未指定解码方式。解法改用response.content.decode(utf-8)或在headers中声明Accept-Charset: utf-8。5.3 MicroPython运行时陷阱陷阱1MemoryError在urequests.get()后出现真相不是内存不足而是gc.collect()未及时触发。MicroPython GC不自动运行。解法在HTTP请求前后手动调用gc.collect()import gc gc.collect() # 请求前清理 response urequests.get(...) gc.collect() # 请求后清理陷阱2OSError: [Errno 1] EPERM在ussl.wrap_socket()时发生真相固件未启用TLS或证书文件路径错误。解法确认固件版本用os.listdir()检查证书文件是否存在证书必须为DER格式。陷阱3Pico W连接WiFi后HTTP请求变慢真相WiFi驱动在STA模式下降低CPU频率以省电。解法在连接WiFi后强制恢复频率import machine machine.freq(133000000) # 恢复133MHz主频最后分享一个血泪教训我在调试一个温湿度上报项目时发现Pico W每天凌晨3点必掉线。排查三天后发现是路由器开启了“夜间节能模式”自动关闭了2.4GHz频段。解决方案不是改代码而是把路由器固件升级到最新版——有时候最硬的bug不在代码里而在你摸不到的物理世界。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →