اذهب إلى المحتوى

التعامل مع العمليات الأبناء Child Process في Node.js


Hassan Hedr

عند تشغيل أي برنامج في نود 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.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...