DPMP · Dual-Punch Multi-Path Protocol
双打洞多路径协议 —— 一个 P2P 连接与传输底层库。
DPMP 只做一件事:把字节可靠地从 A 送到 B。
你提供「服务器 + 房间号」,它返回「一个可收发 bytes 的通道」;
至于这些字节是文件、视频帧还是游戏消息 —— 由你的上层定义。
适用场景:文件互传、远程控制、投屏、游戏房间等一切需要 P2P 的软件底座。
核心能力(两项独创)
-
双打洞协同(Dual-Punch) 先通过 UDP 打洞建立可靠控制通道(UDP-RTP),再用该通道作为控制面 协调 TCP 打洞(轮次对齐、候选并行、就绪信号、失败通知)。 控制面与数据面分离,显著提升 TCP 打洞成功率。
-
梯度冗余多路径(Multi-Path) 同一对端维护多条路径,按 hot / warm_safe / warm_loose 三级角色分级调度: 热备承载数据、保守暖备待命、宽松暖备兜底。热备断线自动接力、换路续传。
分层架构
| 层 | 模块 | 职责 |
|---|---|---|
| 协议 | dpmp.protocol |
线格式常量与编解码(两端唯一事实来源,不可配) |
| 连接 | dpmp.link |
发现 / 信令 / 双打洞 / UDP-RTP / 路径调度 / 连接管理 |
| 传输 | dpmp.stream |
统一字节流通道(屏蔽 TCP / UDP-RTP 差异) |
| 配置 | dpmp.config |
调优参数(全可配) |
| 默认 | dpmp.defaults |
默认信令服务器(便利入口,可覆盖) |
| 工具 | dpmp.util |
本机 IP / 子网 / 广播地址 / 设备标识 |
依赖:纯标准库,零第三方。
安装
pip install -e .
快速开始
1. 公网模式(异地互传)
from dpmp.link.signaling import SignalingClient
from dpmp.link.manager import LinkManager
# ① 信令客户端(不传服务器则用内置默认;见「默认服务器」一节)
sig = SignalingClient(
servers=["your.server.com:3336", "backup.com:3336"], # 多服务器故障转移
room="my_room", # 房间号,两端填同一个
name="my_pc", # 显示名
tcp_port=9998, # 本机打洞 / 长连接端口
lan_ips=[], # 本机局域网 IP(可空)
device_id="dev-001", # 稳定设备标识(去重用)
punch_local_port=9998, # 打洞本地端口
udp_hole_port=9996, # UDP 打洞端口
)
# ② 连接管理(打洞 + 多路径 + 保活)
lm = LinkManager(sig, local_tcp_port=9998)
# ③ 把信令事件接到连接管理(必须)
sig.on_joined = lm.on_joined
sig.on_member_join = lm.on_member_join
sig.on_member_leave = lm.on_member_leave
sig.on_punch_go = lm.on_punch_go
sig.on_udp_hole_ready = lm.on_udp_hole_ready
sig.on_mapping_ready = lm.mark_mapping_ready
# ④ 连接就绪回调 —— 在这里跑你自己的协议
def on_ready(peer_id, sock, member):
sock.sendall(b"hello") # 发
data = sock.recv(4096) # 收
lm.on_socket_ready = on_ready
# ⑤ 启动
lm.start()
sig.start() # 依次尝试 servers,第一个成功即停
# ⑥ 网络切换时重建(宿主检测到后调用,见「网络切换重建」一节)
# 示例:Android 在 NetworkCallback 里、桌面在 IP 轮询里调用
def on_network_changed():
lm.on_network_changed() # 关闭旧连接 + 信令重绑 + 重新打洞(复用身份)
2. 局域网模式(无需服务器)
from dpmp.link.discovery import Discovery
d = Discovery(
device_id="dev-001", hostname="my-pc",
on_new_node=lambda ip, msg: print("发现设备", ip),
)
d.start()
d.broadcast_search() # 广播搜索
d.scan_subnet("192.168.1.0/24") # 扫描子网
print(d.get_nodes()) # {ip: {hostname, device_id, mac, ...}}
3. 主动发送(按路径角色选路)
ch = lm.get_send_channel(peer_id) # 返回 (sock, io_lock, is_udp, role) 或 None
if ch:
sock, lock, is_udp, role = ch # role: hot / warm_safe / warm_loose
with lock:
sock.sendall(b"data")
4. 单独使用打洞
from dpmp.link.puncher import HolePuncher
p = HolePuncher(local_tcp_port=9998)
p.set_udp_hint(peer_id, ip, udp_port) # 双打洞:用 UDP 端口预测 TCP 候选
result = p.punch(peer, at_ms) # 并行打洞,返回 PunchResult(含 sock)
5. 网络切换重建(接口反转)
DPMP 平台无关,由宿主检测网络变化,检测到后调用一行:
# 宿主:检测到 Wi-Fi/蜂窝切换、IP 变化时调用
lm.on_network_changed()
DPMP 内部自动完成:关闭旧连接 → 信令重开并重新加入(复用旧 id,无感重建) → 对所有成员重新打洞。带 10 秒冷却。
宿主检测示例(平台相关):
# Android:ConnectivityManager.NetworkCallback.onAvailable 里
# 桌面:定时轮询本机 IP 集合,发现变化时
# 只要最终调用 lm.on_network_changed() 即可
服务器地址
三种写法
# 写法一:IP + 端口
SignalingClient(server_ip="1.2.3.4", server_port=3336, ...)
# 写法二:多服务器列表(第一个失败自动切下一个)
SignalingClient(servers=[
"primary.example.com:3336",
"backup1.example.com:3336",
("backup2.example.com", 3336, 3337, 3338), # ip, port, tcp_port, nat_port
], auto_failover=True, ...)
# 写法三:字典
SignalingClient(servers=[{"ip": "1.2.3.4", "port": 3336}], ...)
默认服务器(便利入口,非依赖)
不传任何服务器时,回退到内置默认服务器,方便开箱即用:
from dpmp import DEFAULT_SERVER, DEFAULT_SERVER_EXPIRES, check_default_server_expiry
print(DEFAULT_SERVER) # "42.194.133.132"
print(DEFAULT_SERVER_EXPIRES) # "2026-11-01"
print(check_default_server_expiry())
# {'status': 'ok', 'days_left': 38, 'expires': '2026-11-01'}
默认服务器只是便利,不是依赖:
- 使用默认服务器时,
start()会自动检查到期并在临近/过期时提示; - 传了自己的
server_ip/servers后,默认服务器完全被绕过; - 局域网模式根本不使用服务器。
⚠️ 默认服务器到期后,请改用自建服务器或备用服务器。 修改默认值见
dpmp/defaults.py。
配置
调优参数通过 Config 按实例配置(协议常量不可配):
from dpmp import Config
cfg = Config(
rtp_window_init=64, # UDP-RTP 自适应窗口初始值
rtp_window_min=1, # 自适应窗口下限
rtp_window_max=256, # 自适应窗口上限(AIMD 动态调节)
rtp_keepalive_interval=10, # 保活间隔
punch_retry=5, # 打洞重试次数
punch_connect_timeout=4.0, # 打洞连接超时
punch_concurrency=8, # 打洞并发任务数
punch_candidate_concurrency=2,# 单次打洞中【并发 connect 的候选数】上限
heartbeat_interval=20, # 信令心跳间隔
proto_floor={"tcp": 30, "udp": 15}, # 保活硬性下限
rebuild_cooldown_sec=30.0, # 重建冷却
)
sig = SignalingClient(..., config=cfg)
lm = LinkManager(sig, 9998, config=cfg)
# 或改全局默认
import dpmp
dpmp.DEFAULT_CONFIG.rtp_window = 200
分层原则:
- 协议常量不可配(改了与对端不兼容)
- 调优参数、地址端口全部可配
自建服务器
信令服务器随本项目一起开源(见仓库 server/ 目录),
只牵线、不传数据、带宽近乎为零。可自行部署:
- Linux:
install_linux.sh(systemd) - Windows:
install_windows.bat(计划任务) - 配置:
config.json(端口、房间上限、心跳超时等)
服务器地址通过 server_ip / servers 传给 SignalingClient,
客户端不写死任何服务器。
API 参考
顶层(import dpmp)
| 名称 | 说明 |
|---|---|
Config / DEFAULT_CONFIG |
调优参数 |
DEFAULT_SERVER / DEFAULT_SERVER_EXPIRES |
默认服务器与到期日 |
default_servers() |
默认服务器候选列表 |
check_default_server_expiry() |
检查默认服务器到期状态 |
__version__ / __protocol__ |
版本 / 协议标识 |
连接层(dpmp.link)
| 名称 | 说明 |
|---|---|
Discovery |
局域网 UDP 发现(广播 / 扫描 / 心跳) |
SignalingClient |
公网信令客户端(房间 / 校时 / 映射观测 / NAT 探测) |
HolePuncher / PunchResult |
双打洞核心 / 打洞结果 |
UdpReliableSocket |
UDP 可靠通道(UDP-RTP) |
PeerPathScheduler / Path / KeepaliveScheduler |
梯度冗余多路径调度 |
LinkManager / Conn |
连接管理 / 单条连接 |
传输层(dpmp.stream)
| 名称 | 说明 |
|---|---|
Channel |
统一字节流通道(sendall / recv / recv_exact / close) |
wrap_channel(sock, io_lock, role) |
包装底层 socket |
is_udp_rtp(sock) |
判断是否 UDP-RTP 通道 |
工具层(dpmp.util.net)
| 名称 | 说明 |
|---|---|
get_all_local_ips(ipv6=False) |
本机所有 IP |
get_all_subnets() |
本机所有子网 CIDR |
get_broadcast_addrs() |
广播地址 |
get_subnet_for_ip(ip) |
由 IP 推断子网 |
get_mac_address() |
本机 MAC |
get_or_create_device_id(path) |
持久化设备 UUID |
关键回调与方法
LinkManager 回调
| 回调 | 触发时机 |
|---|---|
on_socket_ready(peer_id, sock, member) |
TCP 通道建立(最常用) |
on_udp_ready(peer_id, rtp) |
UDP-RTP 通道建立 |
on_state_changed() |
成员状态变化(刷新 UI) |
LinkManager 方法
| 方法 | 用途 |
|---|---|
get_send_channel(peer_id, exclude_socks=None) |
按角色选路,返回可发送通道 |
get_socket(peer_id) |
取 TCP socket |
get_udp_socket(peer_id) |
取 UDP-RTP 通道 |
get_members() |
成员列表(含状态、路径角色) |
get_path_roles(peer_id) |
各路径角色 |
mark_active(peer_id) |
标记活跃(重置空闲降频) |
on_network_changed() |
网络切换时由宿主调用,重建所有连接(见下节) |
start() / stop() |
启动 / 停止 |
网络切换重建(接口反转)
网络切换(Wi-Fi ↔ 蜂窝、IP 变化)后,旧网络上的所有 socket 与 NAT 映射全部失效。 DPMP 是平台无关的库,无法自己感知网络变化,因此采用「接口反转」:
- 宿主应用负责【检测】(平台相关:Android NetworkCallback、桌面 IP 轮询等)
- DPMP 负责【重建】(统一逻辑,所有用户受益)
宿主检测到网络变化后,只需一行调用:
lm.on_network_changed()
DPMP 内部编排完整重建:
- 关闭所有现存连接(旧 NAT 映射已失效)
- 让信令客户端重开 socket 并重新加入(
SignalingClient.rebind()) - 对所有成员重新发起打洞
无感重建:重建时
- 不发 BYE(服务器保留成员条目,对端不掉线)
- 携带旧
my_id(reuse_id)——服务器支持则复用身份,对端看到的是 「同一成员回归」;旧服务器忽略该字段,退化为生成新 id(等价重新 join)
带 10 秒冷却,避免网络抖动频繁重建。
端口说明
| 端口 | 协议 | 用途 |
|---|---|---|
| 3336 | UDP | 信令服务器(加入房间 / 心跳 / 打洞协调) |
| 3337 | TCP | 信令服务器 TCP 映射观测 |
| 3338 | UDP | 信令服务器 NAT 类型探测 |
| 9996 | UDP | 房间模式 UDP 打洞 |
| 9998 | TCP | 房间模式打洞 / 长连接 |
| 9997 | UDP | 局域网主动扫描 |
| 9998 | UDP | 局域网设备发现(广播 / 心跳) |
| 9999 | TCP | 局域网文件传输 |
所有端口均可通过参数或 Config 覆盖。
协议文档
DPMP_PROTOCOL.md—— 完整协议规范(两端实现唯一事实来源)test_vectors.json—— 协议测试向量(防 Python / Kotlin 两端漂移)
版本历史
0.1.2
- 新增:端到端示例
examples/echo.py——加入房间 → 打洞 → 收发文本消息 → 回显, 最贴近真实用法的完整可运行示例(局域网发现示例仍见examples/basic_lan.py)。 - 新增:协议编解码精确布局(
DPMP_PROTOCOL.md§7)——UDP-RTP 包头逐字节 偏移表、各控制报文 payload 结构、v1↔v2 版本协商流程、test_vectors.json 解读。 - 新增:类型注解 +
py.typed(PEP 561)——全部公开 API 加类型标注, IDE(PyCharm / VSCode)可获得参数与返回值补全。 - 文档:更新信令服务器说明——它随本项目一起开源(见仓库
server/目录), 不再是"独立开源"。 - 本次不改变任何协议与传输行为,库功能与 0.1.1 一致;仅新增示例、文档与类型信息。
0.1.1(底层能力补强)
- 新增:UDP-RTP 自适应窗口(AIMD)——发送端按 ACK 加性增、按超时乘性减, 在 1~256 之间动态调节;高延迟链路上吞吐上限提升数倍。
- 新增:打洞候选并发限制(
punch_candidate_concurrency,默认 2)—— 降低同一本地端口上 SO_REUSEPORT 的入站 SYN 匹配冲突。 - 新增:网络切换重建(接口反转)——
SignalingClient.rebind()+LinkManager.on_network_changed();宿主检测网络变化,DPMP 统一重建。 - 新增:身份复用(
reuse_id)——重建时复用旧 my_id,实现无感重建。 - 修复:收到对端 PUNCH_FAIL 时未关闭半开连接——该 socket 可能被
get_send_channel选为发送通道,把数据发进单向死连接。现在无条件关闭并 触发路径轮转。
0.1.0(初版)
- 双打洞协同、UDP-RTP 可靠通道、梯度冗余多路径、局域网发现、公网信令。
许可证
本项目采用 Apache License 2.0 开源许可,详见 LICENSE 文件。
你可以自由使用、修改、分发本项目(含商业用途),需保留版权声明与许可声明。
Release files for dpmp 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| dpmp-0.1.2.tar.gz | 56.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| dpmp-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 113.4 kB
Release files / dpmp-0.1.2.tar.gz
| Download URL | dpmp-0.1.2.tar.gz |
|---|---|
| Size | 56.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2a9139bccc4878c362faf43db10fa1a291c5075db710f5123931d113ff6e72e8
|
|
BLAKE2b-256 checksum How to use checksums |
511c2e93d3afcbe095f568bb58f554dcd1a87832db1c6f90fb05ec7863a4d9e8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.15
|
Release files / dpmp-0.1.2-py3-none-any.whl
| Download URL | dpmp-0.1.2-py3-none-any.whl |
|---|---|
| Size | 57.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
df4be7440d6904d904bbbe87345d9923b0244b3d7e612fbdfb9ec2ebda20181f
|
|
BLAKE2b-256 checksum How to use checksums |
49469bac438ed34f474d582d0cbf2a04d53f43841fbc240b2ecf80316941e1aa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.15
|