Node.js Internals'ı Keşfetme

Yayınlanan: 2022-03-10
Kısa özet ↬ Node.js, web geliştiricileri için ilginç bir araçtır. Yüksek düzeyde eşzamanlılık ile, web geliştirmede kullanılacak araçları seçen insanlar için lider bir aday haline geldi. Bu makalede, Node.js'yi neyin oluşturduğunu öğreneceğiz, ona anlamlı bir tanım vereceğiz, Node.js'nin iç öğelerinin birbirleriyle nasıl etkileşime girdiğini anlayacağız ve GitHub'da Node.js için proje deposunu keşfedeceğiz.

Node.js'nin Ryan Dahl tarafından 8 Kasım 2009'da European JSConf'ta tanıtılmasından bu yana, teknoloji endüstrisinde geniş bir kullanım gördü. Netflix, Uber ve LinkedIn gibi şirketler, Node.js'nin yüksek miktarda trafiğe ve eşzamanlılığa dayanabileceği iddiasına güvenilirlik kazandırıyor.

Temel bilgilerle donanmış olan Node.js'nin başlangıç ​​ve orta düzey geliştiricileri birçok şeyle mücadele ediyor: "Bu sadece bir çalışma zamanı!" “Olay döngüleri var!” “Node.js, JavaScript gibi tek iş parçacıklıdır!”

Bu iddialardan bazıları doğru olsa da, JavaScript'i nasıl çalıştırdığını anlayarak, gerçekte tek iş parçacıklı olup olmadığını görerek ve son olarak, temel bağımlılıkları olan V8 ve libuv arasındaki bağlantıyı daha iyi anlayarak Node.js çalışma zamanını daha derinlemesine inceleyeceğiz. .

Önkoşullar

  • Temel JavaScript bilgisi
  • Node.js semantiğine aşinalık ( require , fs )

Node.js Nedir?

Pek çok kişinin Node.js hakkında neye inandığını varsaymak cazip gelebilir, bunun en yaygın tanımı JavaScript dili için bir çalışma zamanı olmasıdır. Bunu düşünmek için, bu sonuca neyin yol açtığını anlamalıyız.

Node.js genellikle C++ ve JavaScript'in bir kombinasyonu olarak tanımlanır. C++ bölümü, bilgisayara bağlı donanıma erişimi mümkün kılan düşük seviyeli kod çalıştıran bağlamalardan oluşur. JavaScript bölümü, kaynak kodu olarak JavaScript'i alır ve onu, V8 motoru adlı popüler bir dil yorumlayıcısında çalıştırır.

Bu anlayışla, Node.js'yi tarayıcı ortamının dışında programları çalıştırmak için JavaScript ve C++'ı birleştiren benzersiz bir araç olarak tanımlayabiliriz.

Ama aslında buna çalışma zamanı diyebilir miyiz? Bunu belirlemek için, bir çalışma zamanının ne olduğunu tanımlayalım.

StackOverflow'a verdiği yanıtlardan birinde DJNA, çalışma zamanı ortamını "bir programı yürütmek için ihtiyacınız olan her şey, ancak onu değiştirmek için hiçbir araç yok" olarak tanımlar. Bu tanıma göre, kodumuzu çalıştırırken olan her şeyin (herhangi bir dilde) bir runtime ortamında çalıştığını güvenle söyleyebiliriz.

Diğer dillerin kendi çalışma zamanı ortamı vardır. Java için bu, Java Runtime Environment'dır (JRE). .NET için bu, Ortak Dil Çalışma Zamanı'dır (CLR). Erlang için, BEAM'dir.

Bununla birlikte, bu çalışma zamanlarından bazılarının kendilerine bağlı olan başka dilleri vardır. Örneğin, Java, bir JRE'nin anlayabileceği kodlamak için derleyen bir programlama dili olan Kotlin'e sahiptir. Erlang'ın İksir'i var. .NET Framework olarak bilinen, tümü CLR'de çalışan birçok .NET geliştirme çeşidi olduğunu biliyoruz.

Artık çalışma zamanının bir programın başarılı bir şekilde yürütülebilmesi için sağlanan bir ortam olduğunu anlıyoruz ve V8 ve bir dizi C++ kitaplığının bir Node.js uygulamasının yürütülmesini mümkün kıldığını biliyoruz. Node.js'nin kendisi, bu kitaplıkları bir varlık haline getirmek için her şeyi birbirine bağlayan asıl çalışma zamanıdır ve Node.js'nin neyle oluşturulduğundan bağımsız olarak yalnızca bir dili (JavaScript) anlar.

Atlamadan sonra daha fazlası! Aşağıdan okumaya devam edin ↓

Node.js'nin İç Yapısı

node index.js komutunu kullanarak komut satırımızdan bir Node.js programını ( index.js gibi) çalıştırmayı denediğimizde, Node.js çalışma zamanını çağırırız. Bu çalışma zamanı, belirtildiği gibi, iki bağımsız bağımlılıktan oluşur, V8 ve libuv.

Core Node.js bağımlılıkları
Core Node.js Bağımlılıkları (Geniş önizleme)

V8, Google tarafından oluşturulan ve sürdürülen bir projedir. JavaScript kaynak kodunu alır ve tarayıcı ortamının dışında çalıştırır. Bir programı bir node komutuyla çalıştırdığımızda, kaynak kod Node.js çalışma zamanı tarafından yürütülmek üzere V8'e iletilir.

Libuv kitaplığı, işletim sistemine düşük düzeyde erişim sağlayan C++ kodunu içerir. Ağ oluşturma, dosya sistemine yazma ve eşzamanlılık gibi işlevler, JavaScript kodumuzu çalıştıran Node.js parçası olan V8'de varsayılan olarak gönderilmez. Kitaplık seti ile libuv, bu yardımcı programları ve daha fazlasını bir Node.js ortamında sağlar.

Node.js, iki kitaplığı bir arada tutan ve böylece benzersiz bir çözüm haline gelen yapıştırıcıdır. Bir betiğin yürütülmesi boyunca Node.js, kontrolün hangi projeye ne zaman devredileceğini anlar.

Sunucu Taraflı Programlar İçin İlginç API'ler

Biraz JavaScript geçmişini incelersek, bunun tarayıcıdaki bir sayfaya bazı işlevler ve etkileşimler eklemek için olduğunu biliriz. Ve tarayıcıda, sayfayı oluşturan belge nesne modelinin (DOM) öğeleriyle etkileşime girerdik. Bunun için topluca DOM API olarak adlandırılan bir dizi API mevcuttur.

DOM yalnızca tarayıcıda bulunur; bir sayfayı oluşturmak için ayrıştırılan şeydir ve temel olarak HTML olarak bilinen biçimlendirme dilinde yazılmıştır. Ayrıca, tarayıcı bir pencerede bulunur, dolayısıyla bir JavaScript bağlamında sayfadaki tüm nesneler için bir kök görevi gören window nesnesi. Bu ortama tarayıcı ortamı denir ve JavaScript için bir çalışma zamanı ortamıdır.

Node.js API'leri bazı işlevler için libuv'u çağırır
Node.js API'leri libuv ile etkileşime girer (Geniş önizleme)

Node.js ortamında, sayfa veya tarayıcı gibi bir şeye sahip değiliz - bu, global pencere nesnesi hakkındaki bilgimizi geçersiz kılar. Sahip olduğumuz şey, bir JavaScript programına ek işlevsellik sağlamak için işletim sistemiyle etkileşime giren bir dizi API'dir. Node.js için bu API'ler ( fs , path , buffer , events , HTTP ve benzeri), sahip olduğumuz şekliyle, yalnızca Node.js için mevcuttur ve Node.js (kendisi bir çalışma zamanı) tarafından sağlanır, böylece biz Node.js için yazılmış programları çalıştırabilir.

Deney: fs.writeFile Nasıl Yeni Bir Dosya Oluşturur?

V8, JavaScript'i tarayıcının dışında çalıştırmak için oluşturulduysa ve bir Node.js ortamı bir tarayıcıyla aynı içeriğe veya ortama sahip değilse, dosya sistemine erişmek veya bir HTTP sunucusu yapmak gibi bir şeyi nasıl yapardık?

Örnek olarak, geçerli dizindeki dosya sistemine bir dosya yazan basit bir Node.js uygulamasını ele alalım:

 const fs = require("fs") fs.writeFile("./test.txt", "text");

Görüldüğü gibi dosya sistemine yeni bir dosya yazmaya çalışıyoruz. Bu özellik JavaScript dilinde mevcut değildir; yalnızca Node.js ortamında kullanılabilir. Bu nasıl yürütülür?

Bunu anlamak için Node.js kod tabanında bir tur atalım.

Node.js için GitHub deposuna giderken iki ana klasör görüyoruz, src ve lib . lib klasörü, her Node.js kurulumunda varsayılan olarak bulunan güzel modüller setini sağlayan JavaScript koduna sahiptir. src klasörü, libuv için C++ kitaplıklarını içerir.

lib klasörüne fs.js dosyasını incelersek, etkileyici JavaScript koduyla dolu olduğunu göreceğiz. 1880 satırında, bir exports beyanı göreceğiz. Bu ifade, fs modülünü içe aktararak erişebildiğimiz her şeyi dışa aktarır ve bunun writeFile adlı bir işlevi dışa aktardığını görebiliriz.

function writeFile( (fonksiyonun tanımlandığı yer) işlevinin aranması bizi işlevin dört parametreyle tanımlandığını gördüğümüz 1303 satırına götürür:

 function writeFile(path, data, options, callback) { callback = maybeCallback(callback || options); options = getOptions(options, { encoding: 'utf8', mode: 0o666, flag: 'w' }); const flag = options.flag || 'w'; if (!isArrayBufferView(data)) { validateStringAfterArrayBufferView(data, 'data'); data = Buffer.from(data, options.encoding || 'utf8'); } if (isFd(path)) { const isUserFd = true; writeAll(path, isUserFd, data, 0, data.byteLength, callback); return; } fs.open(path, flag, options.mode, (openErr, fd) => { if (openErr) { callback(openErr); } else { const isUserFd = false; writeAll(fd, isUserFd, data, 0, data.byteLength, callback); } }); }

1315 ve 1324 satırlarında, bazı doğrulama kontrollerinden sonra writeAll adlı tek bir işlevin çağrıldığını görüyoruz. Bu işlevi aynı fs.js dosyasında 1278 satırında buluyoruz.

 function writeAll(fd, isUserFd, buffer, offset, length, callback) { // write(fd, buffer, offset, length, position, callback) fs.write(fd, buffer, offset, length, null, (writeErr, written) => { if (writeErr) { if (isUserFd) { callback(writeErr); } else { fs.close(fd, function close() { callback(writeErr); }); } } else if (written === length) { if (isUserFd) { callback(null); } else { fs.close(fd, callback); } } else { offset += written; length -= written; writeAll(fd, isUserFd, buffer, offset, length, callback); } }); }

Bu modülün kendisini çağırmaya çalıştığını da belirtmek ilginçtir. Bunu, fs.write olarak adlandırılan 1280 satırında görüyoruz. write işlevini ararken, biraz bilgi keşfedeceğiz.

write işlevi 571. satırda başlar ve yaklaşık 42 satır çalışır. Bu işlevde yinelenen bir model görüyoruz: 594 ve 612. satırlarda görüldüğü gibi binding modülündeki bir işlevi çağırma şekli. binding modülündeki bir işlev yalnızca bu işlevde değil, dışa aktarılan hemen hemen her işlevde çağrılır. fs.js dosya dosyasında. Bu konuda çok özel bir şey olmalı.

binding değişkeni dosyanın en üst kısmında 58. satırda bildirilir ve bu işlev çağrısına tıklandığında GitHub'ın yardımıyla bazı bilgiler ortaya çıkar.

Bağlayıcı değişkenin beyanı
Bağlayıcı değişkenin bildirimi (Büyük önizleme)

Bu internalBinding işlevi, loaders adlı modülde bulunur. Loaders modülünün ana işlevi, tüm libuv kitaplıklarını yüklemek ve bunları Node.js ile V8 projesi aracılığıyla birbirine bağlamaktır. Bunu nasıl yaptığı oldukça büyülü, ancak daha fazlasını öğrenmek için fs modülü tarafından çağrılan writeBuffer işlevine yakından bakabiliriz.

Bunun libuv ile nerede bağlantı kurduğuna ve V8'in nereden geldiğine bakmalıyız. Yükleyici modülünün en üstünde, bazı iyi belgeler şunu belirtiyor:

 // This file is compiled and run by node.cc before bootstrap/node.js // was called, therefore the loaders are bootstraped before we start to // actually bootstrap Node.js. It creates the following objects: // // C++ binding loaders: // - process.binding(): the legacy C++ binding loader, accessible from user land // because it is an object attached to the global process object. // These C++ bindings are created using NODE_BUILTIN_MODULE_CONTEXT_AWARE() // and have their nm_flags set to NM_F_BUILTIN. We do not make any guarantees // about the stability of these bindings, but still have to take care of // compatibility issues caused by them from time to time. // - process._linkedBinding(): intended to be used by embedders to add // additional C++ bindings in their applications. These C++ bindings // can be created using NODE_MODULE_CONTEXT_AWARE_CPP() with the flag // NM_F_LINKED. // - internalBinding(): the private internal C++ binding loader, inaccessible // from user land unless through `require('internal/test/binding')`. // These C++ bindings are created using NODE_MODULE_CONTEXT_AWARE_INTERNAL() // and have their nm_flags set to NM_F_INTERNAL. // // Internal JavaScript module loader: // - NativeModule: a minimal module system used to load the JavaScript core // modules found in lib/**/*.js and deps/**/*.js. All core modules are // compiled into the node binary via node_javascript.cc generated by js2c.py, // so they can be loaded faster without the cost of I/O. This class makes the // lib/internal/*, deps/internal/* modules and internalBinding() available by // default to core modules, and lets the core modules require itself via // require('internal/bootstrap/loaders') even when this file is not written in // CommonJS style.

Burada öğrendiğimiz şey, Node.js projesinin JavaScript bölümündeki binding nesnesinden çağrılan her modül için, src klasöründeki C++ bölümünde bunun bir karşılığı olduğudur.

fs turumuzdan bunu yapan modülün node_file.cc içerisinde olduğunu görüyoruz. Modül üzerinden erişilebilen her fonksiyon dosyada tanımlanır; örneğin, 2258 satırında writeBuffer var. C++ dosyasındaki bu yöntemin asıl tanımı 1785 satırındadır. Ayrıca, libuv'un dosyaya asıl yazmayı yapan kısmına yapılan çağrı 1809 ve satırlarda bulunabilir. 1815, burada libuv işlevi uv_fs_write eşzamansız olarak çağrılır.

Bu Anlayıştan Ne Kazanıyoruz?

Diğer birçok yorumlanmış dil çalışma zamanı gibi, Node.js'nin çalışma zamanı da saldırıya uğrayabilir. Daha fazla anlayışla, sadece kaynağa bakarak standart dağıtımla imkansız olan şeyleri yapabiliriz. Bazı işlevlerin çağrılma biçiminde değişiklik yapmak için kitaplıklar ekleyebiliriz. Ancak her şeyden önce, bu anlayış daha fazla araştırma için bir temeldir.

Node.js Tek İş parçacıklı mı?

Libuv ve V8 üzerinde oturan Node.js, tarayıcıda çalışan tipik bir JavaScript motorunun sahip olmadığı bazı ek işlevlere erişime sahiptir.

Bir tarayıcıda çalışan herhangi bir JavaScript, tek bir iş parçacığında yürütülür. Bir programın yürütülmesindeki bir iş parçacığı, programın yürütüldüğü CPU'nun üzerinde duran bir kara kutu gibidir. Bir Node.js bağlamında, bazı kodlar, makinelerimizin taşıyabileceği kadar çok iş parçacığında yürütülebilir.

Bu iddiayı doğrulamak için basit bir kod parçasını inceleyelim.

 const fs = require("fs"); // A little benchmarking const startTime = Date.now() fs.writeFile("./test.txt", "test", (err) => { If (error) { console.log(err) } console.log("1 Done: ", Date.now() — startTime) });

Yukarıdaki snippet'te, mevcut dizindeki diskte yeni bir dosya oluşturmaya çalışıyoruz. Bunun ne kadar sürebileceğini görmek için, komut dosyasının başlangıç ​​zamanını izlemek için küçük bir kıyaslama ekledik, bu da bize dosyayı oluşturan komut dosyasının milisaniye cinsinden süresini verir.

Yukarıdaki kodu çalıştırırsak aşağıdaki gibi bir sonuç elde ederiz.

Node.js'de tek bir dosya oluşturmak için geçen sürenin sonucu
Node.js'de tek bir dosya oluşturmak için geçen süre (Geniş önizleme)
 $ node ./test.js -> 1 Done: 0.003s

Bu çok etkileyici: sadece 0.003 saniye.

Ama gerçekten ilginç bir şey yapalım. Önce yeni dosyayı oluşturan kodu çoğaltalım ve günlük ifadesindeki sayıyı konumlarını yansıtacak şekilde güncelleyelim:

 const fs = require("fs"); // A little benchmarking const startTime = Date.now() fs.writeFile("./test1.txt", "test", function (err) { if (err) { console.log(err) } console.log("1 Done: %ss", (Date.now() — startTime) / 1000) }); fs.writeFile("./test2.txt", "test", function (err) { if (err) { console.log(err) } console.log("2 Done: %ss", (Date.now() — startTime) / 1000) }); fs.writeFile("./test3.txt", "test", function (err) { if (err) { console.log(err) } console.log("3 Done: %ss", (Date.now() — startTime) / 1000) }); fs.writeFile("./test4.txt", "test", function (err) { if (err) { console.log(err) } console.log("4 Done: %ss", (Date.now() — startTime) / 1000) });

Bu kodu çalıştırmayı denersek, aklımızı başımızdan alacak bir şey elde ederiz. İşte sonucum:

Birden çok dosya oluşturmak için geçen sürenin sonucu
Aynı anda birçok dosya oluşturma (Geniş önizleme)

İlk olarak, sonuçların tutarlı olmadığını fark edeceğiz. İkinci olarak sürenin arttığını görüyoruz. Ne oluyor?

Düşük Seviyeli Görevler Yetkilendiriliyor

Node.js, şimdi bildiğimiz gibi tek iş parçacıklı. Node.js'nin bazı bölümleri JavaScript'te, diğerleri ise C++'da yazılmıştır. Node.js, tarayıcı ortamından aşina olduğumuz olay döngüsü ve çağrı yığınının aynı kavramlarını kullanır; bu, Node.js'nin JavaScript bölümlerinin tek iş parçacıklı olduğu anlamına gelir. Ancak bir işletim sistemiyle konuşmayı gerektiren düşük seviyeli görev tek iş parçacıklı değildir.

Düşük seviyeli görevler, libuv aracılığıyla işletim sistemine devredilir
Node.js düşük seviyeli görev delegasyonu (Geniş önizleme)

Bir çağrı Node.js tarafından libuv amaçlı olarak algılandığında, bu görevi libuv'a devreder. Çalışmasında, libuv bazı kitaplıkları için iş parçacıkları gerektirir, bu nedenle ihtiyaç duyulduğunda Node.js programlarının yürütülmesinde iş parçacığı havuzunun kullanılması.

Varsayılan olarak, libuv tarafından sağlanan Node.js iş parçacığı havuzunda dört iş parçacığı bulunur. Komut dosyamızın üst kısmındaki process.env.UV_THREADPOOL_SIZE çağırarak bu iş parçacığı havuzunu artırabilir veya azaltabiliriz.

 // script.js process.env.UV_THREADPOOL_SIZE = 6; // … // …

Dosya Oluşturma Programımıza Neler Oluyor?

Dosyamızı oluşturmak için kodu çağırdığımızda, Node.js kodunun bu görev için bir iş parçacığı tahsis eden libuv kısmına çarpıyor gibi görünüyor. Libuv'daki bu bölüm, dosya üzerinde çalışmadan önce disk hakkında bazı istatistiksel bilgiler alır.

Bu istatistiksel kontrolün tamamlanması biraz zaman alabilir; bu nedenle, istatistiksel kontrol tamamlanana kadar iş parçacığı diğer bazı görevler için serbest bırakılır. Kontrol tamamlandığında, libuv bölümü mevcut herhangi bir iş parçacığını işgal eder veya bunun için bir iş parçacığı uygun olana kadar bekler.

Yalnızca dört çağrımız ve dört ileti dizimiz var, yani dolaşmak için yeterli ileti dizisi var. Tek soru, her iş parçacığının görevini ne kadar hızlı işleyeceğidir. İş parçacığı havuzuna girecek ilk kodun önce sonucunu döndüreceğini ve kodunu çalıştırırken diğer tüm iş parçacıklarını engellediğini fark edeceğiz.

Çözüm

Artık Node.js'nin ne olduğunu anlıyoruz. Bunun bir çalışma zamanı olduğunu biliyoruz. Çalışma zamanının ne olduğunu tanımladık. Ve Node.js tarafından sağlanan çalışma zamanını neyin oluşturduğunu derinlemesine inceledik.

Uzun yoldan geldik. Ve GitHub'daki Node.js deposundaki küçük turumuzdan, burada yaptığımız işlemin aynısını izleyerek ilgilenebileceğimiz herhangi bir API'yi keşfedebiliriz. Node.js açık kaynaktır, yani kesinlikle kaynağa dalabiliriz, değil mi?

Node.js çalışma zamanında olanların düşük düzeylerinden birkaçına değinmiş olsak da, hepsini bildiğimizi varsaymamalıyız. Aşağıdaki kaynaklar, bilgimizi geliştirebileceğimiz bazı bilgilere işaret etmektedir:

  • Node.js'ye Giriş
    Resmi bir web sitesi olan Node.dev, Node.js'nin ne olduğunu ve paket yöneticilerini açıklar ve bunun üzerine kurulu web çerçevelerini listeler.
  • “JavaScript & Node.js”, Düğüm Başlangıç ​​Kitabı
    Manuel Kiessling'in bu kitabı, her ikisi de aynı dilde yazılmış olmasına rağmen tarayıcıdaki JavaScript'in Node.js'deki ile aynı olmadığı konusunda uyarıda bulunduktan sonra Node.js'yi açıklamak için harika bir iş çıkarıyor.
  • Node.js'nin Başlangıcı
    Bu başlangıç ​​kitabı, çalışma zamanının bir açıklamasının ötesine geçer. Paketler, akışlar ve Express çerçevesiyle bir web sunucusu oluşturma hakkında bilgi verir.
  • libUV
    Bu, Node.js çalışma zamanının destekleyici C++ kodunun resmi belgesidir.
  • V8
    Bu, JavaScript motorunun JavaScript ile Node.js yazmayı mümkün kılan resmi belgesidir.