Vorwort Seit wir Node.js kennen, besteht unser Wissen darüber aus diesen Schlüsselwörtern: ereignisgesteuert, nicht blockierende E/A, effizient und leichtgewichtig. So beschreibt es sich selbst auf seiner offiziellen Website. Wenn wir also zum ersten Mal mit Nodejs in Kontakt kommen, werden wir einige Fragen haben: 1. Warum kann im Browser ausgeführtes Javascript auf einer so niedrigen Ebene mit dem Betriebssystem interagieren? Fühlen Sie sich nach dem Lesen dieser Fragen überfordert? Keine Sorge, lesen wir diesen Artikel langsam und behalten dabei diese Fragen im Hinterkopf. Architektur auf einen BlickDie obigen Fragen sind alle sehr grundlegend, also beginnen wir mit Node.js selbst und werfen einen Blick auf die Struktur von Node.js. Die in Javascript geschriebene Standardbibliothek Node.js ist die API, die bei unserer Nutzung direkt aufgerufen werden kann. Sie können es im Lib-Verzeichnis im Quellcode sehen. Knotenbindungen, diese Schicht ist der Schlüssel zur Kommunikation zwischen Javascript und dem zugrunde liegenden C/C++. Ersteres ruft Letzteres über Bindungen auf, um Daten miteinander auszutauschen. Implementiert in node.cc Diese Schicht ist der Schlüssel zur Unterstützung des Betriebs von Node.js und ist in C/C++ implementiert. Interaktion mit dem BetriebssystemWenn wir beispielsweise eine Datei öffnen und einige Vorgänge ausführen möchten, können wir den folgenden Code schreiben: var fs = require('fs');fs.open('./test.txt', "w", function(err, fd) { //..etwas tun}); Der Aufrufvorgang dieses Codes kann grob wie folgt beschrieben werden: lib/fs.js → src/node_file.cc → uv_fs lib/fs.js asynchrone Funktion öffnen (Pfad, Flags, Modus) { Modus = Modusnummer (Modus, 0o666); Pfad = getPathFromURL (Pfad); validatePath(Pfad); validateUint32(Modus, 'Modus'); returniere neuen FileHandle( warte auf binding.openFileHandle(pathModule.toNamespacedPath(path), stringToFlags(flags), Modus, kUsePromises)); } src/node_file.cc statisches void Öffnen (const FunctionCallbackInfo & args) { Umgebung * env = Umgebung::GetCurrent (args); const int argc = args.Length (); wenn (req_wrap_async ! = nullptr) { // öffnen (Pfad, Flags, Modus, req) AsyncCall (env, req_wrap_async, args, "öffnen", UTF8, AfterInteger, uv_fs_open, *Pfad, Flags, Modus); } sonst { // öffnen(Pfad, Flags, Modus, undefiniert, ctx) CHECK_EQ(argc, 5); FSReqWrapSync req_wrap_sync; FS_SYNC_TRACE_BEGIN(öffnen); int Ergebnis = SyncCall(Umgebung, Argumente[4], &req_wrap_sync, "öffnen", uv_fs_open, *Pfad, Flags, Modus); FS_SYNC_TRACE_END(öffnen); args.GetReturnValue().Set(Ergebnis); } } uv_fs /* Öffnen Sie die Zieldatei. */ dstfd = uv_fs_open(NULL, &fs_req, req->neuer_Pfad, Zielflaggen, statsbuf.st_mode, NULL); uv_fs_req_cleanup(&fs_req); Ein Bild von Node.js in einfachen Worten: Insbesondere wenn wir fs.open aufrufen, ruft Node.js die Open-Funktion auf C/C++-Ebene über process.binding auf und ruft dann darüber die spezifische Methode uv_fs_open in Libuv auf. Schließlich wird das Ausführungsergebnis über einen Rückruf zurückgegeben, um den Vorgang abzuschließen. Die Methoden, die wir in Javascript aufrufen, werden letztendlich über process.binding an die C/C++-Ebene übergeben und führen dort letztendlich die eigentlichen Operationen aus. So interagiert Node.js mit dem Betriebssystem. Einzelner Thread In herkömmlichen Webdienstmodellen wird Multithreading hauptsächlich zur Lösung von Parallelitätsproblemen verwendet. Da E/A blockiert ist, bedeutet ein einzelner Thread, dass Benutzer warten müssen, was offensichtlich unvernünftig ist. Daher werden mehrere Threads erstellt, um auf Benutzeranforderungen zu reagieren. Der einzelne Thread von Node.js bedeutet, dass der Hauptthread ein „einzelner Thread“ ist und der Hauptthread den Programmcode Schritt für Schritt gemäß der Codierungsreihenfolge ausführt. Wenn der synchrone Code blockiert ist und der Hauptthread belegt ist, bleibt die nachfolgende Ausführung des Programmcodes hängen. Üben Sie einen Testcode: var http = erfordern ('http'); Funktion sleep (Zeit) { var _exit = Date.now() + Zeit * 1000; während (Date.now() < _exit) {} return; }var server = http.createServer(Funktion(req, res){ Schlaf (10); res.end('Server schläft 10 Sek.'); }); server.listen(8080); Hier ist ein Stapeldiagramm des Codeblocks: Ändern Sie zuerst den Code von index.js entsprechend, öffnen Sie dann den Browser und Sie werden feststellen, dass der Browser nach 10 Sekunden antwortet und „Hello Node.js“ eingibt. JavaScript ist eine interpretierte Sprache. Code wird Zeile für Zeile in den Stapel geschoben, und zwar in der Reihenfolge, in der er codiert und ausgeführt wird. Nach Abschluss der Ausführung wird der Code entfernt und die nächste Codezeile zur Ausführung eingefügt. Im Stapeldiagramm des obigen Codeblocks wird das Programm, wenn der Hauptthread die Anforderung akzeptiert, zur synchronen Ausführung in den Ruheausführungsblock verschoben (wir gehen davon aus, dass dies die Geschäftsverarbeitung des Programms ist). Wenn innerhalb von 10 Sekunden eine zweite Anforderung eingeht, wird sie in den Stapel verschoben und wartet 10 Sekunden, bis sie abgeschlossen ist, bevor die nächste Anforderung weiter verarbeitet wird. Nachfolgende Anforderungen werden angehalten und warten, bis die vorherige synchrone Ausführung abgeschlossen ist, bevor sie ausgeführt werden. Dann fragen wir uns vielleicht: Warum kann ein einzelner Thread so effizient sein und Zehntausende von Prozessen gleichzeitig verarbeiten, ohne Blockaden zu verursachen? Dies ist das, was wir im Folgenden als ereignisgesteuert bezeichnen. Ereignisgesteuert/EreignisschleifeEine Ereignisschleife ist eine Programmierkonstruktion, die auf Ereignisse oder Nachrichten in einem Programm wartet und diese versendet. 1. Jeder Node.js-Prozess hat nur einen Hauptthread, der Programmcode ausführt und einen Ausführungskontextstapel bildet. Der einzelne Thread von node.js, den wir sehen, ist nur ein js-Hauptthread. Der asynchrone Vorgang wird im Wesentlichen vom Threadpool ausgeführt. Node übergibt alle blockierenden Vorgänge zur Implementierung an den internen Threadpool. Er ist nur für die kontinuierliche Roundtrip-Planung verantwortlich und führt keine echten E/A-Vorgänge aus, wodurch asynchrone, nicht blockierende E/A realisiert wird. Dies ist die Essenz des einzelnen und ereignisgesteuerten Node-Threads. Implementierung der Ereignisschleife in Node.js:Node.js verwendet V8 als Parsing-Engine von js und verwendet seine eigene libuv für die E/A-Verarbeitung. Libuv ist eine ereignisgesteuerte plattformübergreifende Abstraktionsschicht, die einige zugrunde liegende Funktionen verschiedener Betriebssysteme kapselt und der Außenwelt eine einheitliche API bereitstellt. Der Ereignisschleifenmechanismus ist ebenfalls darin implementiert. In src/node.cc: Umgebung*UmgebungErstellen(DatenIsolieren*DatenIsolieren, Lokaler Kontext, int argc, const char* const* argv, int exec_argc, const char* const* exec_argv) { Isolieren* isolieren = Kontext->GetIsolate(); HandleScope handle_scope(isolieren); Kontext::Bereich context_scope(Kontext); auto env = neue Umgebung(isolate_data, Kontext, v8_platform.GetTracingAgent()); env->Start(argc, argv, exec_argc, exec_argv, v8_is_profiling); gibt Umgebung zurück; } Dieser Code erstellt eine Knotenausführungsumgebung. In der dritten Zeile sehen Sie uv_default_loop(), eine Funktion in der libuv-Bibliothek. Sie initialisiert die uv-Bibliothek selbst und die darin enthaltene default_loop_struct und gibt einen Zeiger darauf zurück, default_loop_ptr. Danach lädt Node die Ausführungsumgebung, führt einige Setup-Vorgänge durch und startet dann die Ereignisschleife. { SealHandleScope-Siegel (isolier); bool mehr; env.performance_state()->Markieren( Knoten::Leistung::NODE_PERFORMANCE_MILESTONE_LOOP_START); Tun { uv_run(env.event_loop(), UV_RUN_DEFAULT); v8_platform.DrainVMTasks(isolieren); mehr = uv_loop_alive(env.event_loop()); wenn (mehr) weitermachen; RunBeforeExit(&env); // Gib `beforeExit` aus, wenn die Schleife entweder nach der Ausgabe aktiv wurde // Ereignis oder nach dem Ausführen einiger Rückrufe. mehr = uv_loop_alive(env.event_loop()); } während (mehr == wahr); env.performance_state()->Markieren( Knoten::Leistung::NODE_PERFORMANCE_MILESTONE_LOOP_EXIT); } env.set_trace_sync_io(falsch); const int exit_code = EmitExit(&env); Führt einen Fehler aus, indem er auf "Ausführen" klickt. „more“ wird verwendet, um anzugeben, ob mit dem nächsten Zyklus fortgefahren werden soll. env->event_loop() gibt den zuvor in env gespeicherten default_loop_ptr zurück und die Funktion uv_run startet die Ereignisschleife von libuv im angegebenen UV_RUN_DEFAULT-Modus. Wenn keine E/A-Ereignisse und keine Timer-Ereignisse vorhanden sind, gibt uv_loop_alive „false“ zurück. Ausführungsreihenfolge der EreignisschleifeGemäß der offiziellen Einführung von Node.js enthält jede Ereignisschleife 6 Phasen, die der Implementierung im libuv-Quellcode entsprechen, wie in der folgenden Abbildung dargestellt:
Kernfunktion uv_run: Quellcode Kernquellcode int uv_run(uv_loop_t* loop, uv_run_mode mode) { int timeout; int r; int ran_pending; //Zuerst prüfen, ob unsere Schleife noch aktiv ist //Alive bedeutet, ob sich ein asynchroner Task in der Schleife befindet //Wenn nicht, einfach beenden r = uv__loop_alive(loop); if (!r) uv__update_time(loop); //Die legendäre Ereignisschleife, richtig gelesen! Es ist eine große Weile while (r != 0 && loop->stop_flag == 0) { //Ereignisphase aktualisieren uv__update_time(loop); //Timer-Rückruf verarbeiten uv__run_timers(loop); //Asynchroner Task-Rückruf verarbeiten ran_pending = uv__run_pending(loop); //Nutzlose Phase uv__run_idle(loop); uv__run_prepare(loop); //Das ist hier zu beachten //Von hier bis zum folgenden uv__io_poll ist es sehr schwer zu verstehen //Denken Sie zuerst daran, dass Timeout eine Zeit ist //Nachdem uv_backend_timeout berechnet wurde, wird es an uv__io_poll übergeben //Wenn Timeout = 0, überspringt uv__io_poll Timeout = 0 direkt; wenn ((Modus == UV_RUN_ONCE && !ran_pending) || Modus == UV_RUN_DEFAULT) Zeitüberschreitung = uv_backend_timeout(Schleife); uv__io_poll(loop, timeout); // einfach setImmediate ausführen uv__run_check(loop); //Dateideskriptoren und andere Operationen schließen uv__run_closing_handles(loop); if (mode == UV_RUN_ONCE) { /* UV_RUN_ONCE bedeutet Vorwärtsfortschritt: mindestens ein Callback muss * aufgerufen wurde, wenn es zurückkehrt. uv__io_poll() kann zurückkehren, ohne * I/O (also: keine Callbacks) wenn das Timeout abgelaufen ist - das heißt wir * über ausstehende Timer verfügen, die die Vorwärtsfortschrittsbeschränkung erfüllen. * * UV_RUN_NOWAIT gibt keine Garantien für den Fortschritt und wird daher weggelassen. * der Scheck. */ uv__update_time(Schleife); uv__run_timers(Schleife); } r = uv__loop_alive(Schleife); wenn (Modus == UV_RUN_ONCE || Modus == UV_RUN_NOWAIT) abbrechen; } /* Die if-Anweisung lässt gcc es in einen bedingten Speicher kompilieren. Vermeidet * Verschmutzen einer Cache-Zeile. */ wenn (Schleife->Stopp_Flag != 0) Schleife->Stopp_Flag = 0; returniere r; } Ich habe den Code sehr ausführlich geschrieben und glaube, dass ihn diejenigen, die mit C-Code nicht vertraut sind, leicht verstehen können. Ja, die Ereignisschleife ist nur eine große Weile! Damit ist der Schleier des Geheimnisses gelüftet. uv__io_poll-PhaseDiese Phase ist sehr clever gestaltet. Der zweite Parameter dieser Funktion ist ein Timeout-Parameter, und dieses Timeout stammt von der Funktion uv_backend_timeout. Schauen wir uns das mal an! Quellcode int uv_backend_timeout(const uv_loop_t* loop) { wenn (loop->stop_flag != 0) gibt 0 zurück; wenn (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop)) gibt 0 zurück; wenn (!QUEUE_EMPTY(&loop->idle_handles)) gibt 0 zurück; wenn (!QUEUE_EMPTY(&loop->pending_queue)) gibt 0 zurück; wenn (loop->closing_handles) gibt 0 zurück; gibt uv__next_timeout(loop) zurück; } Es stellt sich heraus, dass es sich um eine mehrstufige if-Funktion handelt. Lassen Sie uns sie Schritt für Schritt analysieren. 1. stop_flag: Wenn dieses Flag 0 ist, bedeutet dies, dass die Ereignisschleife nach dem Ausführen dieser Runde beendet wird und die Rückgabezeit 0 ist 2. !uv__has_active_handles und !uv__has_active_reqs: Wie die Namen andeuten, muss die Timeout-Zeit 0 sein, wenn keine asynchronen Aufgaben (einschließlich Timer und asynchroner E/A) vorhanden sind. 3. QUEUE_EMPTY(idle_handles) und QUEUE_EMPTY(pending_queue): Asynchrone Aufgaben werden in pending_queue registriert. Unabhängig davon, ob sie erfolgreich waren oder nicht, wurden sie registriert. Wenn nichts vorhanden ist, sind diese beiden Warteschlangen leer, sodass kein Warten erforderlich ist. 4. closing_handles: Unsere Schleife ist in die Abschlussphase eingetreten, kein Grund zu warten Alle oben genannten Bedingungen werden beurteilt und beurteilt, nur um auf diesen Satz zu warten, der uv__next_timeout (Schleife) zurückgibt. Dieser Satz sagt uv__io_poll: Wie lange wirst du anhalten? Als nächstes sehen wir weiter, wie dieses magische uv__next_timeout die Zeit erhält. int uv__next_timeout(const uv_loop_t* Schleife) { const Struktur heap_node* heap_node; const uv_timer_t* Handle; uint64_t Unterschied; heap_node = heap_min((const struct heap*) &loop->timer_heap); if (heap_node == NULL) return -1; /* auf unbestimmte Zeit blockieren */ handle = container_of(heap_node, uv_timer_t, heap_node); if (handle->timeout time) return 0; //Dieser Code liefert die Schlüsselanleitung diff = handle->timeout - loop->time; //Darf nicht größer sein als das Maximum INT_MAX wenn (diff > INT_MAX) diff = INT_MAX; gibt diff zurück; } Nach Ablauf der Wartezeit wird die Prüfphase eingeleitet. Anschließend wird die Phase „closing_handles“ eingeleitet und eine Ereignisschleife wird beendet. Da es sich um eine Quellcodeanalyse handelt, werde ich nicht ins Detail gehen. Sie können nur die offizielle Dokumentation lesen. Zusammenfassen1. Nodejs interagiert mit dem Betriebssystem. Die Methoden, die wir in Javascript aufrufen, werden schließlich über process.binding an die C/C++-Ebene übergeben und führen schließlich die eigentlichen Operationen aus. So interagiert Node.js mit dem Betriebssystem. 2. Das sogenannte Single-Threaded-NodeJS ist nur der Haupt-Thread. Alle Netzwerkanforderungen oder asynchronen Aufgaben werden zur Implementierung an den internen Thread-Pool übergeben. Es ist nur für die kontinuierliche Roundtrip-Planung verantwortlich, und die Ereignisschleife steuert kontinuierlich die Ereignisausführung. 3. Der Grund, warum Nodejs eine hohe Parallelität mit einem einzelnen Thread verarbeiten kann, liegt am Ereignisschleifenmechanismus der Libuv-Schicht und der zugrunde liegenden Thread-Pool-Implementierung. 4. Die Ereignisschleife ist der Hauptthread, der kontinuierlich Ereignisse aus der Ereigniswarteschlange des Hauptthreads liest und die Ausführung aller asynchronen Rückruffunktionen steuert. Die Ereignisschleife hat insgesamt 7 Stufen, jede Stufe hat eine Aufgabenwarteschlange. Wenn alle Stufen einmal nacheinander ausgeführt werden, schließt die Ereignisschleife einen Tick ab. Das Obige ist der detaillierte Inhalt der Erkundung von Nodejs, um ein tiefgreifendes Verständnis des Prinzips der Single-Threaded-Hochparallelität zu erlangen. Weitere Informationen zu Nodejs finden Sie in den anderen verwandten Artikeln auf 123WORDPRESS.COM! Das könnte Sie auch interessieren:
|
>>: CentOS 7: Erläuterung zum Wechseln des Boot-Kernels und des Boot-Modus
Überblick In tatsächlichen Geschäftsszenarioanwen...
Inhaltsverzeichnis Vorwort Dynamisches SQL 1. Sch...
Ich habe zuvor einige dynamische Routing-Einstell...
Einführung in Rahmeneigenschaften border -Eigensc...
Die meisten der folgenden Befehle müssen in der K...
Die Rolle der Schnittstelle: Schnittstelle, auf E...
Inhaltsverzeichnis Zweck des Teleports So funktio...
Hallo zusammen, heute lernen wir die Installation...
1. Laden Sie das Installationspaket herunter Das ...
Inhaltsverzeichnis Hintergrund CommonsChunkPlugin...
MySQL-Batch löschen großer Datenmengen Angenommen...
Als Reaktion auf die Popularität von nodejs haben...
Inhaltsverzeichnis 1. Deklarieren Sie eine Funkti...
Vor kurzem habe ich einen solchen Effekt implemen...
<div Klasse="Seitenleiste"> <d...