コネクタコンポーネントから Tomcat のスレッドモデルを見る - BIO モード (推奨)

コネクタコンポーネントから Tomcat のスレッドモデルを見る - BIO モード (推奨)

Tomcat の上位バージョンでは、デフォルト モードは NIO モードを使用することになります。Tomcat 9 では、BIO モード Http11Protocol の実装も削除されました。しかし、BIO の動作メカニズムとその長所と短所を理解することは、他のモードを学習するのに役立ちます。比較して初めて、他のモデルの利点を知ることができます。

Http11Protocol は、ソケット接続からの受信、処理、クライアントへの応答のプロセス全体を含む、ブロッキング HTTP プロトコル通信を表します。主に JIoEndpoint コンポーネントと Http11Processor コンポーネントが含まれています。起動時に、JIoEndpoint コンポーネントは特定のポートでリッスンを開始します。リクエストが到着すると、スレッド プールにスローされ、タスクが処理されます。処理中、HTTP プロトコルはプロトコル パーサー Http11Processor コンポーネントによって解析され、アダプタを介して指定されたコンテナーと照合され、処理されてクライアントに応答されます。

ここでは、Spring Boot に組み込まれた Tomcat を組み合わせて、コネクタがどのように機能するかを確認します。 Spring Boot の下位バージョンを使用することをお勧めします。Spring Boot の上位バージョンでは、Tomcat 9 がすでに使用されています。 Tomcat 9 では BIO 実装が削除されました。ここで選択した Spring Boot のバージョンは 2.0.0.RELEASE です。

コネクタコンポーネントのソースコードを表示する方法

ここで、コネクタ コンポーネントのソース コードを通じて、コネクタ コンポーネントの動作プロセスの分析を開始します。しかし、Tomcat のソースコードは非常に多いので、どのように読めばよいのでしょうか?前の記事では、次の図に示すように、Tomcat の起動プロセスをまとめました。

上記のシーケンス図は、コネクタ コンポーネントの init メソッドと start メソッドから始めて、コネクタ コンポーネントのソース コードを分析するためのアイデアを提供します。

コネクタコンポーネントの動作シーケンス図

Spring Boot に組み込まれている Tomcat はデフォルトで NIO モードを使用します。BIO モードを学習したい場合は、自分で作業する必要があります。 Spring Boot はWebServerFactoryCustomizerインターフェースを提供しており、これを実装することでサーブレット コンテナ ファクトリをカスタマイズできます。以下は私が独自に実装した設定クラスです。これは単純に IO モデルを BIO モードに設定するものです。他の設定が必要な場合は、このクラス内で追加の設定を行うこともできます。

@構成
パブリッククラスTomcatConfig {

 @ビーン
 パブリック WebServerFactoryCustomizer tomcatCustomizer() {
  新しいTomcatCustomerConfig()を返します。
 }

 パブリッククラスTomcatCustomerConfigはWebServerFactoryCustomizer<TomcatServletWebServerFactory>を実装します。
  @オーバーライド
  パブリック void カスタマイズ (TomcatServletWebServerFactory ファクトリー) {
   if (ファクトリー != null) {
    factory.setProtocol("org.apache.coyote.http11.Http11Protocol");
   }
  }
 }
}

上記の設定後、Tomcat のコネクタ コンポーネントは BIO モードでリクエストを処理します。

Tomcat には大量のコードがあるため、1 つの記事ですべてのコードを分析するのは現実的ではありません。ここでは、コネクタ コンポーネントのタイミング図を整理しました。このタイミング図に基づいて、いくつかの重要なコード ポイントを分析しました。その他の詳細については、タイミング図に従って自分でコードを確認してください。このコードはそれほど複雑ではありません。

ここでのキーコードは、JIoEndpoint の init() メソッドと start() メソッドにあります。 JIoEndpoint の init() メソッドは主に ServerSocket のポートバインディングを実行します。具体的なコードは次のとおりです。

@オーバーライド
パブリック void bind() は例外をスローします {

 // アクセプタのスレッド数のデフォルトを初期化する
 アクセプタスレッドカウント == 0 の場合
  アクセプタスレッド数 = 1;
 }
 // maxConnectionsを初期化する
 getMaxConnections() == 0 の場合 {
  // ユーザーが値を設定していない場合は、デフォルトを使用します
  Executor で接続できる最大スレッド数を設定します。
 }

 (serverSocketFactory == null)の場合{
  SSLが有効になっている場合
   サーバーソケットファクトリー =
    ハンドラ.getSslImplementation().getServerSocketFactory(これ);
  } それ以外 {
   serverSocketFactory = 新しい DefaultServerSocketFactory(this);
  }
 }
 // ServerSocket のポートバインディングは次のとおりです。if (serverSocket == null) {
  試す {
   getAddress() が null の場合
    //特定のアドレスが指定されていない場合、Tomcatはすべてのアドレスからのリクエストをリッスンします。serverSocket = serverSocketFactory.createSocket(getPort(),
                バックログを取得します。
   } それ以外 {
    //特定のアドレスを指定します。Tomcat はこのアドレスからのリクエストのみをリッスンします。serverSocket = serverSocketFactory.createSocket(getPort(),
                getBacklog()、getAddress());
   }
  } catch (BindException orig) {
   文字列メッセージ;
   getAddress() が null の場合
    msg = orig.getMessage() + " <null>:" + getPort();
   それ以外
    メッセージ = orig.getMessage() + " " +
    getAddress().toString() + ":" + getPort();
   BindException は、新しい BindException(msg) になります。
   be.initCause(orig);
   投げる;
  }
 }

}

JIoEndpoint の開始メソッドを見てみましょう。

パブリック void startInternal() 例外をスローします {

 if (!実行中) {
  実行中 = true;
  一時停止 = false;

  //スレッドプールを作成する if (getExecutor() == null) {
   エグゼキュータを作成します。
  }
  //ConnectionLatch を作成する
  接続ラッチを初期化します。
  // リクエスト処理の初期スレッドである Accept スレッドを作成します。startAcceptorThreads();
  // 非同期タイムアウトスレッドを開始する
  スレッドタイムアウトThread = new Thread(new AsyncTimeout(),
           getName() + "-AsyncTimeout");
  タイムアウトスレッド。優先度を設定します。(スレッド優先度);
  タイムアウトスレッド.setDaemon(true);
  タイムアウトスレッドを開始します。
 }
}

上記のコードでは、startAcceptorThreads() メソッドに注目する必要があります。この Accept スレッドの具体的な実装を見てみましょう。

保護された最終void startAcceptorThreads() {
 スレッドカウントを取得します。
 acceptors = 新しいAcceptor[count];
 //設定に応じて、受け入れスレッドの数を設定します for (int i = 0; i < count; i++) {
  アクセプタ[i] = createAcceptor();
  文字列 threadName = getName() + "-Acceptor-" + i;
  アクセプタ[i].setThreadName(スレッド名);
  スレッド t = new Thread(acceptors[i], threadName);
  t.setPriority(getAcceptorThreadPriority());
  t.setDaemon(getDaemon());
  t.start();
 }
}

Acceptor スレッドの具体的な処理の実装については、run メソッドに注目してください。

保護されたクラス Acceptor は AbstractEndpoint.Acceptor を拡張します {

  @オーバーライド
  パブリックボイド実行() {

   エラー遅延 = 0;
   // シャットダウンコマンドを受信するまでループします
   (実行中){
    // エンドポイントが一時停止している場合はループする
    while (一時停止中 && 実行中) {
     状態 = AcceptorState.PAUSED;
     試す {
      スレッド.sleep(50);
     } キャッチ (InterruptedException e) {
      // 無視する
     }
    }

    if (!実行中) {
     壊す;
    }
    状態 = AcceptorState.RUNNING;

    試す {
     //最大接続数に達した場合は待機します
     //接続制限に達すると、アクセプタ スレッドは他のスレッドが解放されるまで待機状態になります。これは、接続数によるフロー制御の単純な手段です。 //これは、AQS コンポーネント (LimitLatch) を実装することで実現されます。考え方としては、まず同期装置の最大制限を初期化し、次にソケットを受信するたびに count 変数を 1 ずつ増やし、ソケットを閉じるたびに count 変数を 1 ずつ減らします。
     カウントアップまたは接続待ち();
     ソケット socket = null;
     試す {
      //次のソケット接続を受け入れます。接続が確立されない場合、このメソッドはブロックされます。socket = serverSocketFactory.acceptSocket(serverSocket);
     } キャッチ (IOException ioe) {
      //例外が発生した場合は接続を解放します countDownConnection();
      エラー遅延 = handleExceptionWithDelay(エラー遅延);
      ioeを投げる;
     }
     // 正常に承認されました。エラー遅延をリセットします
     エラー遅延 = 0;
     //ソケットを適切に設定します if (running && !paused && setSocketOptions(socket)) {
      // このソケット要求を処理します。これが重要なポイントでもあります。
      if (!processSocket(socket)) {
       カウントダウン接続();
       // すぐにソケットを閉じる
       ソケットを閉じます。
      }
     } それ以外 {
      カウントダウン接続();
      // すぐにソケットを閉じる
      ソケットを閉じます。
     }
    } キャッチ (IOException x) {
     if (実行中) {
      ログエラー(sm.getString("endpoint.accept.fail"), x);
     }
    } キャッチ (NullPointerException npe) {
     if (実行中) {
      ログエラー(sm.getString("endpoint.accept.fail"), npe);
     }
    } キャッチ (Throwable t) {
     例外をスローする
     ログエラー(sm.getString("endpoint.accept.fail"), t);
    }
   }
   状態 = AcceptorState.ENDED;
  }
 }

上記のスレッド処理クラスの processSocket(socket) は、特定のリクエストを処理するためのメソッドです。このメソッドはリクエストをパッケージ化し、処理のためにスレッド プールに「スロー」します。ただし、これはコネクタ コンポーネントの焦点では​​ありません。後ほど、リクエスト フローの紹介時に Tomcat がリクエストを処理する方法を紹介します。

ここでは、Tomcat の BIO モードについて簡単に紹介しました。実際、BIO モードを簡略化すると、従来の ServerSocket の動作となり、リクエストの処理はスレッド プールで最適化されていることがわかります。

BIOモードの概要

上図の各コンポーネントの簡単な説明を以下に示します。

電流制限コンポーネント LimitLatch

LimitLatch コンポーネントはフロー制御コンポーネントであり、Tomcat コンポーネントが大量のフローによって圧倒されるのを防ぐことを目的としています。 LimitLatch は AQS メカニズムを通じて実装されます。このコンポーネントが起動すると、まず同期装置の最大制限値を初期化し、受信したソケットごとに count 変数を 1 ずつ増やし、閉じたソケットごとに count 変数を 1 ずつ減らします。接続数が最大値に達すると、Acceptor スレッドは待機状態になり、新しいソケット接続を受け入れなくなります。

最大接続数に達すると (LimitLatch コンポーネントが最大値に達し、アクセプタ コンポーネントがブロックされる)、基盤となるオペレーティング システムはクライアント接続を引き続き受信し、要求をキュー (バックログ キュー) に入れることに注意してください。このキューにはデフォルトの長さがあり、デフォルト値は 100 です。もちろん、この値は server.xml の Connector ノードの acceptCount 属性を通じて構成できます。短時間内に大量のリクエストが送信され、バックログ キューがいっぱいになると、オペレーティング システムは後続の接続を拒否し、「接続拒否」を返します。

BIO モードでは、LimitLatch コンポーネントでサポートされる接続の最大数は、server.xml の Connector ノードの maxConnections 属性によって設定されます。-1 に設定されている場合、制限がないことを意味します。

アクセプター

このコンポーネントの役割は非常に単純で、ソケット接続を受信し、ソケットに対応する設定を行い、それを直接スレッド プールに渡して処理することです。受け入れスレッドの数も設定できます。

ソケットファクトリー ServerSocketFactory

ソケット接続を受け入れるとき、Acceptor スレッドは ServerSocketFactory コンポーネントを通じて取得されます。 Tomcat には、DefaultServerSocketFactory と JSSESocketFactory という 2 つの ServerSocketFactory 実装があります。それぞれ HTTP および HTTPS の場合に対応します。

Tomcat には、暗号化されたチャネルを使用するかどうかを識別するために使用される変数 SSLEnabled があります。この変数を定義することで、使用するファクトリ クラスを決定できます。Tomcat は、ユーザーがカスタマイズできる外部構成ファイルを提供します。次の構成では、SSLEnabled="true" は暗号化を使用すること、つまり JSSESocketFactory を使用して特定のソケット接続を受け入れることを意味します。

<コネクタ ポート="8443" プロトコル="org.apache.coyote.http11.Http11NioProtocol"
   最大スレッド数="150" SSL有効="true">
 <SSLホスト構成>
  <証明書 certificateKeystoreFile="conf/localhost-rsa.jks"
      タイプ = "RSA" />
 </SSLホスト構成>
</コネクタ>

スレッドプールコンポーネント

Tomcat のスレッド プールは、JDK のスレッド プールを単純に変更したものです。スレッド作成戦略には若干の違いがあります。Tomcat のスレッド プールは、スレッド数が coreSize を超えた直後にスレッドをキューに送信せず、まずアクティブ スレッド数が maxSize に達したかどうかを判断し、maxSize に達した後にのみスレッドをキューに送信します。

コネクタ コンポーネントの Executor は、共有 Executor とプライベート Executor の 2 種類に分かれています。共有 Executor は、サービス コンポーネントで定義された Executor です。

タスク定義者 SocketProcessor

ソケットをスレッド プールにスローする前に、タスクがソケットを処理する方法を定義する必要があります。 SocketProcessor はタスク定義であり、このクラスは Runnable インターフェースを実装します。

保護されたクラス SocketProcessor は Runnable を実装します {
 //デバッグ時には、このクラスのrunメソッドからデバッグを開始できます@Override
 パブリックボイド実行() { 
 	//ソケットを処理して応答を出力する //接続リミッター LimitLatch を 1 減らす //ソケットを閉じる}
}

SocketProcessor のタスクは、主にソケットの処理とクライアントへの応答、接続カウンターの 1 減算、ソケットのクローズの 3 つの部分に分かれています。その中で、ソケットの処理は最も重要かつ最も複雑です。ソケットの処理には、基盤となるソケット バイト ストリームの読み取り、HTTP プロトコル要求メッセージの解析 (要求行、要求ヘッダー、要求本体などの情報の解析)、要求行の解析から取得したパスに従って対応する仮想ホスト上の Web プロジェクト リソースの検索、処理結果に従って HTTP プロトコル応答メッセージを組み立ててクライアントに出力することが含まれます。

この記事は主にコネクタのスレッド モデルに関するもので、内容が多すぎて混乱しやすいため、ここではソケットの具体的な処理フローを分析することはしません。Tomcat のソケットの具体的な処理を分析する記事は後日作成する予定です。

要約する

これで、コネクタ コンポーネントの観点から見た Tomcat のスレッド モデル (BIO モデル) に関するこの記事は終了です。Tomcat のスレッド モデルの詳細については、123WORDPRESS.COM の以前の記事を検索するか、次の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM をよろしくお願いいたします。

以下もご興味があるかもしれません:
  • Webリクエストと処理のTomcatソースコード分析
  • Tomcatはスレッドプールを使用してリモート同時リクエストを処理します。
  • リクエストを処理するためのTomcatのスレッドモデルの詳細な説明

<<:  よく使われる3つのMySQLデータ型

>>:  便利なモバイルスクロールプラグイン BetterScroll

推薦する

MySQL におけるデータベース間関連クエリメソッド

ビジネスシナリオ: 異なるデータベース内のテーブルをクエリするたとえば、関連付けられるテーブルは、マ...

初心者がdockerにmysqlをインストールするときに遭遇するさまざまな問題

序文最近、パソコンのシャットダウンに時間がかかることが多く、強制的にシャットダウンするには電源ボタン...

HTML テーブル タグ チュートリアル (31): セルの幅と高さの属性 WIDTH、HEIGHT

デフォルトでは、セルの幅と高さはコンテンツに応じて自動的に調整されますが、セルの幅と高さを手動で設定...

MySQL における悲観的ロックと楽観的ロック

リレーショナル データベースでは、悲観的ロックと楽観的ロックがリソース同時実行シナリオのソリューショ...

Windows Apache 環境で SSL 証明書を展開して、Web サイトを https 対応にする方法

SSL 証明書の使用についてはここでは説明しません。SSL 証明書を導入する必要がある友人は、すでに...

Linux コマンド クエリ アプレットでの WePY クラウド開発の実践

みなさんこんにちは。今日は Linux コマンド クエリ アプレットでの WePY クラウド開発の実...

MySQL遅延レプリケーションライブラリ方式の詳細な説明

簡単に言えば、遅延レプリケーションとは、スレーブ データベースがマスター データベースより 1 時間...

CSS を使用してプログレスバーと順序プログレスバーを実装する例

この半月、期末試験の準備にかなりのエネルギーを費やしました。今日はしっかり復習するべきだったのですが...

MySQL パフォーマンスの包括的な最適化方法リファレンス、CPU、ファイルシステムの選択から mysql.cnf パラメータの最適化まで

この記事では、一般的な MySQL 最適化方法をいくつかまとめて簡単に紹介します。これは、フルタイム...

JavaScriptはランダムコードの生成と検証を実現する

JavaScriptでのランダムコードの生成と検証は参考までに。具体的な内容は以下のとおりです。イベ...

Ubuntu で中国語入力方法が使えない場合の解決策

Ubuntu では中国語入力方法の解決策はありません。仮想マシンや Ubuntu システムをインスト...

Tomcatの全体構造の簡単な紹介

Tomcat は Web コンテナとして広く知られています。Java を学び始めたときから現在の仕事...

Mysqlは実行中のトランザクションを照会し、ロックを待機する方法

navicatを使用してテストと学習を行います。まず、 set autocommit = 0;を使用...

JavaがMySQL 8.0に接続できない問題の解決策

この記事では、参考までにMySQL 8.0に接続できないJavaの問題をまとめて紹介します。具体的な...