7.0 KiB
7.0 KiB
UdpComm 技术文档
# UdpComm - UDP 通信模块技术文档
## 概述
`UdpComm` 是一个基于 Python 的异步 UDP 通信类,支持单播发送、广播、接收 JSON 或二进制数据,并通过后台线程处理 I/O 多路复用(`select`),适用于轻量级网络服务或设备发现等场景。
该模块封装了 UDP 套接字的基本操作,提供非阻塞接收与带缓冲的发送机制,同时支持自定义回调函数处理接收到的数据。
---
## 依赖说明
### 第三方/内部依赖
- `appPublic.sockPackage.get_free_local_addr`: 获取本机可用 IP 地址。
- `appPublic.background.Background`: 用于在后台运行任务的线程包装器。
> ⚠️ 注意:这两个模块属于项目私有库 `appPublic`,需确保其已安装并可导入。
### 内置模块
- `socket`: 提供底层网络通信功能。
- `select`: 实现 I/O 多路复用。
- `json`: 用于序列化和反序列化 JSON 数据。
- `time`: 控制循环延迟。
- `traceback.print_exc`: 打印异常堆栈信息。
---
## 常量
| 名称 | 值 | 说明 |
|------------|--------------|------|
| `BUFSIZE` | `1024 * 64` | 接收缓冲区大小,即每次最多接收 65536 字节 |
---
## 类定义:`UdpComm`
### 构造函数:`__init__(self, port, callback, timeout=0)`
初始化 UDP 通信实例。
#### 参数:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|-----------|----------|------|--------|------|
| `port` | int | 是 | - | 绑定的本地 UDP 端口号 |
| `callback`| function | 是 | - | 回调函数,用于处理接收到的数据,签名应为 `callback(data, addr)` |
| `timeout` | float | 否 | 0 | 超时时间(未启用) |
#### 初始化行为:
- 自动获取本机 IP 地址(用于后续广播计算)。
- 创建并绑定 UDP 套接字到指定端口 (`('', port)`)。
- 启动后台线程执行 `run()` 方法,持续监听消息。
- 初始化空发送缓冲队列 `buffer`。
> 📌 注:当前版本中 `setblocking` 和 `settimeout` 已被注释,实际为非阻塞模式配合 `select` 使用。
---
### 核心方法
#### `run(self)`
主循环逻辑,由后台线程执行。
##### 功能流程:
1. 使用 `select([sock], outs, [], 0.1)` 监听:
- 输入事件:是否有数据可读。
- 输出事件:是否可以发送数据(当缓冲区不为空时触发)。
2. 若有数据到达:
- 调用 `recvfrom()` 读取数据。
- 解析首字节标识符:
- `'b'` → 视为原始 **bytes** 数据,直接传递给回调。
- 其他 → 视为 UTF-8 编码的 JSON 字符串,尝试反序列化后传入回调。
- 出错时打印异常及原始数据,但不停止运行(仅中断本次处理)。
3. 若可写且缓冲区有数据:
- 从 `buffer` 中取出第一条待发消息并使用 `sendto()` 发送。
4. 循环结束条件:`self.run_flg == False`
5. 最终关闭套接字。
> ⏱️ 每次循环休眠 `0.1` 秒以降低 CPU 占用。
---
#### `stop(self)`
安全停止通信线程。
##### 行为:
- 设置标志位 `run_flg = False`,通知 `run()` 结束循环。
- 关闭 UDP 套接字。
- 等待后台线程退出(`join()`)。
> ✅ 推荐在程序退出前调用此方法释放资源。
---
#### `broadcast(self, data)`
向局域网广播消息(目标地址 `.255`)。
##### 参数:
| 参数名 | 类型 | 说明 |
|-------|------|------|
| `data` | any (serializable) or bytes | 要广播的数据 |
##### 处理逻辑:
- 若 `data` 非 `bytes` 类型,则将其 JSON 序列化并编码为 UTF-8。
- 构造广播地址(如本机 IP 为 `192.168.1.100`,则广播地址为 `192.168.1.255`)。
- 创建临时 UDP 客户端套接字,启用广播选项(`SO_BROADCAST=1`)。
- 发送数据到 `(broadcast_host, self.port)`。
> 🔊 广播不会经过 `send()` 缓冲区,是即时发送。
---
#### `send(self, data, addr)`
将数据加入发送缓冲区,异步发送至指定地址。
##### 参数:
| 参数名 | 类型 | 说明 |
|-------|--------------|------|
| `data` | str / dict / bytes | 待发送数据 |
| `addr` | tuple or list | 目标地址 `(ip, port)` |
##### 数据封装规则:
- 若 `data` 不是 `bytes`:添加前缀 `'j'`,表示 JSON 数据。
- 若 `data` 是 `bytes`:添加前缀 `'b'`,表示二进制数据。
> 示例:
> - `send({"msg": "hello"}, ("192.168.1.2", 5000))` → 实际发送 `'j{"msg":"hello"}'`
> - `send(b'\x01\x02', ("192.168.1.2", 5000))` → 实际发送 `'b\x01\x02'`
> 🔄 数据会被追加到 `self.buffer`,由 `run()` 线程异步发送。
---
#### `sends(self, data, addrs)`
批量发送相同数据到多个地址。
##### 参数:
| 参数名 | 类型 | 说明 |
|--------|-------------|------|
| `data` | any / bytes | 要发送的数据 |
| `addrs`| list of tuple/list | 多个目标地址列表 |
##### 行为:
遍历 `addrs` 列表,对每个地址调用 `self.send(data, addr)`。
---
## 数据格式约定
| 首字节 | 含义 | 数据内容 |
|--------|------------|------------------------|
| `'b'` | Binary | 原始二进制数据(无编码) |
| `'j'` | JSON | UTF-8 编码的 JSON 字符串 |
接收端根据首字节判断如何解析后续数据。
---
## 回调函数规范
用户提供的 `callback` 函数必须接受两个参数:
```python
def callback(data, addr):
# data: 解码后的对象(dict / list / bytes)
# addr: 发送方地址 tuple(ip: str, port: int)
pass
示例:
def msg_handle(data, addr):
print('addr:', addr, 'data=', data)
使用示例
作为服务器启动并监听
python udpcomm.py 50000
交互式输入格式:目标端口:消息内容
例如:
50001:{"cmd": "discover"}
→ 将 JSON 数据发送到 localhost:50001(IP 为空时自动替换为 '')
在代码中使用
def on_message(data, addr):
print(f"Received from {addr}: {data}")
# 启动监听
comm = UdpComm(port=50000, callback=on_message)
# 发送消息
comm.send({"hello": "world"}, ("192.168.1.50", 50000))
# 广播
comm.broadcast({"type": "service_announce"})
# 停止服务
comm.stop()
注意事项与限制
- ❗ 线程安全:
buffer未加锁,若多线程调用send()可能存在竞争风险。建议外部同步或改用线程安全队列。 - 💤 性能:每轮
sleep(0.1)可能影响实时性;可根据需要调整间隔。 - 🧹 错误处理:JSON 解析失败仅打印日志,不会终止服务。
- 🌐 广播范围:依赖子网掩码配置,
.255未必总是有效广播地址。 - 🚫 IPv6 不支持:目前仅限 IPv4。
TODO 改进建议
- 添加日志模块替代
print - 使用
queue.Queue替代list实现线程安全缓冲 - 支持 IPv6
- 增加单元测试
- 提供上下文管理器(
with支持)
> 文档版本:v1.0
> 最后更新:2025-04-05