4. Android多屏显示模式

消息协议,说白了就是设备之间沟通的「共同语言」。多屏互动系统里,手机、平板、电视、投影仪要能互相理解,就得有一套大家都遵守的规矩。我个人习惯,先把消息格式定死,再谈别的。

4.1 为什么选 JSON?

你可能要问,为什么不直接用二进制?嗯,这里有个取舍。二进制确实省带宽,但调试起来太痛苦了。我记得有一次排查一个投屏问题,抓包出来全是乱码,折腾了一整天才发现是字节序搞反了。

JSON 的好处很明显:

  • 人类可读 —— 出问题了,打开日志一眼就能看出哪不对
  • 跨语言 —— C++、Java、Python、JavaScript 都能解析
  • 生态成熟 —— 几乎每种语言都有现成的库

当然,JSON 也有缺点,比如数据冗余大、解析慢。但在多屏互动这种场景下,消息量不大,JSON 完全够用。真要追求极致性能,可以后续再优化成 Protocol Buffers。

4.2 消息格式定义

我设计消息格式时,遵循一个原则:头尾固定,中间灵活。每条消息都包含一个固定结构的头部,和一个可变的数据体。

核心消息结构:

{
  "version": "1.0",
  "msgId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "type": "SCREEN_CAST_START",
  "timestamp": 1699000000123,
  "source": "phone_01",
  "target": "tv_living_room",
  "payload": {
    // 具体业务数据
  }
}

每个字段的含义:

字段 类型 说明
version string 协议版本号,用于兼容升级
msgId string (UUID) 全局唯一消息ID,用于去重和追踪
type string 消息类型枚举值
timestamp number (毫秒) 消息生成时间戳
source string 发送方设备ID
target string 接收方设备ID(广播时可为空)
payload object 业务数据体

小技巧:msgId 一定要用 UUID,别用自增ID。我曾经遇到过两个设备同时发消息,自增ID冲突导致消息丢失,排查了好久才发现是ID重复了。

4.3 消息类型枚举

消息类型我用枚举来管理,这样代码里不会出现魔法字符串。我习惯把类型分成几大类:

// 设备发现与连接
DEVICE_DISCOVER        = "DEVICE_DISCOVER"        // 发现设备
DEVICE_JOIN            = "DEVICE_JOIN"            // 设备加入
DEVICE_LEAVE           = "DEVICE_LEAVE"           // 设备离开
DEVICE_HEARTBEAT       = "DEVICE_HEARTBEAT"       // 心跳

// 投屏控制
SCREEN_CAST_START      = "SCREEN_CAST_START"      // 开始投屏
SCREEN_CAST_STOP       = "SCREEN_CAST_STOP"       // 停止投屏
SCREEN_CAST_PAUSE      = "SCREEN_CAST_PAUSE"      // 暂停投屏
SCREEN_CAST_RESUME     = "SCREEN_CAST_RESUME"     // 恢复投屏

// 媒体控制
MEDIA_PLAY             = "MEDIA_PLAY"             // 播放
MEDIA_PAUSE            = "MEDIA_PAUSE"            // 暂停
MEDIA_SEEK             = "MEDIA_SEEK"             // 跳转
MEDIA_VOLUME           = "MEDIA_VOLUME"           // 音量

// 系统控制
SYSTEM_REBOOT          = "SYSTEM_REBOOT"          // 重启
SYSTEM_UPDATE          = "SYSTEM_UPDATE"          // 更新
SYSTEM_ERROR           = "SYSTEM_ERROR"           // 错误报告

你可能会问,为什么不用数字枚举?数字确实省空间,但可读性太差了。日志里看到 type: 3,你还得去查表才知道是什么意思。字符串虽然长一点,但一眼就能看懂。

4.4 消息序列化与反序列化

序列化就是把对象转成 JSON 字符串,反序列化就是反过来。听起来简单,但坑不少。

4.4.1 序列化实现

我用 JavaScript 举个例子,其他语言思路一样:

class Message {
  constructor(type, payload, source, target) {
    this.version = "1.0";
    this.msgId = this.generateUUID();
    this.type = type;
    this.timestamp = Date.now();
    this.source = source;
    this.target = target || "";
    this.payload = payload;
  }

  generateUUID() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
      const r = Math.random() * 16 | 0;
      const v = c === 'x' ? r : (r & 0x3 | 0x8);
      return v.toString(16);
    });
  }

  // 序列化
  serialize() {
    return JSON.stringify(this);
  }

  // 反序列化(静态方法)
  static deserialize(jsonStr) {
    try {
      const obj = JSON.parse(jsonStr);
      // 校验必填字段
      if (!obj.type || !obj.msgId) {
        throw new Error("消息格式错误:缺少必填字段");
      }
      return obj;
    } catch (e) {
      console.error("反序列化失败:", e.message);
      return null;
    }
  }
}

// 使用示例
const msg = new Message(
  "SCREEN_CAST_START",
  { resolution: "1920x1080", fps: 60 },
  "phone_01",
  "tv_living_room"
);

const jsonStr = msg.serialize();
console.log(jsonStr);
// 输出: {"version":"1.0","msgId":"a1b2...","type":"SCREEN_CAST_START",...}

const parsed = Message.deserialize(jsonStr);
console.log(parsed.type); // SCREEN_CAST_START

注意:反序列化一定要加 try-catch。网络传输中什么情况都可能发生,收到一段损坏的 JSON 是常有的事。不加异常处理,整个程序可能直接崩溃。

4.4.2 反序列化的校验

我曾经在项目里吃过亏:反序列化时没做校验,结果收到一条缺少 type 字段的消息,程序直接抛空指针异常。从那以后,我每次反序列化都会做三层校验:

  1. 格式校验 —— 是不是合法的 JSON
  2. 必填字段校验 —— type、msgId、version 不能为空
  3. 业务逻辑校验 —— type 值是否在枚举范围内
function validateMessage(obj) {
  // 第一层:格式校验
  if (typeof obj !== 'object' || obj === null) {
    return { valid: false, reason: "消息格式错误" };
  }

  // 第二层:必填字段
  const required = ['version', 'msgId', 'type', 'timestamp'];
  for (const field of required) {
    if (obj[field] === undefined || obj[field] === null) {
      return { valid: false, reason: `缺少必填字段: ${field}` };
    }
  }

  // 第三层:类型枚举校验
  const validTypes = [
    'DEVICE_DISCOVER', 'DEVICE_JOIN', 'DEVICE_LEAVE',
    'SCREEN_CAST_START', 'SCREEN_CAST_STOP',
    'MEDIA_PLAY', 'MEDIA_PAUSE'
    // ... 完整列表
  ];
  if (!validTypes.includes(obj.type)) {
    return { valid: false, reason: `未知消息类型: ${obj.type}` };
  }

  return { valid: true };
}

4.5 消息协议的整体流程

说了这么多,咱们用一张图把整个流程串起来:

消息协议处理流程 发送方设备 序列化 (JSON.stringify) 网络传输 (TCP/UDP) 反序列化 (JSON.parse) 接收方设备 三层校验 关键点: • 发送方:构造 Message 对象 → 序列化 → 发送 • 接收方:接收数据 → 反序列化 → 校验 → 处理

4.6 避坑指南

最后,分享几个我踩过的坑:

坑1:时间戳精度

我曾经用秒级时间戳,结果两个消息在同一秒内发出,导致顺序错乱。后来统一改成毫秒级,问题解决。

坑2:字段命名风格

团队里有人用 camelCase,有人用 snake_case,解析的时候各种报错。我建议统一用 camelCase,因为 JavaScript 和 Java 都默认支持。

坑3:超大 payload

有一次投屏传了一张 4K 截图,JSON 字符串直接 10MB+,网络传输超时。后来我规定:payload 超过 1MB 的数据,先压缩再传输,或者改用分片传输。