DiarkisとC#によるRUDP通信の実装

はじめに

「RUDP」とは「ReliableUDP(User Datagram Protocol)」の略で、UDPメッセージの通信に利用されます。
UDPはTCPと異なり、転送効率を優先して信頼性を落としたプロトコルで、メッセージは転送中に失われる可能性があります。
RUDPは、UDPを拡張し、メッセージを失うことなく送信先に届けるようにするものです。
本エントリでは、DiarkisでRUDP通信を実装することを紹介します。
RUDPそのものについては、IETFのドラフトや各社の実装を取り上げた個別の記事を参照してください。
Diarkisは、当社の提供する大規模マルチプレーヤーアプリのための通信エンジンであり、UDP・TCP・HTTPなどの標準プロトコルと並んで、独自のRUDPを実装しています。

DiarkisにおけるRUDP実装

DiarkisのRUDP実装は、以下の機能を備えています。
  1. 再送機構による信頼性の高い通信
  1. パケット到達順序の保証
  1. 大きなサイズのパケットを送受信する際の分割・結合
Diarkisにおいては、RUDPもしくはプレーンなUDPどちらで送信するかをメッセージ送信時に選択できます。
送信するメッセージには、転送モードを示すヘッダが付与されます。
メッセージフラグには以下のものがあります。
  1. プレーンUDPメッセージ
  1. RUDP初期化パケット
  1. RUDPデータパケット
  1. RUDP応答確認パケット
  1. RUDP再送データパケット
  1. RUDP再送確認パケット
 

1. 再送機構による信頼性の高い通信

パケット送信側は、受信したパケットごとに受信者から応答確認パケットを受け取ります。
受信者からの確認応答パケットが受け取れないメッセージは、設定された時間が経過すると再送信されます。
再送信の間隔は、受信側の悪意ある挙動を防ぐ観点から、試行回数が増えるごとに長くなります。
同じパケットに対する再送信が複数失敗した場合、送信者は「受信者がいなくなった」とみなして送信を終了します。

2. パケット到達順序の保証

プレーンなUDPプロトコルは、パケットの到達順序を保証しませんが、DiarkisにおけるRUDPは、TCPのようにパケットの到達順序を保証する機能が追加されています。
パケットが失われて再送が発生した場合、その次のパケットは、前のパケットが受信されるまで処理されません。

3. 大きなサイズのパケットを送受信する際の分割・結合

多くのクラウドサービス(GCP、AWS、Azure)では、UDPのパケットサイズ(MTU)を1,400バイト程度(各クラウドにより異なる)に制限しており、MTUを超えるパケットは強制的にドロップされてしまいます。 また、多くの商用ルーターにおいても大きなパケットを分割することがあります。 分割されたパケットのうち1つでも失われると、残りも失われるためパケットロス発生の可能性が高まることになります。
Diarkisでは、この問題を回避するため1,400バイトを超える大きなパケットを送信する場合、DiarkisのRUDPはパケットを複数の個別のパケットに分割して送信し、受信側でこれらのパケットを、元のパケットに結合する機能を備えています。

実装に関する技術詳細

DiarkisのRUDPクライアントは2つのスレッドを生成します。一方のスレッドはパケットの送信を担当し、もう一方のスレッドはパケットの受信を担当します。メインスレッドは、送信するパケットを送信バッファーに送信して、送信者スレッドがピックアップして送信できるようにします。受信者バッファは、送信されたパケットの順序でポップされる受信パケットをスタックします。パケットの並べ替えと分割パケットの再構築は受信者スレッドで行われますが、受信したパケットの実際の処理はメインスレッドでのみ行われます。同様に、メインスレッドは実際にパケットを送信することはなく、送信者スレッドのバッファーにパケットを送信するだけです。

2つのスレッドを利用する理由

パケットが到着するのを待っているときは、アプリケーションはブロックして待機する必要があるわけですが、メインスレッドで待機するとアプリケーション全体が停止してしまいます。
これを回避するため、パケットの受信を待って処理するための受信スレッドが必要になります。
パケットを送信するために別のスレッドを使用する理由は、受信スレッドはブロックして待機するため、同じスレッドに送信処理を含めることができないためです。
また、パケットの送信もブロッキング操作であるため、パケットを送信するための専用スレッドも必要になります。

送信パケットに以前のパケットを結合して送信しない理由

以前に送信したパケットを、送信するたびに組み合わせていた場合、パケットのサイズが大きくなるわけですが、クラウドサービスなどのMTU制限がある環境で大きなパケットを送ろうとした場合、パケットロスが起こる可能性が高まります。
このため他のRUDP実装に見られる、送信済みのパケットを次の送信パケットにも含めるといった手法をDiarkisは利用していません。
Diarkisの実装するパケット分割・結合とリトライ機能は、MTU制限のある環境でも高い信頼性を発揮します。
 
RUDP Communication Diagram

C#のSocket.Pollを利用してパケットを受信する

C#のSocketクラスは、着信UDPメッセージをブロックして待機するPollメソッドを提供します。
Thread.Sleepを使用してループを作成する代わりに、このメソッドを使用することでパフォーマンスが向上します(「待機中」のCPU消費量が減ります)。
Socketクラスの代わりにUdpClientクラスを直接使用している場合は、次のようにPollメソッドにアクセスできます。

おわりに

RUDPそのものはIETFのドラフトに定義されていますが、多くのミドルウェアやアプリケーションが実装するものはその定義に従ったものではありません。
Diarkisが独自に実装しているRUDPは、ユーザーのニーズに合わせて、例えばパケット到達順序の機能が必要ない場合は省略するといった、要件や目的に合わせて利用することが可能です。