これを使用することはお勧めしませんが、技術的に 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 のオーバーラップ ファイル転送、および並列計算の実行に対して機能します。