aiohttps异步HTTPS库:uPyPI+MicroPython一键安装 [复制链接]
发表于 昨天 17:44 | 显示全部楼层 |阅读模式
弁言

做 MicroPython 嵌入式开辟的朋侪,谁没被​内存溢出​、​HTTP/HTTPS 哀求壅闭​、大文件传输崩掉这些破事折磨过?尤其是在 ESP32S3、树莓派 Pico W 这种资源有限的装备上,想对接云端 API、传个音频 / 图片,大概搞个 SSE 流式通讯,稍不留意就 OOM(内存溢出),调试到心态爆炸。
我踩了无数坑才磨出 aiohttps 这个库 —— 它完全基于 MicroPython 原生的 asyncio、socket 和 ssl 模块打造,​零外部依靠​,专门为嵌入式装备的内存瓶颈量身定做。焦点筹划就两个关键点:

  • 一是​1KB 分块流式读写​,不管是下载大文件还是上传数据,内存峰值永世卡在 1KB,彻底告别 OOM;
  • 二是​异步非壅闭​,哀求时每 5ms 就让出 CPU,绝不霸占资源,让装备能同时处置处罚多使命。
更知心的是,它直接办理了 MicroPython ure 正则剖析的递归溢出题目,用纯字符串操纵搞定 URL 剖析;还封装了 GET/POST/PUT/DELETE 全量 HTTP 方法,流式下载、SSE 逐行读取、大 Body 分块发送这些高频场景都给你做好了。不管你是给 ESP32S3 做智能终端应用,还是在 Pico W 上对接 LLM、IoT 平台,aiohttps 都能让你少走 90% 的弯路,把精神放在业务逻辑上,而不是和底层网络死磕。
原理

说白了,aiohttps 就是一套给嵌入式装备量身定做的、低内存、非壅闭的 HTTP/HTTPS 协议实现
MicroPython 的 asyncio 变乱循环最怕壅闭:平凡的 socket.recv() 是同步的,一碰到没数据的情况,就会不停卡着等,把 CPU 占死,其他协程(好比你的 uPyOS 里的 UI 革新、传感器读取)全都会停摆。我是这么改的:

  • 先把 socket 设为​非壅闭模式​(sock.setblocking(False)):如许 recv() 没数据的时间不会卡死,只会返回空值大概抛非常。
  • 然后套一层循环轮询:每次读不到数据,就 await asyncio.sleep_ms(5) 让出 CPU,给其他使命留时间片。
  • 这个节奏和官方的 async_websocket_client 完全对齐,哪怕你的 uPyOS 里跑着 10 个联网应用,也不会被单个哀求拖垮。
嵌入式装备的内存太金贵了:Pico W 只有 100 多 KB 可用 RAM,ESP32S3 固然强,但也架不住一次性读个 1MB 的文件进内存,直接就炸了。以是焦点筹划就是 **“边用边扔” 的 1KB 分块战略 **:

  • 下载场景​:每次只从 socket 读 1KB 数据,立即写到文件里,写完就开释这块内存。不管文件是 10KB 还是 10MB,内存里永世只有 1KB 的缓存,绝不大概由于文件太大 OOM。
  • 上传场景​:假如是传文件,先读文件巨细填到 Content-Length 里,然后每次读 1KB 文件数据,分块发送,每发一块就 sleep_ms(5) 让出 CPU,不会把 socket 缓冲区堵死;传大 JSON/base64 图片的时间,也是按 1KB 分块写,制止大 payload 把非壅闭缓冲区撑爆。
MicroPython 自带的 ure 正则引擎是递归实现的,碰到带一堆鉴权参数的长 URL(凌驾 200 字符),直打仗发 maximum recursion depth exceeded 错误,连哀求都发不出去。以是我干脆​弃用了全部正则​,用纯字符串方法剖析 URL:

  • 先用 str.startswith 判断是 HTTP 还是 HTTPS 协议;
  • 用 str.find("//") 跳过协议头,再用 str.find("/") 分离 host 和 path;
  • 末了用 str.find(":") 分离 host 和 port,全程零递归、零正则,再长的 URL 都不会崩。
而且,这里我也做了一些优化:

  • 自动根据 URL 的 scheme 选协议:HTTP 直接明文毗连,HTTPS 自动走 TLS 加密,不消用户手动切换。
  • TLS 握手用原生 ssl.wrap_socket,默认用 cert_reqs=ssl.CERT_NONE(不验证服务端证书),制止加载证书占内存 —— 究竟嵌入式装备存根证书太贫苦,用户要生产情况验证的话,本身改参数就行,默认给个低内存方案。
  • 也办理了嵌入式 TLS 握手慢的题目:用 lwIP 软件 TLS 第一次握手要 2-4 秒,这个是硬件限定,我在文档里也写清晰了,制止用户误以为是库的 bug。
整个库只依靠 MicroPython 内置的 socket、ssl、asyncio 模块,没有任何第三方包。
测试

这里,各人在uPyPI(https://upypi.net/)中搜刮aiohttps:

点击复制安装下令:

粘贴到终端运行下载即可,然后将下面的代码烧录到测试的单片机上:
  1. # Python env   : MicroPython v1.23.0
  2. # -*- coding: utf-8 -*-
  3. # @Time    : 2026/04/15
  4. # @Author  : leeqingsui
  5. # @File    : main.py
  6. # @Description : aiohttps async HTTPS client test for MicroPython on Raspberry Pi Pico 2W
  7. # ======================================== 导入相关模块 =========================================
  8. import network
  9. import asyncio
  10. import time
  11. import json
  12. import ntptime
  13. import aiohttps
  14. # ======================================== 全局变量 ============================================
  15. WIFI_SSID     = "Y/OURSPACE"
  16. WIFI_PASSWORD = "qc123456789"
  17. # ======================================== 功能函数 ============================================
  18. def connect_wifi():
  19.     """
  20.     连接 WiFi 并返回网络对象。
  21.     Returns:
  22.         network.WLAN: 已连接的 WLAN 对象;连接失败时返回 None。
  23.     ==========================================
  24.     Connect to WiFi and return the network object.
  25.     Returns:
  26.         network.WLAN: Connected WLAN object; None if connection fails.
  27.     """
  28.     # 初始化 WiFi 为 STA 模式
  29.     wlan = network.WLAN(network.STA_IF)
  30.     wlan.active(True)
  31.     # 避免重复连接
  32.     if not wlan.isconnected():
  33.         print("Connecting to WiFi: {}".format(WIFI_SSID))
  34.         wlan.connect(WIFI_SSID, WIFI_PASSWORD)
  35.         timeout = 15
  36.         while not wlan.isconnected() and timeout > 0:
  37.             time.sleep(1)
  38.             timeout -= 1
  39.             print("Connecting... {}s remaining".format(timeout))
  40.         if wlan.isconnected():
  41.             print("WiFi connected, IP: {}".format(wlan.ifconfig()[0]))
  42.         else:
  43.             print("WiFi connection failed")
  44.             return None
  45.     else:
  46.         print("WiFi already connected")
  47.     return wlan
  48. def sync_ntp():
  49.     """
  50.     通过 NTP 同步系统时间。
  51.     ==========================================
  52.     Sync system time via NTP.
  53.     """
  54.     for host in ("ntp.aliyun.com", "ntp.tencent.com", "pool.ntp.org"):
  55.         try:
  56.             ntptime.host = host
  57.             ntptime.settime()
  58.             t = time.gmtime()
  59.             print("NTP synced via {}: {}-{:02d}-{:02d} {:02d}:{:02d}:{:02d} UTC".format(
  60.                 host, t[0], t[1], t[2], t[3], t[4], t[5]))
  61.             return
  62.         except Exception as e:
  63.             print("NTP failed ({}): {}".format(host, e))
  64.     print("NTP sync unavailable.")
  65. async def test_aiohttps():
  66.     """
  67.     aiohttps 库全量测试,使用 httpbin.org 作为公开测试服务器。
  68.     Test 1: GET  -> json()         HTTPS 握手 + 全量读取 + JSON 解析
  69.     Test 2: POST -> json()         str 请求体上传 + 服务端回显
  70.     Test 3: GET  -> save()         流式下载写文件(4096 字节)
  71.     Test 4: GET  -> status 404     非 200 状态码不抛异常
  72.     Test 5: GET  -> text           resp.text 属性读取
  73.     Test 6: POST -> bytes body     bytes 类型请求体
  74.     Test 7: PUT  -> request()      非 GET/POST 方法
  75.     Test 8: GET  -> http://        HTTP 明文连接
  76.     Test 9: GET  -> iter_lines()   SSE 流式逐行读取
  77.     ==========================================
  78.     Full test for aiohttps using httpbin.org as the public test server.
  79.     """
  80.     # 1. 连接 WiFi
  81.     if not connect_wifi():
  82.         return
  83.     # 2. 同步 NTP
  84.     sync_ntp()
  85.     print("--- aiohttps Test Start ---")
  86.     # Test 1: HTTPS GET -> json()
  87.     print("[1/8] GET https://httpbin.org/get")
  88.     try:
  89.         resp = await aiohttps.get("https://httpbin.org/get")
  90.         print("  status:", resp.status)
  91.         data = await resp.json()
  92.         print("  origin IP:", data.get("origin", "?"))
  93.         print("  [1/8] PASS" if resp.status == 200 else "  [1/8] FAIL")
  94.     except Exception as e:
  95.         print("  [1/8] ERROR:", e)
  96.     # Test 2: HTTPS POST str body -> json()
  97.     print("[2/8] POST https://httpbin.org/post (str body)")
  98.     try:
  99.         body = json.dumps({"device": "pico2w", "lib": "aiohttps"})
  100.         resp = await aiohttps.post(
  101.             "https://httpbin.org/post",
  102.             headers={"Content-Type": "application/json"},
  103.             data=body,
  104.         )
  105.         data = await resp.json()
  106.         echoed = data.get("json", {})
  107.         ok = echoed.get("device") == "pico2w" and echoed.get("lib") == "aiohttps"
  108.         print("  [2/8] PASS" if ok else "  [2/8] FAIL (echo mismatch)")
  109.     except Exception as e:
  110.         print("  [2/8] ERROR:", e)
  111.     # Test 3: HTTPS GET -> save() streaming download
  112.     print("[3/8] GET https://httpbin.org/bytes/4096 -> save test.bin")
  113.     try:
  114.         resp = await aiohttps.get("https://httpbin.org/bytes/4096")
  115.         n = await resp.save("test.bin")
  116.         print("  saved:", n, "bytes")
  117.         print("  [3/8] PASS" if n == 4096 else "  [3/8] FAIL (expected 4096, got {})".format(n))
  118.     except Exception as e:
  119.         print("  [3/8] ERROR:", e)
  120.     # Test 4: non-200 status code (404)
  121.     print("[4/8] GET https://httpbin.org/status/404")
  122.     try:
  123.         resp = await aiohttps.get("https://httpbin.org/status/404")
  124.         resp.close()
  125.         print("  status:", resp.status)
  126.         print("  [4/8] PASS" if resp.status == 404 else "  [4/8] FAIL (expected 404)")
  127.     except Exception as e:
  128.         print("  [4/8] ERROR:", e)
  129.     # Test 5: resp.text property
  130.     print("[5/8] GET https://httpbin.org/encoding/utf8 -> text")
  131.     try:
  132.         resp = await aiohttps.get("https://httpbin.org/encoding/utf8")
  133.         t = await resp.text
  134.         print("  text length:", len(t))
  135.         print("  [5/8] PASS" if len(t) > 0 else "  [5/8] FAIL (empty text)")
  136.     except Exception as e:
  137.         print("  [5/8] ERROR:", e)
  138.     # Test 6: POST bytes body
  139.     print("[6/8] POST https://httpbin.org/post (bytes body)")
  140.     try:
  141.         body = b"\x00\x01\x02\x03\xff"
  142.         resp = await aiohttps.post(
  143.             "https://httpbin.org/post",
  144.             headers={"Content-Type": "application/octet-stream"},
  145.             data=body,
  146.         )
  147.         await resp.text  # 消费响应体(二进制回显不能 json())
  148.         ok = resp.status == 200
  149.         print("  [6/8] PASS" if ok else "  [6/8] FAIL")
  150.     except Exception as e:
  151.         print("  [6/8] ERROR:", e)
  152.     # Test 7: PUT method via request()
  153.     print("[7/8] PUT https://httpbin.org/put")
  154.     try:
  155.         resp = await aiohttps.request(
  156.             "PUT", "https://httpbin.org/put",
  157.             headers={"Content-Type": "application/json"},
  158.             data=json.dumps({"action": "put_test"}),
  159.         )
  160.         data = await resp.json()
  161.         ok = resp.status == 200 and data.get("json", {}).get("action") == "put_test"
  162.         print("  [7/8] PASS" if ok else "  [7/8] FAIL")
  163.     except Exception as e:
  164.         print("  [7/8] ERROR:", e)
  165.     # Test 8: HTTP (plain, non-HTTPS)
  166.     print("[8/9] GET http://httpbin.org/get (plain HTTP)")
  167.     try:
  168.         resp = await aiohttps.get("http://httpbin.org/get")
  169.         print("  status:", resp.status)
  170.         data = await resp.json()
  171.         print("  origin IP:", data.get("origin", "?"))
  172.         print("  [8/9] PASS" if resp.status == 200 else "  [8/9] FAIL")
  173.     except Exception as e:
  174.         print("  [8/9] ERROR:", e)
  175.     # Test 9: iter_lines() streaming SSE
  176.     print("[9/9] GET https://httpbin.org/stream/3 -> iter_lines()")
  177.     try:
  178.         resp = await aiohttps.get("https://httpbin.org/stream/3")
  179.         print("  status:", resp.status)
  180.         count = 0
  181.         async for line in resp.iter_lines():
  182.             line = line.strip()
  183.             if not line:
  184.                 # 跳过空行(HTTP 分隔行)
  185.                 continue
  186.             count += 1
  187.             print("  line {}: {} bytes".format(count, len(line)))
  188.         print("  [9/9] PASS" if count >= 3 else "  [9/9] FAIL (expected >=3 lines, got {})".format(count))
  189.     except Exception as e:
  190.         print("  [9/9] ERROR:", e)
  191.     print("--- aiohttps Test Done ---")
  192. # ======================================== 自定义类 ============================================
  193. # ======================================== 初始化配置 ===========================================
  194. time.sleep(3)
  195. print("FreakStudio: aiohttps async HTTPS client test")
  196. # ========================================  主程序  ===========================================
  197. if __name__ == "__main__":
  198.     asyncio.run(test_aiohttps())
复制代码
上面案例中,在树莓派 Pico 2W 硬件平台上完成 aiohttps 异步 HTTPS 客户端全功能验证测试,步伐内置 WiFi 自动毗连、多源 NTP 时间同步功能,并通过 9 项尺度化测试用例,全面验证了库的焦点本领:支持 HTTPS/HTTP 双协议、GET/POST/PUT 全 HTTP 方法,可实现 JSON / 二进制数据收发、大文件流式下载、404 等非 200 状态码兼容、SSE 流式逐行读取等高频场景,全程异步非壅闭运行、内存占用极低,无任何第三方依靠。



免责声明:如果侵犯了您的权益,请联系站长及时删除侵权内容,谢谢合作!qidao123.com:ToB企服之家,中国第一个企服评测及软件市场,开放入驻,技术点评得现金.

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表