مرحبا يا هبر! أقدم إليكم ترجمة المقال "كل ما تحتاج لمعرفته حول Node.js" لجورج رامون.

في الوقت الحاضر ، تعد منصة Node.js واحدة من أكثر المنصات شيوعًا لبناء واجهات برمجة تطبيقات REST تتسم بالكفاءة والقابلية للتطوير. كما أنها مناسبة لإنشاء تطبيقات متنقلة مختلطة ، وبرامج سطح مكتب ، وحتى إنترنت الأشياء.
لقد كنت أعمل مع منصة Node.js منذ أكثر من 6 سنوات وأحبها بالفعل. تحاول هذه المشاركة أن تكون دليلًا لكيفية عمل Node.js فعليًا.
لنبدأ!
ما سيتم مناقشته:
العالم قبل Node.js
خادم متعدد مؤشرات الترابط
تعمل تطبيقات الويب المكتوبة بعد بنية العميل / الخادم كما يلي - يطلب العميل المورد الضروري من الخادم ويقوم الخادم بإرسال المورد استجابة لذلك. في هذا المخطط ، يستجيب الخادم للطلب وينهي الاتصال.
هذا النموذج فعال لأن كل طلب إلى الخادم يستهلك الموارد (الذاكرة ، وقت المعالج ، إلخ). من أجل معالجة كل طلب لاحق من العميل ، يجب على الخادم إكمال معالجة الطلب السابق.
هل هذا يعني أن الخادم يمكنه معالجة طلب واحد فقط في المرة الواحدة؟ ليس حقا! عندما يتلقى الخادم طلبًا جديدًا ، فإنه ينشئ سلسلة رسائل منفصلة لمعالجته.
التدفق ، وبكلمات بسيطة ، هو الوقت والموارد التي تخصصها وحدة المعالجة المركزية لتنفيذ مجموعة صغيرة من التعليمات. ومع ذلك ، يمكن للخادم معالجة العديد من الطلبات في وقت واحد ، ولكن طلب واحد فقط لكل مؤشر ترابط. يسمى هذا النموذج أيضًا نموذج مؤشر ترابط لكل طلب .

لمعالجة طلبات N ، يحتاج الخادم إلى مؤشرات ترابط N. إذا كان الخادم يتلقى طلبات N + 1 ، فيجب عليه الانتظار حتى يتوفر أحد سلاسل الرسائل.
في الشكل أعلاه ، يمكن أن يقوم الخادم بمعالجة ما يصل إلى 4 طلبات (سلاسل) في وقت واحد ، وعندما يتلقى الطلبات الثلاثة التالية ، يجب أن تنتظر هذه الطلبات حتى يصبح أي من سلاسل العمليات الأربعة هذه متاحًا.
تتمثل إحدى طرق التخلص من القيود في إضافة المزيد من الموارد (الذاكرة ، مراكز المعالج ، وما إلى ذلك) إلى الخادم ، ولكن هذا ليس هو الحل الأفضل ....
وبطبيعة الحال ، لا تنسى القيود التكنولوجية.
منع المدخلات / الإخراج
العدد المحدود من مؤشرات الترابط على الخادم ليس هو المشكلة الوحيدة. ربما كنت تتساءل لماذا مؤشر ترابط واحد لا يمكن معالجة طلبات متعددة في نفس الوقت؟ كل ذلك بسبب حظر عمليات الإدخال / الإخراج .
افترض أنك تقوم بتطوير متجر عبر الإنترنت وتحتاج إلى صفحة حيث يمكن للمستخدم عرض قائمة بجميع المنتجات.
يطرق المستخدم http://yourstore.com/products والخادم يعرض ملف HTML مع جميع المنتجات من قاعدة البيانات استجابة. ليس على الإطلاق معقدة ، أليس كذلك؟
ولكن ماذا يحدث وراء الكواليس؟
- عندما يطرق المستخدم
/products
يجب تنفيذ طريقة أو وظيفة معينة من أجل معالجة الطلب. تقوم قطعة صغيرة من التعليمات البرمجية (الخاصة بك أو إطار العمل الخاص بك) بتوزيع عنوان URL الخاص بالطلب وتبحث عن طريقة أو وظيفة مناسبة. الدفق قيد التشغيل . 
- الآن يتم تنفيذ الطريقة أو الوظيفة المطلوبة ، كما في الفقرة الأولى ، يعمل الخيط.

- نظرًا لأنك مطور جيد ، يمكنك حفظ جميع سجلات النظام في ملف ، وبالطبع ، للتأكد من أن الموجه يقوم بتنفيذ الطريقة / الوظيفة المطلوبة - يمكنك أيضًا تسجيل السطر "تنفيذ الطريقة العاشرة !!". لكن كل هذا يحظر العمليات تيار الإدخال / الإخراج ينتظر .

- يتم حفظ جميع السجلات ويتم تنفيذ خطوط الوظائف التالية. الخيط يعمل مرة أخرى .

- حان الوقت للوصول إلى قاعدة البيانات والحصول على جميع المنتجات - استعلام بسيط مثل
SELECT * FROM products
يؤدي وظيفته ، ولكن هل تخمين ماذا؟ نعم ، هذه عملية حظر إدخال / إخراج. الدفق ينتظر . 
- لقد تلقيت مجموعة أو قائمة بجميع المنتجات ، ولكن تأكد من أنك تعهدت بكل هذا. الدفق ينتظر .

- لديك الآن جميع المنتجات وقد حان الوقت لتقديم القالب للصفحة المستقبلية ، ولكن قبل ذلك تحتاج إلى قراءتها. الدفق ينتظر .

- يقوم محرك العرض بعمله ويرسل استجابة إلى العميل. الخيط يعمل مرة أخرى .

- التدفق مجاني ، مثل طائر في السماء.

ما مدى بطء عمليات الإدخال / الإخراج؟ حسنا هذا يعتمد على محددة. لنلقِ نظرة على الطاولة:
عمليات قراءة الشبكة والقرص بطيئة للغاية. تخيل عدد الطلبات أو المكالمات إلى واجهات برمجة التطبيقات الخارجية التي يمكن لنظامك التعامل معها خلال هذا الوقت.
لتلخيص: عمليات الإدخال / الإخراج جعل مؤشر الترابط الانتظار وإهدار الموارد.
مشكلة C10K
المشكلة
C10k (المهندس C10k ؛ اتصالات 10k - مشكلة الاتصالات 10 آلاف)
في أوائل عام 2000 ، كانت أجهزة الخادم والعميل بطيئة. نشأت المشكلة عند معالجة 10،000 عميل اتصالات إلى نفس الجهاز بشكل متوازٍ.
ولكن لماذا لم يتمكن نموذج الخيط لكل طلب (خيط حسب الطلب) من حل هذه المشكلة؟ حسنا ، دعنا نستخدم الرياضيات قليلا.
يخصص التنفيذ الأصلي لمؤشرات الترابط أكثر من 1 ميغابايت من الذاكرة لكل قطار ، مما يترك هذا - مقابل 10 آلاف مؤشر ترابط ، هناك حاجة إلى 10 جيجابايت من ذاكرة الوصول العشوائي وهذا مخصص فقط لمجموعة البيانات. نعم ، ولا تنسوا ، نحن في أوائل الألفية الثانية !!
اليوم ، تعمل أجهزة الكمبيوتر العميلة والخوادم بشكل أسرع وأكثر كفاءة ويمكن لأي لغة برمجة أو إطار عمل تقريبًا التعامل مع هذه المشكلة. ولكن في الواقع ، لم يتم حل المشكلة. بالنسبة إلى 10 ملايين اتصال عميل بجهاز واحد ، تعود المشكلة مرة أخرى (ولكنها الآن مشكلة C10M ).
جافا سكريبت الانقاذ؟
المخربون الحذر
!!!
Node.js في الواقع يحل مشكلة C10K ... ولكن كيف؟
لم تكن جافا سكريبت من جانب الخادم شيئًا جديدًا وغير عادي في أوائل عام 2000 ، ففي ذلك الوقت كانت هناك تطبيقات بالفعل على رأس JVM (آلة جافا الافتراضية) - RingoJS و AppEngineJS ، والتي عملت على نموذج مؤشر ترابط لكل طلب.
ولكن إذا لم يتمكنوا من حل المشكلة ، فكيف يمكن Node.js؟! كل ذلك لأن جافا سكريبت مترابطة .
Node.js وحلقة الحدث
نود.جي إس
Node.js عبارة عن نظام أساسي للخوادم يعمل على محرك Google Chrome - V8 ، والذي يمكنه ترجمة كود JavaScript إلى رمز الجهاز.
يستخدم Node.js نموذجًا قائمًا على الأحداث وهيكلة الإدخال / الإخراج غير المحظورة ، مما يجعله خفيف الوزن وفعال. هذا ليس إطارًا ولا مكتبة ، بل هو وقت تشغيل JavaScript.
دعنا نكتب مثالا صغيرا:
عدم حظر i / o
يستخدم Node.js عمليات الإدخال / الإخراج غير المحظورة ، ماذا يعني هذا:
- لن يتم حظر مؤشر الترابط الرئيسي بواسطة عمليات الإدخال / الإخراج.
- سوف يستمر الخادم في خدمة الطلبات.
- سيكون لدينا للعمل مع رمز غير متزامن .
دعنا نكتب مثالًا يرسل فيه الخادم صفحة HTML استجابةً لطلب /home
، ولكل الطلبات الأخرى - "Hello World". لإرسال صفحة HTML ، يجب عليك أولاً قراءتها من ملف.
home.html
<html> <body> <h1>This is home page</h1> </body> </html>
index.js
const http = require('http'); const fs = require('fs'); const server = http.createServer(function(request, response) { if (request.url === '/home') { fs.readFile(`${ __dirname }/home.html`, function (err, content) { if (!err) { response.setHeader('Content-Type', 'text/html'); response.write(content); } else { response.statusCode = 500; response.write('An error has ocurred'); } response.end(); }); } else { response.write('Hello World'); response.end(); } }); server.listen(8080);
إذا كان عنوان url المطلوب هو /home
، فسيتم استخدام وحدة fs
الأصلية لقراءة ملف home.html
.
الدالات التي تندرج في fs.readFile
و fs.readFile
كحجج رد اتصال . سيتم تنفيذ هذه الوظائف في مرحلة ما في المستقبل (الأولى بمجرد أن يتلقى الخادم الطلب ، والثانية عند قراءة الملف من القرص ووضعه في المخزن المؤقت).
أثناء قراءة الملف من القرص ، يمكن لـ Node.js معالجة الطلبات الأخرى وحتى قراءة الملف مرة أخرى وكل هذا في دفق واحد ... لكن كيف؟!
حلقة الحدث
حلقة الحدث هي السحر الذي يحدث داخل Node.js. هذا هو حرفيا حلقة لا نهاية لها وفعلا موضوع واحد.
Libuv هي مكتبة C تنفذ هذا النمط وهي جزء من Node.js. kernel يمكنك معرفة المزيد عن libuv هنا .
دورة الأحداث لها 6 مراحل ، كل تنفيذ من كل 6 مراحل يسمى علامة .
- مؤقتات : في هذه المرحلة ، يتم تنفيذ عمليات الاسترجاعات المجدولة بواسطة أساليب
setTimeout()
و setInterval()
؛ - عمليات الاسترجاعات المعلقة : يتم تنفيذ جميع عمليات الاسترجاعات تقريبًا ، باستثناء الأحداث
close
setImmediate()
ضبط الوقت و setImmediate()
؛ - الخمول ، وإعداد : تستخدم للأغراض الداخلية فقط ؛
- استطلاع الرأي : مسؤول عن تلقي أحداث I / O جديدة. قد Node.js حظر في هذه المرحلة ؛
- تحقق : يتم تنفيذ عمليات الاسترجاعات الناتجة عن الأسلوب
setImmediate()
في هذه المرحلة ؛ - عمليات رد الاتصال عن قرب : على سبيل المثال
socket.on('close', ...)
؛
حسنًا ، يوجد مؤشر ترابط واحد فقط ، وهذا الخيط عبارة عن حلقة حدث ، ولكن من الذي يقوم بكل عمليات الإدخال / الإخراج؟
انتبه
!!!
عندما تحتاج حلقة حدث إلى إجراء عملية إدخال / إخراج ، فإنها تستخدم مؤشر ترابط نظام التشغيل من تجمع مؤشرات الترابط ، وعند اكتمال المهمة ، يتم وضع قائمة رد الاتصال أثناء مرحلة الاسترجاعات المعلقة .
أليس هذا رائعًا؟
مشكلة المهام كثيفة وحدة المعالجة المركزية
Node.js يبدو مثاليا! يمكنك إنشاء ما تريد.
دعنا نكتب API لحساب الأعداد الأولية.
الرقم الأولي هو رقم صحيح (طبيعي) أكبر من واحد وقسم للقسمة على 1 فقط وفي حد ذاته.
عند إعطاء رقم N ، يجب على API حساب وإرجاع أول أعداد N الأولية في القائمة (أو المصفوفة).
primes.js
function isPrime(n) { for(let i = 2, s = Math.sqrt(n); i <= s; i++) { if(n % i === 0) return false; } return n > 1; } function nthPrime(n) { let counter = n; let iterator = 2; let result = []; while(counter > 0) { isPrime(iterator) && result.push(iterator) && counter--; iterator++; } return result; } module.exports = { isPrime, nthPrime };
index.js
const http = require('http'); const url = require('url'); const primes = require('./primes'); const server = http.createServer(function (request, response) { const { pathname, query } = url.parse(request.url, true); if (pathname === '/primes') { const result = primes.nthPrime(query.n || 0); response.setHeader('Content-Type', 'application/json'); response.write(JSON.stringify(result)); response.end(); } else { response.statusCode = 404; response.write('Not Found'); response.end(); } }); server.listen(8080);
prime.js
هو تطبيق العمليات الحسابية الضرورية: تقوم الدالة isPrime
بالتحقق مما إذا كان الرقم أولي ، ويقوم nthPrime بإرجاع N هذه الأرقام.
ملف index.js
مسؤول عن إنشاء الخادم ويستخدم الوحدة النمطية prime.js
لمعالجة كل طلب /primes
. يتم طرح الرقم N خلال سلسلة الاستعلام في URL.
للحصول على أول 20 أعدادًا أولية ، نحتاج إلى تقديم طلب إلى http://localhost:8080/primes?n=20
.
افترض أن لدينا 3 عملاء يطرقوننا ويحاولون الوصول إلى واجهة برمجة تطبيقات الإدخال / الإخراج غير المحظورة:
- أول استعلامات 5 الأعداد الأولية في كل ثانية.
- الثاني يسأل عن 1000 الأعداد الأولية في كل ثانية
- يطلب الثالث 1000000000 الأعداد الأولية ، ولكن ...
عندما يرسل العميل الثالث طلبًا ، يتم حظر مؤشر الترابط الرئيسي وهذا هو العرض الرئيسي لمشكلة المهام الكثيفة وحدة المعالجة المركزية . عندما يكون مؤشر الترابط الرئيسي مشغولًا بأداء مهمة "ثقيلة" ، فإنه يتعذر الوصول إلى مهام أخرى.
ولكن ماذا عن ليبوف؟ إذا كنت تتذكر ، فهذه المكتبة تساعد Node.js على إجراء عمليات الإدخال / الإخراج باستخدام مؤشرات نظام التشغيل لتجنب عرقلة سلسلة الرسائل الرئيسية وأنت على حق تمامًا ، هذا هو الحل لمشكلتنا ، ولكن لكي يكون ذلك ممكنًا ، يجب أن تتم كتابة الوحدة النمطية لدينا باللغة C ++ حتى libuv يمكن أن تعمل معها.
لحسن الحظ ، بدءًا من الإصدار v10.5 ، تمت إضافة وحدة مؤشرات ترابط Worker الأصلية إلى Node.js.
العمال وتدفقاتهم
كما تخبرنا الوثائق :
العاملون مفيدون في تنفيذ عمليات JavaScript المركزة على وحدة المعالجة المركزية ؛ لا تستخدمها في عمليات الإدخال / الإخراج ، فالآليات المضمنة بالفعل في Node.js تتعامل بشكل أكثر كفاءة مع هذه المهام من مؤشر ترابط العامل.
إصلاح التعليمات البرمجية
حان الوقت لإعادة كتابة التعليمات البرمجية لدينا:
primes-workerthreads.js
const { workerData, parentPort } = require('worker_threads'); function isPrime(n) { for(let i = 2, s = Math.sqrt(n); i <= s; i++) if(n % i === 0) return false; return n > 1; } function nthPrime(n) { let counter = n; let iterator = 2; let result = []; while(counter > 0) { isPrime(iterator) && result.push(iterator) && counter--; iterator++; } return result; } parentPort.postMessage(nthPrime(workerData.n));
index-workerthreads.js
const http = require('http'); const url = require('url'); const { Worker } = require('worker_threads'); const server = http.createServer(function (request, response) { const { pathname, query } = url.parse(request.url, true); if (pathname === '/primes') { const worker = new Worker('./primes-workerthreads.js', { workerData: { n: query.n || 0 } }); worker.on('error', function () { response.statusCode = 500; response.write('Oops there was an error...'); response.end(); }); let result; worker.on('message', function (message) { result = message; }); worker.on('exit', function () { response.setHeader('Content-Type', 'application/json'); response.write(JSON.stringify(result)); response.end(); }); } else { response.statusCode = 404; response.write('Not Found'); response.end(); } }); server.listen(8080);
في index-workerthreads.js
، يقوم كل طلب بـ /primes
index-workerthreads.js
بإنشاء مثيل لفئة Worker
(من الوحدة النمطية worker_threads
الأصلية) لتحميل وتنفيذ primes-workerthreads.js
في مؤشر ترابط العامل. عندما يتم حساب قائمة الأعداد الأولية وجاهزة ، يتم تشغيل حدث message
- تقع النتيجة في الدفق الرئيسي بسبب عدم ترك العامل للعمل ، كما يقوم أيضًا بتشغيل حدث exit
، مما يسمح للتيار الرئيسي بإرسال البيانات إلى العميل.
primes-workerthreads.js
تغيرت قليلاً. يقوم باستيراد workerData
(هذه نسخة من المعلمات التي تم تمريرها من الخيط الرئيسي) و parentPort
التي يتم من خلالها parentPort
نتيجة عمل العامل إلى الخيط الرئيسي.
الآن دعونا نجرب مثالنا مرة أخرى ونرى ما سيحدث:
لم يعد الخيط الرئيسي محظورًا
!!!!!
الآن كل شيء يعمل كما يجب ، ولكن لا يزال إنتاج العمال دون سبب ممارسة جيدة ؛ إنشاء خيوط ليست متعة رخيصة. تأكد من إنشاء تجمع مؤشرات ترابط قبل هذا.
استنتاج
Node.js هي تقنية قوية يجب استكشافها كلما أمكن ذلك.
توصيتي الشخصية - دائما تكون غريبة! إذا كنت تعرف كيف يعمل شيء ما من الداخل ، فيمكنك التعامل معه بكفاءة أكبر.
هذا كل شيء للرجال اليوم. آمل أن يكون هذا المنشور مفيدًا لك وأنك تعلمت شيئًا جديدًا حول Node.js.
شكرا لقراءة ورؤيتك في المشاركات القادمة.
.