- 嵌入式
- 语言运行时
- 编程语言
- 解释器
- 编译器
- 物联网
- 系统编程
【免费下载链接】micropython
MicroPython - a lean and efficient Python implementation for microcontrollers and constrained systems
导读
TCP Socket 是互联网应用最基础的通信构件——它能够在两个网络设备之间建立一条可靠的字节流通道,HTTP、telnet、SSH、SMTP 等上层协议全部构建在它之上。本教程基于 MicroPython 官方 ESP8266 教程的《Network - TCP sockets》章节,以乐鑫 ESP8266 开发板为核心,手把手带你完成三个实战项目:通过 telnet 服务流式拉取 Star Wars Ascii 动画、用 HTTP GET 请求下载网页、以及在本机上搭建一个展示全部 GPIO 引脚状态的 HTTP 服务器。读完本文,你将掌握socket模块的核心 API 用法、getaddrinfo地址解析的正确姿势、客户端与服务器两种编程模型,并能直接在 REPL 或boot.py中落地运行。
前置要求:本文所有示例都假设你的 ESP8266 已经通过
network.WLAN成功接入 WiFi(可参考 ESP8266 网络基础教程)。Socket 模块的完整 API 参考见 socket 模块文档。
TCP Socket 是什么:互联网的"可靠字节流"
在深入代码之前,先建立对 TCP Socket 的直观认识:
- **Socket(套接字)**是网络设备上的一个端点。当两个设备上的 socket 连接在一起,通信就可以进行。可以把 socket 想象成两台设备之间的一条虚拟管道。
- 可靠字节流:与 UDP 的无连接、不保证送达不同,TCP 保证数据按序、不丢失地传输,因此适合承载 HTTP、telnet、SSH 这类对数据完整性要求高的协议。
- 端口(Port):每个上层协议都绑定一个固定的整数端口,例如 HTTP 是 80、telnet 是 23、SSH 是 22。给定"IP 地址 + 端口号",你就能定位并连接到一个远程设备上的特定服务。
在 MicroPython 中,socket模块提供对 BSD socket 接口的访问。它与 CPython 最大的差异在于:MicroPython 的 socket 对象直接实现了 stream(文件类)接口,而 CPython 需要用makefile()转换(在 MicroPython 中makefile()仍然存在,但只是空操作,socket 模块文档 明确指出调用它仅为保持与 CPython 的兼容性)。
地址解析:getaddrinfo 的正确用法
所有网络程序的第一步都是把"主机名 + 端口"解析成可连接的地址。MicroPython 的规范做法是使用socket.getaddrinfo(host, port):
>>> import socket >>> addr_info = socket.getaddrinfo("towel.blinkenlights.nl", 23)这个函数返回的是一个5 元组列表,每个元组结构为(family, type, proto, canonname, sockaddr)。以addr_info[0][-1]取到最后一个元素,就是(ip, port)形式的可直接连接的地址:
>>> addr = addr_info[0][-1]如果你在 REPL 里分别输入addr_info和addr,会直观看到两者保存的信息差异——前者是包含地址族、socket 类型、协议等完整信息的列表,后者只是我们实际需要的 IP 与端口对。
从源码层面看,extmod/modsocket.c 中mod_socket_getaddrinfo的实现揭示了它的内部行为:
- 先尝试把主机名当 IP 地址解析:如果传入的字符串本身就是点分十进制形式(如
"10.10.10.10"),直接通过netutils_parse_ipv4_addr解析,跳过 DNS 查询; - 否则遍历已注册的网卡(NIC):在所有
mod_network_nic_list中的网卡里寻找实现了gethostbyname协议的网卡执行 DNS 查询,找不到可用网卡时抛出OSError("no available NIC"); - 组装 5 元组返回:固定返回
AF_INET、SOCK_STREAM类型的地址元组。
getaddrinfo还支持最多 6 个参数(host, port, af, type, proto, flags),用于过滤地址族与 socket 类型,例如socket.getaddrinfo(host, 80, 0, socket.SOCK_STREAM)可保证返回的地址适合流式连接。官方文档建议:在编写可移植程序时始终使用getaddrinfo,即使地址是数字形式也应如此——这是最高效且最可移植的地址处理方式。
仓库中的 tests/net_inet/getaddrinfo.py 覆盖了该函数的各类边界情况:不存在的域名、非法主机名、纯 IP 地址、0.0.0.0以及有效域名,全部用try/except OSError包裹验证其异常行为,可作为你测试网络解析的参考模板。
实战一:连接 telnet 服务,流式下载 Star Wars Ascii 动画
blinkenlights.nl 网站的 Star Wars Asciimation 服务使用 telnet 协议(端口 23),向所有连接者流式推送 ASCII 字符动画。它不需要任何认证,连接即可开始接收数据,非常适合作为 TCP 客户端入门的第一个练习。
第一步:导入 socket 模块
>>> import socket第二步:解析服务器地址
>>> addr_info = socket.getaddrinfo("towel.blinkenlights.nl", 23) >>> addr = addr_info[0][-1]第三步:创建 socket 并连接
>>> s = socket.socket() >>> s.connect(addr)第四步:循环接收并打印数据
>>> while True: ... data = s.recv(500) ... print(str(data, 'utf8'), end='') ...当这个循环开始执行,动画就会一帧帧地在终端上"播放"出来,按Ctrl-C即可中断。
这段代码演示了 TCP 客户端的完整四步流程:解析地址 → 创建 socket → connect → 循环 recv。几个值得注意的细节:
s.recv(500)指定每次最多读取 500 字节——这是缓冲区上限,实际返回的字节数可能更少,因此循环不断调用直到服务端关闭连接;recv返回的是bytes对象,需要用str(data, 'utf8')解码为文本;end=''避免 print 自动换行破坏动画帧;- 这段代码同样可以在 PC 上的标准 Python 中运行(MicroPython 的 socket API 与 CPython 高度兼容),非常适合在电脑上先调试通网络服务再移植到开发板。
从源码看,extmod/modsocket.c 中socket_recv会先检查 socket 是否已绑定网卡(未连接时抛ENOTCONN),随后调用网卡协议层的recv实现,并将返回的字节填充到vstr中构造 bytes 对象返回;若对端关闭连接返回 0,则返回空 bytes。
实战二:HTTP GET 请求——向 Web 服务器下载网页
HTTP 协议运行在 80 端口,与 telnet 的"连上就收"不同,客户端必须先发送一个GET请求,指明要获取的页面路径,服务器才会返回内容。下面的函数封装了完整的 HTTP 下载流程:
def http_get(url): import socket _, _, host, path = url.split('/', 3) addr = socket.getaddrinfo(host, 80)[0][-1] s = socket.socket() s.connect(addr) s.send(bytes('GET /%s HTTP/1.0\r\nHost: %s\r\n\r\n' % (path, host), 'utf8')) while True: data = s.recv(100) if data: print(str(data, 'utf8'), end='') else: break s.close()调用方式:
>>> http_get('http://micropython.org/ks/test.html')执行后网页 HTML 会被逐块打印到控制台。逐行拆解这个函数:
| 步骤 | 代码 | 作用 |
|---|---|---|
| 解析 URL | _, _, host, path = url.split('/', 3) | 把http://micropython.org/ks/test.html拆成 host 与路径。split('/', 3)按斜杠最多切 3 次,跳过http:与空串,得到host = 'micropython.org'、path = 'ks/test.html' |
| 解析地址 | addr = socket.getaddrinfo(host, 80)[0][-1] | 解析主机名到(ip, 80)地址对 |
| 建立连接 | s.connect(addr) | TCP 三次握手 |
| 发送请求 | s.send(bytes('GET /%s HTTP/1.0\r\nHost: %s\r\n\r\n' ...)) | 构造 HTTP/1.0 GET 请求报文。\r\n是 HTTP 的行结束符,末尾空行表示请求头结束;Host头用于虚拟主机 |
| 接收响应 | while True: data = s.recv(100) | 分块读取响应;当recv返回空(服务端关闭连接)时退出循环 |
| 释放资源 | s.close() | 显式关闭 socket |
注意 HTTP 报文格式:请求行GET /path HTTP/1.0、请求头Host: hostname,以及请求头与正文之间必须有一个空行(\r\n\r\n),这是 HTTP 规范要求、极易遗漏的细节。bytes(..., 'utf8')将字符串编码为字节流发送。
http_get是一次性函数;如果你要复用连接发送/接收多次数据,应使用s.sendall(data)(保证全部字节发送完毕)配合s.read()/s.readline()等流式方法,这些方法在 MicroPython 中由 stream 接口直接提供(见 extmod/modsocket.c 中 socket 的方法表,read/readline/write直接映射到通用 stream 实现)。
实战三:HTTP 服务器——让 ESP8266 成为 Web 服务器
现在把角色反转:让 ESP8266 充当服务器,向浏览器提供页面。下面的代码创建一个简单的 HTTP 服务器,页面是一张展示所有 GPIO 引脚电平状态的表格:
import machine pins = [machine.Pin(i, machine.Pin.IN) for i in (0, 2, 4, 5, 12, 13, 14, 15)] html = """<!DOCTYPE html> <html> <head> <title>ESP8266 Pins</title> </head> <body> <h1>ESP8266 Pins</h1> <table border="1"> <tr><th>Pin</th><th>Value</th></tr> %s </table> </body> </html> """ import socket addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1] s = socket.socket() s.bind(addr) s.listen(1) print('listening on', addr) while True: cl, addr = s.accept() print('client connected from', addr) cl_file = cl.makefile('rwb', 0) while True: line = cl_file.readline() if not line or line == b'\r\n': break rows = ['<tr><td>%s</td><td>%d</td></tr>' % (str(p), p.value()) for p in pins] response = html % '\n'.join(rows) cl.send('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n') cl.send(response) cl.close()这个例子是标准的 TCP 服务器四步流程:bind → listen → accept → 循环处理。服务器核心要素分析:
- 绑定通配地址:
socket.getaddrinfo('0.0.0.0', 80)让服务器监听所有网络接口(无论板子从路由器获得什么 IP,客户端都能访问),bind(addr)将 socket 绑定到该地址与 80 端口。注意在 ESP8266 上绑定 80 端口前应确认没有其他服务占用该端口。 - 监听队列:
s.listen(1)允许 1 个未处理的连接请求排队,多余的连接会被拒绝。源码中socket_listen(extmod/modsocket.c)会先将负的 backlog 钳制为 0。 - accept 返回连接:
s.accept()阻塞等待客户端连接,返回(cl, addr)二元组——cl是专用于该连接的新 socket 对象,addr是客户端地址(源码见 extmod/modsocket.c,新 socket 继承父 socket 的地址族、类型与协议)。 - 读取请求头:
cl.makefile('rwb', 0)创建与 socket 关联的文件对象('rwb'表示可读可写二进制模式,0表示无缓冲;MicroPython 会忽略 buffering 参数并视为 0)。readline()逐行读取 HTTP 请求头,遇到空行b'\r\n'即请求头结束——服务器不必解析请求内容,只需读出并丢弃请求头即可返回响应。 - 构造并发送响应:先用
p.value()实时读取每个 GPIO 引脚电平,生成 HTML 表格行,再拼接完整页面。响应报文由状态行HTTP/1.0 200 OK、响应头Content-type: text/html、空行与正文组成。 - 关闭连接:处理完一个请求后
cl.close()关闭该连接;注意makefile()返回的文件对象被关闭时会同时关闭底层 socket(socket 模块文档 中的明确提示)。
浏览器访问http://<ESP8266的IP>/即可看到带引脚的实时状态表格。在 ESP8266 上运行 HTTP 服务器时应留意内存与功耗:请求频率高时可以在循环中加入machine.idle()或适当延时,避免长时间满负荷运行。
服务器示例的测试验证
仓库中 tests/multi_net/tcp_data.py 是一个双实例 TCP 服务器/客户端对测用例,可视为本示例的精简版本:服务器端bind → listen → accept → recv → send,客户端connect → send → recv,完整覆盖了上述 API 的调用链,是理解 MicroPython 网络栈行为的绝佳参考。
进阶:TCP 编程的可靠性要点
1. 用流式接口代替原生方法
MicroPython 的 socket 对象本身就是一个流对象,read()、readline()、write()直接可用。官方推荐用write()替代sendall()(阻塞模式下二者都保证"无短写",非阻塞模式下sendall()行为未定义而write()会返回实际写入字节数),用read()替代recv()可享受"无短读"语义。这在处理 HTTP 这类需要完整读取一行/一段数据的协议时尤其方便。
2. 超时与非阻塞
settimeout(value)设置阻塞操作超时(单位秒):非零值超时后抛OSError,0切换为非阻塞模式,None恢复阻塞模式。setblocking(flag)是其快捷方式:setblocking(True)等价settimeout(None),setblocking(False)等价settimeout(0)(源码见 extmod/modsocket.c,超时值在内部换算为毫秒存储)。
更可移植的等待方案是使用select.poll():它可以同时轮询多个流对象,poller.poll(1000)以毫秒为单位等待就绪事件,从而优雅地实现"非阻塞 + 超时"语义。ESP8266 上如果某个网络操作卡死,超时机制是保证程序健壮性的关键。
3. 异常处理
MicroPython 没有 CPython 的socket.error与socket.gaierror,一律抛OSError。getaddrinfo的解析错误用负数错误码表示,与 errno 的正数错误码分属不同命名空间。因此:
- 解析域名时用
except OSError捕获(tests/net_inet/getaddrinfo.py 展示了这一模式); - 通用的
except OSError:写法在 MicroPython 与 CPython 下都能工作,保持代码双端兼容。
4. 资源管理
socket 对象在垃圾回收时会自动关闭,但官方明确建议用完后立即显式close(),尤其在服务器场景(每个accept()产生一个新 socket,若不关闭会迅速耗尽内存)。MicroPython 的 socket 对象带有析构器(mp_obj_malloc_with_finaliser,见 extmod/modsocket.c),但依赖 GC 释放资源在内存紧张的 ESP8266 上并不明智。
5. 复用地址
服务器重启时可能遇到address in use错误,可在bind前执行s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)允许地址复用——tests/multi_net/tcp_data.py 的服务器端正是这么做的,这也是嵌入式 Web 服务器开发中的常见惯例。
总结
| 场景 | 核心 API 流程 | 关键点 |
|---|---|---|
| TCP 客户端(telnet 拉流) | getaddrinfo → socket() → connect → recv 循环 | 无需认证,连上即收;Ctrl-C中断 |
| HTTP 客户端(网页下载) | getaddrinfo → socket → connect → send(GET) → recv 循环 | HTTP 报文格式:请求行 + Host 头 + 空行 |
| HTTP 服务器(GPIO 看板) | getaddrinfo('0.0.0.0') → socket → bind → listen → accept → 收发 → close | 请求头以空行结束;每个连接独立 socket 需关闭 |
通过这三个循序渐进的项目,你已经掌握了 MicroPython 在 ESP8266 上使用 TCP socket 完成"下载数据"与"提供服务"两种角色的全部要点:地址解析用getaddrinfo、客户端记住"解析-连接-收发"三步、服务器记住"绑定-监听-接受-关闭"四步、始终显式关闭 socket。这套模式适用于 HTTP、MQTT、WebSocket、SSH 等几乎所有基于 TCP 的协议,是进一步开发 ESP8266 物联网应用的基石。建议接着阅读 ESP8266 教程索引 中的 文件系统 与 引脚控制 章节,将网络能力与存储、GPIO 结合,构建更完整的嵌入式应用。
- 嵌入式
- 语言运行时
- 编程语言
- 解释器
- 编译器
- 物联网
- 系统编程
【免费下载链接】micropython
MicroPython - a lean and efficient Python implementation for microcontrollers and constrained systems
相关推荐
OfficeCLI 扩展图表实战指南:用 cx:chart 家族一键生成 Waterfall、Treemap、Histogram 与 Pareto
OfficeCLI 扩展图表实战指南:用 cx:chart 家族一键生成 Waterfall、Treemap、Histogram 与 Pareto 导读 Exc
人工智能AI 应用AI 技能CLIMCP 服务30分钟上手Odin网络编程:从TCP客户端到HTTP服务器实战指南
30分钟上手Odin网络编程:从TCP客户端到HTTP服务器实战指南 你是否还在为网络编程的复杂概念而头疼?是否想快速构建可靠的网络应用却不知从何下手?本文将带
编程语言编译器语言运行时RIOT gcoap 示例实战指南:基于 CoAP 的 IoT 网络客户端与服务器
RIOT gcoap 示例实战指南:基于 CoAP 的 IoT 网络客户端与服务器 导读 本文围绕 RIOT 官方示例 examples/networking/
物联网嵌入式操作系统实时系统
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考