何が良いですか?多数の小さなTCPパケット、または1つの長いパケットですか?[閉まっている]


16

私が作っているゲームのために、サーバーとの間で大量のデータを送受信しています。

現在、次のような位置データを送信しています。

sendToClient((("UID:" + cl.uid +";x:" + cl.x)));
sendToClient((("UID:" + cl.uid +";y:" + cl.y)));
sendToClient((("UID:" + cl.uid +";z:" + cl.z)));

明らかに、それぞれのX、Y、およびZ値を送信しています。

このようなデータを送信する方が効率的ですか?

sendToClient((("UID:" + cl.uid +"|" + cl.x + "|" + cl.y + "|" + cl.z)));

2
私の経験では、パケット損失は通常5%未満です。
ムカホ

5
sendToClientは実際にパケットを送信しますか?もしそうなら、どうやってそれをさせたのですか?
user253751

1
@mucaho自分で測定したことはありませんが、TCPのエッジが粗いのは驚きです。0.5%以下のようなものを望んでいました。
パンツァークライシス

1
@Panzercrisis私はあなたに同意しなければなりません。個人的には、5%の損失は容認できないと感じています。たとえば、ゲーム内で新しい船が出現するなど、私のようなものを送信する場合、そのパケットが受信されない可能性が1%でも、目に見えない船を取得するため、悲惨です。
joehot200

2
他のコメントで指摘されているように、私は5%を上限として意味しました:)実際にははるかに優れています。
ムカホ

回答:


28

TCPセグメントには非常に多くのオーバーヘッドがあります。1つのTCPパケットで10バイトのメッセージを送信すると、実際に送信します。

  • 16バイトのIPv4ヘッダー(IPv6が一般的になると40バイトに増加します)
  • 16バイトのTCPヘッダー
  • 10バイトのペイロード
  • 使用されるデータリンクおよび物理層プロトコルの追加オーバーヘッド

10バイトのデータを転送するために42バイトのトラフィックが発生します。したがって、使用可能な帯域幅の25%未満しか使用しません。そして、それはまだイーサネットやPPPoEのような低レベルのプロトコルが消費するオーバーヘッドを考慮していません(しかし、これらは非常に多くの選択肢があるので推定するのは難しいです)。

また、多くの小さなパケットは、ルーター、ファイアウォール、スイッチ、およびその他のネットワークインフラストラクチャ機器により大きな負荷をかけるため、サービスプロバイダーとユーザーが高品質のハードウェアに投資しない場合、これは別のボトルネックになる可能性があります。

そのため、使用可能なすべてのデータを1つのTCPセグメントで一度に送信するようにしてください。

パケット損失の処理について:TCPを使用する場合、心配する必要はありません。プロトコル自体は、失われたパケットが再送信され、パケットが順番に処理されることを保証するため、送信するすべてのパケットは反対側に到着し、送信した順に到着すると想定できます。これの代価は、パケット損失がある場合、プレーヤーがかなりの遅れを経験することです。なぜなら、1つのドロップされたパケットは、再要求されて受信されるまでデータストリーム全体を停止するからです。

これが問題になる場合は、常にUDPを使用できます。しかし、その後、あなたは失われたとアウトオブオーダーメッセージ(メッセージということは、少なくとも保証のための独自のソリューションを見つける必要がありませんが、到着した完全で無傷の到着を)。


1
TCPがパケット損失を回復する方法のオーバーヘッドは、パケットのサイズに応じて価値がありますか?
パンツァークライシス

@Panzercrisis再送信が必要な大きなパケットがある場合のみ。
フィリップ

8
OSは、ほぼ確実にNagles Algorithm en.wikipedia.org/wiki/Nagle%27s_algorithmを発信データに適用します。これは、アプリ内で別々の書き込みを行ったり、それらを結合したりしても、それらが結合されることを意味します実際にTCPを介してそれらを渡す前に。
バリティ

1
@Vality私が使用したほとんどのソケットAPIでは、各ソケットのnagleをアクティブまたは非アクティブにできます。ほとんどのゲームでは、通常、帯域幅を節約するよりも低レイテンシーの方が重要なので、無効にすることをお勧めします。
フィリップ

4
Nagleのアルゴリズムは1つですが、データが送信側でバッファリングされる唯一の理由ではありません。データ送信を確実に強制する方法はありません。また、バッファリング/フラグメンテーションは、データが送信された後、NAT、ルーター、プロキシ、または受信側でも発生する可能性があります。TCPは、データを受信するサイズとタイミングについては保証しません。データが順番どおりに確実に到着することのみを保証します。サイズの保証が必要な場合は、UDPを使用してください。TCP が理解しやすいように思われるという事実は、TCP がすべての問題に最適なツールとは言えません!
パンダパジャマ

10

(理由の範囲内で)1つ大きい方が優れています。

あなたが言ったように、パケット損失が主な理由です。パケットは通常、固定サイズのフレームで送信されるため、10個の小さなフレームで10個のフレームを使用するよりも、大きなメッセージで1個のフレームを使用する方が適切です。

ただし、標準TCPでは、無効にしない限り、これは実際には問題ではありません。(これはNagleのアルゴリズムと呼ばれ、ゲームの場合は無効にする必要があります。)TCPは固定タイムアウトまで、またはパッケージが「フル」になるまで待機します。「フル」とは、フレームサイズによってある程度決定される、わずかに魔法の数字です。


Nagleのアルゴリズムについて聞いたことがありますが、無効にするのは本当に良い考えですか?StackOverflowの回答からすぐに来ましたが、誰かがそれがより効率的であると言ったのです(そして、明らかな理由で効率が欲しいです)。
joehot200

6
@ joehot200それに対する唯一の正しい答えは「依存する」です。はい、大量のデータを送信する方が効率的ですが、ゲームが必要とする傾向があるリアルタイムストリーミングの場合はそうではありません。
Dサイド

4
@ joehot200:Nagleのアルゴリズムは、特定のTCP実装が時々使用する遅延確認応答アルゴリズムとの相互作用が不十分です。一部のTCP実装では、すぐにさらにデータが続くと予想される場合、データを取得した後にACKの送信を遅らせます(後のパケットを確認すると、前のパケットも暗黙的に確認するため)。Nagleのアルゴリズムでは、データを送信したが確認応答を受信して​​いない場合、ユニットは部分的なパケットを送信すべきではないと述べています。時々、2つのアプローチは相互にひどく相互作用し、各当事者は相手が何かをするのを待っていますが、
...-supercat

2
...「サニティタイマー」が起動し、状況を解決します(1秒程度)。
スーパーキャット

2
残念ながら、Nagleのアルゴリズムを無効にしても、他のホスト側でのバッファリングを防ぐためには何もしません。Nagleのアルゴリズムを無効にしても、ほとんどの人が探しているのは、recv()呼び出しごとに1つの呼び出しを取得することではありませんsend()。ただし、UDPと同様に、これを保証するプロトコルを使用します。「TCPだけがあれば、すべてがストリームのように見える」
パンダパジャマ

6

以前の回答はすべて間違っています。実際には、1つの長いsend()呼び出しを発行するか、いくつかの小さなsend()呼び出しを発行するかは問題ではありません。

Phillipが述べているように、TCPセグメントにはオーバーヘッドがありますが、アプリケーションプログラマーとしては、セグメントの生成方法を制御することはできません。簡単な言葉で:

1つのsend()呼び出しが必ずしも1つのTCPセグメントに変換されるわけではありません。

OSは、完全に無料ですべてのデータをバッファリングし、一つのセグメントにそれを送ったり、長いものを取ると、いくつかの小さなセグメントにそれを破ります。

これにはいくつかの意味がありますが、最も重要なことは次のとおりです。

1つのsend()呼び出し、または1つのTCPセグメントはrecv()、もう一方の端での1つの成功した呼び出しに必ずしも変換されません

この背後にある理由は、TCPがストリームプロトコルであることです。TCPはデータをバイトの長いストリームとして扱い、「パケット」の概念はまったくありません。ではsend()、あなたは、そのストリームにバイトを追加し、とrecv()あなたが他の側オフのバイトを取得します。TCPは、データが可能な限り高速で反対側に届くように、適切と思われる場所でデータを積極的にバッファリングおよび分割します。

TCPで「パケット」を送受信する場合は、パケット開始マーカー、長さマーカーなどを実装する必要があります。代わりにUDPのようなメッセージ指向プロトコルを使用してはどうですか?UDPは、1つのsend()呼び出しが1つの送信データグラムと1 つの呼び出しに変換されることを保証しrecv()ます!

TCPだけを持っている場合、すべてがストリームのように見えます


1
各メッセージにメッセージ長を接頭辞として付けることはそれほど難しくありません。
ysdx

Nagleのアルゴリズムが有効かどうかに関係なく、パケットの集約に関しては1つのスイッチを切り替える必要があります。充填不足のパケットを迅速に配信するためにゲームネットワーキングでオフにすることは珍しくありません。
ラースヴィクルンド

これは完全にOSまたはライブラリ固有です。また、あなたは多くのコントロールを持っています-あなたが望むなら。完全に制御できないのは事実です。TCPは常に2つのメッセージを結合したり、MTUに適合しない場合は1つを分割したりできますが、それでも正しい方向にヒントを与えることができます。さまざまな構成設定の設定、1秒間隔でのメッセージの手動送信、またはデータのバッファリングとワンショットでの送信。
ドルス

@ysdx:いいえ、送信側ではなく、受信側であります。データを取得する場所を正確に保証するものではないrecv()ため、それを補正するために独自のバッファリングを行う必要があります。UDPを介した信頼性の実装と同じ難易度でランク付けします。
パンダパジャマ

@Pagnda Pajama:受信側の単純な実装は次のとおりですwhile(1) { uint16_t size; read(sock, &size, sizeof(size)); size = ntoh(size); char message[size]; read(sock, buffer, size); handleMessage(message); }(簡潔にするためにエラー処理と部分的な読み取りを省略しますが、あまり変わりません)。これを行うことはselectそれほど複雑ではなく、TCPを使用している場合は、おそらく部分的なメッセージをバッファリングする必要があるでしょう。UDPを介した堅牢な信頼性の実装は、それよりもはるかに複雑です。
ysdx

3

多くの小さなパッケージで問題ありません。実際、TCPのオーバーヘッドが心配な場合は、bufferstream最大1500文字(またはTCP MTUが何であれ、動的に要求するのが最善)を収集するものを挿入し、1か所で問題に対処します。そうすることで、そうでなければ作成する余分なパッケージごとに最大40バイトのオーバーヘッドを節約できます。

とはいえ、送信するデータは少ない方が良いので、そこに大きなオブジェクトを作成すると役立ちます。もちろん、送信する"UID:10|1|2|3より送信するのは小さくなりますUID:10;x:1UID:10;y:2UID:10;z:3。実際、この時点でも、車輪を再発明するべきではありません。そのようなデータを10バイト文字列以下に減らすことができるprotobufのようなライブラリを使用してください。

Flushストリームへのデータの追加を停止するとすぐに、何かを送信する前に無限に待機する可能性があるため、忘れてはならない唯一のことは、関連する場所でコマンドをストリームに挿入することです。クライアントがそのデータを待っており、クライアントが次のコマンドを送信するまでサーバーが新しいものを送信しない場合、本当に問題が生じます。

パッケージの損失は、ここでわずかに影響する可能性があります。送信したすべてのバイトが破損する可能性があり、TCPは再送信を自動的に要求します。パッケージが小さいほど、すべてのパッケージが破損する可能性が低くなりますが、オーバーヘッドが増えるため、さらに多くのバイトを送信し、失われたパッケージの確率をさらに高めます。パッケージが失われると、TCPは、失われたパッケージが再送信および受信されるまで、後続のデータをすべてバッファリングします。これにより、大きな遅延(ping)が発生します。パッケージの損失による帯域幅の全体的な損失は無視できる場合がありますが、ゲームではpingの増加は望ましくありません。

結論:できるだけ少ないデータを送信し、大きなパッケージを送信します。そうするために独自の低レベルのメソッドを作成しないでくださいbufferstream


実際、このような単純なものの場合は、簡単にロールバックできます。他の人のライブラリを使用するために50ページのドキュメントを調べるよりもはるかに簡単であり、その後もバグや落とし穴に対処する必要があります。
パセリエ

確かに、自分で書くのbufferstreamは簡単です。だから私はそれをメソッドと呼んでいます。バッファロジックをメッセージコードと統合するのではなく、1か所で処理する必要があります。オブジェクトのシリアル化については、他の人がそこに置く何千もの工数よりも良いものを得るのは非常に疑いがあります。
ドルス

2

私自身はネットワークプログラミングの初心者ですが、いくつかの点を追加して、限られた経験を共有したいと思います。

  • TCPはオーバーヘッドを意味します-関連する統計を測定する必要があります
  • UDPは事実上のネットワークゲームシナリオのソリューションですが、それに依存するすべての実装には、パケットが失われたり順序が狂って送信されたりすることを考慮したCPU側のアルゴリズムが追加されています

測定に関して、考慮すべきメトリックは次のとおりです。

前述のように、ある意味で制限がなく、UDPを使用できることがわかった場合は、そのために行ってください。UDPベースの実装がいくつかあるため、車輪を再発明したり、長年の経験や実績のある経験に反して作業する必要はありません。言及する価値のある実装は次のとおりです。

結論:UDP実装はTCP実装よりも(3倍)優れている可能性があるため、UDPに適したシナリオを特定したら、考慮することは理にかなっています。注意してください!UDPの上に完全なTCPスタックを実装することは常に悪い考えです。


UDPを使用していました。TCPに切り替えただけです。UDPのパケット損失は、クライアントが必要とする重要なデータに対しては単純に受け入れられませんでした。UDP経由で移動データを送信できます。
joehot200

したがって、できる最善のことは、実際に、重要な操作にのみTCPを使用するか、UDPベースのソフトウェアプロトコル実装を使用することです(Enetは単純で、UDTは十分にテストされています)。ただし、最初に損失を測定し、UDTが優位性をもたらすかどうかを判断します。
テオドロン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.