跳到正文
本頁目錄

Tap 協議

tap 是一個只讀的二進制流,包含客戶端收發的遊戲數據包,供 bedboo 的 bedd 傳輸使用。 配置打開 tap 之前它是關閉的(配置)。

管道與幀

\\.\pipe\<pipeName>-tap,只在配置 tap 打開時存在(默認關閉)。一次一個讀取方,僅本機、僅當前用戶。BeDD 寫,讀取方只讀。 它包含客戶端收到的每個客戶端方向遊戲數據包、它發出的每個服務端方向數據包的回顯(不含 login、握手和 request_network_settings),以及 JSON 標記。 bedboo 把名字推導為 <控制管道>-tap。

幀,小端序:

text
u32 len (bytes after this field) | u8 kind | u8 flags | u16 0 | u64 seq | u64 tUs (steady clock µs) | payload

len 最多 16 MiB + 20。seq 在進程內遞增(回放的幀保留原來的 seq)。

kindpayload
1CLIENTBOUND一個遊戲數據包:varint 頭(id | senderSub<<10 | targetSub<<12)+ 包體,與 bedrock-protocol 的 parsePacketBuffer 讀取方式一致
2SERVERBOUND_ECHO同上,客戶端發出的包(從不包括 id 1、3、4、193)
4MARKERUTF-8 JSON

讀取方忽略其他 kind。flags:1 回放(來自加入日誌),2 重新序列化(Packet::write,不是線上字節),4 本地連接,8 被抑制(保留)。

標記字段
HELLO(總是第一個)tap:1、mod、game:"1.26.51.01"、protocol(2193)、pid、link{state, type, address}、journal{frames, bytes, complete}(這個讀取方被回放的內容)
LINK_UPtype remote|local、address
LINK_DOWNreason
REPLAY_BEGINframes
REPLAY_ENDcomplete(日誌超過 tapJournalMiB 時為 false)
DROPPEDcount;之後管道關閉

讀取方依次收到 HELLO、REPLAY_BEGIN、加入日誌的幀(帶回放標誌)、REPLAY_END,然後是實時流;回放期間到達的實時幀等到 REPLAY_END 之後再發, 回放已包含的實時幀按 seq 跳過。沒有日誌時這一對標記之間為空(complete:false)。落後超過 32 MiB 的讀取方會收到 DROPPED 並被斷開;重新連接會得到完整回放。

所有發佈者共用 src/TapSink.h,它必須保持逐字節一致:每個發佈者用 prev = gEmit.exchange(&own) 安裝自己的 sink,把每幀轉發給 prev, 移除時只有 gEmit 仍是自己時才放回 prev(compare-exchange)。鉤子只複製字節;管道的寫線程負責清空隊列。

遠程服務器(NetTap.cpp)

客戶端的 NetworkSystem 跟蹤它唯一的出站連接($onNewOutgoingConnection;$onConnectionClosed、$onAllConnectionsClosed、closeConnection 和 disconnect 結束它)。在遠程連接上,NetworkConnection::receivePacket(在它之後,HasData)給出每個已解壓、已解密的包, NetworkSystem::_sendInternal 給出客戶端發出的每個已序列化包:線上字節,flags 為 0。打開配置 trace 時,每個連接的前 50 個包會記為 tap trace #n in|out id len:它們的順序與 BDS 抓包一致(network_settings、server_to_client_handshake、play_status、resource_packs_info、……、start_game、……)。

Blob 緩存。 打開 blob 緩存的客戶端收到的子區塊只是 blob 哈希,以及它缺的 blob(client_cache_miss_response);其餘來自它磁盤上的緩存, 從不經過網絡,讀取方就無法構建世界。因此 tap 打開時,BeDD 在客戶端的 client_cache_status 發出前清掉其中的 enabled,服務器就會完整發送每個子區塊。 回顯裡顯示 enabled:false。

單人世界和局域網主機世界(TapLoopback.cpp,配置 tapLoopback)

tapLoopback(默認 true,只在 tap 打開時有效)把單人世界和客戶端開的局域網世界也放到 tap 管道上。這時客戶端通過 LocalConnector 與進程內的服務器通信,遠程抓取不發佈任何東西;改為掛鉤 LoopbackPacketSender,每個包重新序列化(varint 頭 id | senderSub << 10 | targetSub << 12, 然後用發送方 NetworkSystem 的 ReflectionCtx 調 Packet::write)。幀帶 reserialized(2)和 local(4)標誌。

  • CLIENTBOUND:服務器的發送器($sendToClient 兩個重載、$sendToClients、$sendBroadcast 兩個重載、$sendTo),只針對在服務器上連接類型為 Local 的 NetworkIdentifier。局域網訪客是遠程連接,從不發佈(計為 guest)。廣播在本地玩家收到 start_game 之後才發佈。
  • SERVERBOUND_ECHO:客戶端的發送器($sendToServer、$send、$sendTo),從不包括 id 1、3、4 或 193。
  • 區塊:集成服務器通過同一個發送器回應每個 subchunk_request,所以客戶端交換的每個 subchunk 和 subchunk_request 都在 tap 上 (遊戲內驗證過:subchunk 幀數與 ServerNetworkHandler::handle(SubChunkRequestPacket) 調用次數相同)。客戶端以一個原點(它自己的子區塊) 為基準請求,每列最多到 highest_subchunk_count;本地客戶端發送的 client_cache_status 為 enabled:false,所以每項都帶數據。
  • 只在客戶端的連接(tap::gLinkType)為本地時。客戶端 NetworkSystem 上的 $onNewOutgoingConnection / $onConnectionClosed 按連接類型設置它, 和 tap 核心一樣,所以迴環 tap 不依賴 tap 核心也能工作。還不知道連接時(進程內服務器在客戶端的連接調用返回前就開始發送),客戶端的 Local 連接也算;這個檢查從不寫 gLinkType。遠程抓取只在 Remote 連接上發佈(本地連接也會走 _sendInternal),所以沒有包會被髮布兩次。 一個發送方法調用另一個時,包只發布一次。
  • 沒有 tap 管道(tap::gEmit 為空)時幀只計數。state 報告 stats.tapLoopback{enabled, hooks, link, clientbound, echo, bytes, unpublished, guest}。
  • 打開 trace 時,前 50 幀會記入日誌(trace tap loopback: in|out id= len=),前 4000 幀按 tap 管道的幀格式寫入 mods\BeDD\data\tap-loopback.bin(seq 和 tUs 為 0)。

加入日誌(TapJournal.cpp)

tap 打開時,日誌掛到 bedd::tap::gEmit 鏈上並設置 bedd::tap::gReplay,tap 管道對每個新讀取方在 REPLAY_BEGIN 和 REPLAY_END 之間調用它。 回放的幀保持原來的 seq 和順序,帶 replay 標誌。

  • 只保留 CLIENTBOUND 幀。日誌在新連接(LINK_UP 標記或 gLinkType 變化)和 start_game(11)時重置,start_game 成為它的第一幀。
  • 壓縮鍵(只讀前導字段;同一個鍵以最後一幀為準):level_chunk(x、z、維度)、update_block(位置、層)、set_time、set_health、 network_chunk_publisher_update、current_structure_feature、inventory_content(窗口)、inventory_slot(窗口、格子)、level_event (事件 id,所以天氣保持最後的狀態)、update_abilities(unique id)、各 add_* 包(unique id;add_player 按 runtime id),以及按 runtime id 的 set_entity_motion、move_entity、move_player 和 mob_armor_equipment。只帶變化字段的包按實體和字段集合作鍵,所以每個字段都保持最後的值: set_entity_data(元數據鍵)、update_attributes(屬性名)、move_entity_delta(出現的字段)和 mob_effect(效果)。
  • subchunk 按子區塊保留,而不是按包:客戶端以一個原點為基準請求所有子區塊,所以只有當更新的幀覆蓋了一個包裡的每個子區塊(原點 + 每項的偏移) 時,這個包才會被丟棄。日誌無法遍歷的包整個保留。
  • remove_entity(14)刪除該實體的 add_entity/add_item_entity/add_player 幀(add_player 按其 unique id 的字節匹配),以及所有按它的 runtime id 作鍵的幀。
  • 不保留:text、animate、network_stack_latency、play_sound、stop_sound、level_sound_event(及 v2)、spawn_particle_effect、level_event_generic。
  • 超過 tapJournalMiB(默認 128)後不再保留幀,日誌在下次重置前報告 complete:false:這時中途接入的讀取方會缺區塊,直到玩家重新進入這些區塊或客戶端重新加入。
  • state 報告 stats.journal:enabled、frames、bytes、complete,以及計數 seen(客戶端方向幀)、compacted(被同鍵的後續幀取代)、 pruned(add 幀被 remove_entity 刪除的實體)、transient(不保留)、overCap(超過 tapJournalMiB 後未保留)、resets 和 replays。