Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

23 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

TeamViewRelay-Protocol

TeamViewRelay 的共享协议源。

目录

  • proto/teamviewer/v1/teamviewer.proto:唯一的 ProtoBuf 真相源
  • buf.yaml:协议模块 lint / breaking 配置

职责边界

  • 本仓库只负责共享 .proto 定义与 buf 规则。
  • 本仓库不直接向消费仓库写入生成代码。
  • Python / TypeScript / Java 生成物都由消费仓库在本地或 CI 中自行生成。

Tag 规则

  • 协议版本使用 proto/vX.Y.Z 命名,例如 proto/v0.6.0。
  • 这里的 tag 表示“协议发布版本”,不是 Mod、后端或前端脚本的应用版本。
  • 消费仓库必须锚定具体 submodule commit,不允许依赖本仓库 branch head 作为构建输入。

何时升级协议版本

以下变更默认需要创建新的协议 tag:

  • wire shape 变化,例如字段新增、删除、改名、类型变化、oneof 结构变化
  • 兼容性基线变化,例如最低兼容协议版本变化
  • 需要三端同步升级的协议语义变化

推荐发布流程

  1. 在本仓库修改 proto/teamviewer/v1/teamviewer.proto
  2. 运行 buf lint
  3. 在三个消费仓库验证生成与构建
  4. 创建并 push 协议 tag,例如 proto/v0.6.0
  5. 在消费仓库把 third_party/TeamViewRelay-Protocol 升级到该 tag 对应 commit
  6. 重新生成代码、运行测试并提交 submodule 指针更新

消费方式

消费仓库通过 third_party/TeamViewRelay-Protocol 这一 git submodule 锚定本仓库的具体 commit:

  • Minecraft-TeamViewer-Backend
  • Minecraft-TeamViewer-Web-Script
  • Minecraft_TeamViewer

不要依赖本仓库的 branch head;应先发布协议 tag,再在消费仓库中更新 submodule 指针。

Tab 历史同步(0.7.0)

0.7.0 为每个房间维护“每个玩家 UUID 最新一次被观察到的 Tab 标签”,用于离线玩家的城镇归属分析。 它不是标签变更审计日志;实时 Tab 数据始终应覆盖历史缓存。

  • TabHistorySubscribeRequest / TabHistoryDigest:订阅数据集 head,digest 变化后再发起同步。
  • TabHistorySyncRequest / TabHistorySyncChunk:支持完整镜像和从已知 revision 开始的增量镜像。
  • TabHistoryLookupRequest / TabHistoryLookupChunk:按 UUID 或玩家名查询,不要求客户端下载整个房间。
  • 握手中的 TabHistoryCapabilities 声明单消息的条目数、编码字节数和查询 selector 上限。max_chunk_entries 不是房间玩家总数上限;数千条记录会拆成多个 chunk。
  • 所有请求都隐式绑定当前 WebSocket 握手确定的 room_code,请求消息不接受另一个房间号。
  • TabPlayerEntry.scoreboard_team_id 是原版计分板 Team 的内部名称;prefix、suffix、Team color 和解析后的 格式 span 分别透传。旧客户端继续读取无格式的 display_name / prefixed_name。
  • digest 与 entry ETag 使用 SHA-256;完整/增量响应中的所有 chunk 使用同一个固定 TabHistoryHead。

Battle chunk 公开模型与摘要(0.7.1)

0.7.1 统一将公开的 battle chunk 状态表示为结构化条目列表。坐标只出现在 ref 中,data 不再重复 dimension、chunkX、chunkZ,也不公开 dimension|chunkX|chunkZ 形式的合成 ID:

[
  {
    "ref": {
      "dimension": "minecraft:overworld",
      "chunkX": 12,
      "chunkZ": 34
    },
    "data": {
      "colorRaw": "#ff0000",
      "colorMode": "raw_observed",
      "mode": "simmc"
    }
  }
]

实现内部可以用合成字符串或结构体作为索引,但 snapshot、patch、管理 API 和 0.7.1 摘要必须使用 {ref,data},不得把内部索引暴露为公开合同。ProtoBuf wire 消息原本已经使用 BattleChunkRef + BattleChunkValue,因此 0.7.1 不增加、删除或重编号字段。

Digest.battle_chunks 按连接协商出的协议版本选择合同:

  • < 0.7.1 使用旧合同:按 dimension|chunkX|chunkZ 排序,每行是 canonicalJson(key) + ":" + canonicalJson(flattenedValue) + "\n"。flattenedValue 必须包含 dimension、chunkX、chunkZ 和业务字段。这保持与 0.7.0 Mod 及旧 Python Backend 一致。
  • >= 0.7.1 使用新合同:按 (dimension, chunkX, chunkZ) 排序,每行只写 canonicalJson({"ref":ref,"data":data}) + "\n",不添加合成 key 前缀。
  • 两种合同都对完整字节串计算 SHA-1,使用小写十六进制的前 16 位。空集合摘要为 da39a3ee5e6b4b0d。

这里的 canonicalJson 递归按 JSON object key 的 Unicode 字典序排列;数组保持顺序;字符串使用标准 JSON 转义;null 字段先删除。Battle chunk 摘要不包含 observedAt、positionSampledAt、 alignmentSource、reporterId。缺失的 colorMode 按 raw_observed 参与摘要,以兼容旧数据。

固定测试向量如下(roomCode 为 default):

{
  "ref": {"dimension":"minecraft:overworld","chunkX":1,"chunkZ":2},
  "data": {"colorRaw":"#112233","colorMode":"raw_observed","mode":"simmc","roomCode":"default"}
}
  • 0.7.0 官方旧合同:d6fccb4a1bd18438
  • 0.7.0 Rust 历史缺陷投影(仅用于新 Mod 兼容旧服务端):9cecf13aa4592a1c
  • 0.7.1 {ref,data} 合同:31c63cd6e92bbc39

0.7.0 Rust 历史缺陷会从 flattenedValue 漏掉三个坐标字段。服务端不得继续生成该摘要;0.7.1 Mod 连接 0.7.0 服务端时可以同时接受官方旧合同和这个已知缺陷值,避免无休止全量重同步。

外部目录与关系查询(0.8.0)

0.8.0 允许外部来源发布完整玩家目录、组织节点和组织关系边。关系图不进入实时 SnapshotFull,而是通过 独立查询消息按需读取,避免把玩家关系展开为 O(n²) 的玩家对。

  • ExternalSourceCapabilities 在来源握手时声明数据集格式、覆盖范围和 stale 阈值。
  • ExternalDatasetPublish 分块发布关系快照或 patch;完整目录与实时在线 Tab 是不同数据集。
  • PlayerDirectoryLookupRequest 支持 UUID、稳定 player ID 和名字查询;名字可以返回多个同名玩家。
  • PlayerRelationQueryRequest 既可查询指定目标,也可在目标为空时筛选当前玩家的全部关系。
  • PlayerReportPolicy 是服务端建议,不是强制命令。只有健康且完整的外部在线目录才能建议抑制 Tab; 位置信息默认继续上报。
  • 数据过期后仍可查询,但响应必须设置 stale,由客户端决定如何展示。

teamviewer.v1 包名保持不变,0.8.0 只追加字段和消息。0.7.1 及更早的客户端会忽略新增字段,继续使用 实时 Tab、Tab 历史和本地手工关系标记。

不可靠通道泛化(0.9.0)

0.9.0 把 0.8.1 引入的 WebMapHandshakeRequest.accepts_unreliable_positions 布尔声明泛化为通道列表:

  • 新增 enum UnreliableChannel(UNRELIABLE_CHANNEL_MOVEMENT = 1 起,后续按需扩枚举);
  • WebMapHandshakeRequest.accepts_channels(repeated UnreliableChannel,字段号 6)声明客户端可经 不可靠 datagram 消费的通道;服务端必须忽略未知枚举值(proto3 保留为 unknown fields);
  • 旧字段 accepts_unreliable_positions(字段号 5)标记 deprecated:true 语义等价于 accepts_channels = [UNRELIABLE_CHANNEL_MOVEMENT],服务端以此映射兼容 0.8.1 客户端。

本版本为纯增量:0.8.1 客户端/服务端互操作行为不变。

0.9.0-alpha 内的发布记录

  • 0.8.1 tag 撤销重发:原 tag proto/v0.8.1 忘加 alpha 后缀,已从本地与远端撤销, 原地(同一提交 f9060f4,树内容一字不改)重发为 proto/v0.8.1-alpha.1。协议版本号 0.8.1 本身继续有效,只修正发布属性。
  • 0.9.0-alpha.4 传输层/应用层分层:DoorControlFrame 系消息从 teamviewer.proto 迁出至 teamviewer/door/v1/door.proto(字段号逐一不变,线上零变化),并新增 BulkTransferStart 与 bulk 传输通道约定;door-control 下行流全部压缩套 按需实体化(首帧写出才过线,流序不变)。
  • 0.9.0-alpha.5 上行位置 datagram:PlayerHandshakeRequest.unreliable_channels(13)+ HandshakeAck.unreliable_channels_accepted(19),Player 上报对称化接收路径。
  • 0.9.0-alpha.6 下行消费对称化:PlayerHandshakeRequest.accepts_channels(14)+ HandshakeAck.downlink_channels_accepted(20)——Player 通道获得与 WebMap 相同的 movement datagram 消费声明/回执;回执字段对两类连接统一填写。纯增量,wire 版本串 仍为 0.9.0。

传输门约定

TeamViewRelay 服务端并行开放多扇“传输门”,所有门共享同一套应用层(WireEnvelope)语义,仅线路封装不同。 下列约定是各门实现的共同合同,任何一端的实现副本都不得漂移;变更必须先改本节再改代码。

门与端口

门 端口 客户端 会话识别
WebSocket 8765/tcp(/web-map/ws) 浏览器脚本、Java mod URL path + 握手消息
WebTransport 8766/udp(/web-map/wt) 浏览器脚本 URL path + 握手消息
裸 QUIC 8767/udp Java mod 无 path,由首个握手消息的通道/载荷类型自识别

流分帧(各门可靠流通道通用)

  • 帧格式为 [varint 长度][payload],varint 采用 LEB128(与 protobuf 同款);长度只计 payload。
  • 帧上限 8 MiB(后续启用压缩的协议版本中,上限以解压后大小计);帧头最长 4 字节。
  • 非法帧头(varint 超 4 字节或长度越限)必须显式断开连接,不得静默等待或丢弃。
  • 历史:后端 1.2.0-alpha.4 及更早使用 4 字节大端长度前缀,该格式已废弃。
  • WebSocket 门不使用本分帧:WS 帧自带消息边界,应用消息整帧直传。

datagram 通道(movement 位置批等不可靠载荷)

  • 自后端 1.2.0-alpha.5 起,全协议版本 datagram 一律无长度前缀:datagram 自带报文边界。
  • plain 版本:payload 即裸 protobuf WireEnvelope。
  • 历史:1.2.0-alpha.4 及更早的 [4 字节大端长度 + envelope] datagram 格式已废弃。
  • datagram 为尽力投递:载荷必须自包含、可独立解析,丢失由下一 tick 或低频保底全量自愈。
  • 上行对称化(0.9.0-alpha.5):Player 通道客户端可在 PlayerHandshakeRequest.unreliable_channels (字段 13)声明"我将以 datagram 上送"的通道(首值 UNRELIABLE_CHANNEL_MOVEMENT = 逐 tick 玩家位置 upsert);服务端在 HandshakeAck.unreliable_channels_accepted(字段 19)回执实际 启用子集(需会话具备 datagram 能力),未回执 = 全部走可靠流。上行 datagram 载荷 = 裸 WireEnvelope{PLAYER, PlayerReportBundle},仅 players_patch 有值(带 submit_player_id), 无长度前缀、恒 plain;未声明会话发来的 datagram 直接丢弃(尽力投递语义,不算违规)。 丢失零重传:由 digest 核对 + refresh_req + 周期全量自愈。
  • 下行消费声明(0.9.0-alpha.6):Player 通道客户端可在 PlayerHandshakeRequest.accepts_channels (字段 14,语义镜像 WebMapHandshakeRequest.accepts_channels)声明"我可经 datagram 消费" 的通道;WebMap 通道继续经字段 6(或 deprecated 布尔)声明。服务端统一在 HandshakeAck.downlink_channels_accepted(字段 20)回执实际启用的下行消费子集 (需会话具备 datagram 能力);未回执 = 位置全部由可靠流承载。启用后服务端在可靠流 patch 中剥离逐 tick 位置(低频保底刷新除外),movement datagram 载荷 = 裸 WireEnvelope{WEB_MAP, Patch{players}},与消费端连接角色无关(共享批缓存不按 角色分裂,消费端按载荷类型识别),压缩由协商套决定(plain/zstd 单帧/字典),无长度前缀。 声明了消费但无法解 datagram 的客户端自担损失——低频保底全量最终收敛。

握手与认证

  • 连接建立后 10 秒内,可靠流上的首帧必须是合法握手消息,否则服务端断开连接。这是应用层策略, 适用于所有门(含裸 QUIC 无 path 的自识别场景)。

压缩与版本协商命名(0.9.x 压缩版本)

协议版本 WS 子协议 WT URL query ?suite= QUIC ALPN
plain teamviewrelay.plain.v1 plain teamviewrelay/v1
+zstd teamviewrelay.zstd.v1 zstd teamviewrelay/v1+zstd
+zstd-dict teamviewrelay.zstd-dict.v1 zstd-dict teamviewrelay/v1+zstd-dict
  • WS 子协议与 QUIC ALPN 按上表;WT 门以 extended CONNECT 目标 URL 的 query 参数 ?suite=<短名> 为协商唯一权威。浏览器不把 WT-Protocol 响应回执暴露给页面脚本 (Chromium issue 435589295),响应头协商通道在浏览器侧断裂,故改走两端共享的 URL: 客户端把选定套的短名拼进 query,服务端从同一字符串解析,两端必然一致。未携带 suite、值为空或无法识别时一律取 zstd-dict(压缩率最高)。
  • 客户端按偏好序提供候选,服务端在门原生协商载体(WS 101 回显 / TLS ALPN / WT query) 中确定其一;协商全程零额外 RTT,压缩不在 protobuf 层协商。
  • WS 门的服务端回显必须取自客户端实际提供的候选列表(RFC 6455 硬性要求,严格客户端拒绝 列表外的回显);因此任何套都原样回显,不做套间折叠。WS 门无 datagram,+zstd-dict 在 WS 门的流行为与 +zstd 完全一致,客户端按"收到 dict 回显即 zstd 流语义"解释。 WT 门的服务端仍以裸 token 回显 WT-Protocol 响应头,仅作未来浏览器实现协商语义后 的前向兼容。
  • 裸 QUIC 门 ALPN 未匹配任何候选时握手必须失败。
  • 压缩为单向语义:zstd 压缩版本(+zstd/+zstd-dict)仅作用于下行 (服务端→客户端)的可靠流与 datagram;上行(客户端→服务端)恒为 plain—— 可靠流按 [varint 长度][payload] 原样分帧(WS 门为 plain 消息),datagram 为裸 protobuf envelope。上行载荷小(握手、命令、回执),且浏览器端解压库 fzstd 仅解压;客户端不得对上行启用压缩——服务端一律按 plain 解读, 压缩字节过不了 10 秒首帧握手或 envelope 解码。

传输层/应用层分层与 door-control 流(0.9.0-alpha.4 分层,0.9.0-alpha.2 定稿语义)

门级控制帧自 0.9.0-alpha.4 起独立在传输层协议文件 teamviewer/door/v1/door.proto (package teamviewer.door.v1,含 DoorControlFrame),应用层文件 teamviewer/v1/teamviewer.proto 只描述 WireEnvelope 承载的应用层消息。 迁移保持字段号逐一不变,线上字节零变化(proto package 改名对 wire 不可见)。

QUIC/WT 门适配层在全部压缩套(plain/+zstd/+zstd-dict)为门级控制帧 预留各一条额外单向流:下行门控流按需实体化(QUIC 流首帧写出才过线, 服务端在首帧需要时才开流,流序仍恒为服务端第 2 条;空闲零开销;字典载荷只在 +zstd-dict 套出现):

  • 分层铁律:door-control 流不是应用层信道,绝不允许承载 WireEnvelope; 应用流也绝不承载 DoorControlFrame。两类载荷都是 protobuf,跨层混用时首字段 wire type 必然冲突(WireEnvelope.channel 是 varint,DoorControlFrame.payload 是 length-delimited),解析当场报错;解析失败即协议违规,必须显式断开连接, 不得静默等待或丢弃(DoorControlFrame 内未知 oneof 成员仍前向兼容容忍); 已知许可例外:浏览器脚本端(fzstd 纯 JS 解压)对门控流损坏选择静默降级 不断连——丢弃字典帧、回退独立压缩语义;Java mod 与服务端仍严格执行断连;
  • 流识别按开启序约定:客户端第 1 条双向流 = 应用层上行;服务端第 1 条单向流 = 应用层 下行;服务端第 2 条单向流 = door-control 下行(服务端→客户端);客户端第 1 条单向流 = door-control 上行(客户端→服务端);合同外流(客户端第 2 条起双向流/单向流)一律 违规断连;
  • door-control 流使用与流通道相同的 [varint][payload] 分帧,payload 为序列化的 DoorControlFrame;WS 门(无独立传输流语义)不涉及门控流;
  • datagram 字典模式(+zstd-dict 套):
    • 服务端用近期 movement 批训练字典(推荐 4096 字节;接收端应拒绝异常超大内容,建议 上限 16 KiB),经 door-control 下行流发送 DoorControlFrame{dict_offer: {dictionary_id, content}};dictionary_id 为连接内不透明的短标识(建议内容哈希前缀),字典内容按 连接独立,不得跨连接共享或缓存;content 是不透明 zstd 训练字节(压缩状态), 外层帧本身是标准 protobuf;
    • 客户端完整接收并安装字典后,经 door-control 上行流回 DoorControlFrame{dict_ready: {dictionary_id}};
    • 服务端仅在收到 dict-ready(ID) 后才用该字典压缩 datagram(激活);激活前按独立 (无字典)zstd 压缩。字典内容与使用它的数据分别走可靠有序流与 datagram:ready 只能 在 offer 完整安装后发出,故字典字节必然先于用它压缩的任何 datagram 过线;
    • 客户端按连接只保留 current + previous 两个字典(旧 ID 是乱序在途帧的宽限,收到新 字典压缩的帧后即可释放更旧);服务端同款 current + previous 生命周期,新 ID 激活即 逐出更旧;连接关闭全部释放;
    • 重训设最小间隔(建议 ≥60 秒)加玩家集变化阈值,防重训风暴;
    • 唯一理论竞态是客户端 door-control 安装任务滞后于 datagram 接收:zstd 字典不匹配会 响亮报错(非静默错位),按普通丢包丢帧即可——movement 下一 tick 自愈 + 低频保底 全量刷新兜底,代价与 UDP 丢包同级。

bulk 传输通道(0.9.0-alpha.4 引入)

战区地图等大内容(10 MiB 级)走服务端第 3 条起的专用单向流,与应用下行流、 door-control 流互不混用,实现"大内容不阻塞小消息"的传输层分层:

  • 服务端先经 door-control 下行流发送 DoorControlFrame{bulk_transfer_start: {transfer_id, total_len, content_type}} 通告,再开启 bulk 流;
  • bulk 流自标识:载荷以 [varint len][transfer_id UTF-8] 前缀开头,其后是 total_len 字节的原始内容(不做 protobuf 封装,零逐块开销)。QUIC 不保证跨流顺序,接收端按 transfer_id 把流匹配到通告:先于通告到达的字节进有界缓冲(建议上限 4 MiB), 超上限或超时仍无匹配通告 = 协议违规断连;
  • 低优先级的实现优先用传输栈的本地发送优先级(quinn set_priority: 数值越大越先发送,bulk 流取负值,门控/应用流恒先出队);辅以小块写入 (每次 ≤64 KiB)与拥塞控制运行时反馈(cwnd/RTT)自节流,防止灌满 连接发送队列把高优流队头阻塞在 bulk 字节后面;不得按已知链路预算 调参——拥塞控制自行探测路径;
  • 完整性与验收由接收方核对 total_len 与内容自校验(如内容图案 checksum)完成; bulk 传输失败不拖垮连接,应用层流与 datagram 照常。

给开发者的约束

  • 不要在消费仓库中复制、重建或手改 .proto 副本。
  • 修改协议后,必须明确说明目标 tag、兼容性影响,以及需要更新哪些消费仓库。
  • 任何破坏兼容的协议变更都必须显式记录,不允许“静默升级”。

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors