この記事では、粘着性のある TCP データ パケットとその解決方法について説明します。

スティッキーバッグの問題の概要

背景を説明する

ネットワーク データ送信に TCP プロトコルを使用するソフトウェア設計では、スティッキー パケットという一般的な問題があります。これは主に、最新のオペレーティング システムのネットワーク送信メカニズムによるものです。

ネットワーク通信で使用されるソケット テクノロジは、実際には、アプリケーション層プログラムとネットワーク カード インターフェイス間の転送機能を実装する連続キャッシュ (ストリーム バッファ) を提供するシステム カーネルによって実装されることがわかっています。

連続バッファには複数のデータパケットが連続して格納されます データパケットを読み出す際には、送信側の送信境界が判断できないため、ある推定値のサイズを使用してデータを読み出します 双方のサイズが一致しないと、データ パケットの境界がずれると、誤ったデータ パケットが読み取られ、元のデータの意味が誤解されてしまいます。

粘着バッグのコンセプト

スティッキー問題の本質はデータの読み取り境界エラーによって発生するもので、次の図で現象を視覚的に理解できます。

図 1 に示すように、現在のソケット キャッシュには 6 つのデータ パケットが到着しており、そのサイズは図に示すとおりです。アプリケーションがデータを収集するとき (図 2 を参照)、読み取りに 300 バイトの要件を採用し、誤って pkg1 と pkg2 を 1 つのパッケージとして一緒に収集します。

実際、pkg1 はテキスト ファイルのコンテンツである可能性が高く、pkg2 はオーディオ コンテンツである可能性があり、これら 2 つの無関係なデータ パケットが 1 つのパケットにまとめられて処理されており、これは明らかに不適切です。深刻な場合には、pkg2 が失われると、ソフトウェアが異常なブランチに落ち、インシデントが発生する可能性があります。

したがって、スティッキー パッケージの問題は、すべてのソフトウェア設計者 (プロジェクト マネージャー) の大きな注目を集めるに違いありません。

それでは、なぜ受信プログラムに 100 バイトに従って読み取らせればよいのかと疑問に思う読者もいるかもしれません。TCP プログラミングをある程度知っていれば、そのような問題は起こらないと思います。

ネットワーク通信プログラムでは、データ パケットのサイズは通常、特にソフトウェア設計段階では決定できず、固定値として決定することはできません。たとえば、チャット ソフトウェア クライアントが TCP を使用して、認証とログインのためにユーザー名とパスワードをサーバーに送信する場合、データ パケットはわずか数十バイト、最大でも数百バイトで送信できると思います。非常に大きなパケットを送信する必要がある場合があり、ビデオ ファイルをパッケージで送信する場合でも、1 パッケージあたり数キロバイトになるはずです。(ある国の電気通信プラットフォームの MW では、一度に 15,000 バイトの電話データが送信されたと言われています)。

この場合、送信するデータのパケットサイズは固定できず、受信先も固定できない。したがって、ポーリング受信では、より合理的な推定値が使用されるのが一般的です。(ネットワーク カードの MTU は 1500 バイトなので、この推定値は通常 MTU の 1 ~ 3 倍になります)。

読者は粘着性のある袋の問題について予備的に理解しておく必要があると思います。

バッグのベタつきを回避する設計

1: 固定長で送信

データ送信時には固定長設計が使用されます。つまり、どれだけのデータを送っても、固定長(説明の便宜上、ここでは固定長をLENとして記録します)にパケット化されます。つまり、送信側がデータを送信するときは、LENとしてパケット化されます。長さ。

このように、受信側は固定のLENで受信するため、送信と受信が1対1で対応することができます。サブパッケージ化する場合、複数の完全な LEN パケットに完全に分割されない場合があります。通常、最後のパケットは LEN よりも小さくなります。このとき、最後のパケットは欠落部分を空白バイトで埋めることができます。

もちろん、このアプローチには欠点があります。

1. 最後のパケットの不十分な長さが空白部分で埋められており、無効なバイトオーダーです。この無効な部分は、受信者が識別するのが困難になる可能性がありますが、これは単に埋めるためのものであり、実際には意味がありません。これにより、受信側がその意味を処理する際に問題が生じます。もちろん、フラグ ビットを追加することで補償できる解決策もあります。つまり、各データ パケットの先頭に固定長のヘッダーを追加し、データ パケットのエンド マークを一緒に送信します。受信側はこのマークにより無効なバイト列を確認し、データを完全に受信することができる。

2. 送信されるパケットの長さがランダムに分散されると、帯域幅が無駄になります。たとえば、送信長は 1,100、1000、4000 バイトなどであり、それらはすべて最大固定長である 4000 に従って送信される必要があります。4000 バイトより小さいデータ パケットを持つ他のパケットも同様に埋められます。 4000 の場合、ネットワーク負荷が無駄に浪費されます。

要約すると、このソリューションは、送信されるデータ パケットの長さが比較的安定している (特定の固定値になる傾向がある) 状況に適しており、より良い結果が得られます。

2: テールマークシーケンス

送信する各データ パケットの最後に特別なバイト シーケンスを設定します。このシーケンスには特別な意味があり、文字列の終了文字識別子 "\0" と同じです。データの終わりをマークするために使用されます。そうして初めて、受信したデータを分析し、末尾のシーケンスを通じてデータ パケットの境界を確認することができます。

この方法の欠点はさらに明白です。

1. 受信側はデータを分析し、テール シーケンスを識別する必要があります。

2. 末尾配列の決定自体が問題である。「\0」のようなターミネータとして使用できるシーケンスは何ですか? このシーケンスは、「\0」が無効な文字列内容であるため文字列の終了マークとして使用できるのと同様に、人間やプログラムによって一般に認識される意味を持たないデータ シーケンスである必要があります。では、通常のネットワーク通信におけるこのシーケンスは何でしょうか? しばらくは正解を見つけるのは難しいと思います。

3: ヘッダーマークの段階的な受信

この方法は、著者の限られた知識に基づく最良の方法です。効率を損なうことなく、あらゆるサイズのパケットの境界問題を完全に解決します。

このメソッドの実装は次のとおりです。

1. ユーザー ヘッダーを定義し、ヘッダーで送信される各データ パケットのサイズを指定します。

2. 受信機は受信するたびに、まずヘッダーのサイズ分のデータを読み出しますので、必然的にヘッダーの 1 つ分のデータのみを読み出し、ヘッダーからデータパケットのデータサイズを取得します。

3. このサイズに合わせて再度読み込みを行うと、データの内容を読み取ることができます。

このようにして、各データ パケットは送信時にヘッダーでカプセル化され、受信者は 2 回に分けてパケットを受信します。1 回目はヘッダーを受信し、2 回目はパケットのサイズに基づいてデータ コンテンツを受信します。ヘッダー。

(ここでの data[0] の本質は、データのテキスト部分を指すポインタであるか、連続データ領域の開始位置である可能性があります。したがって、この場合は data[user_size] として設計できます) 。)

以下は設計思想を示す図です。

図からわかるように、データはヘッダーをカプセル化する動作で送信され、受信側は各パケットの受信を 2 回に分割します。

この計画は洗練されているように見えますが、実際には欠陥もあります。

1. ヘッダーは小さいですが、各パケットはより多くの sizeof(_data_head) データをカプセル化する必要があり、累積的な影響を完全に無視することはできません。

2. レシーバーの受信動作が 2 回に分割され、つまりデータ読み取り操作が 2 倍になり、データ読み取り操作の recv または read が両方ともシステム コールとなるため、カーネルにとって許容できないオーバーヘッドになります。は完全に無視され、プログラムへのパフォーマンスへの影響は無視できます (システム コールは非常に高速です)。

利点: プログラム設計の複雑さが回避され、その有効性の検証が容易になり、ソフトウェア設計の安定性要件を満たすのが容易になります。

 Information Direct: Linux カーネル ソース コード テクノロジ学習ルート + ビデオ チュートリアル カーネル ソース コード

Learning Express: Linux カーネル ソース コード メモリ チューニング ファイル システム プロセス管理 デバイス ドライバー/ネットワーク プロトコル スタック

補充する

袋の貼り付けを検討する必要があるのはどのような場合ですか?

1. 毎回 TCP を使用してデータを送信すると、相手との接続が確立され、データを送信した後に双方が接続を閉じるため、スティッキー パケットの問題が発生しません(パケット構造は 1 つだけで、http プロトコルに似ています)。

接続を閉じるには、両方の当事者が終了接続を送信する必要があります (TCP 終了プロトコルを参照)。例: A は B に文字列を送信する必要があり、その後、A は B との接続を確立し、「こんにちは、自分自身を与えてください」など、双方がデフォルトで使用するプロトコル文字を送信し、B がメッセージ、バッファ データを受信して​​接続を閉じると、文字の段落が送信されていることは誰もが知っているため、スティッキー パケットの問題を考慮する必要がありません。

2. 送信するデータがファイル転送などの構造を持たない場合、送信者は送信するだけ、受信者は受信して保存するだけで問題ありません。パケット スティッキングを考慮する必要はありません。

3. 双方が接続を確立した場合、接続後の一定期間内に異なる構造のデータを送信する必要があります。たとえば、接続後は次のような構造があります。

1) 「こんにちは、食事をください」

2)「自分で私に食べ物を与えないでください」

この場合、送信者がこれら 2 つのパケットを連続して送信すると、受信者は「hello give me sth abour owns」を受信する可能性があります。

これでは受け取る側もバカになってしまいますが、一体何が起こっているのでしょうか?わかりませんが、プロトコルではそんな変な文字列は規定していないので、パケットに分割する必要がありますが、分割方法も双方でパケット構造を工夫する必要があるので、一般的にはデータ長などのパケットがヘッダーに追加されますので必ず受信してください。

スティッキー パケットの理由: ストリーミング送信で発生します。UDP にはメッセージ境界があるため、スティッキー パケットは存在しません。

1 送信側は送信する前にバッファがいっぱいになるまで待つ必要があるため、スティッキー パケットが発生します。

2 受信側がバッファ内のパケットを時間内に受信しないため、複数のパケットが受信されます。

解決:

スティッキング現象を回避するには、以下のような対策が考えられます。

まず、ユーザーはプログラミング設定により、送信側によるパケットスタック現象を回避することができます. TCP には、直ちにデータを強制的に送信するプッシュ操作コマンドが用意されています. TCP ソフトウェアは、操作コマンドを受信した後、直ちにこのデータを送信します。送信バッファがいっぱいになるまで待ちます。

第二に、受信機によって引き起こされるスティッキー パケットについては、プログラム設計の最適化、受信プロセスの作業負荷の合理化、受信プロセスの優先順位の向上などの対策を使用して、タイムリーにデータを受信することでスティッキー現象を最小限に抑えることができます。

3 つ目は受信側が制御するもので、構造フィールドに従ってデータのパケットを複数回受信し、マージするように手動で制御することで、パケットのスティッキングを回避できます。

上記の 3 つの対策にはすべて欠点があります。

最初のプログラミング設定方法は、送信側によって引き起こされるスティッキー パケットを回避できますが、最適化アルゴリズムがオフになり、ネットワーク送信効率が低下し、アプリケーションのパフォーマンスに影響を与えるため、一般的には推奨されません。

2 番目の方法は、スティッキー パケットの可能性を減らすことしかできませんが、スティッキー パケットを完全に回避することはできません。送信頻度が高い場合やネットワーク バーストにより、一定時間内のデータ パケットが受信側に早く到着する可能性があり、スティッキー パケットを完全に回避することはできません。受信者 まだ受信するには遅すぎる可能性があり、スティッキー パケットが発生します。

3 番目の方法はスティッキー パケットを回避しますが、アプリケーションの効率が低く、リアルタイム アプリケーションには適していません。

TCP ベースの通信プログラムでパックとアンパックが必要な理由

TCP は「ストリーム」プロトコルです。いわゆるストリームとは、境界のない一連のデータです。川の流れを考えると、境界線がなく 1 つの部分につながっています。しかし、一般的な通信プログラムの開発では、ログイン時やログアウト時のデータパケットなど、それぞれ独立したデータパケットを定義する必要があります TCPの「フロー」の特性やネットワークの状況により、データ送信中に以下のような状況が発生します。

send を 2 回連続して呼び出して、それぞれ data1 と data2 の 2 つのデータを送信したとすると、受信側の受信状況は次のとおりです (もちろんこれ以外にも状況はありますが、ここでは代表的な状況のみを示します)。

A. Data1 が最初に受信され、次に data2 が受信されます。

B. まず data1 のデータの一部を受信し、次に data1 の残りの部分と data2 のすべてを受信します。

C. まずdata1の全データとdata2の一部のデータを受信し、次にdata2の残りのデータを受信しました。

D. data1とdata2の全データを一度に受信します。

状況 A の場合、これはまさに必要なことなので、これ以上は説明しません。B、C、D の場合、状況は誰もがよく「スティッキー パケット」と呼ぶもので、受信したデータを独立したデータ パケットに解凍する必要があります。解凍するには、送信側でパケットをカプセル化する必要があります。

もう 1 つ: UDP の場合、UDP は「データ パケット」プロトコル、つまり 2 つのデータの間に境界があるため、アンパックの問題はありません。受信側はデータを受信できないか、完全なデータを受信します。 . データは少なくても多くても受信されません。

なぜBCDが起こるのでしょうか?

「パケットの固着」は、送信側または受信側で発生する可能性があります。

1. Nagle アルゴリズムによって引き起こされる送信側のスティッキー パケット:

Nagleのアルゴリズムは、ネットワークの伝送効率を向上させるアルゴリズムです。簡単に言うと、送信のために TCP にデータを送信するとき、TCP はデータをすぐに送信せず、待機期間中にまだ送信するデータがあるかどうかを確認するために少しの間待機します。は一度にデータを送信します。2つのデータが送信されます。

ネーグルアルゴリズムの簡単な説明ですので、詳しくは関連書籍をお読みください。C や D のような状況は、Nagle のアルゴリズムによって引き起こされる可能性があります。

2. 受信側のスティッキー パケットにより、受信側は時間内にパケットを受信できません。

TCP は受信したデータを独自のバッファに保存し、アプリケーション層にデータを取得するように通知します。アプリケーション層が何らかの理由でTCPデータの取り出しが間に合わなかった場合、TCPバッファに複数のデータが蓄積されることになります。

梱包と開梱の方法

私が最初に「スティッキー パケット」の問題に遭遇したとき、2 回の送信の間に短期間スリープを呼び出すことで問題を解決しました。

このソリューションの欠点は明らかで、伝送効率が大幅に低下し、信頼性が低くなります。その後、応答方式で解決しましたが、ほとんどの場合は可能ですが、Bのような状況は解決できません。また、応答方式では通信量が増加し、ネットワーク負荷が増加します。次のステップは、データ パケットをパックおよびアンパックすることです。

パッケージ化とは、データにヘッダーを追加して、データ パケットをヘッダーとパケット本体の 2 つの部分に分割することです (後で不正なパケットをフィルタリングするときに、パケットに「パケット末尾」コンテンツが追加されます)。

ヘッダーは実際には固定サイズの構造体です。パッケージの長さを表す構造体メンバー変数があります。これは非常に重要な変数です。必要に応じて他の構造体メンバーを定義できます。パケット ヘッダーの固定長と、パケット ヘッダー内のパケット本体の長さを含む変数に従って、完全なデータ パケットを正しく分割できます。

解凍には、現在、次の 2 つの方法をよく使用します。

1. 動的バッファ一時保存方式。バッファが動的である理由は、バッファリングされるデータの長さがバッファの長さを超えると、バッファ長が増加するためです。おおよそのプロセスは次のように説明されます。

A. コネクションごとに動的にバッファを割り当て、通常は構造体を介してバッファと SOCKET を関連付ける B. データを受信したら、まずそのデータをバッファに格納する C. バッファ領域のデータ長が十分であるかどうかの判断パケットヘッダーの長さ、そうでない場合はアンパック操作は実行されません D. パケットヘッダーデータに基づいてパケットボディの長さを表す変数を解析します E. バッファー領域内のデータの長さを決定しますパケットヘッダー パケットボディの長さが十分ですか? 十分でない場合、アンパック操作は実行されません F、データパケット全体を取り出します。ここでの「取得」とは、データパケットをバッファからコピーするだけでなく、バ​​ッファからデータパケットを削除することも意味しており、削除方法としては、パケットの後ろにあるデータをバッファの先頭アドレスに移動することである。

この方法には 2 つの欠点があります。 1. 各接続にバッファを動的に割り当てると、メモリ使用量が増加します。2. データをコピーする必要がある場所は 3 つあります。1 つはデータをバッファに保存する場所、1 つはバッファから完全なデータ パケットを取り出す場所、そして 1 つはバッファからデータ パケットを削除する場所です。

このアプローチの欠点については前述しました。以下にリングバッファを使用する改善方法を示しますが、この改善方法でも1番目の欠点と最初のデータコピーは解決できず、3番目のデータコピーしか解決できません(ここが問題箇所です)。ほとんどのデータがコピーされます)。) 2 番目の解凍方法は、これら 2 つの問題を解決します。

リング バッファの実装では、有効なデータの先頭と末尾をそれぞれ指す 2 つのポインタを定義します。データの保存および削除時には、先頭ポインタと末尾ポインタのみが移動します。

2. 基礎となるバッファを使用して解凍します。

TCP はバッファも維持するため、TCP バッファを完全に使用してデータをキャッシュできるため、接続ごとにバッファを割り当てる必要がありません。一方、recv または wsarecv には、受信するデータの長さを示すパラメーターがあることがわかっています。これら 2 つの条件を使用して、最初の方法を最適化できます。

SOCKET をブロックするには、ループを使用してパケット ヘッダー長のデータを受信し、次にパケット本体の長さを表す変数を解析してから、ループを使用してパケット本体長のデータを受信します。関連するコードは次のとおりです。

char PackageHead[1024];
char PackageContext[1024*20];

int len;
PACKAGE_HEAD *pPackageHead;
while( m_bClose == false )
{
    memset(PackageHead,0,sizeof(PACKAGE_HEAD));
    len = m_TcpSock.ReceiveSize((char*)PackageHead,sizeof(PACKAGE_HEAD));
    if( len == SOCKET_ERROR )
    {
        break;
    }
    if(len == 0)
   {
      break;
   }
    pPackageHead = (PACKAGE_HEAD *)PackageHead;
    memset(PackageContext,0,sizeof(PackageContext));
    if(pPackageHead->nDataLen>0)
    {
    len = m_TcpSock.ReceiveSize((char*)PackageContext,pPackageHead->nDataLen);
    }
 }

m_TcpSock は SOCKET をカプセル化するクラスの変数で、ReceiveSize は一定長のデータを受信するために使用され、一定長のデータを受信するかネットワークエラーが発生するまで戻りません。

int winSocket::ReceiveSize( char* strData, int iLen )
{
    if( strData == NULL )
        return ERR_BADPARAM;
    char *p = strData;
    int len = iLen;
    int ret = 0;
    int returnlen = 0;
    while( len > 0)
    {
        ret = recv( m_hSocket, p+(iLen-len), iLen-returnlen, 0 );
        if ( ret == SOCKET_ERROR || ret == 0 )
        {
            return ret;
        }
        len -= ret;
        returnlen += ret;
    }
    return returnlen;
}

完了ポートなどの非ブロッキング SOCKET の場合、ヘッダー長のデータを受信するリクエストを送信できます。GetQueuedCompletionStatusが戻ってきたら、受信したデータの長さがパケットヘッダの長さと等しいかどうかを判定し、等しい場合はパケットボディ長のデータ受信リクエストを送信し、等しくない場合はパケットヘッダ長のデータ受信リクエストを送信する。残りのデータを受信するために送信されます。パッケージ本体を受け取るときにも同様のアプローチが使用されます。

原著者: 一緒に埋め込まれた学習

おすすめ

転載: blog.csdn.net/youzhangjing_/article/details/132832826