Betrachten des Threadmodells von Tomcat aus der Connector-Komponente – BIO-Modus (empfohlen)

Betrachten des Threadmodells von Tomcat aus der Connector-Komponente – BIO-Modus (empfohlen)

In höheren Versionen von Tomcat ist der Standardmodus die Verwendung des NIO-Modus. In Tomcat 9 wurde die Implementierung des BIO-Modus Http11Protocol sogar gelöscht. Aber das Verständnis der Funktionsweise von BIO und seiner Vor- und Nachteile ist hilfreich für das Erlernen anderer Modi. Erst durch einen Vergleich können Sie die Vorteile anderer Modelle erkennen.

Http11Protocol stellt die blockierende HTTP-Protokollkommunikation dar, die den gesamten Prozess des Empfangens, Verarbeitens und Antwortens an den Client über die Socket-Verbindung umfasst. Es enthält hauptsächlich die JIoEndpoint-Komponente und die Http11Processor-Komponente. Beim Start beginnt die JIoEndpoint-Komponente, auf einem bestimmten Port zu lauschen. Wenn eine Anforderung eintrifft, wird sie in den Threadpool geworfen, der die Aufgabe verarbeitet. Während der Verarbeitung wird das HTTP-Protokoll von der Protokollparserkomponente Http11Processor analysiert und über den Adapter dem angegebenen Container zur Verarbeitung und Antwort an den Client zugeordnet.

Hier kombinieren wir den in Spring Boot eingebetteten Tomcat, um zu sehen, wie der Connector funktioniert. Es wird empfohlen, eine niedrigere Version von Spring Boot zu verwenden. In höheren Versionen von Spring Boot wird bereits Tomcat 9 verwendet. Tomcat 9 hat die BIO-Implementierung entfernt. Die Spring Boot-Version, die ich hier gewählt habe, ist 2.0.0.RELEASE.

So zeigen Sie den Quellcode der Connector-Komponente an

Wir beginnen nun mit der Analyse des Arbeitsvorgangs der Connector-Komponente anhand des Quellcodes der Connector-Komponente. Aber es gibt so viel Quellcode für Tomcat, wie sollen wir ihn lesen? Der vorherige Artikel fasst den Tomcat-Startvorgang zusammen, wie in der folgenden Abbildung dargestellt:

Das obige Sequenzdiagramm liefert uns Anregungen zur Analyse des Quellcodes der Connector-Komponente: Beginnen Sie mit der Init-Methode und der Start-Methode der Connector-Komponente.

Diagramm der Arbeitssequenz für die Verbindungskomponenten

Der in Spring Boot eingebettete Tomcat verwendet standardmäßig den NIO-Modus. Wenn Sie den BIO-Modus studieren möchten, müssen Sie selbst daran arbeiten. Spring Boot bietet die Schnittstelle WebServerFactoryCustomizer , die wir implementieren können, um die Servlet-Container-Factory anzupassen. Nachfolgend sehen Sie eine Konfigurationsklasse, die ich selbst implementiert habe. Sie setzt das IO-Modell einfach in den BIO-Modus. Wenn Sie andere Konfigurationen vornehmen müssen, können Sie darin auch zusätzliche Konfigurationen vornehmen.

@Konfiguration
öffentliche Klasse TomcatConfig {

 @Bohne
 öffentlicher WebServerFactoryCustomizer tomcatCustomizer() {
  gibt neue TomcatCustomerConfig() zurück;
 }

 öffentliche Klasse TomcatCustomerConfig implementiert WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
  @Überschreiben
  öffentliche Leere anpassen (TomcatServletWebServerFactory Fabrik) {
   wenn (Fabrik != null) {
    factory.setProtocol("org.apache.coyote.http11.Http11Protocol");
   }
  }
 }
}

Nach der obigen Konfiguration verarbeitet die Connector-Komponente von Tomcat Anfragen im BIO-Modus.

Da Tomcat über eine Menge Code verfügt, ist es nicht realistisch, den gesamten Code in einem Artikel zu analysieren. Hier habe ich das Zeitdiagramm der Verbindungskomponente sortiert. Basierend auf diesem Zeitdiagramm habe ich mehrere wichtige Codepunkte analysiert. Weitere Einzelheiten können Sie sich den Code anhand meines Zeitdiagramms selbst ansehen. Dieser Code ist nicht sehr kompliziert.

Der Schlüsselcode befindet sich hier in der init()-Methode und der start()-Methode von JIoEndpoint. Die init()-Methode von JIoEndpoint führt hauptsächlich die Portbindung von ServerSocket durch. Der spezifische Code lautet wie folgt:

@Überschreiben
public void bind() wirft Exception {

 // Thread-Anzahl-Standardwerte für Akzeptor initialisieren
 wenn (acceptorThreadCount == 0) {
  AkzeptorThreadCount = 1;
 }
 //maxConnections initialisieren
 wenn (getMaxConnections() == 0) {
  // Der Benutzer hat keinen Wert festgelegt - verwenden Sie den Standardwert
  : SetzeMaxConnections(getMaxThreadsWithExecutor());
 }

 wenn (serverSocketFactory == null) {
  wenn (istSSLEnabled()) {
   serverSocketFactory =
    handler.getSslImplementation().getServerSocketFactory(diese);
  } anders {
   serverSocketFactory = neue DefaultServerSocketFactory(diese);
  }
 }
 //Hier ist die Portbindung für ServerSocket if (serverSocket == null) {
  versuchen {
   wenn (getAddress() == null) {
    //Wenn keine bestimmte Adresse angegeben ist, wartet Tomcat auf Anfragen von allen Adressen serverSocket = serverSocketFactory.createSocket(getPort(),
                getBacklog());
   } anders {
    //Geben Sie die spezifische Adresse an, Tomcat wartet nur auf Anfragen von dieser Adresse serverSocket = serverSocketFactory.createSocket(getPort(),
                getBacklog(), getAddress());
   }
  } Fang (BindException orig) {
   Zeichenfolgennachricht;
   wenn (getAddress() == null)
    msg = orig.getMessage() + " <null>:" + getPort();
   anders
    msg = orig.getMessage() + " " +
    getAddress().toString() + ":" + getPort();
   BindException sein = neue BindException(msg);
   sein.initCause(orig);
   werfen sein;
  }
 }

}

Schauen wir uns die Startmethode von JIoEndpoint an.

public void startInternal() löst Exception aus {

 wenn (!läuft) {
  läuft = wahr;
  pausiert = falsch;

  //Thread-Pool erstellen if (getExecutor() == null) {
   Executor erstellen();
  }
  //ConnectionLatch erstellen
  initializeConnectionLatch();
  //Erstellen Sie den Accept-Thread, der der erste Thread für die Anforderungsverarbeitung ist. startAcceptorThreads();
  // Asynchronen Timeout-Thread starten
  Thread-TimeoutThread = neuer Thread (neues AsyncTimeout (),
           getName() + "-AsyncTimeout");
  : TimeoutThread.setPriority(threadPriority);
  setDaemon(true);
  timeoutThread.start();
 }
}

Im obigen Code müssen wir uns auf die Methode startAcceptorThreads() konzentrieren. Schauen wir uns die konkrete Implementierung dieses Accept-Threads an.

geschützt final void startAcceptorThreads() {
 Int Anzahl = getAcceptorThreadCount();
 Akzeptoren = neuer Akzeptor[Anzahl];
 //Lege gemäß der Konfiguration eine bestimmte Anzahl von Accept-Threads fest für (int i = 0; i < count; i++) {
  Akzeptoren[i] = erstelleAkzeptor();
  Zeichenfolge threadName = getName() + "-Acceptor-" + i;
  Akzeptoren[i].setThreadName(ThreadName);
  Thread t = neuer Thread (Akzeptoren[i], Threadname);
  t.setPriority(getAcceptorThreadPriority());
  t.setDaemon(getDaemon());
  t.start();
 }
}

Konzentrieren Sie sich bei der spezifischen Verarbeitungsimplementierung des Acceptor-Threads auf die Run-Methode.

geschützte Klasse Acceptor erweitert AbstractEndpoint.Acceptor {

  @Überschreiben
  öffentliche Leere ausführen() {

   int Fehlerverzögerung = 0;
   //Schleife, bis wir einen Shutdown-Befehl erhalten
   während (läuft) {
    // Schleife, wenn der Endpunkt angehalten ist
    während (pausiert und ausgeführt) {
     Zustand = Akzeptorzustand.PAUSED;
     versuchen {
      Thread.sleep(50);
     } Fang (UnterbrocheneAusnahme e) {
      // Ignorieren
     }
    }

    wenn (!läuft) {
     brechen;
    }
    Zustand = Akzeptorzustand.LÄUFT;

    versuchen {
     //wenn wir die maximale Anzahl an Verbindungen erreicht haben, warten
     //Wenn das Verbindungslimit erreicht ist, wechselt der Akzeptor-Thread in einen Wartezustand, bis andere Threads freigegeben werden. Dies ist eine einfache Möglichkeit zur Flusssteuerung anhand der Anzahl der Verbindungen. //Dies wird durch die Implementierung der AQS-Komponente (LimitLatch) erreicht. Die Idee besteht darin, zuerst das maximale Limit des Synchronizers zu initialisieren, dann die Zählvariable für jeden empfangenen Socket um 1 zu erhöhen und die Zählvariable für jeden geschlossenen Socket um 1 zu verringern.
     zähleUpOderWarteVerbindung();
     Socket-Socket = null;
     versuchen {
      //Nächste Socket-Verbindung akzeptieren. Wenn keine Verbindung hergestellt wird, wird diese Methode blockiert. socket = serverSocketFactory.acceptSocket(serverSocket);
     } Fang (IOException ioe) {
      //Wenn eine Ausnahme auftritt, geben Sie eine Verbindung frei countDownConnection();
      Fehlerverzögerung = handleExceptionWithDelay(Fehlerverzögerung);
      wirf ioe;
     }
     // Erfolgreiches Akzeptieren, Fehlerverzögerung zurücksetzen
     Fehlerverzögerung = 0;
     //Konfiguriere den Socket entsprechend if (running && !paused && setSocketOptions(socket)) {
      // Diese Socket-Anforderung verarbeiten. Das ist auch der entscheidende Punkt.
      wenn (!processSocket(socket)) {
       countDownConnection();
       // Socket sofort schließen
       Schließen Sie Socket(Socket);
      }
     } anders {
      countDownConnection();
      // Socket sofort schließen
      Schließen Sie Socket(Socket);
     }
    } Fang (IOException x) {
     wenn (läuft) {
      log.error(sm.getString("endpoint.accept.fail"), x);
     }
    } Fang (NullPointerException npe) {
     wenn (läuft) {
      log.error(sm.getString("endpoint.accept.fail"), npe);
     }
    } fangen (Wurfbares t) {
     ExceptionUtils.handleThrowable(t);
     log.error(sm.getString("endpoint.accept.fail"), t);
    }
   }
   Zustand = Akzeptorzustand.ENDED;
  }
 }

Der processSocket(socket) in der obigen Thread-Verarbeitungsklasse ist die Methode zum Verarbeiten bestimmter Anforderungen. Diese Methode verpackt die Anforderung und „wirft“ sie zur Verarbeitung in den Thread-Pool. Dies ist jedoch nicht der Schwerpunkt der Connector-Komponente. Später werden wir bei der Einführung des Anforderungsflusses vorstellen, wie Tomcat Anforderungen verarbeitet.

Hier haben wir kurz den BIO-Modus von Tomcat vorgestellt. Tatsächlich können Sie erkennen, dass der vereinfachte BIO-Modus dem Betrieb des herkömmlichen ServerSocket entspricht und die Verarbeitung von Anforderungen mithilfe des Thread-Pools optimiert wird.

BIO-Modus-Zusammenfassung

Nachfolgend finden Sie eine kurze Beschreibung der einzelnen Komponenten in der obigen Abbildung.

Strombegrenzende Komponente LimitLatch

Die LimitLatch-Komponente ist eine Flusssteuerungskomponente, die verhindern soll, dass die Tomcat-Komponente durch große Flüsse überlastet wird. LimitLatch wird über den AQS-Mechanismus implementiert. Wenn diese Komponente gestartet wird, initialisiert sie zuerst den maximalen Grenzwert des Synchronizers, erhöht dann die Zählvariable um 1 für jeden empfangenen Socket und verringert die Zählvariable um 1 für jeden geschlossenen Socket. Wenn die Anzahl der Verbindungen den Maximalwert erreicht, wechselt der Acceptor-Thread in einen Wartezustand und akzeptiert keine neuen Socket-Verbindungen mehr.

Es ist zu beachten, dass, wenn die maximale Anzahl an Verbindungen erreicht ist (die LimitLatch-Komponente hat ihren Maximalwert erreicht und die Akzeptor-Komponente ist blockiert), das zugrunde liegende Betriebssystem weiterhin Client-Verbindungen empfängt und die Anfragen in eine Warteschlange (Backlog-Warteschlange) einreiht. Diese Warteschlange hat eine Standardlänge, der Standardwert ist 100. Natürlich kann dieser Wert über das acceptCount-Attribut des Connector-Knotens in server.xml konfiguriert werden. Wenn innerhalb kurzer Zeit eine große Anzahl von Anfragen eingeht und die Backlog-Warteschlange voll ist, verweigert das Betriebssystem die Annahme nachfolgender Verbindungen und gibt „Verbindung abgelehnt“ zurück.

Im BIO-Modus wird die maximale Anzahl von Verbindungen, die von der LimitLatch-Komponente unterstützt werden, über das Attribut maxConnections des Connector-Knotens in server.xml festgelegt. Wenn es auf -1 gesetzt ist, bedeutet dies, dass keine Begrenzung besteht.

Akzeptor

Die Verantwortung dieser Komponente ist sehr einfach: Sie besteht darin, die Socket-Verbindung zu empfangen, entsprechende Einstellungen für den Socket vorzunehmen und ihn dann zur Verarbeitung direkt an den Thread-Pool weiterzuleiten. Auch die Anzahl der Accept-Threads lässt sich konfigurieren.

Socket-Fabrik ServerSocketFactory

Der Acceptor-Thread wird beim Akzeptieren einer Socket-Verbindung durch die ServerSocketFactory-Komponente abgerufen. Es gibt zwei ServerSocketFactory-Implementierungen in Tomcat: DefaultServerSocketFactory und JSSESocketFactory. Entsprechend den Fällen von HTTP und HTTPS.

In Tomcat gibt es eine Variable SSLEnabled, mit der angegeben wird, ob ein verschlüsselter Kanal verwendet werden soll. Durch die Definition dieser Variable können Sie entscheiden, welche Factory-Klasse verwendet werden soll. Tomcat stellt eine externe Konfigurationsdatei bereit, die Benutzer anpassen können. In der folgenden Konfiguration bedeutet SSLEnabled="true", dass Verschlüsselung verwendet wird, d. h., dass JSSESocketFactory verwendet wird, um bestimmte Socket-Verbindungen zu akzeptieren.

<Connector-Port="8443" Protokoll="org.apache.coyote.http11.Http11NioProtocol"
   maxThreads="150" SSLEnabled="true">
 <SSLHostConfig>
  <Zertifikat certificateKeystoreFile="conf/localhost-rsa.jks"
      Typ="RSA" />
 </SSLHostConfig>
</Anschluss>

Thread-Pool-Komponente

Der Thread-Pool in Tomcat ist eine einfache Modifikation des Thread-Pools in JDK. Es gibt einen kleinen Unterschied bei der Strategie zur Thread-Erstellung: Der Thread-Pool in Tomcat sendet Threads nicht sofort an die Warteschlange, wenn die Anzahl der Threads größer als coreSize ist, sondern ermittelt zunächst, ob die Anzahl der aktiven Threads maxSize erreicht hat, und sendet Threads erst an die Warteschlange, wenn maxSize erreicht ist.

Der Executor der Connector-Komponente ist in zwei Typen unterteilt: gemeinsam genutzter Executor und privater Executor. Ein gemeinsam genutzter Executor ist der in der Servicekomponente definierte Executor.

Aufgabendefinierer SocketProcessor

Bevor wir den Socket in den Thread-Pool werfen, müssen wir definieren, wie die Aufgabe mit dem Socket umgeht. SocketProcessor ist die Aufgabendefinition und diese Klasse implementiert die Runnable-Schnittstelle.

geschützte Klasse SocketProcessor implementiert Runnable {
 //Beim Debuggen können Sie mit dem Debuggen über die Run-Methode dieser Klasse beginnen @Override
 öffentliche Leere ausführen() { 
 	//Socket verarbeiten und Antwort ausgeben //Verbindungsbegrenzer LimitLatch um eins verringern //Socket schließen}
}

Die Aufgaben von SocketProcessor gliedern sich hauptsächlich in drei Teile: Sockets verarbeiten und auf Clients reagieren, Verbindungszähler um 1 reduzieren und Sockets schließen. Unter ihnen ist die Verarbeitung von Sockets die wichtigste und komplexeste. Sie umfasst das Lesen des zugrunde liegenden Socket-Bytestreams, das Parsen der HTTP-Protokollanforderungsnachricht (Parsen von Anforderungszeile, Anforderungsheader, Anforderungstext und anderen Informationen), das Suchen der Webprojektressourcen auf dem entsprechenden virtuellen Host gemäß dem durch das Parsen der Anforderungszeile erhaltenen Pfad und das Zusammenstellen der HTTP-Protokollantwortnachricht gemäß den Verarbeitungsergebnissen und deren Ausgabe an den Client.

Wir werden den spezifischen Verarbeitungsablauf des Sockets hier vorerst nicht analysieren, da es in diesem Artikel hauptsächlich um das Thread-Modell des Connectors geht, das zu viele Dinge beinhaltet und leicht zu verwirren ist. Später wird ein Artikel geschrieben, der die spezifische Verarbeitung des Sockets durch Tomcat analysiert.

Zusammenfassen

Damit ist dieser Artikel über das Thread-Modell von Tomcat aus der Perspektive der Verbindungskomponenten – das BIO-Modell – abgeschlossen. Weitere Informationen zum Thread-Modell von Tomcat finden Sie in den vorherigen Artikeln von 123WORDPRESS.COM oder in den folgenden verwandten Artikeln. Ich hoffe, Sie werden 123WORDPRESS.COM auch in Zukunft unterstützen!

Das könnte Sie auch interessieren:
  • Tomcat-Quellcodeanalyse und -Verarbeitung
  • Tomcat verwendet Thread-Pool zur Verarbeitung gleichzeitiger Remote-Anfragen
  • Detaillierte Erläuterung des Thread-Modells von Tomcat zur Verarbeitung von Anforderungen

<<:  Drei häufig verwendete MySQL-Datentypen

>>:  Ein nützliches mobiles Scrolling-Plugin BetterScroll

Artikel empfehlen

css3-Animation, Ballrollen, js-Steuerung, Animationspause

Mit CSS3 können Animationen erstellt werden, die ...

So ändern Sie das Terminal in Ubuntu 18 in eine schöne Eingabeaufforderung

Ich habe VMware und Ubuntu neu installiert, aber ...

Die „3I“-Standards für erfolgreiche Printwerbung

Für viele inländische Werbetreibende ist die Erste...

So erstellen Sie schnell einen FTP-Dateidienst mit FileZilla

Um die Speicherung und den Zugriff auf Dateien zu...

Docker-Installations- und Konfigurationsschritte für RabbitMQ

Inhaltsverzeichnis Bereitstellung auf einem einze...

MySQL verwendet frm-Dateien und ibd-Dateien, um Tabellendaten wiederherzustellen

Inhaltsverzeichnis Einführung in FRM-Dateien und ...

Natives JS zur Realisierung eines einfachen Schlangenspiels

In diesem Artikel wird der spezifische Code von j...

HTML-Formular_PowerNode Java Academy

1. Formular 1. Die Rolle des Formulars HTML-Formu...

Detaillierte Erklärung der Docker-Maschinennutzung

Docker-Machine ist ein offiziell von Docker berei...

Wichtige Updates für MySQL 8.0.23 (neue Funktionen)

Autor: Guan Changlong ist DBA in der Delivery Ser...

So installieren Sie MySQL 5.7 manuell auf CentOS 7.4

MySQL-Datenbanken werden häufig verwendet, insbes...