Tomcat が非同期サーブレットを実装する方法の詳細な説明

Tomcat が非同期サーブレットを実装する方法の詳細な説明

序文

これまでの Tomcat シリーズの記事を通じて、私のブログを読んでいる学生は Tomcat についてより明確に理解できるはずです。以前のブログでは、SpringBoot フレームワークで Tomcat がどのように起動されるか、Tomcat の内部コンポーネントがどのように設計されているか、リクエストがどのように流れるかについて説明しました。次に、Tomcat の非同期サーブレット、Tomcat が非同期サーブレットを実装する方法、および非同期サーブレットの使用シナリオについて説明します。

非同期サーブレットの実践

サーブレットを実装するために、SpringBoot フレームワークを直接使用します。ここでは、サーブレット コードのみを示します。

@WebServlet(urlPatterns = "/async",asyncSupported = true)
翻訳者
パブリッククラスAsyncServletはHttpServletを拡張します{

 ExecutorService executorService =Executors.newSingleThreadExecutor();

 @オーバーライド
  保護された void doGet(HttpServletRequest req, HttpServletResponse resp) は ServletException、IOException をスローします {
  //非同期を開始し、非同期コンテキストを取得します。final AsyncContext ctx = req.startAsync();
  // スレッドプールの非同期実行を送信します。executorService.execute(new Runnable() {


   @オーバーライド
   パブリックボイド実行() {
    試す {
     log.info("非同期サービスの実行準備ができました");
     //時間のかかるタスクをシミュレートする Thread.sleep(10000L);
     ctx.getResponse().getWriter().print("非同期サーブレット");
     log.info("非同期サービスが実行されました");
    } キャッチ (IOException e) {
     e.printStackTrace();
    } キャッチ (InterruptedException e) {
     e.printStackTrace();
    }
    //最後に、実行が完了するとコールバックが完了します。
    ctx.complete();
   }
  });
 }

上記のコードは非同期サーブレットを実装し、 doGetメソッドを実装します。SpringBoot では、サーブレットをスキャンするためにクラスを開始し、 @ServletComponentScanアノテーションを追加する必要があることに注意してください。コードが記述されたので、実際にどのように動作するかを見てみましょう。

リクエストを送信すると、ページが応答し、リクエストに 10.05 秒かかることがわかります。つまり、サーブレットは正常に実行されています。学生の中には、これは非同期サーブレットではないのかと尋ねる人もいるでしょう。応答時間が速くなければ意味がありません。はい、応答時間は短縮できません。依然としてビジネス ロジックに依存しますが、非同期サーブレット要求の後、ビジネスの非同期実行に依存して、すぐに戻ることができます。つまり、Tomcat のスレッドをすぐにリサイクルできます。デフォルトでは、Tomcat のコア スレッドは 10 で、最大スレッド数は 200 です。時間内にスレッドをリサイクルできるため、より多くの要求を処理してスループットを向上させることができます。これは、非同期サーブレットの主な機能でもあります。

非同期サーブレットの内部

非同期サーブレットの役割を理解した後、Tomcat がどのようにして最初の非同期サーブレットになったのかを見てみましょう。実際、上記のコードの主なコアロジックは、 final AsyncContext ctx = req.startAsync();ctx.complete(); 2 つの部分で構成されています。これらが何をするのか見てみましょう。

 パブリックAsyncContext startAsync(ServletRequest リクエスト、
   ServletResponse レスポンス) {
  (!isAsyncSupported())の場合{
   不正な状態例外 ise =
     新しい IllegalStateException(sm.getString("request.asyncNotSupported"));
   log.warn(sm.getString("coyoteRequest.noAsync",
     StringUtils.join(getNonAsyncClassNames()))、ise);
   投げる
  }

  (asyncContext == null)の場合{
   asyncContext = 新しい AsyncContextImpl(this);
  }

  asyncContext.setStarted(getContext(), リクエスト, レスポンス,
    リクエスト==getRequest() && レスポンス==getResponse().getResponse());
  asyncContext.setTimeout(getConnector().getAsyncTimeout());

  asyncContext を返します。
 }

req.startAsync();非同期コンテキストを保存し、 Timeoutなどの基本的な情報を設定するだけであることがわかりました。ちなみに、ここで設定されているデフォルトのタイムアウトは 30 秒です。つまり、非同期処理ロジックは 30 秒を超えるとエラーを報告することになります。このとき、 ctx.complete();を実行すると IllegalStateException がスローされます。

ctx.complete();のロジックを見てみましょう。

 パブリックボイドコンプリート(){
  ログがデバッグ有効の場合
   logDebug("完了 ");
  }
  チェック();
  リクエスト.getCoyoteRequest().action(ActionCode.ASYNC_COMPLETE, null);
 }
//クラス: AbstractProcessor 
 パブリックファイナルボイドアクション(アクションコードアクションコード、オブジェクトパラメータ) {
 ASYNC_COMPLETEの場合: {
   ディスパッチをクリアします。
   (asyncStateMachine.asyncComplete())の場合{
    SocketEvent を処理します (SocketEvent.OPEN_READ、true)。
   }
   壊す;
  } 
 }
 //クラス: AbstractProcessor 
保護されたvoid processSocketEvent(SocketEventイベント、ブールディスパッチ) {
  SocketWrapperBase<?> socketWrapper = getSocketWrapper();
  ソケットラッパーが null の場合
   socketWrapper.processSocket(イベント、ディスパッチ);
  }
 }
 //クラス: AbstractEndpoint
パブリックブール型プロセスソケット(SocketWrapperBase<S> socketWrapper,
   SocketEvent イベント、ブールディスパッチ){
  //一部のコードを省略 SocketProcessorBase<S> sc = null;
   プロセッサキャッシュが null の場合
    sc = プロセッサキャッシュ.pop();
   }
   (sc == null)の場合{
    sc = createSocketProcessor(socketWrapper、イベント);
   } それ以外 {
    sc.reset(socketWrapper、イベント);
   }
   実行者 executor = getExecutor();
   if (ディスパッチ && エグゼキュータ != null) {
    実行者.execute(sc);
   } それ以外 {
    sc.run();
   }
 
  true を返します。
 }

そのため、ここではAbstractEndpointprocessSocketメソッドが呼び出されます。前回のブログを読んだ方は、 EndPointを使ってリクエストを受け付けて処理し、その後Processorに渡してプロトコル処理が行われるという印象を持っているはずです。

クラス: AbstractProcessorLight
パブリック SocketState プロセス (SocketWrapperBase<?> socketWrapper、SocketEvent ステータス)
   IOException をスローします {
  //直径の一部を省略
  SocketState 状態 = SocketState.CLOSED;
  イテレータ<DispatchType> ディスパッチ = null;
  する {
   if (ディスパッチ!= null) {
    ディスパッチタイプ nextDispatch = dispatches.next();
    状態 = ディスパッチ(nextDispatch.getSocketStatus());
   } そうでない場合 (ステータス == SocketEvent.DISCONNECT) {
   
   } そうでない場合 (isAsync() || isUpgrade() || state == SocketState.ASYNC_END) {
    状態 = ディスパッチ(ステータス);
    状態 == SocketState.OPEN の場合 {
     状態 = サービス(socketWrapper);
    }
   } そうでない場合 (ステータス == SocketEvent.OPEN_WRITE) {
    状態 = SocketState.LONG;
   } それ以外の場合 (ステータス == SocketEvent.OPEN_READ) {
    状態 = サービス(socketWrapper);
   } それ以外 {
    状態 = SocketState.CLOSED;
   }

  } while (state == SocketState.ASYNC_END ||
    ディスパッチ != null && 状態 != SocketState.CLOSED);

  状態を返します。
 }

この部分がポイントです。AbstractProcessorLight AbstractProcessorLightSocketEventの状態に基づいて、 service(socketWrapper)を呼び出すかどうかを決定します。このメソッドは、最終的にコンテナを呼び出して、ビジネス ロジックの呼び出しを完了します。リクエストは実行が完了した後に呼び出され、コンテナに入ってはいけません。そうしないと、無限ループになります。ここでは、 isAsync()によって判断され、 dispatch(status)に入り、最終的にCoyoteAdapterasyncDispatchメソッドが呼び出されます。

パブリックブールasyncDispatch(org.apache.coyote.Request req, org.apache.coyote.Response res,
   SocketEventステータス)例外をスローします{
  //一部のコードを省略 Request request = (Request) req.getNote(ADAPTER_NOTES);
  レスポンス response = (レスポンス) res.getNote(ADAPTER_NOTES);
  ブール値の成功 = true;
  AsyncContextImpl は、リクエストの getAsyncContextInternal() を返します。
  試す {
   リクエストが非同期の場合
    レスポンスを一時停止に設定します(false);
   }

   (ステータス==SocketEvent.TIMEOUT)の場合{
    (!asyncConImpl.timeout())の場合{
     asyncConImpl.setErrorState(null、false);
    }
   } そうでない場合 (ステータス == SocketEvent.ERROR) {
    
   }

   リクエストが非同期ディスパッチされている場合、
    書き込みリスナー writeListener = res.getWriteListener();
    ReadListener の readListener = req.getReadListener();
    if (writeListener != null && status == SocketEvent.OPEN_WRITE) {
     クラスローダー oldCL = null;
     試す {
      oldCL = request.getContext().bind(false, null);
      res.onWritePossible(); //ここでブラウザのレスポンスを実行し、データを書き込みます if (request.isFinished() && req.sendAllDataReadEvent() &&
        readListener != null) {
       readListener.onAllDataRead();
      }
     } キャッチ (Throwable t) {
      
     ついに
      リクエスト.getContext().unbind(false, oldCL);
     }
    } 
    }
   }
   //ここでは非同期が進行中であると判断されます。つまり、これは完了メソッドのコールバックではなく、通常の非同期リクエストであり、コンテナは引き続き呼び出されます。
   リクエストが非同期にディスパッチされるかどうか
    コネクタ.getService().getContainer().getPipeline().getFirst().invoke()
      リクエスト、レスポンス);
    Throwable t = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
    t != nullの場合{
     asyncConImpl.setErrorState(t, true);
    }
   }
   //ここで、タイムアウトまたはエラーが発生した場合、request.isAsync() は false を返し、できるだけ早くクライアントにエラーを出力することに注意してください。
   リクエストが非同期の場合
    //これも出力ロジック request.finishRequest();
    レスポンスを終了します。
   }
   //リクエストとレスポンスを破棄する
   if (!成功 || !request.isAsync()) {
    updateWrapperErrorCount(リクエスト、レスポンス);
    リクエスト.recycle();
    レスポンスをリサイクルします。
   }
  }
  成功を返します。
 }

上記のコードは、 ctx.complete()実行される最終メソッドです (もちろん、多くの詳細は省略されています)。これにより、データの出力が完了し、最終的にブラウザに出力されます。

ここで、非同期実行が完了した後、 ctx.complete()を呼び出すとブラウザに出力されることはわかっているが、最初の doGet 要求が実行された後、Tomcat はクライアントに戻る必要がないことをどのようにして知るのか、と疑問に思う学生もいるかもしれません。キーコードはCoyoteAdapterserviceメソッドにあります。コードの一部は次のとおりです。

 postParseSuccess = postParseRequest(req、リクエスト、res、応答);
   //一部のコードを省略 if (postParseSuccess) {
    リクエスト.setAsyncSupported()
      コネクタ.getService().getContainer().getPipeline().isAsyncSupported());
    コネクタ.getService().getContainer().getPipeline().getFirst().invoke()
      リクエスト、レスポンス);
   }
   リクエストが非同期の場合
    非同期 = true;
    } それ以外 {
    // クライアントにデータを出力 request.finishRequest();
    レスポンスを終了します。
   非同期の場合
    updateWrapperErrorCount(リクエスト、レスポンス);
    //リクエストとレスポンスを破棄する
    リクエスト.recycle();
    レスポンスをリサイクルします。
   }

Servletを呼び出した後、コードのこの部分はrequest.isAsync()を使用して、非同期リクエストかどうかを判断します。非同期リクエストの場合は、 async = trueを設定します。非同期リクエストの場合は、クライアントにデータを出力するロジックを実行し、同時にrequestresponseを破棄します。これにより、リクエストが完了した後にクライアントに応答しない操作が完了します。

Spring Boot の @EnableAsync アノテーションが非同期サーブレットではない理由

この記事を書く準備をしていたときに多くの情報を検索したところ、SpringBoot 非同期プログラミングに関する多くの資料が@EnableAsyncアノテーションに依存し、次にControllerでマルチスレッドを使用してビジネス ロジックを完成させ、最後に結果を要約して戻り出力を完成させていることがわかりました。ここに、金採りの男による記事「初心者のための SpringBoot 非同期プログラミング ガイド」の例があります。この記事は非常にわかりやすく、非常に優れています。ビジネスの観点から見ると、確かに非同期プログラミングですが、問題があります。ビジネスの並列処理は別として、リクエスト全体に対して非同期ではありません。つまり、Tomcat スレッドをすぐに解放できないため、非同期サーブレットの効果が得られません。ここでも上記を参考にデモを書きましたが、なぜ非同期にならないのかを検証してみましょう。

@レストコントローラ
翻訳者
パブリッククラスTestController{
 オートワイヤード
 プライベート TestService サービス。

 @GetMapping("/hello")
 パブリック文字列テスト(){
  試す {
   log.info("testAsynch 開始");
   完了可能なFuture<String> test1 = service.test1();
   完了可能なFuture<String> test2 = service.test2();
   完了可能なFuture<String> test3 = service.test3();
   テスト1、テスト2、テスト3の完了可能なFuture。
   ログ情報("test1=====" + test1.get());
   ログ情報("test2=====" + test2.get());
   ログ情報("test3=====" + test3.get());
  } キャッチ (InterruptedException e) {
   e.printStackTrace();
  } キャッチ (ExecutionException e) {
   e.printStackTrace();
  }
  「hello」を返します。
 }
@サービス
パブリッククラスTestService{
 @Async("asyncExecutor")
 パブリックCompletableFuture<String> test1()はInterruptedExceptionをスローします{
  スレッドスリープ(3000L);
  CompletableFuture.completedFuture("test1") を返します。
 }

 @Async("asyncExecutor")
 パブリックCompletableFuture<String> test2()はInterruptedExceptionをスローします{
  スレッドスリープ(3000L);
  CompletableFuture.completedFuture("test2") を返します。
 }

 @Async("asyncExecutor")
 パブリックCompletableFuture<String> test3()はInterruptedExceptionをスローします{
  スレッドスリープ(3000L);
  CompletableFuture.completedFuture("test3") を返します。
 }
}
@SpringBootアプリケーション
@非同期を有効にする
パブリッククラスTomcatdebugApplication {

 パブリック静的voidメイン(String[] args) {
  TomcatdebugApplication.class を SpringApplication.run します。
 }

 @Bean(名前 = "asyncExecutor")
 パブリックエグゼキュータ asyncExecutor() {
  ThreadPoolTask​​Executor 実行プログラム = 新しい ThreadPoolTask​​Executor();
  executor.setCorePoolSize(3);
  実行者.setMaxPoolSize(3);
  実行者.setQueueCapacity(100);
  executor.setThreadNamePrefix("AsynchThread-");
  実行者を初期化します。
  実行者を返す。
 }

ここで実行して効果を確認します

ここでは、リクエスト後、コンテナを呼び出してビジネスロジックを実行する前にブレークポイントを設定し、戻った後にブレークポイントを設定しています。 Controllerの実行後、リクエストはCoyoteAdapterに戻り、 request.isAsync()が判定されます。図によると、 falseあるため、 request.finishRequest()response.finishResponse()実行され、レスポンスの終了が実行され、リクエストとレスポンスの本体が破棄されます。興味深いのは、実験していたときに、 request.isAsync()を実行する前に、レスポンス本文がすでにブラウザ ページに表示されていたことです。これは、SpringBoot フレームワークがStringHttpMessageConverterクラスのwriteInternalメソッドを通じてすでにレスポンス本文を出力しているためです。

上記の分析の核となるロジックは、Tomcat のスレッドがCoyoteAdapterを実行してコンテナを呼び出した後、リクエストが返されるまで待機し、非同期リクエストであるかどうかを判断し、リクエストを処理する必要があるということです。実行が完了すると、スレッドをリサイクルできます。私の最初の非同期サーブレットの例では、 doGet メソッドを実行した後、すぐに戻ります。つまり、 request.isAsync()のロジックに直接進み、その後、スレッド全体のロジックが実行され、スレッドがリサイクルされます。

非同期サーブレットの使用シナリオについてお話ししましょう

ここまで分析した結果、非同期サーブレットの使用シナリオはどのようなものになるでしょうか?実際には、非同期サーブレットによってシステムのスループットが向上し、より多くのリクエストを受け入れることができるという 1 つのポイントを把握するだけで分析できます。 Web システム内の Tomcat のスレッド数が足りず、大量のリクエストが待機しているとします。このとき、Web システムのアプリケーション レベルでの最適化、つまりビジネス ロジックの応答時間を短縮できなくなります。このとき、ユーザーの待機時間を短縮してスループットを上げたい場合は、非同期サーブレットの使用を試してみるとよいでしょう。

実際の例を見てみましょう。たとえば、テキスト メッセージ システムを作成する場合、テキスト メッセージ システムはリアルタイム パフォーマンスに対する要件が非常に高いため、待機時間をできるだけ短くする必要があり、送信機能は実際にはオペレーターに送信を委託します。つまり、インターフェイスを呼び出す必要があります。同時実行性が非常に高いと仮定すると、この時点でビジネス システムがテキスト メッセージ送信機能を呼び出し、Tomcat スレッド プールが使い果たされ、残りの要求がキューで待機する可能性があります。このとき、テキスト メッセージの遅延が増加します。この問題を解決するには、非同期サーブレットを導入して、より多くのテキスト メッセージ送信要求を受け入れ、テキスト メッセージの遅延を減らすことができます。

要約する

この記事では、まず手動で非同期サーブレットを作成し、非同期サーブレットの役割と、Tomcat 内で非同期サーブレットがどのように実装されているかを分析しました。次に、インターネットで人気の SpringBoot 非同期プログラミングに基づいて説明しました。これは Tomcat 内の非同期サーブレットではありません。最後に、非同期サーブレットの使用シナリオについて説明し、非同期サーブレットを試すことができる状況を分析しました。

以上がこの記事の全内容です。皆様の勉強のお役に立てれば幸いです。また、123WORDPRESS.COM を応援していただければ幸いです。

以下もご興味があるかもしれません:
  • IDEA2021 tomcat10 サーブレットの新しいバージョンの落とし穴
  • Tomcat でのサーブレットの作成と実装に関する深い理解
  • サーブレットとTomcat_PowerNode Javaアカデミー
  • Tomcat で非同期サーブレットを実装する方法
  • Tomcat のサーブレット オブジェクト プールの紹介と使用
  • Tomcat におけるサーブレットの動作メカニズムの詳細な紹介
  • ソースコード分析からTomcatがサーブレットの初期化を呼び出す方法の詳細な説明

<<:  MySQL のジオメトリ型を使用して経度と緯度の距離の問題を処理する方法

>>:  Vueドロップダウンリストの2つの実装方法の比較

推薦する

Centos7 のインストールと Mysql5.7 の設定

ステップ1: MySQL YUMソースを取得するMySQLの公式サイトにアクセスして、RPMパッケー...

Javascriptでシンプルなナビゲーションバーを実装

この記事では、参考までに、シンプルなナビゲーションバーを実装するためのJavascriptの具体的な...

VMware 仮想マシンで HTTP サービスを確立して分析する手順

1. xshell を使用して仮想マシンに接続するか、仮想マシンに直接コマンドを入力します。以下はx...

Ubuntuデュアルシステムが起動時に停止する問題の解決方法の詳細な説明

起動時に Ubuntu デュアル システムが停止する問題の解決方法 (Ubuntu 16.04 およ...

Vue が値を返してフォームを動的に生成し、データを送信する仕組みの詳細な説明

目次解決された主な問題1. バックエンドから返され、バックエンドに送信されるデータは、次の形式になり...

ウォーターフォールフローレイアウト(無限読み込み)を実現する js

この記事の例では、ウォーターフォールフローレイアウトを実装するためのjsの具体的なコードを参考までに...

CSS属性のデフォルト値width: autoとwidth: 100%の違いの詳細な説明

幅: 自動子要素(コンテンツ+パディング+境界線+余白を含む)は、親要素のコンテンツ領域全体を埋めま...

Linux に MySQL をインストールする方法 (yum とソース コードのコンパイル)

Linux に MySQL をインストールするには、yum インストールとソース コード コンパイ...

HTML タグ dl dt dd 使用方法

基本構造:コードをコピーコードは次のとおりです。 <ダウンロード> <dt>...

jQueryは画像追従効果を実現します

この記事では、画像フォロー効果を実現するためのjQueryの具体的なコードを参考までに紹介します。具...

Vue はアップロードされた画像に透かしを追加する機能を実装します

この記事では、Vueでアップロードされた画像に透かしを追加する具体的な実装コードを参考までに共有しま...

Docker を使用した Laravel アプリケーションのデプロイ例

この記事で使用されているPHPベースイメージはphp:7.3-apacheです。この記事の Lara...

Ubuntu 19でdockerソースをインストールできない問題を共有する

主要な Web サイトと個人的な習慣に従って、Docker ソースを追加するには次の方法を使用します...

VUE でタブページを切り替える 4 つの方法

目次1. 静的実装方法: 2. 第2のシミュレーション動的方法3. 3番目の動的データ方式4. 動的...

JS WebSocketを使用して簡単なチャットを実装する方法

目次ショートポーリングロングポーリングウェブソケットコミュニケーションの原則シンプルな1対1チャット...