树莓派 Pico 上使用 MicroPython urequests 实现 HTTP 客户端的实战指南
最近把一个树莓派 Pico 接入局域网做数据采集本来以为最麻烦的是传感器接线结果真正让我折腾了一晚上的是MicroPython 里那个看着人畜无害的urequests库。说它坑吧它确实好用几行代码就能把数据 POST 到服务端说它好用吧遇到超时、内存不足、HTTPS 握手失败的时候报错信息又让人一头雾水。这篇文章把我从零开始用urequests在 Pico 上实现 HTTP 客户端的完整过程、踩坑记录和优化思路一次讲透适合刚入手 Pico 想让它联网的玩家也适合已经在用 MicroPython 做小项目、但被内存和异常折磨过的朋友。先说清楚一件事Pico 上的 HTTP 客户端和电脑上写的 HTTP 客户端完全是两码事。电脑上你随便pip install requests内存按 GB 算连接失败大不了重试但在 Pico 上整个 MicroPython 运行时也就占用几百 KB RAMurequests是一套专门为嵌入式环境精简过的 HTTP 客户端实现它复用了底层usocket和ussl模块API 长得像requests但实现思路完全不同。理解了这个本质区别你后面遇到的所有问题都能找到根因。1. 为什么在 Pico 上做 HTTP 客户端不能直接照搬电脑上的写法1.1 HTTP 客户端的本质一次请求背后发生了什么无论你用的是电脑还是 PicoHTTP 客户端的本质都是相同的建立 TCP 连接、按照 HTTP 协议格式发送请求报文、读取服务端返回的响应报文、解析状态行和消息体。一个最简单的 GET 请求报文长这样GET /api/data HTTP/1.1 Host: 192.168.1.100:8080 Connection: close服务端返回的响应报文类似HTTP/1.1 200 OK Content-Type: application/json Content-Length: 45 {temperature: 25.6, humidity: 60.1}电脑上的requests库帮你封装了这一切你只需要调用requests.get(url)库内部会处理 DNS 解析、建立 socket 连接、拼接请求头、读取响应、解压内容、编码转换这些琐碎细节。urequests做的事情本质上也一样但为了塞进几十 KB 的内存里它砍掉了大量功能没有会话Session机制、没有自动重试、没有完善的连接池、对 chunked 编码的支持非常有限、对 HTTPS 的支持也受制于底层ussl模块的能力。1.2 资源约束才是真正的难点Pico 标准版有 264KB RAM 和 2MB 闪存Pico W 也是 264KB RAM但多了一个无线模块。看着数字不小但 MicroPython 解释器本身要占用一部分运行时对象、GC 堆、socket 缓冲区再加上你要处理的数据能用给你的空间其实很紧张。我在实际项目里测试过一个正常的 GET 请求流程MicroPython 加上网络栈的内存占用通常在 30~60KB 左右如果响应体稍微大一点比如超过 5KBurequests在解析响应时很容易触发MemoryError。这一点你必须在一开始就有心理预期在 Pico 上做 HTTP 客户端核心不是“怎么把请求发出去”而是“怎么在有限的资源里把数据安全地取回来”。1.3 urequests 与 requests 的差异对照为了让你直观感受差异我整理了一张对比表这也是我刚开始用时反复踩坑的地方能力项requestsCPythonurequestsMicroPython内存占用数 MB 到数十 MB单请求几 KB 到几十 KBSession / Cookie完善支持无连接池复用默认启用无每次请求新建连接超时控制timeout参数灵活支持但底层依赖 socket 超时HTTPS完整 CA 证书链验证依赖ussl常需要关闭验证响应自动解压支持 gzip 等不支持chunked 响应自动处理部分固件不支持表现不稳定json() 方法完整 JSON 解析有但大 JSON 容易内存不足这张表不是在说 urequests 不行而是告诉你它是一个“够用但脆弱”的工具。你在设计业务逻辑的时候必须把它当成一个随时可能因为资源不足而失败的组件来对待而不是像用电脑上的 requests 那样“发了就完事”。1.4 什么时候用 urequests什么时候该换方案不是所有 Pico 上的 HTTP 需求都适合用 urequests。我的经验是这样的简单的 GET/POST、小数据量、低频请求比如每分钟一次直接上 urequests省事。需要长时间保持连接的场景比如 WebSocket、MQTT over WebSocket不要用 urequests它没有合适的连接池机制。海量数据上传优先考虑分块压缩上传或者干脆换用 MQTT 这类更适合嵌入式环境的协议。对延迟和稳定性要求高的场景考虑用 C 写的固件扩展或者换 ESP32资源更充裕。这个判断非常关键能帮你避免在错误的架构上浪费时间。2. 环境准备固件选择、烧录与网络初始化的关键细节2.1 硬件与固件版本的选择要说urequests用得顺手先得把地基打好。Pico 的 HTTP 客户端有两种典型硬件方案一是用 Pico W自带无线模块直接连 WiFi二是用标准 Pico 加外置以太网模块常见的是 WIZnet W5500 或者 ENC28J60。我个人建议如果你没有特殊需求新手直接上 Pico W省掉一堆 SPI 配置和驱动调试的麻烦。我最早用的是标准 Pico 加 W5500 模块网络初始化就得写好几十行代码后来换到 Pico W整个初始化代码缩短到十行以内。固件方面官方 MicroPython 固件本身就带urequests、network、usocket这些模块你不需要额外安装任何库。但有两个细节必须注意固件发布日期尽量新。2023 年之后的固件对network模块和 WiFi 连接稳定性做了大量改进旧固件容易出现 WiFi 掉线后无法重连的问题。如果你需要在 HTTPS 请求里做真正的证书校验可能需要自行编译固件并内置 CA 证书这个工作量不小多数场景下直接关闭验证即可后面我会讲。2.2 烧录固件的步骤烧录固件的流程我简单梳理一遍网上教程很多这里只点关键按住 Pico 板子上的 BOOTSEL 按键用 USB 线连接电脑板子会以 U 盘模式挂载。去 MicroPython 官网下载对应板型的.uf2固件文件。把.uf2文件拖入 U 盘板子会自动重启并进入 MicroPython 模式。用 Thonny 或mpremote工具连接串口验证固件版本。一个很容易忽略的点如果你用的是 Pico W下载固件时一定要选带W的版本比如RPI_PICO_W普通 Pico 的固件虽然也能烧进 Pico W但无线功能会失效network.WLAN直接报错。这个问题我见过不止一次排查时最容易被忽略。2.3 WiFi 初始化连接状态的正确检查方式网络初始化看起来简单但连接状态的判断有一个很容易踩的坑。直接看代码import network import time wlan network.WLAN(network.STA_IF) wlan.active(True) wlan.connect(your_ssid, your_password) # 错误示范只等 2 秒就判断 time.sleep(2) if wlan.isconnected(): print(connected) else: print(failed)wlan.connect()是一个异步返回的方法它启动连接过程后就立刻返回了真正建立连接需要几百毫秒到几秒不等。你如果没有等够时间就判断大概率得到未连接的错误结果。正确的做法是轮询isconnected()加上一个总超时上限import network import time wlan network.WLAN(network.STA_IF) wlan.active(True) wlan.connect(your_ssid, your_password) max_wait 10 while max_wait 0: if wlan.isconnected(): break max_wait - 1 time.sleep(1) if wlan.isconnected(): print(连接成功IP 地址:, wlan.ifconfig()[0]) else: print(连接失败请检查 SSID 和密码)轮询而不是固定 sleep好处是快的时候能立刻往下走慢的时候能等到足够时间不会白白浪费等待。实际项目里我还建议在connect()前先wlan.active(False)再active(True)重置状态避免 WiFi 模块处于异常状态时连接超时。2.4 确认网络可达性再做 HTTP 请求在写 HTTP 客户端之前先确认网络层通了。一个最直接的办法就是 ping 服务器地址但 Pico 上做 ping 不太方便我一般用一个更简单的方式直接尝试用 socket 连接目标端口。import socket def net_check(host, port, timeout3): try: addr socket.getaddrinfo(host, port)[0][4] s socket.socket() s.settimeout(timeout) s.connect(addr) s.close() print(f{host}:{port} 可达) return True except Exception as e: print(f{host}:{port} 不可达:, e) return False net_check(192.168.1.100, 8080)这里用到了socket.getaddrinfo它同时完成 DNS 解析和地址族选择。[0][4]取的是第一个解析结果的二元组(ip, port)。这一步能帮你把问题范围缩小如果连这个都失败那就别折腾urequests了先排查网络配置、路由器、防火墙。3. urequests 核心 API 拆解一次 GET 请求背后的完整链路3.1 request() 基函数与快捷方法的本质urequests的核心是一个request(method, url, dataNone, jsonNone, headers{}, streamNone, timeout-1)函数get()、post()、put()、delete()这些方法全都只是对request()调用的封装。以post()为例它的实现本质上就是def post(url, dataNone, jsonNone, headers{}, timeout-1): return request(POST, url, datadata, jsonjson, headersheaders, timeouttimeout)了解这一点有什么用当你需要发送自定义方法比如 PATCH、OPTIONS时你可以直接调用request()而不用纠结有没有对应的快捷方法。3.2 url 解析与连接建立的内部流程基于近版 urequests 源码request()内部的处理逻辑简单梳理大致是当传入data或json参数时自动设置Content-Type和Content-Length请求头。解析 url提取协议、host、port、路径。调用socket.getaddrinfo(host, port)解析域名。创建 socket设置超时建立 TCP 连接。若协议是https在 socket 上包一层ussl.wrap_socket做 TLS 握手。拼接请求头和消息体通过send()发送出去。读取响应状态行和响应头构造Response对象。源码里有一个很容易被忽略的细节协议解析用的是url.split(://, 1)然后根据schema判断端口http 默认 80https 默认 443。如果你写 url 时带了自定义端口比如http://192.168.1.100:8080/api它会在解析 path 时同时提取端口这个逻辑在多数固件里是正常的但如果你用了一些非标准 scheme就很容易出问题。3.3 Response 对象的属性和方法request()返回的Response对象是你在 Pico 上处理 HTTP 响应的核心接口它包含以下几个关键成员status_codeHTTP 状态码int 类型比如 200、404、500。reason状态文本比如 OK、Not Found。headers响应头是一个特殊对象支持headers.get(Content-Type)这样的访问方式。text按 utf-8 解码后的文本内容property惰性计算。content原始字节流内容property惰性计算。.json()把content用 JSON 解析后返回字典。.close()释放底层 socket 连接。这里有一个很关键的点无论是访问text还是调用json()数据都会被一次性完整读入内存。如果响应体很大这个过程很容易触发MemoryError。所以我的建议是能拿到响应后尽快处理、尽快close()不要长时间持有 Response 对象。3.4 请求头与数据的序列化细节用urequests发送数据的灵活性其实很高关键是理解data和json两个参数的区别data参数传字符串或字节串请求体会按原始内容发送Content-Type默认是application/x-www-form-urlencoded。json参数传字典或列表内部会调用ujson.dumps()序列化Content-Type自动设置为application/json。import ujson import urequests # 方式一发送 JSON resp urequests.post( http://192.168.1.100:8080/api/temp, json{temp: 25.6, hum: 60}, ) # 方式二发送表单编码 resp urequests.post( http://192.168.1.100:8080/api/temp, datatemp25.6hum60, ) # 方式三自定义请求头 resp urequests.post( http://192.168.1.100:8080/api/temp, json{temp: 25.6}, headers{X-Device-ID: pico-001}, )注意一点Content-Length是urequests根据数据长度自动算好并写入的如果你在headers里手动覆盖了Content-Length可能与实际发送长度不一致导致服务端解析出错。我在调试时就犯过这个错服务端一直报请求体不完整排查了半天才发现是自己多此一举地设置了Content-Length。4. 完整实战Pico 采集温湿度数据并 POST 到服务端4.1 场景与硬件接线前面讲的都是理论这里给一个可以直接抄的完整例子。场景Pico W 通过 DHT11 温湿度传感器采集数据每 30 秒通过 HTTP POST 上传到局域网内的一台服务器服务端用 Python Flask 写一个接口接收数据。需要的硬件树莓派 Pico W × 1DHT11 温湿度传感器 × 1面包板和杜邦线若干USB 供电线 × 1DHT11 的接线方式VCC 接 3.3V 或 5V具体看模块建议 3.3VGND 接 GNDDATA 接 GPIO 引脚我用的是 GP15。4.2 服务端接口的最小实现先写服务端方便待会儿联调。新建一个server.pyfrom flask import Flask, request, jsonify app Flask(__name__) app.route(/api/temp, methods[POST]) def receive_temp(): data request.get_json() print(收到设备数据:, data) return jsonify({code: 0, msg: ok}) if __name__ __main__: app.run(host0.0.0.0, port8080)然后在电脑上跑python server.py确保电脑 IP 是192.168.1.100根据你自己的局域网环境调整。4.3 Pico 端完整代码Pico 端的代码分为三块WiFi 连接、DHT11 读取、HTTP 上传。下面是完整代码import network import urequests import time import dht from machine import Pin # WiFi 配置 SSID your_ssid PASSWORD your_password SERVER_URL http://192.168.1.100:8080/api/temp # DHT11 引脚 dht_pin Pin(15, Pin.IN) dht_sensor dht.DHT11(dht_pin) # WiFi 连接 wlan network.WLAN(network.STA_IF) wlan.active(False) # 先重置 time.sleep(1) wlan.active(True) wlan.connect(SSID, PASSWORD) max_wait 10 while max_wait 0: if wlan.isconnected(): break max_wait - 1 time.sleep(1) if not wlan.isconnected(): print(WiFi 连接失败程序退出) raise SystemExit print(网络连接成功IP:, wlan.ifconfig()[0]) # 采集并上传 def read_dht11(): dht_sensor.measure() temp dht_sensor.temperature() hum dht_sensor.humidity() return temp, hum def upload_data(temp, hum): payload {temperature: temp, humidity: hum, device: pico-001} headers {Content-Type: application/json} try: resp urequests.post(SERVER_URL, jsonpayload, headersheaders, timeout5) print(状态码:, resp.status_code) print(响应内容:, resp.text) resp.close() return True except Exception as e: print(上传失败:, e) return False while True: try: temp, hum read_dht11() print(f温度: {temp}°C, 湿度: {hum}%) if upload_data(temp, hum): print(上传成功) else: print(上传失败等待下次重试) except Exception as e: print(采集或上传异常:, e) time.sleep(30)这个代码里有两个细节值得说道说道第一个是wlan.active(False)重置 WiFi 模块。有些情况下比如之前脚本异常退出WiFi 模块会处于一个半死不活的状态直接active(True)connect()会出现一直无法连接的情况先断开再重开能解决大部分问题。第二个是timeout5的设置。如果服务器没开机urequests.post()默认会一直阻塞下去整个主循环就被卡死了。给请求设置超时是嵌入式 HTTP 客户端最基本也最重要的自我保护。4.4 第一次跑通的预期结果正常跑通后串口输出大致是这样Network connected, IP: 192.168.1.37 温度: 26°C, 湿度: 58% 状态码: 200 响应内容: {code: 0, msg: ok} 上传成功服务端控制台会打印收到设备数据: {temperature: 26, humidity: 58, device: pico-001}到这里一个最基础的 Pico HTTP 客户端就工作了。不过这只是万里长征第一步后面的坑才是这篇文章真正有价值的部分。5. 内存不足与异常处理我实测中最常遇到的四个坑5.1 坑一MemoryError——响应体一大利就崩我最早遇到的坑就是MemoryError。场景是请求一个返回 JSON 数组的接口返回数据大概 10KB 左右结果resp.text一访问直接抛异常MemoryError: memory allocation failed, allocating xxxx bytes这个问题的根源在于urequests解析响应的方式是把整个响应体读入一个 bytes 对象再转成字符串。10KB 的 JSON 在电脑上微不足道但在 Pico 上可能就把剩余的堆空间吃光了。当时我用的是逐个读取响应内容而不是用resp.text一次性读入import urequests resp urequests.get(http://192.168.1.100:8080/api/list, timeout3) if resp.status_code 200: # 方法一分块读取遇到大响应不容易崩 data bytearray() chunk_size 256 while True: chunk resp.raw.read(chunk_size) if not chunk: break data.extend(chunk) if len(data) 8192: break # 设置上限防止无限增长 content bytes(data) else: content b resp.close()这里用到了resp.raw它是底层 socket 的SocketIO包装支持read(n)按块读取这样就不会一次性读入所有数据。配合一个上限判断可以防止响应体异常巨大时把内存耗尽。经验总结在 Pico 上处理 HTTP 响应永远要假设响应体会很大永远要设置内存上限。5.2 坑二连接超时与服务器不可达第二个高频问题就是超时。一开始我天真地以为timeout5只是限制“连接建立”的时间其实在urequests的底层实现里timeout同时作用于每个 socket 读写操作。如果你看到这样的报错OSError: [Errno 110] ETIMEDOUT说明 socket 在指定时间内没有完成读或写操作。可能的原因包括服务器未启动、防火墙屏蔽了端口、服务器响应太慢、WiFi 信号弱导致丢包。解决思路分两层第一请求前先做网络可达性检查用前面提到的net_check()第二在except分支里做好分类处理至少把“网络不通”和“服务端异常”分开记录方便后续排查。5.3 坑三WiFi 掉线后 urequests 直接卡死这是最隐蔽的坑程序运行一段时间后WiFi 信号弱或者路由器重启Pico 的 WiFi 断线了。下次执行urequests.post()时本应立即报错但某些固件版本的 socket 在底层处于一个奇怪的状态会导致请求长时间没有响应看起来像是死锁了。我在现场调试时遇到过一次上传间隔是 30 秒跑了两小时左右某次urequests.post()之后程序就卡住了串口没有任何输出连 LED 轮询都不动了。复位后又能跑一阵然后又卡死。这个问题的根因是WiFi 掉线后socket 连接可能处于半开状态urequests内部的send()和recv()由于底层驱动的不完善没有在预期时间内返回错误。解决思路在每次请求前检查wlan.isconnected()断开则尝试重连。给 socket 设置严格的超时让读写出错时能快速返回。更稳妥的方案是在异常处理中主动wlan.disconnect()再重新连接。改造后的上传函数def upload_data_safe(temp, hum): # 检查 WiFi 状态断了就重连 if not wlan.isconnected(): print(WiFi 已断开尝试重连...) wlan.disconnect() wlan.connect(SSID, PASSWORD) max_wait 10 while max_wait 0: if wlan.isconnected(): break max_wait - 1 time.sleep(1) if not wlan.isconnected(): return False payload {temperature: temp, humidity: hum} try: resp urequests.post(SERVER_URL, jsonpayload, timeout5) result resp.status_code resp.close() return result 200 except Exception as e: print(上传异常:, e) return False5.4 坑四HTTPS 请求的证书验证问题最后说一下 HTTPS。Pico 上访问 HTTPS 接口时urequests底层会调用ussl.wrap_socket()建立一个加密连接。问题在于 MicroPython 默认固件不包含完整 CA 证书库当你访问一个用 Lets Encrypt 等机构签发证书的网站时ussl的证书验证要么无法执行要么直接报错OSError: [Errno 5] EIO # 常见于证书验证失败对于互联网络环境下的场景一个实用的備选方案是把证书验证关闭。具体做法是在request()之前打一个补丁import usocket import ussl # 备份原始 wrap_socket _original_wrap_socket ussl.wrap_socket def _insecure_wrap_socket(sock, *args, **kwargs): kwargs[server_hostname] kwargs.get(server_hostname, None) try: return _original_wrap_socket(sock, *args, **kwargs) except TypeError: # 部分固件不支持 server_hostname 参数 return _original_wrap_socket(sock, *args) ussl.wrap_socket _insecure_wrap_socket但要注意不同固件版本的ussl.wrap_socket签名有差异有的支持cert_reqsussl.CERT_NONE有的根本不让你传参数。比较靠谱的方式是直接用CERT_NONEimport ussl s socket.socket() s.connect((api.example.com, 443)) s ussl.wrap_socket(s, server_hostnameapi.example.com, cert_reqsussl.CERT_NONE)明说一句关闭证书验证意味着中间人攻击防护失效切勿在保密性要求高的正式项目里使用。如果确实需要校验证书你需要把服务端的证书以.pem格式放入 Pico 文件系统并在调用wrap_socket时传入cert和server_hostname参数这部分稍复杂但网上有一些编译自定义固件的教程可以参考。6. 从能跑到用好优化超时策略、连接复用与代码结构6.1 超时策略的精细控制前面提到timeout5表示 socket 操作的超时但实际项目中我更推荐分级超时的思路连接超时connect3~5 秒。目标不可达时应快速失败。读取超时read5~10 秒。服务端处理请求需要时间但不应无限等。urequests的timeout参数直接传递给 socket 的settimeout()对 connect 和 read 都生效没法分别设置。如果确实需要细分你得手动创建 socket 并自己封装请求或者直接用usocket写完整的 HTTP 客户端代码量会大很多但对超时和内存的控制会精准很多。一个折中方案是用urequests的timeout作为全局兜底同时用gc.collect()在每次请求前主动回收内存import gc import urequests def http_get_json(url, timeout5): gc.collect() print(剩余堆内存:, gc.mem_free()) resp urequests.get(url, timeouttimeout) try: if resp.status_code 200: return resp.json() return None finally: resp.close() gc.collect()gc.collect()不会让内存变多但它能让碎片化的内存被整理合并减少大对象分配失败的几率。这在响应体比较大时帮助非常明显。6.2 连接复用的概念与局限Pico 上每次urequests请求都是独立的 TCP 连接请求结束后连接被销毁。虽然 HTTP/1.1 支持 keep-alive 长连接但urequests并没有提供便捷的复用接口。如果你需要在短时间内发起多次请求比如一次上传 100 条数据每发一条就新建一次 TCP 连接效率很低而且更容易触发内存碎片。实测下来一个比较实用的优化是把批量操作合并到一个请求里。比如攒 10 条数据一次性 POST 一个 JSON 数组。这样既减少连接次数也减少响应次数内存占用反而更可控。6.3 按用途封装客户端函数写到这里强烈建议你把 HTTP 客户端封装成统一的模块而不是在主逻辑里到处urequests.get()裸调。我自己的做法是单独建一个http_client.py对外暴露几个函数# http_client.py import gc import urequests import network class HTTPError(Exception): pass class PicoHTTPClient: def __init__(self, wlan, base_url, timeout5): self.wlan wlan self.base_url base_url.rstrip(/) self.timeout timeout def _check_network(self): if not self.wlan.isconnected(): raise HTTPError(WiFi 已断开) def get(self, path, paramsNone): self._check_network() gc.collect() url self.base_url / path.lstrip(/) if params: query .join(f{k}{v} for k, v in params.items()) url url ? query try: resp urequests.get(url, timeoutself.timeout) if resp.status_code ! 200: raise HTTPError(fHTTP {resp.status_code}) data resp.json() return data except HTTPError: raise except Exception as e: raise HTTPError(f请求失败: {e}) from e finally: try: resp.close() except Exception: pass def post(self, path, dataNone, jsonNone): self._check_network() gc.collect() url self.base_url / path.lstrip(/) try: resp urequests.post(url, datadata, jsonjson, timeoutself.timeout) if resp.status_code ! 200: raise HTTPError(fHTTP {resp.status_code}) return resp.json() except HTTPError: raise except Exception as e: raise HTTPError(f请求失败: {e}) from e finally: try: resp.close() except Exception: pass封装的核心目的不是写一堆花哨的面向对象代码而是把网络检查、内存回收、异常转换、连接关闭这些琐碎的安全逻辑收敛到一个地方。主程序里调起来一目了然client PicoHTTPClient(wlan, http://192.168.1.100:8080) try: result client.post(/api/temp, json{temp: 26.0}) print(result) except HTTPError as e: print(业务请求失败:, e)6.4 数据完整性校验与重试策略最后一点是重试。urequests没有内置重试机制但它在嵌入式环境中的失败率远高于电脑上的 requests所以我建议在业务层加一个简单的重试逻辑def post_with_retry(client, path, payload, retries3, delay2): for attempt in range(retries): try: result client.post(path, jsonpayload) return result except HTTPError as e: print(f第 {attempt1} 次重试失败: {e}) if attempt retries - 1: time.sleep(delay) raise HTTPError(f重试 {retries} 次仍然失败)重试时要避免“无脑重试”如果服务端返回的是 4xx比如请求格式不对、鉴权失败重试多少次都没意义只有 5xx、网络超时这类临时性错误才值得重试。更严谨的做法是在post_with_retry里判断异常类型只有网络类异常才触发重试。写到这里这篇关于 MicroPythonurequests库和 Pico HTTP 客户端的实践梳理就差不多了。我最后想说的是很多人玩 Pico 都会止步于能跑通一个 demo但真正让项目稳定运行的往往是那些看不见的细节WiFi 掉线重连、内存碎片整理、超时兜底、异常分类处理。每次遇到MemoryError或者莫名卡死的时候别急着骂库不好用先把这些底层逻辑想清楚你会发现自己对嵌入式网络编程的理解又深了一层。如果你照着我这套思路把代码写出来哪怕只是改一改超时参数都会有完全不同的体验。