基於 KCP 的即時遊戲通訊案例研究
在即時線上遊戲中,玩家的輸入傳送到伺服器以及結果返回所花費的延遲時間非常重要。TCP 雖然能保證可靠性與傳輸順序,但在等待遺失資料的同時,後續到達資料的傳遞也可能會延遲。UDP 不保證傳輸順序與到達,因此若要處理需要可靠性的訊息,就需要有相應的補強機制。
KCP 主要是基於 UDP 提供重傳與順序控制的協定。本文將探討 KCP 縮短延遲時間的原理,並以遊戲通訊案例為基礎,整理連線、驗證過程與訊息結構。驗證流程與應用程式訊息格式可能會依據實作方式而有所不同。
KCP 是如何縮短延遲時間的?
KCP 透過使用接收確認與重傳的 ARQ 方式來實現可靠傳輸。透過調整重傳政策與更新週期,可以減少遺失後的等待時間,但根據設定的不同,頻寬與 CPU 使用量也可能會增加。
實際效能會根據網路狀態與設定而有所不同。
- 選擇性重傳: 根據接收確認資訊,挑選遺失的資料進行重傳。TCP 使用 SACK 時也能進行選擇性重傳,因此這不能視為 KCP 獨有的功能。
- 調整重傳等待時間: 在快速模式下,當重複發生遺失時,會縮小重傳等待時間的增長幅度,以加快復原速度。
- 快速重傳: 發送端透過觀察後續封包的 ACK 是否到達,來推測前面封包的遺失。若滿足設定的條件,就會在逾時之前進行重傳。
- 短更新週期: 縮短更新與傳輸週期,減少等待 ACK 與重傳資料發送的時間。實際傳輸時間點也會受到應用程式呼叫週期的影響。
由於 KCP 同樣會依序傳遞資料,因此若前面的資料遺失,後面資料必須等待的現象仍然存在。其核心不在於消除這種等待,而在於更快地復原遺失的資料。
連線與驗證過程
與遊戲伺服器通訊之前,需要先進行帳號驗證並選擇要連線的伺服器。以下是簡化後的流程範例,展示遊戲伺服器如何驗證透過網頁驗證所取得的權杖。
在此範例中,網頁伺服器負責驗證帳號,而遊戲伺服器則負責驗證權杖以啟動遊戲工作階段。區分這兩個階段的角色,有助於理解登入之後的通訊流程。
KCP 本身不具備加密或使用者驗證功能。若需要安全性,必須建構額外的安全層。Protobuf 也是一種資料序列化格式,因此僅使用 Protobuf 來表示金鑰或權杖,並不能保證安全傳輸。
KCP 標頭與應用程式訊息
KCP 區段由 24 位元組的標頭與資料區域組成。下表中從 conv 到 len 的部分為 KCP 標頭,而資料區域內的 PackLen、MsgType、CmdId 等則是本案例中所探討的應用程式訊息格式。此內部格式並非 KCP 的共通規格。
表格中的一格代表 1 位元組。若應用程式訊息較大,可能會被分割成多個 KCP 區段,因此內部表格可視為展示重新組合後訊息結構的範例。
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ||||||||||||||||||||||||||||||||
| conv | cmd | frg | wnd | ||||||||||||||||||||||||||||||||||||
| ts | sn | ||||||||||||||||||||||||||||||||||||||
| una | len | ||||||||||||||||||||||||||||||||||||||
Data: 應用程式訊息範例
|
|||||||||||||||||||||||||||||||||||||||
在 KCP 標頭中,conv 是交談識別碼,cmd 是指令種類,frg 是分割資訊,wnd 是接收視窗大小。ts 代表時間戳記,sn 代表區段順序編號,una 代表下一個要接收的順序編號,len 則表示資料區域的長度。
應用程式訊息的欄位可以作如下解讀。長度與檢驗和的計算範圍必須確認各實作方式的規則。
PackLen:表示訊息長度的欄位。MsgType:訊息類型與旗標資訊。seqNo:應用程式訊息的順序編號。rpcId:用來串聯遠端程序呼叫請求與回應的識別碼。CmdId:表示要如何解讀 Payload 的訊息種類識別碼。crc32:用於錯誤偵測的檢驗和。無法替代密碼學驗證。Payload:序列化後的訊息資料。若有套用加密,需先解密後再進行解讀。
使用 Protobuf 解讀 Payload
本案例的 Payload 包含以 Protocol Buffers (Protobuf) 序列化後的資料。Protobuf 是一種以二進位表示結構化資料的格式,使用編號來取代欄位名稱。雖然它可以簡潔地表示遊戲訊息,但其大小與處理效能會依資料結構與實作方式而有所不同。
首先在 .proto 檔案中定義訊息結構,並使用 protoc 產生符合所使用語言的程式碼。發送端使用此程式碼將資料序列化,而接收端則使用相同的訊息定義進行反序列化。
例如,可以定義一個包含兩個字串的訊息,如下所示:
假設應用程式中將 CmdId 與訊息種類的對應關係訂為如下。這個編號並不是 Protobuf 自動賦予的值,而是應用程式所訂定的規則。
當把 message 為 Hello World!、name 為 AZA.GG 的訊息進行序列化時,會變成以下的位元組序列。底下的 Payload 是加密前的資料。
每個字串前面都會加上包含欄位編號與資料類型的標籤,以及字串的位元組長度。0a 與 12 分別是第 1 欄位與第 2 欄位的標籤,而 0c 與 06 則分別代表字串長度 12 與 6。
接收端確認 CmdId 為 11 後,便會將 Payload 解讀為 HelloWorld。像這樣,KCP 負責傳輸與重傳,應用程式標頭負責區分訊息,而 Protobuf 則負責資料表示。