本文描述仓库当前 PROTOCOL_VERSION = 2 的线协议。v2 在原有扩展屏信令上加入键鼠控制和剪贴板消息,不与 v1 混用。
| 用途 | 方向 | 默认端口 | 传输 | 上限 |
|---|---|---|---|---|
| 子端发现 | Windows 子端 → IPv4 广播 | UDP 47771 |
单个 UTF-8 JSON 对象 | 解析上限 4 KiB |
| WebRTC 信令 | Mac 主端 → Windows 子端 | TCP 47772 |
明文 WebSocket ws://、UTF-8 JSON |
单消息 256 KiB |
| 视频 | Mac 主端 → Windows 子端 | 动态 | WebRTC ICE/DTLS-SRTP | 由 WebRTC 决定 |
当前没有 HTTP API、云服务、STUN 或 TURN。信令端口可以在子端设置中修改,发现端口是固定协议常量。
- 除
welcome外,信令消息都包含整数protocol: 2和受支持的type。 - 接收方严格要求协议版本相等,不进行版本协商。
- JSON 顶层必须是普通对象,不能是数组、标量或带特殊原型的内部对象。
- 名称去除控制字符、前后空白,并截断到 64 个字符。
- 无效、超限或未知信令导致子端以 WebSocket 关闭码
1008断开。 - 子端只保留一个活动 WebSocket 会话;第二个连接以
1013(“子端当前正在使用”)关闭。 - 正常主动断开使用
1000;服务停止使用1001。
Windows 子端启动后立即广播,之后默认每 1500 ms 向 255.255.255.255 及每个活动 IPv4 网卡的定向广播地址发送:
{
"type": "lanextend.receiver",
"protocol": 2,
"id": "5dd9a8d2-f997-4f85-b1e5-f086d1164441",
"name": "会议室 Windows",
"port": 47772,
"platform": "win32",
"capabilities": ["video", "fullscreen", "input", "clipboard"],
"display": { "width": 1920, "height": 1080, "scaleFactor": 1 }
}字段约束:
| 字段 | 要求 |
|---|---|
type |
必须等于 lanextend.receiver |
protocol |
必须等于 2 |
id |
非空字符串,最长 128;正常实现首次运行生成 UUID 并持久化 |
name |
非空字符串,最长 64 |
port |
整数 1–65535 |
platform |
当前实现发送 win32;其他值被归一为 unknown |
capabilities |
可选字符串数组;接收方最多保留前 8 项 |
display |
Windows 主显示器物理像素宽高和缩放,用于主端布局与绝对指针坐标 |
Mac 端把 UDP 数据报的来源地址作为设备 host,不会采用报文中自报的 IP。设备超过 6 秒未再广播即从在线列表移除,但已记忆设备仍以离线状态保留。
广播可被同网设备伪造;id、名称和来源 IPv4 都不是可信身份。
主端连接 ws://<receiver-private-ip>:<port>。连接建立后,子端首先发送不经过通用信令解析器的欢迎消息:
{
"type": "welcome",
"protocol": 2,
"receiver": {
"id": "5dd9a8d2-f997-4f85-b1e5-f086d1164441",
"name": "会议室 Windows",
"port": 47772,
"capabilities": ["video", "fullscreen", "input", "clipboard"],
"display": { "width": 1920, "height": 1080, "scaleFactor": 1 }
}
}主端从 WebSocket open 起等待 welcome,当前握手超时为 10 秒。
主端先用 welcome.receiver.id/name/port 更新当前设备,并把手动添加时的临时 ID 替换为子端持久 UUID。默认“创建扩展屏”模式还会用这个真实 UUID 派生稳定显示 serial。然后主端发送身份介绍;这里的 hostId 与 name 只用于会话标识,不构成认证:
{
"type": "hello",
"protocol": 2,
"hostId": "mac-host-id",
"name": "设计部 Mac"
}hostId 必须是最长 256 的非空字符串,name 最长 64。
由主端发送 SDP offer:
{
"type": "offer",
"protocol": 2,
"sdp": {
"type": "offer",
"sdp": "v=0\r\n..."
}
}由子端发送 SDP answer;结构同上,但顶层和内层 type 都是 answer。SDP 字符串最长 220000 个字符。
双向发送 ICE candidate:
{
"type": "ice",
"protocol": 2,
"candidate": {
"candidate": "candidate:...",
"sdpMid": "0",
"sdpMLineIndex": 0
}
}结束候选收集可以发送 candidate: null。非空 candidate 必须是对象且 candidate.candidate 为最长 8192 的非空字符串;其他候选字段交给 WebRTC 实现处理。
用于应用层存活探测,时间戳必须是非负有限数。主端默认每 5 秒发送一次:
{"type":"ping","protocol":2,"timestamp":1786320000000}接收 ping 的一端使用相同时间戳回复 pong,主端据此展示应用层 RTT。连续约 15 秒没有 pong 时,主端把会话视为失联。它不是时钟同步,也不能证明对端身份。
{
"type": "disconnect",
"protocol": 2,
"reason": "主端主动断开"
}reason 可省略;存在时必须是 UTF-8 编码后不超过 120 字节的字符串。子端发出 WebSocket close frame 时还会按 UTF-8 安全边界截断到协议允许的 123 字节,不会截断多字节字符。
输入模式在 hello 后由主端发送:
{
"type": "control",
"protocol": 2,
"action": "share-start",
"clipboard": true,
"screen": { "width": 1920, "height": 1080 }
}子端启动 Windows 输入 helper 后回复 share-ready,并返回实际主屏宽高。运行期间 active/inactive 表示控制权是否已跨到 Windows;share-stop 结束共享,error 携带最长 512 字符的错误说明。
鼠标、按键和释放事件使用 input:
{"type":"input","protocol":2,"event":{"kind":"pointer","x":960,"y":540}}
{"type":"input","protocol":2,"event":{"kind":"key","vk":65,"down":true}}
{"type":"input","protocol":2,"event":{"kind":"releaseAll"}}支持的 kind 为 pointer/button/wheel/key/releaseAll。坐标是 Windows 主显示器内的逻辑像素;键盘使用 Windows virtual-key code。鼠标移动在 Mac 端合并为约 8 ms 一批,按键、点击或滚轮前会先刷新待发送位置。
纯文本剪贴板双向发送:
{"type":"clipboard","protocol":2,"text":"同步文本","revision":"mac-id:12"}文本 UTF-8 上限为 128 KiB;revision 用于观测,实际回环抑制以本地最近内容为准。不支持文件、图片和富文本。
sequenceDiagram
participant M as Mac 主端
participant W as Windows 子端
M->>W: 用户选择设备并点“扩展”,WebSocket connect
W-->>M: welcome(protocol=2)
M->>M: 核对子端真实 UUID并更新记忆
M->>W: hello(protocol=2)
M->>M: 自动创建/定位虚拟显示捕获源
M->>W: offer(SDP)
W-->>M: answer(SDP)
par ICE 交换
M->>W: ice(candidate)
and
W-->>M: ice(candidate)
end
M->>W: WebRTC video track
loop 会话保活
M->>W: ping(timestamp)
W-->>M: pong(timestamp)
end
M->>W: disconnect(reason)
M-xW: close WebSocket/WebRTC
实际 ICE 与 SDP 消息可能交错;当前双端会缓存远端描述设置前到达的候选。RTCPeerConnection 使用空 iceServers,所以只有局域网 host candidates,没有 STUN/TURN/NAT 中继。主端使用 sendonly 视频 transceiver,优先 H.264、以 VP8 兜底,并把设置中的码率/FPS作为 sender 上限目标;协商/编码器最终值以运行 stats 为准。offer 发出后当前最终协商超时为 20 秒。
键鼠模式用同一套 welcome → hello → ping/pong → disconnect 外壳,但以 share-start/share-ready 代替 SDP/ICE/WebRTC;它不发送视频。
自动重连属于客户端策略,不改变线协议:只有网络/异常断开才按约 1.6、3.2、6.4、12 秒退避(之后封顶 12 秒);用户主动断开、收到显式 disconnect,或 WebSocket 以 1000/1008 结束时不自动重连。
主端 GUI 的手动目标只接受以下 IPv4:
10.0.0.0/8172.16.0.0/12192.168.0.0/16127.0.0.0/8169.254.0.0/16
这只是减少误连公网的输入校验,不是访问控制。当前不接受 IPv6、DNS 主机名或公网 IPv4。
| 属性 | 当前状态 |
|---|---|
| 视频传输机密性/完整性 | 由 WebRTC DTLS-SRTP 提供 |
| 发现真实性 | 无 |
| 设备身份认证 | 无 |
| WebSocket 信令机密性/完整性 | 无 TLS,仅有格式校验 |
| 重放保护 | 无应用层保证 |
| 授权 | 仅“主端主动连接 + 子端单会话”的流程限制 |
| DoS 缓解 | 消息大小、字段校验和单会话限制;不构成完整防护 |
因此协议 v2 只适用于用户自己的受控局域网。当前产品选择不实现账号、配对或 TLS;如果未来扩大到共享网络,应另行提升协议并设计设备身份和加密。
以下变更必须提升 PROTOCOL_VERSION:
- 修改既有字段含义、必填性、范围或信令方向;
- 更换媒体协商流程或安全模型;
- 增加必须被旧端理解的消息;
- 改变端口/发现模型且没有兼容回退。
仅增加可忽略的 GUI 元数据也应先确认当前解析器是否会保留;v2 解析器只验证已知关键字段,不提供通用扩展协商。