dpmp 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dpmp/__init__.py +27 -0
- dpmp/config.py +74 -0
- dpmp/defaults.py +62 -0
- dpmp/link/__init__.py +27 -0
- dpmp/link/discovery.py +376 -0
- dpmp/link/manager.py +887 -0
- dpmp/link/natprobe.py +78 -0
- dpmp/link/path.py +180 -0
- dpmp/link/puncher.py +253 -0
- dpmp/link/rtp.py +521 -0
- dpmp/link/signaling.py +641 -0
- dpmp/protocol/__init__.py +7 -0
- dpmp/protocol/addr.py +37 -0
- dpmp/protocol/codec.py +89 -0
- dpmp/protocol/constants.py +140 -0
- dpmp/stream/__init__.py +13 -0
- dpmp/stream/channel.py +62 -0
- dpmp/util/__init__.py +2 -0
- dpmp/util/net.py +115 -0
- dpmp-0.1.1.dist-info/METADATA +415 -0
- dpmp-0.1.1.dist-info/RECORD +24 -0
- dpmp-0.1.1.dist-info/WHEEL +5 -0
- dpmp-0.1.1.dist-info/licenses/LICENSE +201 -0
- dpmp-0.1.1.dist-info/top_level.txt +1 -0
dpmp/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""DPMP — Dual-Punch Multi-Path Protocol.
|
|
3
|
+
|
|
4
|
+
P2P 连接与传输底层库。
|
|
5
|
+
|
|
6
|
+
对外分层:
|
|
7
|
+
dpmp.protocol 协议常量与编解码
|
|
8
|
+
dpmp.link 连接层(发现 / 信令 / 双打洞 / UDP-RTP / 路径调度 / 连接管理)
|
|
9
|
+
dpmp.stream 传输层(统一字节流通道 + 多径选路)
|
|
10
|
+
dpmp.util 工具层(网络探测 / 设备标识)
|
|
11
|
+
|
|
12
|
+
典型用法(远程控制 / 投屏 / 游戏房间通用):
|
|
13
|
+
|
|
14
|
+
from dpmp.link.manager import LinkManager
|
|
15
|
+
from dpmp.link.signaling import SignalingClient
|
|
16
|
+
|
|
17
|
+
lm = LinkManager(sig, local_tcp_port=9998)
|
|
18
|
+
lm.on_socket_ready = lambda pid, sock, m: start_my_protocol(sock)
|
|
19
|
+
# 之后用 lm.get_send_channel(pid) 拿到可用通道发送字节流
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from .config import Config, DEFAULT_CONFIG
|
|
23
|
+
from .defaults import (DEFAULT_SERVER, DEFAULT_SERVER_EXPIRES,
|
|
24
|
+
default_servers, check_default_server_expiry)
|
|
25
|
+
|
|
26
|
+
__version__ = "0.1.1"
|
|
27
|
+
__protocol__ = "DPMP/1.0"
|
dpmp/config.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""DPMP 运行时配置(调优参数)。
|
|
3
|
+
|
|
4
|
+
协议常量(消息类型 / 包类型 / 字段格式)见 dpmp.protocol.constants,
|
|
5
|
+
**故意不可配**——它们是对端约定,改了就不兼容。
|
|
6
|
+
|
|
7
|
+
本模块只放**不影响兼容性**的调优参数,支持按实例覆盖:
|
|
8
|
+
|
|
9
|
+
from dpmp import Config
|
|
10
|
+
cfg = Config()
|
|
11
|
+
cfg.rtp_window = 128
|
|
12
|
+
sig = SignalingClient(..., config=cfg)
|
|
13
|
+
|
|
14
|
+
全局默认:dpmp.config.DEFAULT_CONFIG(改它影响所有未显式传 config 的实例)。
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from .protocol import constants as C
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Config:
|
|
21
|
+
"""DPMP 调优参数集合。所有字段都有默认值,可逐个覆盖。"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, **kw):
|
|
24
|
+
self.reset()
|
|
25
|
+
for k, v in kw.items():
|
|
26
|
+
if hasattr(self, k):
|
|
27
|
+
setattr(self, k, v)
|
|
28
|
+
|
|
29
|
+
def reset(self):
|
|
30
|
+
"""恢复全部默认值。"""
|
|
31
|
+
# ---- UDP-RTP ----
|
|
32
|
+
self.rtp_max_payload = C.RTP_MAX_PAYLOAD
|
|
33
|
+
self.rtp_window = C.RTP_WINDOW # 初始值(兼容名)
|
|
34
|
+
self.rtp_window_min = C.RTP_WINDOW_MIN # 自适应窗口下限
|
|
35
|
+
self.rtp_window_max = C.RTP_WINDOW_MAX # 自适应窗口上限
|
|
36
|
+
self.rtp_window_init = C.RTP_WINDOW_INIT
|
|
37
|
+
self.rtp_rto_ms = C.RTP_RTO_MS
|
|
38
|
+
self.rtp_rtx_interval = C.RTP_RTX_INTERVAL
|
|
39
|
+
self.rtp_keepalive_interval = C.RTP_KEEPALIVE_INTERVAL
|
|
40
|
+
self.rtp_peer_dead_timeout = C.RTP_PEER_DEAD_TIMEOUT
|
|
41
|
+
# ---- SYNC(TCP 打洞时刻对齐)----
|
|
42
|
+
self.sync_sample_count = C.SYNC_SAMPLE_COUNT
|
|
43
|
+
self.sync_timeout = C.SYNC_TIMEOUT
|
|
44
|
+
self.sync_commit_delay_ms = C.SYNC_COMMIT_DELAY_MS
|
|
45
|
+
self.sync_commit_resend = C.SYNC_COMMIT_RESEND
|
|
46
|
+
self.sync_commit_resend_interval = C.SYNC_COMMIT_RESEND_INTERVAL
|
|
47
|
+
# ---- 打洞 ----
|
|
48
|
+
self.punch_connect_timeout = C.PUNCH_CONNECT_TIMEOUT
|
|
49
|
+
self.punch_concurrency = C.PUNCH_CONCURRENCY
|
|
50
|
+
self.punch_candidate_concurrency = C.PUNCH_CANDIDATE_CONCURRENCY
|
|
51
|
+
self.punch_retry = C.PUNCH_RETRY
|
|
52
|
+
self.punch_retry_backoff = C.PUNCH_RETRY_BACKOFF
|
|
53
|
+
self.punch_port_predict_range = tuple(C.PUNCH_PORT_PREDICT_RANGE)
|
|
54
|
+
# ---- 信令 ----
|
|
55
|
+
self.heartbeat_interval = C.HEARTBEAT_INTERVAL
|
|
56
|
+
self.recv_timeout = C.RECV_TIMEOUT
|
|
57
|
+
self.udp_probe_interval = C.UDP_PROBE_INTERVAL
|
|
58
|
+
self.udp_probe_duration = C.UDP_PROBE_DURATION
|
|
59
|
+
# ---- 路径调度(梯度冗余多路径)----
|
|
60
|
+
self.proto_floor = dict(C.PROTO_FLOOR)
|
|
61
|
+
self.proto_safe_cap = dict(C.PROTO_SAFE_CAP)
|
|
62
|
+
self.role_factor = dict(C.ROLE_FACTOR)
|
|
63
|
+
self.proto_priority = dict(C.PROTO_PRIORITY)
|
|
64
|
+
# ---- 连接管理 ----
|
|
65
|
+
self.rebuild_cooldown_sec = 30.0
|
|
66
|
+
self.keepalive_tick = 1.0
|
|
67
|
+
self.idle_factor_tiers = [(60, 1), (600, 4), (3600, 16)]
|
|
68
|
+
self.idle_factor_max = 64
|
|
69
|
+
# ---- 局域网发现 ----
|
|
70
|
+
self.discover_interval = C.HEARTBEAT_INTERVAL
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# 全局默认配置(未显式传 config 的实例都用它)
|
|
74
|
+
DEFAULT_CONFIG = Config()
|
dpmp/defaults.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""DPMP 应用默认值(非协议常量)。
|
|
3
|
+
|
|
4
|
+
协议常量见 dpmp.protocol.constants(与对端约定,不可配)。
|
|
5
|
+
本模块放**部署相关的默认值**——默认信令服务器等,方便开箱即用。
|
|
6
|
+
这些值随时可被使用者的参数覆盖,且**不应被协议层依赖**。
|
|
7
|
+
|
|
8
|
+
⚠️ 默认服务器是「便利」,不是「依赖」:
|
|
9
|
+
· 到期后客户端会明确提示,使用者应改用自建/备用服务器;
|
|
10
|
+
· 通过 SignalingClient(servers=[...]) 可完全绕过默认值;
|
|
11
|
+
· 局域网模式完全不使用服务器。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .protocol import constants as C
|
|
15
|
+
|
|
16
|
+
# ============================================================
|
|
17
|
+
# 默认信令服务器(便利入口,非强制)
|
|
18
|
+
# ============================================================
|
|
19
|
+
DEFAULT_SERVER = "42.194.133.132"
|
|
20
|
+
DEFAULT_SERVER_PORT = C.DEFAULT_SERVER_PORT # 3336
|
|
21
|
+
DEFAULT_SERVER_TCP_PORT = C.DEFAULT_SERVER_TCP_PORT # 3337
|
|
22
|
+
DEFAULT_NAT_PROBE_PORT = C.NAT_PROBE_PORT # 3338
|
|
23
|
+
|
|
24
|
+
# 默认服务器到期日(ISO 日期)。到期/临近时客户端会提示。
|
|
25
|
+
# 使用者应在此日期后改用自建服务器或备用服务器。
|
|
26
|
+
DEFAULT_SERVER_EXPIRES = "2026-11-01"
|
|
27
|
+
|
|
28
|
+
# 临近到期的提醒阈值(天)
|
|
29
|
+
EXPIRY_WARN_DAYS = 7
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def default_servers():
|
|
33
|
+
"""返回默认服务器候选列表(单个)。"""
|
|
34
|
+
return [(DEFAULT_SERVER, DEFAULT_SERVER_PORT,
|
|
35
|
+
DEFAULT_SERVER_TCP_PORT, DEFAULT_NAT_PROBE_PORT)]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def check_default_server_expiry(today=None):
|
|
39
|
+
"""检查默认服务器的到期状态。
|
|
40
|
+
|
|
41
|
+
返回 dict:
|
|
42
|
+
{"status": "ok"|"soon"|"expired"|"unknown",
|
|
43
|
+
"days_left": int|None,
|
|
44
|
+
"expires": "YYYY-MM-DD"}
|
|
45
|
+
"""
|
|
46
|
+
import datetime
|
|
47
|
+
if today is None:
|
|
48
|
+
today = datetime.date.today()
|
|
49
|
+
try:
|
|
50
|
+
exp = datetime.date.fromisoformat(DEFAULT_SERVER_EXPIRES)
|
|
51
|
+
except Exception:
|
|
52
|
+
return {"status": "unknown", "days_left": None,
|
|
53
|
+
"expires": DEFAULT_SERVER_EXPIRES}
|
|
54
|
+
days_left = (exp - today).days
|
|
55
|
+
if days_left < 0:
|
|
56
|
+
status = "expired"
|
|
57
|
+
elif days_left <= EXPIRY_WARN_DAYS:
|
|
58
|
+
status = "soon"
|
|
59
|
+
else:
|
|
60
|
+
status = "ok"
|
|
61
|
+
return {"status": status, "days_left": days_left,
|
|
62
|
+
"expires": DEFAULT_SERVER_EXPIRES}
|
dpmp/link/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""DPMP 连接层。
|
|
3
|
+
|
|
4
|
+
负责「找到对端 + 建立可用通道」:
|
|
5
|
+
discovery 局域网 UDP 发现(广播 / 扫描 / 心跳)
|
|
6
|
+
signaling 公网信令客户端(加入房间 / 心跳 / 成员事件)
|
|
7
|
+
puncher 双打洞核心(TCP 同时打开 + UDP 端口预测)
|
|
8
|
+
rtp UDP 可靠通道(UDP-RTP)
|
|
9
|
+
path 梯度冗余多路径调度
|
|
10
|
+
manager 连接管理(全连接 + 长连接 + 入站接管 + 保活)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .discovery import Discovery
|
|
14
|
+
from .signaling import SignalingClient
|
|
15
|
+
from .puncher import HolePuncher, PunchResult
|
|
16
|
+
from .rtp import UdpReliableSocket
|
|
17
|
+
from .path import PeerPathScheduler, Path, KeepaliveScheduler
|
|
18
|
+
from .manager import LinkManager, Conn
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"Discovery",
|
|
22
|
+
"SignalingClient",
|
|
23
|
+
"HolePuncher", "PunchResult",
|
|
24
|
+
"UdpReliableSocket",
|
|
25
|
+
"PeerPathScheduler", "Path", "KeepaliveScheduler",
|
|
26
|
+
"LinkManager", "Conn",
|
|
27
|
+
]
|
dpmp/link/discovery.py
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""局域网设备发现:UDP 广播 + 主动扫描 + 心跳。
|
|
3
|
+
|
|
4
|
+
节点按 device_id 去重(IP 变化不产生重复条目)。发现完全依赖
|
|
5
|
+
「请求 → 响应」:收到广播/扫描请求 → 回 reply;收到 reply → 登记节点。
|
|
6
|
+
|
|
7
|
+
端口:
|
|
8
|
+
· 9998 UDP 广播 / 心跳
|
|
9
|
+
· 9997 UDP 主动扫描(与收发隔离,避免扫描影响传输)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import socket
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
|
|
17
|
+
from ..protocol import constants as C
|
|
18
|
+
from ..util.net import (get_all_local_ips, get_broadcast_addrs,
|
|
19
|
+
get_subnet_for_ip, get_mac_address)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Node:
|
|
23
|
+
"""一个已发现的对端设备(dict 形式存储于 Discovery.nodes)。"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Discovery:
|
|
27
|
+
"""局域网设备发现。
|
|
28
|
+
|
|
29
|
+
参数:
|
|
30
|
+
device_id:本机稳定标识(用于去重,不显示自己)
|
|
31
|
+
hostname:本机显示名
|
|
32
|
+
log:日志回调
|
|
33
|
+
on_new_node(ip, msg):发现新节点时回调
|
|
34
|
+
on_node_gone(ip):节点下线时回调
|
|
35
|
+
|
|
36
|
+
对外:
|
|
37
|
+
nodes {ip: node_dict}
|
|
38
|
+
start() / stop()
|
|
39
|
+
broadcast_search() 广播搜索
|
|
40
|
+
scan_subnet(cidr) 扫描子网
|
|
41
|
+
add_manual_node(ip) 手动添加
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, device_id, hostname, log=None,
|
|
45
|
+
on_new_node=None, on_node_gone=None,
|
|
46
|
+
udp_port=None, scan_port=None, config=None):
|
|
47
|
+
if config is None:
|
|
48
|
+
from ..config import DEFAULT_CONFIG
|
|
49
|
+
config = DEFAULT_CONFIG
|
|
50
|
+
self.cfg = config
|
|
51
|
+
self.discover_interval = config.discover_interval
|
|
52
|
+
self.device_id = device_id or ""
|
|
53
|
+
self.hostname = hostname or socket.gethostname()
|
|
54
|
+
self.mac = get_mac_address()
|
|
55
|
+
self.log = log or (lambda m: None)
|
|
56
|
+
self.on_new_node = on_new_node
|
|
57
|
+
self.on_node_gone = on_node_gone
|
|
58
|
+
# 局域网端口(默认 9998 发现 / 9997 扫描,可自定义)
|
|
59
|
+
self.udp_port = udp_port or C.LAN_UDP_PORT
|
|
60
|
+
self.scan_port = scan_port or C.SCAN_PORT
|
|
61
|
+
|
|
62
|
+
self.my_ips = get_all_local_ips(ipv6=False)
|
|
63
|
+
self.my_ips_v6 = get_all_local_ips(ipv6=True)
|
|
64
|
+
self.broadcast_addrs = get_broadcast_addrs()
|
|
65
|
+
|
|
66
|
+
self.nodes = {}
|
|
67
|
+
self.lock = threading.Lock()
|
|
68
|
+
self.running = False
|
|
69
|
+
self.broadcast_sockets = []
|
|
70
|
+
self.listen_sockets = []
|
|
71
|
+
self.auto_scan_enabled = False
|
|
72
|
+
self.scanning = False
|
|
73
|
+
self.pausing_network = False # 传输时暂停广播/扫描/心跳
|
|
74
|
+
|
|
75
|
+
# ---------- 生命周期 ----------
|
|
76
|
+
|
|
77
|
+
def start(self):
|
|
78
|
+
self.running = True
|
|
79
|
+
threading.Thread(target=self._udp_listener, daemon=True).start()
|
|
80
|
+
threading.Thread(target=self._scan_listener, daemon=True).start()
|
|
81
|
+
self._start_broadcasters()
|
|
82
|
+
self.log("本机 IPv4: %s" % ", ".join(self.my_ips))
|
|
83
|
+
if self.my_ips_v6:
|
|
84
|
+
self.log("本机 IPv6: %s" % ", ".join(self.my_ips_v6))
|
|
85
|
+
self.log("广播地址: %s" % ", ".join(self.broadcast_addrs))
|
|
86
|
+
|
|
87
|
+
def stop(self):
|
|
88
|
+
self.running = False
|
|
89
|
+
for s in self.broadcast_sockets + self.listen_sockets:
|
|
90
|
+
try:
|
|
91
|
+
s.close()
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
self._send_bye()
|
|
95
|
+
|
|
96
|
+
# ---------- 消息构造 ----------
|
|
97
|
+
|
|
98
|
+
def _build_msg(self, **extra):
|
|
99
|
+
msg = {
|
|
100
|
+
C.LAN_F_HOSTNAME: self.hostname,
|
|
101
|
+
C.LAN_F_DEVICE_ID: self.device_id,
|
|
102
|
+
C.LAN_F_MAC: self.mac,
|
|
103
|
+
}
|
|
104
|
+
msg.update(extra)
|
|
105
|
+
return json.dumps(msg).encode("utf-8")
|
|
106
|
+
|
|
107
|
+
def _upsert_node(self, remote_ip, msg, source="udp"):
|
|
108
|
+
"""按 device_id 去重地插入/更新节点。返回 True 表示是新节点。"""
|
|
109
|
+
device_id = msg.get(C.LAN_F_DEVICE_ID, "") or ""
|
|
110
|
+
hostname = msg.get(C.LAN_F_HOSTNAME, remote_ip) or remote_ip
|
|
111
|
+
mac = msg.get(C.LAN_F_MAC, "") or ""
|
|
112
|
+
is_new = False
|
|
113
|
+
with self.lock:
|
|
114
|
+
if device_id:
|
|
115
|
+
for old_ip in list(self.nodes.keys()):
|
|
116
|
+
if old_ip == remote_ip:
|
|
117
|
+
continue
|
|
118
|
+
if self.nodes[old_ip].get(C.LAN_F_DEVICE_ID) == device_id:
|
|
119
|
+
del self.nodes[old_ip]
|
|
120
|
+
if remote_ip in self.nodes:
|
|
121
|
+
self.nodes[remote_ip][C.LAN_F_HOSTNAME] = hostname
|
|
122
|
+
self.nodes[remote_ip][C.LAN_F_DEVICE_ID] = device_id
|
|
123
|
+
self.nodes[remote_ip][C.LAN_F_MAC] = mac
|
|
124
|
+
self.nodes[remote_ip]["last_seen"] = time.time()
|
|
125
|
+
self.nodes[remote_ip]["heartbeat_fail"] = 0
|
|
126
|
+
else:
|
|
127
|
+
self.nodes[remote_ip] = {
|
|
128
|
+
C.LAN_F_HOSTNAME: hostname,
|
|
129
|
+
C.LAN_F_DEVICE_ID: device_id,
|
|
130
|
+
C.LAN_F_MAC: mac,
|
|
131
|
+
"last_seen": time.time(),
|
|
132
|
+
"source": source,
|
|
133
|
+
"heartbeat_fail": 0,
|
|
134
|
+
}
|
|
135
|
+
is_new = True
|
|
136
|
+
return is_new
|
|
137
|
+
|
|
138
|
+
def get_nodes(self):
|
|
139
|
+
with self.lock:
|
|
140
|
+
return dict(self.nodes)
|
|
141
|
+
|
|
142
|
+
# ---------- 广播 ----------
|
|
143
|
+
|
|
144
|
+
def _start_broadcasters(self):
|
|
145
|
+
for ip_str in self.my_ips:
|
|
146
|
+
try:
|
|
147
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
148
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
|
149
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
150
|
+
sock.bind((ip_str, 0))
|
|
151
|
+
self.broadcast_sockets.append(sock)
|
|
152
|
+
threading.Thread(target=self._udp_broadcaster_on_sock,
|
|
153
|
+
args=(sock, ip_str), daemon=True).start()
|
|
154
|
+
except Exception as e:
|
|
155
|
+
self.log("[警告] 无法为 %s 创建广播 socket: %s" % (ip_str, e))
|
|
156
|
+
try:
|
|
157
|
+
global_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
158
|
+
global_sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
|
159
|
+
global_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
160
|
+
self.broadcast_sockets.append(global_sock)
|
|
161
|
+
threading.Thread(target=self._udp_broadcaster_on_sock,
|
|
162
|
+
args=(global_sock, None), daemon=True).start()
|
|
163
|
+
except Exception as e:
|
|
164
|
+
self.log("[警告] 全局广播 socket 创建失败: %s" % e)
|
|
165
|
+
|
|
166
|
+
def _udp_broadcaster_on_sock(self, sock, bind_ip):
|
|
167
|
+
"""周期性广播搜索:前 5 秒每 0.2s,之后每 20s。"""
|
|
168
|
+
start = time.time()
|
|
169
|
+
while self.running:
|
|
170
|
+
if not self.pausing_network:
|
|
171
|
+
try:
|
|
172
|
+
sock.sendto(self._build_msg(**{C.LAN_K_DISCOVERY: True}),
|
|
173
|
+
("255.255.255.255", self.udp_port))
|
|
174
|
+
except Exception:
|
|
175
|
+
pass
|
|
176
|
+
elapsed = time.time() - start
|
|
177
|
+
if elapsed < 5:
|
|
178
|
+
time.sleep(0.2)
|
|
179
|
+
else:
|
|
180
|
+
time.sleep(self.discover_interval)
|
|
181
|
+
|
|
182
|
+
def broadcast_search(self):
|
|
183
|
+
"""广播搜索:向所有广播地址爆发式发送探测包,等待设备回复。"""
|
|
184
|
+
self.log("[广播搜索] 发送广播探测,等待设备回复...")
|
|
185
|
+
msg = self._build_msg(**{C.LAN_K_DISCOVERY: True})
|
|
186
|
+
for _ in range(3):
|
|
187
|
+
for bcast_addr in self.broadcast_addrs:
|
|
188
|
+
try:
|
|
189
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
190
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
|
191
|
+
s.settimeout(1)
|
|
192
|
+
s.sendto(msg, (bcast_addr, self.udp_port))
|
|
193
|
+
s.close()
|
|
194
|
+
except Exception:
|
|
195
|
+
pass
|
|
196
|
+
time.sleep(0.2)
|
|
197
|
+
self.log("[广播搜索] 完成")
|
|
198
|
+
|
|
199
|
+
# ---------- 接收 ----------
|
|
200
|
+
|
|
201
|
+
def _udp_listener(self):
|
|
202
|
+
sock4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
203
|
+
sock4.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
204
|
+
sock4.bind(("", self.udp_port))
|
|
205
|
+
sock4.settimeout(1.0)
|
|
206
|
+
self.listen_sockets.append(sock4)
|
|
207
|
+
try:
|
|
208
|
+
sock6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
|
|
209
|
+
sock6.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
210
|
+
sock6.bind(("::", self.udp_port))
|
|
211
|
+
sock6.settimeout(1.0)
|
|
212
|
+
self.listen_sockets.append(sock6)
|
|
213
|
+
except Exception:
|
|
214
|
+
sock6 = None
|
|
215
|
+
|
|
216
|
+
def listen_sock(sock, is_ipv6=False):
|
|
217
|
+
while self.running:
|
|
218
|
+
try:
|
|
219
|
+
data, addr = sock.recvfrom(1024)
|
|
220
|
+
msg = json.loads(data.decode("utf-8"))
|
|
221
|
+
remote_ip = addr[0]
|
|
222
|
+
if remote_ip in self.my_ips or remote_ip in self.my_ips_v6:
|
|
223
|
+
continue
|
|
224
|
+
if remote_ip in ("127.0.0.1", "::1", "0.0.0.0"):
|
|
225
|
+
continue
|
|
226
|
+
if msg.get(C.LAN_F_DEVICE_ID) and msg.get(C.LAN_F_DEVICE_ID) == self.device_id:
|
|
227
|
+
continue
|
|
228
|
+
if msg.get(C.LAN_K_BYE):
|
|
229
|
+
with self.lock:
|
|
230
|
+
removed = remote_ip in self.nodes
|
|
231
|
+
self.nodes.pop(remote_ip, None)
|
|
232
|
+
if removed and self.on_node_gone:
|
|
233
|
+
try:
|
|
234
|
+
self.on_node_gone(remote_ip)
|
|
235
|
+
except Exception:
|
|
236
|
+
pass
|
|
237
|
+
continue
|
|
238
|
+
if msg.get(C.LAN_K_HEARTBEAT):
|
|
239
|
+
with self.lock:
|
|
240
|
+
if remote_ip in self.nodes:
|
|
241
|
+
self.nodes[remote_ip]["last_seen"] = time.time()
|
|
242
|
+
reply = self._build_msg(**{C.LAN_K_HEARTBEAT: True, C.LAN_K_ACK: True})
|
|
243
|
+
sock.sendto(reply, (remote_ip, self.udp_port))
|
|
244
|
+
continue
|
|
245
|
+
is_new = self._upsert_node(remote_ip, msg, source="udp")
|
|
246
|
+
if is_new:
|
|
247
|
+
hn = msg.get(C.LAN_F_HOSTNAME, remote_ip)
|
|
248
|
+
self.log("[发现] 新设备 %s (%s)" % (hn, remote_ip))
|
|
249
|
+
if self.on_new_node:
|
|
250
|
+
try:
|
|
251
|
+
self.on_new_node(remote_ip, msg)
|
|
252
|
+
except Exception:
|
|
253
|
+
pass
|
|
254
|
+
if not msg.get(C.LAN_K_REPLY):
|
|
255
|
+
sock.sendto(self._build_msg(**{C.LAN_K_REPLY: True}),
|
|
256
|
+
(remote_ip, self.udp_port))
|
|
257
|
+
except socket.timeout:
|
|
258
|
+
continue
|
|
259
|
+
except OSError:
|
|
260
|
+
break
|
|
261
|
+
except Exception:
|
|
262
|
+
pass
|
|
263
|
+
try:
|
|
264
|
+
sock.close()
|
|
265
|
+
except Exception:
|
|
266
|
+
pass
|
|
267
|
+
|
|
268
|
+
threading.Thread(target=listen_sock, args=(sock4, False), daemon=True).start()
|
|
269
|
+
if sock6:
|
|
270
|
+
threading.Thread(target=listen_sock, args=(sock6, True), daemon=True).start()
|
|
271
|
+
|
|
272
|
+
# ---------- 扫描 ----------
|
|
273
|
+
|
|
274
|
+
def _scan_listener(self):
|
|
275
|
+
"""扫描端口监听:收到扫描探测则回 scan_reply。"""
|
|
276
|
+
try:
|
|
277
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
278
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
279
|
+
sock.bind(("", self.scan_port))
|
|
280
|
+
sock.settimeout(1.0)
|
|
281
|
+
self.listen_sockets.append(sock)
|
|
282
|
+
except Exception as e:
|
|
283
|
+
self.log("[扫描] 监听失败: %s" % e)
|
|
284
|
+
return
|
|
285
|
+
while self.running:
|
|
286
|
+
try:
|
|
287
|
+
data, addr = sock.recvfrom(1024)
|
|
288
|
+
msg = json.loads(data.decode("utf-8"))
|
|
289
|
+
remote_ip = addr[0]
|
|
290
|
+
if msg.get(C.LAN_F_DEVICE_ID) and msg.get(C.LAN_F_DEVICE_ID) == self.device_id:
|
|
291
|
+
continue
|
|
292
|
+
if remote_ip in self.my_ips or remote_ip in ("127.0.0.1", "::1"):
|
|
293
|
+
continue
|
|
294
|
+
sock.sendto(self._build_msg(**{C.LAN_K_SCAN_REPLY: True}),
|
|
295
|
+
(remote_ip, self.scan_port))
|
|
296
|
+
is_new = self._upsert_node(remote_ip, msg, source="scan")
|
|
297
|
+
if is_new and self.on_new_node:
|
|
298
|
+
try:
|
|
299
|
+
self.on_new_node(remote_ip, msg)
|
|
300
|
+
except Exception:
|
|
301
|
+
pass
|
|
302
|
+
except socket.timeout:
|
|
303
|
+
continue
|
|
304
|
+
except OSError:
|
|
305
|
+
break
|
|
306
|
+
except Exception:
|
|
307
|
+
pass
|
|
308
|
+
try:
|
|
309
|
+
sock.close()
|
|
310
|
+
except Exception:
|
|
311
|
+
pass
|
|
312
|
+
|
|
313
|
+
def scan_subnet(self, network_cidr, callback=None):
|
|
314
|
+
"""扫描一个子网(IPv4),发现的对端通过 on_new_node 回调。"""
|
|
315
|
+
import ipaddress
|
|
316
|
+
try:
|
|
317
|
+
net = ipaddress.IPv4Network(network_cidr, strict=False)
|
|
318
|
+
except Exception:
|
|
319
|
+
return
|
|
320
|
+
self.scanning = True
|
|
321
|
+
msg = self._build_msg(**{C.LAN_K_SCAN_REPLY: True})
|
|
322
|
+
try:
|
|
323
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
324
|
+
sock.settimeout(0.3)
|
|
325
|
+
for host in net.hosts():
|
|
326
|
+
if not self.running:
|
|
327
|
+
break
|
|
328
|
+
try:
|
|
329
|
+
sock.sendto(msg, (str(host), self.scan_port))
|
|
330
|
+
except Exception:
|
|
331
|
+
pass
|
|
332
|
+
except Exception as e:
|
|
333
|
+
self.log("[扫描] 子网 %s 扫描异常: %s" % (network_cidr, e))
|
|
334
|
+
finally:
|
|
335
|
+
try:
|
|
336
|
+
sock.close()
|
|
337
|
+
except Exception:
|
|
338
|
+
pass
|
|
339
|
+
self.scanning = False
|
|
340
|
+
|
|
341
|
+
def add_manual_node(self, ip, hostname=None):
|
|
342
|
+
"""手动添加节点,并主动发探测让对方也发现我们。"""
|
|
343
|
+
with self.lock:
|
|
344
|
+
self.nodes[ip] = {
|
|
345
|
+
"hostname": hostname or ip,
|
|
346
|
+
"last_seen": time.time(),
|
|
347
|
+
"source": "manual",
|
|
348
|
+
"heartbeat_fail": 0,
|
|
349
|
+
}
|
|
350
|
+
self._send_probe_to(ip)
|
|
351
|
+
self.log("[手动添加] 已向 %s 发送探测" % ip)
|
|
352
|
+
|
|
353
|
+
def _send_probe_to(self, ip):
|
|
354
|
+
try:
|
|
355
|
+
family = socket.AF_INET6 if ":" in ip else socket.AF_INET
|
|
356
|
+
s = socket.socket(family, socket.SOCK_DGRAM)
|
|
357
|
+
s.settimeout(2)
|
|
358
|
+
s.sendto(self._build_msg(), (ip, self.udp_port))
|
|
359
|
+
try:
|
|
360
|
+
s.sendto(self._build_msg(**{C.LAN_K_SCAN_REPLY: True}), (ip, self.scan_port))
|
|
361
|
+
except Exception:
|
|
362
|
+
pass
|
|
363
|
+
s.close()
|
|
364
|
+
except Exception as e:
|
|
365
|
+
self.log("[手动添加] 向 %s 发送探测失败: %s" % (ip, e))
|
|
366
|
+
|
|
367
|
+
# ---------- 下线 ----------
|
|
368
|
+
|
|
369
|
+
def _send_bye(self):
|
|
370
|
+
try:
|
|
371
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
372
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
|
373
|
+
s.sendto(self._build_msg(**{C.LAN_K_BYE: True}), ("255.255.255.255", self.udp_port))
|
|
374
|
+
s.close()
|
|
375
|
+
except Exception:
|
|
376
|
+
pass
|