15

TCPに関しては、入出力完了ポートが何のためにあるのかについてはよく知っています。

しかし、たとえば、FPS ゲームのコーディングを行っている場合や、低レイテンシーが必要な場合は問題が発生する可能性があります。プレーヤーの空間データを失うことを犠牲にしても、最高のプレイ体験を提供するためにプレーヤーに即座に応答する必要があります。行く。UDPを使用する必要があることが明らかになり、座標の更新を頻繁に送信するだけでなく、チャット メッセージなどのイベントを処理するために、ある程度信頼性の高いプロトコルを実装する必要があります (TCP は UDP でパケット損失を引き起こすため、これら 2 つを混在させないようにする必要があります)。または、パケット損失が重大な場合がある発砲。

MMOFPSゲームに適用されるパフォーマンスを目指しているとしましょう。これは、1つの永続的な世界で何百人ものプレイヤーに会うことを可能にし、銃で戦うことは別として、チャットメッセージなどを介して通信できるようにします-このようなものは実際に存在します.うまく動作します - PlanetSide 2 をチェックしてください。

ネット上の多くの記事 (msdn など) では、ソケットのオーバーラップが最適であり、IOCP は最高レベルの概念であると述べていますが、TCP 以外のプロトコルを使用する場合を区別していないようです。

したがって、そのようなサーバーを開発するときに使用される I/O 手法に関する信頼できる情報はほとんどありません。これを見てきましたが、このトピックは非常に物議をかもしているようで、これも見ましが、最初のリンクでの議論を考慮してください。 、2番目の仮定に従うべきかどうか、UDPでIOCPを使用する必要があるかどうか、そしてそうでない場合、 UDPに関して最もスケーラブルで効率的なI / Oコンセプトは何ですか

それとも、時期尚早の別の最適化を行っているだけで、現時点では事前に考える必要はありませんか?

gamedev.stackexchange.com に投稿することを考えましたが、この質問は汎用ネットワーキングに当てはまると思います。

4

7 に答える 7

15

これを使用することはお勧めしませんが、技術的に UDP データグラムを受信する最も効率的な方法は、単にブロックすることですrecvfrom(またはブロックするWSARecvFrom場合)。もちろん、そのための専用スレッドが必要です。そうしないと、ブロックしている間はほとんど何も起こりません。

TCP 以外では、プロトコルに接続が組み込まれておらず、境界が定義されていないストリームもありませんつまり、受信するすべてのデータグラムで送信者のアドレスを取得し、メッセージ全体を取得するか、何も取得しないことを意味します。いつも。例外なく。
今、ブロック中recvfromカーネルへの 1 つのコンテキスト スイッチと、何かを受信したときに 1 つのコンテキスト スイッチバックを意味します。同時に複数の読み取りをオーバーラップさせても、速度は速くなりません。これは、ネットワーク上で同時に到達できるデータグラムが 1 つだけであるためです。これは、最も制限的な要因です (CPU 時間はボトルネックではありません!)。IOCP を使用するということは、受信用に 2 つ、通知用に 2 つ、少なくとも 4 つのコンテキスト スイッチを意味します。NtTestAlertあるいは、APC キューを実行する必要があるか、または実行する必要があるため、完了コールバックと受信をオーバーラップしてもあまり良くありませんSleepEx。したがって、少なくとも 2 つの余分なコンテキスト スイッチがあります (ただし、すべての通知を一緒にすると +2 にすぎず、偶然にとにかくすでに寝ています)。

ただし
、IOCP と重複した読み取りを使用することは、最も効率的でなくても、それを行うための最良の方法です。完了ポートは TCP の使用に関係なく、UDP でも問題なく動作します。オーバーラップ読み取りを使用している限り、使用するプロトコルは問題ではありません (ネットワークかディスクか、その他の待機可能または警告可能なカーネル オブジェクトであっても)。
また、完了ポートのために余分に数百サイクルを消費するかどうかは、レイテンシーまたは CPU 負荷のどちらにもあまり関係ありません。ここでは、「ナノ」対「ミリ」について話しています。100 万分の 1 です。一方、完了ポートは全体的に非常に快適で、健全で効率的なシステムです。

たとえば、時間内に ACK を受信しなかった場合に再送信するためのロジックを簡単に実装できます (信頼性の形式が必要な場合に行う必要があります。UDP はそれを行いません)。キープアライブも同様です。キープアライブの場合は、何か
を受信するたびにリセットする待機可能なタイマー (15 秒または 20 秒後に起動する可能性があります) を追加します。完了ポートがこのタイマーがオフになったことを通知した場合、接続が切断されていることがわかります。 再送信の場合、たとえば にタイムアウトを設定すると、起動するたびに、かなり古く、まだ ACK されていないすべてのパケットを見つけることができます。 ロジック全体が 1 か所で発生するので、非常に便利です。用途が広く、効率的で、間違いを犯しにくいです。
GetQueuedCompletionStatus

完了ポートで複数のスレッド (実際には、CPU のコア数よりも多くのスレッド) をブロックすることもできます。多くのスレッドは賢明でない設計のように思えますが、実際には最善の方法です。

完了ポートは、後入れ先出しの順序で N 個のスレッドまでウェイクします。N は、別のことを行うように指示しない限り、コアの数です。これらのスレッドのいずれかがブロックされた場合、未処理のイベントを処理するために別のスレッドが起動されます。これは、最悪の場合、余分なスレッドが短時間実行される可能性があることを意味しますが、これは許容範囲です。平均的なケースでは、実行する作業がある限りプロセッサの使用率を 100% 近くに保ち、それ以外の場合はゼロに保ちます。これは非常に優れています。LIFO ウェイクはプロセッサ キャッシュに有利であり、スレッド コンテキストの切り替えを低く抑えます。

これは、着信データグラムをブロックして待機し、それを処理 (復号化、解凍、ロジックの実行、ディスクからの読み取りなど) できることを意味します。別のスレッドは、次のマイクロ秒で来る可能性のある次のデータグラムをすぐに処理する準備が整います。重複したディスク IO を同じ完了ポートで使用することもできます。タスクに分割できるコンピューティング作業 (AI など) がある場合は、PostQueuedCompletionStatusそれらを完了ポートに手動でポスト ( ) することもでき、並列タスク スケジューラを無料で利用できます。あなたがしなければならないのは、OVERLAPPEDを構造体にラップし、その後にいくつかの追加データがあり、認識できるキーを使用することだけです。スレッドの同期について心配する必要はありません。魔法のように機能します (厳密には、OVERLAPPED独自の通知を投稿するときのカスタム構造では、渡した構造で動作しますが、オペレーティング システムに嘘をつくのは好きではありません。

たとえば、ディスクから読み取る場合など、ブロックするかどうかはそれほど重要ではありません。時々これが起こり、あなたはそれを助けることができません。つまり、1 つのスレッドがブロックされても、システムは引き続きメッセージを受信し、それに反応します! 完了ポートは、必要に応じてプールから別のスレッドを自動的にプルします。

TCP が UDP でパケットロスを誘発することについては、これは私が都市伝説と呼ぶ傾向があるものです (多少正しいですが)。しかし、この一般的なマントラの言い方は誤解を招くものです。むかしむかし、ルーターがTCPを優先してUDP を破棄し、それによってパケット損失を誘発するというのは本当だったかもしれません (この件に関する研究は存在しますが、これは 10 年近く前のものです)。しかし、それは確かに今日では当てはまりません。
より真実な見方は、何でも送信するとパケットロスが発生します。TCP は TCP でパケット損失を誘発し、UDP は TCP でパケット損失を誘発し、その逆も同様です。これは通常の状態です (ちなみに、これは TCP が輻輳制御を実装する方法です)。ルーターは、通常、他のプラグのケーブルが「サイレント」である場合に 1 つの着信パケットを転送し、ハード デッドライン (多くの場合、バッファーは意図的に小さい) でいくつかのパケットをキューに入れます。オプションで、何らかの形式の QoS を適用することもできます。単純に黙って他のすべてをドロップします
かなり厳しいリアルタイム要件 (VoIP、ビデオ ストリーミングなど) を持つ多くのアプリケーションは、現在 UDP を使用しており、1 つまたは 2 つのパケット損失にはうまく対処できますが、重大で繰り返し発生するパケット損失はまったく好みません。それでも、多くの TCP トラフィックがあるネットワークでは問題なく動作することは明らかです。私の電話 (何百万人もの人々の電話と同様) は VoIP のみで動作し、データはインターネット トラフィックと同じルーターを経由します。どんなに頑張っても、TCP でドロップアウトを引き起こす方法はありません。
その日常の観察から、TCP を支持して UDP がドロップされていないことは確かです。どちらかといえば、QoS は TCP よりも UDP を優先するかもしれませんが、不利になることはほとんどありません。
そうしないと、Web サイトを開くとすぐに VoIP などのサービスが途切れてしまい、DVD ISO ファイルのサイズの何かをダウンロードすると、まったく利用できなくなります。

編集:
IOCP を使用したシンプルな生活がどのようになるかについてのアイデアを提供するために (多少簡素化され、ユーティリティ関数が欠落しています):

for(;;)
{
    if(GetQueuedCompletionStatus(iocp, &n, &k, (OVERLAPPED**)&o, 100) == 0)
    {
        if(o == 0) // ---> timeout, mark and sweep
        {
            CheckAndResendMarkedDgrams();  // resend those from last pass
            MarkUnackedDgrams();           // mark new ones
        } 
        else
        {   // zero return value but lpOverlapped is not null:
            // this means an error occurred
            HandleError(k, o);
        }
        continue;
    }

    if(n == 0 && k == 0 && o == 0)
    {
        // zero size and zero handle is my termination message
        // re-post, then break, so all threads on the IOCP will
        // one by one wake up and exit in a controlled manner
        PostQueuedCompletionStatus(iocp, 0, 0, 0);
        break;
    }
    else if(n == -1) // my magic value for "execute user task"
    {
        TaskStruct *t = (TaskStruct*)o;
        t->funcptr(t->arg);
    }
    else
    {
        /* received data or finished file I/O, do whatever you do */
    }
}

完了メッセージの処理、ユーザー タスク、およびスレッド制御の両方のロジック全体が 1 つの単純なループで発生し、あいまいなものや複雑なパスがなく、すべてのスレッドがこの同じ同一のループのみを実行することに注意してください。
同じコードは、1 つのソケットにサービスを提供する 1 つのスレッド、または 5,000 のソケットにサービスを提供する 50 のプールのうちの 16 のスレッド、10 のオーバーラップ ファイル転送、および並列計算の実行に対して機能します。

于 2012-07-09T10:03:22.517 に答える
7

ネットワークプロトコルとしてUDPを使用する多くのFPSゲームのコードを見てきました。

標準的な解決策は、1つのゲームフレームを更新するために必要なすべてのデータを1つの大きなUDPパケットで送信することです。そのパケットには、フレーム番号とチェックサムが含まれている必要があります。もちろん、パケットは圧縮する必要があります。

通常、UDPパケットには、プレーヤーの近くにあるすべてのエンティティの位置と速度、送信されたチャットメッセージ、および最近のすべての状態の変化が含まれています。(例:作成された新しいエンティティ、破棄されたエンティティなど)

次に、クライアントはUDPパケットをリッスンします。フレーム番号が最も大きいパケットのみを使用します。したがって、順序が正しくないパケットが表示された場合、古いパケットは単に無視されます。

チェックサムが間違っているパケットも無視されます。

各パケットには、クライアントのゲーム状態をサーバーと同期するためのすべての情報が含まれている必要があります。

チャットメッセージは複数のパケットで繰り返し送信され、各メッセージには一意のメッセージIDがあります。たとえば、同じチャットメッセージを1秒に相当するフレームで再送信します。クライアントがチャットメッセージを60回取得した後、それを見逃した場合、ネットワークチャネルの品質が低すぎてゲームをプレイできません。クライアントは、まだ表示されていないメッセージIDを持つUDPパケットで取得したメッセージを表示します。

同様に、作成または破棄されるオブジェクトの場合。作成または破棄されたすべてのオブジェクトには、サーバーによって設定された一意のオブジェクトIDがあります。オブジェクトに対応するオブジェクトIDが以前に操作されていない場合、オブジェクトは作成または破棄されます。

したがって、ここで重要なのは、データを冗長的に送信し、すべての状態遷移をサーバーによって設定された一意のIDにキー設定することです。

@edit:別の投稿者は、チャットメッセージの場合、別のポートで別のプロトコルを使用することをお勧めします。そして、彼らはおそらくそれが最適であるということについて正しいかもしれません。これは、遅延が重要ではないメッセージタイプの場合ですが、信頼性がより重要であるため、別のポートを開いてTCPを使用することをお勧めします。しかし、それは後の演習として残しておきます。ゲームで最初は1つのチャネルだけを使用し、後でさまざまな障害モードで複数のポート、複数のチャネルの変動を把握する方が、確かに簡単でクリーンです。(たとえば、UDPチャネルは機能しているが、チャットチャネルがダウンした場合はどうなりますか?一方のポートを開くことに成功し、もう一方のポートを開くことができなかった場合はどうなりますか?)

于 2012-07-09T13:02:58.157 に答える
4

私がクライアントに対してこれを行ったとき、私たちはENetをベースの信頼できる UDP プロトコルとして使用し、これをゼロから再実装してサーバー側に IOCP を使用し、クライアント側には自由に利用できる ENet コードを使用しました。

IOCP は UDP で正常に動作し、処理している可能性のあるすべての TCP 接続とうまく統合されます (TCP、WebSocket、または UDP クライアント接続とサーバー ノード間の TCP 接続があり、これらすべてを同じスレッド プールにプラグインすることができます。 want は便利です)。

絶対的なレイテンシーと UDP パケット処理速度が最も重要な場合 (実際にはそうではない可能性があります)、新しい Server 2012 RIO API を使用する価値があるかもしれませんが、まだ確信が持てません (いくつかの予備的なパフォーマンス テストといくつかのパフォーマンス テストについては、こちらを参照してください)。例のサーバー)。

1 回の呼び出しで複数のデータグラムを引き戻すことができるため、データグラムごとのコンテキスト スイッチが減少するため、インバウンド データを処理するために GetQueuedCompletionStatusEx() を使用することを検討することをお勧めします。

于 2012-07-10T12:13:00.727 に答える
3

私は、PlanetSide のような古いゲームを、最新のネットワーク実装のパラゴンとして保持するつもりはありません。特に、彼らのネットワーク ライブラリの内部を見たことはありません。:)

コミュニケーションの種類が異なれば、必要な方法論も異なります。上記の回答の 1 つは、フレーム/位置の更新とチャット メッセージの違いについて説明していますが、両方に同じトランスポートを使用するのはおそらくばかげているとは認識していません。テキスト形式のチャットでは、チャットの実装とチャット サーバーの間に接続された TCP ソケットを使用する必要があります。議論しないで、ただそれをしてください。

そのため、到着する UDP パケットを介して更新を行うゲーム クライアントの場合、ネットワーク アダプターからカーネルを経由してアプリケーションに至る最も効率的なパスは、(ほとんどの場合) ブロッキング recv になります。ネットワークからパケットをリッピングし、それらの有効性を検証し (chksum の一致、シーケンス番号の増加、その他のチェック)、データを内部オブジェクトに逆シリアル化し、内部キューのオブジェクトをアプリケーション スレッドにキューイングするスレッドを作成します。そのような更新を処理します。

しかし、私の言葉を鵜呑みにしないでください。テストしてください! ブロッキング スレッドとキューを使用してオブジェクトを配信し、3 種類または 4 種類のパケットを受信して​​デシリアライズできる小さなプログラムを作成し、完了ルーチンでデシリアライズとキューイングを使用して、単一のスレッドと IOCP を使用してプログラムを書き直します。十分な数のパケットを通過させて実行時間を分単位で取得し、どれが最速かをテストします。テスト アプリの何か (スレッドなど) がキューからオブジェクトを消費していることを確認して、相対的なパフォーマンスの全体像を把握します。

2 つのテスト プログラムが完了したら、ここに投稿して、どちらがうまくいったかをお知らせください。どれが一番速かったか、どちらが将来維持したいか、どれが機能するまでに最も時間がかかったか、など。

于 2012-07-10T00:02:17.070 に答える
2

多数の同時接続をサポートしたい場合は、イベント駆動型ネットワーク アプローチを使用する必要があります。libev ( nodeJS で使用)libeventの 2 つの優れたライブラリを知っています。それらは非常に携帯性が高く、使いやすいです。何百もの並列 TCP/UDP(DNS) 接続をサポートするアプリケーションで libevent を使用することに成功しました。

イベント駆動型のネットワーク I/O を使用することは、サーバーでの時期尚早な最適化ではなく、デフォルトの設計パターンであるべきだと思います。手早くプロトタイプを実装したい場合は、より高水準の言語から始める方がよいかもしれません。JavaScript にはnodeJSがあり、Python にはTwistedがあります。どちらも個人的におすすめです。

于 2012-07-15T07:24:07.387 に答える