apppublic/aidocs/udp_comm.md
2025-10-05 11:23:33 +08:00

240 lines
7.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# `UdpComm` 技术文档
```markdown
# 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
```
示例:
```python
def msg_handle(data, addr):
print('addr:', addr, 'data=', data)
```
---
## 使用示例
### 作为服务器启动并监听
```bash
python udpcomm.py 50000
```
交互式输入格式:`目标端口:消息内容`
例如:
```
50001:{"cmd": "discover"}
```
→ 将 JSON 数据发送到 `localhost:50001`IP 为空时自动替换为 `''`
### 在代码中使用
```python
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()
```
---
## 注意事项与限制
1.**线程安全**`buffer` 未加锁,若多线程调用 `send()` 可能存在竞争风险。建议外部同步或改用线程安全队列。
2. 💤 **性能**:每轮 `sleep(0.1)` 可能影响实时性;可根据需要调整间隔。
3. 🧹 **错误处理**JSON 解析失败仅打印日志,不会终止服务。
4. 🌐 **广播范围**:依赖子网掩码配置,`.255` 未必总是有效广播地址。
5. 🚫 **IPv6 不支持**:目前仅限 IPv4。
---
## TODO 改进建议
- [ ] 添加日志模块替代 `print`
- [ ] 使用 `queue.Queue` 替代 `list` 实现线程安全缓冲
- [ ] 支持 IPv6
- [ ] 增加单元测试
- [ ] 提供上下文管理器(`with` 支持)
---
```
> 文档版本v1.0
> 最后更新2025-04-05