KCPベースのリアルタイムゲーム通信の事例研究

リアルタイムオンラインゲームでは、プレイヤーの入力がサーバーに伝送され、その結果が返ってくるまでの遅延時間が重要である。TCPは信頼性と転送順序を保証するが、消失したデータを待っている間に後ろに到着したデータの伝送も遅れることがある。UDPは転送順序と到着を保証しないため、信頼性が必要なメッセージを扱うには、これを補完する仕組みが必要である。

KCPは主にUDP上で再送と順序制御を提供するプロトコルである。この記事では、KCPが遅延時間を短縮する原理を調べ、ゲーム通信の事例をもとに接続・認証のプロセスとメッセージ構造を整理する。認証手順とアプリケーションメッセージの形式は、実装によって異なる場合がある。

KCPはどのように遅延時間を短縮するのか?

KCPは、受信確認と再送を使用するARQ方式で信頼性のある転送を実装する。再送ポリシーと更新周期を調整して消失後の待ち時間を減らすことができ、設定によって帯域幅とCPU使用量が増加する場合がある。

実際のパフォーマンスは、ネットワークの状態と設定によって異なる。

  • 選択적 재전송: 受信確認情報をもとに消失したデータを選んで再送する。TCPもSACKを使用すれば選択的再送が可能であるため、これをKCP独自の機能と見なすことはできない。
  • 再送待ち時間の調整: 高速モードでは、消失が繰り返されたときに再送待ち時間が増加する幅を縮小し、復旧を早める。
  • 高速再送: 送信側は、後ろのパケットのACKが到着するのを見て、前のパケットの消失を推測する。設定された条件を満たすと、タイムアウト前に再送する。
  • 短い更新周期: 更新・転送周期を短縮し、ACKや再送データが送信されるまでの待ち時間を減らすことができる。実際の転送タイミングは、アプリケーションの呼び出し周期にも影響を受ける。

KCPも順序通りにデータを伝達するため、前のデータが消失すると後ろのデータが待機する現象は残る。核心は、この待機をなくすことではなく、消失したデータをより迅速に復旧することにある。

接続と認証のプロセス

ゲームサーバーと通信する前には、アカウント認証と接続するサーバーの選択が必要である。以下は、ウェブ認証で受け取ったトークンをゲームサーバーで確認する流れを単純化した例である。

sequenceDiagram participant Client as クライアント participant Web as ウェブ認証サーバー participant Game as ゲームサーバー Client->>Web: サーバー一覧照会 (HTTPS) Web-->>Client: サーバー一覧返却 Client->>Web: アカウント認証 (HTTPS) Web-->>Client: ログイン用トークン発行 Note over Client: 接続するサーバーを選択 Client->>Game: 接続要求および状態確認 Game-->>Client: サーバー状態返却 Note over Client,Game: 認証された鍵交換などで暗号化セッション準備 Client->>Game: 保護されたチャネルでログイン用トークン伝達 Game-->>Client: トークン検証および接続承認 Note over Client,Game: ゲームメッセージ送受信開始

この例で、ウェブサーバーはアカウントを認証し、ゲームサーバーはトークンを検証してゲームセッションを開始する。2つの段階の役割を区別すると、ログイン以降の通信の流れを理解しやすい。

KCP自体には暗号化やユーザー認証機能がない。セキュリティが必要な場合は、個別のセキュリティ層を構築する必要がある。Protobufも同様にデータをシリアライズする形式であるため、Protobufで鍵やトークンを表現するだけでは安全な伝送は保証されない。

KCPヘッダーとアプリケーションメッセージ

KCPセグメントは24バイトのヘッダーとデータ領域で構成される。下の表の conv から len までがKCPヘッダーであり、データ領域内の PackLen、MsgType、CmdId などは、この事例で扱うアプリケーションメッセージの形式である。この内部形式はKCPの共通規格ではない。

表は1マスを1バイトで表す。アプリケーションメッセージが大きい場合、複数のKCPセグメントに分割される可能性があるため、内部の表は再組み立てされたメッセージの構造を示す例として読み取ればよい。

KCPセグメントとアプリケーションメッセージ構造の例
0 1 2 3 4 5 6 7
conv cmd frg wnd
ts sn
una len
Data: アプリケーションメッセージの例
0 1 2 3 4 5 6 7
PackLen 0 MsgType seqNo
rpcId CmdId crc32
Payload

KCPヘッダーで、conv は会話識別子、cmd はコマンドの種類、frg は分割情報、wnd は受信ウィンドウサイズである。ts はタイムスタンプ、sn はセグメント順序番号、una は次に受信する順序番号、len はデータ領域の長さを表す。

アプリケーションメッセージのフィールドは次のように解釈できる。長さとチェックサムの計算範囲は、各実装の規則を確認する必要がある。

  • PackLen: メッセージの長さを表すフィールド。
  • MsgType: メッセージの種類とフラグ情報。
  • seqNo: アプリケーションメッセージの順序番号。
  • rpcId: リモートプロシージャコールの要求と応答を結びつける識別子。
  • CmdId: ペイロードを解釈するメッセージの種類を表す識別子。
  • crc32: エラー検出用のチェックサム。暗号学的認証の代わりにはならない。
  • Payload: シリアライズされたメッセージデータ。暗号化を適用した場合は復号化した後に解釈する。

Protobufによるペイロードの解釈

この事例のペイロードは、Protocol Buffers(Protobuf)でシリアライズされたデータを格納する。Protobufは構造化データをバイナリで表現する形式であり、フィールド名の代わりに番号を使用する。ゲームメッセージを簡潔に表現できるが、サイズと処理性能はデータ構造と実装によって異なる。

まず .proto ファイルにメッセージ構造を定義し、protoc で使用する言語に合ったコードを生成する。送信側はこのコードでデータをシリアライズし、受信側は同じメッセージ定義を使用してデシリアライズする。

例えば、次のように文字列2つを格納するメッセージを定義できる。

proto
syntax = "proto3";

message HelloWorld {
  string message = 1;
  string name = 2;
}

アプリケーションで CmdId とメッセージ種類の対応関係を次のように定めたと仮定する。この番号は、Protobufが自動的に付与する値ではなく、アプリケーションが定めた規則である。

text
ServerStatusNotice = 10;
HelloWorld = 11;
GameBanNotice = 12;

message が Hello World!、name が AZA.GG であるメッセージをシリアライズすると、次のバイト列になる。以下のペイロードは暗号化前のデータである。

text
CmdId: 11
Payload: 0a 0c 48 65 6c 6c 6f 20 57 6f 72 6c 64 21 12 06 41 5a 41 2e 47 47

各文字列の前には、フィールド番号とデータ型を含むタグ、文字列のバイト長が付く。0a と 12 はそれぞれ1番目と2番目のフィールドのタグであり、0c と 06 は文字列の長さである12と6を表す。

text
Field 1: タグ 0a | 長さ 0c (12) | "Hello World!"
Field 2: タグ 12 | 長さ 06 (6)  | "AZA.GG"

受信側は CmdId が 11 であることを確認し、ペイロードを HelloWorld として解釈する。このように、KCPは転送と再送を、アプリケーションヘッダーはメッセージの区別を、Protobufはデータ表現を担当する。