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

7.0 KiB
Raw Blame History

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:50001IP 为空时自动替换为 ''

在代码中使用

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