عند تشغيل أي برنامج في نود Node.js ستعمل نسخة منه افتراضيًا ضمن عملية process واحدة في نظام التشغيل، وسيُنفذ فيها البرنامج ضمن خيط معالجة thread وحيد، وكما تعلمنا في المقال الخامس طرق كتابة شيفرات غير متزامنة التنفيذ في Node.js من هذه السلسلة فإن تنفيذ البرنامج ضمن خيط وحيد ضمن العملية سيؤدي لأن تعيق العمليات التي تحتاج مدة طويلة لتنفيذها في جافاسكربت تنفيذ العمليات أو الشيفرات التي تليها ضمن خيط التنفيذ لحين انتهاءها، وهنا يأتي دور إنشاء عملية ابن child process منفصلة عن العملية الرئيسية، وهي عملية تُنشئها عملية أخرى وتُستخدم لتنفيذ المهام الطويلة، وبهذه الطريقة يمكن لنظام التشغيل تنفيذ كلا العمليتين الأب والابن معًا أو بنفس الوقت على التوازي دون أن يعيق أي منهما تنفيذ الآخر.
توفر نود لذلك الغرض الوحدة البرمجية child_process
التي تحتوي على توابع عدة تساعد في إنشاء عمليات جديدة، وحتى توابع للتعامل مع نظام التشغيل مباشرةً وتنفيذ الأوامر ضمن الصدفة shell، لذا يمكن لمسؤولي إدارة النظام الاستفادة من نود في تنفيذ أوامر الصدفة لإدارة نظام التشغيل وترتيب تلك الأوامر ضمن وحدات برمجية بدلًا من تنفيذ ملفات أوامر الصدفة مباشرةً.
سنتعلم في هذا المقال طرق إنشاء عمليات أبناء بتطبيق عدة أمثلة حيث سننشئ تلك العمليات بالاستعانة بالوحدة البرمجية child_process
ونعاين نتيجة تنفيذها على شكل مخزن مؤقت buffer أو سلسلة نصية باستخدام التابع exec()
، وسنتعلم كيف يمكن قراءة نتيجة تنفيذ تلك العملية من مجرى للبيانات data stream باستخدام التابع spawn()
، ثم سننفذ برنامج نود آخر ضمن عملية منفصلة باستخدام fork()
ونتعلم طريقة التواصل معه أثناء تشغيله، وسنطبق هذه الأفكار على مثال لبرنامج مهمته عرض قائمة محتويات مجلد ما، وبرنامج آخر للبحث عن الملفات، وآخر لخادم ويب يدعم عدة مسارات فرعية.
المستلزمات
هذا المقال جزء من سلسلة دليل تعلم Node.js لذا يجب قبل قراءته:
- تثبيت نسخة نود على الجهاز، حيث استخدمنا في هذا المقال الإصدار رقم 10.22.0.
- معرفة عن طريقة عمل خوادم الويب في نود، ويمكنك مراجعة مقال السابع إنشاء خادم ويب في Node.js من هذه السلسلة.
إنشاء عملية ابن باستخدام exec
عادة ما ننشئ عملية ابن لتنفيذ بعض الأوامر ضمن نظام التشغيل، فمثلًا للتعديل على خرج برنامج ما في نود بعد تنفيذه ضمن الصدفة نمرر خرج ذلك البرنامج أو نعيد توجيهه إلى أمر آخر، وهنا يأتي دور التابع exec()
الذي يمكننا من إنشاء عملية صدفة جديدة بنفس الطريقة وتنفيذ الأوامر ضمنها لكن من قبل برنامج نود، حيث يُخزَّن خرج ذلك الأمر ضمن مخزن مؤقت في الذاكرة ويمكننا بعدها الوصول إليه بتمرير دالة رد نداء callback function للتابع exec()
.
لنبدأ بإنشاء عملية ابن جديدة في نود ولكن أولًا ننشئ مجلد جديد سيحتوي على البرامج التي سنعمل عليها في هذا المقال بالاسم child-processes
كالتالي:
$ mkdir child-processes
وندخل إلى المجلد:
$ cd child-processes
ننشئ ملف جافاسكربت جديد بالاسم listFiles.js ونفتحه باستخدام أي محرر نصوص:
$ nano listFiles.js
سنستخدم في هذه الوحدة البرمجية التابع exec()
لتنفيذ أمر عرض الملفات والمجلدات ضمن المجلد الحالي ls
، ومهمة برنامجنا هو قراءة خرج ذلك الأمر وعرضه للمستخدم، لذا نضيف الشيفرة التالية:
const { exec } = require('child_process'); exec('ls -lh', (error, stdout, stderr) => { if (error) { console.error(`error: ${error.message}`); return; } if (stderr) { console.error(`stderr: ${stderr}`); return; } console.log(`stdout:\n${stdout}`); });
بدأنا باستيراد التابع exec()
من الوحدة child_process
، ثم استدعيناه بتمرير الأمر الذي نريد تنفيذه كمعامل أول، وهو الأمر ls -lh
الذي سيعرض كافة الملفات والمجلدات الموجودة ضمن المجلد الحالي بصيغة مفصلة، وسيعرض وحدة الحجم للملفات بصيغة مقروءة، وسيعرض أيضًا الحجم الكلي لها في أول سطر من الخرج.
والمعامل الثاني المُمرر هو دالة رد النداء تقبل ثلاث معاملات، الأول كائن الخطأ error
والثاني الخرج القياسي stdout
والثالث خرج الخطأ stderr
، فإذا فشل تنفيذ الأمر سيحتوي المعامل error
على كائن خطأ يشرح سبب حدوثه مثلًا عندما لا تعثر الصدفة على الأمر الذي نحاول تنفيذه، وإذا نُفذ الأمر بنجاح سيحتوي المعامل الثاني stdout
على البيانات التي تكتب في مجرى الخرج القياسي، أما المعامل الثالث stderr
سيمثل مجرى الخطأ القياسي ويحوي على أي بيانات يكتبها الأمر إلى ذلك المجرى.
ملاحظة: يوجد فرق بين كائن الخطأ error
ومجرى الخطأ stderr
، فإذا فشل تنفيذ الأمر كليًا سيمثل المعامل error
ذلك الخطأ، بينما إذا نُفذ الأمر وكتب هو إلى مجرى الخطأ فيمكننا قراءة أي بيانات تكتب فيه من المعامل stderr
، ويفضل دومًا معالجة كل احتمالات الخرج الممكنة من كلا هذين المعاملين مع أي عملية ابن.
نتحقق داخل دالة رد النداء الممررة من وجود أي خطأ أولًا، فإذا وُجد خطأ سنطبع رسالة الخطأ message
وهي الخاصية ضمن كائن الخطأ Error
باستدعاء أمر طباعة الخطأ console.error()
، ثم ننهي تنفيذ التابع مباشرةً باستخدام return
، وبعدها نتحقق من طباعة الأمر لأي أخطاء تُكتَب ضمن مجرى الخطأ القياسي وإذا وجد نطبع الرسالة وننهي تنفيذ التابع باستخدام return
أيضًا، وإلا يكون الأمر قد نُفِّذ بنجاح، ونطبع حينها الخرج إلى الطرفية باستخدام console.log()
.
والآن نخرج من الملف ثم ننفذ البرنامج ونعاين النتيجة، وفي حال كنت تستخدم محرر النصوص نانو nano كما في أمثلتنا يمكنك الخروج منه بالضغط على الاختصار CTRL+X، ولتشغيل البرنامج ننفذ الأمر node
كالتالي:
$ node listFiles.js
نحصل على الخرج:
stdout: total 4.0K -rw-rw-r-- 1 hassan hassan 280 Jul 27 16:35 listFiles.js
وهو محتوى المجلد child-processes مع تفاصيل عن الملفات الموجودة ضمنه، وحجم المجلد الكلي في السطر الأول، وهو ما يدل على تنفيذ البرنامج listFiles.js للأمر ls -lh
ضمن الصدفة وقراءة نتيجته وطباعتها بنجاح.
والآن سنتعرف على طريقة مختلفة لتنفيذ عملية ما على التوازي مع العملية الحالية، حيث توفر الوحدة child_process
التابع execFile()
الذي يُمكننا من تشغيل الملفات التنفيذية، والفرق بينه وبين الأمر exec()
أن المعامل الأول المُمرر له سيكون مسار الملف التنفيذي الذي نريد تشغيله بدلًا من أمر يراد تنفيذه في الصدفة، وبطريقة مشابهة لعمل التابع exec()
سيُخزن ناتج التنفيذ ضمن مخزن مؤقت يمكننا الوصول إليه ضمن دالة رد النداء الممررة، والتي تقبل المعاملات الثلاث نفسها error
و stdout
و stderr
.
ملاحظة: يجب الانتباه أنه لا يمكن تشغيل الملفات التنفيذية ذات الصيغ .bat
و .cmd
على ويندوز، وذلك لأن التابع execFile()
لا ينشئ الصدفة التي تحتاج إليها تلك الملفات لتشغيلها، بينما على الأنظمة مثل يونكس و لينكس و نظام ماك لا تحتاج الملفات التنفيذية إلى صدفة لتشغيلها، لذا لتنفيذ الملفات التنفيذية على ويندوز يمكن استخدام التابع exec()
لأنه سيُنشئ لها صدفة عند التنفيذ، أو يمكن استدعاؤها باستخدام التابع spawn()
وهو ما سنتعرف عليه لاحقًا، ولكن الملفات التنفيذية ذات اللاحقة .exe
يمكن تشغيلها ضمن ويندوز باستخدام execFile()
مباشرةً، حيث أنها لا تحتاج لصدفة لتشغيلها.
والآن نبدأ بإنشاء الملف التنفيذي الذي سنحاول تنفيذه باستخدام execFile()
، حيث سنكتب نصًا برمجيًا ضمن صدفة باش bash مهمته تنزيل صورة شعار بيئة نود من الموقع الرئيسي لها، ثم يعيد ترميز صورة الشعار تلك بصيغة Base64 لنتعامل معها كسلسلة نصية بمحارف ASCII، ونبدأ بإنشاء ملف تنفيذي جديد بالاسم processNodejsImage.sh:
$ nano processNodejsImage.sh
ونضيف إليه الشيفرة التالية لتحميل وتحويل صورة الشعار:
#!/bin/bash curl -s https://nodejs.org/static/images/logos/nodejs-new-pantone-black.svg > nodejs-logo.svg base64 nodejs-logo.svg
التعليمة في السطر الأول تسمى شِبانغ shebang، وتستخدم ضمن أنظمة يونكس ولينكس ونظام ماك لتحديد الصدفة التي نريد تشغيل النص البرمجي أو السكربت ضمنها، والتعليمة التالية هي الأمر curl
وهي أداة سطر أوامر تمكننا من نقل البيانات من وإلى الخوادم، ويمكننا الاستفادة منها لتنزيل شعار نود من الموقع الرئيسي له، ثم نعيد توجيه الخرج لحفظ الصورة بعد تنزيلها إلى ملف بالاسم nodejs-logo.svg، أما التعليمة الأخيرة تستخدم الأداة base64
لإعادة ترميز محتوى ملف الشعار nodejs-logo.svg الذي نزلناه سابقًا، ثم سيُطبَع نتيجة الترميز إلى الطرفية أي مجرى الخرج القياسي وهو خرج تنفيذ النص البرمجي هذا بالكامل.
والآن نحفظ الملف ونخرج منه ونضيف إذن تنفيذ هذا النص البرمجي لكي نستطيع تنفيذه كالتالي:
$ chmod u+x processNodejsImage.sh
يمنح هذا الأمر المستخدم الحالي صلاحية التنفيذ لذلك الملف.
يمكننا الآن البدء بكتابة برنامج نود الذي سيُنفذ ذلك النص البرمجي باستخدام التابع execFile()
ضمن عملية ابن منفصلة ثم طباعة خرج التنفيذ، لذا نُنشئ ملف جافاسكربت جديد بالاسم getNodejsImage.js:
$ nano getNodejsImage.js
ونكتب الشيفرة التالية:
const { execFile } = require('child_process'); execFile(__dirname + '/processNodejsImage.sh', (error, stdout, stderr) => { if (error) { console.error(`error: ${error.message}`); return; } if (stderr) { console.error(`stderr: ${stderr}`); return; } console.log(`stdout:\n${stdout}`); });
استوردنا التابع execFile()
من الوحدة child_process
واستدعيناه بتمرير مسار ملف النص البرمجي، حيث استفدنا من قيمة الثابت __dirname
الذي توفره نود للحصول على مسار المجلد الحالي الذي يحتوي على النص البرمجي، وبذلك يمكن للبرنامج الإشارة إلى النص البرمجي processNodejsImage.sh دومًا مهما كان نظام التشغيل الذي ينفذه أو مكان تنفيذ البرنامج getNodejsImage.js على نظام الملفات، وفي حالتنا يجب أن يكون مكان كل من الملفين getNodejsImage.js و processNodejsImage.sh في نفس المجلد.
أما المعامل الثاني المُمرر هو رد نداء ويقبل ثلاثة معاملات، الأول كائن الخطأ error
والثاني الخرج القياسي stdout
والثالث خرج الخطأ stderr
، وكما فعلنا سابقًا عند استخدام exec()
سنتحقق من حالة وخرج التنفيذ ونطبعها إلى الطرفية.
والآن نحفظ الملف ونخرج من محرر النصوص ثم نشغله باستخدام الأمر node
كالتالي:
$ node getNodejsImage.js
لنحصل على الخرج:
stdout: PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNDQyLjQgMjcwLjkiPjxkZWZzPjxsaW5lYXJHcmFkaWVudCBpZD0iYiIgeDE9IjE4MC43IiB5MT0iODAuNyIge ...
تجاهلنا عرض الخرج كاملًا بسبب حجمه الكبير، ولكن النص البرمجي processNodejsImage.sh نزل الصورة أولًا بعدها أعاد ترميزها بصيغة base64، ويمكن التأكد من ذلك بمعاينة الصورة التي تم تنزيلها والموجودة ضمن المجلد الحالي، ولنتأكد يمكننا تنفيذ البرنامج السابق listFiles.js لمعاينة المحتوى الجديد للمجلد الحالي:
$ node listFiles.js
سنلاحظ ظهور الخرج التالي:
stdout: total 20K -rw-rw-r-- 1 hassan hassan 316 Jul 27 17:56 getNodejsImage.js -rw-rw-r-- 1 hassan hassan 280 Jul 27 16:35 listFiles.js -rw-rw-r-- 1 hassan hassan 5.4K Jul 27 18:01 nodejs-logo.svg -rwxrw-r-- 1 hassan hassan 129 Jul 27 17:56 processNodejsImage.sh
بذلك نكون قد نفذنا بنجاح النص البرمجي processNodejsImage.sh ضمن عملية ابن من برنامج نود باستخدام التابع execFile()
.
تعلمنا في هذه الفقرة كيف يمكن للتابعين exec()
و execFile()
تنفيذ الأوامر ضمن صدفة نظام التشغيل داخل عملية ابن منفصلة في نود، وتوفر نود أيضًا التابع spawn()
والذي يشبه في عمله هذين التابعين، ولكن الفرق في عمله أنه لا يقرأ خرج تنفيذ الأمر دفعة واحدة بل على عدة دفعات ضمن مجرى للبيانات stream، وهو ما سنتعرف عليه بالتفصيل في الفقرة التالية.
إنشاء عملية ابن باستخدام spawn
يمكن استدعاء التابع spawn()
لتنفيذ الأوامر ضمن عملية منفصلة والحصول على بيانات الخرج من ذلك الأمر عن طريق الواجهة البرمجية API لمجرى البيانات في نود، وذلك عبر الاستماع لبعض الأحداث المعينة على كائن المجرى لخرج ذلك الأمر.
مجاري البيانات streams في نود هي نسخة من صنف مرسل الأحداث event emitter الذي تعرفنا عليه بالتفصيل في المقال التاسع من هذه السلسلة وعندما يكون خرج الأمر الذي سننفذه كبير نسبيًا فيفضل استخدام التابع spawn()
بدلًا من التابعينexec()
و execFile()
، وذلك لأن التابعين exec()
و execFile()
سيخزنان خرج الأمر كاملًا ضمن مخزن مؤقت في الذاكرة، ما سيؤثر على أداء النظام، بينما باستعمال المجرى stream يمكننا قراءة البيانات من الخرج ومعالجتها على عدة دفعات، ما يؤدي لخفض استعمال الذاكرة والسماح لنا بمعالجة البيانات الكبيرة.
سنتعرف الآن على طريقة استخدام التابع spawn()
لإنشاء عملية ابن، لذلك نبدأ بكتابة برنامج في نود مهمته تنفيذ أمر البحث عن الملفات find
ضمن عملية ابن لعرض كل الملفات الموجودة ضمن المجلد الحالي، ونبدأ بإنشاء ملف جافاسكربت جديد بالاسم findFiles.js:
$ nano findFiles.js
ونستدعي التابع spawn()
لتنفيذ أمر البحث:
const { spawn } = require('child_process'); const child = spawn('find', ['.']);
بدأنا باستيراد التابع spawn()
من الوحدة child_process
، ثم استدعيناه لإنشاء عملية ابن جديدة يُنفذ ضمنها الأمر find
وخزنّا نتيجة تنفيذ التابع ضمن المتغير child
للاستماع لاحقًا إلى الأحداث الذي ستطلقها العملية الابن، ونلاحظ تمرير الأمر الذي نريد تنفيذه find
كمعامل أول للتابع spawn()
، أما المعامل الثاني فهو مصفوفة من المعاملات التي نريد تمريرها لذلك الأمر، ويكون الأمر النهائي الذي سينفذ هو أمر البحث find
مع تمرير المعامل .
للدلالة على البحث عن كل الملفات الموجودة ضمن المجلد الحالي، أي شكل الأمر المنفذ النهائي هو find .
.
وسابقًا عند استخدام التابعين exec()
و execFile()
مررنا لهما شكل الأمر الذي نريد تنفيذه بصيغته النهائية ضمن السلسلة النصية، أما عند استدعاء spawn()
فيجب تمرير المعاملات للأمر المُنفذ ضمن مصفوفة، وذلك لأن هذا التابع لا يُنشئ صَدفة جديدة قبل إنشاء وتشغيل العملية، أما إذا أردنا تمرير المعاملات مع الأمر بنفس السلسلة النصية يجب إنشاء صَدفة جديدة لتفسر ذلك.
ولنكمل معالجة تنفيذ الأمر بإضافة توابع استماع للخرج كالتالي:
const { spawn } = require('child_process'); const child = spawn('find', ['.']); child.stdout.on('data', data => { console.log(`stdout:\n${data}`); }); child.stderr.on('data', data => { console.error(`stderr: ${data}`); });
كما ذكرنا سابقًا يمكن للأوامر كتابة الخرج على كل من مجرى الخرج القياسي stdout
ومجرى خرج الأخطاء stderr
، لذا يجب إضافة من يستمع لهما على كل مجرى باستخدام التابع on()
ونطبع البيانات التي تُرسل ضمن ذلك الحدث إلى الطرفية.
نستمع بعدها للحدث error
الذي سيُطلق في حال فشل تنفيذ الأمر، والحدث close
الذي سيُطلق بعد انتهاء تنفيذ الأمر وإغلاق المجرى، ونكمل الآن كتابة البرنامج ليصبح كالتالي:
const { spawn } = require('child_process'); const child = spawn('find', ['.']); child.stdout.on('data', (data) => { console.log(`stdout:\n${data}`); }); child.stderr.on('data', (data) => { console.error(`stderr: ${data}`); }); child.on('error', (error) => { console.error(`error: ${error.message}`); }); child.on('close', (code) => { console.log(`child process exited with code ${code}`); });
لاحظ أن الاستماع لكل من الحدثين error
و close
يكون على كائن العملية child
مباشرةً، ولاحظ ضمن حدث الخطأ error
أنه يوفر لنا كائن خطأ Error
يعبر عن المشكلة، وفي تلك الحالة سنطبع رسالة الخطأ message
إلى الطرفية، أما ضمن حدث الإغلاق close
تمرر نود رمز الخروج للأمر بعد تنفيذه، ومنه يمكننا معرفة نجاح أو فشل تنفيذ الأمر، فعند نجاح التنفيذ سيعيد الأمر الرمز صفر 0
وإلا سيعيد رمز خروج أكبر من الصفر.
والآن نحفظ الملف ونخرج منه ثم ننفذ البرنامج باستخدام الأمر node
:
$ node findFiles.js
ونحصل على الخرج:
stdout: . ./findFiles.js ./listFiles.js ./nodejs-logo.svg ./processNodejsImage.sh ./getNodejsImage.js child process exited with code 0
يظهر لنا قائمة بكافة الملفات الموجودة ضمن المجلد الحالي، وفي آخر سطر يظهر رمز الخروج 0
ما يدل على نجاح التنفيذ، ومع أن الملفات ضمن المجلد الحالي قليلة لكن في حال نفذنا نفس الأمر ضمن مجلد آخر قد يظهر لنا قائمة طويلة جدًا من الملفات الموجودة ضمن كل المجلدات التي يمكن للمستخدم الوصول إليها، ولكن وبما أننا استخدمنا التابع spawn()
فلا مشكلة في ذلك حيث سنعالج الخرج بأفضل طريقة ممكنة باستخدام مجاري البيانات بدلًا من تخزين الخرج كاملًا في الذاكرة ضمن مخزن مؤقت.
وبذلك نكون قد تعلمنا طرق إنشاء عملية ابن في نود لتنفيذ الأوامر الخارجية ضمن نظام التشغيل، وتتيح نود أيضًا طريقة لإنشاء عملية ابن لتنفيذ برامج نود أخرى، وذلك باستعمال التابع fork()
وهو ما سنتعرف عليه في الفقرة التالية.
إنشاء عملية ابن باستخدام fork
تتيح نود التابع fork()
المشابه للتابع spawn()
لإنشاء عملية جديدة ابن لتنفيذ برنامج نود ما، وما يميز التابع fork()
عن التوابع الأخرى مثل spawn()
أو exec()
هو إمكانية التواصل بين العملية الأب والابن، إذ إضافة لقراءة خرج الأمر الذي ننفذه باستخدام fork()
يمكن للعملية الأب إرسال رسائل للعملية الابن والتواصل معها، ويمكن للعملية الابن أيضًا التواصل مع العملية الأب بنفس الطريقة.
وسنتعرف في هذا المثال على طريقة إنشاء عملية ابن باستخدام fork()
والاستفادة منها في تحسين أداء التطبيق الذي نطوره، حيث وبما أن البرامج في نود تعمل ضمن عملية واحدة فالمهام التي تحتاج لمعالجة طويلة من من قبل المعالج ستعيق عمل الشيفرات التالية في باقي البرنامج، فمثلًا المهام التي تتطلب تكرار تنفيذ حلقة برمجية ما لمرات عديدة طويلة، أو تفسير ملفات كبيرة من صيغة JSON، حيث ستشكل هذه العمليات عائقًا في بعض التطبيقات وتؤثر على الأداء، فمثلًا لا يمكن لخادم ويب أن تعيق عمله مثل تلك المهام الطويلة، حيث سيمنعه ذلك من استقبال الطلبات الجديدة ومعالجتها لحين الانتهاء من تنفيذ تلك المهام، لذا سنختبر ذلك بإنشاء خادم ويب يحتوي على مسارين الأول سيُنفذ عملية تحتاج لمعالجة طويلة وتعيق عمل عملية نود للخادم، والثاني سيعيد كائن بصيغة JSON يحوي على الرسالة hello
.
لنبدأ بإنشاء ملف جافاسكربت جديد لخادم HTTP بالاسم httpServer.js كالتالي:
$ nano httpServer.js
نبدأ بإعداد الخادم أولًا باستيراد الوحدة البرمجية http
ثم إنشاء تابع استماع لمعالجة الطلبات الواردة، وكائن للخادم وربط تابع الاستماع معه، والآن نضيف الشيفرة التالية إلى الملف:
const http = require('http'); const host = 'localhost'; const port = 8000; const requestListener = function (req, res) {}; const server = http.createServer(requestListener); server.listen(port, host, () => { console.log(`Server is running on http://${host}:${port}`); });
سيكون الخادم متاحًا للوصول على العنوان http://localhost:8000
، والآن سنكتب دالة مهمتها إعاقة عمل الخادم عبر حلقة ستُنفذ لعدد كبير من المرات، ونضيفها قبل التابع requestListener()
كالتالي:
... const port = 8000; const slowFunction = () => { let counter = 0; while (counter < 5000000000) { counter++; } return counter; } const requestListener = function (req, res) {}; ...
وضمن تابع معالجة الطلب requestListener()
سنستدعي تابع الإعاقة slowFunction()
على المسار الفرعي، بينما سنعيد رسالة JSON على المسار الآخر كالتالي:
... const requestListener = function (req, res) { if (req.url === '/total') { let slowResult = slowFunction(); let message = `{"totalCount":${slowResult}}`; console.log('Returning /total results'); res.setHeader('Content-Type', 'application/json'); res.writeHead(200); res.end(message); } else if (req.url === '/hello') { console.log('Returning /hello results'); res.setHeader('Content-Type', 'application/json'); res.writeHead(200); res.end(`{"message":"hello"}`); } }; ...
إذا تواصلنا مع الخادم على المسار الفرعي /total
سيُنفذ تابع الإعاقة slowFunction()
، أما على المسار الفرعي /hello
سنعيد الرسالة التالية بصيغة JSON {"message":"hello"}
، والآن نحفظ الملف ونخرج منه ثم نشغل الخادم باستخدام الأمر node
كالتالي:
$ node httpServer.js
ليظهر لنا الرسالة التالية ضمن الخرج:
Server is running on http://localhost:8000
يمكننا بدء الاختبار الآن ولهذا نحتاج لطرفيتين إضافيتين، ففي الأولى سنستخدم الأمر curl
لإرسال طلب للخادم على المسار /total
لإبطاء الخادم كالتالي:
$ curl http://localhost:8000/total
وضمن الطرفية الثانية نستخدم الأمر curl
لإرسال طلب على المسار الآخر /hello
كالتالي:
$ curl http://localhost:8000/hello
سيعيد الطلب الأول القيمة التالية:
{"totalCount":5000000000}
بينما سيعيد الطلب الثاني القيمة:
{"message":"hello"}
ونلاحظ أن الطلب الثاني للمسار /hello
اكتمل بعد انتهاء معالجة الطلب على المسار /total
، حيث أعاق تنفيذ التابع slowFunction()
معالجة أي طلبات وتنفيذ أي شيفرات على الخادم لحين انتهائه، ويمكننا التأكد من ذلك من خرج طرفية الخادم نفسه حيث نلاحظ ترتيب إرسال الرد على تلك الطلبات:
Returning /total results Returning /hello results
في مثل تلك الحالات يأتي دور التابع fork()
لإنشاء عملية ابن جديدة يمكن توكيل معالجة المهام الطويلة إليها للسماح للخادم بالعمل على معالجة الطلبات الجديدة القادمة دون توقف، وسنطبق ذلك في مثالنا بنقل تابع المهمة الطويلة إلى وحدة برمجية منفصلة، حيث سيستدعيها خادم الويب لاحقًا ضمن عملية ابن منفصلة عند كل طلب إلى المسار الفرعي /total
ويستمع إلى نتيجة التنفيذ.
نبدأ بإنشاء ملف جافاسكربت بالاسم getCount.js سيحوي على التابع slowFunction()
:
$ nano getCount.js
ونضيف داخله ذلك التابع:
const slowFunction = () => { let counter = 0; while (counter < 5000000000) { counter++; } return counter; }
وبما أننا ننوي استدعاء هذا التابع كعملية ابن باستخدام fork()
يمكننا إضافة شيفرة للتواصل مع العملية الأب تعلمه عند انتهاء تنفيذ التابع slowFunction()
، لهذا نضيف الشيفرة التالية التي سترسل رسالة للعملية الأب تحوي على كائن JSON لنتيجة التنفيذ ولإرسالها إلى المستخدم:
const slowFunction = () => { let counter = 0; while (counter < 5000000000) { counter++; } return counter; } process.on('message', (message) => { if (message == 'START') { console.log('Child process received START message'); let slowResult = slowFunction(); let message = `{"totalCount":${slowResult}}`; process.send(message); } });
كما نلاحظ بإمكاننا الوصول للرسائل التي يُنشئها التابع fork()
بين العملية الأب والابن عن طريق القيمة العامة للكائن process
الذي يمثل العملية، حيث يمكننا إضافة مُستمع لحدث إرسال الرسائل message
والتحقق ما إذا كانت الرسالة هي حدث بدء عملية المعالجة START
الذي سيرسلها الخادم عند ورود طلب إلى المسار الفرعي /total
، ونستجيب لتلك الرسالة بتنفيذ تابع المعالجة slowFunction()
ثم ننشئ السلسلة النصية للرد بصيغة JSON والتي تحوي على نتيجة التنفيذ، ثم نستدعي التابع process.send()
لإرسال رسالة للعملية الأب تعلمه بالنتيجة.
والآن نحفظ الملف ونخرج منه ونعود لملف الخادم httpServer.js للتعديل عليه وإضافة استدعاء للتابع slowFunction()
بإنشاء عملية ابن لتنفيذ البرنامج ضمن الملف getCount.js، فنبدأ باستيراد التابع fork()
من الوحدة البرمجية child_process
كالتالي:
const http = require('http'); const { fork } = require('child_process'); ...
ثم نزيل التابع slowFunction()
من هذا الملف بما أننا نقلناه إلى وحدة برمجية منفصلة، ونعدل تابع معالجة الطلبات requestListener()
ليُنشئ العملية الابن كالتالي:
... const port = 8000; const requestListener = function (req, res) { if (req.url === '/total') { const child = fork(__dirname + '/getCount'); child.on('message', (message) => { console.log('Returning /total results'); res.setHeader('Content-Type', 'application/json'); res.writeHead(200); res.end(message); }); child.send('START'); } else if (req.url === '/hello') { console.log('Returning /hello results'); res.setHeader('Content-Type', 'application/json'); res.writeHead(200); res.end(`{"message":"hello"}`); } }; ...
ينتج الآن عن الطلبات الواردة إلى المسار /total
إنشاء عملية ابن باستخدام fork()
، حيث مررنا لهذا التابع مسار وحدة نود البرمجية التي نريد تنفيذها، وهو الملف getCount.js في حالتنا ضمن المجلد الحالي، لهذا استفدنا هذه المرة أيضًا من قيمة المتغير __dirname
، وخزنا قيمة العملية الابن ضمن المتغير child
للتعامل معها.
أضفنا بعدها مستمعًا إلى الكائن child
ليستقبل الرسائل الواردة من العملية الابن، وتحديدًا لاستقبال الرسالة التي سيرسلها تنفيذ الملف getCount.js الحاوية على سلسلة نصية بصيغة JSON لنتيجة تنفيذ حلقة while
، وعند وصول تلك الرسالة نرسلها مباشرة إلى المستخدم كما هي.
ويمكننا التواصل مع العملية الابن باستدعاء التابع send()
من الكائن child
لإرسال رسالة لها، حيث نرسل الرسالة START
التي سيستقبلها البرنامج ضمن العملية الابن لينفذ التابع slowFunction()
داخله استجابة لها.
والآن نحفظ الملف ونخرج منه ونختبر الميزة التي قدمها استخدام fork()
لخادم HTTP بتشغيل الخادم من ملف httpServer.js باستخدام الأمر node
كالتالي:
$ node httpServer.js
وسيظهر لنا الخرج التالي:
Server is running on http://localhost:8000
وكما فعلنا سابقًا لاختبار عمل الخادم سنحتاج لطرفيتين، ففي الأولى سنستخدم الأمر curl
لإرسال طلب للخادم على المسار /total
والذي سيحتاج بعض الوقت للاكتمال:
$ curl http://localhost:8000/total
وضمن الطرفية الثانية نستخدم الأمر curl
لإرسال طلب على المسار الآخر /hello
والذي سيرسل لنا الرد هذه المرة بسرعة:
$ curl http://localhost:8000/hello
سيعيد الطلب الأول القيمة التالية:
{"totalCount":5000000000}
بينما سيعيد الطلب الثاني القيمة:
{"message":"hello"}
نلاحظ الفرق هذه المرة بأن الطلب للمسار /hello
تم بسرعة، ويمكننا التأكد من ذلك أيضًا من الرسائل الظاهرة في طرفية الخادم:
Child process received START message Returning /hello results Returning /total results
حيث يظهر أن الطلب على المسار /hello
تم استقباله بعد إنشاء العملية الابن ومعالجته قبل انتهاء عملها، وهذا بسبب نقل العملية التي تأخذ وقتا طويلًا إلى عملية ابن منفصلة واستدعائها باستخدام fork()
، حيث بقي الخادم متفرغًا لمعالجة الطلبات الجديدة الواردة وتنفيذ شيفرات جافاسكربت، وهذا بفضل الميزة الذي يوفرها التابع fork()
من إرسال الرسائل إلى العملية الابن للتحكم بتنفيذ العمليات ضمنها، وإمكانية قراءة البيانات المُرسلة من قبل العملية لمعالجتها ضمن العملية الأب.
ختامًا
تعرفنا في هذا المقال على طرق مختلفة لإنشاء عملية ابن في نود، حيث تعلمنا كيف يمكن استخدام التابع exec()
لإنشاء عملية ابن جديدة لتنفيذ أوامر الصدفة من قبل شيفرة برنامج نود، وبعدها تعرفنا على التابع execFile()
الذي يُمكننا من تشغيل الملفات التنفيذية، ثم تعرفنا على التابع spawn()
الذي يسمح بتنفيذ الأوامر وقراءة نتيجتها عبر مجرى للبيانات دون إنشاء صدفة لها كما يفعل التابعان exec()
و execFile()
، وأخيرًا تعرفنا على التابع fork()
الذي يسمح بالتواصل بين العملية الأب والابن.
ويمكنك الرجوع إلى التوثيق الرسمي للوحدة البرمجية child_process
من نود للتعرف عليها أكثر.
ترجمة -وبتصرف- للمقال How To Launch Child Processes in Node.js لصاحبه Stack Abuse.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.