TCPプロトコルは、ストリーミングプロトコルであり、
多くの読者は、インターネットの知識にアクセスするので、あなたはフレーズを聞いたはずです:TCPプロトコルはストリーミングのプロトコルです。最後にこの文だからそれは何を意味するのでしょうか?契約はバイトのストリームに水のように、コンテンツとコンテンツ記号の間には明確な境界が存在しないこと、いわゆるストリーミングプロトコル、我々は、人々はこれらの契約の境界に移動する必要があります。
例えば、AはBのTCP通信と、AがBに100バイト及び200バイトのパケットを送信した後、Bがそれを受信する方法は?B 100は、最初のバイトを受信することができ、200バイトが受信し、また、最初の50バイト、受信の250のバイトを受信することができる、または受信するために、100バイト、受信の100のバイトを受信します200バイト、20又は最初に受信したバイト、20バイトを受信し、その後、60バイト、受信の100のバイトを受信し、50のバイトを受信し、50のバイトが受信......
法律を知らない読者が見られませんか?ルールは、受信した300バイトの合計数のいずれの形態であってもよい倍以上B、Bに送信さ300バイトの合計です。100バイト、200バイトと仮定すると、それぞれ、BのAは、エンドAを送信するために、これを区別することができ、データパケットを送信しているが、Bのために、長さが人工的でない場合や複数のデータパケット各Bは有効なパケットとして受信したデータのバイト数を知っているべきではありません。各時間が規制どのくらいのデータプロトコルフォーマット仕様としては、パッケージの要素の一つです。
次のような初心者の書き込みコードがしばしばあります。
送信者:
1//...省略创建socket,建立连接等部分不相关的逻辑...
2char buf[] = "the quick brown fox jumps over a lazy dog.";
3int n = send(socket, buf, strlen(buf), 0);
4//...省略出错处理逻辑...
受信側:
1//省略创建socket,建立连接等部分不相关的逻辑...
2char recvBuf[50] = { 0 };
3int n = recv(socket, recvBuf, 50, 0);
4//省略出错处理逻辑...
5printf("recvBuf: %s", recvBuf);
問題自体の議論を集中させるためには、私はここでは省略し、いくつかのロジックは、接続エラー処理を確立します。送信された文字列の受信側に送信側のコードは、「速い茶色のキツネは、のろまなイヌに飛びかかった。」、受信機はそれを印刷受け取った後。
このコードは、このマシンの良い仕事とほぼ同様で、予定の期待の文字列として受信側をプリントアウトしますが、ローカルエリアネットワークまたは問題のパブリックネットワーク環境に、すなわち、受信側では、文字列から、総合印刷ではないかもしれません、送信側が連続的に複数回に文字列を送信する場合、印刷された文字列の受信側は、不完全または文字化けしています。不完全な理由がよく、すなわち、完全な文字列の長さよりも小さい特定の受信したデータの末尾、アレイは、列の端部は依然として0、printf関数である部分文字列を受信した後、recvbuf 0に空にされ始め、理解しましたゼロの文字マーク出力の最後の最後でみると、文字化けした理由は、特定の所得データだけでなく、完全な文字列が含まれている場合、文字列はまた、次の部分が含まれていることですし、recvBuf配列が満たされる、printfの機能出力ときは、メモリがクロスボーダーの上に読み込まれるので、まだ、ゼロ文字マーク出力の最後の最後にことになるだろう、とこれまでに発見されており、国境を越えた後のメモリが読めない文字がハッシュ後に表示されているかもしれません。
私はあなたがTCPプロトコルの直感的な理解がストリーミングプロトコルである持っていることを理解してほしい。この例を与えます。このため、我々は設計されたネットワーク通信プロトコルであり、受信側がデータパケットを解析するようにバイト数の位置から取り出し知っているように、人為的にすべての所定のバイトストリーム境界で終了を送受信する必要がありますやるべき仕事のフォーマットの一つ。
二つの問題を解決するためにどのようにパッケージを貼り付け
ネットワーク通信プログラムや技術的な面接の実際の開発は、通常よりお願いします問題のインタビュアーは、次のとおりです。ネットワークトラフィックは、どのようにスティックパッケージを解決するとき?
いくつかの面接を求めてmay'm:ネットワークトラフィックは、どのようにスティック包装、パケットロスやパケット並べ替え問題を解決するとき?実際には、この質問は、研究のインタビューで知識の基礎はあるインタビュアーのネットワーク、プロトコルがTCPの場合は、ほとんどのシナリオでは、パケットロスやパケットの並べ替え問題はないです、TCPの通信は、信頼性の高い通信、TCPプロトコルスタックは、シーケンスを介して行われ数およびパケット再送メカニズム整然とした確認パケットを確実にするために、正しく宛先に送信する必要があります。パケット損失の少量を受け入れることができない場合、UDPプロトコルあれば、それはUDPに基づいて独自に達成しなければならないが、この順TCPに似ていますかつ信頼性の高い搬送機構(例えばRTPプロトコル、RUDPプロトコル)。だから、問題の解体後、どのようにだけ固執パッケージの問題を解決します。
最初のもので説明スティックパッケージは、いわゆるスティックパッケージがピアに2つ以上のデータパケットに連続的に送られ、担当ピアは、1より大きく1よりも大きいパケットを受信することができる、とすることができますパッケージプラスパッケージの一部、または単にいくつかの完全なパッケージを一緒に(を含む)いくつかの。もちろん、データはこれだけでは、一般的にも呼ばれ、パッケージの一部を受け取ることができるの半分パック。
半分パックまたはスティックパッケージの問題かどうか、その根本的な原因は、上記の形式で説明したTCPプロトコルは、データをストリーミングされています。アイデアは区別するために国境の受信データパケットおよびパッケージから問題を解決する方法を考えますか。それでは、どのようにそれを区別するのですか?三つの主要な方法があります。
固定パケット長のパケット
名前が示唆するように、各プロトコルのパケットの、すなわち長さが固定されています。例えば、我々は、例えば、各プロトコルのパケットの所定のサイズは、64(いない場合、それは最初の堆積になる)は、それぞれ解決するために取り出し、完全受信バイト、64バイトであることができます。
単純な通信プロトコル形式が、このような柔軟性が乏しいです。パケットの内容のバイト数が少ないような\ 0(?特別なコンテンツで満たされていない場合、どのようにパッケージ内に通常のコンテンツを識別するための情報でそれを充填されている)のような特別な情報が必要で充填指定し、残りのスペースよりもあれば、パッケージの内容は、指定されたバイトを超えた場合番号は、パケットの断片化が再び得点、追加の処理ロジックの必要性 - 送信側のサブコントラクト断片は、受信側は、パケットスロット再組み立て(サブコンテンツをフラグメントは、以下に詳述します)。
パッケージの終わりをマークする文字(文字列)を指定するには
このより一般的なプロトコルパケットは、すなわち、バイトストリームは、パケットが特別なシンボル値を検出したときに終了すると考えられます。例えば、我々は、FTPプロトコル、SMTPメール・プロトコル、コマンドまたはデータの部分に精通している「\ R \ n」は(いわゆる続く CRLFは)完成したパッケージを表します。それぞれの出会いAの前に受信したデータの終了後、「\ rを\ n」はプット・パケットとして。
このプロトコルは、典型的には、いくつかの用途のために使用されている制御様々なコマンドを含む、その欠点は、プロトコルデータパケットの内容はパケット終了フラグ部の文字を必要とする場合、受信されるのを避けるために、これらの文字または退避動作のトランスコーディングを行う必要があるということです牙はパケットフラグとエラー解像度の終了を間違え。
パケットヘッダ+ボディフォーマット
このパケットフォーマットは、一般的に二つの部分、即ちヘッダとボディ、固定サイズのヘッダに分割され、そしてどのように次のパケット本体を説明するためにパケットヘッダフィールドを含まなければなりません。
例えば:
1struct msg_header
2{
3 int32_t bodySize;
4 int32_t cmd;
5};
これはbodySizeは、このパッケージを含めることがどのくらいであることを指定、典型的なヘッダフォーマットです。ヘッダのサイズは固定されているので(これは、サイズ(int32_t)で+はsizeof(int32_t)= 8バイト)、第一の電荷ヘッダサイズの端のバイト数(もちろん、キャッシュされた又は十分に近いまで、第一されていない場合)、次いでパケットヘッダ解析を収集封入体パケットヘッダ指定されたサイズ、および十分に近い他の介在物によれば、処理を完了するために、パッケージに組み立てられます。いくつかの実装では、bodySizeのヘッダがpackageSizeフィールドと呼ばれる別で置き換えることができる、このフィールドの意味は、この時、パッケージ全体の大きさで、私たちは(ここではsizeof(msg_header))packageSizeマイナスヘッダサイズを使用することができるようになりますパッケージ本体のサイズ、上記の原理を計算します。
ほとんどのネットワークライブラリを使用すると、通常、データパケット境界と解析を所有する必要があり、一般的なネットワークライブラリは、それが契約の不確実性のために異なるプロトコルをサポートする必要性から外れているので、できないプロトコルフォーマットに従って、この機能を提供していません。予め設けられたコードをアンパック詳細。もちろん、これは絶対的なものではなく、いくつかのネットワークライブラリがこの機能を提供するがあります。Javaでは網状ネットワークフレームワークは、固定長プロトコルパケットは、ターミネーターなどの特殊文字に係るハンドルプロトコルパケットにDelimiterBasedFrameDecoderクラスを提供するために使用される(プロセス・カスタム・フォーマット・プロトコル・パケットにByteToMessageDecoderが設けられているハンドルの長さにFixedLengthFrameDecoderクラスを提供しましたデータパケットを解凍するために)()パケットのパケットヘッダ+本文の形式を処理しますが、サブクラスByteToMessageDecoder連続して、あなたの特定のプロトコル形式に基づいてデコードをオーバーライドする必要があります方法。
これら三つのパッケージ形式は、読者が深い理解を期待し、その長所と短所の基本原則を把握することができます。
三開梱処理
パケットは、先に説明したデータの3つのフォーマットを理解した後、我々はパケットフォーマットのためにこれらの3つの技術がどのように処理すべきかを説明する必要があります。これはプロセスの流れは同じ、ここでヘッダ+封入 この形式のパケットについて説明します。次のようにプロセスは以下のとおりです。
我々は次のようにヘッダフォーマットであると仮定する。
1//强制一字节对齐
2#pragma pack(push, 1)
3//协议头
4struct msg
5{
6 int32_t bodysize; //包体大小
7};
8#pragma pack(pop)
次のように上記の手順のコードは次のとおりです。
1//包最大字节数限制为10M
2#define MAX_PACKAGE_SIZE 10 * 1024 * 1024
3
4void ChatSession::OnRead(const std::shared_ptr<TcpConnection>& conn, Buffer* pBuffer, Timestamp receivTime)
5{
6 while (true)
7 {
8 //不够一个包头大小
9 if (pBuffer->readableBytes() < (size_t)sizeof(msg))
10 {
11 //LOGI << "buffer is not enough for a package header, pBuffer->readableBytes()=" << pBuffer->readableBytes() << ", sizeof(msg)=" << sizeof(msg);
12 return;
13 }
14
15 //取包头信息
16 msg header;
17 memcpy(&header, pBuffer->peek(), sizeof(msg));
18
19 //包头有错误,立即关闭连接
20 if (header.bodysize <= 0 || header.bodysize > MAX_PACKAGE_SIZE)
21 {
22 //客户端发非法数据包,服务器主动关闭之
23 LOGE("Illegal package, bodysize: %lld, close TcpConnection, client: %s", header.bodysize, conn->peerAddress().toIpPort().c_str());
24 conn->forceClose();
25 return;
26 }
27
28 //收到的数据不够一个完整的包
29 if (pBuffer->readableBytes() < (size_t)header.bodysize + sizeof(msg))
30 return;
31
32 pBuffer->retrieve(sizeof(msg));
33 //inbuf用来存放当前要处理的包
34 std::string inbuf;
35 inbuf.append(pBuffer->peek(), header.bodysize);
36 pBuffer->retrieve(header.bodysize);
37 //解包和业务处理
38 if (!Process(conn, inbuf.c_str(), inbuf.length()))
39 {
40 //客户端发非法数据包,服务器主动关闭之
41 LOGE("Process package error, close TcpConnection, client: %s", conn->peerAddress().toIpPort().c_str());
42 conn->forceClose();
43 return;
44 }
45 }// end while-loop
46}
そして、コードの流れを示す上述の処理フローはpBufferカスタムコードここでは、データがこのバッファに受信された場合に受信したバイト数が決定されるので、受信バッファ、同じですこの方法は、オブジェクトに対応する数だけの使用を必要とします。私はいくつかの詳細を強調するために必要なコード:
-
包頭を服用する場合は、代わりに出バッファpBufferから直接データを取る、パケットヘッダアウトのデータサイズをコピーする必要がありますので、次のパケットのヘッダた場合、(すなわちpBufferから削除されたデータの取り出し)フィールドには、パッケージ本体のサイズを取得する際に、残りの場合のデータは、パッケージ本体のサイズではありませんが、あなたは、データバッファにこのヘッダーバックを配置する必要があります。データパケット全体のサイズのこの不必要な動作を、必要なだけのバッファサイズ(コード:header.bodysize +はsizeof(MSG))を避けるために、あなただけここで、バッファからのデータパケットの全体のサイズを削除する必要がありますPEEK()メソッドをのぞく語(中国語は「一目」または「のぞき」として翻訳することができます)を意味pBuffer->。
-
本体の大きさにより得られたパケットヘッダは、あなたが私は0より大きく超えない1024×1024×10(すなわち、10 M)である必要があり、ここで必要bodysize数値bodysizeを検証する必要がある場合。もちろん、実際の開発では、あなた自身のニーズに応じて(封入サイズが許可されているいくつかのビジネスシナリオで0バイトのパケットである)リミットbodysizeを決定することをお勧めします。不正なデータは、例えば、比較的大きな値bodysize提供されるクライアント、×1024×1024×1024 1(すなわち、G 1)から送られてくることが想定されるので、これは、下限値を上部に決定しなければならず、覚えて、あなたロジックは、サービスで、その結果、あなたのプロセスを強制終了されます、それはあなたのプロセスのメモリが特定のしきい値に達した検出したときに、サーバーのメモリはすぐに、オペレーティングシステム疲れでしょう、あなたがクライアントから送信されたデータのキャッシュを保持するようになりますもはや通常の外部サービスすることができます。あなたが検出された場合はbodysizeフィールドは、直接、この道路の接続外れ、違法bodysizeのために設定あなたの上限値と下限値を満たしています。また、自己保護サービスで、不正なパケットによる損失を避けます。
-
あなたはそれが必要であり、whileループの内部でロジックを処理全体の判断包頭、包含とパケットを気づいているかどうかはわかりません。ループしながら、あなたはより多くのワンタイム・パッケージより受信したときにこれがない、あなただけの次のプロセスを扱いますし、データの新しいバッチは、トリガが再びこのロジックが来るまで待つ必要があるだろう。このような原因の結果は、ピアがあなただけになるまで待たなければならないあなたの答えの背面に再び送信されるピア・データのいずれかを答えることができる、あなたに複数のリクエストを送ったということです。これはにあるスティックパッケージの正しい処理ロジック。
そして、上記のコードは、最も基本的で粘着性とパケットのパケット半加工メカニズム、いわゆるパケット処理ロジックの技術的な解決策は、(処理ロジックは、後の章では、再導入事業をアンパック)。私は、例えば、我々は合意パッケージを与える、我々は多くの機能を解凍するために拡張することができ、読者がそれらを理解することを願って、自分の基礎を理解することは、我々はこのような次のヘッダーとなり、圧縮機能のサポートが追加されます。
1#pragma pack(push, 1)
2//协议头
3struct msg
4{
5 char compressflag; //压缩标志,如果为1,则启用压缩,反之不启用压缩
6 int32_t originsize; //包体压缩前大小
7 int32_t compresssize; //包体压缩后大小
8 char reserved[16]; //保留字段,用于将来拓展
9};
10#pragma pack(pop)
次のようにコードを変更しました。
1void ChatSession::OnRead(const std::shared_ptr<TcpConnection>& conn, Buffer* pBuffer, Timestamp receivTime)
2{
3 while (true)
4 {
5 //不够一个包头大小
6 if (pBuffer->readableBytes() < (size_t)sizeof(msg))
7 {
8 //LOGI << "buffer is not enough for a package header, pBuffer->readableBytes()=" << pBuffer->readableBytes() << ", sizeof(msg)=" << sizeof(msg);
9 return;
10 }
11
12 //取包头信息
13 msg header;
14 memcpy(&header, pBuffer->peek(), sizeof(msg));
15
16 //数据包压缩过
17 if (header.compressflag == PACKAGE_COMPRESSED)
18 {
19 //包头有错误,立即关闭连接
20 if (header.compresssize <= 0 || header.compresssize > MAX_PACKAGE_SIZE ||
21 header.originsize <= 0 || header.originsize > MAX_PACKAGE_SIZE)
22 {
23 //客户端发非法数据包,服务器主动关闭之
24 LOGE("Illegal package, compresssize: %lld, originsize: %lld, close TcpConnection, client: %s", header.compresssize, header.originsize, conn->peerAddress().toIpPort().c_str());
25 conn->forceClose();
26 return;
27 }
28
29 //收到的数据不够一个完整的包
30 if (pBuffer->readableBytes() < (size_t)header.compresssize + sizeof(msg))
31 return;
32
33 pBuffer->retrieve(sizeof(msg));
34 std::string inbuf;
35 inbuf.append(pBuffer->peek(), header.compresssize);
36 pBuffer->retrieve(header.compresssize);
37 std::string destbuf;
38 if (!ZlibUtil::UncompressBuf(inbuf, destbuf, header.originsize))
39 {
40 LOGE("uncompress error, client: %s", conn->peerAddress().toIpPort().c_str());
41 conn->forceClose();
42 return;
43 }
44
45 //业务逻辑处理
46 if (!Process(conn, destbuf.c_str(), destbuf.length()))
47 {
48 //客户端发非法数据包,服务器主动关闭之
49 LOGE("Process error, close TcpConnection, client: %s", conn->peerAddress().toIpPort().c_str());
50 conn->forceClose();
51 return;
52 }
53 }
54 //数据包未压缩
55 else
56 {
57 //包头有错误,立即关闭连接
58 if (header.originsize <= 0 || header.originsize > MAX_PACKAGE_SIZE)
59 {
60 //客户端发非法数据包,服务器主动关闭之
61 LOGE("Illegal package, compresssize: %lld, originsize: %lld, close TcpConnection, client: %s", header.compresssize, header.originsize, conn->peerAddress().toIpPort().c_str());
62 conn->forceClose();
63 return;
64 }
65
66 //收到的数据不够一个完整的包
67 if (pBuffer->readableBytes() < (size_t)header.originsize + sizeof(msg))
68 return;
69
70 pBuffer->retrieve(sizeof(msg));
71 std::string inbuf;
72 inbuf.append(pBuffer->peek(), header.originsize);
73 pBuffer->retrieve(header.originsize);
74 //业务逻辑处理
75 if (!Process(conn, inbuf.c_str(), inbuf.length()))
76 {
77 //客户端发非法数据包,服务器主动关闭之
78 LOGE("Process error, close TcpConnection, client: %s", conn->peerAddress().toIpPort().c_str());
79 conn->forceClose();
80 return;
81 }
82 }// end else
83
84 }// end while-loop
85}
コードの最初のヘッダフィールド圧縮フラグが圧縮袋体かどうかが決定される、圧縮場合、パッケージ本体のサイズを除去するために伸張、伸張後のデータは、実際のデータトラフィックです。プログラムフロー・チャートを通じて、次のとおりです。
コードはどのように設計するかに受信バッファを、私たちは後ほど記事で詳しく意志、変数pBuffer受信バッファがあります。