[Linux] 高度な IO --- Reactor ネットワーク IO 設計パターン

実際、人は誘惑に抵抗するのが難しく、誘惑から遠ざかることしかできないので、自分の集中力を過信しないでください。
ここに画像の説明を挿入


1. LT および ET モード

1. LT と ET の動作原理を理解する

1.
マルチチャネル転送インターフェイス select poll epoll によって行われる作業は、実際にはイベント通知です。イベントの到着を上位層に通知するだけです。準備完了イベントの処理作業は、これらの API によって完了しません。これらのインターフェイスが実行されるときイベント通知、独自の戦略はありますか?
実際にはあります. ネットワーク プログラミングでは、select poll は LT 動作モードのみをサポートしますが、epoll は LT 動作モードに加えて ET 動作モードもサポートします. 異なる動作モードは、異なる readiness イベント通知戦略に対応します. LT モードはこれらの IO です デフォルトET モードはインターフェイスの動作モードであり、epoll の効率的な動作モードです。

2.
ET モードと LT モードの違いを誰もが理解できるように例を挙げてみましょう (速達配達の例) 新任の
配達員 Xiao Li は、寮棟 24 の Zhang San に速達便を配達したいと考えています。 6、7人の配達員を連れて、シャオ・リーは雪24の1階に行き、それから2階のチャン・サンに電話して、下に来て配達員を取りに来るように伝えましたが、チャン・サンは闇ゲームをしていました。正直者のシャオ・リーは、張三がまだ速達を取りに来ないのを見て、もう一度張三に電話して尋ねました。張三は速達を取りに来るように言ったが、張三はまた、私はすぐに速達を取りに行くと言いました、本当にすぐに、しかししばらくしても張三はまだ降りてこなかったので、シャオ・リーは張三に電話することしかできませんでした、張三、速達が到着しました、できるだけ早く降りて速達を取りに来てください最後に、張三と彼の友達は反対側のクリスタルを押し終えて階下に行きました宅配便を迎えに行きます。しかし、張三が連れ去ったのは 3 人の宅配便だけで、まだ 3 人の宅配便が残っています。張三には何もすることがありません。張三は一度にたくさんの速達しか受け取れないので、張三は 3 つの速達を受け取りました。上の階で配達物を受け取り、ルームメイトと白黒ゲームを続けました。しばらくして、シャオ・リーは再び張三に電話して、「張三、速達の受け取りがまだ終わっていません。6個の商品を購入しましたが、3個しか受け取っていません。まだ受け取っていない荷物が3個残っています」と言いました。張三はまた言いました、「分かった、分かった、すぐに取りに行きます。」しかし実際には前の行動を繰り返し、しばらくして階下に降りて残りの3つの包みを持ち去りました。荷物はすべて持ち去られた、シャオ・リー、もうチャン・サンには電話しない。
昔の優条配達員であるシャオ・ワンは、偶然にも寮24号棟で張三に宅配便を配達していた。偶然にも、今回は張三が配達員を6人追加で購入したため、シャオ・ワンもたまたま6つの荷物を張三に届けることになった。シャオ・ワンは張三の建物の下に到着し、張三に電話し、「張三、一度しか電話しないよ。今すぐ速達を取りに来ないなら、あなたが連絡しない限り、後で電話はしません」と言いました。新しい宅配便を購入しました。担当の宅配便の数が増えたら、親切にまたお電話させていただきます。それ以外の場合は、電話は 1 回だけです。取りに来られない場合は、宅配便、それならあなたのことは気にしません、他の顧客に速達で送ります。張三義は、「それは不可能です。今すぐに速達を取りに来ないと、宅配業者は将来電話をかけてくれなくなります。では、階下に行って宅配業者が見つからなかったらどうなりますか?」と聞きました。速達が届かない? そこで、Zhang Sanyi San さんはすぐに階下に宅配便を迎えに行きました。張三は一度にそんなに多くの速達を受け取ることはできませんが、張三は速達を見逃すわけにはいきません。なぜなら、シャオ・ワンは次回また張三に電話をかけないだろうからです。そこで、張三は3つの速達を置くために2階に行きました。配達物を手に持ち、すぐに階下に戻り、残りの配達員3人を連れて行きます。

3.
上記の 2 つの例では、Xiao Li の動作モードは実際には LT モードと呼ばれるレベル トリガー モードであり、Xiao Wang の動作モードは ET モードと呼ばれるエッジ トリガー モードであり、これもマルチチャネル スイッチング インターフェイスです。効率的なモード。
LT が epoll で動作する方法は、epoll がソック上の準備完了イベントを検出すると、epoll_wait がすぐに戻ってプログラマにイベントの準備ができたことを通知することです。プログラマは、ソック バッファ内のデータの一部のみを読み取ることを選択できます。残りのデータは一時的に読み込みを停止し、次回のrecv呼び出し時にsockバッファに残っているデータを読み込みます。もちろん、epoll_wait は通知されてから呼び出されるため、プログラマがすぐにソケット内のデータを取り除かない限り、後で epoll_wait が呼び出されたときに、epoll_wait は依然として Ready イベントを通知し、プログラマにデータを読み取るように指示します。残りのデータ、およびこのメソッドは LT モードです、つまり、最下層に読み取られていないデータがある限り、epoll_wait が返されたときにユーザーにデータを読み取るように常に通知されます。
ET の対応する動作方法は、基底層に読み取られていないデータがある場合、後続の epoll_wait はプログラマにイベントの準備ができたことを通知しません。基底データが増加した場合にのみ、epoll_wait はプログラマに再度通知し、それ以外の場合はプログラマに通知します。 epoll_wait はプログラマに 1 回だけ通知します。

2. コードを通じて LT 動作モードと ET 動作モードの違いを観察する

1.
前の記事では、epoll_server について書きました。もちろん、epoll_server のデフォルトの動作モードも LT モードです。次のコードでは、ready イベントを処理するインターフェイス HandlerEvent() をブロックしました。クライアント接続が到着すると、サーバー epoll_wait は listensock 上の読み取りイベントの準備ができていることを確実に検出するため、epoll_wait はデータを処理する必要があることをプログラマに通知します。ただし、プログラマがデータを処理していない場合、epoll_wait はプログラマにデータが処理されていないことを通知します。したがって、モニターの出力から判断すると、epoll_wait が戻った後、戻り値 n に従ってデフォルトのブランチに入っているはずで、そのたびに epoll_wait はプログラマにイベントの準備ができたことを通知します。最下位層にイベントの準備ができている限り、モニターはイベントの準備ができている状態で印刷を続けます listensock の場合、カーネルのリスニング キューに接続の準備ができている限り、準備は完了しています epoll_wait は常にプログラマにイベントが発生したことを通知します準備ができていますので、急いで処理してください。(シャオ・リーと同じように、チャン・サンが速達便を取り上げない限り、シャオ・リーはチャン・サンに電話し続けるでしょう)

ここに画像の説明を挿入

2.
epoll の下部にある赤黒ツリーに listensock を追加する場合、listensock の読み取りイベントだけでなく、EPOLLIN と EPOLLET がビットごとの OR 演算されている限り、listensock の動作モードも ET になります。
したがって、接続が到着すると、サーバーはイベントの準備ができていることを 1 回だけ出力することがわかります。新しい接続が到着しない限り、epoll_wait はプログラマにイベントの準備ができたことを 1 回だけ通知します。新しい接続が到着しない限り、それは次のことを意味します。カーネルはキュー内の Ready イベントをリッスンします。さらに多くの接続があります。つまり、listensock の下部にはさらに多くのデータがあります。この時点で、epoll_wait はプログラマにイベントの準備ができていることを再度通知します。早く対処してください。一方、listensock の基になるデータが将来増加しない限り、epoll_wait はプログラマに通知しません。
また、設定したタイムアウトはブロッキング待機であるため、新しい接続が到着しない限りサーバーはブロックされ、epoll_wait 呼び出しは返されず、プログラマには通知されないことがわかります。一方、LT モードでは、epoll_wait は毎回ブロッキング待ちですが、epoll_wait は毎回戻り、プログラマに毎回通知される点が両者の違いです。エッジトリガーは 1 回のみトリガーし、水平トリガーは常にトリガーします

ここに画像の説明を挿入

3. ET モードが効率的である理由 (fd はノンブロッキングである必要がある)

1.
ET モードはなぜ効率的ですか? これは非常に重要な面接の質問です。多くの面接官がネットワークの側面について質問するとき、彼らは、select poll epoll の使用法、epoll の基本原理、3 つのインターフェースの長所と短所、および 2 つの機能について話してほしいと求めます。 epoll の動作モードの種類と ET モードが効率的である理由 ET モードが効率的である理由も高周波の問題です。

2.
ET モードでは、基になるデータが何もない状態からそれ以上になった場合にのみ、上位層に 1 回通知されます。通知メカニズムは rbtree+ready_queue+cb であるため、ET 通知メカニズムはプログラマに 1 回強制します。基になるすべてのデータを読み取ります。データ。一度に読み込まれないと、データが失われる可能性があります。相手がデータを送信し続けるとは保証できません。これが保証できない場合、次回 epoll_wait が通知してくれる保証はありません。保証はできません。sock データの一部しか読み取っていない可能性がありますが、epoll_wait が再度通知しない可能性があり、その結果、後続のデータを読み取れなくなるため、すべてを一度に読み込む必要があります。データが読み取られます。
すべての基礎となるデータが一度に読み取られるようにするにはどうすればよいでしょうか? その場合、ループ内でのみ読み取ることができます。recv を 1 回だけ呼び出した場合、基になるすべてのデータが一度に読み取られるという保証はありません。したがって、while ループを作成して、データが読み取れなくなるまでソケット受信バッファー内のデータを読み続けることができますが、実際にはここで別の問題が発生します。ソケットがブロックされている場合、ループの終わりまでデータは確実に存在しません。このとき、sockがブロックされているため、データが到着するまで最後のrecvシステムコールでサーバーがブロックされ、この時点でサーバーは一時停止されます。サーバーが一時停止されたら終了です~サーバーの
場合サーバーが停止されると、サーバーは実行できなくなり、顧客にサービスを提供できなくなり、多くの企業が利益を失う可能性が非常に高いため、サーバーを停止することはもちろん、停止してはなりません。お客様にサービスを提供するため。ノンブロッキング ファイル記述子を使用する場合、recv がデータを読み取れない場合、recv は -1 を返し、エラー コードは EAGAIN と EWOULDBLOCK に設定されます。これら 2 つのエラー コードの値は同じであり、次のことを判断できます。現時点では、基礎となるデータをすべて一度に読み取ります。
したがって、エンジニアリングの実践では、epoll が ET モードで動作する場合、特定のリソースの準備が整うまでの待機によってサーバーが一時停止されないように、ファイル記述子を非ブロッキングに設定する必要があります。

3.
ET モードでは fd がノンブロッキングでなければならない理由を説明した後、ET モードが効率的であるのはなぜですか? ET モードは 1 回しか通知されず、プログラマはすべてのデータを一度に読み取らなければならないため、ET モードは効率的であると言う人もいるかもしれません。この質問が 100 点満点である場合、あなたの答えは 20 点しか獲得できません。答えは実際には答えへの単なる導きであり、最も重要な部分をまだ言っていません。
プログラマーがすべてのデータを一度に読み取らなければならない場合、上位層はできるだけ早くデータを持ち出すことができることを意味しませんか? できるだけ早くデータを削除した後、より大きな 16 ビット ウィンドウ サイズを相手に送信できるため、相手はより大きなスライディング ウィンドウ サイズを更新し、基礎となるデータ送信の効率を向上させ、TCP 遅延をより効果的に使用できるようになります。レスポンス、スライディングウィンドウ、その他の戦略。これがETモデルの高効率の最大の理由です。
ET モードでは、遅延応答、スライディング ウィンドウなど、データ送信効率を向上させるための TCP のさまざまな戦略を有効に活用できるためです。
以前 TCP について話したときに、TCP ヘッダーに PSH と呼ばれるフィールドがあります。実際、このフィールドが設定されている場合、epoll_wait はこのフィールドを通知メカニズムに変換し、できるだけ早くデータを読み取るように上位層に通知します。 。

4. LTおよびETモードでの読み取り方法

二、Reactor

1.tcpServer.hpp

1.1 接続構造

1.
ソケットが通信するとき、各ソケットはカーネル内に受信バッファーと送信バッファーを作成することがわかっています。このようなバッファーは多くの場合ヒープ上で開かれ、一時変数 charbuffer[1024] に従いません。スタックが完了すると破棄されます。フレーム スペースが破壊されると、ネットワークで受信したデータとネットワークに送信されるデータをより適切に保存できます。スタック上のスペースがネットワークで送受信されるデータの保存に使用される場合、データはおそらく破壊されます。変数が配置されているスタック フレームが破壊されている限り、変数内のデータは、次に変数が再度開かれたときに、最初に保存されていたネットワーク データから初期化されていないランダム データに変更されるためです。
したがって、各ソックスが独自の送受信バッファを持つことができるようにするために、サーバーを作成するときにソックスにネットワーク データを保存するために文字バッファ [1024] を使用するのではなく、代わりに通信ソックスを表すために Connection 構造を使用します。 、この構造体には、通信ソケット記述子 _sock と、sock に対応する _inbuffer および _outbuffer が含まれています。
さらに、構造体には、sock に対応する読み取りメソッド、書き込みメソッド、例外メソッドをそれぞれ表す 3 つのコールバック メソッド _recver、_sender、および _Excepter も含まれています。 func_t はラッパー型で、パッケージの内容は関数ポインターであり、戻り値を返します。値は void で、パラメータは Connection ポインタ タイプです。これら 3 つのパラメータは、実際には Reactor モードの魔法のような機能です。後で Reactor を要約するときに、なぜ Connection がこのように設計されているのか、また Reactor がなぜこのように設計されているのかがわかります。リアクターモードと呼ばれます。Reactor ネットワーク ライブラリを実装するには、この接続が鍵となります。
この構造体には、追加のサーバー タイプ ポインターも含まれています。シナリオによっては、たとえば、Connection 構造体と TcpServer サーバー クラスがファイルに分割されます。このとき、クラス内の Connection コールバック メソッド メソッドで TcpServer を呼び出したい場合は、このバック ポインタは、TcpServer のメソッドを取得するのに役立ちますが、今日は両方のクラスが tcpServer.hpp に配置されているため、今日は必要ありません。
Connection はまた、登録関数とソケットを閉じる関数の 2 つの関数を実装します。登録関数は、外部実装されたソケットに対応する読み取りメソッド、書き込みメソッド、例外メソッドを構造体 Connection に登録するために使用されます。靴下が置かれている場所。

ここに画像の説明を挿入

1.2 サーバーを初期化する

1.
initServer インターフェースは依然として最初に listensock を作成し、サーバーの IP アドレスとポート番号をバインドし、サーバーを listen 状態に設定します。これは Reactor ネットワーク・ライブラリーであるため、使用されるマルチパス・インターフェースは epoll でなければなりません。 sock と同様に、今日の epoll に対応するインターフェースもカプセル化し、コンポーネントとして使用するために epoller.hpp に個別に実装しました。
サーバーの実行を開始すると、多数の新しい接続構造オブジェクトが必要になりますが、これらの構造オブジェクトを管理する必要がありますか? もちろん必要なので、サーバークラスでハッシュテーブル_connectionsを定義します。sockをハッシュテーブルのキー値として使用し、sockに対応する構造体接続をキー値に対応する値、つまりハッシュとして使用します。バケットに格納されている値には今日はハッシュの競合がないため、各キー値のハッシュ バケットには 1 つの値 (接続構造) のみが含まれます。サーバーを初期化するとき、最初の値をハッシュに追加する必要があります
。ハッシュ テーブル内のソケットは listensock である必要があるため、initServer メソッドで、最初に listensock をハッシュ テーブルに追加し、同時に listensock に対応するイベントを処理するメソッドを渡します。注意する必要があるのは、メソッドを読み取り、他の 2 つのメソッドを nullptr に設定することだけです。

2.
AddConnectionでは、イベントにEPOLLETがあるかどうかを判定する必要があるが、EPOLLETがある場合はファイルディスクリプタがノンブロッキングである必要があるため、sockをノンブロッキングに設定する必要がある 設定方法もfcntlを使用するだけで簡単同様に、fcntl も SetNonBlock() メソッドにカプセル化して使用します。Reactor の epoll の動作モードは ET であるため、Reactor ネットワーク ライブラリは効率的です。
次のステップでは、新しい接続構造を作成し、コールバック メソッドの値や構造内のファイル記述子の値などを設定するなど、構造のフィールドに値を入力します。接続構造が作成された後、カプセル化された AddEvent インターフェイスを呼び出し、監視のために epoll するソケットとイベントを引き渡す必要があります。最後に、管理のために新しい構造をハッシュ テーブルに引き渡すことを忘れないでください。 。

2.
コードの実装に関しては、AddConnection にパラメーターを渡すときに、バインドの使用である C++11 の知識が使用されます。一般的に、ラッパーでラップされた関数ポインター型をラッパー型に渡すと、このとき、ラッパーは本質的にはラップされたオブジェクトのメソッドを内部で呼び出すファンクターであるため、パラメーターの受け渡しには問題はありません。
しかし、クラス内でパラメータを渡すと、問題が発生します。型の不一致の問題が発生します。この問題は本当にうんざりします。関数はテンプレートであるため、この問題が報告されると、多くのエラーが報告されます。 C++ エラーで最も嫌なことはテンプレート エラーであり、エラーが報告されると人々は爆発します。そうは言っても、なぜ型が一致しないのでしょうか? クラス内のメソッドがクラス内で呼び出されるとき、実際には this ポインターを通じて呼び出されるため、Accepter メソッドを直接 AddConnection に渡すと、Accepter の最初のパラメーターが this ポインターであるため、2 つの型は一致しません。 、正しいです。メソッドは、ラッパーのアダプター バインドを使用してパラメーターを渡し、バインドはアクセプターをバインドします。アクセプターの最初のパラメーターはthis pointer なので、最初のパラメーターを this に固定でき、次のパラメーターは今は渡されませんが、Accepter メソッドが呼び出されたときに渡される必要があります。この方法でのみ、クラス メンバー関数ポインターをラッパー型に渡すことができます。クラスで。
ただし、一般的には使用されない別の方法があります。ラムダ式を使用してパラメータを渡すことです。ラムダはコンテキストの this ポインタをキャプチャし、ラムダ型をラッパー型に渡すことができます。この方法は一般的には使用されませんが、はい、関数とバインドは適応モードです。この 2 つを一緒に使用するとより快適です。ラムダ メソッドを理解するのは良いことです。

ここに画像の説明を挿入

1.3 イベントディスパッチャー

1.
イベント ディスパッチャーは、実行を開始しようとしている実サーバーです。サーバーは、準備ができているすべての接続を処理します。まず、接続がハッシュ テーブルにない場合、この接続のソケットが追加されていないことを意味しますepoll モデルに追加します。赤黒ツリーは直接処理できません。最初に赤黒ツリーに追加してから、epoll_wait に準備完了の接続を取得させ、プログラマに通知してこの時点で処理する必要があります。そのため、待機せずにデータ コピーを直接処理します。
ループ内で Ready イベントを処理するメソッドは非常に単純です。ready fd が読み取りイベントに関係する場合は、sock が配置されている接続構造内で読み取りメソッドを直接呼び出します。書き込みイベントの場合は、書き方 以上です。fdが異常事態を気にするなら?という人もいます。実は、異常イベントの多くはreadイベントでもありますが、writeイベントもあるので、readメソッドとwriteメソッドに例外処理のロジックを直接組み込むことができ、異常イベントが来たら、対応するreadメソッドに直接遷移します。メソッドまたは write メソッド内部で対応するロジックを実行します。
異常なイベントが発生すると、異常なイベントは epoll_wait によって返されるイベント セットにカーネルによって自動的に設定されます。この異常なイベントはソケットに関連付けられている必要があります。たとえば、クライアントとサーバーはソケットを介して通信します。クライアントが突然接続を閉じると、サーバーのソックは元々読み取りイベントを処理していました。このとき、カーネルは例外イベントをソックが処理するイベント セットに自動的に設定します。 sock は考慮しますが、read メソッドは例外イベントを付随的に処理します。サーバーの通信ソケットを閉じます。クライアントが接続を切断したため、サーバーはクライアントとの接続を維持する必要はありません。サーバーは切断するだけです。これは、ロジックは read メソッドで実装できます。

ここに画像の説明を挿入

2.
以下のようなイベント ディスパッチャーは、典型的な Reactor リアクター モードです。接続が来ると、処理のために対応するソックスが配置されている接続内のコールバック メソッドを直接呼び出すことができます。これは化学反応のようなものです。接続時リクエストや通信ネットワークのデータが到着すると、コードは化学反応のようなもので、接続に対応したリッスンソックや通信に対応したソックのメソッドを自動的に呼び出して処理することができ、化学反応器のようなものです。このようなネットワーク ライブラリは Reactor と呼ばれますが、その理由は、各ソケットに独自の対応する読み取りおよび書き込み例外メソッドがあるためです。
listensock に対応する _recver メソッドが Accepter 関数、通信 Sock に対応する _recver メソッドが Recver 関数、通信 Sock に対応する _sender メソッドが Sender 関数です。

ここに画像の説明を挿入

1.4 コールバック関数

1.
接続が listensock の下部に到着すると、epoll_wait がプログラマにイベントの到着を通知した後、listensock に対応する _recver コールバック メソッドが呼び出されます。このコールバック メソッドは、listensock を接続構造に追加するときに、すでに Accepter Assigned を listensock の _recver コールバック メソッドにバインドしています。
Accepter に入った後、listensock の基礎となる接続の読み取りを開始しますが、listensock の基礎となるすべてのデータを一度に読み取ることができることを保証できますか? システム コールを受け入れるとき、一度に取得できる接続は 1 つだけですが、listensock の下部に多数の接続がある場合はどうなるでしょうか。現在、epoll は ET モードになっています。一度読み込んだだけで、その後新しい接続が来ない場合はどうなりますか? 確立されていない接続に対応するクライアントはサーバーと通信できません。この問題はあなたのサーバーが原因です。私のクライアントはあなたと正常に通信しています。その結果、あなたのサーバーは私の接続要求を受け入れません。この場合、それは意味しますサーバーコードにバグがあるということです。
したがって、アクセプターでは、リスナーソケットの下部にあるすべてのデータが一度に読み取られるようにするために、リスナーソケットの下部にあるデータを周期的に読み取る必要があるため、アクセプターは読み取りのためにループを強制終了する必要があります。 ET モードでは、サーバーがループ読み取りのために一時停止されるのではないかと心配しています。すべてのファイル記述子は、当社によって非ブロッキングに設定されています。受け入れ接続が通信のために確立されたら、次のステップは、この接続を _connections ハッシュ テーブルに追加することです。 AddConnection では、ソケットに対応する Connection 構造体を作成し、構造体のフィールドに値を入力し、コールバック メソッドを構造体のメンバー変数に設定します。また、AddConnection は、ソケットと、それが関係するイベントも赤に設定します。 - epoll モデルの black ツリー。これにより、epoll はプログラマが懸念する fd の準備状況を監視します。
listensock の場合、読み取りイベントのみを考慮するため、AddConnection にパラメーターを渡すときに、最後の 2 つのメソッドは渡されません。しかし、通信 Sock の場合、最後の 2 つのメソッドも将来呼び出されるため、これらも渡す必要があります。ここでパラメーターを渡す場合、パラメーターはメンバー関数であるため、バインド固定パラメーター メソッドも使用してパラメーターを渡す必要があります。
accept システムコールの戻り値が 0 未満で、エラーコードが EAGAIN または EWOULDBLOCK に設定されている場合、このラウンドで listensock の下に用意されているすべてのデータを accept が読み取ったことを意味し、無限ループから抜け出すことができます。エラー コードが EINTR に設定されている場合は、プロセスが特定の受信シグナルに対応するハンドラー メソッドを実行している可能性があり、ここでの accept システム コールが中断される可能性があることを意味します。この時点では、基になるデータの読み取りを続行する必要があります。 listensock がループ内にあるため、続行します。別の可能性、つまり、accept システム コールが実際に失敗する可能性があり、現時点でのアプローチはループから抜け出すことです。

ここに画像の説明を挿入

2.
Recver には以前と同じ問題がまだあります。これは、3 つのマルチチャネル インターフェイス サーバーを作成するときにも解決されていない問題です。すべてのデータを一度に読み取れるようにするにはどうすればよいですか? それが保証できない場合は、Accepter と同様に、読み取りのためにループを強制終了する必要があります。recv 戻り値が 0 より大きい場合、最初に読み取りデータをバッファーに入れます。バッファーはどこですか? 実際、 conn パラメータが指す構造体には、構造体の sock に対応する送受信バッファが存在します。次に、外部から渡されたコールバック関数 _service が呼び出され、サーバーが受信したデータに対してアプリケーション層のビジネス ロジック処理が実行されます。
recv が 0 を読み取る場合、クライアントが接続を閉じたことを意味し、これは異常イベントとみなされ、sock に対応する例外処理メソッドを直接コールバックできます。
recv の戻り値が 0 未満で、エラー コードが EAGAIN または EWOULDBLOCK に設定されている場合、これは、recv がソケットの下部にあるすべてのデータを読み取って、この時点でループから抜け出したことを意味します。または、シグナルによって中断される可能性がある場合、ループはこの時点で実行を継続する必要があります。もう 1 つの状況は、recv システム コールが実際に間違っており、この時点でそれを処理するために sock の異常なメソッドも呼び出されることです。
ビジネスロジック処理メソッドは、このループ内のすべてのデータを読み取った後、すべてのデータを処理する必要があります。

ここに画像の説明を挿入

3.
以前にサーバーに書き込むとき、書き込みイベントを扱ったことはありません。書き込みイベントは読み取りイベントと同じではありません。常に読み取りイベントを考慮するように設定する必要がありますが、カーネルの送信バッファーにより書き込みイベントは通常準備ができています。空である可能性が高いです。スペースはあります。毎回読み取りイベントの処理を手伝ってくれるように epoll に依頼する必要がある場合、ほとんどの場合、データを送信するときにアプリケーションを直接コピーすることになるため、これは実際にはリソースの無駄です。レイヤ データをカーネル バッファに転送します。はい、待機はありませんが、recv は異なります。recv が読み取り中の場合、データはまだネットワーク内にある可能性があるため、recv が待機する可能性は比較的高いため、読み取りイベントの場合は、これは、sock が関係するイベント コレクションに設定する必要があります。
ただし、これは書き込みイベントには当てはまりません。書き込みイベントは時々ケア コレクションに設定する必要があります。たとえば、今回はデータを一度に送信しなかったものの、書き込みイベントをケアするようにソックスを設定しなかった場合、次の書き込みイベントの準備ができたら、カーネル送信バッファーにスペースがある場合でも、epoll_wait は通知しません。残りのデータを送信するにはどうすればよいので、この時点で書き込みイベントのケアを設定する必要があります。そして、epoll_wait を使用して、ソック上の書き込みイベントを監視できるようにします。これにより、次回 epoll_wait が通知したときに、前回送信されなかったデータを引き続き送信できます。
このとき、ET モードは 1 回しか通知しないのではないかと疑問に思う人もいるかもしれません。今回は書き込みケアを設定したが、次回データを送信するときに送信がまだ完了していない場合 (カーネル送信バッファに空き領域が残っていない可能性があるため)、ET モードは後で通知しません。残りのデータを送るには?ET モードは、基礎となる Ready イベント ステータスが変化すると、上位層に再度通知します。読み取りイベントの場合、データがスクラッチからマルチステートに変化すると、ET は上位層に再度通知します。書き込みイベントの場合、データの残りのスペースがなくなると、ET は上位層に再度通知します。カーネル送信バッファが何もない状態から複数の状態に変化する場合、ET も上位層に一度通知します。ET が通知するため、データ送信が不完全であるという問題を心配する必要はありません。
ループの外側では、アウトバッファが空かどうかを判断して、書き込みイベント懸念を設定するかどうかを決定するだけで済みます。データが送信されると、書き込みイベント懸念はキャンセルされ、epoll リソースは占有されません。次回書き込みイベントの準備ができていることを確認したいため、epoll_wait は書き込みイベントを処理するように通知できます。

ここに画像の説明を挿入

4.
異常イベントの処理方法としては、まず epoll モデルから異常イベントを一律に削除し、次にファイル記述子をクローズし、最後にハッシュテーブル _connecions から conn を削除します。
conn ポインタが指す接続構造空間は自分で解放する必要があることに注意してください。ハッシュテーブルが消去されていませんか?なぜプログラマー自身が接続構造空間を削除する必要があるのでしょうか?
ここで説明したいことの 1 つは、すべてのコンテナーが消去されると、コンテナー自体によって作成されたスペースのみが解放されるということです。ハッシュ テーブルのようなコンテナーは新しいノードを作成し、そこに conn ポインターとポインターが格納されます。次のノード. ハッシュ テーブルの消去が呼び出されると、ハッシュ テーブルは独自の新しいノード空間を解放するだけです. このノード空間には Connection 型のポインタが格納されており、このポインタ変数は構造体を指します。スペース、ハッシュ テーブルはこれらのことを考慮しません。コンテナは、開いたスペースのみを解放します。このスペースにはポインタ変数が含まれており、他の変数が存在する可能性があります。コンテナは、これらの変数が配置されている構造のみを解放します。ボディのスペース。この構造は、今日の接続ポインタなど、ユーザーが保存したいものを保存するためにコンテナによって開かれる必要があります。もちろん、他の変数を保存することも可能です。ハッシュ テーブルの場合は、ハッシュ テーブルは単一リンク リストにリンクされたベクトルを使用して実装されるため、リンク リスト ノード タイプを格納することもできます。
したがって、conn が指す領域を手動で解放する必要があります。conn が指すヒープ領域リソースを手動で解放したくない場合は、スマート ポインター オブジェクトを保存できます。このようにして、ハッシュ テーブルが消去されると、実際には、 Connection タイプ ポインタを格納する構造体を削除します。これにより、構造体のデストラクタが呼び出されます。このような構造体の内部にデストラクタを自分で書きません。デフォルトでコンパイラによって生成されたものを使用できます。コンパイラは処理しません。組み込み型。カスタム型はクラスのデストラクター、つまりスマート ポインター オブジェクトのデストラクターを呼び出します。デストラクター内で、conn が指す接続構造の動的メモリが解放されます。
これは実際には非常に面倒なので手動で解放するしかないのですが、手動で解放しないとメモリリークが発生します。

ここに画像の説明を挿入
5.
次の記事の第 5 部の 2 番目のタイトルでは、オブジェクトのメンバー変数に対してコンパイラによって生成されるデストラクターの処理戦略について説明します。このデストラクターは、組み込み型を処理せず、カスタム型のクラスを呼び出します。デストラクター。
カスタム型のポインターであっても、実際にはコンパイラによって組み込み型とみなされ、ポインター型のデストラクターは呼び出されないことに注意してください。
デストラクターが呼び出されると、削除対象のヒープ領域が解放され、オペレーティング システムに返されます。

[C++] クラスとオブジェクトのコアの概要

以下は、ポインターがカスタム型の場合、デフォルトでコンパイラーによって生成されるデストラクターが対応するデストラクターを呼び出さないことを証明するプロセスです。これは組み込み型の処理戦略と同じです。
ここに画像の説明を挿入

ここに画像の説明を挿入

1.5 epoller.hpp

以下に、カプセル化された epoll のさまざまなインターフェイスを示します。これらは難しくありません。以前に LT モードの epoll サーバーの単純なバージョンを作成したことがあり、epoll の使用に困難はないはずなので、ここでは詳しく説明しません。コードの実装を簡単に見てみましょう。今日は、これらの小さなコンポーネントがどのように実装されるかではなく、以前の Reactor の実装に焦点を当てます。

ここに画像の説明を挿入

2.プロトコル.hpp

2.1 完全なメッセージを解析する

1.
実際、tcpServer.hpp の説明の後、Reactor ネットワーク ライブラリの焦点は完了しました。つまり、ネットワーク IO レベルでの処理接続が到着し、ネットワーク データ送信を処理する作業が完了しました。
次のprotocol.hppは、スティッキー・パケットの問題への対処方法、アプリケーション層でのプロトコルのカスタマイズ方法、アプリケーション層プロトコル・ヘッダーの追加または削除、シリアル化の方法など、Reactorネットワーク・ライブラリに基づいてサーバーのアプリケーション層にのみアクセスします。シリアル化とその他のタスクはすべてアプリケーション層に属します。
しかし実際には、以前にプロトコルのカスタマイズとシリアル化と逆シリアル化について話したとき、つまりネットワーク バージョンの電卓を実装したときに、これらのタスクはすでに実装されていたため、protocol.hpp はその時点のコードから直接コピーされました。 、メッセージを解析するコードのみが変更されています。
したがって、その時点でプロトコルをカスタマイズする方法を忘れた場合は、戻って記事をもう一度読むことができます。

プロトコルのカスタマイズ + シリアル化および逆シリアル化

2.
アプリケーション層で sock の _inbuffer データを解析するために次のインターフェースが使用されますが、TCP はバイト ストリーム指向であるため、完全なメッセージをどのように解析するかという問題はアプリケーション層で実行する必要があります。
この時点でプロトコルを決定しました。プロトコル ヘッダーとペイロードの間に LINE_SEP があり、\r\n であり、ペイロードの末尾も \r\n です。プロトコル ヘッダーはペイロードのバイト サイズを示しますしたがって、バイト ストリームの _inbuffer では、完全なメッセージを解析するロジックは次のようになります。
安全上の理由から、最初に出力パラメータ テキストを空の文字列に設定し、次にインバッファ内で LINE_SEP の反復子の位置を見つけます。それを見つけて、ヘッダー部分 substr out をインターセプトし、ペイロードのサイズを取得するためにその stoi を整数に変換し、インターセプトしたヘッダーのクラス内関数 size() を呼び出してバイト サイズを取得します。これらのバイト サイズを合計すると、完全なメッセージのバイト サイズを取得できます。
最後のステップは、0 から始まる total_len バイトを直接インターセプトし、インターセプトした文字列を出力パラメーター テキストに入れることです。次に、インバッファから 0 バイトから total_len バイトまでのデータを削除するだけで、実際にはデータが上書きされます。このようにして、大量のバイト ストリーム データから完全なメッセージをインターセプトします。

ここに画像の説明を挿入

2.2 アプリケーション層プロトコルのカスタマイズ

実は、これらのアプリケーション層プロトコルに関するコードについては、前回のネットワーク版電卓でも解説していますので、簡単に説明しておきますので、混乱しているベテランの方は、最初に書いた記事を参照していただければと思います。

移動: プロトコルのカスタマイズ + シリアル化と逆シリアル化

1.
以下はアプリケーション層ヘッダーの追加と削除ですが、ヘッダーを追加する場合、実際にはヘッダーとペイロードの間とペイロードの最後に LINE_SEP を追加すれば、ヘッダーの内容はペイロードの長さ。
ヘッダーを削除するときは、最初の LINE_SEP を区切り文字として使用してヘッダー文字列をインターセプトし、ペイロードの長さを取得します。次に、最初の LINE_SEP 位置から部分文字列をインターセプトします。インターセプトの長さはペイロードの長さになります。はい、完全なペイロードを取得できます。

ここに画像の説明を挿入

2.3 シリアル化と逆シリアル化

1.
以下はシリアル化と逆シリアル化の作業です。私たちは主に独自のソリューションと json ソリューションを使用します。企業は通常、内部的に protobuf を使用し、外部的に json を使用します。json の使い方がわかりません。ざっとしか使えず、体系的に学んでいないため、以下では独自のシリアル化および逆シリアル化ソリューションについてのみ説明します。ただし、実際に社内で使用する場合は注意が必要です。シリアル化と逆シリアル化には既製のソリューションがあり、プログラマーが自分でそれらを記述することはありません。しかし今日、学習者である私たちは、自分で書くときにシリアル化と逆シリアル化がどのような作業を行うのかをよりよく理解できるようにする必要があり、これは間違いなく学習者にとって大きな利益になります。

2.
リクエスト メッセージのシリアル化では、実際には Request 構造体の _x _op _y とその他のフィールドを文字列に結合します。これでシリアル化作業は完了しますが、これは単なるシリアル化ではありません。シリアル化することもできます。したがって、文字列を結合する場合、受信リクエスト メッセージの逆シリアル化を容易にするために、_x と _op、および _op と _y の間に区切り文字として SEP が必要です。(構造化データ → バイト ストリーム データ)
デシリアライズは実際には文字列操作であり、文字列内の _x _y _op をインターセプトし、それらを int int char 型に変換し、構造体の 3 つのメンバー変数に割り当てます。 Request Inside、これでデシリアライズが完了します。仕事。(バイトストリームデータ → 構造化データ)

ここに画像の説明を挿入

3.
応答メッセージのシリアル化は、int 型の終了コードと計算結果を string 型に変換し、途中に SEP フィールドを繋ぐだけで構造化データがシリアル化されます。
逆シリアル化の作業も非常に簡単で、文字列の終了コードと結果部分を部分文字列にインターセプトし、それを int 型に変換することでシリアル化を構造化データに変換するだけです。

ここに画像の説明を挿入

3.main.cc

3.1 ビジネスロジックの処理

1.
Reactor サーバー全体の呼び出しロジックは以下の通りで、まずサーバーを初期化し、イベントディスパッチインターフェイス Dispatcher を実行します。
サーバーのアプリケーション層によって提供されるサービスはコンピューティング サービスであるため、サーバー オブジェクトを構築するときに、上位層の処理ロジック関数 Calculate もサーバー オブジェクトに渡す必要があります。
サーバーが Recver メソッドを実行すると、データを受信した後、コールバック関数が呼び出され、実行フローは、読み取ったデータに対して業務処理を行うために Calculate メソッドを実行します。

ここに画像の説明を挿入

2.
Calculate はビジネス ロジックの処理メソッドです。メソッド内に while ループを作成します。完全なメッセージが解析できる限り、ループに入り、受信したメッセージに対してアプリケーション層のロジック処理を実行できます。データが _inbuffer の場合取得され、残りのデータは完全なメッセージを形成できません。つまり、ParseOnePackage がエラーを起こした場合、この時点でループを終了することを選択し、処理されたすべての要求メッセージ、つまり構築された応答メッセージを顧客端末に送信します。 、誰かがすべての応答メッセージをクライアントに送信する方法を言いましたか?実際、これは非常に単純で、各リクエスト メッセージが ParseOnePackage で処理された後、対応するレスポンス メッセージが conn 内の送信バッファ _outbuffer に配置されるため、ループが飛び出した時点で、_outbuffer にはすでに準備完了のレスポンス メッセージが大量に格納されています。メッセージが生成されるので、この時はconn内のsenderメソッドを呼び出して送信するだけです。こうやって見てみると、このコンはとても役に立つのでしょうか?これは、Reactor コードによって実装されたすべてのモジュールを通じて実行されます。
ParseOnePackage 内部も非常に簡単です。protocol.hpp 内でリクエスト/レスポンス メッセージをすでにシリアル化および逆シリアル化し、アプリケーション層メッセージのヘッダーとペイロードを分離し、ヘッダーを追加しているため、ParseOnePackage では呼び出すだけで済みます。対応するprotocol.hppに内部的に実装されたメソッド。たとえば、最初にヘッダーを削除し、次に逆シリアル化インターフェイスを呼び出して構造化リクエストを取得し、構造化リクエストと初期化されていない構造化レスポンスに対して cal 処理を実行します。cal 処理内で、対応する計算作業が実際に実行され、計算作業が行われます。最後に、構造化された応答メッセージに結果を入力し、応答メッセージをシリアル化し、ヘッダーなどを追加して、最後に完全な応答メッセージをアウトバッファに配置します。ループが終了したら、すべての応答メッセージを相手と一体となって。

ここに画像の説明を挿入

3.2 Reactorサーバーの運用結果

1.
クライアントは記述しません プロトコルのカスタマイズについて話すとき、calclient と calserver はすでに実装されているため、ここではクライアントとして calclient を直接使用します。
実行結果から、通常のデータ計算リクエストの場合、サーバーは対応する計算結果を返すことができ、Ctrl+C で TCP 接続が切断されるなど、クライアントで例外が発生した場合にもサーバーが応答できることがわかります。サーバーなどの対応する処理は、対応する TCP 接続も閉じ、同時に、ソケットに対応する接続​​構造など、ソケットに対応するすべてのリソースを解放し、epoll モデルからソケットを削除し、ソケットを削除します。ハッシュ テーブル、sock ファイル記述子を閉じるなど。

ここに画像の説明を挿入

4. Reactor パターンを要約する

1.
Reactor についての私の個人的な理解は、Reactor は主にイベントのディスパッチと自動応答を中心に展開しているということです。たとえば、接続リクエストが到着すると、epoll_wait は準備完了イベントが到着したことをプログラマに思い出させます。それはできるだけ早く処理されるべきであり、 Readyイベントに関連付けられたsockがAの接続構造に相当します この構造がリアクターモデルの本質だと思います どのようなreadyイベントであっても各sockに対応するコールバックメソッドがあるので扱いやすいですReady イベント。接続内の対応するメソッドを直接コールバックできます。つまり、読み取りイベントの場合は読み取りメソッドを呼び出し、書き込みイベントの場合は書き込みメソッドを呼び出し、例外イベントの場合は、例外イベントは、読み取りメソッドまたは書き込みメソッドでの IO の処理中に処理されます。
Reactor は化学リアクターのようなものだと感じています。このリアクターに接続リクエストやネットワーク データを投げ込むと、リアクターは自動的に対応する処理メカニズムを照合して、受信したイベントを処理します。これは非常に便利です。 ET モードと EPOLL に移行すると、Reactor は大量の同時接続を処理するときに優れた強度を発揮できるようになります。

2.
現在実装しているサーバーは半同期と半非同期です。半同期とは、Reactor が準備イベントの通知を確実にするだけでなく、IO も担当することを意味します。半非同期とは、今日のサーバーがビジネス処理も実装することを意味します。

ここに画像の説明を挿入

おすすめ

転載: blog.csdn.net/erridjsis/article/details/132548615