- 嵌入式
- 语言运行时
- 编程语言
- 解释器
- 编译器
- 物联网
- 系统编程
【免费下载链接】micropython
MicroPython - a lean and efficient Python implementation for microcontrollers and constrained systems
socket模块为 MicroPython 提供访问 BSD 套接字接口的能力,是嵌入式设备实现 TCP/UDP 网络通信、构建客户端与服务端程序的基础模块。本文以仓库文档 docs/library/socket.rst 为主体,结合 extmod/modsocket.c 等核心源码与tests/下的测试用例,系统讲解地址格式、全部函数与类方法、与 CPython 的差异以及错误处理约定,帮助读者写出既能在 MicroPython 上高效运行、又能保持 CPython 兼容的网络代码。
模块定位与 CPython 核心差异
MicroPython 的socket模块与 CPython 同名模块在接口上高度一致,但有一处根本性的设计差异:MicroPython 的 socket 对象直接实现了 stream(文件类)接口。也就是说,你可以直接在 socket 对象上调用read()、write()、readline()、readinto()等方法,而 CPython 中必须先通过makefile()把 socket 转换为文件对象才能使用这些方法。
在源码层面,这一设计体现在 extmod/modsocket.c:socket 类型注册了mp_stream_p_t协议(包含read、write、ioctl三个回调),并在方法表中把read/readinto/readline/write直接映射到通用流实现mp_stream_read_obj、mp_stream_readinto_obj、mp_stream_unbuffered_readline_obj、mp_stream_write_obj(见 extmod/modsocket.c)。
为了保证与 CPython 的兼容性,MicroPython 仍然提供makefile()方法,但它在底层是一个 no-op——直接返回 socket 对象自身(见 extmod/modsocket.c)。因此在编写需要同时运行于两端的代码时,照常调用makefile()不会出错,但也不必依赖它。
Socket 地址格式:getaddrinfo 优先
socket模块的"原生"地址格式是getaddrinfo()返回的不透明数据类型。MicroPython 官方强烈建议:无论目标是域名还是数字 IP 地址,都通过getaddrinfo()解析地址,这是最省内存、最高效也最可移植的方式:
import socket # 域名必须通过 getaddrinfo 解析 sockaddr = socket.getaddrinfo('www.micropython.org', 80)[0][-1] # 即使是数字地址也必须用 getaddrinfo 处理 sockaddr = socket.getaddrinfo('127.0.0.1', 80)[0][-1] # 现在可以直接使用该地址 sock.connect(sockaddr)getaddrinfo()返回的 5 元组结构为(family, type, proto, canonname, sockaddr),取最后一个元素[-1]即可得到可直接传给connect()/bind()的地址。
元组格式(CPython 兼容捷径)
出于 CPython 兼容考虑,socket模块也接受元组形式的地址,但有以下限制:
- IPv4 元组:
(ipv4_address, port),其中ipv4_address是点分十进制字符串(如"8.8.8.8"),port是 1~65535 的整数。元组中的地址不接受域名,必须先用socket.getaddrinfo()解析。 - IPv6 元组:
(ipv6_address, port, flowinfo, scopeid),其中ipv6_address是冒号分隔的十六进制字符串(如"2001:db8::1"),port为 1~65535 的整数,flowinfo必须为 0,scopeid是链路本地地址的接口作用域标识。同样不接受域名。IPv6 是否可用取决于具体的 MicroPython 移植版本(port)。
需要注意,MicroPython 移植版(port)众多:socket模块可能内建,也可能需要从micropython-lib单独安装(例如 Unix 移植版就是后者);且部分移植版在元组格式中只接受数字地址,解析域名仍必须使用getaddrinfo。因此官方给出两条经验法则:
- 编写可移植应用时,一律使用
getaddrinfo; - 元组格式只作为快速原型和交互式 REPL 实验的捷径,且需确认当前移植版支持。
从源码看,connect()/bind()/sendto()在底层正是通过netutils_parse_inet_addr()解析传入的地址并提取 IP 与端口(见 extmod/modsocket.c 与 extmod/modsocket.c),随后再调用 NIC 协议层完成实际操作。
模块级函数
getaddrinfo(host, port, af=0, type=0, proto=0, flags=0, /)
将 host/port 参数翻译为一组 5 元组,每个元组包含创建连接到该服务所需的所有参数。可选参数af、type、proto(含义与socket()构造函数相同)用于过滤返回的地址类型;若某参数未指定或为 0,则可能返回所有地址组合,需要调用方自行过滤。
典型用法对比:
import socket s = socket.socket() # 不推荐:假定未指定 type 时会返回 SOCK_STREAM 地址,这未必成立 s.connect(socket.getaddrinfo('www.micropython.org', 80)[0][-1]) # 推荐:显式过滤,保证拿到可进行流式连接的地址 s.connect(socket.getaddrinfo('www.micropython.org', 80, 0, socket.SOCK_STREAM)[0][-1])与 CPython 的差异(错误处理):CPython 在解析失败时抛出socket.gaierror(OSError子类);MicroPython 没有socket.gaierror,直接抛出OSError。且getaddrinfo()的错误号自成一套命名空间,与errno模块的错误号不一定对应。MicroPython 用负数表示getaddrinfo()错误、正数表示标准系统错误来加以区分(可通过异常的e.args[0]取得错误号)。负数值的约定属于临时性细节,未来可能变化。
源码实现细节(见 extmod/modsocket.c):实现会先尝试用netutils_parse_ipv4_addr()判断 host 是否已是 IP 形式;若不是,则遍历已注册的 NIC 列表,调用支持gethostbyname的 NIC 完成 DNS 解析,若没有任何可用 NIC 则抛出OSError("no available NIC")。此外,传入的过滤参数如果超出实现支持的范围(例如要求 IPv6 或非 STREAM 类型),会触发一个RuntimeWarning:"unsupported getaddrinfo constraints",而不会静默失败。仓库中的 tests/net_inet/getaddrinfo.py 覆盖了不存在的域名、非法主机名、纯 IP、0.0.0.0、真实域名等多种解析场景。
inet_ntop(af, bin_addr)
将指定地址族af的二进制网络地址bin_addr转换为文本表示:
>>> socket.inet_ntop(socket.AF_INET, b"\x7f\0\0\1") '127.0.0.1'inet_pton(af, txt_addr)
将指定地址族af的文本网络地址txt_addr转换为二进制表示:
>>> socket.inet_pton(socket.AF_INET, "1.2.3.4") b'\x01\x02\x03\x04'这两个函数并非所有移植版都提供。例如 Unix 移植版在 ports/unix/modsocket.c 中直接基于系统的inet_pton()/inet_ntop()实现并注册到模块命名空间;而通用网络实现 extmod/modsocket.c 的模块全局表中并没有这两个函数。使用前请确认目标移植版的支持情况。
常量清单
socket模块提供以下常量:
| 常量 | 说明 |
|---|---|
AF_INET/AF_INET6 | 地址族类型;可用性取决于具体移植版 |
SOCK_STREAM/SOCK_DGRAM | 套接字类型(流 / 数据报) |
IPPROTO_UDP/IPPROTO_TCP | IP 协议号;可用性取决于具体移植版 |
SOL_* | 套接字选项层级,作为setsockopt()的第一个参数;具体清单取决于移植版 |
SO_* | 套接字选项,作为setsockopt()的第二个参数;具体清单取决于移植版 |
IPPROTO_SEC(仅 WiPy) | 特殊协议值,用于创建 SSL 兼容套接字 |
关于IPPROTO_UDP/IPPROTO_TCP有一个重要提示:调用socket.socket()时通常不需要也不建议指定协议号(部分移植版甚至可能没有IPPROTO_*常量),因为套接字类型会自动选择协议——SOCK_STREAM自动选IPPROTO_TCP,SOCK_DGRAM自动选IPPROTO_UDP。这两个常量的实际用途是作为setsockopt()的参数。
通用实现 extmod/modsocket.c 中实际注册的模块级常量包括:AF_INET、AF_INET6、SOCK_STREAM、SOCK_DGRAM、SOCK_RAW,以及SOL_SOCKET、SO_REUSEADDR、SO_BROADCAST、SO_KEEPALIVE、SO_SNDTIMEO、SO_RCVTIMEO;IPPROTO_*系列在通用实现中被注释掉,仅在部分移植版中可见。这也解释了文档中"清单取决于移植版"的表述。
socket 类
构造函数:socket(af=AF_INET, type=SOCK_STREAM, proto=IPPROTO_TCP, /)
创建指定地址族、类型与协议号的套接字。proto多数情况下无需指定(见上文常量说明),由type自动选择协议:
# 创建 STREAM TCP 套接字 socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 创建 DGRAM UDP 套接字 socket.socket(socket.AF_INET, socket.SOCK_DGRAM)源码中的默认值是domain=AF_INET, type=SOCK_STREAM, proto=0,套接字初始处于"未绑定任何 NIC"状态(见 extmod/modsocket.c);直到执行bind()/connect()/sendto()等操作时,才会依据目标 IP 自动选择合适的网络接口(NIC)并真正打开底层套接字(socket_select_nic(),见 extmod/modsocket.c)。这也是嵌入式环境"先建对象、后绑网卡"的懒加载设计。
连接与服务端方法
- close():关闭套接字并释放全部资源,之后对该对象的一切操作都会失败;若协议支持,对端会收到 EOF 指示。套接字在垃圾回收时会被自动关闭,但官方建议用完立即显式
close()。底层由通用流关闭回调实现(mp_stream_close_obj,见 extmod/modsocket.c)。 - bind(address):将套接字绑定到
address,套接字不得重复绑定。 - listen([backlog]):使服务端套接字进入监听状态。
backlog若指定则必须不小于 0(更小会被强制设为 0),表示系统在拒绝新连接前允许排队的未接受连接数;不指定时使用合理默认值。源码中默认值来自MICROPY_PY_SOCKET_LISTEN_BACKLOG_DEFAULT,且负值会被截为 0(见 extmod/modsocket.c)。 - accept():接受一个连接。套接字必须已绑定并处于监听状态。返回
(conn, address)二元组:conn是可用于收发数据的新套接字对象,address是对端绑定的地址。源码实现中,新套接字会继承父套接字的地址族、类型与协议,并复用父套接字的 NIC(见 extmod/modsocket.c)。 - connect(address):连接到远端
address。源码会先解析地址、自动选择 NIC,然后调用 NIC 的connect并把状态置为MOD_NETWORK_SS_CONNECTED(见 extmod/modsocket.c)。
一个最小可用的 TCP 服务端骨架:
import socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(socket.getaddrinfo('0.0.0.0', 8080)[0][-1]) s.listen(5) while True: conn, addr = s.accept() # 处理连接 ... conn.close()数据收发方法
send(bytes):向已连接的远端发送数据,返回实际发送的字节数——可能小于数据长度("短写" short write)。
sendall(bytes):逐块连续发送,保证把数据全部发出,行为与
send()不同。在非阻塞套接字上其行为未定义,因此 MicroPython 官方建议改用write()方法:它在阻塞套接字上同样保证"无短写",在非阻塞套接字上则返回实际发送的字节数。源码实现印证了这一点(见 extmod/modsocket.c):阻塞模式下
sendall进入while (bufinfo.len != 0)循环反复调用 NIC 的send并推进缓冲区指针,直至全部发完;非阻塞模式(timeout == 0)下只发一次,若未能一次发完则直接抛出MP_EAGAIN。recv(bufsize, [flags]):接收数据,返回 bytes 对象,最多接收
bufsize字节。多数移植版支持可选flags参数,其常量与 CPython 含义相同;所有支持flags的移植版都支持MSG_PEEK与MSG_DONTWAIT。sendto(bytes, address):向
address指定的目标发送数据,套接字本身不应已连接(典型 UDP 用法)。recvfrom(bufsize, [flags]):接收数据,返回
(bytes, address)二元组;flags说明同recv。源码实现会把(数据, (IP, 端口))封装成二元组返回(见 extmod/modsocket.c)。
一个最小的 UDP 收发示例:
import socket # 接收端 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.bind(socket.getaddrinfo('0.0.0.0', 9000)[0][-1]) data, addr = s.recvfrom(1024) # 发送端 s2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s2.sendto(b'hello', socket.getaddrinfo('127.0.0.1', 9000)[0][-1])套接字选项:setsockopt(level, optname, value)
设置指定套接字选项的值,所需符号常量(SO_*等)定义于模块中。value可以是整数,也可以是表示缓冲区的 bytes-like 对象。源码实现支持三种取值形态:整数直接按 4 字节传递;SO_*为 20 且值为None时传递空指针;可调用对象则作为回调传递(见 extmod/modsocket.c)。典型用途如:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 地址重用超时与阻塞模式:settimeout / setblocking
settimeout(value):注意并非每个移植版都支持此方法。value为非负浮点数(秒)或None:
- 非零值:后续套接字操作在超时时间内未完成即抛出
OSError; - 0:切换到非阻塞模式;
None:切换到阻塞模式。
源码中的映射(见 extmod/modsocket.c):None映射为内部-1(阻塞),数值以秒为单位乘以 1000 转为毫秒传给 NIC 层。测试 tests/extmod/socket_udp_nonblock.py 验证了settimeout(0)之后,在无数据到达时调用recv(1)会抛出errno.EAGAIN。
setblocking(flag):flag为假值时设为非阻塞,否则设为阻塞。它是settimeout()的快捷写法(见 extmod/modsocket.c):
sock.setblocking(True)等价于sock.settimeout(None)sock.setblocking(False)等价于sock.settimeout(0)
跨移植版建议:若目标移植版不支持settimeout,更通用可移植的替代方案是使用select.poll,它可以同时等待多个对象(不止套接字,还包括支持轮询的通用 stream 对象):
import select # 不推荐(依赖 settimeout): # s.settimeout(1.0) # s.read(10) # 可能超时 # 推荐: poller = select.poll() poller.register(s, select.POLLIN) res = poller.poll(1000) # 单位:毫秒 if not res: # s 仍无数据可读,即视为超时 pass与 CPython 的差异:CPython 超时时抛出socket.timeout(OSError子类);MicroPython 直接抛OSError。因此用except OSError:捕获异常,代码可在两端同时工作。
文件类接口方法
- makefile(mode='rb', buffering=0, /):返回与套接字关联的文件对象,仅支持二进制模式(
'rb'、'wb'、'rwb');CPython 的encoding、errors、newline参数不支持。两个关键差异:MicroPython 不支持缓冲流,buffering值被忽略视为 0(无缓冲);关闭makefile()返回的文件对象会同时关闭原套接字。实现上该方法直接返回套接字自身(见 extmod/modsocket.c)。 - read([size]):读取至多
size字节并返回 bytes 对象。不指定size时读到 EOF(套接字关闭前不会返回);遵循"无短读"策略,尽力读满请求长度,非阻塞套接字可能返回较少数据。 - readinto(buf[, nbytes]):读入
buf,最多len(buf)字节(指定nbytes则至多该值),同样遵循"无短读";返回读入字节数。 - readline():读取一行(以换行符结尾),返回该行。底层由
mp_stream_unbuffered_readline_obj实现。 - write(buf):写入整个缓冲区("无短写"),非阻塞套接字可能写入部分并返回实际写入字节数。
这些方法让 socket 可以直接配合select、asyncio等基于 stream 协议的框架使用,是嵌入式开发中非常实用的能力。
异常约定:用 OSError 统一处理
MicroPython没有socket.error异常。CPython 曾提供现已废弃的socket.error,它是OSError的别名;在 MicroPython 中请直接使用OSError。
综合来看,MicroPython 的套接字编程错误处理可以归结为一个统一原则:所有套接字错误(包括超时、DNS 解析失败、未连接时的操作)都是OSError,这与 CPython 中"捕获OSError也能兼容"的做法一致。仓库测试 tests/extmod/socket_tcp_basic.py 验证了在全新未连接套接字上调用recv(1)会抛出errno.ENOTCONN。
实战综合示例:TCP 客户端
综合上文所有要点,一个完整、可移植的 TCP 客户端如下:
import socket def http_get(host, path="/"): # 1. 解析地址:始终使用 getaddrinfo,显式过滤出 STREAM 地址 addr = socket.getaddrinfo(host, 80, 0, socket.SOCK_STREAM)[0][-1] # 2. 创建并连接(type 自动选择 TCP 协议) s = socket.socket() s.connect(addr) # 3. 发送请求:write 保证无短写 s.write(b"GET %s HTTP/1.0\r\nHost: %s\r\n\r\n" % (path, host)) # 4. 读取响应:readline 按行读取 resp = b"" while True: line = s.readline() if not line: break resp += line s.close() return resp print(http_get("micropython.org", "/"))如需超时保护且目标移植版不支持settimeout,可将第 2 步之后替换为select.poll轮询方案(见上文"超时与阻塞模式"小节)。
结语
MicroPython 的socket模块以 BSD 套接字接口为蓝本,针对资源受限环境做了务实取舍:地址解析统一收敛到getaddrinfo(),套接字直接具备 stream 文件接口,错误处理统一为OSError。本文覆盖了地址格式、模块函数、常量、类方法与底层实现;更深入的内容可继续阅读:
- 官方文档原文:docs/library/socket.rst
- 通用实现源码:extmod/modsocket.c
- Unix 移植版实现(含
inet_pton/inet_ntop):ports/unix/modsocket.c - 网络相关测试:tests/net_inet/getaddrinfo.py、tests/extmod/socket_tcp_basic.py、tests/extmod/socket_udp_nonblock.py
- 嵌入式
- 语言运行时
- 编程语言
- 解释器
- 编译器
- 物联网
- 系统编程
【免费下载链接】micropython
MicroPython - a lean and efficient Python implementation for microcontrollers and constrained systems
相关推荐
CPython socket 模块全解析:BSD 底层网络接口、地址族与 TCP/IP 实战
CPython socket 模块全解析:BSD 底层网络接口、地址族与 TCP/IP 实战 socket 是 CPython 标准库中最贴近操作系统内核的模块
编程语言语言运行时解释器标准库WAMR Socket API 实战指南:在 WebAssembly 中完整使用 Berkeley/POSIX 套接字接口
WAMR Socket API 实战指南:在 WebAssembly 中完整使用 Berkeley/POSIX 套接字接口 导读 本文基于 WAMR(WebAs
可观测性云原生Asterinas套接字:网络套接字API的实现
Asterinas套接字:网络套接字API的实现 概述 Asterinas是一个用Rust编写的安全、快速、通用的操作系统内核,提供Linux兼容的ABI(Ap
操作系统内核驱动系统编程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考