Tap 協議
tap 是一個只讀的二進制流,包含客戶端收發的遊戲數據包,供 bedboo 的 bedd 傳輸使用。
配置打開 tap 之前它是關閉的(配置)。
管道與幀
\\.\pipe\<pipeName>-tap,只在配置 tap 打開時存在(默認關閉)。一次一個讀取方,僅本機、僅當前用戶。BeDD 寫,讀取方只讀。
它包含客戶端收到的每個客戶端方向遊戲數據包、它發出的每個服務端方向數據包的回顯(不含 login、握手和 request_network_settings),以及 JSON 標記。
bedboo 把名字推導為 <控制管道>-tap。
幀,小端序:
u32 len (bytes after this field) | u8 kind | u8 flags | u16 0 | u64 seq | u64 tUs (steady clock µs) | payloadlen 最多 16 MiB + 20。seq 在進程內遞增(回放的幀保留原來的 seq)。
| kind | payload | |
|---|---|---|
| 1 | CLIENTBOUND | 一個遊戲數據包:varint 頭(id | senderSub<<10 | targetSub<<12)+ 包體,與 bedrock-protocol 的 parsePacketBuffer 讀取方式一致 |
| 2 | SERVERBOUND_ECHO | 同上,客戶端發出的包(從不包括 id 1、3、4、193) |
| 4 | MARKER | UTF-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_UP | type remote|local、address |
LINK_DOWN | reason |
REPLAY_BEGIN | frames |
REPLAY_END | complete(日誌超過 tapJournalMiB 時為 false) |
DROPPED | count;之後管道關閉 |
讀取方依次收到 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。