3. 多屏异显硬件基础

多屏互动系统里,最让人头疼的问题是什么?我个人觉得,是「设备找不到设备」。你想想看,手机想投屏到电视,电视得先知道手机在哪;电脑要扩展屏幕到平板,平板得先被电脑发现。这个「发现」的过程,就是本章要聊的核心——局域网设备发现协议。

说白了,设备发现就是让网络里的各个节点互相打个招呼:「嘿,我在这,我能干这些事。」没有这个机制,后面的连接、传输、同步全是空谈。我在做第一个多屏原型时,就栽在这个坑里——设备死活发现不了对方,折腾了两天才发现是广播地址写错了。

3.1 局域网设备发现机制

局域网设备发现,本质上解决两个问题:

  • 谁在线?——网络里有哪些设备处于可连接状态。
  • 能干什么?——每个设备支持哪些功能(投屏、扩展、镜像、控制)。

常见的发现机制有三种,我列个表对比一下:

机制 原理 优点 缺点 适用场景
UDP 广播 向子网广播地址发消息,所有设备都能收到 实现简单,无需中心节点 广播风暴风险,跨网段无效 小型局域网,设备数 < 50
UDP 组播 加入特定组播组,只向组内成员发送 节省带宽,可跨网段 需要 IGMP 支持,配置稍复杂 中型网络,设备数 50~200
mDNS/DNS-SD 基于多播 DNS 的服务发现 零配置,苹果生态原生支持 依赖系统服务,延迟不稳定 跨平台、消费级场景

嗯,这里要注意:不要一上来就选最复杂的。我见过不少团队直接上 mDNS,结果在 Windows 上兼容性出问题,最后又改回 UDP 广播。对于多屏互动这种小规模局域网场景(通常 2~10 台设备),UDP 广播是最务实的选择。

3.2 基于UDP广播的发现协议

UDP 广播的原理很简单:设备往 255.255.255.255 或子网广播地址发一个数据包,同一子网内所有设备都能收到。收到后,设备判断是不是给自己的消息,是就回复,不是就丢弃。

我习惯把发现协议分成两个阶段:

  1. 探测阶段(Probe)——新设备上线,主动发广播:「有人吗?」
  2. 应答阶段(Response)——在线设备收到后,回复:「我在,这是我的信息。」

这里有个关键点:探测消息要周期性发送。为什么?因为设备可能随时上线或下线。我一般设置 3~5 秒发一次探测,连续 3 次没收到应答就认为设备离线。这个「心跳机制」能保证设备列表实时更新。

避坑指南:我曾经把探测间隔设成 1 秒,结果 20 台设备同时在线时,网络瞬间被广播包塞满,丢包率飙升。后来改成 3 秒 + 随机偏移 0~500ms,问题就解决了。广播不是越快越好,要给网络留喘气的时间。

3.3 自定义协议格式

UDP 广播只是传输方式,真正要落地,还得定义一套协议格式。说白了,就是约定好「消息长什么样,每个字段什么意思」。

我设计过一套协议,结构如下:

+----------------+----------------+----------------+----------------+
|   Magic Number |   Version      |   Message Type |   Payload Len  |
|    (4 bytes)   |    (1 byte)    |    (1 byte)    |    (2 bytes)   |
+----------------+----------------+----------------+----------------+
|                        Payload (变长)                              |
|              (设备信息、能力列表、时间戳等)                          |
+-------------------------------------------------------------------+

各字段说明:

  • Magic Number:固定值 0x4D534443(即 "MSDC" 的 ASCII),用来快速校验是不是本协议的消息。收到包先检查这个,不对直接丢弃。
  • Version:协议版本号,方便后续升级兼容。我一般从 0x01 开始。
  • Message Type:消息类型,比如 0x01 表示探测,0x02 表示应答,0x03 表示离线通知。
  • Payload Len:负载长度,单位字节。接收方根据这个值知道要读多少数据。
  • Payload:实际数据,用 JSON 或 Protobuf 编码。我推荐 JSON,调试方便,解析简单。

Payload 的 JSON 结构示例:

{
  "device_id": "TV-LivingRoom-001",
  "device_name": "客厅电视",
  "device_type": "display",
  "ip": "192.168.1.100",
  "port": 9000,
  "capabilities": ["screen_mirror", "extend_display", "remote_control"],
  "timestamp": 1712345678
}
核心设计原则:协议要「自描述」。也就是说,收到一个包,不需要额外上下文就能解析出完整信息。device_id、ip、port 这些字段缺一不可,否则对方没法主动连接你。

3.4 完整发现流程

把上面这些串起来,一个完整的设备发现流程是这样的:

  1. 设备 A 上线,启动 UDP 监听端口 9000。
  2. 设备 A 每 3 秒向 255.255.255.255:9000 发送探测包(Type=0x01)。
  3. 设备 B 收到探测包,校验 Magic Number 和 Version,通过后解析 Payload。
  4. 设备 B 发现是探测消息,立即回复应答包(Type=0x02),带上自己的设备信息。
  5. 设备 A 收到应答,更新本地设备列表,显示「客厅电视已连接」。
  6. 设备 B 下线时,发送离线通知包(Type=0x03),设备 A 收到后移除该设备。

这个流程看起来简单,但实际落地时有很多细节。比如:收到重复的应答怎么办?我习惯用 device_id 做去重,同一个 ID 只保留最新一条记录。再比如:设备突然断电,没来得及发离线通知怎么办?那就靠心跳超时——连续 9 秒没收到心跳,就认为设备离线。

注意:UDP 广播包默认不会被路由器转发到其他子网。如果你的设备分布在不同的 VLAN 或子网里,广播发现会失效。这时候要么用组播,要么在网关上配置 UDP 广播中继。我在一个跨楼层部署的项目里就吃过这个亏,最后加了一台广播中继服务器才搞定。

3.5 协议设计中的常见陷阱

做协议设计这么多年,我踩过不少坑。列几个典型的:

  • Payload 太大——UDP 单包最大 65535 字节,但实际网络 MTU 通常是 1500 字节。超过 MTU 的包会被分片,分片丢失会导致整个包作废。所以 Payload 最好控制在 1400 字节以内。
  • 没有版本协商——协议升级后,老设备发来的包新设备不认识。我建议在应答包里带上「支持的最高版本」,双方协商使用共同版本。
  • 忽略安全——UDP 广播是明文传输,任何人都能伪造发现包。生产环境至少要加一个简单的 Token 校验,防止恶意设备混入。
  • 端口写死——万一端口被占用,整个发现机制就废了。我习惯让设备尝试绑定端口,失败则自动 +1 重试,并在 Payload 里告知实际端口。

3.6 本章知识结构图

下面这张 SVG 图把本章的核心逻辑串起来了,你可以对照着理解:

设备发现协议核心结构 局域网发现机制 UDP 广播 / 组播 / mDNS 自定义协议格式 Magic + Version + Type + Payload 完整发现流程 探测 → 应答 → 心跳 → 离线 探测阶段 (Probe) Type=0x01 周期 3s + 随机偏移 广播地址 255.255.255.255 应答阶段 (Response) Type=0x02 携带设备信息 + 能力列表 单播回复 离线通知 (Bye) Type=0x03 主动发送 心跳超时 9s 被动离线 核心原则:自描述、轻量、心跳保活、版本兼容 Payload 控制在 1400 字节以内,避免 IP 分片

这张图从左到右展示了发现机制、协议格式、完整流程三个核心模块,以及探测、应答、离线三个具体阶段。你可以把它当作本章的「知识地图」,写代码时对照着来,不容易漏掉关键环节。

3.7 小结

设备发现是多屏互动的第一步,也是最重要的一步。UDP 广播虽然「土」,但在小规模局域网里足够稳定、足够快。自定义协议时,记得把 Magic Number、版本号、消息类型、负载长度这四个字段作为标配,Payload 用 JSON 方便调试,后期再考虑换成 Protobuf 压缩体积。