Multiplayer RPC APIs over ENet 【Godot 源代碼閱讀】

????? Enet Multiplayer APIs
Godot 4.0 RC1 終于被我等出來(lái)了[大狗頭]

為了祝賀 Godot 4.0 艱辛地從 Alpha、Beta,再到 RC 的歷程,終于直到正式發(fā)布前的階段性成就,特意獻(xiàn)上一份 Godot 源代碼閱讀材料,魚(yú)樂(lè)諸君。內(nèi)容有點(diǎn)長(zhǎng),像裹腳布,主要是代碼和草圖有半。
首先,由于涉及到源代碼閱讀,這就需要討論一下閱讀代碼的方法論。閱讀源代碼的能力算是程序員的一種底層基礎(chǔ)能力之一,這個(gè)能力的重要性與研究數(shù)據(jù)結(jié)構(gòu)與算法同等重要。軟件開(kāi)發(fā)的過(guò)程中必不避免接觸到其他人的工作成果,而閱讀源代碼可能是出于學(xué)習(xí)行為,也可以是出于項(xiàng)目需求問(wèn)題的解決。
這里就閱讀源代碼給出幾點(diǎn)參考意見(jiàn):
?源代碼是人類的思想表達(dá)形式之一,閱讀源代碼相當(dāng)于在做一個(gè)思維逆向工程,與軟件破解逆向程沒(méi)有本質(zhì)區(qū)別。
每個(gè)人的代碼組織會(huì)有所差異,與各人的思維方式直接相關(guān),應(yīng)該盡量嘗試代入作者的思維看待問(wèn)題。
?不要想著通透所有代碼,應(yīng)該輕重分明,把握主線。閱讀源代碼前先建立目標(biāo),避免過(guò)多關(guān)注不相關(guān)代碼。
閱讀過(guò)程陷入停滯,就應(yīng)該暫停一下,可能是某些相關(guān)的基礎(chǔ)缺失導(dǎo)致這種結(jié)果,應(yīng)該將時(shí)間用在打基礎(chǔ)上。
不要完美主義,但可以更進(jìn)一步,關(guān)注一每個(gè)小問(wèn)題的解決。不要糾結(jié)缺失,它是提供一個(gè)未來(lái)進(jìn)步的空間。
保持使用筆記的習(xí)慣,特別是方便快速檢索和加查的工具。我推薦具有強(qiáng)大跳轉(zhuǎn)功能的 Sublime Text。
將閱讀結(jié)果整理成流程圖,像以下這種簡(jiǎn)潔風(fēng)格 ASCII 流程圖最愛(ài),它也是 RFC 文檔的圖表工具。
最后,以史為鏡,可以知興衰;以人為鏡,可以知得失。遇事不決問(wèn)歷史。
記得第一次嘗試閱讀的源代碼是 Wordpress,這是一個(gè) Web 內(nèi)容管理工具,我的習(xí)慣是先過(guò)一遍官方文檔。學(xué)習(xí) Python、Lua、Vim 或者 Rust 等等,在也保持同樣的習(xí)慣,當(dāng)然,這可能要消耗大量時(shí)間。
特別是一點(diǎn),當(dāng)過(guò)程進(jìn)行不下去的時(shí)候,幾乎可以直接下定論:你可能遇到一個(gè)全新領(lǐng)域,對(duì)此,開(kāi)啟一個(gè)學(xué)習(xí)專題,進(jìn)行深入的探索才是出路。沒(méi)有一個(gè)工程可以單靠單一的技術(shù)實(shí)現(xiàn)的,涉及面廣泛是一個(gè)成功項(xiàng)目的標(biāo)志,探索過(guò)程帶來(lái)的認(rèn)識(shí)提升,也就是閱讀源代碼所帶來(lái)的終極效果。
ENet 與 RPC 協(xié)議基礎(chǔ)
在 Godot RPC API 系統(tǒng)中,`MultiplayerAPI` 和 `MultiplayerPeer` 是兩種最重要的類型,前者用來(lái)管理 RPC 協(xié)議的規(guī)則與配置,后者通過(guò)具體網(wǎng)絡(luò)傳輸協(xié)議提供網(wǎng)絡(luò)通信支持,其中又以 ENet 協(xié)議的實(shí)現(xiàn)類型 `NetworkedMultiplayerENet` 或者新版本中的 `ENetMultiplayerPeer` 為基礎(chǔ)。
通過(guò)后續(xù)的分析,最終可以得到以下這樣的一個(gè) RPC API 執(zhí)行邏輯圖,這里先展示出來(lái)。此圖簡(jiǎn)化了網(wǎng)絡(luò)傳遞,著重 RPC 執(zhí)行過(guò)程。發(fā)起 RPC API 調(diào)用過(guò)程是左側(cè) ENet Peer Host,以 `rpc()` 方法執(zhí)行為線索,其實(shí) `rset()` 最后也會(huì)執(zhí)行到 `_send_rpc()`,統(tǒng)一由 ENetPeer 進(jìn)行網(wǎng)絡(luò)傳輸:
Birrell 和 Nelson 在 1984 發(fā)表于 ACM Transactions on Computer Systems 的論文? Implementing remote procedure calls 對(duì) Remote Procedure Call Protocol (RPC) 遠(yuǎn)程過(guò)程調(diào)用協(xié)議做了經(jīng)典的詮釋??蛻舳顺绦蛲高^(guò)網(wǎng)絡(luò)調(diào)用遠(yuǎn)程計(jì)算機(jī)上的對(duì)象,就像調(diào)用本地應(yīng)用程序一樣。
第一代 RPC 是 ONC RPC,以前稱為 Sun RPC,它提供一個(gè)編譯器,需要用戶定義一個(gè)遠(yuǎn)程過(guò)程接口來(lái)生成客戶機(jī)和服務(wù)器的存根函數(shù),client stub,這個(gè)編譯器叫做 rpcgen,谷歌的 protobuf 構(gòu)架也是這種。
第二代 RPC 支持對(duì)象,以微軟 DCOM(COM+) 為代表。面向?qū)ο蟮恼Z(yǔ)言開(kāi)始在 1980 年代末興起,很明顯,當(dāng)時(shí)的 Sun ONC 和 DCE RPC 系統(tǒng)都沒(méi)有提供任何支持諸如從遠(yuǎn)程類實(shí)例化遠(yuǎn)程對(duì)象、跟蹤對(duì)象的實(shí)例或提供支持多態(tài)性。
第三代 RPC 以 Web Services,Simple Object Access Protoco (SOAP) 簡(jiǎn)單對(duì)象訪問(wèn)協(xié)議為代表,是 Web 平臺(tái)編程流行的產(chǎn)物。
Enet 省略了某些更高級(jí)別的網(wǎng)絡(luò)功能,例如身份驗(yàn)證,服務(wù)發(fā)現(xiàn),加密或其他特定于應(yīng)用程序的類似任務(wù),因此該庫(kù)保持靈活,可移植且易于嵌入,并具以下優(yōu)點(diǎn):
01. 克服 TCP 不支持多信道問(wèn)題;
02. 克服 TCP 需要用戶自己處理粘包問(wèn)題,UPD 數(shù)據(jù)包則先天有邊界保護(hù);
03. 克服 UDP 不支持排序,連接管理,帶寬資源管理,數(shù)據(jù)包的大小有限制;
04. Enet 基于單一 UDP 協(xié)議實(shí)現(xiàn)了具有 UDP 和 TCP 等價(jià)功能,但比同時(shí)集成兩者更干凈、統(tǒng)一。
TCP 協(xié)議特點(diǎn):
?TCP 基于連接實(shí)現(xiàn)可靠性傳輸,使開(kāi)發(fā)更為簡(jiǎn)單,但同時(shí)也帶來(lái)效率慢的特點(diǎn)。
?TCP 為流量設(shè)計(jì),可使用滑動(dòng)窗口控制每秒內(nèi)可以傳輸多少KB的數(shù)據(jù),講究的是充分利用帶寬。
?TCP 為了可靠性使用復(fù)雜的擁塞控制算法,3 次握手 4 次揮手建立和斷開(kāi)連接、重傳策略。
?TCP 內(nèi)置在系統(tǒng)協(xié)議棧中,極難對(duì)其進(jìn)行改進(jìn)。?
滑動(dòng)窗口是一種基于雙指針的一種算法思想,兩個(gè)指針指向的元素之間形成一個(gè)窗口,通過(guò)調(diào)整窗口大小來(lái)控制數(shù)據(jù)包的數(shù)量,以實(shí)現(xiàn)流量控制。窗口有兩類,一種是固定大小類的窗口,一類是大小動(dòng)態(tài)變化的窗口。
UDP 協(xié)議特點(diǎn):
UDP 協(xié)議基于無(wú)連接進(jìn)行高效的不可靠數(shù)據(jù)傳輸,但同時(shí)應(yīng)用層收到的數(shù)據(jù)有缺失、亂序等問(wèn)題。
?UDP 協(xié)議頭開(kāi)銷小,使用 8 個(gè)字節(jié),相比 TCP 要占用 20 個(gè)字節(jié)。
?UDP 面向報(bào)文,有數(shù)據(jù)包邊界保護(hù)。
?UDP 支持一對(duì)一、一對(duì)多、多對(duì)一和多對(duì)多的交互通信等。
UDP 協(xié)議以其簡(jiǎn)單、傳輸快的優(yōu)勢(shì),在越來(lái)越多場(chǎng)景下取代 TCP,如網(wǎng)頁(yè)瀏覽、流媒體、實(shí)時(shí)游戲、物聯(lián)網(wǎng)。
從數(shù)據(jù)類型定義的角度來(lái)看,Godot 集成 ENet 源代碼中:
- 每個(gè)主機(jī)對(duì)應(yīng) `ENetHost`:An ENet host for communicating with peers.
- 每個(gè)連接端對(duì)應(yīng)的是 `ENetPeer`: An ENet peer which data packets may be sent or received from.?
- 每個(gè)數(shù)據(jù)包對(duì)應(yīng) `ENetPacket`:ENet packet structure.
- 每個(gè)數(shù)據(jù)信道對(duì)應(yīng) `ENetChannel`:ENet packet structure.

單連接多頻道
ENet 起源自 Lee Salzman 開(kāi)發(fā)的一個(gè)免費(fèi)聯(lián)網(wǎng) FSP 游戲,最新版本是 Cube 2: Sauerbraten,為其編寫(xiě)的網(wǎng)絡(luò)庫(kù)就是 ENet。
基于 ENet 實(shí)現(xiàn)的各種 RPC API 有兩種調(diào)用方式:
?Reliable 可靠:當(dāng)函數(shù)調(diào)用到達(dá)時(shí),將返回確認(rèn);一定時(shí)間后沒(méi)有收到確認(rèn),則重新發(fā)送函數(shù)調(diào)用。
?Unreliable 不可靠:函數(shù)調(diào)用只發(fā)送一次,不檢查是否到達(dá),沒(méi)有額外開(kāi)銷,典型的 UPD 協(xié)議風(fēng)格。

服務(wù)器/客戶端網(wǎng)絡(luò)模型 Peer-to-Peer 是兩種常見(jiàn)的網(wǎng)絡(luò)模型,而 P2P 則是去中心化的網(wǎng)絡(luò)模型。
ENet 對(duì)等網(wǎng)中所有主機(jī)在創(chuàng)建連接時(shí)都會(huì)相互確認(rèn)連接,雖然建立連接時(shí)也需要服務(wù)器,但和傳統(tǒng)的 C/S 網(wǎng)絡(luò)構(gòu)架不同,ENet 中是對(duì)等網(wǎng)絡(luò)模型。Peer to Peer (P2P) 打破了傳統(tǒng)的 Client/Server (C/S) 模式,在網(wǎng)絡(luò)中的每個(gè)節(jié)點(diǎn)的地位都是對(duì)等的。每個(gè)節(jié)點(diǎn)既充當(dāng)服務(wù)器,為其他節(jié)點(diǎn)提供服務(wù),同時(shí)也享用其他節(jié)點(diǎn)提供的服務(wù)。“Peer”在英語(yǔ)里有“對(duì)等者、伙伴、對(duì)端”的意義。
根據(jù) ENet 文檔描述,其功能包括:
- **連接管理** Connection Management:提供與外部主機(jī)通信的接口,動(dòng)態(tài)監(jiān)管外部主機(jī)及網(wǎng)絡(luò)狀況。
- **排序** Sequencing:提供多個(gè)合理排序的網(wǎng)絡(luò)包流而非單一比特流,從而簡(jiǎn)化不同類型數(shù)據(jù)的傳輸。
- **通道** Channels:連接可用多個(gè)具有網(wǎng)絡(luò)包獨(dú)立排序的數(shù)據(jù)通道,解決因推遲可靠網(wǎng)絡(luò)包的亂序排序問(wèn)題。
- **可靠性** Reliability:可靠性是可選項(xiàng),外部主機(jī)在特定時(shí)間內(nèi)沒(méi)有確認(rèn)收到網(wǎng)絡(luò)包就觸發(fā)重傳。
- **拆分和重組** Fragmentation & Reassembly:大數(shù)據(jù)包拆解發(fā)送,在接收端重組,此過(guò)程自動(dòng)完成。
- **聚合** Aggregation:集合多個(gè)協(xié)議指令,ack,packet transfer 等,確??捎眯?,減少丟包及延時(shí)等。
- **適應(yīng)性** Adaptability:使用動(dòng)態(tài)適應(yīng)的數(shù)據(jù)窗口,和靜態(tài)的帶寬分配機(jī)制,解決網(wǎng)絡(luò)擁塞問(wèn)題。
ENet 作為一種有傳輸信道的協(xié)議,ENetHost 以 channelLimit 指示信道數(shù)量,取值范圍 [1, 255],默認(rèn)定義了三種專用信道:
- **SYSCH_CONFIG** 系統(tǒng)配置變更通知
- **SYSCH_RELIABLE** 可靠模式專用通道
- **SYSCH_UNRELIABLE** 非可靠模式通道
ENet 協(xié)議定義了 12 條指令,每條指令都相應(yīng)定義了其數(shù)據(jù)結(jié)構(gòu)體,其中的:
- Acknowledge 確認(rèn)指令,在收到可靠包后用來(lái)答復(fù)發(fā)送方,表明已經(jīng)收到了相應(yīng)的數(shù)據(jù)包;
- Ping 指令用于監(jiān)測(cè)外部主機(jī)的連接狀態(tài)等等,與心跳包發(fā)送時(shí)機(jī)相關(guān);
- connect 指令用于主動(dòng)發(fā)起連接的一端進(jìn)行主動(dòng)連接操作
- Verify Connect 建立連接時(shí),在第二次握手用于答復(fù),同時(shí)用于主動(dòng)連接方同步被動(dòng)連接方的相關(guān)信息。
- Bandwidth Limit 流量限制指令,調(diào)節(jié)對(duì)端對(duì)應(yīng)本地的 peer 的帶寬的相應(yīng)的數(shù)值。
- Throttle Configure 流量控制調(diào)整指令,用于調(diào)節(jié)由 RTT 控制的 packetThrottle 相關(guān)設(shè)置。
- Send Reliable 可靠包發(fā)送指令,用于發(fā)送不用分片的可靠包,相應(yīng)的數(shù)據(jù)跟在指令的后面。
- Send Unreliable 不可靠包發(fā)送指令,發(fā)送不用分片的不可靠包,與 unrealiable 包的實(shí)現(xiàn)機(jī)制相關(guān)。
- Send Unsequenced 指令發(fā)送不需要分片的 Unsequenced,用相應(yīng)的 unsequencedGroup 標(biāo)記序號(hào)。
- Send Fragment 指令用于發(fā)送所有需要分片的數(shù)據(jù)包,通過(guò) flag 標(biāo)記 reliable 和 unreliable 等類型。
在創(chuàng)建時(shí),ENet 會(huì)建立一個(gè) `ENetHost` 作為通信的客戶端,包含與 peer 進(jìn)行通信的 socket。使用一個(gè)ENetList dispatchQueue 隊(duì)列存放有事件產(chǎn)生的 peers,還使用一個(gè) ENetPeer* peers 數(shù)組用于存放與外部客戶端通信的 peer 數(shù)據(jù)結(jié)構(gòu)。
ENet 使用比 TCP 更輕量的三次握手連接的二次握手?jǐn)噙B,使用 verify Connect 指令確認(rèn),同時(shí)它用于主動(dòng)連接方同步被動(dòng)連接方的相關(guān)信息。


ENet 在連接建立過(guò)程中可以改變 peer 的狀態(tài),過(guò)程如下:
- 首先,兩個(gè) host 建立連接前需要保證 peers 數(shù)組內(nèi)有空閑的 peer,其狀態(tài)為 `disconnected`。
- 主動(dòng)連接方使用空閑 peer 向?qū)Χ税l(fā)送 `connect` 指令,狀態(tài)變?yōu)?`connecting`。
- 對(duì)端接收到 `connect` 指令后,返回 `verify connect` 指令作為答復(fù),狀態(tài)變?yōu)?ack connect。
- 主動(dòng)連接方收到 `verify connect` 指令后,并答復(fù)一個(gè) ack 指令,其狀態(tài)變?yōu)?`connected`,連接建立。
- 并且,向用戶 dispatch 一個(gè) **connect event** 事件。
- 對(duì)端接收到 ack 命令后,狀態(tài)變?yōu)?`connnected`,雙方連接建立完成。
- 并且,同樣向用戶 dispatch 一個(gè) **connect event** 事件。
至此,雙方連接建立完成。



ENet 提供了三種斷開(kāi)連接的方式:disconnect, disconneted now 和 disconnect later。
在斷開(kāi)連接再發(fā)起時(shí),出現(xiàn)新舊兩個(gè)連接的端口號(hào)相同的情況,會(huì)被判定為相同的連接,ENet 使用 SessionID 防止兩個(gè)具有相同 IP 地址和端口號(hào)的前后兩次連接發(fā)送的數(shù)據(jù)發(fā)生混淆。而 TCP 協(xié)議會(huì)使主動(dòng)斷開(kāi)連接的一方處于 TIME_WAIT 的狀態(tài)來(lái)防止這種情況的發(fā)生。
但是,這只是簡(jiǎn)單的 ID 匹配,所以并不能像 TCP 100% 防止兩次連接中數(shù)據(jù)包混淆這種情況的發(fā)生,但是大部分情況下仍是有效的。
ENet 采用的是選擇重傳的方式,為保證新舊窗口的序號(hào)沒(méi)有重疊,窗口的最大尺寸不應(yīng)該超過(guò)序號(hào)空間的一半。ENet 在發(fā)送新的數(shù)據(jù)包時(shí)會(huì)通過(guò)**usedReliableWindows**判斷當(dāng)前窗口占用是否與空閑窗口重疊,如果重疊則暫停數(shù)據(jù)包的發(fā)送。reliableWindows 會(huì)記錄各個(gè)窗口中目前在傳輸中的包的個(gè)數(shù)。
更進(jìn)一步,ENet 提供了一個(gè)動(dòng)態(tài)的閥門(mén),packet throttle,來(lái)響應(yīng)網(wǎng)絡(luò)連接時(shí)帶來(lái)的偏差,通過(guò)限制包的發(fā)送數(shù)量來(lái)應(yīng)對(duì)各種類型的網(wǎng)絡(luò)擁塞問(wèn)題。
網(wǎng)絡(luò)狀況評(píng)估指標(biāo):
? ? RTTV[n] = RTTV[n-1]* 3/4 + (RTT[n] - RTTS[n-1])1/4
? ? RTTS[n] = RTTS[n-1] * 7/8 +RTT[n]* 1/8
? ? RTO Limit: Min(30, Max(5, RTO * 32))
?Round-Trip Time (RTT) 往返時(shí)延,是指數(shù)據(jù)從網(wǎng)絡(luò)一端傳到另一端所需的時(shí)間。
?RTT smoothed (RTTs) 單位時(shí)間內(nèi) RTT 加權(quán)平均值。
?RTT variance (RTTv) 單位時(shí)間內(nèi) RTT 偏差加權(quán)平均值。
?Retransmission TimeOut (RTO) 重傳超時(shí)時(shí)間。初始 RTO = RTTs + 4RTTv
初始超時(shí)重傳時(shí)間 RTO = RTTs+4RTTv,下次超時(shí)重傳時(shí)間是上一次重傳時(shí)間的兩倍,但最大不超過(guò) 500ms。超時(shí)斷開(kāi)判定使用心跳信號(hào),即在沒(méi)有發(fā)送數(shù)據(jù)包的狀態(tài),超過(guò) 500ms 就會(huì)發(fā)送一個(gè)心跳包。斷開(kāi)連接判定條件: currentTime-earliestTimeout > RTO Limit。
Godot 源代碼閱讀
為了對(duì) ENet 的源代碼作一管中窺豹之舉,這里以 Unique ID 以及它在接收端的身份 Sender ID 作為 Godot 集成的 ENet 源代碼閱讀線索,對(duì)涉及的關(guān)鍵內(nèi)容進(jìn)行梳理。
Godot 使用 C++ 封裝了基于 C 語(yǔ)言的 ENet,整個(gè)構(gòu)架封裝在 NetworkedMultiplayerENet 類型中。而 ENet 源代碼存放在第三方目錄下。
ENet 定義了四種事件,使用 I/O 事件輪詢模型,封裝在 NetworkedMultiplayerENet::poll()。當(dāng)新連接請(qǐng)求到來(lái),根據(jù)讀取到的 peer id,分別觸發(fā)連接狀態(tài)信號(hào)。在服務(wù)器端不觸發(fā)連接成功信號(hào),但會(huì)向當(dāng)前所有已連接的 peers 發(fā)送通告。將新加入 peer 通告給已有的 peers,反過(guò)來(lái)又將已有的 peers 通告給新加入的 peer,這種雙向的操作體現(xiàn)了對(duì)等網(wǎng)的基本特征:
在接收數(shù)據(jù)階段,也會(huì)觸發(fā) peer 的連接與斷開(kāi)信號(hào),對(duì)應(yīng) SYSCH_CONFIG 信道發(fā)送的 SYSMSG_ADD_PEER 和 SYSMSG_REMOVE_PEER 兩種配置消息。接收的數(shù)據(jù)保存到 **incoming_packets**,稍后就可以使用它。
NetworkedMultiplayerPeer 中定義的 get_unique_id() 方法是一個(gè)純虛函數(shù),它需要由子類根據(jù)具體協(xié)議來(lái)實(shí)現(xiàn),例如 NetworkedMultiplayerENet 的實(shí)現(xiàn)中,就使用 1 代表服務(wù)器。而創(chuàng)建服務(wù)器時(shí),就根據(jù)時(shí)鐘加用戶目錄、數(shù)據(jù)指針地址等生成一個(gè) ID,使用的算法是 Hash Djb 2:
觸發(fā) `peer_connected` 信號(hào)時(shí),或者自己給自己發(fā)送 RPC 調(diào)用時(shí),get_rpc_sender_id() 返回值為 0。Godot 3.5 中如果配置錯(cuò)誤會(huì)導(dǎo)致?ScreenTree 獲取到的發(fā)送方 ID 總是為 0。它又直接包裝調(diào)用 MultiplayerAPI 的 get_rpc_sender_id() 方法獲取 peer ID,而后者直接返回?cái)?shù)據(jù)成員 `rpc_sender_id`。這個(gè)值會(huì)在MultiplayerAPI 構(gòu)造函數(shù)中初始化為 0。
MultiplayerAPI 有多個(gè)位置涉及 rpc_sender_id 的讀寫(xiě):
- **MultiplayerAPI::MultiplayerAPI()** 構(gòu)造器中初始化為 0 值;
- 執(zhí)行 `poll()` 方法輪詢數(shù)據(jù)時(shí),`get_packet_peer()` 獲取 ID;
- **MultiplayerAPI::rpcp()** 執(zhí)行遠(yuǎn)程調(diào)用;
- **MultiplayerAPI::rsetp()** 執(zhí)行遠(yuǎn)程屬性設(shè)置;
一般同不直接調(diào)用 NetworkPeer `poll()` 方法,而是通過(guò) MultiplayerAPI 根據(jù)當(dāng)前聯(lián)接狀態(tài)調(diào)用它。
ENet 接收到的數(shù)據(jù)會(huì)保存到 **incoming_packets**,其中就包含 sender id,`get_packet_peer()` 就可以查詢數(shù)據(jù)包中的這個(gè) ID,在輪詢時(shí),這個(gè) ID 會(huì)在 `_process_packet()` 期間生效。根據(jù)數(shù)據(jù)包指定的命令,數(shù)據(jù)包處理函數(shù)會(huì)進(jìn)行相應(yīng)操作,Godot 將這些命令定義在枚舉類型 `NetworkCommands`,對(duì)應(yīng)各種 RPC 行為。也就是說(shuō),要讀取 sender id,就應(yīng)該在執(zhí)行 REMOTE_CALL 和 REMOTE_SET 兩個(gè)命令期間進(jìn)行,也就是遠(yuǎn)端 RPC 遠(yuǎn)程調(diào)用方法執(zhí)行時(shí)。
其中的 RAW 命令是更低層的數(shù)據(jù)接收處理,會(huì)觸發(fā) **network_peer_packet** 信號(hào),讓用戶有機(jī)會(huì)參與。
將關(guān)注點(diǎn)轉(zhuǎn)到 process packet 的部分來(lái),它才是 Godot 3.x RPC 遠(yuǎn)程調(diào)用的終點(diǎn)站。在這里,會(huì)觸發(fā) Godot 3.x 兩類遠(yuǎn)程調(diào)用的響應(yīng)方法,即 `rpc()` 和 `rset()` 兩者調(diào)用的遠(yuǎn)程方法。
RPC 執(zhí)行到 `_process_rpc()` 或者 `_process_rset()` 就要進(jìn)行鑒權(quán),如果節(jié)點(diǎn)沒(méi)有相應(yīng)的權(quán)限,則不給予執(zhí)行,還要檢測(cè)數(shù)據(jù)包是否有問(wèn)題等等,并提示錯(cuò)誤信息。`_can_call_mode()` 方法驗(yàn)證 RPC 配置與節(jié)點(diǎn)設(shè)置是否對(duì)應(yīng):
通過(guò)以上分析,最終可以得到開(kāi)始展示的 RPC API 執(zhí)行邏輯圖,簡(jiǎn)化了網(wǎng)絡(luò)傳遞,著重 RPC 執(zhí)行過(guò)程。從發(fā)起 RPC API 調(diào)用 `rpc()` 方法,到遠(yuǎn)端執(zhí)行為線索,`rset()` 最后也會(huì)執(zhí)行到 `_send_rpc()`,由?ENetPeer 進(jìn)行網(wǎng)絡(luò)傳輸。
執(zhí)行到 MultiplayerEnet 的 `put_packet()` 方法后,就即將進(jìn)入 ENet 源代碼的 C API,其中有`enet_peer_send()` 和 `enet_host_broadcast()`,分別用于單端消息和廣播消息的發(fā)送。
另外一個(gè)路線是 rset() -> rsetp(),大體上流程一致。
在數(shù)據(jù)經(jīng)網(wǎng)絡(luò)傳輸之前或接收到數(shù)據(jù),需要相應(yīng)調(diào)用 `encode_variant()` 和 `decode_variant()` 進(jìn)行類型進(jìn)制制表達(dá)轉(zhuǎn)換,涉及到對(duì)象的序列化與反序列化,Serialization vs. Deserialization。Go 語(yǔ)言中又稱為 marshalling 和 unmarshalling。這是一個(gè)新的領(lǐng)域,可以開(kāi)啟另一個(gè)專題進(jìn)行探索。
源代碼文件參考:
- godot-3.5.1-stable\core\io\marshalls.h
- godot-3.5.1-stable\scene\main\node.cpp
- godot-3.5.1-stable\core\io\multiplayer_api.cpp@_send_rpc
- godot-3.5.1-stable\modules\enet\networked_multiplayer_enet.cpp@put_packet
- godot-3.5.1-stable\thirdparty\enet\peer.c@enet_peer_send
- godot-3.5.1-stable\thirdparty\enet\host.c@enet_host_broadcast
- godot-3.5.1-stable\core\io\packet_peer.h
- godot-3.5.1-stable\core\io\packet_peer_udp.cpp
- godot\core\io\net_socket.h
- godot\core\io\marshalls.cpp
- godot\scene\main\multiplayer_api.cpp
- godot\modules\multiplayer\scene_multiplayer.h
- godot\modules\multiplayer\scene_rpc_interface.h
- godot\core\io\packet_peer.h
- godot\modules\multiplayer\scene_cache_interface.h
參考
?[Godot history in images!](https://godotengine.org/article/godot-history-images/)
?[ENet with DTLS encryption in 4.0](https://godotengine.org/article/enet-dtls-encryption/)
?[冗余傳輸機(jī)制的網(wǎng)絡(luò)庫(kù)](https://github.com/Uyouii/Redundancy-Transmission-Protocol)
?[Enet實(shí)現(xiàn)原理](https://blog.csdn.net/gamekit/article/details/107092176)
?[ENet Reliable UDP networking library](https://github.com/lsalzman/enet)
?[Lee Salzman's Page of Random Stuff](http://sauerbraten.org/lee/)
?[ENet v1.3.17 Reliable UDP networking library](http://enet.bespin.org/Features.html)
?[What Every Programmer Needs To Know About Game Networking](https://gafferongames.com/post/what_every_programmer_needs_to_know_about_game_networking/)
?[UDP vs. TCP by Glenn Fiedler](https://gafferongames.com/post/udp_vs_tcp/)
?[User Datagram Protocol (UDP)](https://www.khanacademy.org/a/transmission-control-protocol--tcp)
?[Transmission Control Protocol (TCP)](https://www.khanacademy.org/a/user-datagram-protocol-udp)
?[TCP 圖解千百問(wèn)](https://mp.weixin.qq.com/s/tH8RFmjrveOmgLvk9hmrkw)
?[gRPC Docs](https://grpc.io/docs/)
?[Protocol Buffers](https://github.com/protocolbuffers/protobuf)
?[Protocol Buffers Documentation](https://protobuf.dev)
?[Protobuf in GDScript](https://github.com/oniksan/godobuf)