عبد الصمد العماري

الأعضاء
  • المساهمات

    30
  • تاريخ الانضمام

  • تاريخ آخر زيارة

السُّمعة بالموقع

3 Neutral
  1. لقد ركزنا في المقام الأول حتى هذه اللحظة على الجوانب الوظيفية للشبكات. ومع ذلك، من المتوقع أيضًا أن تعمل شبكات الحواسيب، مثل أي نظام حاسوبي، بصورة جيّدة. وذلك لأن فعالية الحوسبة الموزعة عبر الشبكة غالبًا ما تعتمد بصفة مباشرة على الكفاءة التي توفر الشبكة بها البياناتِ الحوسبية. ورغم أنّ العبارة المأثورة للبرمجة القديمة "احصل عليه بصورةٍ صحيحةٍ أولًا، بعد ذلك اجعله سريعًا" تظلّ صحيحةً، إلا أنه من الضروري في مجال الشبكيات أن "تُصمّم من أجل الأداء الفعّال". لذلك من المهمّ فهم العوامل المختلفة التي تؤثر على أداء الشبكة: حيز النطاق التراسلي (Bandwidth) ووقت الاستجابة (Latency) يُقاس أداء الشبكة بطريقتين أساسيتين: حيز النطاق التراسلي (bandwidth) ويسمى أيضًا الإنتاجية (throughput)، ووقت الاستجابة (latency) ويسمى أيضًا التأخير (delay). يعبّر عن حيز نطاق الشبكة التراسلي بعدد البِتات التي يمكن إرسالها عبر الشبكة في فترة زمنية معيّنة. على سبيل المثال، قد يكون للشبكة حيز نطاق تراسلي يبلغ 10 مليون بت في الثانية (Mbps)، مما يعني أنها قادرة على توصيل 10 مليون بِت في الثانية. من المفيد أحيانًا التفكير في حيز النطاق التراسلي من حيث المدة التي يستغرقها إرسال كل جزء من البيانات. يستغرق 0.1 ميكروثانية (μs) لإرسال كل بِت على شبكة بسرعة 10 ميجابت في الثانية على سبيل المثال. حيز النطاق التراسلي والإنتاجية هما مصطلحان مختلفان اختلافًا دقيقًا. بدايةً، حيز النطاق التراسلي هو بالمعنى الحرفي قياسٌ لعرض نطاق التردد. على سبيل المثال، كانت خطوط الهاتف القديمة من الدرجة الصوتية تدعم نطاق تردد يتراوح بين 300 إلى 3300 هرتز؛ وكان يقال أنّ حيز النطاق التراسلي هو : 3300 هرتز - 300 هرتز = 3000 هرتز. إذا رأيت مصطلح حيز النطاق التراسلي (bandwidth) مستخدمًا في حالةٍ يقاس فيها بالهرتز، فمن الراجح أنه يشير إلى نطاق الإشارات التي يمكن استيعابها. عندما نتحدث عن حيز النطاق التراسلي لرابط اتصالٍ، فإننا نشير عادةً إلى عدد البِتات في الثانية التي يمكن إرسالها على الرابط. يُسمى هذا أحيانًا أيضًا معدّل البيانات (data rate). يمكن أن نقول أنّ حيز النطاق التراسلي لرابطٍ إثيرنت هو 10 ميجابت في الثانية. ومع ذلك، نستطيع أيضًا أن نميّز على نحو مفيدٍ بين الحدّ الأقصى لمعدل البيانات المتاح على الرابط وعدد البِتات في الثانية التي يمكننا بالفعل إرسالها عبر الرابط من الناحية العملية. نميل إلى استخدام كلمة الإنتاجية للإشارة إلى الأداء المُقَاس للنظام. وبالتالي، وبسبب أوجه القصور المختلفة في التنفيذ، قد يحقّق زوج من العقد متصل عبر رابط بجيز نطاق تراسلي يبلغ 10 ميجابت في الثانية إنتاجيةً لا تتجاوز 2 ميجابت في الثانية فقط. وهذا يعني أن التطبيق على أحد المضيفين يمكن أن يرسل البيانات إلى المضيف الآخر بسرعة 2 ميجابت في الثانية. نتحدث غالبًا، في النهاية، عن ما نسمّيه متطلبات حيز النطاق التراسلي لتطبيقٍ ما، وهو عدد البِتات في الثانية التي تحتاج إلى إرسالها عبر الشبكة من أجل أداء مقبولٍ. بالنسبة لبعض التطبيقات، قد يكون هذا "كل ما يمكننا الحصول عليه"؛ وبالنسبة لبعض التطبيقات الأخرى، قد يكون عددًا ثابتًا (ويفضل ألا يزيد عن حيز النطاق التراسلي المتوفر)؛ وبالنسبة لبقية التطبيقات، قد يكون عددًا يختلف باختلاف الزمن. وسوف نقدّم المزيد حول هذا الموضوع لاحقًا في هذا القسم. رغم أنّنا نستطيع أن نتكلّم عن حيز النطاق التراسلي للشبكة بأكملها، فإنّنا في بعض الأحيان نرغب أن نكون أكثر دقة بالتركيز مثلًا على حيز النطاق التراسلي لرابط فيزيائي واحدٍ أو قناة منطقية من عمليةٍ لعملية. على المستوى الفيزيائي، يتحسّن حيز النطاق التراسلي باستمرار، وبلا نهاية في الأفق. بصورة بديهية، إذا نظرت إلى ثانية من الزمن على أنّها مسافة يمكنك قياسها باستخدام المسطرةِ وإلى حيز النطاق التراسلي على أنّه عدد البِتات التي تتناسب مع هذه المسافة، فيمكنك التفكير في كل بِت على شكل نبضةٍ ذات عرض معيّن. على سبيل المثال في رابطٍ 1 ميجابت في الثانية كما في القسم (أ) من الشكل الآتي، وسيكون عرض كل بِت هو 1 ميكرو ثانية، بينما في رابطٍ 2 ميجابت في الثانية كما في القسم (ب) من الشكل الآتي، وسيكون عرض كل بت هو 0.5 ميكرو ثانية. كلما كانت تقنية الإرسال والاستقبال أعقد، كلما أصبح كل بِت أضيق، وبالتالي كلما زاد حيز النطاق التراسلي. بالنسبة للقنوات المنطقية من عملية لعملية، يتأثر جيز النطاق التراسلي أيضًا بعوامل أخرى، بما في ذلك عدد المرات التي يجب على البرنامج الذي ينفّذ القناة التعاملَ فيها مع كل بِت من البيانات، وربما تحويله. يتوافق مقياس الأداء الثاني، أي وقت الاستجابة (latency)، مع المدّة التي تستغرقها رسالةٌ للانتقال من أحد طرفي الشبكة إلى الطرف الآخر. (وكما هو الحال مع حيز النطاق التراسلي، يمكن أن نركّز على وقت الاستجابة على رابطٍ واحدٍ أو قناة من طرف لطرف)، حيث يُقاس وقت الاستجابة بدقّة نسبة إلى الزمن. على سبيل المثال، يمكن أن يكون وقت الاستجابة لشبكةٍ عابرةٍ للقارات 24 ميلي ثانية (ms)؛ وهذا يعني أنّ الرسالة تستغرق 24 ميلي ثانية للانتقال من أحد ساحلي أمريكا الشمالية إلى الساحل الآخر. هناك العديد من المواقف التي يكون فيها مهمًّا للغاية معرفةُ الوقت المستغرق لإرسال رسالة من أحد طرفي الشبكة إلى الطرف الآخر والعكس كذلك عوض وقت الاستجابة في اتجاه واحدٍ. نسمي ذلك وقت الذهاب والإياب (round-trip time أو اختصارًا RTT) للشبكة. غالبًا ما ننظر إلى وقت الاستجابة على أنه يتكوّن من ثلاث عناصر. العنصر الأول هو تأخير انتشار سرعة الضوء، ويحدث هذا التأخير لأنه لا يوجد شيء، بما في ذلك بِتٌ يتنقل عبر سلكٍ، يمكنه أن يتجاوز سرعة الضوء. إذا كنت تعرف المسافة بين نقطتين، فيمكنك حساب وقت الاستجابة لسرعة الضوء، ولكن يجب عليك توخي الحذر لأن الضوء ينتقل عبر وسائط مختلفة بسرعات مختلفة: فهو ينتقل بسرعة 3.0 × 108 m/s في الفراغ، وبسرعة 2.3 × 108 m/s في الكابل النحاسي و 2.0 × 108 m/s في الألياف البصرية. العنصر الثاني هو مقدار الوقت المستغرق لإرسال وحدة بيانات، وهو تناسبٌ بين حيز النطاق التراسلي للشبكة وحجم الرزمة التي تُنقَل البيانات فيها. أمّا العنصر الأخير فهو التأخير الذي قد يكون في طابورٍ داخل الشبكة نظرًا لأن المبدّلات تحتاج عمومًا إلى تخزين الرزَم لبعض الوقت قبل إعادة توجيهها على رابطٍ صادرٍ. وعلى أساس ما سبق، يمكننا تحديد وقت الاستجابة الإجمالي على النّحو التالي : تُعبِّر المسافة Distance هنا عن طول السلك الذي ستنتقل عليه البيانات، وسرعة الضوء SpeedOfLight هي السرعة الفعلية للضوء على هذا السلك، والحجم هو حجم Size رزمة البيانات، وحيز النطاق التراسلي Bandwidth هو حيز النطاق التراسلي الذي تُرسَل الرزمة عليه. لاحظ أنه إذا كانت الرسالة تحتوي على بِتٍ واحدٍ فقط وكنّا نتحدث عن رابط واحد (وليس الشبكة بأكملها)، فإن عنصري الإرسال Transmit والطابور Queue لا تكون لهما قيمة، وعليه يُوافق وقتُ الاستجابة تأخيرَ الانتشار فحسب. يُحدّد مفهوما حيز النطاق التراسلي ووقت الاستجابة معًا خصائصَ الأداء لرابط أو قناة معينة. ومع ذلك، تعتمد أهميتهما النسبية على التطبيق. فبالنسبة لبعض التطبيقات، يهيمن وقت الاستجابة على حيز النطاق التراسلي. على سبيل المثال، العميل الذي يرسل رسالةً مؤلفة من بايتٍ واحد إلى خادومٍ ويتلقى رسالةً مؤلفة من بايت واحدٍ في المقابل يكون محدودًا في وقت الاستجابة. إذا افترضنا أنه لا توجد حوسبةٌ حقيقية في عملية إعداد الرّد، فإن التطبيق سيعمل بصورة مختلفة كثيرًا على قناة عابرة للقارات بزمنٍ RTT يساوي 100 ميلي ثانية مقارنةً بقناةٍ عبر الغرفة بزمنٍ RTT يساوي 1 ميلي ثانية. سواءٌ كانت القناة 1 ميجابت في الثانية أو 100 ميجابت في الثانية فهذا غير مهمّ نسبيًا، رغم أنّ الأوّل يشير إلى أن وقت إرسال البايت Transmit هو 8 ميكرو ثانية والأخير يشير إلى وقت إرسالٍ Transmit هو 0.08 ميكرو ثانية. في المقابل، لنأخذ برنامج مكتبة رقمية يُطلب منه إحضار صورة بحجم 25 ميجا بايت، كلّما زاد حيز النطاق التراسلي المتوفر، كلما زادت قدرته على إرسال الصورة إلى المستخدم بصورةٍ أسرع. في هذه الحالة، يهيمن حيز النطاق التراسلي للقناة على الأداء. لرؤية هذا الأمر، نفترض أن القناة لديها حيز نطاق تراسلي يبلغ 10 ميجابت في الثانية، سيستغرق الأمر 20 ثانية لإرسال الصورة (25×106×8 بِت) / (10×106 بِت في الثانية) = 20 ثانية، ولهذا لن يكون الأمر ذا أهمّية نسبيًا سواءٌ كانت الصورة على الجانب الآخر من قناة 1 ميلي ثانية أو من قناةٍ 100 ميلي ثانية، لأن الفرق بين زمن استجابة 20.001 ثانية وزمن استجابة 20.1 ثانية لا يكاد يذكر. يُعطي الشكل التالي فكرةً عن الكيفية التي يمكن أن يهيمن بها وقت الاستجابة أو حيز النطاق التراسلي على الأداء في ظروف مختلفة. ويوضح الرسم البياني المدة التي يستغرقها نقل الكائنات ذات الأحجام المختلفة (1 بايت، 2 كيلوبايت، 1 ميجابايت) عبر الشبكات بأزمنةِ RTT تتراوح من 1 إلى 100 ميلي ثانية وسرعات روابط تبلغ 1.5 أو 10 ميجابت في الثانية. ونستخدم المقاييس اللوغاريتمية لإظهار الأداء النسبي. بالنسبة لكائن 1 بايت (ضغطةٌ على لوحة المفاتيح مثلًا)، يظل وقت الاستجابة مساويًا تمامًا تقريبًا لزمن RTT، إذ لا يمكنك التمييز بين شبكة 1.5 ميجابت في الثانية وشبكة 10 ميجابت في الثانية. بالنسبة لكائنٍ 2 كيلوبايت (على سبيل المثال مثلًا) ، فإن سرعة الرابط تحدث فرقًا كبيرًا على شبكةٍ ذات زمن RTT يبلغ 1 ميلي ثانية ولكن الفرق لا يذكر على شبكةٍ ذات زمن RTT يساوي 100 ميلي ثانية. وبالنسبة لكائن 1 ميجابايت (صورة رقمية مثلًا)، فإن RTT لا يشكّل أيّ فرقٍ، إنها سرعة الرابط هي التي تسيطر على الأداء عبر المجال الكامل لزمن RTT. لاحظ أنه في هذا الكتاب نستخدم المصطلحين وقت الاستجابة والتأخير بصورة عامة للإشارة إلى الوقت الذي يستغرقه أداء وظيفة معينة، مثل توصيل رسالة أو نقل كائن. عندما نشير إلى مقدار الوقت المحدد الذي يستغرقه إرسال إشارة تنتشر من أحد طرفي الرابط إلى طرف آخر، فإننا نستخدم مصطلح تأخير الانتشار (propagation delay). ونوضح كذلك في سياق الحديث ما إذا كنا نشير إلى وقت الاستجابة في اتجاه واحدٍ أو وقت ذهاب وإياب. نشير هنا كذلك إلى أنّ أجهزة الحاسوب أصبحت سريعةً جدًا لدرجة أنه عندما نربطها بالشبكات، يكون من المفيد أحيانًا التفكير، مجازيًا على الأقل، بما يمكن أن نسمّيه التعليمات لكلّ ميل. خُذ ما يحدث عندما يرسل جهاز حاسوب قادر على تنفيذ 100 مليار تعليمة في الثانية رسالةً على قناة ذات زمن RTT هو 100 ميلي ثانية. (لتسهيل العملية الحسابية، افترض أن الرسالة تغطي مسافة 5000 ميل). إذا كان هذا الحاسوب يبقى في وضع الخمول خلال المدّة 100 ميلي ثانية انتظارًا لرسالة الرّد، فإنّه ضيّع الفرصة لتنفيذ 10 مليار تعليمة، أو 2 مليون تعليمة لكل ميل. وكان من الأفضل بالنّسبة له الخروج من الشبكة لتجاوز هذا الهدر. جداء التأخير في حيز النطاق التراسلي (Delay × Bandwidth Product) من المفيد أيضًا التحدث عن جداء هذين المقياسين، الذي يسمى غالبًا جداء حيز النطاق التراسلي في وقت الاستجابة (delay × bandwidth product). بصورة بديهية، إذا نظرنا إلى قناةٍ بين زوج من العمليات على شكل أنبوب مجوف كما في الشكل التالي، إذ يقابل وقتُ الاستجابة طولَ الأنبوب ويمثّل قطرُ الأنبوب حيزَ النطاق التراسلي، فإن جداء حيز النطاق التراسلي في وقت الاستجابة سيعطي حجم الأنبوب، أي الحدّ الأقصى لعدد البِتات التي يمكن أن تمرّ عبر الأنبوب في أي لحظة معينة. يُمكن أن نعبّر عنها بطريقة أخرى، إذا كان وقت الاستجابة (الذي يقاس بالزمن) يتوافق مع طول الأنبوب، فعندما تعطي عرض كل بِت (يقاس أيضًا بالزمن)، فيمكنك حساب عدد البِتات التي ستملأ الأنبوب. على سبيل المثال، يمكن لقناة عابرة للقارات ذات وقت استجابة في اتجاه واحد قدره 50 ميلي ثانية وحيز نطاق تراسلي يبلغ 45 ميجابت في الثانية أن تستوعب ما مقداره: 50 × 10-3 × 45 × 106 bits/sec = 2.25 × 106 bits أو حوالي 280 كيلو بايت من البيانات. وبعبارة أخرى، فإن هذه القناة (الأنبوب) في المثال يمكنها أن تَسَعَ نفس عدد بايتات ذاكرة حاسوب شخصي من فترة أوائل الثمانينات. تُعدّ معرفة جداء حيز النطاق التراسلي في وقت الاستجابة أمرًا مهمًّا عند إنشاء شبكات عالية الأداء لأنه يتوافق مع عدد البِتات التي يجب أن يرسلها المرسل قبل وصول البِت الأول إلى جهاز الاستقبال. إذا كان المُرسِل يتوقع من الجهاز المُستقبِل أن يشير بطريقة ما إلى أن البِتات قد بدأت في الوصول، وكان الأمر يستغرق وقت استجابة آخر تعيد فيه القناة هذه الإشارة إلى المرسل، فيمكن أن يكون هذا الأخير قد أرسل ما يُعادل جداء حيز النطاق التراسلي في الزمن RTT من البيانات قبل أن يسمع من المُستقبِل أن كل شيء على ما يرام. يُقال هنا أن البِتات في الأنبوب "في حالة طيران"، مما يعني أنه إذا طلب المُستقبِل من المُرسِل التوقف عن الإرسال، فقد يتلقى ما مقداره جداء حيز النطاق التراسلي في الزمن RTT من البيانات قبل أن يتمكن المرسل من الاستجابة. في المثال أعلاه، يُعادل هذا المقدار 5.5 × 106 بِت (671 كيلوبايت) من البيانات. من ناحية أخرى، إذا لم يملأ المرسل الأنبوب، أي لم يرسل بيانات تُعادل في مقدارها جداء حيز النطاق التراسلي في الزمن RTT قبل توقّفه لانتظار الإشارة، فإن المرسل لن يكون استخدم الشبكة بالكامل. لاحظ أننا نهتمّ في معظم الوقت بسيناريو RTT، والذي نشير إليه ببساطة على أنه يُعادل جداء حيز النطاق التراسلي في التأخير، دون أن نقول صراحةً أن "التأخير" هو RTT (أي ضِعف التأخير في اتجاه واحد). عادة ما يبيّن السياق هل يعني "التأخير" في الجداء (التأخير×حيز النطاق التراسلي) وقت استجابةٍ أحادي الاتجاه أو زمنًا RTT. ويوضح الجدول التالي بعض الأمثلة على جداءات (حيز النطاق التراسلي×التأخير) لبعض روابط الشبكة النموذجية. نوع الرابط حيز النطاق التراسلي المسافة في اتجاه واحد RTT جداء RTT × حيز النطاق التراسلي شبكة لاسلكية محلية 54 ميجابت في الثانية 50 متر 0.33 ميكرو ثانية 18 بت رابط فضائي 1 جيجابت في الثانية 35,000 كيلومتر 230 ميلي ثانية 230 ميجابت ألياف بصرية عبر الدول 10 جيجابت في الثانية 4,000 كيلومتر 40 ميلي ثانية 400 ميجابت table { width: 100%; } thead { vertical-align: middle; text-align: center;} td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } الشبكات عالية السرعة (High-Speed Networks) تدفع الزيادة المستمرة الملحوظة في حيز النطاق التراسلي مصمّمي الشبكة للتفكير في الذي يحدث في المستوى الحدّي أو، بعبارة أخرى، كيف يُؤثّر وجود حيز نطاق تراسلي لا نهائي متاحٍ على تصميم الشبكة. ورغم أنّ الشبكات عالية السرعة تجلب تغييرًا كبيرًا في حيز النطاق التراسلي المتاح للتطبيقات، إلا أنّ تأثيرها في كثير من النواحي على كيفية تفكيرنا بشأن الشبكات يكون بخصوص ما الذي لا يتغير عند زيادة حيز النطاق التراسلي : وهو سرعة الضوء. على حد تعبير سكوتي في سلسلة الخيال العلمي ستار تريك : "إنّنا لا نستطيع تغيير قوانين الفيزياء". وبعبارة أخرى، لا تعني "السرعة العالية" أن وقت الاستجابة يتحسن بنفس معدّل تحسّن حيز النطاق التراسلي. إنّ الزمن RTT عبر القارات لرابط بسرعة 1 جيجابت في الثانية يساوي 100 ميلي ثانية، وهو نفسُه تمامًا لرابط 1 ميجابت في الثانية. لتقدير أهمية زيادة حيز النطاق التراسلي مقابل وقت الاستجابة الثابت، انظر إلى ما يتطلّبه إرسال ملف حجمه 1 ميجا بايت عبر شبكة 1 ميجابت في الثانية مقابل شبكة 1 جيجابت في الثانية، وكلاهما لهما زمن RTT يساوي 100 ميلي ثانية . في حالة شبكة 1 ميجابت في الثانية، تحتاج إلى 80 مرة ذهابًا وإيابًا لإرسال الملف؛ فخلال كل RTT، يُرسَل 1.25% من حجم الملف. وعلى النقيض، لا يكاد نفس الملف ذو الحجم 1 ميجابايت يشغل قيمة زمن RTT واحدٍ للرابط 1 جيجابت في الثانية، والذي قيمة جداء التأخيرxحيز النطاق الترسلي هي 12.5 ميجا بايت. يوضح الشكل التالي الفرق بين الشبكتين. في الواقع، يبدو ملف 1 ميجا بايت وكأنه تدفقٌ من البيانات التي يجب إرسالها عبر شبكة 1 ميجابت في الثانية، بينما يبدو وكأنه مجرّد رزمة واحدة على شبكة 1 جيجابت في الثانية. لمساعدتك في فهم هذه النقطة، لاحظ أن ملف 1 ميجا بايت لشبكة 1 جيجابت في الثانية يمكنه أن يمثّل رزمة 1 كيلوبايت لشبكة 1 ميجابت في الثانية. هناك طريقة أخرى للنظر إلى هذه المسألة هي أنه يمكن إرسال المزيد من البيانات خلال كل زمنٍ RTT على شبكة عالية السرعة، لدرجة أن زمن RTT واحدًا يصبح مقدارًا كبيرًا من الوقت. وبالتالي، في حين أنك لن تبالي بالفرق بين نقل الملفات الذي يستغرق 101 ضعفٍ لزمن RTT عوض 100 ضعفٍ لزمن RTT (فالفرق النسبي هو 1% فقط)، فإنّ الفرق بين ضعفٍ واحدٍ لزمن RTT و ضعفين لزمن RTT يصبح كبيرًا بصورة مفاجئة، بزيادةٍ بنسبة 100%. وبعبارة أخرى، فإن معيار وقت الاستجابة يبدأ في الهيمنة على تفكيرنا حول تصميم الشبكة عوضَ معيار الإنتاجية. ربما تكون أفضل طريقة لفهم العلاقة بين الإنتاجية ووقت الاستجابة هي العودة إلى الأساسيات. تُعطى الإنتاجية الفعلية من طرف لطرف والتي يمكن تحقيقها عبر شبكة ما من خلال العلاقة البسيطة: إذ لا يتضمن زمن النقل العناصر أحادية الاتجاه المحددة سلفًا في هذا القسم فحسب، بل أيضًا أي وقت إضافي ينقضي في طلب النقل أو إعداده. بصفة عامة، نمثّل هذه العلاقة على النحو: نستخدم في هذه العلاقة لحساب رسالة طلب تُرسَل عبر الشبكة والبيانات التي ترسَل في الردّ عليها. على سبيل المثال، لنأخذ موقفًا يريد فيه المستخدم إحضار ملف 1 ميجا بايت عبر رابطٍ 1 جيجابت في الثانية مع زمن ذهاب وإياب يبلغ 100 ميلي ثانية. يتضمن هذا كلاً من وقت الإرسال لـ 1 ميجا بايت (1 \ 1 جيجابت في الثانية × 1 ميجابايت = 8 ميلي ثانية) وزمن RTT الذي هو 100 ميلي ثانية، بزمن نقل إجمالي يبلغ 108 ميلي ثانية. هذا يعني أن الإنتاجية الفعلية ستكون : 1 ميجا بايت \ 108 ميلي ثانية = 74.1 ميجابت في الثانية، وليس 1 جيجابت في الثانية. بوضوح أكثر، سيساعد نقل كمية أكبر من البيانات على تحسين الإنتاجية الفعلية، إذ سيؤدي حجم النقل الكبير بصورة لا متناهية إلى اقتراب الإنتاجية الفعلية من حيز النطاق التراسلي للشبكة. من ناحية أخرى، فإن الاضطرار لانتظار أكثر من ضعف واحدٍ لزمن RTT، لإعادة إرسال الرزم المفقودة على سبيل المثال، سيؤثر على الإنتاجية الفعلية لأيّ نقل بحجم محدود وسيكون ملحوظًا بصورة أكبر في عمليات النقل الصغيرة. متطلّبات أداء التطبيق الفعال ركّزت المناقشة في هذا القسم بصورة محورية على أداء الشبكة؛ أي أنّ حديثنا انصبّ حول ما سيدعمه رابط أو قناة معينة. وكان الافتراض الذي لم يُفصح عنه هو أن برامج التطبيقات لها احتياجات بسيطة، فهي تريد نفس القدر من حيز النطاق التراسلي الذي يمكن أن توفره الشبكة. وينطبق هذا بالتأكيد على برنامج المكتبة الرقمية المذكور أعلاه والذي يسترجع صورة بحجم 250 ميجا بايت. وهكذا تزداد قدرة البرنامج على إعادة الصورة بصورة أسرع إلى المستخدم بزيادة حيز النطاق التراسلي المتوفر. ومع ذلك، فإن بعض التطبيقات قادرة على تعيين حدّ أعلى لمقدار حيز النطاق التراسلي الذي تحتاجه. تطبيقات الفيديو هي مثال رئيسي. لنفترض أنك ترغب في تدفق مقطع فيديو بحجم ربع شاشة التلفزيون القياسية؛ أي أنه يحتوي على دقة 352×240 بكسل. إذا مثّلنا كل بكسل بـ 24 بِت من المعلومات، كما هو الحال بالنسبة للألوان 24 بِت، فسيكون حجم كل إطار (352 × 240 × 24) / 8 = 247.5 كيلوبايت. إذا كان التطبيق يحتاج إلى دعم معدّل إطارات بمقدار 30 إطارًا في الثانية، فإنه قد يطلب معدل إنتاجية يبلغ 75 ميجابت في الثانية. وهكذا، فإن قدرة الشبكة على توفير المزيد من حيز النطاق التراسلي ليست ذات فائدة لمثل هذا التطبيق لأنه يحتوي فقط على الكثير من البيانات لإرسالها في فترة زمنية معينة. لسوء الحظ، فإن الوضع ليس بهذه البساطة كما يوحي هذا المثال. فنظرًا لأن الفرق بين أي إطارين متجاورين في تدفق فيديو غالبًا ما يكون صغيرًا، فمن الممكن ضغط الفيديو عبر إرسال الاختلافات فقط بين الإطارات المتجاورة. يمكن أيضًا ضغط كل إطار لأنه لا يمكن رؤية كل التفاصيل في الصورة بسهولة بالعين البشرية. لا يتدفق الفيديو المضغوط بمعدلٍ ثابت، ولكنه يختلف مع الوقت وفقًا لعوامل مثل حجم العمليّة وتفاصيل الصورة وخوارزمية الضغط المستخدمة. لذلك، يمكن تحديد متوسط متطلبات حيز النطاق التراسلي، ولكن قد يكون المعدل اللحظي أكثر أو أقل. القضية الرئيسية هنا هي المجال الزمني الذي يُحسَب المعدّل من خلاله. لنفترض أنه يمكن ضغط تطبيق الفيديو هذا إلى الحدّ الذي يجعله يحتاج إلى 2 ميجابت في الثانية في المتوسط فقط. إذا أرسل 1 ميجابت في فاصل زمني مدته ثانية واحدة و3 ميجابت في الفاصل الزمني الموالي الذي مدته ثانية واحدة كذلك، فإنه في المجال الزمني الاجمالي الذي مدته ثانيتان سيكون الإرسال بمعدل 2 ميجابت في الثانية؛ ومع ذلك، لن يعني هذا الكثير بالنسبة لقناة صُمِّمت لدعم ما لا يزيد عن 2 ميجابت في الثانية الواحدة. وعليه، من الواضح أن مجرد معرفة متوسط احتياجات عرض النطاق التراسلي لتطبيقٍ ما لن يكون كافيًا دائمًا. ومع ذلك، يمكن بصفة عامة وضع حدّ أعلى لحجم الرشقة (burst) التي يُحتمل أن يرسلها تطبيق مثل هذا. يمكن وصف الرشقة على أنّها معدّل الذروة الذي يستمرّ فترة معيّنة من الزمن. ويمكن كذلك وصفها على أنها عدد البايتات التي يمكن إرسالها بمعدّل الذروة قبل العودة إلى المعدّل المتوسط أو معدلٍ أقل. إذا كان معدل الذروة هذا أعلى من سعة القناة المتاحة، فيجب تخزين البيانات الزائدة في مكان ما، لتُرسَل لاحقًا. إن معرفة حجم الرشقة التي يمكن إرسالها يسمح لمصمم الشبكة بتخصيص سعة تخزين كافية لاحتواء هذه الرشقة. ومثلما يحتاج عرض النطاق التراسلي للتطبيق أن يكون شيئًا مختلفًا عن "كلّ ما يمكن الحصول عليه"، قد تكون متطلبات التأخير الخاص بالتطبيق أعقد من مجرد "أقلّ قدرٍ ممكن من التأخير". في حالة التأخير، لا يهم أحيانًا ما إذا كان زمن الوصول الأحادي للشبكة هو 100 ميلي ثانية أو 500 ميلي ثانية بقدر أهمّية الاختلاف في وقت الاستجابة من رزمةٍ إلى أخرى. يسمى هذا الاختلاف في وقت الاستجابة التقلقل (jitter). لنأخذ الحالة التي يرسل فيها المصدر رزمةً مرة كل 33 ميلي ثانية، كما هو الحال بالنسبة لتطبيق فيديو يرسل إطارات 30 مرة في الثانية. إذا وصلت الحرزم إلى الوجهة متباعدة بمسافة 33 ميلي ثانية بالضبط، فيمكننا أن نستنتج أن التأخير الذي واجهته كل رزمة في الشبكة كان هو نفسه تمامًا. إذا كان التباعد بين وقت وصول الرزم إلى الوجهة، الذي يسمى أحيانًا الفجوة بين الرزَم (inter-packet gap)، متغيرًا، فيجب أن يكون التأخير الذي يحدثه تسلسل الرزم متغيرًا أيضًا، ويقال أن الشبكة أدخلت تقلقلًا في تدفق الرزمة، كما هو موضح في الشكل التالي. لا يدخل هذا الاختلاف بصفة عامة في رابط فيزيائي واحدٍ، ولكن يمكن أن يحدث عندما تواجه الرزم تأخيرات مختلفة في طابور شبكة تبديل الرزم متعدّدة العُقَد التوجيهية. يتوافق هذا التأخير في الطابور مع عنصر وقت الاستجابة المحدّد مسبقًا في هذا القسم، والذي يتغيّر مع الوقت. لفهم أهمية التقلقل، افترض أن الرزم التي تُرسَل عبر الشبكة تحتوي على إطارات فيديو، ولعرض هذه الإطارات على الشاشة، يحتاج جهاز الاستقبال إلى تلقي إطار جديد كل 33 ميلي ثانية. إذا وصل إطار في وقت مبكر، فيمكن ببساطة حفظه في جهاز الاستقبال حتى يحين وقت عرضه. لسوء الحظ، إذا وصل إطار في وقت متأخر، فلن يكون لدى جهاز الاستقبال الإطار الذي يحتاجه في الوقت المناسب لتحديث الشاشة، وستتأثر جودة الفيديو سلبًا؛ إذ لن تكون سلسة. وينبغي أن نشير هنا أنه ليس من الضروري القضاء على التقلقل، بل فقط معرفة مدى تأثيره السلبي. والسبب في ذلك هو أنه إذا كان جهاز الاستقبال يعرف الحدود العليا والسفلى في وقت الاستجابة التي يمكن أن تواجهها الرزمة، فيمكن أن يؤخر الوقت الذي يبدأ فيه تشغيل الفيديو (أي يعرض الإطار الأول) لفترة كافية ليضمن فيما بعد أن يجد دائمًا الإطار الذي سيعرضه عند الحاجة إليه. يعمل جهاز الاستقبال على تأخير الإطار عبر تخزينه في مخزن مؤقت، مما يزيل التقلقل بصورة فعالة. منظور الفصل الأول: سرعة الميزة (Feature Velocity) يقدّم هذا الفصل بعض أصحاب المصلحة في شبكات الحواسيب، وهم مصمّمو الشبكات ومطورو التطبيقات والمستخدمون النهائيون ومشغلو الشبكات، وذلك من أجل المساعدة في تحديد المتطلبات التقنية التي تحدّد كيفية تصميم الشبكات وبنائها. يفترض هذا أن جميع قرارات التصميم فنية بحتة، ولكن بالطبع، هذا ليس هو الحال في العادة. فهناك عوامل أخرى كثيرة تؤثر أيضًا على كيفية تصميم وبناء الشبكات مثل قوانين السوق، والسياسات الحكومية والاعتبارات الأخلاقية. من بين هذه العوامل، يكون السوق هو الأكثر تأثيرًا، ويتوافق مع التفاعل بين مشغلي الشبكات (AT&T و Comcast و Verizon و DT و NTT وChina Unicom على سبيل المثال) ومورّدي معدّات الشبكة (مثل Cisco و Juniper و Ericsson و Nokia و Huawei وNEC) ومزوّدي التطبيقات والخدمات (مثل Facebook و Google و Amazon و Microsoft و Apple و Netflix و Spotify)، وبطبيعة الحال كذلك المشتركين والعملاء (أي الأفراد، ولكن أيضًا المؤسّسات والشركات). لا تكون الخطوط بين هؤلاء الفاعلين واضحة دائمًا، إذ تلعب العديد من الشركات أدوارًا متعددة. وأبرز مثال على ذلك هو مزودو الخدمات السحابية الضخمة، الذين (a) يبنون معدات شبكاتهم الخاصة باستخدام مكوناتٍ قليلة التكلفة، (b) وينشرون ويديرون شبكاتهم الخاصة، (c) ويقدّمون خدمات وتطبيقات للمستخدم النهائي على الشبكة. عندما تحلّل هذه العوامل الأخرى في عملية التصميم الفني، فإنك تدرك أن هناك افتراضين ضمنيين في الكتاب يحتاجان إلى إعادة تقييم. أوّلهما هو أن تصميم الشبكة هو عملٌ لمرة واحدة. أي اعمل على بنائها مرة واحدة واستخدمها إلى الأبد (مع الحرص على ترقية الأجهزة حتى يتمكن المستخدمون من الاستمتاع بفوائد أحدث تحسينات الأداء). والثاني هو أن وظيفة بناء الشبكة منفصلة إلى حد كبير عن وظيفة تشغيل الشبكة. ولكن، ليس أي من هذين الافتراضين صحيحًا تمامًا. يتطوّر تصميم الشبكة بصورة واضحة، وقد وثّقت الإصدارات الانجليزية الأصلية المتتالية لهذا الكتاب هذه التغييرات على مر السنين. وكان القيام بذلك على جدول زمني يُقاس بالسنوات أمرًا جيدًا من الناحية التاريخية، ولكن أي شخص نزّل واستخدم أحدث تطبيق للهواتف الذكية يعرف مدى بطء أي شيء قِيسَ منذ سنوات وفقًا لمعايير اليوم. لذا يجب أن يكون التصميم من أجل التطور جزءًا من عملية صنع القرار. بالنسبة للنقطة الثانية، فإن الشركات التي تبني الشبكات دائمًا ما تكون هي نفسها التي تديرها. وهي تُعرف إجمالًا باسم مشغلي الشبكات (network operators)، وتشمل الشركات المدرجة أعلاه. ولكن إذا نظرنا مرة أخرى إلى مفهوم السحابة للاستلهام منه، فإننا نرى أن مسألة التطوير-مع-التشغيل ليس معمولًا بها على مستوى الشركة فقط، ولكن أيضًا في كيفية تنظيم شركات الخدمات السحابية السريعة لفرقها الهندسية حول نموذج DevOps. (إذا لم تكن لديك دراية بمفهوم DevOps، ننصحك بالاطّلاع على هذين المقالين "ما المقصود بـ DevOps؟" و "لماذا ينبغي على فرق التطوير تبنّي ثقافة DevOps؟" لأخذ فكرة عنه وعن كيفية عمله وتطبيقه). ما يعنيه كل هذا هو أن شبكات الحواسيب هي الآن في خضم تحول كبير، إذ يحاول مشغلو الشبكات تسريع وتيرة الابتكار (المعروفة أحيانًا باسم سرعة الميزة)، ومع ذلك يواصلون تقديم خدمة موثوقة (الحفاظ على الاستقرار). وهم يفعلون ذلك بصفة متزايدة من خلال تبني أفضل الممارسات لمزودي الخدمات السحابية، والتي يمكن تلخيصها في أمرين رئيسيين: (1) الاستفادة من الأجهزة العتادية قليلة التكلفة ونقل كل الذكاء إلى البرمجيات، و(2) اعتماد العمليات الهندسية الرشيقة التي تكسر الحواجز بين التطوير والتشغيل. يُطلق على هذا التحول أحيانًا "إضفاء الطابع السّحابي (cloudification)" أو "إضفاء الطابع البرمجي (softwarization)" على الشبكة، ورغم أن الإنترنت كان لديها دائمًا نظام بيئي قوي للبرمجيات، إلا أنّه كان مقصورًا تاريخيًا على التطبيقات التي تعمل في المستوى الأعلى للشّبكة (مثل استخدام واجهة Socket API). ما تغيّر في الوقت الحالي هو تطبيق نفس الممارسات الهندسية المستوحاة من السحابة على الأجزاء الداخلية للشبكة. وتمثّل هذه المقاربة الجديدة، المعروفة بالشبكات المعرّفة برمجيًّا Software) Defined Networks أو اختصارًا SDN)، عاملًا مغيّرًا للعبة، ليس من حيث كيفية التعامل مع التحديات التقنية الأساسية للتأطير والتوجيه والتجزئة/إعادة التجميع وجدولة الرزم والتحكم في الاحتقان والأمن وما إلى ذلك. ولكن من حيث سرعة تطوّر الشبكة لدعم الميزات الجديدة. هذا التحول مهمّ للغاية لدرجة أننا نتناوله مرة أخرى في قسم المنظور في نهاية كل فصل. وكما سيكشفه هذا البحث، فإن ما يحدث في صناعة الشبكات يتعلق في جزءٍ منه بالتكنولوجيا، ولكن في أجزاء أخرى أيضًا بالعديد من العوامل غير التقنية الأخرى، وكلها شهادة على مدى عمق الإنترنت في حياتنا. ترجمة -وبتصرّف- للقسم Performance من الفصل Foundation من كتاب Computer Networks: A Systems Approach
  2. تُعدّ معماريات الشبكة وتوصيفات البروتوكول من الأمور الأساسية، لكن المخطط الجيد ليس كافيًا لتفسير النجاح الهائل للإنترنت: لقد زاد عدد أجهزة الحاسوب المتصلة بالإنترنت بصورة هائلة على مدار أكثر من ثلاثة عقود (رغم صعوبة الحصول على أرقام دقيقة). إذ قُدِّر عدد مستخدمي الإنترنت بحوالي 4.5 مليار بنهاية عام 2019، أي ما يفوق نصف سكان العالم. فما الذي يفسر نجاح الإنترنت؟ هناك بالتأكيد العديد من العوامل المساهمة (بما في ذلك البنية الجيدة)، ولكن هناك شيء واحد أعطى للإنترنت هذا النجاح الهائل، وهو أن الكثير من وظائفها تُوفِّرها البرامج التي تعمل على أجهزة الحاسوب ذات الأغراض العامة. وتكمن أهمية ذلك في إمكانية إضافة وظائف جديدة بسهولة من خلال "مجرد مسألة برمجيّة صغيرة". ونتيجة لذلك، ظهرت التطبيقات والخدمات الجديدة بوتيرة لا تصدق. وهناك عاملٌ مرتبط بذلك هو الزيادة الهائلة في قوّة الحوسبة المتاحة في الأجهزة. ورغم أن شبكات الحاسوب كانت دائمًا قادرة من حيث المبدأ على نقل أي نوع من المعلومات، مثل عينات الصوت الرقمي والصور الرقمية، وما إلى ذلك، إلا أن هذه الإمكانات لم تكن مثيرة للاهتمام بصورة خاصّةٍ إذ كانت أجهزة الحاسوب التي ترسل وتستقبل هذه البيانات بطيئة جدًا في القيام بأي شيء مفيد بالمعلومات. أمّا اليوم، فتقريبًا جميع أجهزة الحاسوب الحالية قادرة على تشغيل الصوت والفيديو الرقمي بسرعةٍ ودقّةٍ فعّالتين تمامًا. في السنوات التي تلت ظهور الطبعة الانجليزية الأولى من هذا الكتاب، أصبحت كتابة التطبيقات المتصلة بالشبكة نشاطًا رئيسيًا وليست وظيفة لعددٍ قليل من المتخصصين. وقد لعبت العديد من العوامل دورًا في ذلك، بما في ذلك وجود أدوات أفضل لتسهيل المهمة وفتح أسواق جديدة مثل تطبيقات الهواتف الذكية. النقطة التي ينبغي ملاحظتها هنا هي أنّ معرفة كيفية تنفيذ برمجيات الشبكة هو جزء أساسي من فهم الشبكات الحاسوبية، ورغم أنّك لن تُكلَّف على الأرجح بتنفيذ بروتوكولٍ في المستوى الأدنى مثل بروتوكول IP، فهناك احتمال جيّدٌ أن تجد سببًا لتنفيذ بروتوكول في المستوى التطبيقي، ربّما ذلك "التطبيق القاتل" والمحيّر الذي سيجلب لك شُهرة وثروة لا يمكن تصوّرها. من أجل الانطلاق، يعرض لك هذا القسم بعض المشكلات التي ينطوي عليها تنفيذ تطبيق شبكي في المستوى الأعلى للإنترنت. عادةً ما تكون هذه البرامج في الوقت ذاته تطبيقًا (أي مصمّمًا للتفاعل مع المستخدمين) وبروتوكولًا (أي يتواصل مع نظرائه عبر الشبكة). واجهة برمجة التطبيقات API (المقابس Sockets) عندما يتعلّق الأمر بتنفيذ تطبيق شبكي، تكون نقطة البداية هي الواجهة التي تصدّرها الشبكة. ونظرًا لأن معظم بروتوكولات الشبكة موجودة في البرمجيات (خاصة تلك الموجودة في المستوى الأعلى في مجموعة البروتوكولات)، ولأن جميع أنظمة الحاسوب تقريبًا تنفذ بروتوكولات الشبكة كجزءٍ من نظام التشغيل، فعندما نشير إلى الواجهة "التي تصدّرها الشبكة"، فإننا نشير بصفة عامة إلى الواجهة التي يوفّرها نظام التشغيل لنظامه الفرعي الخاصّ بالشبكات. غالبًا ما تسمى هذه الواجهة واجهة برمجة التطبيقات (Application Programming Interface أو اختصارًا API) في الشبكة. رغم أن لكلّ نظام تشغيل الحرية في تحديد واجهة برمجة تطبيقات الشبكة الخاصة به (ومعظمها لديه واجهته الخاصّة)، إلا أنه بمرور الوقت أصبحت بعض واجهات برمجة التطبيقات هذه مدعومة على نطاق واسع؛ بمعنى أنّها نُقِلت إلى أنظمة تشغيل غير نظامها الأصلي. هذا ما حدث مع واجهة المقبس (socket interface) التي أُنشِئت في الأصل من خلال توزيعة بيركلي لنظام التشغيل يونيكس، والتي تدعمها الآن جميع أنظمة التشغيل الشائعة تقريبًا، وهي تمثّل أساسًا للواجهات في لغات برمجية خاصّة، مثل مكتبة مقابس جافا أو بايثون. نستخدم في هذا الكتاب نظام لينكس ولغة سي لجميع أمثلة الشيفرة البرمجية، وذلك لأن لينوكس مفتوح المصدر ولأن سي تظل هي اللغة المفضلة لمكونات الشبكة الداخلية. (تتمتع لغة سي أيضًا بميزة كشف جميع التفاصيل ذات المستوى المنخفض، وهو أمر مفيد في فهم الأفكار الأساسية). قبل توصيف واجهة المقبس، من المهم أن تبقي المسألتين التاليتين منفصلتين في ذهنك. المسألة الأولى أن كل بروتوكول يوفر مجموعة معينة من الخدمات (services)، والثانية أن API توفر صياغة نحوية (syntax) يمكن من خلالها استدعاء هذه الخدمات على نظام حاسوبٍ معين. بعد ذلك يكون التنفيذ مسؤولًا عن ربط (mapping) مجموعة العمليات والكائنات الملموسة المحددة بواسطة واجهة برمجة التطبيقات (API) مع مجموعة الخدمات المجرّدة التي يحددها البروتوكول. إذا أنجزت عملًا جيدًا في تعريف الواجهة، فسيكون من الممكن استخدام الصياغة النحوية للواجهة من أجل استدعاء خدمات العديد من البروتوكولات المختلفة. لقد كان هذا الشّمول بالتأكيد هدفًا رئيسيًا لاستخدام واجهة المقبس رغم أنّه بعيد عن الكمال. ليس مستغربًا أن يكون التجريد الرئيسي لواجهة المقبس هو المقبس (socket). إن أفضل وسيلة لفهم المقبس هي النظر إليه كالنقطة التي ترتبط فيها عمليةُ تطبيقٍ محليٍّ بالشبكة. تحدّد الواجهة عمليات إنشاء المقبس، وربط المقبس بالشبكة، وإرسال واستقبال الرسائل من خلال المقبس، وفصل المقبس. لتبسيط الحديث، سنقتصر على إظهار كيفية استخدام المقابس على بروتوكول TCP: تتمثّل الخطوة الأولى في إنشاء مقبس باستخدام العملية التالية: int socket(int domain, int type, int protocol); تأخذ هذه العملية ثلاث وسائط لسببٍ مهمٍّ هو أنّ واجهة المقبس مصمَّمة لتكون عامة بما يكفي لدعم أي مجموعة بروتوكولات أساسية. على وجه التحديد، يحدّد الوسيط النطاق domain عائلةَ البروتوكولات التي ستُستخدَم : يشير PF_INET إلى عائلة الإنترنت، ويشير PF_UNIX إلى مجموعة تعليمات يونيكس، ويشير PF_PACKET إلى الوصول المباشر إلى واجهة الشبكة (أي أنه يتجاوز رزمة البروتوكولات TCP/IP). يشير الوسيط النوع type إلى دلالات الاتصال، إذ يستخدم SOCK_STREAM للإشارة إلى تدفق بايتات، أمّا SOCK_DGRAM فهو بديل يشير إلى خدمة موجهة للرسالة، مثل تلك التي يوفرها بروتوكول UDP، وحدّد الوسيط protocol البروتوكولَ المحدد الذي سيُستخدَم هنا. في حالتنا هذه، سيكون الوسيط هو UNSPEC لأن الجمع بين PF_INET و SOCK_STREAM يعني TCP. أخيرًا، تكون القيمة المُعادة من socket مِقبضًا (handle) للمقبس الجديد الذي أُنشِئ، أي معرّفًا يمكننا من خلاله الرجوع إلى المقبس في المستقبل، ويمكن إعطاؤه كوسيط للعمليات اللاحقة على هذا المقبس. تعتمد الخطوة التالية على ما إذا كان الجهاز عميلًا أو خادومًا. على جهاز الخادوم، تؤدي عملية التطبيق إلى فتحٍ سلبيٍّ (passive open)، بمعنى أنّ الخادوم يصرّح باستعداده لقبول الاتصالات، ولكنه لا ينشئ اتصالًا فعليًا. يفعل الخادوم ذلك باستدعاء العمليات الثلاث التالية: int bind(int socket, struct sockaddr *address, int addr_len); int listen(int socket, int backlog); int accept(int socket, struct sockaddr *address, int *addr_len); تربط عملية الربط bind، كما يوحي اسمها، على ربط المقبس الجديد الذي أُنشِئ بالعنوان address المحدد. هذا هو عنوان الشبكة الخاصّ بالطرف المحلي أي الخادوم. لاحظ أنه عند استخدامه في بروتوكولات الإنترنت، يكون وسيط العنوان address عبارةً عن بنية بيانات يتضمن عنوان IP الخاص بالخادوم ورقم منفذ TCP. تُستخدَم المنافذ لتحديد العمليّات بطريقة غير مباشرة، فهي شكلٌ من أشكال مفاتيح demux. ويكون رقم المنفذ في العادة عبارةً عن بعض الأرقام التي يُعرَف جيّدًا أنّها خاصّة بالخدمة المقدمة؛ على سبيل المثال، تقبل خواديم الويب عادةً الاتصالات على المنفذ 80. تحدّد عملية الاستماع listen بعد ذلك عدد الاتصالات التي يمكن أن تكون معلّقةً على المقبس المحدّد. وأخيرًا، تقوم وظيفة القبول accept بالفتح السلبي. إنها عمليةٌ حاجزة (blocking operation) لا تُرجِع أي قيمة إلا بعد أن يُنشِئ طرفٌ بعيدٌ اتصالًا، وعندما يكون الأمر كذلك، فهي تُرجِع مقبسًا جديدًا يتوافق مع هذا الاتصال الذي أُنشِئ لتوّه، ويتضمّن وسيط العنوان address عنوان طرف الاتصال البعيد. لاحظ أنه عندما تُرجِع العملية accept، يكون المقبس الأصلي الذي قُدّم كوسيط مازال موجودًا وما زال يتوافق مع الفتح السلبي؛ إنّه يُستخدَم في الاستدعاءات المستقبلية للعملية accept. تؤدي عملية التطبيق إلى فتحٍ نشط على جهاز العميل؛ أي أنّ العميل يصرّح بمن يريد التواصل معه باستدعاء العملية التالية: int connect(int socket, struct sockaddr *address, int addr_len); لا تُرجِع هذه العملية كذلك حتى يُنشِئ TCP اتصالًا ناجحًا، وعندها تكون للتطبيق الحرّية في بدء إرسال البيانات. في هذه الحالة، يتضمن وسيط العنوان address عنوان طرف الاتّصال البعيد. من الناحية العملية، يحدّد العميل عادةً عنوان الطرف البعيد فقط ويتيح للنظام إضافة المعلومات المحلية. في حين أنّ الخادوم يستمع عادةً للرسائل على منفذٍ معروفٍ، لا يهتم العميل في العادة بالمنفذ الذي يستخدمه لنفسه؛ إذ يختار نظام التشغيل ببساطة منفذًا غير مستخدم. بمجرد إنشاء الاتصال، تستدعي عمليات التطبيق العمليتين التاليتين لإرسال البيانات وتلقيها: int send(int socket, char *message, int msg_len, int flags); int recv(int socket, char *buffer, int buf_len, int flags); ترسل العملية الأولى الرسالة message المحدّدة عبر المقبس socket المحدد، بينما تتلقى العملية الثانية رسالةً من المقبس socket المحدد في المخزَن المؤقت buffer المحدد. تأخذ كلتا العمليتين مجموعةً من الرايات flags التي تتحكم في تفاصيل معيّنة للعملية. ثورة التطبيقات المدعَّمة بالمقابس ليس من المبالغة التشديد على أهمية واجهة برمجة التطبيقات المدعّمة بالمقابس (Socket API). فهي تحدد النقطة الفاصلة بين التطبيقات التي تعمل في المستوى الأعلى للإنترنت وتفاصيل كيفية بناء وعمل الإنترنت. إذ توفّر المقابس واجهة مُحدَّدة جيدًا ومستقرة، مما أدّى إلى ثورة في كتابة تطبيقات الإنترنت جعلتها صناعةً بمليارات الدولارات. فبعد البدايات المتواضعة لنموذج العميل/الخادوم وعددٍ قليل من برامج التطبيقات البسيطة مثل البريد الإلكتروني ونقل الملفات وتسجيل الدخول عن بُعد، أصبح بإمكان الجميع الآن الوصول إلى مَعِينٍ لا ينضب من التطبيقات السحابية من هواتفهم الذكية. يضع هذا القسم الأساس من خلال إعادة النظر في بساطة برنامج العميل الذي يفتح مقبسًا حتى يتمكن من تبادل الرسائل مع برنامج الخادوم، ولكن اليوم يوجد نظام بيئي غني في طبقةٍ فوق واجهة Socket API. تتضمن هذه الطبقة عددًا كبيرًا من الأدوات السحابية التي تخفض حاجز تنفيذ التطبيقات القابلة للتطوير. مثال تطبيقي نعرض الآن تنفيذ برنامج عميل/خادوم بسيط يستخدم واجهة المقبس لإرسال الرسائل عبر اتصال TCP. يستخدم البرنامج أيضًا أدوات شبكات لينكس الأخرى، والتي نستعرضها أثناء تقدّمنا. يسمح تطبيقنا للمستخدم على جهازٍ بكتابة وإرسال نصّ إلى مستخدم على جهاز آخر. إنه نسخة مبسطة من برنامج لينكس talk، والتي تشبه البرنامج في مركز تطبيقات المراسلة الفورية: طرف العميل (Client) نبدأ مع جانب العميل، الذي يأخذ اسم الجهاز البعيد كوسيط. ثم يستدعي أداة لينكس لترجمة هذا الاسم إلى عنوان IP للمضيف البعيد. الخطوة التالية هي إنشاء بنية بيانات العنوان (sin) الذي تترقّبه واجهة المقبس. لاحظ أن بنية البيانات هذه تحدّد أننا سنستخدم المقبس للاتصال بالإنترنت (AF_INET). في مثالنا، نستخدم منفذ TCP ذي الرّقم 5432 على أنّه منفذ الخادوم المعروف؛ هذا يعني أنّه منفذٌ لم يُعيّن لأي خدمة إنترنت أخرى. الخطوة الأخيرة في إعداد الاتصال هي استدعاء العملتيين socket و connect. عندما تعيد العملية، يُؤسَّس الاتصال مباشرةً بعد ذلك ويَدخل برنامج العميل في حلقته الرئيسية (main loop)، والتي تقرأ النص من المدخل الاعتيادي وترسله عبر المقبس. #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #define SERVER_PORT 5432 #define MAX_LINE 256 int main(int argc, char * argv[]) { FILE *fp; struct hostent *hp; struct sockaddr_in sin; char *host; char buf[MAX_LINE]; int s; int len; if (argc==2) { host = argv[1]; } else { fprintf(stderr, "usage: simplex-talk host\n"); exit(1); } /* ترجمة اسم المضيف إلى عنوان أي بي النظير */ hp = gethostbyname(host); if (!hp) { fprintf(stderr, "simplex-talk: unknown host: %s\n", host); exit(1); } /* بناء بينة بيانات العنوان */ bzero((char *)&sin, sizeof(sin)); sin.sin_family = AF_INET; bcopy(hp->h_addr, (char *)&sin.sin_addr, hp->h_length); sin.sin_port = htons(SERVER_PORT); /* active open */ if ((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) { perror("simplex-talk: socket"); exit(1); } if (connect(s, (struct sockaddr *)&sin, sizeof(sin)) < 0) { perror("simplex-talk: connect"); close(s); exit(1); } /* الحلقة الرئيسية: الحصول على سطور من النص وإرسالها */ while (fgets(buf, sizeof(buf), stdin)) { buf[MAX_LINE-1] = '\0'; len = strlen(buf) + 1; send(s, buf, len, 0); } } طرف الخادوم (Server) يجري الأمر كذلك في الخادوم بنفس البساطة. فهو يُنشِئ أولًا بنية بيانات العنوان عبر تحديد رقم المنفذ الخاص به (SERVER_PORT). وبما أنّه لا يُحدّد عنوان IP، فإن برنامج التطبيق يكون على استعدادٍ لقبول الاتصالات على أيّ عنوان IP للمضيف المحلي. بعد ذلك، يُجري الخادوم الخطوات الأولية التي يتضمّنها الفتح السلبي؛ إذ يُنشِئ المقبس، ويربطه بالعنوان المحلّي، ويُعيّن الحد الأقصى لعدد الاتصالات المعلّقة المسموح بها. وأخيرًا، تترقّب الحلقة الرئيسية محاولة الاتصال من مضيف بعيدٍ، وعندما يتمّ ذلك، فهي تتلقى الحروف التي تصل عند الاتصال وتطبعها: #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #define SERVER_PORT 5432 #define MAX_PENDING 5 #define MAX_LINE 256 int main() { struct sockaddr_in sin; char buf[MAX_LINE]; int buf_len, addr_len; int s, new_s; /* بناء بنية بيانات العنوان */ bzero((char *)&sin, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_addr.s_addr = INADDR_ANY; sin.sin_port = htons(SERVER_PORT); /* إعداد الفتح السلبي */ if ((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) { perror("simplex-talk: socket"); exit(1); } if ((bind(s, (struct sockaddr *)&sin, sizeof(sin))) < 0) { perror("simplex-talk: bind"); exit(1); } listen(s, MAX_PENDING); /* انتظر الاتصال، ثم استقبل النص اطبعه */ while(1) { if ((new_s = accept(s, (struct sockaddr *)&sin, &addr_len)) < 0) { perror("simplex-talk: accept"); exit(1); } while (buf_len = recv(new_s, buf, sizeof(buf), 0)) fputs(buf, stdout); close(new_s); } } ترجمة -وبتصرّف- للقسم Software من فصل Foundation من كتاب Computer Networks: A Systems Approach
  3. وضعَ القسم السابق -المتطلبات اللازمة لبناء شبكة حاسوبية- مجموعةً كبيرة جدًا من المتطلبات لتصميم الشبكة، إذ يجب أن توفّر الشبكة الحاسوبية اتصالًا عامًا وذا تكلفة فعّالة وعادلًا وقويًا بين عدد كبير من أجهزة الحاسوب. ولكن، لا تظلّ الشبكاتُ ثابتةً في أي وقتٍ من الأوقات، بل يجب أن تتطور لتتوافق مع التغييرات في كل من التقنيات الأساسية التي تستند إليها وكذلك التغييرات في المتطلبات التي تفرضها برامج التطبيقات عليها. علاوة على ذلك، يجب أن تكون الشبكات قابلةً للإدارة بشريًا بمستويات مختلفة من المهارة. ومن الواضح أن تصميم شبكة لتلبية كلّ هذه المتطلبات ليس بالمهمة اليسيرة. طوَّر مصممو الشبكات للمساعدة في التعامل مع هذا التعقيد مخططات عامة، تسمى عادةً معماريات الشبكات (network architectures)، والتي توجّه تصميم الشبكات وتنفيذها. يحدّد هذا القسم بعناية أكبر ما نعنيه بمعمارية الشبكة من خلال تقديم الأفكار المركزية المشتركة بين جميع معماريات الشبكة. كما يعرض أيضًا معماريتين من أكثر المعماريات المرجعية انتشارًا في عالم الشبكات وهما معمارية OSI أو معمارية الطبقات السبع ومعمارية الإنترنت. الطبقات (Layering) والبروتوكولات (Protocols) يعني التجريد (Abstraction) إخفاء تفاصيل التنفيذ وراء واجهة محددة جيدًا، وهو يمثّل الأداة الأساسية التي يستخدمها مصممو النظام لإدارة التعقيد. تتمثل فكرة التجريد في تحديد نموذج يمكنه التقاط بعض الجوانب المهمة للنظام، وتغليف هذا النموذج في كائن يوفر واجهةً يمكن التعامل معها عبر مكونات أخرى من النظام، وإخفاء تفاصيل كيفية وضع هذا الكائن عن مستخدميه. يكمن التحدي في تحديد التجريدات التي تقدم خدمة في وقت واحد تثبت فائدتها في عدد كبير من المواقف ويمكن تنفيذها بكفاءة في النظام الأساسي. هذا بالضبط ما كنا نفعله عندما قدمنا فكرة القناة في القسم السابق: كنا نقدم تجريدًا للتطبيقات يُخفي تعقيد الشبكة عن منشئي التطبيقات. يقود التجريد بصورة طبيعية إلى مفهوم الطبقات، وبالخصوص في أنظمة الشبكات. الفكرة العامة هي أننا ننطلق من الخدمات التي تقدمها الأجهزة الأساسية ثم نضيف سلسلةً من الطبقات، يوفر كلّ منها مستوى خدمة أعلى (أي أكثر تجريدًا). تُنشَأ الخدمات المقدمة في الطبقات العليا بناءً على الخدمات التي تقدمها الطبقات السفلى. بالاعتماد على مناقشة المتطلبات الواردة في القسم السابق، على سبيل المثال، نستطيع أن نتخيل شبكة بسيطة تحتوي على طبقتين من التجريد محصورة بين البرامج التطبيقية والعتاد الأساسي، كما هو موضّح في الشكل السابق. قد توفر الطبقة الموجودة أعلى العتاد مباشرة في هذه الحالة اتصالًا من مضيف لمضيف بصورةٍ تُجرّد حقيقةَ أنه قد يكون هناك مخطط شبكة (topology) شديد التعقيد بين أيّ زوج من المضيفين. تعتمد الطبقة التالية على خدمة الاتصال المتاحة من مضيف لمضيف وتوفر دعمًا للقنوات "عمليةٍ لعملية"، بصورةٍ تُجرّد حقيقةَ أن الشبكة تفقد الرسائل أحيانًا، على سبيل المثال. يوفّر مفهوم الطبقات ميزتين أساسيتين. فهو يجزّئ أولاً مشكلة بناء الشبكة إلى مكونات أكثر قابلية للإدارة. عوضًا عن تنفيذ برنامج مترابط يقوم بكل ما تريده على الإطلاق، يمكنك استخدام عدة طبقات، كلّ منها يحلّ جزءًا واحدًا من المشكلة. وثانيًا، يوفر تصميمًا أكثر نمطيةً. إذا قررت إضافة بعض الخدمات الجديدة، فقد تحتاج فقط إلى تعديل الوظيفة في طبقة واحدة، وإعادة استخدام الوظائف المقدّمة في جميع الطبقات الأخرى. قد يكون التفكير في النظام كتسلسل خطي للطبقات إفراطًا في التبسيط، ولكن، في كثير من الأحيان، هناك العديد من التجريدات المقدمة على أي مستوى معين من النظام، كل منها يقدم خدمة مختلفة للطبقات الأعلى ولكن يعتمد على نفس المستوى المنخفض من التجريد. لفهم ذلك أكثر، نأخذ نوعي القنوات الذين ناقشناهما سابقًا. أحدهما يوفر خدمة طلب/استجابة ويدعم الآخر خدمة تدفق الرسائل. قد تكون هاتان القناتان معطيات بديلة في مستوى معيّن من النظام الشبكي متعدد المستويات، كما هو موضح في الشكل التالي. إذا اعتمدنا على هذه المناقشة للطبقات كأساسٍ، سنكون على استعداد لمناقشة معمارية الشبكة بصورة أدق. بالنسبة للمبتدئين، تسمى الكائنات المجردة التي تشكل طبقات نظام الشبكة بالبروتوكولات. أي أن البروتوكول يوفر خدمة اتصال تستخدمها كائنات المستوى الأعلى (مثل عمليات التطبيق، أو ربما بروتوكولات المستوى الأعلى) لتبادل الرسائل. على سبيل المثال، يمكننا أن نتخيل شبكةً تدعم بروتوكول الطلب/الاستجابة وبروتوكول تدفق الرسائل، المطابقين لقنوات الطلب/الاستجابة وتدفّق الرسائل التي ناقشناها سابقًا. يحدّد كل بروتوكول واجهات مختلفة. في البداية، يحدد البروتوكول واجهةَ خدمةٍ للكائنات الأخرى على نفس الحاسوب الذي يريد استخدام خدمات الاتصال الخاصة به. تحدد واجهة الخدمة هذه العمليات التي يمكن أن تؤديها الكائنات المحلية على البروتوكول. على سبيل المثال، يدعم بروتوكول الطلب/الاستجابة العمليات التي يمكن للتطبيق من خلالها إرسال الرسائل واستلامها. يمكن أن يدعم تنفيذ بروتوكول HTTP عمليةَ جلب صفحة ذات شكل نصٍّ ترابطي عن بعد من الخادوم. وقد يستدعي تطبيقٌ مثل متصفح الويب عمليةَ كهذه كلما احتاج المتصفح إلى الحصول على صفحة جديدة (على سبيل المثال، عندما ينقر المستخدم على رابط في الصفحة المعروضة حاليًا). ثانيًا، يحدّد البروتوكول واجهة نظيرٍ (peer interface) من أجل نظيره على جهازٍ آخرٍ. تحدد هذه الواجهة الثانية شكل ومعنى الرسائل المتبادلة بين نظراء البروتوكول لتنفيذ خدمة الاتصال. وهذا سيحدّد الطريقة التي يتواصل بها بروتوكول الطلب/الاستجابة على أحد الأجهزة مع نظيره على جهاز آخر. في حالة HTTP، على سبيل المثال، تحدّد مواصفات البروتوكول بالتفصيل كيفية تنسيق الأمر GET، والوسائط التي يمكن استخدامها مع الأمر، وكيف ينبغي أن يستجيب خادوم الويب عندما يتلقى مثل هذا الأمر. يحدد البروتوكول خدمةَ الاتصال التي يُصدّرها محليًا في نفس الجهاز (عبر واجهة الخدمة)، إلى جانب مجموعةٍ من القواعد التي تحكم الرسائل التي يتبادلها البروتوكول مع نظرائه لتنفيذ هذه الخدمة (واجهة النظير). نوضّح هذا الوضع في الشكل التالي: باستثناء مستوى العتاد الذي يتواصل فيه النظراء بصفة مباشرة فيما بينهم عبر وسيط فيزيائي، يكون الاتصال من نظير لنظير بصفة غير مباشرة، إذ يتواصل كلّ بروتوكول مع نظيره عن طريق تمرير الرسائل إلى أحد بروتوكولات المستوى الأدنى، والذي يسلمها بدوره إلى نظيره. وعلاوة على ذلك، من المحتمل أن يكون هناك أكثر من بروتوكول واحد على أي مستوى معين، يقدم كل منها خدمة اتصال مختلفة. لذلك فإننا نمثل مجموعة البروتوكولات التي تشكل نظام شبكة برسم بياني للبروتوكول (protocol graph). تتوافق عُقد الرسم البياني مع البروتوكولات، وتمثل الأضلاع (edges) علاقة اعتمادية (depends on). على سبيل المثال، يوضّح الشكل التالي رسمًا بيانيًا للبروتوكول لنظام الطبقات الافتراضي الذي ناقشناه، إذ ينفّذ البروتوكولان RRP (بروتوكول الطلب / الاستجابة (Request/Reply Protocol)) و MSP (بروتوكول تدفّق الرسائل (Message Stream Protocol)) نوعين مختلفين من قنوات عمليةٍ لعملية، ويعتمد كلاهما على بروتوكول مضيفٍ لمضيف (Host-to-Host Protocol أو اختصارًا HHP) الذي يوفّر خدمة اتصال من مضيف لمضيف. في هذا المثال، نستطيع أن نفترض أن برنامج الوصول إلى الملفات على المضيف 1 يريد إرسال رسالة إلى نظيره في المضيف 2 باستخدام خدمة الاتصال التي يقدمها بروتوكول RRP. في هذه الحالة، يطلب تطبيق الملف من بروتوكول RRP إرسال الرسالة نيابة عنه. وللتواصل مع نظيره، يستدعي بروتوكولُ RRP خدماتِ بروتوكول HHP، والذي بدوره ينقل الرسالة إلى نظيره على الجهاز الآخر. بمجرد وصول الرسالة إلى نسخة بروتوكول HHP على المضيف 2، يمرّر بروتوكول HHP الرسالة إلى بروتوكول RRP، والذي بدوره يُسلّمها إلى تطبيق الملف. في هذه الحالة الخاصّة، يُقال أن التطبيق يستخدم خدمات رزمة البروتوكولات RRP/HHP. لاحظ أن مصطلح البروتوكول يستخدم بطريقتين مختلفتين. أحيانًا يشير إلى الواجهات المجردة أي العمليات التي تحدّدها واجهة الخدمة وشكل ومعنى الرسائل المتبادلة بين النظراء، وأحيانًا أخرى يشير إلى الوحدة النمطية التي تنفّذ هاتين الواجهتين فعليًا. للتمييز بين الواجهات والوحدة النمطية التي تنفذ هذه الواجهات، نشير بشكل عام إلى الأولى على أنها مواصفات البروتوكول (protocol specification). ويُعبَّر عن المواصفات بصورة عامّة باستخدام مزيج من الكلام المنثور (prose) والشيفرة الوهمية (pseudocode) ومخططات انتقال الحالة وصور صيغ الرزم وغيرها من الرموز المجرّدة. وفي الحقيقة، يجب أن يكون الحال كذلك إذ يمكن أن يُنفَّذ بروتوكول معين بطرق مختلفة من قِبَل مبرمجين مختلفين، طالما أن كلًا منهم يلتزم بالمواصفات. يكمن التحدي في ضمان أن يتمكّن تطبيقان مختلفان لنفس المواصفات من التراسل بنجاح. عندما تكون وحدتا بروتوكول (أو أكثر من وحدتين) تطبّقان بدقّة مواصفات البروتوكول، نقول أنّهما تعملان في تداخلٍ بينهما. نستطيع أن نتخيل العديد من البروتوكولات والرسوم البيانية للبروتوكولات المختلفة التي تلبي متطلبات الاتصال لمجموعة من التطبيقات. لحسن الحظ، توجد هيئات دولية للتقييس، مثل فرقة العمل المعنية بهندسة الإنترنت (IETF) ومنظمة المعايير الدولية (ISO)، والتي تضع معايير الرسم البياني الخاص بالبروتوكول. نسمي مجموعة القواعد التي تحكم شكل ومحتوى الرسم البياني للبروتوكول بنية الشبكة. ورغم أنّ هذا لا يدخل في نطاق هذا الكتاب، فقد وضعت هيئات التقييس إجراءات محددة بدقّة لتقديم البروتوكولات، والتحقّق منها، والموافقة عليها في هياكلها الخاصّة. سوف نَصِف فيما بعد باختصارٍ المعماريات التي حددتها IETF و ISO، ولكن هناك قبل ذلك أمران إضافيان نحتاج إلى شرحهما حول آليات وضع البروتوكول ضمن طبقات. التغليف (Encapsulation) لنأخذ ما يحدث عندما يرسل أحد البرامج التطبيقية رسالة إلى نظيره عبر تمرير الرسالة إلى RRP. من زاوية النظر للبرتوكول RRP، فإن الرسالة التي يقدّمها التطبيق هي سلسلة من البايتات غير المُفسَّرة. إنّ بروتوكول RRP لا يهتمّ بنوعية هذه البايتات، هل تمثّل مجموعة من الأعداد الصحيحة أو رسالة بريد إلكتروني أو صورة رقمية أو أيا كان؛ فهو ببساطة مسؤول عن إرسالها إلى نظيره. ومع ذلك، يتوجّب على بروتوكول RRP توصيل معلومات التحكّم إلى نظيره، وتوجيهه لكيفية التعامل مع الرسالة عند تلقّيها، وذلك عبر إرفاق ترويسةٍ بالرسالة. بصورة عامّة، تكون الترويسة عبارة عن بنية بيانات صغيرة، تتراوح بين بضعة بايتات إلى بضع عشرات من البايتات، تستخدمها النظراء للتواصل فيما بينها. وكما يوحي اسمها، تُرفَق الترويسة (header) عادةً في مقدمة الرسالة. ومع ذلك، في بعض الحالات، تُرسَل معلومات التحكم هذه من نظير إلى نظيرٍ في نهاية الرسالة، وفي هذه الحالة تسمى تذييلًا (trailer). يُحدّد بروتوكول RRP الصيغةَ الدقيقة للترويسةِ المرفقة من خلال مواصفات البروتوكول الخاصة به. وتسمى بقية الرسالة، أي البيانات التي تُرسَل نيابة عن التطبيق، نصّ (body) أو حمولة (payload) الرسالة. وهكذا، نقول أنّ بيانات التطبيق مُغلّفة في الرسالة الجديدة التي أنشأها بروتوكول RRP. بعد ذلك، تتكرّر عملية التغليف هذه في جميع مستويات الرسم البياني للبروتوكول. فمثلًا، يغلّف البروتوكول HHP رسالة RRP بترويسة مرفقة خاصّة به. وإذا افترضنا أنّ HHP يرسل الرسالة إلى نظيره عبر الشبكة، فعندما تصِل الرسالة إلى المضيف الوجهة، فإنّ معالجتها الآن تتمّ بالترتيب المعاكس: أي أنّ HHP يفسّر ترويسة نظيره HHP التي في مقدمة الرسالة أولًا (هذا يعني أنّه يتّخذ الإجراء المناسبَ بناءً على محتوى الترويسة)، ثمّ تمرّر حمولة الرسالة وليس ترويسة HHP إلى البروتوكول RRP. ويتّخذ هذا الأخير بدوره أي إجراء تشير إليه ترويسة RRP التي ألحقها نظيره بالرسالة وتمرّر حمولة الرسالة و ليس ترويسة RRP إلى البرنامج التطبيقي. تبقى الرسالة التي مرّرها RRP إلى التطبيق على المضيف 2 هي نفسها تمامًا التي مرّرها التطبيق إلى RRP على المضيف 1. إذ لا تظهر للتطبيق أي ترويسة مرفقة بالرسالة بغرض تنفيذ خدمات الاتصال ذات المستوى الأدنى. ويوضّح الشكل التالي هذه العملية بالكامل. لاحظ أنه في هذا المثال، يمكن أن تفحص العُقد الموجودة في الشبكة (الموجّهات والمبدّلات، على سبيل المثال) ترويسةَ HHP الموجودة في مقدمة الرسالة: لاحظ أنه عندما نقول أن بروتوكولًا من المستوى الأدنى لا يفسر الرسالة التي قدمها أحد بروتوكولات المستوى العلوي، فإننا نعني أنه لا يعرف كيفية استخراج أي معنى من البيانات الواردة في الرسالة. ومع ذلك، في بعض الأحيان، يطبّق بروتوكول المستوى الأدنى بعض التحول البسيط على البيانات التي يقدّمها، مثل ضغطها أو تشفيرها. في هذه الحالة، يعمل البروتوكول على تحويل نص الرسالة بالكامل، بما في ذلك بيانات التطبيق الأصلي وجميع الترويسات التي أرفقتها بروتوكولات المستوى العلوي بهذه البيانات. 3.3.1 الدمج أو تعدد الإرسال (Multiplexing) وفكّ الدمج أو فك تعدد الإرسال (Demultiplexing) ينبغي التذكير أن الفكرة الأساسية لتبديل الرزم هي دمج تدفقات البيانات المتعدّدة عبر رابطٍ فيزيائي واحد. وتنطبق هذه الفكرة نفسها صعودًا ونزولًا على الرسم البياني للبروتوكول، وليس فقط لتبديل العقد. يمكننا التفكير في بروتوكول RRP على أنه ينفّذ قناة اتصال منطقية، مع إرسال رسائل من تطبيقين مختلفين تُدمَج عبر هذه القناة في المضيف المصدر ثم يُفَكّ دمجها مرة أخرى نحو التطبيق المناسب في المضيف الوِجهَة. هذا يعني من الناحية العملية، ببساطةٍ أن الترويسة التي يُلحِقها RRP برسائله تحتوي على معرّف يحدّد التطبيق الذي تنتمي إليه الرسالة. نسمي هذا المعرّف مفتاح فكّ دمج (demultiplexing) بروتوكول RRP، أو مفتاح demux للاختصار. في المضيف المصدر، يُضَمِّن بروتوكول RRP مفتاح demux المناسب في ترويسته. وعندما تُسلَّم الرسالة إلى بروتوكول RRP على المضيف الوجهة، فإنه يكشف عن الترويسة، ويفحص مفتاح demux، ويفكّ دمج الرسالة نحو التطبيق المناسب. ليس البروتوكول RRP فريدًا في دعمه للدمج (أو تعدّد الإرسال)، فكل البروتوكولات تقريبا تنفّذ هذه الآلية. على سبيل المثال، يحتوي بروتوكول HHP على مفتاح demux خاصٍّ به لتحديد الرسائل التي تُمرّر إلى بروتوكول RRP وأيها ستُمرّر إلى بروتوكول MSP. ومع ذلك، لا يوجد اتفاقٌ موحّد بين البروتوكولات، حتى تلك الموجودة داخل معمارية شبكة واحدة، على محتوى مفتاح demux بالضبط. تستخدم بعض البروتوكولات حقلَ 8 بِتات (بمعنى أنها يمكن أن تدعم 256 بروتوكولًا عالي المستوى فقط)، بينما يستخدم البعض الآخر حقول 16 أو 32 بِت. وكذلك، تحتوي بعض البروتوكولات على حقل واحدٍ لفكّ الدمج في ترويستها، بينما يحتوي البعض الآخر منها على زوجٍ من حقول فكّ الدمج. في الحالة الأولى، يُستخدَم نفس مفتاح demux على جانبي الاتصال، بينما يستخدم كل طرفٍ في الحالة الثانية مفتاحًا مختلفًا لتحديد البروتوكول عالي المستوى (أو برنامج التطبيق) الذي ستُسلَّم الرسالة إليه. نموذج OSI ذو الطبقات السبع تعدّ منظّمة ISO واحدة من أوائل المؤسسات التي حددت بصفة رسميّة طريقة مشتركة لتوصيل أجهزة الحاسوب. ويوضّح الشكل التالي البنية التي وضعتها، والتي يطلق عليها معمارية OSI اختصارًا للعبارة Open Systems Interconnection أي ربط الأنظمة المفتوحة، وهي تُقسّم وظائف الشبكة إلى سبع طبقات، إذ يُنفّذ بروتوكول واحد أو أكثر الوظيفة المعيّنة لطبقة معينة. بهذا المعنى، فإن الرسم التخطيطي الوارد ليس هو الرسم البياني للبروتوكول، في حد ذاته، بل هو نموذج مرجعي للرسم البياني للبروتوكول. وغالبًا ما يشار إليه باسم نموذج الطبقات السبع. ورغم أنّه لا توجد شبكة قائمة على OSI تعمل اليوم، إلا أنّ المصطلحات التي حددتها لا تزال تُستخدَم على نطاق واسع، لذلك لا تزال تستحق أن نلقي عليها نظرة سريعة. إذا بدأنا من الأسفل واتجهنا صعودًا، نجد أنّ الطبقة المادية (أو الفيزيائية) (physical layer) تعالج إرسال البِتات الخام عبر رابطٍ للاتصال. ثم تجمع طبقة ربط البيانات (data link layer) تدفقًا من البِتات في مجموعة أكبر تُسمى إطارًا (frame). ويتجسّد في العادة مستوى ربط البيانات من خلال مبدّلات الشبكة إلى جانب برامج تشغيل الأجهزة التي تعمل في نظام تشغيل العقدة. هذا يعني أن الإطارات هي التي تُسلَّم فعليًا إلى المضيفين، وليست البِتات الخام. ثمّ تعالج طبقة الشبكة (network layer) التوجيه بين العقد داخل شبكة تبديل الرزم. في هذه الطبقة، تُسمى وحدة البيانات المتبادلة بين العُقَد عادة رزمةً (packet) عوضًا عن الإطار، رغم أنّها نفس الشيء أساسًا. تُنَفَذ الطبقات الثلاث السفلية على جميع عُقَد الشبكة، بما في ذلك المبدّلات داخل الشبكة والمضيفات المتصلة بالجزء الخارجي للشبكة. بعد ذلك، تُنفِّذ طبقة النقل (transport layer) ما سمّيناه لحدّ الآن قناةَ عمليةٍ لعملية. تسمى وحدة البيانات المتبادلة هنا بصفة شائعة رسالةَ (message) عوضًا عن رزمة أو إطار. تعمل طبقة النقل والطبقات الأعلى في العادة فقط على المضيفين النهائيين وليس على المبدّلات الوسيطة أو الموجهات. وإذا قفزنا إلى الطبقة العليا (أي السابعة) واتجهنا نزولًا، فسنجد طبقة التطبيق (application layer). وتتضمن بروتوكولات طبقة التطبيق أشياء مثل بروتوكول نقل النص الترابطي (HTTP) الذي يُعدّ أساس شبكة الويب العالمية إذ يُمَكّن متصفحات الويب من طلب صفحات من خواديم الويب. ثم هناك طبقة العرض (presentation layer) التي تهتمّ بتنسيق البيانات المتبادلة بين النظراء. فهي تبيّن، على سبيل المثال، ما إذا كان عددٌ صحيحٌ مبنيًا على 16 أو 32 أو 64 بِتًا، أو هل يُنقَل البايت الأكثر أهمية في الأول أم في الأخير، أو كيف يكون تنسيق تدفّق الفيديو… وما إلى ذلك. ونجد أخيرًا طبقة الجلسة (session layer) التي توفّر مساحةً اسمية تُستخدم لربط مسارات النقل المختلفة المحتملة التي تشكّل جزءًا من تطبيق واحد. على سبيل المثال، قد تعمل على إدارة تدفقٍ صوتي وتدفقٍ للفيديو يندمجان معًا في تطبيق مؤتمرات عن بعد. معمارية الإنترنت (Internet Architecture) يوضّح الشكل السابق بنية الإنترنت، والتي تسمى أحيانًا بنية TCP/IP نسبةً إلى بروتوكوليها الرئيسيين TCP وIP. ويرد تمثيل بديل لها في الشكل التالي. وقد تطوّرت معمارية شبكة الإنترنت بناءً على تجارب سابقةٍ لشبكةٍ لتحويل الرزم كانت تُسمى آربانيت (ARPANET). أتى تمويل الإنترنت وآربانيت من وكالة مشاريع البحث المتقدم (Advanced Research Projects Agency أو اختصارًا ARPA)، وهي واحدةٌ من وكالات تمويل البحث والتطوير التابعة لوزارة الدفاع الأمريكية. وكانت شبكتا الإنترنت وآربانيت موجودتان قبل معمارية OSI، وكان للخبرة المكتسبة من بنائهما تأثيرٌ كبيرٌ على نموذج OSI المرجعي. يوضح الشكل التالي الرسم البياني لبروتوكول الإنترنت: مع أنّه يمكن تطبيق نموذج OSI المكوّن من 7 طبقات، بقليلٍ من القدرة على التّخيل، على شبكة الإنترنت، فإنّه غالبًا ما تُستخدَم عوضًا عن ذلك مجموعةٌ أبسط. يوجد في المستوى الأدنى مجموعةٌ متنوعة من بروتوكولات الشبكة، يرمز لها على النحو التالي: NET1 وNET2 وإلى ما ذلك. من الناحية العملية، تُنفَّذ هذه البروتوكولات من خلال مجموعة من العتاد (مثل مبدّل الشبكة) والبرمجيات (مثل برنامج تشغيل جهاز الشبكة). على سبيل المثال، قد تجد بروتوكولات خاصة الإيثرنت (Ethernet) أو بالشبكة اللاسلكية (مثل معايير واي فاي 802.11) في هذه الطبقة. (قد تتضمن هذه البروتوكولات بدورها العديد من الطبقات الفرعية، لكن لا تفترض معمارية الإنترنت أي شيءٍ بخصوصها). تتكون الطبقة التالية من بروتوكول واحد، هو بروتوكول الإنترنت (IP). هذا هو البروتوكول الذي يدعم الربط البيني لتقنيات الشبكات المتعدّدة في شبكة متشابكة منطقية واحدة. تحتوي الطبقة الموجودة أعلى طبقة IP على بروتوكولين رئيسيين، هما بروتوكول التحكم في الإرسال (Transmission Control Protocol أو اختصارًا TCP) وبروتوكول مخطّط بيانات المستخدم (User Datagram Protocol أو اختصارًا UDP). يوفر بروتوكولا TCP وUDP قنوات منطقية بديلة لبرامج التطبيق، إذ يوفّر بروتوكول TCP قناة موثوقة لتدفق البايتات، ويوفّر بروتوكول UDP قناة غير موثوقةٍ لتسليم مخطط البيانات (يمكن عد مخطط البيانات بمثابة مرادف للرسالة). في لغة الإنترنت، يُطلق على بروتوكولي TCP وUDP أحيانًا بروتوكولات من طرفٍ لطرف رغم صحة الإشارة إليهما على أنهما بروتوكولا نقل. يعمل فوق طبقة النقل مجموعة من بروتوكولات التطبيق، مثل HTTP وFTP وTelnet (تسجيل الدخول عن بُعد (remote login)) وSMTP (بروتوكول نقل البريد البسيط (Simple Mail Transfer Protocol))، والتي تتيح التشغيل البيني للتطبيقات الشائعة. لكي تفهم الفرق بين التطبيق وبروتوكول طبقة التطبيق، فكّر في جميع متصفحات الويب المختلفة المتاحة حاليًا أو التي كانت متاحة في السابق (على سبيل المثال فايرفوكس وكروم وسفاري وموزايك وإنترنت إكسبلورر). هناك عدد كبير مماثل من التطبيقات المختلفة لخواديم الويب. إنّ ما يُتيح لك استخدام أيّ من برامج التطبيقات هذه للوصول إلى موقع معين على الويب هو أنها تتوافق جميعها مع بروتوكولِ طبقة التطبيق نفسه أي بروتوكول HTTP. لكن، أحيانًا ينطبق المصطلح نفسه بصورة مربكة على كلّ من التطبيق وبروتوكول طبقة التطبيق الذي يستخدمه (على سبيل المثال، غالبًا ما يُستخدم بروتوكول FTP كاسمٍ لتطبيق يقوم على البروتوكول FTP). يوضح الشكل الآتي شكلًا بديلًا لمعمارية الإنترنت، حيث يشير مصطلح طبقة "الشبكة الفرعية (subnetwork)" على ما تشير إليه طبقة "الشبكة (network)" والتي ترمز الآن إلى "الطبقة 2" (تيمّنًا بنموذج OSI). إنّ معظم الأشخاص الذين يعملون بنشاط في مجال الشبكات على دراية بكلّ من معمارية الإنترنت ومعمارية OSI ذات الطبقات السبع، وهناك اتفاق عام حول كيفية توافق الطبقات بين المعماريتين. إذ تُماثل طبقة التطبيق في معمارية الإنترنت الطبقة 7 في معمارية OSI، وتماثل طبقة النقل الطبقة 4، أمّا طبقة IP (طبقة الشبكة) فهي الطبقة 3، وطبقة الرّبط أو الطبقة الفرعية أسفل IP فهي الطبقة 2. إنّ لمعمارية الإنترنت ثلاث ميزات تستحق التركيز عليها. أولاً، كما هو موضح بصورة أفضل في الشكل السابق، فإن معمارية الإنترنت لا تعني طبقات صارمة. إذ تبقى الحرّية للتطبيق في تجاوز طبقات النقل المحددة واستخدام IP أو إحدى الشبكات الأساسية مباشرة. في الواقع، يظلّ المبرمجون أحرارًا في تحديد ملخصات القناة أو التطبيقات الجديدة التي تعمل فوق أي من البروتوكولات الموجودة. ثانيًا، إذا نظرت عن كثب إلى الرسم البياني للبروتوكول في الشكل الذي يوضح نموذج OSI ذو الطبقات السبع، فسوف تلاحظ شكل الساعة الرملية، واسعًا في الأعلى، وضيقًا في المنتصف، ويعود ليتّسع في الأسفل. يعكس هذا الشكل في الواقع الفلسفة المركزية للهندسة المعمارية. أي أن IP يعمل كنقطة محورية للمعمارية، فهو يحدد طريقةً مشتركة لتبادل الرزم بين مجموعة واسعة من الشبكات. فوق الطبقة IP، يمكن أن يكون هناك العديد من بروتوكولات النقل، يقدّم كلّ منها لبرامج التطبيق تجريدًا مختلفًا للقنوات. وهذا يؤدّي إلى فصل مسألة تسليم الرسائل من مضيف لمضيف تمامًا عن مسألة توفير خدمة اتصال مفيدة من عملية لعملية. أمّا تحت الطبقة IP، تسمح هذه المعمارية بوجود العديد من تقنيات الشبكة المختلفة، التي تتراوح من الإيثرنت إلى اللاسلكية مرورًا بالروابط من نقطة لنقطة. الميزة الثالثة لمعمارية الإنترنت (أو بصورة أدقّ، لثقافة IETF) هي أنّه من أجل تضمين بروتوكول جديد بصفة رسمية في المعمارية، يجب أن يكون هناك توصيفٌ للبروتوكول وعلى الأقل تمثيل تطبيقي واحدٌ للتوصيف (ويفضّل أن يكون اثنان). ويعدّ وجود تطبيقات عمليّة للمعايير أمرًا ضروريًا لكي تعتمَدها IETF. يساعد هذا الجانب الثقافي لدى مُجتمع التصميم على ضمان إمكانية تنفيذ بروتوكولات المعمارية بكفاءة. ربّما يكون أفضل مثال على القيمة التي توليها ثقافة الإنترنت للبرامج العملية هي ذلك الاقتباس المكتوب على القمصان التي تُلبس عادةَ في اجتماعات IETF: التقييس (Standardization) و منظمة IETF رغم أننا نسميها "معمارية الإنترنت" عوضًا عن "معمارية IETF"، إلا أنه من الإنصاف القول أن IETF هي هيئة التقييس الأساسية المسؤولة عن تعريفها وعن تحديد العديد من بروتوكولاتها، مثل TCP وUDP، وIP، وDNS ، وBGP. لكن هندسة الإنترنت تشمل أيضًا العديد من البروتوكولات التي تحدّدها المنظمات الأخرى، بما في ذلك معايير إيثرنت 802.11 و الواي فاي الخاصة بمنظمة IEEE، ومواصفات الويب HTTP/HTML الخاصة بمنظمة W3C، ومعايير الشبكات الخلوية 4G و 5G الخاصة بمنظمة 3GPP، ومعايير ترميز الفيديو H.232 الخاصة بمنظمة ITU-T، وغيرها الكثير. بالإضافة إلى تعريف البنى وتحديد البروتوكولات، هناك منظمات أخرى تدعم الهدف الأكبر وهو التشغيل البيني (interoperability). أحد الأمثلة على ذلك هو IANA (هيئة أرقام الإنترنت المخصّصة Internet Assigned Numbers Authority)، والتي كما يوحي اسمها، مسؤولة عن تسليم المعرّفات الفريدة اللازمة لجعل البروتوكولات تعمل. وتعدّ IANA، بدورها، قسمًا داخل ICANN (مؤسسة الإنترنت للأسماء والأرقام المُخصصة Internt Corporation for Assigned Names and Numbers)، وهي منظمة غير ربحية مسؤولة عن الإشراف العام على الإنترنت. من بين هذه الميزات الثلاث لمعمارية الإنترنت، تكتسي فلسفة تصميم الساعة الرملية أهمّية كبيرة وكافية لتكرار الحديث عنها. يمثل الخصر الضيق للساعة الرملية مجموعة صغيرة ومختارة بعناية من القدرات العالمية التي تسمح لكلّ من التطبيقات في المستوى الأعلى وتقنيات الاتصال في المستوى الأدنى بالتعايش ومشاركة القدرات والتطوّر السريع. ويعدّ نموذج الخصر الضيق مهمًا لقدرة الإنترنت على التكيف مع متطلبات المستخدمين الجديدة والتقنيات المتغيرة. ترجمة -وبتصرّف- للقسم Architecture من فصل Foundation من كتاب Computer Networks: A Systems Approach
  4. لقد وضعنا لأنفسنا هدفًا طموحًا يتجلّى في فهم كيفية بناء شبكة حاسوبية من الألف إلى الياء. سيكون نهجُنا لتحقيق هذا الهدف هو الانطلاق من المبادئ الأولى ثم طرح أنواع الأسئلة التي نطرحها بصورةٍ طبيعية إذا كان الأمر يتعلّق ببناء شبكة بالمعنى الحرفي. سوف نستخدم في كلّ خطوة بروتوكولات اليوم لتوضيح خيارات التصميم المختلفة المتاحة لنا، لكننا لن نقبل هذه القطع الأثرية الموجودة على أنها أمورٌ مقدّسةٌ. عوضًا عن ذلك، سوف نسأل (ونبحث عن الأجوبة اللازمة) عن الغرض من تصميم الشبكات بالطريقة التي هي عليها. ورغم أنّه من المغري الاستسلام لمجرّد فهم الطريقة التي تحدث بها الأمور اليوم، فمن المهمّ التعرف على المفاهيم الأساسية لأن الشبكات تتغير باستمرار مع تطوّر التكنولوجيا واختراع التطبيقات الجديدة. ومن خلال تجربتنا ندرك أنه بمجرد فهمك للأفكار الأساسية، سيكون من السهل نسبيًا استيعاب أي بروتوكول جديد تواجهه. أصحاب المصلحة (Stakeholders) مثلما ذكرنا سابقًا، يمكن للطالب الذي يدرس الشبكات أن يأخذ عدة وجهات نظر. عندما أُلِّفَت الطبعة الأولى الإنجليزية من هذا الكتاب، لم يكن لدى غالبية السكان اتصال بالإنترنت على الإطلاق، وأولئك الذين حصلوا عليه تمكّنوا من ذلك أثناء العمل أو في الجامعة أو عن طريق مودِم الاتصال الهاتفي (dial-up modem) في المنزل. كانت التطبيقات الشائعة حينذاك تعَدّ على أصابع اليد الواحدة. وهكذا، مثل معظم الكتب في ذلك الوقت، ركّز الكتاب على منظور شخصٍ يعمل على تصميم معدّات وبروتوكولات الشبكات. وسوف نواصل التركيز على هذه الرّؤية، ونأمل أنه بعد قراءة هذا الكتاب، سوف تعرف كيفية تصميم معدّات وبروتوكولات الشبكات المستقبلية. ومع ذلك، نريد أيضًا تغطية وجهات نظر اثنين من أصحاب المصلحة الإضافيين: أولئك الذين يطورون التطبيقات الشبكية وكذلك من يديرون الشبكات أو يشغلونها. دعنا نفكر كيف يمكن لأصحاب المصلحة الثلاثة هؤلاء عرض متطلباتهم لشبكة ما: مبرمجُ التطبيق (application programmer): يعرض قائمةَ الخدمات التي يحتاجها تطبيقه على سبيل المثال، ضمان تسليم كل رسالة يرسلها التطبيق دون خطأٍ في غضون فترة زمنية معيّنة أو القدرة على التبديل بأمان بين الاتصالات المختلفة بالشبكة عندما يتنقّل المستخدم بينها. مُشغّل الشبكة (network operator): يعرض خصائص النظام الذي يسهل تدبيره وإدارته على سبيل المثال، حيث يمكن عزل الأخطاء بسهولة، ويمكن إضافة أجهزة جديدة إلى الشبكة وإعدادها بصفة صحيحة، ويكون من السّهل حساب مقدار الاستخدام. مصمّم الشبكة (network designer): يعرض خصائص التصميم الفعّال نسبةً إلى التكلفة على سبيل المثال، أن يكون استخدام موارد الشبكة بكفاءة وتخصيصها إلى حدّ ما لمختلف المستخدمين. ومن الرّاجح أيضًا أن تكون لمسألة الأداء أهمّية في هذا السياق. يحاول هذا القسم استخلاص متطلّبات أصحاب المصلحة المعنيين على اختلافهم من أجل عرضٍ رفيع المستوى للاعتبارات الرئيسية التي تُوجِّه تصميم الشبكة، وبذلك، يُحدّد التحديات التي تتناولها بقية أجزاء هذا الكتاب. الاتصال القابل للتوسع (Scalable Connectivity) إذا بدأنا بالأمور البديهية، فيجب أن توفّر الشبكة إمكانية الاتصال بين مجموعةٍ من أجهزة الحاسوب. في بعض الأحيان يكون إنشاء شبكة محدودة لا تربط سوى عددٍ قليل من الأجهزة المحددة كافيًا. في الحقيقة، ولأسباب تتعلق بالخصوصية والأمان، يكون لدى العديد من شبكات الشركات الخاصة هدفٌ صريحٌ يتجلّى في الحدّ من مجموعة الأجهزة المتصلة. وعلى النقيض من ذلك، تُصمَّم الشبكات الأخرى (التي يعد الإنترنت المثال الرئيسي لها) للنمو على نحوٍ يتيح لها إمكانية توصيل جميع أجهزة الحاسوب في العالم. يُقال عن النّظام المصمَّم لكي يدعم النموّ العشوائي إلى حجمٍ كبيرٍ أنّه مُتوسّع (scale). ويعالج هذا الكتاب تحدّي قابلية التوسع متّخذًا من شبكة الإنترنت نموذجًا. لفهم متطلبات الاتصال بصورة كاملة، نحتاج إلى إلقاء نظرة فاحصة على كيفية توصيل أجهزة الحاسوب في الشبكة. إذ يحدث الاتصال على العديد من المستويات المختلفة. في المستوى الأدنى، يمكن أن تتكوّن الشبكة من جهازي حاسوب أو أكثر متصلة مباشرة ببعض الوسائط الفيزيائية، مثل الكابل المِحوري (coaxial cable) أو الألياف البصرية. نسمي مثل هذا الوسيط الفيزيائي رابطًا (link)، وغالبًا ما نشير إلى أجهزة الحاسوب التي تربطها هذه الوسائط بالعُقَد (nodes). (أحيانًا لا تكون العقدة جهاز حاسوب بل قطعةً فيزيائية أكثر تخصيصًا في الجهاز، لكننا نتجاهل هذا التمييز لأغراض هذه المناقشة). كما هو موضّح في الشّكل التالي، تقتصر الروابط الفيزيائية أحيانًا على عُقدتين (يُسمّى مثل هذا الرابط "من نُقطة لِنُقطة" point-to-point) كما هو موضح في القسم (أ) من الشكل التالي، بينما في حالات أخرى قد تشترك أكثر من عقدتين في رابطٍ فيزيائي واحد (ويُسمّى مثل هذا الرابط متعدّد الوصول) كما هو موضح في القسم (ب) من الشكل التالي. تعدّ الروابط اللاسلكية، مثل تلك التي توفرها الشبكات الخلوية وشبكات الواي فاي، فئةً مهمة من روابط الوصول المتعدد. دائمًا ما يكون حجم روابط الوصول المتعدد محدودًا، من حيث المسافة الجغرافية التي يمكن أن تغطيها وعدد العقد التي يمكنها توصيلها. لهذا السبب، يطبّقون في الغالب ما يسمى بقاعدة الميل الأخير، ويربطون المستخدمين النهائيين ببقية الشبكة. إذا كانت شبكات الحواسيب تقتصر على المواقف التي تكون فيها جميع العُقد متصلةً ببعضها البعض مباشرة عبر وسيط فيزيائي مشترك، فإن الشبكات إما ستكون محدودة جدًا في عدد أجهزة الحاسوب التي يمكن توصيلها، أو أنّ عدد الأسلاك الخارجة من الجزء الخلفي من كلّ عقدة ستصبح بسرعة غير قابلة للإدارة ومكلفة للغاية. لحسن الحظ، لا يعني الاتصال بين عقدتين بالضرورة اتصالًا ماديًا مباشرًا بينهما، إذ يمكن تحقيق الاتصال غير المباشر بين مجموعة من العقد المتعاونة. لنأخذ المثالين التاليين عن كيفية توصيل مجموعة من أجهزة الحاسوب بصورة غير مباشرة. يُظهر الشكل التالي مجموعةً من العُقد، كلّ منها مرتبط بواحد أو أكثر من الروابط من نقطة لنقطة. تعمل هذه العقد المرفقة برابطين على الأقل على تشغيل البرامج التي تعيد توجيه البيانات التي يجري تلقيها على رابط واحد من رابطٍ آخر. إذا نُظِّمت بطريقة منظمة، فإن عُقد التوصيل هذه تُشكّل شبكة تبديل (switched network). وهناك أنواع عديدة من شبكات التبديل، لكن أكثرها شيوعًا هي شبكات تبديل الدارات (circuit switched) وتبديل الرزم (packet switched). يُستخدَم النوع الأول بصفة خاصّة في نظام الهاتف، بينما يُستخدَم النوع الثاني في الغالبية العظمى في شبكات الحواسيب، وهو ما سيكون محور هذا الكتاب. (ومع ذلك، نلاحظ عودة نسبية لنظام تبديل الدارات في مجال الشبكات البصرية، والذي اتضحت أهمّيته بحكم تزايد الطلب على سعة الشبكة باستمرار). وتتجلى الميزة المهمّة في شبكات تبديل الرزم في أنّ العُقَد في هذه الشبكات ترسل كُتلًا منفصلة من البيانات فيما بينها. فَكّر في هذه الكتل من البيانات على أنها تتطابق مع جزء من بيانات التطبيق مثل ملف أو جزء من بريد إلكتروني أو صورة. نحن نسمي كل كتلة من البيانات إما رزمة أو رسالة، والآن نستخدم هذه المصطلحات بصفةٍ تبادليةٍ. تستخدم شبكاتُ تبديل الرزَم (Packet-switched networks) في العادة إستراتيجيةً تسمى خزّن وأعِد التوجيه (store-and-forward). ومثلما يوحي هذا الاسم، تتلقى كل عقدة في شبكة التخزين وإعادة التوجيه أولًا رزمة كاملة عبر رابطٍ ما، وتُخزّن الرزمة في ذاكرتها الداخلية، ثم تُعيد توجيه الحزمة الكاملة إلى العقدة التالية. على النقيض من ذلك، تُنشِئ شبكة تبديل الدارات أولًا دارةً مُخصّصة عبر سلسلة من الروابط ثم تسمح للعقدة المصدر بإرسال مجرىً من البِتات عبر هذه الدارة إلى عقدةٍ وِجهةٍ. إنّ الدافع الرئيسي لاستخدام تبديل الرزم عوضًا عن تبديل الدارات في الشبكة الحاسوبية هو عنصر الكفاءة، الذي نناقشه في القسم الفرعي التالي. تُميز السحابة في الشكل السابق بين العُقد الموجودة في الداخل التي تُزوّد الشبكة (ويطلق عليها عادة مُبدّلات (switches)، ووظيفتها الأساسية هي تخزين الرزم وإعادة توجيهها) والعُقَد الموجودة خارج السحابة التي تستخدم الشبكة (فتُسمى عادةً المضيفين (hosts)، وهم الذين يدعمون المستخدمين ويديرون برامج التطبيقات). ينبغي الإشارة أيضًا أنّ السحابة هي واحدة من أهم رموز شبكات الحواسيب. نستخدم سحابة للدلالة على أي نوع من الشبكات، سواء كان رابطًا واحدًا من نقطة لنقطة، أو رابطًا متعدّد الوصول، أو شبكة تبديل. وبالتالي، كلّما رأيت سحابة مستخدمة في الشكل، يمكنك التفكير فيها كعنصر يمثّل أيًّا من تقنيات الشبكات التي يغطيها هذا الكتاب. يُظهر الشكل التالي الطريقة الثانية التي يمكن من خلالها توصيل مجموعة من الحواسيب بصفة غير مباشرة. في هذه الحالة، هناك مجموعة من الشبكات المستقلة (السحابات) المتصلة بعضها ببعضٍ لتشكيل شبكة متشابكة (internetwork) أو بمعنى آخر شبكة إنترنت. نعتمد اصطلاح إنترنت للإشارة إلى شبكة متشابكةٍ عامة من الشبكات بحرف "i" صغير في المفردة الإنجليزية (internet)، ونعتمد لشبكة الإنترنت TCP/IP التي نستخدمها جميعًا كل يوم الحرف الكبير "I" في المفردة الإنجليزية (Internet). عادةً ما تُسمى العقدة المتصلة بشبكتين أو أكثر موجِّهًا (router) أو بوابةً (gateway)، وهي تلعب دورًا مماثلًا تمامًا للمبدّل (switch)، فهي تعيد توجيه الرسائل من شبكةٍ إلى أخرى. لاحظ أنه يمكن عدُّ شبكة إنترنت في حد ذاتها نوعًا آخر من الشبكات، مما يعني أنه يمكن بناء شبكةٍ متشابكة من مجموعةٍ من الشبكات المتشابكة. وبالتالي، يمكننا بصفة متكرّرة بناء شبكات كبيرة بصورةٍ اعتباطية عن طريق ربط السُّحب لتشكيل سُحبٍ أكبر. نستطيع القول على نحوٍ معقولٍ أن فكرة الربط بين الشبكات المختلفة اختلافًا كبيرًا كانت الابتكار الأساسي للإنترنت وأن النمو الناجح للإنترنت إلى حجمها العالمي والمليارات من العُقَد كان ثمرة بعض قرارات التصميم الجيدة جدًا التي اتخذها مهندسو الإنترنت الأوائل، والتي سنناقشها لاحقًا. إنّ مجرّد وجود مجموعة من المضيفين متصلين بعضهم ببعضٍ بصورةٍ مباشرة أو غير مباشرة لا يعني بالضّرورة أننا نجحنا في توفير اتصال من مُضيفٍ لمُضيفٍ. فالهدف النهائي هو أن تكون كلّ عقدة قادرةً على تحديد أي عُقَد أخرى على الشبكة تريد التواصل معها، ويتم ذلك عن طريق إسناد عنوانٍ (address) لكل عقدة. العنوان هو سلسلة بايتات تُحدّد عقدةً؛ أي أن الشبكة يمكنها استخدام عنوان العقدة لتمييزها عن العُقَد الأخرى المتصلة بالشبكة. عندما تريد العقدة المصدر تسليم رسالة عبر الشبكة إلى عُقدةٍ وِجهةٍ معينةٍ، فإنها تحدّد عنوان العُقدةِ الوِجهة. إذا لم تكن العُقدتان المرسلة والمستقبلة متصلتين مباشرةً، فإنّ مبدّلات الشبكة والموجهات تستخدم هذا العنوان لتحديد كيفية إعادة توجيه الرسالة نحو الوِجهَة. تسمى عملية تحديد كيفية إعادة توجيه الرسائل نحو العُقدة الوِجهة بصورةٍ منهجيةٍ بناءً على عنوانها عمليةَ التوجيه (routing). تفترض هذه المقدمة الموجزة عن العنونة والتوجيه أنّ العقدة المصدر تريد إرسال رسالة إلى عقدةٍ وِجهةٍ واحدةٍ (البث الأحادي unicast). ورغم أنّ هذا السيناريو هو الأكثر شيوعًا، فمن الممكن أيضًا أنّ العقدة المصدر ترغب في بث رسالةٍ إلى جميع العُقد على الشبكة، أو قد ترغب العقدة المصدر في إرسال رسالةٍ إلى مجموعة فرعية من العُقَد الأخرى ولكن ليس جميعها، وهي حالة تسمى البث المتعدد (multicast). وبالتالي، بالإضافة إلى العناوين الخاصة بالعقدة، فمن متطلّبات الشبكة كذلك أن تدعم عناوين البث المتعدّد والبثّ العريض (broadcast). مشاركة الموارد ذات التكلفة الفعالة مثلما ذكرنا في السابق، يركّز هذا الكتاب على شبكات تبديل الرزَم. ويشرح هذا القسم المتطلّبات الرئيسية لشبكات الحواسيب ذات الفعالية التي تقودنا إلى اتخاذ تبديل الرزَم خيارًا استراتيجيًا. بالنظر إلى مجموعة من العُقَد المتصلة بصورةٍ غير مباشرة عن طريق تداخل الشبكات، فمن الممكن أن يُرسل أي زوج من المضيفين رسائل فيما بينهما عبر سلسلة من الروابط والعُقَد. وبطبيعة الحال، لا نرغب في الاكتفاء بمجرّد دعم اتصال زوج واحدٍ فقط من المضيفين، بل نريد أن نوفر لجميع أزواج المضيفين القدرة على تبادل الرسائل. والسؤال إذًا، كيف يشترك جميع المضيفين الذين يرغبون في التواصل في الشبكة، خاصة إذا كانوا يريدون استخدامها في نفس الوقت؟ وإذا لم يكن هذا مشكلة مُقلقة كفايةً، فكيف سيشترك العديد من المضيفين في نفس الرابط عندما يرغبون جميعًا في استخدامه في نفس الوقت؟ لكي نفهم كيف يتشارك المضيفون في الشبكة، نحتاج إلى تقديم مفهوم أساسي، هو الدمج أو تعدد الإرسال (multiplexing)، والذي يعني أن أحد موارد النّظام مشترَكٌ بين عدّة مستخدمين. يمكن تفسير تعدد الإرسال، على نحوٍ بديهيٍّ، بالقياس إلى نظام الحاسوب القائم على مفهوم اقتسام الوقت الذي تشترك فيه مهامّ متعددة معالجًا فعليًا واحدًا، وتعتقد كلّ منها أنّ لها معالجًا خاصًّا بها. وبالمثل، يمكن دمج البيانات التي يرسلها عدة مستخدمين عبر الروابط الفيزيائية التي تُشكّل شبكةً ما. ومن أجل معرفة كيف يعمل هذا الأمر، نفترض الشبكة البسيطة الموضحة في الشكل التالي، إذ يرسل المضيفون الثلاثة المتواجدون على الجانب الأيسر من الشبكة (المرسلون S3-S1) البيانات إلى المضيفين الثلاثة على اليمين (المستقبلون R3-R1) من خلال تشارك شبكة تبديلٍ تحتوي على رابطٍ فيزيائي واحدٍ فقط. (من أجل التبسيط، نفترض أن المضيف S1 يرسل البيانات إلى المضيف المقابل R1، والأمر نفسه بالنسبة للبقية). في هذه الحالة، سوف يجري تجميع ثلاث تدفقات من البيانات، الموافقة لأزواج المضيفين الثلاثة، على رابطٍ فيزيائي واحدٍ عن طريق المبدّل 1، ومن ثم فكّ الدمج مرة أخرى إلى تدفقات منفصلة عن طريق المبدّل 2. لاحظ أننا نتعمد الغموض لما يحتويه "تدفق البيانات" بالضبط. ولأغراض هذه المناقشة، نفترض أنّ لكلّ مضيف على اليسار مقدارٌ كبير من البيانات التي يريد إرسالها إلى نظيره على اليمين. هناك طرقٌ عديدة مختلفة لدمج (multiplexing) التدفقات المتعددة على رابطٍ فيزيائي واحد. إحدى الطرق الشائعة هي تعدّد الإرسال بالتقسيم الزمني المتزامن (synchronous time-division multiplexing أو اختصارًا STDM). تتمثّل فكرة STDM في تقسيم الوقت إلى كمّات (quanta) زمنية متساوية، ومنح الفرصة بالدّور لكلّ تدفقٍ كي يُرسِل بياناته عبر الرابط المادي. وبعبارة أخرى، تُنقل البيانات من S1 إلى R1 أثناء الكمّ 1، وخلال الكم الزمني 2، تُرسَل البيانات من S2 إلى R2؛ وفي الكمّ 3، يُرسل S3 البيانات إلى R3. عند هذه النقطة، يبدأ التدفق الأول (S1 إلى R1) مرة أخرى، وتتكرّر العملية. طريقةُ أخرى تستخدم كثيرًا هي تعدّد الإرسال بتقسيم التردد (frequency-division multiplexing أو اختصارًا FDM). وتتجلى فكرة FDM في إرسال كل تدفّق عبر الرابط الفيزيائي بتردّدٍ مختلفٍ، بالطريقة نفسها التي تُرسَل بها الإشارات لمحطّات التلفزيون المختلفة بتردّدات مختلفة عبر موجات الهواء أو على رابط الكابل التلفزيوني المِحوري. ورغم سهولة فهمهما، إلّا أنّ كلا الطريقتين STDM و FDM تُظهران بعض القصور في نقطتين مهمّتين. أولًا، عندما لا يكون لدى أيّ من التدفقات (أزواج المضيفين) أيّ بيانات لإرسالها، فإنّ نصيبها من الرابط الفيزيائي، أي من الكمّ الزمني أو التردّد، يظلّ خاملًا، حتى لو كان لدى أحد التدفقات الأخرى بيانات لإرسالها. على سبيل المثال، سيكون على المرسل S3 انتظار دوره خلف S1 و S2 في الفقرة السابقة، حتى لو لم يكن لدى S1 و S2 أي شيء لإرساله. وفي معايير الاتصالات الحاسوبية، يمكن أن يكون مقدار الوقت الذي يبقى فيه الرابط خاملًا كبيرًا جدًا. يمكنك أن تأخذ على سبيل المثال مقدار الوقت الذي تقضيه في قراءة صفحة ويب (ترك الرابط خاملًا) وتُقارِنه بالوقت الذي تقضيه في جلب الصفحة. ثانيًا، يقتصر كلّ من STDM و FDM على المواقف التي يكون فيها الحدّ الأقصى لعدد التدفقات ثابتًا ومعروفًا مسبقًا. ولن يكون تغيير حجم الكمّ أو إضافة كمّات إضافية في حالة STDM أو إضافة تردّدات جديدة في حالة FDM، أمرًا عمليًّا. تُسمّى صيغة تعدّد الإرسال التي تعالج هذه العيوب، والتي نستخدمها بصورة أكبر في هذا الكتاب، تعدّد الإرسال الإحصائي statistical multiplexing. ورغم أنّ الاسم لا يساعدُ كثيرًا في فهم الفكرة، إلا أن تعدّد الإرسال الإحصائي بسيط للغاية في الحقيقة، ويتلخّص مفهومه في فكرتين رئيسيتين. أولًا، إنه يشبه STDM في مسألة المشاركة الزمنية للرابط الفيزيائي، إذ تُرسَل البيانات أولًا من تدفق واحد عبر الرابط الفيزيائي، ثم تُرسَل البيانات من تدفق آخر، وهكذا. ولكن، على عكس STDM، تُرسل هذه البيانات من كلّ تدفق عند الطلب وليس خلال فترة زمنية محددة مسبقًا. وبالتالي، إذا كان هناك تدفق واحد فقط يحتوي على بيانات لإرسالها، فإنه يمكنه إرسال هذه البيانات دون انتظار كمّه الزّمني الخاصّ، وبالتالي دون الحاجة إلى انتظار مرور الكمّات المخصصة للتدفقات الأخرى دون استخدام. إنّ ميزة تفادي الوقت الضائع هذه هي التي تمنح تبديل الرزَم فعاليته. ومع ذلك، مثلما سنوضّحه فيما بعد، ليس لدى تعدّد الإرسال الإحصائي آليةٌ لضمان أن كل التدفقات تحصل في النهاية على دورها للإرسال عبر الرابط الفيزيائي. أي أنّه بمجرد أن يبدأ التدفق في إرسال البيانات، نحتاج إلى طريقة ما للحد من الإرسال، على النحو الذي لا يضيع فيه دور للتدفقات الأخرى. لمراعاة هذه الحاجة، يحدّد تعدد الإرسال الإحصائي سقفًا أعلى لحجم كتلة البيانات التي يُسمح لكلّ تدفق بإرسالها في وقت معين. يشار عادة إلى كتلة البيانات ذات الحجم المحدود على أنها رزمة، لتمييزها عن الرسالة الكبيرة العشوائية التي قد يرغب برنامج التطبيق في إرسالها. نظرًا لأن شبكة تبديل الرزم تَحُدّ من الحجم الأقصى للرزم، فقد لا يتمكن المضيف من إرسال رسالة كاملة في رزمةٍ واحدة. قد يحتاج المصدر إلى تجزئة الرسالة إلى عِدّة رزَمٍ، مع قيام المُستَقبِل بإعادة تجميع الرزم مرة أخرى في الرسالة الأصلية. بعبارة أخرى، يُرسِل كل تدفّق سلسلةً من الرزم عبر الرابط الفيزيائي، وفق قرارٍ مُتّخَذٍ بشأن أيّ من التدفقات سيرسِل رزمته بعد ذلك على أساس أنّ الإرسال يكون رزمةً رزمةً. لاحظ أنّه إذا كان هناك تدفّقٌ واحدٌ فقط لديه بيانات لإرسالها، فيمكنه إرسال تسلسل من الرزم المتتالية؛ ومع ذلك، في حالة وجود بيانات لإرسالها من أكثر من تدفق واحدٍ، فإنّ رزمها تتشابك على الرابط. يُصوّر الشكل السابق مبدّلًا يعمل على دمج الرزم من مصادر متعددة على رابطٍ مشترك واحدٍ. يمكن اتخاذ القرار بشأن أيّ من الرزم ستُرسَل بعد ذلك على رابطٍ مشتركٍ بعدة طرقٍ مختلفة. على سبيل المثال، في شبكة تتكوّن من مبدّلات متصلة بعضُها ببعضٍ عبر روابط، يُتّخذ القرار على مستوى المبدّل الذي ينقل الرزَم إلى الرابط المشترك. (مثلما سنرى لاحقًا، لا تشتمل جميع شبكات تبديل الرزم في الواقع على مبدّلات، وقد تستخدم آليات أخرى لاتخاذ ذلك القرار). يتخذ كل مبدّل في شبكة تبديل الرزم هذا القرار بشكل مستقلّ، على أساس رزمةٍ بعد رزمةٍ. لكنّ إحدى المشكلات التي تواجه مصمّم الشبكة هي كيفية اتخاذ هذا القرار بطريقة عادلة. على سبيل المثال، يمكن تصميم المبدّل لخدمة رزم البيانات على أساس الداخل أولًا، يخرج أولًا (first-in, first-out أو اختصارًا FIFO). وهناك نهج آخر يتمثّل في إرسال الرزم من كل من التدفقات المختلفة التي ترسل البيانات حاليًا من خلال المبدّل بتخصيصٍ دوريٍ. يمكن القيام بذلك للتأكد من أن تدفقات معينة تتلقى حصّة معينة من حيز النطاق التراسلي (bandwidth) للرابط أو أنّ رزمَها لم تتأخر أبدًا في المبدّل لأكثر من فترة زمنية محدّدة. يقال أحيانًا أن الشبكة التي تحاول تخصيص حيز النطاق التراسلي لتدفقات معينة تدعم جودة الخدمة (quality of service أو اختصارًا QoS). يمكن أن نلاحظ أيضًا في الشكل السابق أنه نظرًا لأنّ على المبدّل أن يدمج ثلاثة تدفقات رزم واردة على رابطٍ صادرٍ واحد، فمن الممكن أن يتلقى المبدّل الرزم بأسرع مما يمكن للرابط المشترك استيعابه. في هذه الحالة، يضطرّ المبدّل إلى تخزين هذه الرزم مؤقتًا في ذاكرته. وإذا كان المبدّل يتلقى الرزم بأسرع مما يمكنه أن يرسلها لفترة طويلة من الزمن، فهذا سيجعل المبدّل في نهاية المطاف يشتغل فوق طاقته التخزينية المؤقتة، وسيضطرّه ذلك إلى إسقاط بعض الرزم. عندما يشتغل المبدّل في هذا الوضع، نقول إنه مُحتقِن (congested). دعم الخدمات المشتركة ركزت المناقشة السابقة على التحدّيات التي ينطوي عليها توفير اتصالٍ فعالٍ نسبة إلى التكلفة بين مجموعة من المضيفين، ولكن سيكون النظر إلى الشبكة الحاسوبية على أنّها مجرّد توصيلٍ للرزَم بين مجموعة من أجهزة الحاسوب نوعًا من الإفراط في التبسيط. إنّ الأدقّ هو التفكير في الشبكة على أنّها توفر وسائل التواصل لمجموعة من عمليات التطبيقات المُوزّعة على أجهزة الحاسوب هذه. وبعبارة أخرى، فإن المتطلب التالي للشبكة الحاسوبية هو أن تكون برامج التطبيقات التي تعمل على الأجهزة المضيفة المتصلة بالشبكة قادرةَ على التواصل بصورة مُجدِية. تحتاج الشبكة من منظور مطوّر التطبيقات، إلى القدرة على تسهيل حياته. عندما يحتاج برنامجان تطبيقيان إلى التواصل فيما بينهما، يجب أن تحدث أشياء معقدة كثيرة تتجاوز مجرد إرسال رسالة من مضيف إلى آخر. قد يكون أحد الخيارات هو أن ينشئ مصممو التطبيقات كل تلك الوظائف المعقدة في كل برنامج تطبيقيٍ. ومع ذلك، نظرًا لأن العديد من التطبيقات تحتاج إلى خدمات مشتركة، سيكون من المنطقي تنفيذ هذه الخدمات المشتركة مرة واحدة ثم السماح لمصمّم التطبيقات بإنشاء تطبيقه بالاعتماد على هذه الخدمات. إن التحدّي الذي يواجه مصمّم الشبكة هو تحديد المجموعة الصحيحة من الخدمات المشتركة بهدف إخفاء تعقيدات الشبكة عن التطبيق دون فرض قيودٍ مفرطةٍ على مصمم التطبيقات. ننظر إلى الشبكة بصورة بديهية على أنّها توفّر قنوات منطقية يمكن من خلالها للعمليات على مستوى التطبيق التواصل فيما بينها، وتوفر كل قناةٍ مجموعة الخدمات التي يتطلبها هذا التطبيق. بعبارة أخرى، تمامًا مثلما نستخدم السحابة لتمثيل الاتصال بصورة مجرّدة بين مجموعة من أجهزة الحاسوب، فإننا ننظر الآن إلى القناة على أنها تربط عمليةً بأخرى. يوضح الشكل التالي زوجًا من العمليات على مستوى التطبيق تتواصل عبر قناة منطقيّة تُنفّذ بدورها فوق سحابة تربط مجموعة من المضيفين. يمكننا أن نَعُدّ القناة أنبوبًا يربط بين تطبيقين على النّحو الذي يتيح للتطبيق المرسل وضعَ البيانات في طرف واحد ويترقّب تسليم البيانات عبر الشبكة إلى التطبيق في الطرف الآخر من الأنبوب. تُطبَّق القنوات المنطقية من عملية لعمليةٍ على مجموعة من القنوات الفيزيائية من مضيفٍ لمضيفٍ مثل أي إجراءٍ تجريدي. هذا هو جوهر مفهوم الطبقات، وحجر الزاوية في معماريات الشبكات التي نُناقشها في القسم التالي. ويكمن التحدي هنا في معرفة الوظائف التي يجب أن تُوفّرها القنوات لبرامج التطبيقات. على سبيل المثال، هل يتطلب التطبيق ضمان تسليم الرسائل المرسلة عبر القناة، أم أنّ الأمر سيكون مقبولًا إذا فشل وصول بعض الرسائل؟ هل من الضروري أن تصل الرسائل إلى العملية المُستَقبِلة بنفس الترتيب الذي تُرسَل به، أم أن المُستلِم لا يهتم بترتيب وصول الرسائل؟ هل تحتاج الشبكة إلى التأكد من عدم وجود أطراف ثالثة قادرة على التنصت على القناة، أو أن الخصوصية ليست مصدر قلق؟ بصورة عامة، توفر الشبكة مجموعةً متنوعة من أصناف القنوات المختلفة، بما يتيح لكل تطبيقٍ أن يختار النوع الذي يلبّي احتياجاته على أفضل وجه. يوضّح الجزء المتبقي من هذا القسم الأسلوب المتّبع في تحديد القنوات المفيدة. تحديد أنماط الاتصال المشتركة يقتضي تصميم القنوات المجردة في البداية فهم احتياجات الاتصال لمجموعة تمثيلية من التطبيقات، ثم استخراج متطلبات الاتصال المشتركة الخاصة بها، وأخيرًا دمج الوظائف التي تلبي هذه المتطلبات في الشبكة. يعدّ برنامج الوصول إلى الملفات مثل بروتوكول نقل الملفات (File Transfer Protocol أو اختصارًا FTP) أو نظام ملفات الشبكة (Network File System أو اختصارًا NFS) أحد أقدم التطبيقات المدعومة على أي شبكة. ورغم اختلاف العديد من التفاصيل، كطريقة نقل الملفات على سبيل المثال التي تتمّ بالكامل عبر الشبكة أو أن القراءة أو الكتابة تكون لكُتل مفردةٍ فقط من الملف في وقت معين، فإن عنصر الاتصال الخاصّ بالولوج إلى الملف عن بعد يُدرِج زوجًا من العمليات، إحداهما تطلب قراءة الملف أو كتابته والعملية الثانية تلبّي هذا الطلب. العملية التي تطلب الوصول إلى الملف تسمى العميل (client)، والعملية التي تدعم الولوج إلى الملف تسمى الخادوم (server). تنطوي قراءة الملف على طلب صغير من العميل إلى الخادوم يرسله في رسالةٍ وعلى استجابة الخادوم برسالة كبيرة تحتوي على البيانات الموجودة في الملف. أما الكتابة فتتمّ على نحوٍ معاكسٍ، إذ يُرسِل العميل رسالة كبيرة تحتوي على البيانات المراد كتابتها إلى الخادوم، ويستجيب الخادوم برسالة صغيرة تؤكد أن الكتابة على القرص قد حدثت. المكتبة الرقمية هي تطبيق أكثر تعقيدًا من نقل الملفات، ولكنها تتطلب خدمات اتصال مماثلة. على سبيل المثال، تُدير رابطة مكائن الحوسبة (Association for Computing Machinery أو اختصارًا ACM) مكتبة رقمية كبيرة لمؤلّفات علوم الحاسوب على الرابط التالي : http://portal.acm.org/dl.cfm تحتوي هذه المكتبة على مجموعة واسعة من ميزات البحث والتصفح لمساعدة المستخدمين في العثور على المقالات التي يريدونها، ولكن في نهاية المطاف فإن معظم ما تفعله هو الاستجابة لطلبات المستخدمين للملفات كالنسخ الإلكترونية لمقالات المجلات مثلًا. بالاعتماد على خدمة الولوج إلى الملفات، وعلى المكتبة الرقمية، وكذلك تطبيقي الفيديو المذكورين في المقدمة (المؤتمرات عبر الفيديو والفيديو حسب الطلب) كعيناتٍ نموذجيةٍ، رُبّما نقرّر أن نوفّر نوعين من القنوات: قنوات الطلب/الاستجابة (request/reply) وقنوات تدفّق الرسائل (message stream). سوف يُستخدَم النوع الأول في تطبيقات نقل الملفات والمكتبات الرقمية، وسوف يضمن استلام كلّ رسالةٍ مُرسَلةٍ من أحد الجانبين إلى الجانب الآخر، كما سيضمن تسليم نسخة واحدة فقط من كلّ رسالة. وقد تحمي قناة الطلب/الاستجابة أيضًا خصوصية وتكامل البيانات التي تتدفّق عليها حتى لا تتمكّن الأطراف غير المصرّح لها من قراءة أو تعديل البيانات التي يجري تبادُلها بين عمليات العميل والخادوم. أما قناة تدفق الرسائل فيمكن استخدامها في تطبيقات الفيديو عند الطلب والمؤتمرات عبر الفيديو، شريطة أن تكون مُهيّأة لدعم حركة المرور أحادية الاتجاه وذات الاتجاهين ودعمٍ خصائص التأخير المختلفة. قد لا تحتاج قناة تدفق الرسائل إلى ضمان تسليم جميع الرسائل، إذ يمكن أن يعمل تطبيق الفيديو بصورة مناسبة حتى لو لم يستقبل بعضَ مقاطع الفيديو. ومع ذلك، ينبغي التأكد من وصول تلك الرسائل المُستلَمة بالترتيب نفسِه الذي أُرسِلت به، لتجنب عرض المقاطع خارج تسلسلها السليم. ومثلما هو الأمر بالنسبة لقناة الطلب/الاستجابة، قد ترغب قناة تدفّق الرسائل في ضمان خصوصية بيانات الفيديو وتكاملها. أخيرًا، قد تحتاج قناة تدفق الرسائل إلى دعم البث المتعدّد من أجل تمكين أطراف متعدّدة من المشاركة في المؤتمر عن بعد أو مشاهدة الفيديو. يسعى مصمّمو الشبكة غالبًا للحصول على أقلِّ عدد من أنواع القنوات المجردة التي يمكنها أن تخدم أكبر عدد من التطبيقات، إلا أنّ هناك خطرًا في محاولة الاكتفاء بعدد قليل جدًا من القنوات المجردة. ببساطة، إذا كانت لديك مطرقة، فكل شيء سيبدو لك مثل المسمار. على سبيل المثال، إذا كان كلّ ما لديك من أنواع القنوات هو قنوات تدفق الرسائل وقنوات الطلب/الاستجابة، فسيغريانك باستخدامهما لتطبيقك التالي، حتى لو لم يكن أيّ منهما يوفّر المتطلبات التي يحتاجها التطبيق بالضبط. وهكذا، من الراجح أن يخترع مصممو الشبكات أنواعًا جديدة من القنوات، وأن يضيفوا بعض الخيارات إلى القنوات الحالية، مادام مبرمجو التطبيقات يخترعون تطبيقات جديدة. لاحظ أيضًا أنه بغض النظر عن الوظيفة التي توفرها قناة معينة بالضبط، سيكون هناك سؤالٌ حول مكان تنفيذ هذه الوظيفة. في كثير من الحالات، يكون من الأسهل النظر للاتصال من مضيف لمضيف في الشبكة الأساسية على أنه مجرد توفير أنبوبٍ للبتات (bit pipe)، مع أية دلالات اتصال عالية المستوى يوفّرها المضيفون النهائيون. وتكمن ميزة هذه المقاربة في حفاظها على المبدّل في منتصف الشبكة بأبسط ما يمكن، إذ إنه ببساطة يُعِيد توجيه الرزم، ولكنها تتطلب من المضيفين النهائيين تحمّل الكثير من العبء لدعم قنوات عمليةٍ لعملية (process-to-process). والحلّ البديل هو إضافة وظائف إضافية إلى المبدّلات، مما يسمح للمضيفين النهائيين بأن يكونوا "أجهزة صامتة" (سماعات الهاتف على سبيل المثال). سوف نرى هذه المسألة المتعلقة بكيفية تقسيم خدمات الشبكة المختلفة بين مبدّلات الرزَم والمضيفين النهائيين (الأجهزة) كمشكلةٍ متكررة في تصميم الشبكات. التسليم الموثوق للرسائل يُعدّ تسليم الرسائل الموثوق به أحد أهم الوظائف التي يمكن أن توفّرها الشبكة مثلما تبيّن في الأمثلة الأخيرة التي ذكرناها. لكنه من الصعب تحديد كيفية توفير هذه الوثوقية (reliability)، دون أن نفهم كيف يمكن أن تفشل الشبكات أولًا. أوّل شيء ينبغي إدراكه هو أن شبكات الحواسيب لا توجد في عالم مثالي. قد تتعطّل الأجهزة ويُعاد تشغيلها لاحقًا، أو تنقطع الألياف، أو تتداخل الإشارات الكهربائية فتضيع البِتات في البيانات المُرسَلة، أو تنفد مساحة التخزين المؤقت في المبدّلات، كل هذه مشكلات فيزيائية تواجه الشبكة. بل إنّ هناك ما يثير القلق أكثر، فالبرامج التي تدير الأجهزة قد تحتوي على أخطاء وأحيانًا تُعيد توجيه الرزم نحو النسيان. وبالتالي، فإنّ أحد المتطلبات الرئيسية للشبكة هو القدرة على التعافي من بعض أنواع حالات الفشل، حتى لا تُضطَر برامج التطبيقات إلى التعامل معها أو حتى أن تكون على علم بها. هناك ثلاثة أصنافٍ عامة من الفشل ينبغي على مصممي الشبكات القلق بشأنها. أولًا، عند إرسال رزمة عبر رابطٍ فيزيائي، يمكن أن تحدث أخطاءُ بِتاتٍ في البيانات؛ أي، أن يتحول البِت 1 إلى 0 أو العكس. في بعض الأحيان يصيب التلف البِتات الفردية، ولكن في أغلب الأحيان يحدث خطأ الرشقة (burst error)، أي أن التلف يلحق بالعديد من البِتات المتتالية. تحدث أخطاء البِت عادةً بفعل القوى الخارجية، مثل ضربات الصواعق، واندفاعات الطاقة، وأفران الميكروويف، إذ تُحدِث تداخلًا مع نقل البيانات. لكن ما يبعث على الارتياح هو أن أخطاء البِتات هذه نادرة إلى حد ما، إذ تؤثر في المتوسط على واحد فقط من كل 106 إلى 107 بِت على كابل نموذجي من النحاس وواحد من كل 1012 إلى 1014 بِت على ألياف بصرية نموذجية. وكما سنرى، هناك تقنياتٌ تكشف عن أخطاء البِتات هذه باحتمالية عالية. وبمجرد اكتشافها، يكون في بعض الأحيان تصحيح هذه الأخطاء ممكنًا، فإذا عرفنا البِتات التالفة، يمكننا ببساطة عكسها، بينما في حالات أخرى يكون الضرر سيئًا لدرجة تستلزم التخلص من الرزمة بأكملها. في مثل هذه الحالة، قد يُتوقع من المُرسِل إعادة إرسال الرزمة. الصنف الثاني من الفشل يكون على مستوى الرزمة وليس في البت، أي عندما تضيع رزمةٌ كاملة في الشبكة. أحد أسباب حدوث ذلك هو أن يكون في الرزمة خطأ بِت غير قابل للتصحيح ويستوجب التخلّص منها. ومع ذلك، فالسبب الراجح هو أن تكون إحدى العُقد التي تتعامل مع الرزمة، كالمبدّل الذي يعيد توجيهها من رابط إلى آخر مثلًا، محملة بشكل زائد فلا يبقى مكانٌ لتخزين الرزمة فتضطر لإسقاطها، وهذه هي مشكلة الاحتقان التي ذكرناها في السّابق. هناك سبب آخر أقل شيوعًا، حينما يحدث خطأ في البرنامج الذي يعمل على إحدى العقد التي تتعامل مع الرزمة. إذ قد يعيد، على سبيل المثال، توجيه رزمة بصورة خاطئة على الرابط الخطأ، فلا تجد الرزمة طريقها إلى الوجهة النهائية. وكما سنرى فيما بعد، فإن إحدى الصعوبات الرئيسية في التعامل مع الرزم المفقودة هي التمييز بين رزمة مفقودة بالفعل وأخرى متأخرة فقط في الوصول إلى الوجهة. يحدث الصنف الثالث من الفشل على مستوى العقدة أو الرابط، كأن ينقطع الرابط الفيزيائي، أو يتعطّل الحاسوب المتصل به. يمكن أن يحدث هذا بسبب عُطلٍ في البرنامج، أو انقطاعٍ في التيار الكهربائي، أو ربّما عمليّة حَفرٍ متهوّرة. كما أن الفشل بسبب الضبط الخاطئ (misconfiguration) لجهازٍ على الشبكة أمر شائع أيضًا. ورغم أنه يمكن تصحيح كلٍّ من هذه الإخفاقات في النهاية، إلا أنّ تأثيرها يمكن أن يكون كبيرًا على الشبكة لفترةٍ طويلةٍ من الزمن. ومع ذلك، فهي لا تستدعي تعطيل الشبكة تمامًا. في شبكة تبديل الحزم، على سبيل المثال، يمكن في بعض الأحيان إعادة التوجيه لتفادي العقدة أو الرابط الفاشل. تتمثل إحدى الصعوبات في التعامل مع هذا الصنف الثالث من الفشل في التمييز بين الحاسوب الفاشل والحاسوب البطيء فقط أو بين الرابط المقطوع والذي يوجد في حالة عدم استقرار فيعطي بذلك عددًا كبيرًا من أخطاء البِت. قابلية الإدارة (Manageability) المُتطلب الأخير، الذي يبدو أنه كثيرًا ما يُهمَل أو يُترَك حتى النهاية (مثلما نفعل هنا)، هو أنّ الشبكات يجب أن تُدارَ. تتضمن إدارة الشبكة ترقية المعدات أثناء نموّ الشبكة لتحمل المزيد من حركة المرور أو الوصول إلى المزيد من المستخدمين، واستكشاف أخطاء الشبكة وإصلاحها عندما تسوء الأمور أو لا يكون الأداء على النحو المطلوب، وإضافة ميزات جديدة لدعم التطبيقات الجديدة. لقد كانت إدارة الشبكات تاريخيًا مظهرًا كثير الاعتماد على الجانب البشري في مجال الشبكات، ورغم أنه من المستبعد إخراج البشر تمامًا من الصّورة، إلا أنّ هذا يتمّ تدريجيًا من خلال الأتمتة وتصميمات الإصلاح الذاتي. يرتبط هذا المتطلب جزئيًا بمسألة قابلية التوسع التي ناقشناها من قبل. فنظرًا لأن الإنترنت طُوِّر لدعم مليارات المستخدمين ومئات الملايين على الأقل من المضيفين، فإن تحديات إبقاء كل شيء يعمل بصورة صحيحة وضبط الأجهزة الجديدة على نحوٍ صحيح في حال إضافتها أصبحت إشكالية متنامية. غالبًا ما يكون ضبط موجهٍ واحد في الشبكة مهمة تُناط بخبيرٍ متمرّس؛ لكنّ ضبط آلاف الموجهات ومعرفة لماذا لا تتصرف شبكة بهذا الحجم كما هو متوقعٌ سيكون مهمةً تتجاوز أيّ إنسان بمفرده. إنّ هذا هو السبب الذي يعطي للأتمتة أهميتها البالغة. تتجلّى إحدى الطرق التي تسهّل إدارة الشبكة في تجنّب التغيير. بمجرد أن تعمل الشبكة، فلا تلمسها ببساطة! تكشف هذه العقلية عن الشّدّ الموجود بين الاستقرار وسرعة الميزة التي تعني إدخال الميزات الجديدة في الشبكة. كان الرهان على الاستقرار هو النهج الذي اتبعته صناعة الاتصالات السلكية واللاسلكية فضلًا عن مديري الأنظمة الجامعية وأقسام تكنولوجيا المعلومات في الشركات لسنوات عديدة، مما يجعلها واحدة من أكثر الصناعات التي تتحرك ببطء وتتجنب المخاطر على الإطلاق. لكن الانفجار الأخير للسحابة غيّر تلك الديناميكية، ممّا جعل من الضروري تحقيق توازنٍ أكثر بين الاستقرار وسرعة الميزة. إن تأثير السحابة على الشبكة هو موضوع يظهر مرارًا وتكرارًا في جميع أجزاء الكتاب، وهو موضوع نوليه اهتمامًا خاصًا في قسم المنظورات في نهاية كل فصل. في الوقت الحالي، يكفي أن نقول أن إدارة شبكة سريعة التطور هي التّحدّي الرئيسي في عالم الشبكيات اليوم. ترجمة -وبتصرّف- للقسم Requirements من فصل Foundation من كتاب Computer Networks: A Systems Approach
  5. المشكلة: بناء شبكة (Building a Network) لنفترض أنك ترغب في بناء شبكة حاسوبية يكون لديها القدرة على النموّ إلى أبعاد عالمية، وعلى دعم تطبيقات متنوعة مثل المؤتمرات عن بعد، والفيديو حسب الطلب، والتجارة الإلكترونية، والحوسبة الموزعة، والمكتبات الرقمية. فما هي التقنيات المتاحة التي ستكون بمثابة اللبنات الأساسية، وما هو نوع هندسة البرمجيات التي ستصمّمها لدمج كتل البناء هذه في خدمة اتصالات فعالة؟ إن الهدف الأساسي لهذا الكتاب هو الإجابة على هذا السؤال، وذلك من أجل وصف مواد البناء المتاحة ثم لتوضيح كيف يمكن استخدامها لبناء شبكةٍ من الأساس. قبل أن نفهم كيفية تصميم الشبكة الحاسوبية، يجب أن نتفق أولًا على مفهوم الشبكة الحاسوبية بالتحديد. في وقت ما، كان مصطلح الشبكة يعني مجموعة الخطوط التسلسلية المستخدمة لربط الطرفيات الصامتة بأجهزة الحواسيب المركزية. وتشمل الشبكاتُ المهمة الأخرى شبكة الهاتف الصوتي وشبكة الكابلات التلفزيونية التي تستخدم لبث الإشارات السمعية البصرية. الأمور الرئيسية المشتركة بين هذه الشبكات هي أنها متخصّصة في معالجة نوع معيّن من البيانات (ضغطات المفاتيح أو الصوت أو الفيديو)، كما أنها في العادة تربط بين الأجهزة ذات الأغراض الخاصة (المحطات وأجهزة الاستقبال اليدوية وأجهزة التلفاز). ما الذي يميز الشبكة الحاسوبية عن هذه الأنواع الأخرى من الشبكات؟ ربما تكون السمة الأهم في الشبكات الحاسوبية هي شموليتها. إذ تُنشَأ الشبكات الحاسوبية بصفة رئيسية من أجهزةٍ قابلة للبرمجة للأغراض العامة، ولا يكون تطويرها من أجل تطبيق معين فحسب، مثل إجراء مكالمات هاتفية أو توصيل إشارات تلفزيونية. وعوضًا عن ذلك، فهي قادرة على حمل أنواع مختلفة من البيانات، كما أنّها تدعم مجموعةً واسعة ومتنامية من التطبيقات. تتولى الشبكات الحاسوبية اليوم الكثير من المهام التي كانت تُؤديها شبكات الاستخدام الواحد. ويبحث هذا الفصل في بعض التطبيقات النموذجية للشبكات الحاسوبية، ويناقش المتطلبات التي يجب أن يكون مصمّم الشبكة الذي يرغب في دعم هذه التطبيقات على علم بها. وكيف يمكننا أن نمضي قدمًا بعد أن نفهم المتطلبات؟ لحسن الحظ، لن نكون بصدد بناء الشبكة الأولى. فهناك آخرون، أبرزهم مجتمع الباحثين المسؤولين عن شبكة الإنترنت، قد أنجزوا المهمة قبلنا. أي أنّنا سنستخدم ثروة الخبرة المتولّدة من الإنترنت لتوجيه تصميمنا. تتجسّد هذه التجربة في بنية الشبكة التي تحدّد مكونات الأجهزة والبرامج المتاحة وتوضّح كيف يمكن ترتيبها لتشكيل نظام شبكةٍ كاملٍ. بالإضافة إلى فهم كيفية بناء الشبكات، تتزايد أهمّية فهم كيفية تشغيلها أو إدارتها وكيفية تطوير تطبيقات الشبكة. لدى جميعنا تقريبًا الآن شبكات حاسوبية في منازلنا ومكاتبنا، وفي بعض الحالات في سياراتنا، لذلك لم تعد شبكات التشغيل مسألة تهم عددًا قليلًا من المتخصصين فقط. ومع انتشار الهواتف الذكية، يطوّر الكثير من أبناء هذا الجيل تطبيقات شبكية أكثر من الماضي. لذلك نحن بحاجة إلى النظر في الشبكات من هذه المنظورات المتعددة: المنشئون (builders) والمشغلون (operators) ومطورو التطبيقات (application developers). يقوم هذا الفصل بأربعة أشياء من أجل الانطلاق على طريق فهم كيفية بناء شبكةٍ وتشغيلها وبرمجتها. في البداية، يستكشف المتطلبات التي تضعها التطبيقات المختلفة والمجتمعات المختلفة من الأشخاص على الشبكة. ثانيًا، يقدّم فكرة بنية الشبكة، التي تضع الأساس لبقية الكتاب. ثالثًا، يقدّم بعض العناصر الرئيسية في تنفيذ الشبكات الحاسوبية. وأخيرًا، يحدّد المقاييس الرئيسية المستخدمة لتقييم أداء الشبكات الحاسوبية. تطبيقات (Applications) شبكة الإنترنت يعرف معظم الناس الإنترنت من خلال تطبيقاته، ونذكر منها على سبيل المثال لا الحصر شبكة الويب العالمية، والبريد الإلكتروني، ووسائل التواصل الاجتماعي، وتدفّق بيانات الموسيقى أو الأفلام، وعقد مؤتمرات الفيديو، والرسائل الفورية، ومشاركة الملفات. وهذا يعني أننا نتفاعل مع الإنترنت كمستخدِمين للشبكة. يمثل مستخدمو الإنترنت أكبر فئة من الأشخاص الذين يتفاعلون مع الإنترنت بطريقة ما، ولكن هناك العديد من الفئات المهمة الأخرى. فهناك مجموعة من الأشخاص الذين يعملون على إنشاء التطبيقات، وهي مجموعة توسعت بصورةٍ كبيرة في السنوات الأخيرة، إذ أنّ منصات البرمجة القوية والأجهزة الجديدة مثل الهواتف الذكية خلقت فرصًا جديدة لتطوير التطبيقات بسرعة وتقديمها إلى سوق كبيرٍ. ثم هناك من يُشغّلون أو يديرون الشبكات، وهي في الغالب وظيفةٌ تُؤدّى من خلف الكواليس، ولكنها مهمّة حرجة وغالبًا ما تكون معقّدة للغاية. مع انتشار الشبكات المنزلية، أصبح المزيد والمزيد من الناس مشغلين للشبكات، ولو على نحوٍ بسيطٍ. وفي الأخير، هناك من يصمّمون ويبنون الأجهزة والبروتوكولات التي تشكّلُ بأجمعها شبكةَ الإنترنت. تمثل هذه المجموعة النهائية الهدفَ التقليدي للكتب المدرسية الخاصة بموضوع الشبكات، وكذلك حتى بالنسبة لهذا الكتاب، كما أنها ستظل محور تركيزنا الرئيسي. ومع ذلك، سنبحث في هذا الكتاب أيضًا في وجهات نظر مطوري التطبيقات ومشغلي الشبكات. سوف يتيح لنا الاطّلاع على وجهات النظر هذه أن نفهم المتطلبات المتنوعة التي يجب أن تلبيها الشبكة بصورة أفضل. كما سيتيح هذا لمطوري التطبيقات أيضًا أن يجعلوا التطبيقات تعمل بصورةٍ أفضل إذا فهموا كيف تعمل هذه التكنولوجيا الأساسية وكيف تتفاعل مع التطبيقات. لذا، قبل أن نبدأ في اكتشاف كيفية إنشاء شبكة، دعنا ننظر عن كثب إلى أنواع التطبيقات التي تدعمها الشبكات في الوقت الحالي.¦ فئات التطبيقات إنّ شبكة الويب العالمية هي تطبيق الإنترنت الذي حوّل الإنترنت من أداة غامضة إلى حدٍ ما يستخدمها في الغالب العلماء والمهندسون إلى ظاهرة التيار السائد كما هي اليوم. لقد أصبح الويب نفسه نظامًا أساسيًا قويًا لدرجة أن الكثير من الناس يخلطونه مع الإنترنت، وقد يكون من المبالغة قليلًا أن نقول أن الويب هو تطبيق واحد. يقدّم الويب في شكله الأساسي واجهةً بسيطة على نحوٍ بديهيٍّ تمامًا. إذ يستعرض المستخدمون صفحات مليئة بالكائنات النصية والرسوم البيانية وينقرون على الكائنات التي يريدون معرفة المزيد عنها، وتظهر صفحة جديدة مرافقة. يدرك معظم الأشخاص أيضًا أنّه، أسفل الأغلفةِ، يكون كلُّ كائنٍ قابلٍ للتحديد في صفحةٍ مرتبطًا بمُعرِّفٍ للصفحة أو الكائن التالي الذي سيُعرَض. هذا المُعرِّف يسمى مُعرِّف الموارد الموحّد (Uniform Resource Locator أو اختصارًا URL)، ويوفر طريقةً لتحديد جميع الكائنات المحتملة التي يمكن عرضها من متصفح الويب. فمثلًا: http://www.cs.princeton.edu/llp/index.html هذا عنوان URL لصفحةٍ تُقدّم معلومات حول أحد مؤلفي هذا الكتاب: تشير السلسلة http إلى أنه يجب استخدام بروتوكول نقل النصوص الترابطية (Hypertext Transfer Protocol أو اختصارًا HTTP) لتحميل الصفحة، www.cs.princeton.edu هو اسم جهاز الخادوم الذي يوفّر الصفحة، ويعرّف الجزء ‎/llp/index.html صفحة لاري الرئيسية بشكل فريد في موقع الويب هذا. ومع ذلك، فإن ما لا يدركه معظم مستخدمي الويب هو أنه من خلال النقر على عنوان URL واحد فقط، هناك عشرات الرسائل يمكن تبادلها عبر الإنترنت، وأكثر من ذلك بكثير إذا كانت صفحة الويب تتشكّل من الكثير من الكائنات المضمّنة. يشتمل تبادل الرسائل هذا على ما يصل إلى ست رسائلٍ لترجمة اسم الخادوم (www.cs.princeton.edu) إلى عنوان IP أي بروتوكول الإنترنت (128.112.136.35)، وثلاث رسائل لإعداد بروتوكول التحكّم بالإرسال ( Transmission Control Protocol أو اختصارًا TCP) بين المتصفّح وهذا الخادوم، وأربع رسائل للمتصفّح من أجل إرسال طلب HTTP من الفئة GET وكذلك للخادوم من أجل الرد على الطلب مع إرفاق الصفحة المطلوبة (دون نسيان الإفادة باستلام الرسالة من كلا الطرفين)، وأربع رسائل لإنهاء اتصال TCP. وبطبيعة الحال، هذا لا يشمل الملايين من الرسائل التي تتبادلها عُقَد شبكة الإنترنت على مدار اليوم، فقط لكي يُعلِم بعضها بعضًا بوجوده واستعداده لتوفير صفحات الويب، وترجمة الأسماء إلى عناوين، وإعادة توجيه الرسائل نحو وِجهاتها النهائية. هناك فئة أخرى من التطبيقات واسعة الانتشار على الإنترنت وهي توصيل تدفّقات الصوت والفيديو. وتُستخدَم هذه التقنية في بعض الخدمات مثل الفيديو وفق الطلب والراديو على الإنترنت. ورغم أنّ جلسة البث تبدأ غالبًا في موقع ويب، إلا أن توصيل الصوت والفيديو له بعض الاختلافات المهمة عن جلب صفحة ويب بسيطة من النصوص والصور. على سبيل المثال، لا يرغب الزائر في الغالب تحميل ملف فيديو بالكامل، وهي عملية قد تستغرق بضع دقائق، قبل مشاهدة المشهد الأول. يتطلّب تدفّق الصوت والفيديو نقل الرسائل في الوقت المناسب من المرسل إلى المستقبل، ويعرض هذا الأخير الفيديو أو يشغّل الصوت تمامًا عند وصوله. يكمن الفرق بين تطبيقات البث والتوصيل التقليدي للنص والرسومات والصور في أن المستخدِم يستهلك تدفقات الصوت والفيديو بصفة مستمرة، وأن أيّ انقطاع، قد يتمثّل في تخطّي بعض الأصوات أو توقف الفيديو، أمرٌ غير مقبول. وعلى العكس، يمكن توصيل صفحة عادية (غير متدفقة) وقراءتها في أجزاء وقطع. ويؤثر هذا الاختلاف على كيفية دعم الشبكة لهذه الفئات المختلفة من التطبيقات. وهناك فئة من التطبيقات مختلفة اختلافًا دقيقًا، وهي تقنية الصوت والفيديو في الزمن الحقيقي (real-time audio and video). ولهذه التطبيقات قيود أكثر تشددًا من تطبيقات البث فيما يخصّ التوقيت. يجب أن تكون التفاعلات بين المشاركين في الوقت المناسب، عند استخدام الصوت عبر بروتوكول الإنترنت مثل تطبيق سكايب أو تطبيق المؤتمرات عبر الفيديو. عندما يقوم شخص ما بإيماءةٍ، يجب أن يُعرض هذا الإجراء في الجانب الآخر بأسرع ما يمكن. ليس تمامًا "في أقرب وقت ممكن". . . تشير أبحاث العوامل البشرية إلى أن 300 ميلي ثانية هي حدّ أعلى معقولٌ لمقدار التأخير ذهابًا وإيابًا الذي يمكن تحمّله في مكالمة هاتفية دون أن تكون هناك شكوى، ويبدو أن التأخير البالغ 100 مللي ثانية جيد جدًا. عندما يحاول أحدهم مقاطعة شخص آخر، يحتاج هذا الأخير إلى سماع ذلك في أقرب وقت ممكن ويقرر ما إذا كان سيسمح بالمقاطعة أو الاستمرار في التحدّث. إن التأخير الشديد في هذا النوع من البيئة يجعل النظام غير قابل للاستخدام. وبالموازنة بين هذا النظام والفيديو عند الطلب، إذا استغرق الأمر عدّة ثوانٍ من وقت بدء المستخدم للفيديو حتى عرض الصورة الأولى، فلا تزال الخدمة تُعدّ مرضية. وكذلك، تستلزم التطبيقات التفاعلية عادة تدفق الصوت و/أو الفيديو في كلا الاتجاهين، بينما يُرسل تطبيق البث في الغالب الفيديو أو الصوت في اتجاهٍ واحدٍ فقط. إنّ أدوات مؤتمرات الفيديو التي تعمل على الإنترنت موجودة الآن منذ أوائل التسعينات، ولكنها حققت استخدامًا واسع النطاق في السنوات القليلة الماضية في ظلّ وجود العديد من المنتجات التجارية في السوق. يظهر مثال على أحد هذه الأنظمة في الوثيقة التالي. وتمامًا مثلما ينطوي تحميل صفحة الويب على أكثر قليلًا مما تُشاهده العين، يحدث الشيء نفسه مع تطبيقات الفيديو. إنّ تضمين محتوى الفيديو في شبكة ذات نطاق تردّدي منخفض نسبيًا، على سبيل المثال، أو التأكد من أن الفيديو والصوت لا يزالان متزامنين ويصلان في الوقت المناسب للحصول على تجربة مستخدم جيدة، كلّها مشاكل يجب على مصممي الشبكات والبروتوكولات القلق بشأنها. سنلقي نظرة على هذه الأمور والعديد من المشكلات الأخرى المتعلقة بتطبيقات الوسائط المتعددة لاحقًا في الكتاب. رغم أنّهما مثالان فقط، إلا أن تحميل الصفحات من الويب والمشاركة في مؤتمر بالفيديو يوضّحان تنوع التطبيقات التي يمكن بناؤها على الإنترنت ويشيران إلى تعقيد تصميم هذه الشبكة العنكبوتية. في جزء لاحق من الكتاب، سوف نعرض تصنيفًا أكثر اكتمالًا لأنواع التطبيقات للمساعدة في توجيه مناقشتنا للقرارات الرئيسية الخاصّة بالتصميم في سعينا لبناء وتشغيل واستخدام الشبكات التي تحتوي على مجموعةٍ واسعة من التطبيقات. يُختتم الكتاب بإعادة النظر في هذين التطبيقين بالتّحديد، بالإضافة إلى العديد من التطبيقات الأخرى التي تبرز اتساع ما هو ممكن على الإنترنت اليوم. في الوقت الحالي، تكفي هذه النظرة السريعة على عددٍ قليل من التطبيقات النموذجية لتمكيننا من بدء البحث في المشكلات التي ينبغي معالجتها إذا أردنا إنشاء شبكةٍ تدعم هذا التنوع في التطبيقات. ترجمة -وبتصرّف- للقسم Applications من فصل Foundation من كتاب Computer Networks: A Systems Approach
  6. فعلا، أحيانا نجد صعوبة في الرفض، فنؤدي الثمن من وقتنا ومجهودنا وحتى من حالتنا النفسية. أعجبتني تجربة الفرق بين "لا أستطيع" و"لا أرغب" 😍
  7. هناك دائمًا طرق متعددة لتعزيز مرونة وأمان تطبيقك Node.js. ويتيح لك استخدام وكيل عكسي مثل Nginx إمكانية تحميل طلبات الرصيد وتخزين محتوى ثابت في ذاكرة التخزين المؤقت وتنفيذ بروتوكول أمان طبقة النقل (TLS) وهي اختصار لـTransport Layer Security. ويضمن تفعيل HTTPS المشفر على خادمك أن الاتصالات من وإلى تطبيقك تبقى آمنة. يتضمن تطبيق وكيل عكسي باستخدام TLS/SSLعلى الحاويات مجموعة من الإجراءات مختلفة عن العمل مباشرة على نظام تشغيل مضيف. على سبيل المثال، إذا كنت تحصل على شهادات من Let’s Encrypt لتطبيق يعمل على الخادم، فستثبّت البرنامج المطلوب مباشرة على مضيفك. أما الحاويات فتسمح لك أن تسلك نهجا مختلفا. باستخدام Docker Compose، يمكنك إنشاء حاويات لتطبيقك، وخادم الويب الخاص بك، وعميل Certbot الذي سيتيح لك الحصول على شهاداتك. ويمكّنك اتباع هذه الخطوات من الاستفادة من مزايا النمطية وقابلية التنقل في سير عملٍ يعتمد على الحاويات. في هذا الدرس، سوف تنشر تطبيق Node.js بوكيل عكسي Nginx باستخدام Docker Compose. وستحصل على شهادات TLS/SSL للمجال المرتبط بتطبيقك كما ستتأكد من حصوله على تصنيف أمان عالي من SSL Labs. وفي الأخير، سوف تنشئ وظيفة cron لتجديد شهاداتك بحيث يظل نطاقك آمنًا. المتطلبات الأساسية لمتابعة هذا البرنامج التعليمي، ستحتاج إلى ما يلي: خادم أوبونتو 18.04 ومستخدم غير جذري ذي صلاحيات sudo وجدار حماية نشط. للحصول على إرشادات حول كيفية إعدادها، يرجى الاطلاع على دليل إعداد الخادم الأولي. Docker مثبت على خادمك. للحصول على إرشادات حول تثبيت Docker، اتبع الخطوتين 1 و 2 للدليل كيفية تثبيت واستخدام Docker على Ubuntu 18.04. للحصول على إرشادات حول تثبيت Compose، اتبع الخطوة الأولى من كيفية تثبيت Docker Compose على أوبونتو 18.04. اسم نطاق مسجل. سيستخدم هذا البرنامج التعليمي example.com طوال الوقت. يمكنك الحصول على نطاق مجاني من Freenom ، أو استخدام مسجل النطاقات الذي تختاره. سجلّا DNS التاليين المعدّين لخادمك. يمكنك متابعة هذه المقدمة إلى DNS في DigitalOcean للحصول على تفاصيل حول كيفية إضافتها إلى حساب DigitalOcean، إذا كنت تستخدمه: سجل A يتضمن example.com يشير إلى عنوان IP العام لخادمك. سجل A يتضمن www.example.com يشير إلى عنوان IP العام لخادمك. الخطوة الأولى: استنساخ واختبار تطبيق Node سوف نبدأ أولًا باستنساخ المستودع الذي يحتوي على شيفرة التطبيق Node، والذي يتضمن الملف Dockerfile الذي سنستخدمه لإنشاء صورة تطبيقنا اعتمادًا على Compose. ويمكننا البدء باختبار التطبيق عبر بنائه وتنفيذه باستخدام الأمر docker run دون وكيل عكسي أو شهادة SSL. في المجلّد الرئيسي للمستخدم غير الجذري، استنسخ مستودع nodejs-image-demo من حساب DigitalOcean Community GitHub. يتضمن مستودع التخزين هذا شيفرة الإعداد الموضح في كيفية إنشاء تطبيق Node.js باستخدام Docker. انسخ المستودع في مجلّد يسمى node_project: git clone https://github.com/do-community/nodejs-image-demo.git node_project انتقل إلى المجلّد node_project: cd node_project يوجد في هذا المجلّد ملفّ Dockerfile يحتوي على إرشادات حول بناء تطبيق Node باستخدام صورة Docker node:10 أضافة إلى محتويات مجلّد مشروعك الحالي. يمكنك إلقاء نظرة على محتويات Dockerfile بكتابة مايلي: cat Dockerfile المخرجات: FROM node:10-alpine RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app WORKDIR /home/node/app COPY package*.json ./ USER node RUN npm install COPY --chown=node:node . . EXPOSE 8080 CMD [ "node", "app.js" ] تبني هذه التعليمات صورة Node عبر نسخ شيفرة المشروع من المجلّد الحالي إلى الحاوية وتثبيت الاعتماديات باستخدام npm install. كما أنها تستخدم أيضًا التخزين المؤقت وطبقات الصور في Docker عبر فصل نسخة من package.json و package-lock.json، التي تحتوي على اعتماديات المشروع المدرجة، عن نسخة باقي شيفرة التطبيق. وتحدد هذه التعليمات كذلك أن تشغيل الحاوية سيكون عبر مستخدم Node غير الجذري بالأذونات المناسبة المعينة على شيفرة التطبيق والمجلّدات node_modules. لمزيد من المعلومات حول أفضل ممارسات Dockerfile و Node image ، يرجى الاطلاع على التوضيحات في الخطوة 3 حول كيفية بناء تطبيق Node.js باستخدام Docker. لاختبار التطبيق بدون SSL، يمكنك بناء الصورة وتعليمها باستخدام Docker والراية t-. سوف نسمّي الصورة node-demo، ولكن تبقى لك الحرية في إعطائها اسمًا آخر: docker build -t node-demo . بمجرد اكتمال عملية البناء، يمكنك عرض قائمة صورك باستخدام docker images: docker images سيظهر لك الإخراج التالي مؤكّدًا بناء صورة التطبيق: REPOSITORY TAG IMAGE ID CREATED SIZE node-demo latest 23961524051d 7 seconds ago 73MB node 10-alpine 8a752d5af4ce 3 weeks ago 70.7MB بعد ذلك، أنشئ الحاوية باستخدام docker run. سوف نستخدم ثلاث رايات مع هذا الأمر: p-: ينشر هذا المنفذ على الحاوية ويوجّهه إلى منفذٍ على مضيفنا. سنستخدم المنفذ 80 على المضيف، ولكن يمكنك تعديل هذا عند الضرورة إذا كان لديك عملية أخرى تشغَل هذا المنفذ. لمزيد من المعلومات، راجع هذه التوضيحات في توثيق Docker حول ربط المنافذ. d-: يشغّل هذا الحاوية في الخلفية. name- : يتيح لنا هذا إعطاء الحاوية اسمًا سهل التذكّر. نفّذ الأمر التالي لإنشاء الحاوية: docker run --name node-demo -p 80:8080 -d node-demo تفقّد الحاويات قيد التشغيل باستخدام docker ps: docker ps سترى الإخراج التالي الذي يؤكد أن حاوية تطبيقك قيد التشغيل: CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 4133b72391da node-demo "node app.js" 17 seconds ago Up 16 seconds يمكنك الآن زيارة عنوان نطاقك لاختبار إعداداتك: http://example.com. لا تنس تعويض example.com باسم نطاقك الخاص. سيعرض تطبيقك صفحة الهبوط التالية: الآن وبعد اختبار التطبيق، يمكنك إيقاف الحاوية وحذف الصور. استخدم docker ps مرة أخرى للحصول على معرّف الحاويات CONTAINER ID: docker ps المخرجات: CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 4133b72391da node-demo "node app.js" 17 seconds ago Up 16 seconds أوقف الحاوية باستخدام docker stop. تأكد من تعويض المعرِّف CONTAINER ID المدرج هنا بمعرّف تطبيقك CONTAINER ID: docker stop 4133b72391da يمكنك الآن حذف الحاوية المتوقفة وجميع الصور، بما في ذلك الصور غير المستخدمة والمعلّقة، باستخدام docker system prune والراية a-: docker system prune -a اكتب y في الإخراج عند ما تطلب منك الكتابة لتأكيد رغبتك في حذف الحاوية والصور المتوقّفة. يرجى العلم أن هذا سيؤدي أيضًا إلى حذف ذاكرة التخزين المؤقت للبناء. بعد اختبار صورة تطبيقك، يمكنك الانتقال إلى بناء بقية الإعداد باستخدام Docker Compose. الخطوة الثانية: تحديد تكوين خادم الويب يمكننا بعد إنشاء Dockerfile لتطبيقنا، إنشاء ملف تكوين لتشغيل حاوية Nginx الخاصة بنا. سنبدأ بالتكوين ذي الحد الأدنى الذي سيتضمن اسم نطاقنا، و الملف الجذري، ومعلومات الوكيل، وكتلة الموقع (Location block) لتوجيه طلبات Certbot إلى المجلّد well-known. إذ سيضع ملفًا مؤقتًا للتحقق من أن معلومات DNS لنطاقنا توجّه نحو خادمنا. أنشئ أولاً مجلّدًا في مجلّد المشروع الحالي لملف التكوين: mkdir nginx-conf افتح الملف باستخدام nano أو المحرر المفضل لديك: nano nginx-conf/nginx.conf أضف كتلة الخادم التالية لطلبات المستخدم الوكيل إلى حاوية تطبيقك Node ولتوجيه طلبات Certbot إلى المجلّد well-known. لا تنس تعويض example.com باسم نطاقك الخاص: server { listen 80; listen [::]:80; root /var/www/html; index index.html index.htm index.nginx-debian.html; server_name example.com www.example.com; location / { proxy_pass http://nodejs:8080; } location ~ /.well-known/acme-challenge { allow all; root /var/www/html; } } تسمح لنا كتلة الخادم هذه بتشغيل حاوية Nginx كبديل عكسي، والذي سيمرر الطلبات إلى حاوية تطبيقنا Node. سيسمح لنا أيضًا باستخدام المكوّن الإضافي webroot الخاص بـ Certbot للحصول على شهادات لنطاقنا. يعتمد هذا المكوّن الإضافي على طريقة التحقق من HTTP-01، والتي تستخدم طلب HTTP لإثبات أن Certbot يمكنه الوصول إلى الموارد من خادمٍ يوافق اسمَ نطاق معين. احفظ الملف وأغلقه بمجرد الانتهاء من التحرير. لمعرفة المزيد حول خوارزميات Nginx لخادم المواقع وكتلها، يرجى الرجوع إلى هذه المقالة حول فهم خوارزميات Nginx للخادم وحظر المواقع. بعد تحديد تفاصيل تكوين خادم الويب، يمكننا الانتقال إلى إنشاء ملف docker-compose.yml، والذي سيتيح لنا إنشاء خدمات التطبيقات الخاصة بنا وحاوية Certbot التي سنستخدمها للحصول على شهاداتنا. الخطوة الثالثة: إنشاء ملف Docker Compose سيحدد ملف docker-compose.yml خدماتنا، بما في ذلك تطبيق Node وخادم الويب. سوف يحدد تفاصيل مثل وحدات التخزين المسماة، والتي ستكون ضرورية لمشاركة بيانات اعتماد SSL بين الحاويات، وكذلك معلومات الشبكة والمنفذ. سوف يسمح لنا أيضًا بتحديد أوامر محددة لتنفيذها عند إنشاء حاوياتنا. هذا الملف هو المورد المركزي الذي سيحدّد كيف ستعمل خدماتنا معًا. افتح الملف في مجلّدك الحالي: nano docker-compose.yml حدد خدمة التطبيق أولًا: version: '3' services: nodejs: build: context: . dockerfile: Dockerfile image: nodejs container_name: nodejs restart: unless-stopped يتضمن تعريف خدمة nodejs ما يلي: Build: يحدّد هذا خيارات التكوين، بما في ذلك context وdockerfile، والتي ستطبّق عندما ينشئ Docker صورة التطبيق. إذا كنت ترغب في استخدام صورة موجودة من سجلٍّ مثل Docker Hub، فيمكنك استخدام التعليمة image عوض ذلك، مع معلومات حول اسم المستخدم والمستودع وعلامة الصورة. Context: يعرّف هذا سياق البناء لصورة التطبيق. وهو في هذه الحالة مجلّد المشروع الحالي. Dockerfile: يحدّد هذا ملف Dockerfile الذي سيستخدمه Compose للبناء وهو الملف Dockerfile الذي رأيناه في الخطوة الأولى. Image و container_name: تعطي هذه أسماء للصورة والحاوية. restart : هذا يحدّد سياسة إعادة التشغيل. تكون قيمته الافتراضية هي "لا"، لكننا عيّننا الحاوية لإعادة تشغيل ما لم يتم إيقافها. لاحظ أننا لا نُضمّن وصلات الربط (bind mounts) مع هذه الخدمة، وذلك لأن إعدادنا يركز على النشر بدلاً من التطوير. لمزيد من المعلومات، يرجى الاطلاع على توثيق Docker على وصلات الربط والحجوم. لتمكين الاتصال بين التطبيق وحاويات خادم الويب، سنضيف أيضًا شبكة جسرية (bridge network) تسمى app-network أسفل تعريف إعادة التشغيل: services: nodejs: ... networks: - app-network تتيح الشبكة الجسرية التي يعرّفها المستخدم الاتصالَ بين الحاويات على مضيف Docker نفسه. ويعمل ذلك على تبسيط حركة المرور والاتصالات داخل التطبيق، لأنه يفتح جميع المنافذ بين الحاويات على نفس الشبكة الجسرية، مع عدم تعريض أي منافذ للعالم الخارجي. وبالتالي، يمكنك الاختيار في فتح المنافذ التي تحتاجها فقط لعرض خدمات واجهتك الأمامية. بعد ذلك، حدّد خدمة خادم الويب: ... webserver: image: nginx:mainline-alpine container_name: webserver restart: unless-stopped ports: - "80:80" volumes: - web-root:/var/www/html - ./nginx-conf:/etc/nginx/conf.d - certbot-etc:/etc/letsencrypt - certbot-var:/var/lib/letsencrypt depends_on: - nodejs networks: - app-network تظل بعض الإعدادات التي حددناها لخدمة nodejs كما هي، لكننا أجرينا أيضًا التغييرات التالية: image: يطلب هذا من Compose سحب أحدث صورة ألبية Nginx ‏(Alpine-based Nginx image) من Docker Hub. لمزيد من المعلومات حول الصور الألبية، يرجى الاطلاع على الخطوة الثالثة من كيفية إنشاء تطبيق Node.js باستخدام Docker. ports: هذا يعرض المنفذ 80 لتفعيل خيارات التكوين التي حددناها في تكوين Nginx. لقد حددنا أيضًا وحدات التخزين المسماة ووصلات الربط التالية: web-root:/var/www/html: سيؤدي ذلك إلى إضافة الأصول الثابتة لموقعنا ، المنسوخة إلى وحدة تخزين تسمى web-root ، إلى مجلّد /var/www/html على الحاوية. ./nginx-conf:/etc/nginx/conf.d: سيؤدي هذا إلى ربط دليل تكوين Nginx على المضيف بالمجلّد ذي الصلة على الحاوية، مع التأكد من أن أي تغييرات نجريها على الملفات الموجودة على المضيف ستنعكس في الحاوية. certbot-etc:/etc/letsencrypt : سيؤدي ذلك إلى ربط شهادات ومفاتيح Let’s Encrypt لنطاقنا على المجلّد المناسب على الحاوية. certbot-var:/var/lib/letsencrypt: يؤدي هذا إلى تحديث مجلّد العمل الافتراضي الخاص بـ Let's Encrypt إلى المجلّد المناسب على الحاوية. بعد ذلك، أضف خيارات التكوين لحاوية certbot. تأكد من تعويض النطاق ومعلومات البريد الإلكتروني باسم نطاقك وبريدك الإلكتروني للاتصال: ... certbot: image: certbot/certbot container_name: certbot volumes: - certbot-etc:/etc/letsencrypt - certbot-var:/var/lib/letsencrypt - web-root:/var/www/html depends_on: - webserver command: certonly --webroot --webroot-path=/var/www/html --email sammy@example.com --agree-tos يطلب هذا التعريف من Compose سحب صورة certbot/certbot من Docker Hub. كما أنّه يستخدم وحدات التخزين المسمّاة لمشاركة الموارد مع حاوية Nginx، بما في ذلك شهادات النّطاق والمفتاح في certbot-etc، ومجلّد عمل Let's Encrypt في certbot-var، وشيفرة التطبيق في web-root. مرة أخرى، لقد استخدمنا depends_on لتحديد أن حاوية certbot يجب أن تشتغل بمجرد تشغيل الخدمة webserver. لقد أضفنا أيضًا خيار command يحدد الأمر الذي سيتم تنفيذه عند بدء تشغيل الحاوية. وهو يتضمن الأمر الفرعي certonly مع الخيارات التالية: webroot--: هذا يطلب من Certbot استخدام البرنامج المساعد webroot لوضع الملفات في مجلد webroot للتصديق. webroot-path--: يحدد مسار المجلّد webroot. email--: بريدك الإلكتروني المفضل للتسجيل والاسترداد. agree-tos--: هذا يحدد أنك توافق على اتفاقية المشتركين في ACME. no-eff-email--: هذا يخبر Certbot أنك لا ترغب في مشاركة بريدك الإلكتروني مع مؤسسة الحدود الإلكترونية Electronic Frontier Foundation (EFF). لا تتردد في حذف هذا إذا كنت تفضل ذلك. staging--: هذا يخبر Certbot أنك ترغب في استخدام بيئة تشغيل Let's Encrypt للحصول على شهادات الاختبار. يتيح لك استخدام هذا الخيار اختبار خيارات التكوين وتفادي حدود طلبات النطاق المحتملة. لمزيد من المعلومات حول هذه الحدود، يرجى الاطلاع على التوثيق الخاص بمعدّل الحدود في Let's Encrypt. d-: يتيح لك ذلك تحديد أسماء النطاق التي ترغب في تطبيقها على طلبك. في هذه الحالة، ضمّننا example.com و www.example.com. لا تنس تعويضها بتفضيلات النطاق الخاصة بك. كخطوة أخيرة، أضف تعريفات وحدة التخزين والشبكة. وتأكد من تعويض اسم المستخدم هنا بمستخدمك غير الجذري: ... volumes: certbot-etc: certbot-var: web-root: driver: local driver_opts: type: none device: /home/sammy/node_project/views/ o: bind networks: app-network: driver: bridge تتضمن وحدات التخزين المسماة لدينا شهادات Certbot ووحدات تخزين مجلّد العمل، ووحدة تخزين الأصول الثابتة لموقعنا، web-root. في معظم الحالات، يكون برنامج التشغيل الافتراضي لوحدات تخزين Docker هو برنامج التشغيل المحلي، والذي يقبل على Linux خيارات مماثلة للأمر mount. ونستطيع بفضل هذا تحديد قائمة خيارات برنامج التشغيل باستخدام driver_opts الذي يربط مجلّد views على المضيف، والذي يحتوي على أصول ثابتة للتطبيق، مع وحدة التخزين في وقت التشغيل. يمكن بعد ذلك مشاركة محتويات المجلّد بين الحاويات. لمزيد من المعلومات حول محتويات المجلّد views، يرجى الاطلاع على الخطوة الثانية من كيفية إنشاء تطبيق Node.js باستخدام Docker. سيبدو ملف docker-compose.yml بهذا الشكل عند الانتهاء: version: '3' services: nodejs: build: context: . dockerfile: Dockerfile image: nodejs container_name: nodejs restart: unless-stopped networks: - app-network webserver: image: nginx:mainline-alpine container_name: webserver restart: unless-stopped ports: - "80:80" volumes: - web-root:/var/www/html - ./nginx-conf:/etc/nginx/conf.d - certbot-etc:/etc/letsencrypt - certbot-var:/var/lib/letsencrypt depends_on: - nodejs networks: - app-network certbot: image: certbot/certbot container_name: certbot volumes: - certbot-etc:/etc/letsencrypt - certbot-var:/var/lib/letsencrypt - web-root:/var/www/html depends_on: - webserver command: certonly --webroot --webroot-path=/var/www/html --email sammy@example.com --agree-tos --no-eff-email --staging -d example.com -d www.example.com volumes: certbot-etc: certbot-var: web-root: driver: local driver_opts: type: none device: /home/sammy/node_project/views/ o: bind networks: app-network: driver: bridge بوضعك لتعريفات الخدمة في مكانها الصحيح، ستكون مستعدًّا لبدء تشغيل الحاويات واختبار طلبات الشهادة الخاصة بك. الخطوة الرابعة: الحصول على شهادات SSL وبيانات الاعتماد يمكننا أن نبدأ تشغيل حاوياتنا باستخدام docker-compose up، وهو الأمر الذي سيؤدي إلى إنشاء وتشغيل حاوياتنا وخدماتنا بالترتيب الذي حددناه. إذا كانت طلبات النطاق ناجحة، فستظهر حالة الخروج الصحيحة في الإخراج والشهادات الملائمة موصولة بالمجلد etc/letsencrypt/live/ على حاوية webserver. أنشئ الخدمات باستخدام docker-compose up والراية d- التي ستشغّل حاويات nodejs و webserver في الخلفية: docker-compose up -d سيظهر لك الإخراج التالي الذي يؤكد أن خدماتك أُنشِئت: Creating nodejs ... done Creating webserver ... done Creating certbot ... done تحقق من حالة خدماتك باستخدام docker-compose ps: docker-compose ps إذا كان كل شيء على ما يرام، فيجب أن تكون خدماتك nodejs و webserver في الحالة "Up" وستكون حاوية certbot في الحالة "0": Name Command State Ports ------------------------------------------------------------------------ certbot certbot certonly --webroot ... Exit 0 nodejs node app.js Up 8080/tcp webserver nginx -g daemon off; Up 0.0.0.0:80->80/tcp إذا رأيت شيءًا آخر غير Up في عمود الحالة لخدمات node وwebserver، أو قيمة أخرى غير الصفر 0 في حالة الخروج للحاوية certbot، فاحرص على التحقق من سجلات الخدمة باستخدام الأمر docker-compose logs: docker-compose logs service_name يمكنك الآن التحقق من تركيب بيانات الاعتماد على حاوية webserver باستخدام docker-compose exec: docker-compose exec webserver ls -la /etc/letsencrypt/live إذا كان الطلب ناجحًا، فسترى في الإخراج ما يلي: total 16 drwx------ 3 root root 4096 Dec 23 16:48 . drwxr-xr-x 9 root root 4096 Dec 23 16:48 .. -rw-r--r-- 1 root root 740 Dec 23 16:48 README drwxr-xr-x 2 root root 4096 Dec 23 16:48 example.com الآن بعد تأكّدك من نجاح طلبك، يمكنك تحرير تعريف خدمة certbot من أجل حذف الراية staging--. افتح docker-compose.yml: nano docker-compose.yml ابحث في الملف عن الجزء الذي يتضمن تعريف خدمة certbot، وعوّض الراية staging-- في الخيار command بالراية force-renewal--، والتي ستخبر Certbot أنك تريد طلب شهادة جديدة بنفس النطاقات التي في الشهادة الحالية. يجب أن يبدو تعريف خدمة certbot الآن كما يلي: ... certbot: image: certbot/certbot container_name: certbot volumes: - certbot-etc:/etc/letsencrypt - certbot-var:/var/lib/letsencrypt - web-root:/var/www/html depends_on: - webserver command: certonly --webroot --webroot-path=/var/www/html --email sammy@example.com --agree-tos يمكنك الآن تنفيذ docker-compose up لإعادة إنشاء حاوية certbot ووحدات التخزين ذات الصلة. سنضمّن هنا أيضًا الخيار no-deps-- لنطلب من Compose تخطي تشغيل خدمة webserver، لأنها قيد التشغيل بالفعل: docker-compose up --force-recreate --no-deps certbot سوف تشير المخرجات التي ستظهر لك إلى نجاح طلبك للشهادة: certbot | IMPORTANT NOTES: certbot | - Congratulations! Your certificate and chain have been saved at: certbot | /etc/letsencrypt/live/example.com/fullchain.pem certbot | Your key file has been saved at: certbot | /etc/letsencrypt/live/example.com/privkey.pem certbot | Your cert will expire on 2019-03-26. To obtain a new or tweaked certbot | version of this certificate in the future, simply run certbot certbot | again. To non-interactively renew *all* of your certificates, run certbot | "certbot renew" certbot | - Your account credentials have been saved in your Certbot certbot | configuration directory at /etc/letsencrypt. You should make a certbot | secure backup of this folder now. This configuration directory will certbot | also contain certificates and private keys obtained by Certbot so certbot | making regular backups of this folder is ideal. certbot | - If you like Certbot, please consider supporting our work by: certbot | certbot | Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate certbot | Donating to EFF: https://eff.org/donate-le certbot | certbot exited with code 0 بحصولك على الشهادات اللازمة، يمكنك الانتقال إلى تعديل تكوين Nginx لتضمين SSL. الخطوة الخامسة: تعديل تكوين خادم الويب وتعريف الخدمة سيتطلب تفعيل SSL في تكوين Nginx الخاص بنا إضافة إعادة توجيه HTTP إلى HTTPS وتحديد شهادة SSL والمواقع الرئيسية. سيتضمن أيضًا تحديد مجموعتنا Diffie-Hellman، والتي سنستخدمها في Perfect Forward Secrecy. ونظرًا لأنك ستعيد إنشاء خدمة webserver لتضمين هذه الإضافات، فيمكنك إيقافها الآن: docker-compose stop webserver أنشئ بعد ذلك مجلّدًا في مجلّد المشروع الحالي من أجل مفتاحك Diffie-Hellman: mkdir dhparam ثم أنشئ مفتاحك باستخدام الأمر openssl: sudo openssl dhparam -out /home/sammy/node_project/dhparam/dhparam-2048.pem 2048 سوف يستغرق الأمر بضع دقائق لإنشاء المفتاح. لإضافة معلومات Diffie-Hellman و SSL ذات الصلة إلى تكوين Nginx، احذف أولاً ملفّ تكوين Nginx الذي أنشأته مسبقًا: rm nginx-conf/nginx.conf افتح نسخة أخرى من الملف: nano nginx-conf/nginx.conf أضف الشيفرة التالية إلى الملف لإعادة توجيه HTTP إلى HTTPS ولإضافة بيانات اعتماد SSL والبروتوكولات وترويسات الأمان (security headers). لاتنس تعويض example.com بنطاقك الخاص: server { listen 80; listen [::]:80; server_name example.com www.example.com; location ~ /.well-known/acme-challenge { allow all; root /var/www/html; } location / { rewrite ^ https://$host$request_uri? permanent; } } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name example.com www.example.com; server_tokens off; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ssl_buffer_size 8k; ssl_dhparam /etc/ssl/certs/dhparam-2048.pem; ssl_protocols TLSv1.2 TLSv1.1 TLSv1; ssl_prefer_server_ciphers on; ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5; ssl_ecdh_curve secp384r1; ssl_session_tickets off; ssl_stapling on; ssl_stapling_verify on; resolver 8.8.8.8; location / { try_files $uri @nodejs; } location @nodejs { proxy_pass http://nodejs:8080; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-XSS-Protection "1; mode=block" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "no-referrer-when-downgrade" always; add_header Content-Security-Policy "default-src * data: 'unsafe-eval' 'unsafe-inline'" always; # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; # enable strict transport security only if you understand the implications } root /var/www/html; index index.html index.htm index.nginx-debian.html; } تحدد هذه الكتلة الخاصة بالخادم HTTP هوية الخادم webroot المستخدم لطلبات تجديد Certbot على الملف .well-known/acme-challenge. وتتضمن أيضًا توجيهًا لإعادة الكتابة (rewrite directive) يوجه طلبات HTTP إلى المجلّد الجذري إلى HTTPS. أما الكتلة الخاصة بخادم HTTPS فيفعّل SSL و HTTP2. لقراءة المزيد حول كيفية تكرار HTTP/2 على بروتوكولات HTTP والمزايا التي يمكن أن يوفرها لتحسين أداء موقع الويب، يرجى الاطلاع على مقدمة كيفية إعداد Nginx مع دعم HTTP/2 على أوبونتو 18.04. يتضمن هذا الجزء أيضًا سلسلة من الخيارات للتأكد من أنك تستخدم أحدث بروتوكولات SSL والتشفيرات وأن تدبيس OSCP) OSCP stapling) قيد التشغيل. ويسمح لك تدبيس OSCP بتقديم استجابة ذات ختم زمني (time-stamped) من سلطة الإشهاد الخاصة بك أثناء عملية إنشاء الاتصال TLS الأولي، والتي يمكنها تسريع عملية المصادقة. يحدّد الجزء أيضًا بيانات اعتماد SSL و Diffie-Hellman والمواقع الرئيسية. ختامًا، لقد نقلنا معلومات مرور الوكيل إلى هذه الكتلة، بما في ذلك كتلة موقع تتضمّن توجيه try_files، لتوجيه الطلبات إلى حاوية تطبيق Node.js ذات تسمية بديلة، وكتلة موقع لتلك التسمية البديلة، والتي تتضمن ترويسات الأمان التي ستمكننا من الحصول على تقييمات حول أشياء مثل مواقع اختبار خادم SSL Labs ومواقع الأمان. تتضمن هذه الترويسات خيارات X-Frame-Options و X-Content-Type-Options وسياسة الإحالة وسياسة أمان المحتوى و X-XSS-Protection. يتم التصريح علنًا بالترويسة HSTS (اختصار للعبارة HTTP Strict Transport Security)، فعّل هذه فقط إذا فهمت الآثار المترتبة على ذلك وعملت على تقييم الوظيفة "التحميل المسبق" (preload). احفظ الملف وأغلقه عند الانتهاء من التحرير. قبل إعادة إنشاء خدمة webserver، ستحتاج إلى إضافة بعض الأشياء إلى تعريف الخدمة في ملف docker-compose.yml ، بما في ذلك معلومات المنفذ ذات الصلة لـ HTTPS وتعريف وحدة تخزين Diffie-Hellman. افتح الملف: nano docker-compose.yml أضف في تعريف خدمة webserver تعيين المنفذ التالي ووحدة التخزين المسماة dhparam: ... webserver: image: nginx:latest container_name: webserver restart: unless-stopped ports: - "80:80" - "443:443" volumes: - web-root:/var/www/html - ./nginx-conf:/etc/nginx/conf.d - certbot-etc:/etc/letsencrypt - certbot-var:/var/lib/letsencrypt - dhparam:/etc/ssl/certs depends_on: - nodejs networks: - app-network أضف بعد ذلك وحدة تخزين dhparam إلى تعريفات وحدات التخزين الخاصة بك: ... volumes: ... dhparam: driver: local driver_opts: type: none device: /home/sammy/node_project/dhparam/ o: bind على غرار وحدة التخزين web-root، ستركّب وحدة تخزين dhparam مفتاح Diffie-Hellman المخزن على المضيف في حاوية webserver. احفظ الملف وأغلقه عند الانتهاء من التحرير. ثم أعد إنشاء خدمة webserver: docker-compose up -d --force-recreate --no-deps webserver وتحقق من خدماتك باستخدام docker-compose ps: docker-compose ps يجب أن تشاهد الإخراج يشير إلى أن العقدة وخدمات خادم الويب تعمل: Name Command State Ports ---------------------------------------------------------------------------------------------- certbot certbot certonly --webroot ... Exit 0 nodejs node app.js Up 8080/tcp webserver nginx -g daemon off; Up 0.0.0.0:443->443/tcp, 0.0.0.0:80->80/tcp ختامًا، يمكنك زيارة عنوان نطاقك لضمان عمل كل شيء على النحو المنتظر. انتقل في متصفحك إلى https://example.com، مع الحرص على تعويض example.com باسم نطاقك. ستظهر لك صفحة الهبوط التالية: وينبغي أن تشاهد أيضًا رمز القفل في مؤشر أمان متصفحك. إذا كنت ترغب في ذلك، يمكنك الانتقال إلى صفحة الهبوط لاختبار SSL Labs Server أو صفحة اختبار خادم ترويسات الأمان. يجب أن تمنح خيارات التكوين التي أدرجناها لموقعك تصنيفًا على كليهما. الخطوة السادسة: تجديد الشهادات تكون شهادات Let’s Encrypt صالحة لمدة 90 يومًا، لذلك ستحتاج إلى إعداد عملية تجديد تلقائية حرصًا على عدم انقضاء صلاحيتها. إحدى الطرق للقيام بذلك هي إنشاء وظيفة باستخدام أداة جدولة cron. في هذه الحالة، سنقوم بجدولة مهمة cron باستخدام سكربتٍ لتجديد شهاداتنا وإعادة تحميل تكوين Nginx الخاص بنا. افتح سكربتًا بالاسم ssl_renew.sh في مجلّد مشروعك: nano ssl_renew.sh أضف الشيفرة التالية إلى السكربت لتجديد شهاداتك وإعادة تحميل تكوين خادم الويب الخاص بك: #!/bin/bash /usr/local/bin/docker-compose -f /home/sammy/node_project/docker-compose.yml run certbot renew --dry-run \ && /usr/local/bin/docker-compose -f /home/sammy/node_project/docker-compose.yml kill -s SIGHUP webserver بالإضافة إلى تحديد موقع ملف docker-compose الثنائي، فإننا نحدد أيضًا موقع ملف docker-compose.yml الخاص بنا من أجل تشغيل أوامر docker-compose. في هذه الحالة، نستخدم docker-compose run لتشغيل حاوية certbot ولإبطال الأمر command المقدم في تعريف خدمتنا بأمر آخر: وهو الأمر الفرعي renew، والذي سيقوم بتجديد الشهادات التي تكون على وشك الانتهاء. لقد قمنا بتضمين خيار dry-run-- هنا لاختبار السكربت. ثم يستخدم السكربت kill docker-compose لإرسال إشارة SIGHUP إلى حاوية خادم الويب لإعادة تحميل تكوين Nginx. لمزيد من المعلومات حول استخدام هذه العملية لإعادة تحميل تكوين Nginx، يرجى الاطلاع على منشور مدونة Docker هذا عن نشر صورة Nginx الرسمية باستخدام Docker. أغلق الملف عند الانتهاء من التحرير. ثم اجعله قابلًا للتنفيذ: chmod +x ssl_renew.sh بعد ذلك، افتح ملف الجذر crontab الخاص بك لتنفيذ سكربت التجديد في مجال زمني محدد: sudo crontab -e إذا كانت هذه هي المرة الأولى التي تقوم فيها بتحرير هذا الملف، فسيُطلب منك اختيار محرّر: no crontab for root - using an empty one Select an editor. To change later, run 'select-editor'. 1. /bin/ed 2. /bin/nano <---- easiest 3. /usr/bin/vim.basic 4. /usr/bin/vim.tiny Choose 1-4 [2]: ... أضف السطر التالي في الجزء السفلي من الملف: ... */5 * * * * /home/sammy/node_project/ssl_renew.sh >> /var/log/cron.log 2>&1 سيعيّن ذلك المجال الزمني للوظيفة على كل خمس دقائق، إذ يمكنك اختبار ما إذا كان طلبك للتجديد يعمل على النحو المنشود. لقد أنشأنا أيضًا ملف سجل، cron.log، لتسجيل المخرجات ذات الصلة من الوظيفة. بعد خمس دقائق، تحقق من cron.log لمعرفة ما إذا كان طلب التجديد قد نجح أم لا: tail -f /var/log/cron.log يجب أن يظهر لك الإخراج التالي مؤكِّدًا نجاح التجديد: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ** DRY RUN: simulating 'certbot renew' close to cert expiry ** (The test certificates below have not been saved.) Congratulations, all renewals succeeded. The following certs have been renewed: /etc/letsencrypt/live/example.com/fullchain.pem (success) ** DRY RUN: simulating 'certbot renew' close to cert expiry ** (The test certificates above have not been saved.) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Killing webserver ... done يمكنك الآن تعديل ملف crontab لتعيين مجال زمني يومي. لتنفيذ السكربت كل يوم عند الظهر، على سبيل المثال، يمكنك تعديل السطر الأخير من الملف ليبدو كما يلي: ... 0 12 * * * /home/sammy/node_project/ssl_renew.sh >> /var/log/cron.log 2>&1 ستحتاج أيضًا إلى حذف الخيار dry-run-- من السكربت ssl_renew.sh: #!/bin/bash /usr/local/bin/docker-compose -f /home/sammy/node_project/docker-compose.yml run certbot renew \ && /usr/local/bin/docker-compose -f /home/sammy/node_project/docker-compose.yml kill -s SIGHUP webserver ستضمن مهمة cron ألا تنقضي صلاحية شهاداتك " Let’s Encrypt" عبر تجديدها عند الاقتضاء. يمكنك أيضًا إعداد تدوير السجل باستخدام الأداة المساعدة Logrotate لتدوير وضغط ملفات السجل. خاتمة لقد استخدمت حاوياتٍ لإعداد وتشغيل تطبيق Node باستخدام وكيل Nginx عكسي. كما حصلت أيضًا على شهادات SSL لنطاق تطبيقك وأعددت وظيفة cron لتجديد هذه الشهادات عند الضرورة. إذا كنت مهتمًا بمعرفة المزيد عن Let's Encrypt Plugins، فيرجى الاطلاع على مقالاتنا حول استخدام المكون الإضافي Nginx أو المكون الإضافي المستقل. يمكنك أيضًا معرفة المزيد حول Docker Compose من خلال الاطلاع على الموارد التالية: كيفية تثبيت Docker Compose على أوبونتو 18.04. كيفية تكوين بيئة اختبار تكامل مستمر مع تكوين Docker و Docker Compose على أوبونتو 16.04. كيفية إعداد Laravel وNginx وMySQL باستخدام Docker. ويعدّ توثيق Docker أيضًا مورداً رائعًا لمعرفة المزيد حول التطبيقات متعددة الحاويات. ترجمة -وبتصرف- للمقال How To Secure a Containerized Node.js Application with Nginx, Let's Encrypt, and Docker Compose لصاحبته Kathleen Juell
  8. Kubernetes هو نظام لتشغيل التطبيقات الحديثة في حاويات على نطاق واسع. يستطيع المطورون من خلاله نشر وإدارة التطبيقات عبر مجموعات من الأجهزة. ورغم إمكانية استخدامه لتحسين الكفاءة والموثوقية في إعدادات التطبيقات ذات نسخة واحدة، فقد صُمّم Kubernetes لتشغيل نسخٍ متعددة للتطبيق عبر مجموعات من الأجهزة. يختار العديد من المطورين استخدام مدير الحزمة Helm عند إنشاء عمليات نشر متعددة الخدمات باستخدام Kubernetes. ويبسّط Helm عملية إنشاء موارد Kubernetes متعددة من خلال تقديم مخططات وقوالب تنسق كيفية تفاعل هذه الكائنات. كما يقدم أيضًا مخططات مُهيأة مسبقًا لمشاريع مفتوحة المصدر. في هذا الدرس، سوف تنشر تطبيق Node.js بقاعدة بيانات MongoDB على عنقود Kubernetes باستخدام مخططات Helm. ستستخدم مخطط تعيين النسخة المتماثلة Helm MongoDB الرسمي لإنشاء كائن StatefulSet يتكون من ثلاث علب (pods) وخدمة بدون رأس (stateless) وثلاث طلبات وحدة تخزين ثابتة PersistentVolumeClaims. ستُنشئ أيضًا مخططًا لنشر تطبيق Node.js متعدد النسخ باستخدام صورة تطبيق مخصصة. سيعكس الإعداد الذي ستنشئه في هذا البرنامج التعليمي وظيفة الشيفرة الموضحة في كيفية إعداد تطبيق node.js لسير عملٍ يعتمد على الحاويات باستخدام Docker Compose وسيكون نقطة انطلاق جيدة لإنشاء تطبيق Node.js مرن مع مخزن بيانات MongoDB يتناسب مع احتياجاتك. المتطلبات الأساسية لإكمال هذا الدرس، ستحتاج إلى: عنقود +Kubernetes 1.10 مع تفعيل التحكم في الوصول المستند إلى الدور role-based access control ‏(RBAC). سيستخدم هذا الإعداد عنقود DigitalOcean Kubernetes، لكن تبقى لك حرية الاختيار في إنشاء عنقود باستخدام طريقة أخرى. أداة سطر الأوامر kubectl المثبتة على جهازك المحلي أو خادم التطوير وإعدادها للاتصال بعنقودك. يمكنك قراءة المزيد حول تثبيت kubectl في التوثيق الرسمي. Docker مثبت على جهازك المحلي أو خادم التطوير. إذا كنت تعمل على نظام أوبونتو 18.04، اتبع الخطوتين 1 و 2 لكيفية تثبيت واستخدام Docker على أوبونتو 18.04؛ خلاف ذلك، اتبع التوثيق الرسمي للحصول على معلومات حول التثبيت على أنظمة التشغيل الأخرى. تأكد من إضافة مستخدمك غير الجذري إلى مجموعة Docker، كما هو موضح في الخطوة 2 من البرنامج التعليمي المرتبط. حساب Docker Hub. للحصول على نظرة عامة حول كيفية إعداده، راجع هذه المقدمة إلى Docker Hub. مدير الحزمة Helm مثبت على جهازك المحلي أو خادم التطوير مع تثبيت Tiller على نظامك، باتباع الإرشادات الموضحة في الخطوتين 1 و 2 حول كيفية تثبيت البرامج على عناقيد Kubernetes باستخدام مدير الحزمة Helm. الخطوة الأولى: استنساخ وتحزيم التطبيق لاستخدام تطبيقنا على Kubernetes، سنحتاج إلى تحزيمه حتى تتمكن الأداة kubelet من سحب الصورة. سنحتاج قبل تحزيم التطبيق، مع ذلك، إلى تعديل العنوان URI لاتصال MongoDB في شيفرة التطبيق للتأكد من أن تطبيقنا يستطيع الاتصال بعناصر مجموعة النسخ المتماثلة التي سننشئها باستخدام مخطط Helm mongodb-replicaset. ستكون خطوتنا الأولى هي استنساخ مستودع node-mongo-docker-dev من حساب DigitalOcean Community GitHub. يتضمن هذا المستودع شيفرة الإعداد الموضح في كيفية إعداد تطبيق node.js لسير عملٍ يعتمد على الحاويات باستخدام Docker Compose، والذي يستخدم تطبيق Node.js لشرح كيفية إعداد بيئة تطوير باستخدام Docker Compose. يمكنك العثور على مزيد من المعلومات حول التطبيق نفسه في سلسلة من الحاويات إلى Kubernetes باستخدام Node.js. انسخ المستودع في مجلّد يسمى node_project: git clone https://github.com/do-community/node-mongo-docker-dev.git node_project انتقل إلى المجلّد node_project: cd node_project يحتوي مجلّد node_project على ملفات ومجلدات لتطبيق معلومات سمك القرش الذي يعمل على مدخلات المستخدم. تمّ تحديثه للعمل على الحاويات وأزيلت منه معلومات التكوين الحساسة والمحددة في شيفرة التطبيق وأُعيد تشكيلها من أجل حقنها عند تنفيذ التطبيق، كما أُلغيَ تحميل حالة التطبيق إلى قاعدة بيانات MongoDB. لمزيد من المعلومات حول تصميم التطبيقات الحديثة عديمة الحالة، يرجى الاطلاع على هيكلة التطبيقات ل Kubernetes وتحديث التطبيقات لKubernetes. عندما ننشر مخطط Helm mongodb-replicaset، فسينشئ ما يلي: كائن StatefulSet بثلاث علب تمثّل مجموعة النسخ المتماثلة MongoDB. سيكون لكل علبة طلب PersistentVolumeClaim مرتبط وسيحتفظ بهوية ثابتة في حالة إعادة الجدولة. مجموعة نسخ متماثلة MongoDB تتكون من العلب الموجودة في StatefulSet. سوف تشمل المجموعة واحدة ابتدائية واثنتان ثانويتان. ستُنسَخ البيانات من العلبة الابتدائية إلى الثانويتين، مما يضمن إتاحة بيانات التطبيق على أعلى مستوى. لكي يتفاعل تطبيقنا مع النسخ المتماثلة لقاعدة البيانات، سيتطلب الأمر تضمين أسماء المضيفين (hostnames) لعناصر مجموعة النسخ المتماثلة بالإضافة إلى اسم النسخة المتماثلة نفسها في العنوان URI لاتصال MongoDB في الشيفرة. يُسمّى الملف الموجود في مخزننا المستنسخ والذي يحدد معلومات اتصال قاعدة البيانات db.js. افتح هذا الملف الآن باستخدام nano أو المحرر المفضل لديك: nano db.js يشتمل الملف حاليًا على قيم ثابتة يمكن الرجوع إليها في اتصال URI لقاعدة البيانات أثناء التشغيل. تُحقًن هذه القيم الثابتة باستخدام خاصية process.env الخاصة ب Node، والتي تُعيد كائنًا يحتوي على معلوماتٍ حول بيئة المستخدم الخاصة بك في وقت التشغيل. يتيح لنا تحديد القيم ديناميكيًا في شيفرة التطبيق فصل الشيفرة عن البنية التحتية الأساسية، وهو أمر ضروري في بيئة ديناميكية وعديمة الحالة (stateless). لمزيد من المعلومات حول إعادة تشكيل شيفرة التطبيق بهذه الطريقة، راجع الخطوة الثانية من كيفية إعداد تطبيق node.js لسير عملٍ يعتمد على الحاويات باستخدام Docker Compose. تبدو ثوابت الاتصال URI وسلسلة URI نفسها حاليًا كما يلي: ... const { MONGO_USERNAME, MONGO_PASSWORD, MONGO_HOSTNAME, MONGO_PORT, MONGO_DB } = process.env; ... const url = `mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}?authSource=admin`; ... تماشيا مع الطريقة 12FA، لا نريد ترميز أسماء مضيفي مثيلات النسخ المتماثلة أو اسم مجموعة النسخ المتماثلة في سلسلة URI هذه. يمكننا توسيع الثابت MONGO_HOSTNAME الحالي ليشمل عِدّة أسماء مضيفين لعناصر مجموعة النسخ المتماثلة. لذلك سنترك الأمر على حاله. وسنحتاج، مع ذلك، إلى إضافة مجموعة متماثلة ثابتة إلى قسم الخيارات في سلسلة URI. أضف MONGO_REPLICASET إلى كل من كائن الثابت URI وسلسلة الاتصال: ... const { MONGO_USERNAME, MONGO_PASSWORD, MONGO_HOSTNAME, MONGO_PORT, MONGO_DB, MONGO_REPLICASET } = process.env; ... const url = `mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}?replicaSet=${MONGO_REPLICASET}&authSource=admin`; ... يتيح استخدام خيار replicaSet في قسم الخيارات في URI تمرير اسم مجموعة النسخ المتماثلة، والتي تتيح لنا بمعية أسماء المضيفين المحدّدة في ثابت MONGO_HOSTNAME الاتصال بعناصر المجموعة. احفظ الملف وأغلقه عند الانتهاء من التحرير. بعد تعديل معلومات اتصال قاعدة البيانات الخاصة بك للعمل مع مجموعات النسخ المتماثلة، يمكنك الآن تحزيم تطبيقك، وبناء الصورة باستخدام الأمر docker build، ودفعها إلى Docker Hub. ابدأ ببناء الصورة باستخدام Docker والراية t- التي تتيح لك تعليم الصورة باسم لا يُنسى. في هذه الحالة، علّم الصورة باسم مستخدمك Docker Hub وسَمِّها node-replicas أو اسمًا تختاره أنت: docker build -t your_dockerhub_username/node-replicas . تحدد النقطة . في الأمر أن سياق البناء هو المجلّد الحالي. سوف يستغرق الأمر دقيقة أو دقيقتين لبناء الصورة. بمجرد اكتماله، تحقق من صورك: docker images سوف يظهر لك الإخراج التالي: REPOSITORY TAG IMAGE ID CREATED SIZE your_dockerhub_username/node-replicas latest 56a69b4bc882 7 seconds ago 90.1MB node 10-alpine aa57b0242b33 6 days ago بعد ذلك، سجّل الدخول إلى حساب Docker Hub الذي أنشأته في المتطلبات الأساسية: docker login -u your_dockerhub_username عندما يطلب منك ذلك، أدخل كلمة مرور حساب Docker Hub. سيؤدي التسجيل بهذه الطريقة إلى إنشاء ملف ‎~/.docker/config.json في المجلّد الرئيسي لمستخدمك غير الجذري باستخدام بيانات اعتماد Docker Hub. ادفع صورة التطبيق إلى Docker Hub باستخدام الأمر docker push.‎ ولا تنس أن تعوّض yourdockerhubusername باسم مستخدمك Docker Hub: docker push your_dockerhub_username/node-replicas لديك الآن صورة للتطبيق يمكنك سحبها لتشغيل التطبيق المنسوخ باستخدام Kubernetes. ستكون الخطوة التالية هي إعداد بارامترات محدّدة لاستخدامها مع مخطط Helm ل MongoDB. الخطوة الثانية: إنشاء أسرار لمجموعة النسخ المتماثلة MongoDB يوفر المخطط stable/mongodb-replicaset خيارات مختلفة عندما يتعلق الأمر باستخدام الأسرار، وسننشئ منها خيارين لاستخدامهما في نشر المخطط: سرٌّ (secret) لمجموعة ملفات النسخ المتماثلة سيعمل ككلمة مرور مشتركة بين عناصر مجموعة النسخ المتماثلة، مما يسمح بتصديق الهوية لأعضاء آخرين. سرٌّ للمستخدم المشرف في MongoDB، الذي سيتم إنشاؤه كمستخدم الجذر في قاعدة بيانات المشرف. سيتيح هذا الدور بإنشاء مستخدمين لاحقًا بأذونات محدودة عند نشر تطبيقك على الإنتاج. بإنشائنا لهذه الأسرار، سنكون قادرين على تعيين قيم المعاملات المفضلة لدينا في ملف قيم مخصص وإنشاء كائن StatefulSet ونسخة متماثلة MongoDB مع مخطط Helm. دعنا أولًا ننشئ ملف keyfile. سنستخدم الأمر openssl مع خيار rand لإنشاء سلسلة عشوائية بحجم 756 بايت لملف keyfile: openssl rand -base64 756 > key.txt سيُشفَّر الإخراج الذي أُنشِئ باستخدام الأمر base64، مما يضمن نقل بيانات موحد، كما سيُعاد توجيهه إلى ملف يسمى key.txt، باتباع الإرشادات الواردة في وثائق مصادقة تخطيط mongodb-replicaset. يجب أن يتراوح طول المفتاح نفسه بين 6 محارف و 1024 محرفًا، ويتألف فقط من المحارف في مجموعة base64. يمكنك الآن إنشاء سر يسمى keyfilesecret على هذا الملف باستخدام kubectl create: kubectl create secret generic keyfilesecret --from-file=key.txt سيُنشئ هذا كائنًا سرًّا في المجال الاسمي الافتراضي، نظرًا لأننا لم ننشئ مجالًا اسميًا محددا لإعدادنا. سيظهر لك الإخراج التالي مشيرًا إلى إنشاء سرّك: secret/keyfilesecret created احذف key.txt: rm key.txt إذا كنت ترغب في حفظ الملف بدلاً من ذلك، فتأكد من تقييد أذوناته وإضافته إلى ملفك gitignore. لإبقائه خارج تحكم الإصدارات. بعد ذلك، أنشئ سرًّا لمستخدمك المشرف في MongoDB. ستكون الخطوة الأولى هي تحويل اسم المستخدم وكلمة المرور المطلوبين إلى base64. ابدأ بتحويل اسم مستخدم قاعدة بياناتك: echo -n 'your_database_username' | base64 دوّن القيمة التي تظهر في الإخراج. بعد ذلك، حوّل كلمة مرورك: echo -n 'your_database_password' | base64 دوّن القيمة الظاهرة في الإخراج هنا أيضا. افتح ملف السر: nano secret.yaml ملاحظة: تُحدّد كائنات Kubernetes عادة باستخدام YAML، الذي يمنع بصرامة علامات التبويب ويتطلب مسافتين للمسافة البادئة. إذا كنت ترغب في التحقق من تنسيق أي من ملفاتك YAML، فيمكنك استخدام linter أو اختبار صحّة صياغة (syntax) تركيبك باستخدام kubectl create مع الرايتين dry-run-- و validate--: kubectl create -f your_yaml_file.yaml --dry-run --validate=true بشكل عام، من المستحسن التحقق من صحة الصياغة قبل إنشاء الموارد باستخدام kubectl. أضف الشيفرة التالية إلى الملف لإنشاء سرّ يحدد المستخدم وكلمة المرور مع القيم المشفرة التي أنشأتها للتو. لا تنس تعويض القيم الوهمية هنا باسم المستخدم وكلمة المرور المشفرة: apiVersion: v1 kind: Secret metadata: name: mongo-secret data: user: your_encoded_username password: your_encoded_password نحن نستخدم هنا أسماء المفاتيح التي يتوقعها مخطط mongodb-replicaset: المستخدم وكلمة المرور. ولقد سمّينا كائن السرّ mongo-secret، ولكن تستطيع تسميته كما تشاء. احفظ الملف وأغلقه عند الانتهاء من التحرير. أنشئ كائن السرّ باستخدام الأمر التالي: kubectl create -f secret.yaml سيظهر لك في الإخراج ما يلي: secret/mongo-secret created يمكنك مرة أخرى إما حذف secret.yaml أو تقييد أذوناته وإضافته إلى ملفك gitignore.. بعد إنشاء كائنات السرّ، يمكنك الانتقال إلى تحديد قيم المعاملات التي ستستخدمها مع مخطط mongodb-replicaset وإنشاء نشر MongoDB. الخطوة الثالثة: تكوين المخطّط MongoDB Helm وإنشاء النشر يأتي Helm مع مستودع نشط ومحفوظ يدعى stable والذي يحتوي على المخطط الذي سنستخدمه: mongodb-replicaset. لاستخدام هذا المخطط مع الأسرار التي أنشأناها للتو، سننشئ ملفًا به قيم معاملات التكوين يسمى mongodb-values.yaml ثم نثبت المخطط باستخدام هذا الملف. سوف يعكس ملفنا mongodb-values.yaml إلى حد كبير ملف value.yaml الافتراضي في مستودع تخزين المخطط mongodb-replicaset. ومع ذلك، سنجري التغييرات التالية على ملفنا: سنعيّن المعامل auth على القيمة true لنضمن تفعيل التفويض (authorization) عند بدء اشتغال مثيلات قاعدة البيانات. هذا يعني أنه سيُطلب من جميع العملاء تصديق الهوية للوصول إلى موارد وعمليات قاعدة البيانات. سنضيف معلومات حول الأسرار التي أنشأناها في الخطوة السابقة حتى يتمكن المخطط من استخدام هذه القيم لإنشاء مجموعة النسخ المتماثلة keyfile والمستخدم المشرف. سنقلّل من سعة وحدات التخزين الثابتة PersistentVolumes المرتبطة بكل علبة Pod في مجموعة StatefulSet لاستخدام الحد الأدنى القابل للتطبيق في وحدات التخزين DigitalOcean ، المحدّد في 1 جيجابايت، رغم أنك تبقى حرًّا في تعديل هذه القيمة لتلبية متطلبات التخزين الخاصة بك. يجب عليك أولاً قبل كتابة ملف mongodb-values.yaml، ومع ذلك، التحقق من أنّك لديك صنف StorageClass أُنشِئ وأُعِدّ لتوفير موارد التخزين. سيكون لكل علبة موجودة في قاعدة بياناتك StatefulSet هوية ملازمة وما يرتبط بها من طلبات PersistentVolumeClaim، والتي ستوفر PersistentVolume ديناميكيًا للعلبة. إذا أُعيدت جدولة العلبة، فستثبّت PersistentVolume على أي عقدة (node) تُجدول عليها العلبة Pod (رغم أنه ينبغي حذف كل وحدة تخزين يدويًا إذا حذفت العلبة Pod أو StatefulSet المقترن بها نهائيًا). وبما أننا نعمل على DigitalOcean Kubernetes، فإن مزود StorageClass الافتراضي الخاص بنا يعيّن على dobs.csi.digitalocean.com أي وحدة التخزين DigitalOcean. ويمكننا التحقق من ذلك بكتابة: kubectl get storageclass إذا كنت تعمل عل عنقود DigitalOcean، فسترى في الإخراج مايلي: NAME PROVISIONER AGE do-block-storage (default) dobs.csi.digitalocean.com 21m إذا كنت لا تعمل مع مجموعة DigitalOcean، فستحتاج إلى إنشاء StorageClass وتكوين مزود من اختيارك. للحصول على تفاصيل حول كيفية القيام بذلك، يرجى الاطلاع على الوثائق الرسمية. الآن وبعد التأكد من تكوين StorageClass، افتح الملفّ mongodb-values.yaml لتحريره: nano mongodb-values.yaml سوف نعيّن القيم في هذا الملف للقيام بما يلي: تفعيل التفويض. تحديد مراجع لملفك keyfilesecret وكائناتك mongo-secret. تحديد القيمة Gi1 لـ PersistentVolumes. تعيين اسم مجموعة النسخ المتماثلة على db. تحديد 3 نسخ متماثلة للمجموعة. تثبيت صورة mongo بأحدث إصدار. انسخ الشيفرة التالية إلى الملف: replicas: 3 port: 27017 replicaSetName: db podDisruptionBudget: {} auth: enabled: true existingKeySecret: keyfilesecret existingAdminSecret: mongo-secret imagePullSecrets: [] installImage: repository: unguiculus/mongodb-install tag: 0.7 pullPolicy: Always copyConfigImage: repository: busybox tag: 1.29.3 pullPolicy: Always image: repository: mongo tag: 4.1.9 pullPolicy: Always extraVars: {} metrics: enabled: false image: repository: ssalaues/mongodb-exporter tag: 0.6.1 pullPolicy: IfNotPresent port: 9216 path: /metrics socketTimeout: 3s syncTimeout: 1m prometheusServiceDiscovery: true resources: {} podAnnotations: {} securityContext: enabled: true runAsUser: 999 fsGroup: 999 runAsNonRoot: true init: resources: {} timeout: 900 resources: {} nodeSelector: {} affinity: {} tolerations: [] extraLabels: {} persistentVolume: enabled: true #storageClass: "-" accessModes: - ReadWriteOnce size: 1Gi annotations: {} serviceAnnotations: {} terminationGracePeriodSeconds: 30 tls: enabled: false configmap: {} readinessProbe: initialDelaySeconds: 5 timeoutSeconds: 1 failureThreshold: 3 periodSeconds: 10 successThreshold: 1 livenessProbe: initialDelaySeconds: 30 timeoutSeconds: 5 failureThreshold: 3 periodSeconds: 10 successThreshold: 1 يوجد هنا تعليق على المُعامل persistentVolume.storageClass: سيؤدي حذف التعليق وتعيين قيمته على "-" إلى تعطيل التزويد الديناميكي. في حالتنا هذه، نظرًا لأننا نترك هذه القيمة غير محددة، فإن المخطط سيختار المزود الافتراضي، أي dobs.csi.digitalocean.com. لاحظ أيضًا وضع الولوج accessMode المقترن بالمفتاح persistentVolume: يعني ReadWriteOnce أن وحدة التخزين المتوفرة ستكون للقراءة والكتابة بواسطة عقدة واحدة فقط. يرجى الاطلاع على التوثيق لمزيد من المعلومات حول أوضاع الولوج المختلفة. لمعرفة المزيد حول المعاملات الأخرى المضمنة في الملف، راجع جدول التكوين المضمّن في المستودع. احفظ الملف وأغلقه عند الانتهاء من التحرير. قبل نشر مخطط mongodb-replicaset، ستحتاج إلى تحديث المستودع الثابت stable باستخدام الأمر helm repo update: helm repo update سيستخرج هذا أحدث معلومات المخطط من المستودع الثابت. أخيرًا، ثبّت المخطط باستخدام الأمر التالي: helm install --name mongo -f mongodb-values.yaml stable/mongodb-replicaset ملاحظة: قبل تثبيت أحد المخططات، يمكنك تنفيذ helm install باستخدام الخيارين run dry-- و debug-- للتحقق من البيانات التي أُنشئت لإصدارك: helm install --name your_release_name -f your_values_file.yaml --dry-run --debug your_chart لاحظ أننا نسمي إصدار Helm بالاسمmongo. سوف يشير هذا الاسم إلى هذا النشر المحدد للمخطط بخيارات التكوين التي حددناها. لقد أشرنا إلى هذه الخيارات من خلال تضمين الراية f- وملفنا mongodb-values.yaml. لاحظ أيضًا أنه نظرًا لأننا لم نضمّن الراية ‎--namespace مع helm install، سيكون إنشاء كائنات المخطط في المجال الاسمي الافتراضي. بمجرد إنشاء الإصدار، ستظهر حالته في الإخراج، إلى جانب معلومات حول الكائنات التي أُنشِئت وإرشادات للتفاعل معها: NAME: mongo LAST DEPLOYED: Tue Apr 16 21:51:05 2019 NAMESPACE: default STATUS: DEPLOYED RESOURCES: ==> v1/ConfigMap NAME DATA AGE mongo-mongodb-replicaset-init 1 1s mongo-mongodb-replicaset-mongodb 1 1s mongo-mongodb-replicaset-tests 1 0s ... يمكنك الآن التحقق من إنشاء علبك pods باستخدام الأمر التالي: kubectl get pods سيظهر لك في الإخراج مايلي عند إنشاء العلب: NAME READY STATUS RESTARTS AGE mongo-mongodb-replicaset-0 1/1 Running 0 67s mongo-mongodb-replicaset-1 0/1 Init:0/3 0 8s تشير المخرجات READY و STATUS هنا إلى أن العلب الموجودة في StatefulSet ليست جاهزة تمامًا: لا تزال حاويات التهيئة المرتبطة بحاويات العلب قيد التشغيل. ولأن إنشاء عناصر StatefulSet يتمّ وفق ترتيب تسلسلي، يجب أن تكون كل علبة في StatefulSet قيد التشغيل وجاهزة قبل إنشاء العلبة التالية. بعد إنشاء العلب وتشغيل جميع الحاويات المرتبطة بها، سيظهر لك هذا الإخراج: NAME READY STATUS RESTARTS AGE mongo-mongodb-replicaset-0 1/1 Running 0 2m33s mongo-mongodb-replicaset-1 1/1 Running 0 94s mongo-mongodb-replicaset-2 1/1 Running 0 36s يشير Running STATUS إلى أن علبك مرتبطة بالعقد وأن الحاويات المرتبطة بتلك العلب تعمل. ويشير READY إلى عدد الحاويات الموجودة في العلب. لمزيد من المعلومات، يرجى الرجوع إلى وثائق دورة حياة العلبة. ملحوظة إذا رأيت مراحل غير متوقعة في العمود STATUS، فتذكر أنه يمكنك استكشاف الأخطاء وإصلاحها باستخدام الأوامر التالية: kubectl describe pods your_pod kubectl logs your_pod لكل علبة موجودة في StatefulSet اسم يجمع اسم StatefulSet مع الرقم الترتيبي للعلبة. ونظرًا لأننا أنشأنا ثلاث نسخ متماثلة، ترقَّم عناصر StatefulSet من 0 إلى 2، ولكل منها إدخال DNS ثابت يتكون من العناصر التالية: ‎$(statefulset-name)-$(ordinal).$(service name).$(namespace).svc.cluster.local. في حالتنا هذه، لكلّ من StatefulSet والخدمة بدون ترويسة التي أنشأها مخطط mongodb-replicaset نفس الاسم: kubectl get statefulset NAME READY AGE mongo-mongodb-replicaset 3/3 4m2s kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.245.0.1 <none> 443/TCP 42m mongo-mongodb-replicaset ClusterIP None <none> 27017/TCP 4m35s mongo-mongodb-replicaset-client ClusterIP None <none> 27017/TCP 4m35s هذا يعني أن العنصر الأول في StatefulSet سيحصل على إدخال DNS التالي: mongo-mongodb-replicaset-0.mongo-mongodb-replicaset.default.svc.cluster.local ولأننا نحتاج إلى أن يتصل تطبيقنا بكل نسخة من إصدارات MongoDB، فمن الضروري أن تتوفر لدينا هذه المعلومات حتى نتمكن من التواصل مباشرة مع العلب، وليس مع الخدمة. عندما ننشئ مخطط Helm للتطبيق المخصص، سنمرّر إدخالات DNS لكل علبة إلى تطبيقنا باستخدام متغيرات البيئة. بعد تشغيل مثيلات قاعدة بياناتك ، يمكنك البدء في إنشاء المخطط لتطبيقك Node. الخطوة الرابعة: إنشاء مخطط تطبيق مخصّص وتكوين المعاملات سنعمل على إنشاء مخطط Helm مخصّص لتطبيق Node وتعديل الملفات الافتراضية في مجلّد المخطّط الأساسي حتى نتيح للتطبيق العمل على مجموعة النسخ المتماثلة التي أنشأناها للتو. وسننشئ كذلك ملفات لتحديد خريطة الإعداد ConfigMap وكائنات السرّ لتطبيقنا. ابدأ أولاً بإنشاء مجلّد مخطط جديد يسمى nodeapp باستخدام الأمر التالي: helm create nodeapp سيؤدي هذا إلى إنشاء مجلّد بالاسم nodeapp في المجلد ‎~/node_project بالموارد التالية: ملف Chart.yaml يحتوي على معلومات أساسية حول المخطط. ملف value.yaml يتيح لك تعيين قيم معاملات محددة، كما فعلت مع نشر MongoDB. ملف helmignore. مع أنماط الملفات والمجلّد التي سيتم تجاهلها عند تحزيم المخططات. مجلّد templates/ بملفات القوالب التي سيولّدها Kubernetes. مجلّد templates/tests/ لملفات الاختبار. مجلّد charts/ لأي مخططات يعتمد عليها هذا المخطط. value.yaml هو الملف الأول الذي سنعدّله من هذه الملفات الافتراضية. افتح هذا الملف الآن: nano nodeapp/values.yaml تشمل القيم التي سنضعها هنا ما يلي: عدد النسخ المتماثلة. صورة التطبيق التي نريد استخدامها. في حالتنا، ستكون هذه هي صورة النسخ المتماثلة للعقدة التي أنشأناها في الخطوة الأولى. نوع الخدمة. في هذه الحالة، سنحدد LoadBalancer لإنشاء نقطة وصول إلى تطبيقنا لأغراض الاختبار. ونظرًا لأننا نعمل على عنقود DigitalOcean Kubernetes، فسيؤدي ذلك إلى إنشاء موازن تحميل DigitalOcean عند نشر مخططنا. يمكنك في الإنتاج تكوين مخططك لاستخدام موارد Ingress و Controllers لتوجيه حركة المرور إلى خدماتك. المنفذ المستهدف targetPort لتحديد المنفذ على العلبة Pod حيث سيتم الكشف عن تطبيقنا. لن ندخل متغيرات البيئة في هذا الملف. وبدلاً من ذلك، سننشئ قوالب لـ ConfigMap وكائنات السرّ ونضيف هذه القيم إلى بيان نشر التطبيق (application Deployment manifest) ، الموجود في ‎~/node_project/nodeapp/templates/deployment.yaml. أضف تكوين القيم التالية في ملف values.yaml: # Default values for nodeapp. # This is a YAML-formatted file. # Declare variables to be passed into your templates. replicaCount: 3 image: repository: your_dockerhub_username/node-replicas tag: latest pullPolicy: IfNotPresent nameOverride: "" fullnameOverride: "" service: type: LoadBalancer port: 80 targetPort: 8080 ... احفظ الملف وأغلقه عند الانتهاء من التحرير. بعد ذلك، افتح ملف secret.yaml في المجلّد nodeapp/templates: nano nodeapp/templates/secret.yaml أضف في هذا الملف قيمًا لثوابت التطبيق MONGOUSERNAME و MONGOPASSWORD. هذه هي الثوابت التي يتوقع تطبيقك الوصول إليها أثناء التشغيل، كما هو محدّد في db.js، ملف اتصال قاعدة البيانات. وأثناء إضافتك لقيم هذه الثوابت، لا تنس استخدام القيم المرمّزة base64 التي استخدمتها سابقًا في الخطوة الثانية عند إنشاء الكائن mongo-secret. إذا كنت بحاجة إلى إعادة إنشاء هذه القيم، يمكنك العودة إلى الخطوة الثانية ونفّذ الأوامر ذات الصلة مرة أخرى. أضف الشيفرة التالية إلى الملف: apiVersion: v1 kind: Secret metadata: name: {{ .Release.Name }}-auth data: MONGO_USERNAME: your_encoded_username MONGO_PASSWORD: your_encoded_password يعتمد اسم كائن السرّ هذا على اسم إصدار Helm، والذي ستحدّده عند نشر مخطط التطبيق. احفظ الملف وأغلقه عند الانتهاء. بعد ذلك، افتح ملفًا لإنشاء ConfigMap لتطبيقك: nano nodeapp/templates/configmap.yaml سوف نحدد في هذا الملف المتغيرات المتبقية التي ينتظرها تطبيقنا: MONGO_HOSTNAME و MONGO_PORT و MONGO_DB و MONGO_REPLICASET. سيتضمن متغير MONGO_HOSTNAME إدخال DNS لكل مثيل في مجموعة النسخ المتماثلة، إذ أن هذا هو ما يتطلبه عنوان URI لاتصال MongoDB. وفقًا لتوثيق Kubernetes، عندما يقوم أحد التطبيقات بإجراء اختبارات الصلاحية والاستعداد، يجب استخدام سجلات SRV عند الاتصال بـالعلب Pods. وكما تطرقنا إليه في الخطوة الثالثة، تتبع سجلات SRV للعلبة هذا النموذج: ‎$(statefulset-name)-$(ordinal).$(service name).$(namespace).svc.cluster.local. ونظرًا لأن تطبيق StatefulSet يجري عمليات فحص الثبات والاستعداد، فسيتوجب علينا استخدام هذه المعرّفات الثابتة عند تحديد قيم المتغير MONGO_HOSTNAME. أضف الشيفرة التالية إلى الملف لتعريف متغيرات MONGOHOSTNAME و MONGOPORT و MONGODB و MONGOREPLICASET. وتبقى لك الحرية في استخدام اسم آخر لقاعدة بيانات MONGODB، ولكن يجب أن تكتب قيمك MONGOHOSTNAME و MONGO_REPLICASET مثلما تظهر هنا: apiVersion: v1 kind: ConfigMap metadata: name: {{ .Release.Name }}-config data: MONGO_HOSTNAME: "mongo-mongodb-replicaset-0.mongo-mongodb-replicaset.default.svc.cluster.local,mongo-mongodb-replicaset-1.mongo-mongodb-replicaset.default.svc.cluster.local,mongo-mongodb-replicaset-2.mongo-mongodb-replicaset.default.svc.cluster.local" MONGO_PORT: "27017" MONGO_DB: "sharkinfo" MONGO_REPLICASET: "db" بما أننا أنشأنا بالفعل كائن StatefulSet ومجموعة النسخ المتماثلة، فيجب أن تظهر أسماء المضيفين الواردة هنا في ملفك تمامًا كما تظهر في هذا المثال. إذا دمّرت هذه الكائنات وأعدت تسمية إصدار Helm ل MongoDB ، فستحتاج إلى مراجعة القيم المضمنة في ConfigMap. ينطبق الشيء نفسه على MONGO_REPLICASET، حيث حدّدنا اسم مجموعة النسخ المتماثلة بإصدار MongoDB. لاحظ أيضًا أن القيم المذكورة هنا موضوعة في شكل اقتباس، وهو ما يتوقّعه Helm لمتغيرات البيئة. احفظ الملف وأغلقه عند الانتهاء من التحرير. بعد تحديد قيم معاملات مخطّطك وإنشاء قوائم Secret و ConfigMap، يمكنك المرور لتحرير قالب نشر التطبيق لاستخدام متغيرات البيئة الخاصة بك. الخطوة الخامسة: دمج متغيرات البيئة في نشر Helm بعد إعدادنا لملفات التطبيق Secret و ConfigMap، سنحتاج إلى التأكد من أن نشر تطبيقنا يستطيع استخدام هذه القيم. وسنعمل أيضًا على تخصيص اختبارات الثبات والاستعداد التي حُدّدت بالفعل في بيان النشر. افتح قالب نشر التطبيق للتحرير: nano nodeapp/templates/deployment.yaml رغم أنّ هذا ملف YAML، فإن قوالب Helm تستخدم صياغة تركيب مختلفة عن ملفات YAML المعيارية Kubernetes لإنشاء كشوف البيانات. لمزيد من المعلومات حول القوالب، راجع توثيق Helm. أضف في الملف أولاً مفتاح env لمواصفات حاوية التطبيق، أسفل مفتاح imagePullPolicy وأعلى ports: apiVersion: apps/v1 kind: Deployment metadata: ... spec: containers: - name: {{ .Chart.Name }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" imagePullPolicy: {{ .Values.image.pullPolicy }} env: ports: بعد ذلك، أضف المفاتيح التالية إلى قائمة متغيرات env: apiVersion: apps/v1 kind: Deployment metadata: ... spec: containers: - name: {{ .Chart.Name }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" imagePullPolicy: {{ .Values.image.pullPolicy }} env: - name: MONGO_USERNAME valueFrom: secretKeyRef: key: MONGO_USERNAME name: {{ .Release.Name }}-auth - name: MONGO_PASSWORD valueFrom: secretKeyRef: key: MONGO_PASSWORD name: {{ .Release.Name }}-auth - name: MONGO_HOSTNAME valueFrom: configMapKeyRef: key: MONGO_HOSTNAME name: {{ .Release.Name }}-config - name: MONGO_PORT valueFrom: configMapKeyRef: key: MONGO_PORT name: {{ .Release.Name }}-config - name: MONGO_DB valueFrom: configMapKeyRef: key: MONGO_DB name: {{ .Release.Name }}-config - name: MONGO_REPLICASET valueFrom: configMapKeyRef: key: MONGO_REPLICASET name: {{ .Release.Name }}-config يتضمن كل متغير مرجعًا إلى قيمته، يعرّف إما بمفتاح secretKeyRef، في حالة قيم السرّ، أو configMapKeyRef لقيم ConfigMap. تشير هذه المفاتيح إلى ملفات Secret و ConfigMap التي أنشأناها في الخطوة السابقة. بعد ذلك، تحت المفتاح ports، عدّل تعريف containerPort لتحديد المنفذ الموجود في الحاوية التي سيُعرَض عليها تطبيقنا: apiVersion: apps/v1 kind: Deployment metadata: ... spec: containers: ... env: ... ports: - name: http containerPort: 8080 protocol: TCP ... دعنا نعدّل بعد ذلك، اختبارات الصلاحية والاستعداد المضمنة في بيان النشر هذا افتراضيًا. تضمن هذه الفحوصات اشتغال علب التطبيق لدينا وجاهزيتها لخدمة حركة المرور: تُقيِّم اختبارات الجاهزية (Readiness) ما إذا كانت العلبة جاهزًا لخدمة حركة المرور أم لا، مع إيقاف جميع الطلبات إلى العلبة حتى تنجح عمليات الفحص. تفحص اختبارات الثبات (Liveness) سلوك التطبيق الأساسي لتحديد ما إذا كان التطبيق في الحاوية قيد التشغيل أو يتصرف كما هو متوقع. في حالة فشل اختبار الثبات، سيعيد Kubernetes تشغيل الحاوية. لمزيد من المعلومات حول كليهما، راجع المناقشة ذات الصلة في هيكلة تطبيقات لـ Kubernetes. في حالتنا هذه، سنبني على طلب httpGet الذي قدمه Helm افتراضيًا ونختبر ما إذا كان تطبيقنا يقبل الطلبات في نقطة نهاية sharks/ أم لا. ستجري خدمة kubelet اختبارًا بإرسال طلب GET إلى خادم Node الذي يشتغل في حاوية علب التطبيق ويستمع على المنفذ 8080. إذا كان رمز الجواب يتراوح بين 200 و 400، فسوف تستنتج kubelet أن الحاوية في وضعٍ صحي. خلاف ذلك، إذا كان الرمز 400 أو 500، فإن kubelet تعمد إمّا إلى إيقاف حركة المرور إلى الحاوية، في حالة اختبار الجاهزية، أو إعادة تشغيل الحاوية، في حالة اختبار الثبات. أضف التعديل التالي إلى المسار path المذكور لاختبارات الثبات والجاهزية: apiVersion: apps/v1 kind: Deployment metadata: ... spec: containers: ... env: ... ports: - name: http containerPort: 8080 protocol: TCP livenessProbe: httpGet: path: /sharks port: http readinessProbe: httpGet: path: /sharks port: http احفظ الملف وأغلقه عند الانتهاء من التحرير. أنت الآن جاهز لإنشاء إصدار التطبيق الخاص بك باستخدام Helm. نفذ الأمر التالي لتثبيت helm، والذي يتضمن اسم الإصدار وموقع مجلّد المخطط: helm install --name nodejs ./nodeapp لا تنس أنه يمكنك تنفيذ تثبيت helm باستخدام الخيارين dry-run-- و debug-- أولاً، كما ذكرناه في الخطوة الثالثة، من أجل التحقق من البيانات (manifests) التي أُنشئت لإصدارك. مرة أخرى، نظرًا لأننا لا نقوم بتضمين الراية --namespace مع helm install، سيكون إنشاء كائنات المخطط في المجال الاسمي الافتراضي. سيظهر لك الإخراج التالي مشيرًا إلى إنشاء إصدارك: NAME: nodejs LAST DEPLOYED: Wed Apr 17 18:10:29 2019 NAMESPACE: default STATUS: DEPLOYED RESOURCES: ==> v1/ConfigMap NAME DATA AGE nodejs-config 4 1s ==> v1/Deployment NAME READY UP-TO-DATE AVAILABLE AGE nodejs-nodeapp 0/3 3 0 1s ... مرة أخرى، سيشير الإخراج إلى حالة الإصدار، بالإضافة إلى معلومات حول الكائنات التي أُنشئت وكيفية تفاعلك معها. تحقق من حالة علبك: kubectl get pods NAME READY STATUS RESTARTS AGE mongo-mongodb-replicaset-0 1/1 Running 0 57m mongo-mongodb-replicaset-1 1/1 Running 0 56m mongo-mongodb-replicaset-2 1/1 Running 0 55m nodejs-nodeapp-577df49dcc-b5fq5 1/1 Running 0 117s nodejs-nodeapp-577df49dcc-bkk66 1/1 Running 0 117s nodejs-nodeapp-577df49dcc-lpmt2 1/1 Running 0 117s تحقق من خدماتك بمجرد تشغيل العلب: kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.245.0.1 <none> 443/TCP 96m mongo-mongodb-replicaset ClusterIP None <none> 27017/TCP 58m mongo-mongodb-replicaset-client ClusterIP None <none> 27017/TCP 58m nodejs-nodeapp LoadBalancer 10.245.33.46 your_lb_ip 80:31518/TCP العنوان EXTERNAL_IP المرتبط بخدمة nodejs هو عنوان IP يمكّنك من الوصول إلى التطبيق. إذا رأيت حالة في عمود EXTERNAL_IP، فهذا يعني أن مُوازِن التحميل لا يزال قيد الإنشاء. بمجرد رؤية عنوان IP في هذا العمود، انتقل إليه في متصفحك: http://yourlbip. ينبغي أن تظهر لك صفحة الهبوط التالية: الآن بعد أن تم تشغيل التطبيق المنسوخ، دعنا نضيف بعض بيانات الاختبار لضمان عمل التماثل بين عناصر مجموعة النسخ المتماثلة. الخطوة السادسة: اختبار المتماثل في MongoDB بعد تشغيل التطبيق وإتاحة الوصول إليه من خلال عنوان IP خارجي، يمكننا الآن إضافة بعض بيانات الاختبار والتأكد من أنها تُنسَخ بشكل متماثل بين عناصر مجموعة النسخ المتماثلة MongoDB. تأكّد أولًا من فتح صفحة الهبوط على متصفحك: انقر على زر الحصول على معلومات القرش. ستظهر لك صفحة ذات نموذج يمكنك فيه إدخال اسم سمك القرش ووصف لسلوكه العام: أضف في النموذج سمكة قرش أولية من اختيارك. سنضيف لغرض التوضيح Megalodon Shark إلى حقل Shark Name، وAncient لحقل Shark Character: انقر على زر الإرسال. سترى صفحة بها معلومات القرش معروضة لك: انتقل الآن مرة أخرى إلى نموذج معلومات سمك القرش من خلال النقر على Sharks في شريط التنقل العلوي: أدخل سمكة قرش جديدة من اختيارك. سنستخدم Whale Shark مع Large: بمجرد نقرك على زر الإرسال، سترى أنه القرش الجديد أضيف إلى المجموعة shark في قاعدة البيانات الخاصة بك: دعنا نتحقق من أن البيانات التي أدخلناها نُسخت نسخًا متماثلاً بين العناصر الأساسية والثانوية في مجموعة النسخ المتماثلة. استعرض قائمة علبك: kubectl get pods NAME READY STATUS RESTARTS AGE mongo-mongodb-replicaset-0 1/1 Running 0 74m mongo-mongodb-replicaset-1 1/1 Running 0 73m mongo-mongodb-replicaset-2 1/1 Running 0 72m nodejs-nodeapp-577df49dcc-b5fq5 1/1 Running 0 5m4s nodejs-nodeapp-577df49dcc-bkk66 1/1 Running 0 5m4s nodejs-nodeapp-577df49dcc-lpmt2 1/1 Running 0 5m4s للوصول إلى صدفة mongo على علبك، يمكنك استخدام الأمر kubectl exec واسم المستخدم الذي استعملته لإنشاء mongo secret في الخطوة الثانية. ادخل إلى صدفة mongo على العلبة الأولى في StatefulSet باستخدام الأمر التالي: kubectl exec -it mongo-mongodb-replicaset-0 -- mongo -u your_database_username -p --authenticationDatabase admin عند يطلب منك ذلك، أدخل كلمة المرور المرتبطة باسم المستخدم هذا: MongoDB shell version v4.1.9 Enter password: سيتم تحويلك إلى صدفة إدارية: MongoDB server version: 4.1.9 Welcome to the MongoDB shell. ... db:PRIMARY> رغم أن شاشة الإدخال نفسها تتضمن هذه المعلومات، فيمكنك التحقق يدويًا لمعرفة أي عناصر مجموعة النسخ المتماثلة هو الأساسي باستخدام التابع ()rs.isMaster: rs.isMaster() سيظهر لك الإخراج التالي، مع الإشارة إلى اسم المضيف الأساسي: db:PRIMARY> rs.isMaster() { "hosts" : [ "mongo-mongodb-replicaset-0.mongo-mongodb-replicaset.default.svc.cluster.local:27017", "mongo-mongodb-replicaset-1.mongo-mongodb-replicaset.default.svc.cluster.local:27017", "mongo-mongodb-replicaset-2.mongo-mongodb-replicaset.default.svc.cluster.local:27017" ], ... "primary" : "mongo-mongodb-replicaset-0.mongo-mongodb-replicaset.default.svc.cluster.local:27017", ... انتقل بعد ذلك إلى قاعدة بياناتك sharkinfo: use sharkinfo switched to db sharkinfo اعرض قائمة المجموعات في قاعدة البيانات: show collections sharks استخرج الملفات في المجموعة: db.sharks.find() سيظهر لك في الإخراج ما يلي: { "_id" : ObjectId("5cb7702c9111a5451c6dc8bb"), "name" : "Megalodon Shark", "character" : "Ancient", "__v" : 0 } { "_id" : ObjectId("5cb77054fcdbf563f3b47365"), "name" : "Whale Shark", "character" : "Large", "__v" : 0 } اخرج الآن من صدفة MongoDB: Exit الآن بعد أن تفحصنا البيانات الموجودة على العنصر الأساسي، دعنا نتحقق من نسخها في الثانوي. نفّذ الأمر exec kubectl في mongo-mongodb-replicaset-1: kubectl exec -it mongo-mongodb-replicaset-1 -- mongo -u your_database_username -p --authenticationDatabase admin بمجرد الدخول إلى الصدفة الإدارية، سنحتاج إلى استخدام التابع ()db.setSlaveOk للسماح بعمليات القراءة من المثيل الثانوي: db.setSlaveOk(1) انتقل إلى قاعدة بيانات sharkinfo: use sharkinfo switched to db sharkinfo اسمح بعمليات قراءة الملفات في المجموعة sharks: db.setSlaveOk(1) استخرج الملفات في المجموعة: db.sharks.find() ينبغي أن تشاهد الآن نفس المعلومات التي شاهدتها عند تنفيذ هذا التابع على العنصر الأساسي: db:SECONDARY> db.sharks.find() { "_id" : ObjectId("5cb7702c9111a5451c6dc8bb"), "name" : "Megalodon Shark", "character" : "Ancient", "__v" : 0 } { "_id" : ObjectId("5cb77054fcdbf563f3b47365"), "name" : "Whale Shark", "character" : "Large", "__v" : 0 } يؤكد هذا الإخراج نسخ بيانات تطبيقك بين عناصر مجموعة النسخ المتماثلة. خاتمة لقد تمكّنت الآن من نشر تطبيق متكرّر (replicated) ومتاح على أعلى مستوى لمعلومات سمك القرش على عنقود Kubernetes باستخدام مخططات Helm. يمكن أن يعمل هذا التطبيق التجريبي وسير العمل الموضح في هذا البرنامج التعليمي كنقطة انطلاق أثناء إنشاء مخططات مخصصة لتطبيقك والاستفادة من مستودع Helm الثابت ومستودعات التخطيط الأخرى. أثناء سعيك نحو الإنتاج، فكر في تنفيذ ما يلي: تسجيل الدّخول والمراقبة بشكل مركزي. يرجى الاطلاع على المناقشة ذات الصلة حول تحديث تطبيقات Kubernetes للحصول على نظرة أشمل. يمكنك أيضًا الاطلاع على كيفية إعداد حزمة تسجيل دخول Elasticsearch, Fluentd and Kibana (EFK) على Kubernetes. راجع أيضًا مقدمة لشبكات الخدمة للحصول على معلومات حول كيفية تنفيذ شبكات الخدمات مثل Istio لهذه الوظيفة. موارد الولوج لتوجيه حركة المرور إلى عنقودك. يعد هذا بديلاً جيدًا لـ LoadBalancer في الحالات التي تشغّل فيها خدمات متعددة، والتي تتطلب كل منها موازِنًا LoadBalancer خاصًّا بها، أو عندما ترغب في تنفيذ استراتيجيات توجيه على مستوى التطبيق (اختبارات A/B & canary، على سبيل المثال). لمزيد من المعلومات، تحقّق من كيفية إعداد Nginx Ingress مع Cert-Manager على DigitalOcean Kubernetes والمناقشة ذات الصلة بالتوجيه في سياق شبكة الخدمة في مقدمة لشبكات الخدمة. استراتيجيات النسخ الاحتياطي لكائناتك Kubernetes. للحصول على إرشادات حول تنفيذ النسخ الاحتياطية باستخدام Velero (سابقًا Heptio Ark) مع منتج Kubernetes الخاص بـ DigitalOcean، يرجى الاطلاع على كيفية عمل نسخة احتياطية واستعادة عنقود Kubernetesعلى DigitalOcean باستخدام Heptio Ark. لمعرفة المزيد حول Helm، راجع مقدمة إلى Helm، ومدير الحزم لـ Kubernetes، وكيفية تثبيت البرامج على عناقيد Kubernetes باستخدام Helm Package Manager، وتوثيق Helm. ترجمة -وبتصرف- للمقال How To Scale a Node.js Application with MongoDB on Kubernetes Using Helm لصاحبته Kathleen Juell
  9. عند إنشائك لتطبيقات حديثة عديمة الحالة (stateless)، فإن عملية إعداد الحاويات لمكونات التطبيق ستكون هي الخطوة الأولى في النشر والتوسيع على الأنظمة الأساسية الموزعة. إذا كنت قد استخدمت Docker Compose في التطوير، فسوف تعمل على تحديث التطبيق وحاوياته بما يلي: استخراج معلومات التكوين اللازمة من شيفرتك. إلغاء تحميل حالة تطبيقك. تحزيم تطبيقك من أجل الاستخدام المتكرر. سيكون لديك أيضًا تعريفات خدمة مكتوبة تحدّد كيف ستشتغل صور حاوياتك. لتشغيل خدماتك على نظام أساسي موزّع مثل Kubernetes، ستحتاج إلى ترجمة تعريفات خدمة Compose إلى كائنات Kubernetes. سيسمح لك ذلك بتوسيع نطاق تطبيقك بنوع من المرونة. وتعدّ kompose إحدى الأدوات التي يمكن أن تسرع عملية الترجمة إلى Kubernetes، وهي أداة تحويل تساعد المطورين على نقل سير عمل Compose إلى منسّق للحاويات مثل Kubernetes أو OpenShift. سوف تعمل في هذا البرنامج التعليمي على ترجمة خدمات Compose إلى كائنات Kubernetes باستخدام kompose. ستستخدم تعريفات الكائنات التي توفرها kompose كنقطة بداية، كما ستجري تعديلات للتأكد من أن إعداداتك ستستخدم الأسرار (secrets) والخدمات (services) وطلبات وحدة التخزين الثابتة (PersistentVolumeClaims) على النحو المتوقّع في Kubernetes. عند نهاية الدرس، سيكون لديك تطبيق Node.js بنسخة واحدة لقاعدة بيانات MongoDB تعمل على نظام Kubernetes. سيعكس هذا الإعداد وظائف الشيفرة الموضحة في كيفية إعداد تطبيق node.js لسير عملٍ يعتمد على الحاويات باستخدام Docker Compose وسيكون نقطة انطلاق جيدة لإنشاء حلّ جاهز للإنتاج يتناسب مع احتياجاتك. المتطلبات الأساسية عنقود Kubernetes 1.10+‎ مع تفعيل التحكم في الوصول المستند إلى الدور role-based access control (RBAC)‎. سيستخدم هذا الإعداد عنقود DigitalOcean Kubernetes، لكن تبقى لك حرية الاختيار في إنشاء عنقود باستخدام طريقة أخرى. أداة سطر الأوامر kubectl المثبتة على جهازك المحلي أو خادم التطوير وإعدادها للاتصال بعنقودك. يمكنك قراءة المزيد حول تثبيت kubectl في التوثيق الرسمي. Docker مثبت على جهازك المحلي أو خادم التطوير. إذا كنت تعمل على نظام أوبونتو 18.04، اتبع الخطوتين 1 و 2 لكيفية تثبيت واستخدام Docker على أوبونتو 18.04؛ خلاف ذلك، اتبع التوثيق الرسمي للحصول على معلومات حول التثبيت على أنظمة التشغيل الأخرى. تأكد من إضافة مستخدمك غير الجذري إلى مجموعة Docker، كما هو موضح في الخطوة 2 من البرنامج التعليمي المرتبط. حساب Docker Hub. للحصول على نظرة عامة حول كيفية إعداده، راجع هذه المقدمة إلى Docker Hub. الخطوة الأولى: تثبيت kompose للبدء في استخدام kompose، انتقل إلى صفحة إصدارات GitHub للمشروع، وانسخ الرابط إلى الإصدار الحالي. الصق هذا الرابط في الأمر curl التالي لتنزيل أحدث إصدار من kompose: curl -L https://github.com/kubernetes/kompose/releases/download/v1.18.0/kompose-linux-amd64 -o kompose للحصول على تفاصيل حول التثبيت على أنظمة غير تابعة لنظام لينكس، يرجى الرجوع إلى إرشادات التثبيت. أنشئ الملف التنفيذي (binary): chmod +x kompose انقله إلى مسارك PATH: sudo mv ./kompose /usr/local/bin/kompose ثم تحقق من تثبيته بشكل صحيح. يمكنك إجراء فحص للإصدار: kompose version إذا كان التثبيت ناجحًا، فسيظهر لك الإخراج التالي: Output 1.18.0 (06a2e56) بعد تثبيت kompose وجاهزيته للاستخدام، يمكنك الآن استنساخ شيفرة المشروع Node.js الذي سيُترجَم إلى Kubernetes. الخطوة الثانية: استنساخ وتحزيم التطبيق لاستخدام تطبيقنا على Kubernetes، سنحتاج إلى استنساخ شيفرة المشروع وتحزيم التطبيق حتى تتمكن خدمة kubelet من سحب الصورة. ستكون خطوتنا الأولى هي استنساخ مستودع node-mongo-docker-dev من حسابDigitalOcean Community GitHub. يتضمن هذا المستودع شيفرة الإعداد الموضح في كيفية إعداد تطبيق node.js لسير عملٍ يعتمد على الحاويات باستخدام Docker Compose، والذي يستخدم تطبيق Node.js لشرح كيفية إعداد بيئة تطوير باستخدام Docker Compose. يمكنك العثور على مزيد من المعلومات حول التطبيق نفسه في سلسلة من الحاويات إلى Kubernetes باستخدام Node.js. استنسخ المستودع في مجلّد يسمى node_project: git clone https://github.com/do-community/node-mongo-docker-dev.git node_project انتقل إلى المجلّد node_project: cd node_project يحتوي مجلّد node_project على ملفات ومجلدات لتطبيق معلومات سمك القرش الذي يعمل على مدخلات المستخدم. لقد حُدِّث للعمل على الحاويات وأزيلت منه معلومات التكوين الحساسة والمحددة في شيفرة التطبيق وأُعيد تشكيلها من أجل حقنها عند تنفيذ التطبيق، كما أُلغيَ تحميل حالة التطبيق إلى قاعدة بيانات MongoDB. لمزيد من المعلومات حول تصميم التطبيقات الحديثة عديمة الحالة، يرجى الاطلاع على هيكلة التطبيقات ل Kubernetes وتحديث التطبيقات لKubernetes. يتضمن مجلّد المشروع ملفّا Dockerfile بتعليمات لبناء صورة التطبيق. دعنا نبني الصورة الآن على النحو الذي يتيح رفعها إلى حسابك Docker Hub واستخدامها في إعداداتك Kubernetes. استخدم الأمر docker build لبناء الصورة باستخدام الراية t-، والتي تتيح لك تعليمها باسم لا يُنسى. في هذه الحالة، علّم الصورة باسم مستخدمك Docker Hub وسمّها node kubernetes أو أيّ اسم من اختيارك: docker build -t your_dockerhub_username/node-kubernetes . تحدد النقطة . في الأمر أن سياق البناء هو المجلّد الحالي. سوف يستغرق الأمر دقيقة أو دقيقتين لبناء الصورة. بمجرد اكتماله، تحقق من صورك: docker images سيظهر لك الإخراج التالي: Output REPOSITORY TAG IMAGE ID CREATED SIZE your_dockerhub_username/node-kubernetes latest 9c6f897e1fbc 3 seconds ago 90MB node 10-alpine 94f3c8956482 12 days ago 71MB بعد ذلك، سجّل الدخول إلى حساب Docker Hub الذي أنشأته في المتطلبات الأساسية: docker login -u your_dockerhub_username أدخل كلمة مرور حساب Docker Hub عندما يُطلب منك ذلك. سيؤدي التسجيل بهذه الطريقة إلى إنشاء ملف ‎~/.docker/config.json في المجلّد الرئيسي للمستخدم الخاص بك باستخدام بيانات اعتماد. Docker Hub ارفع صورة التطبيق إلى Docker Hub باستخدام الأمر docker push. ولا تنس أن تعوّض yourdockerhubusername باسم مستخدمك Docker Hub: docker push your_dockerhub_username/node-kubernetes لديك الآن صورة للتطبيق يمكنك سحبها لتشغيل تطبيقك على Kubernetes. ستكون الخطوة التالية هي ترجمة تعريفات خدمة التطبيق إلى كائنات .Kubernetes الخطوة الثالثة: ترجمة الخدمات إلى كائنات Kubernetes باستخدام kompose يحدّد ملف Docker Compose، المسمى هنا docker-compose.yaml، التعريفات التي ستعمل على تشغيل خدماتنا على Compose. الخدمة في "Compose" هي عبارة عن حاوية قيد التشغيل، وتحتوي تعريفات الخدمة على معلومات حول كيفية تشغيل صورة كل حاوية. في هذه الخطوة، سوف نترجم هذه التعريفات إلى كائنات Kubernetes باستخدام kompose لإنشاء ملفات .yaml سوف تحتوي هذه الملفات على خصائص كائنات Kubernetes التي تصف الحالة التي نريدها عليها. سوف نستخدم هذه الملفات لإنشاء أنواع مختلفة من الكائنات: أوّلها الخدمات، والتي ستضمن بقاء العُلَب (pods) التي تشغّل حاوياتنا متاحةً. ثانيها عمليّات النشر، والتي سوف تحتوي على معلومات حول الحالة التي نريد عليها العلب. ثالثها PersistentVolumeClaim لتوفير تخزين لقاعدة البيانات. رابعها خريطة الإعداد ConfigMap لمتغيرات البيئة التي تُحقَن في وقت التشغيل. وآخرها سرٌّ (secret) لمستخدم قاعدة بيانات التطبيق وكلمة المرور. ستكون بعض هذه التعريفات في الملفات التي سينشئها لناkompose ، والبعض الآخر سوف نحتاج إلى إنشائه بأنفسنا. سنحتاج بدايةّ إلى تعديل بعض التعاريف في ملف docker-compose.yaml للعمل على Kubernetes. سنُضمّن إشارة إلى صورة التطبيق التي بُنيت حديثًا في تعريف خدمتنا nodejs، كما سنحذف الروابط bind mounts والحجوم والأوامر الإضافية التي استخدمناها لتشغيل حاوية التطبيق قيد التطوير باستخدام Compose. بالإضافة إلى ذلك، سنعيد تحديد سياسات إعادة تشغيل كلتا الحاويتين بحيث تتوافق مع السلوك الذي المتوقع في Kubernetes. افتح الملف باستخدام nano أو المحرر المفضل لديك: nano docker-compose.yaml يبدو التعريف الحالي لخدمة تطبيق nodejs كما يلي: ... services: nodejs: build: context: . dockerfile: Dockerfile image: nodejs container_name: nodejs restart: unless-stopped env_file: .env environment: - MONGO_USERNAME=$MONGO_USERNAME - MONGO_PASSWORD=$MONGO_PASSWORD - MONGO_HOSTNAME=db - MONGO_PORT=$MONGO_PORT - MONGO_DB=$MONGO_DB ports: - "80:8080" volumes: - .:/home/node/app - node_modules:/home/node/app/node_modules networks: - app-network command: ./wait-for.sh db:27017 -- /home/node/app/node_modules/.bin/nodemon app.js ... بعدها أَجرِ التعديلات التالية على تعريف الخدمة: استخدم صورة node-kubernetes بدلاً من ملف Dockerfile المحلي. عدّل سياسة إعادة تشغيل الحاوية restart من unless-stopped إلى always. احذف قائمة الحجوم volumes وتعليمات الأوامر command. سيبدو تعريف الخدمة النهائية الآن كما يلي: ... services: nodejs: image: your_dockerhub_username/node-kubernetes container_name: nodejs restart: always env_file: .env environment: - MONGO_USERNAME=$MONGO_USERNAME - MONGO_PASSWORD=$MONGO_PASSWORD - MONGO_HOSTNAME=db - MONGO_PORT=$MONGO_PORT - MONGO_DB=$MONGO_DB ports: - "80:8080" networks: - app-network … بعد ذلك، انزل إلى تعريف الخدمة db. ثم أجر التعديلات التالية: غيّر سياسة إعادة التشغيل restart للخدمة always. احذف ملف env. فبدلاً من استخدام قيمٍ من ملف env.، سنمرّر القيم الخاصة بـ MONGO_INITDB_ROOT_USERNAME و MONGO_INITDB_ROOT_PASSWORD إلى حاوية قاعدة البيانات باستخدام السّر Secret الذي سننشئه في الخطوة الرابعة. سيبدو تعريف خدمة db الآن كما يلي: ... db: image: mongo:4.1.8-xenial container_name: db restart: always environment: - MONGO_INITDB_ROOT_USERNAME=$MONGO_USERNAME - MONGO_INITDB_ROOT_PASSWORD=$MONGO_PASSWORD volumes: - dbdata:/data/db networks: - app-network ... أخيرًا، في الجزء السفلي من الملف، احذف الحجوم node_modules من مفتاح المستوى الأعلى volumes. وسيبدو المفتاح عندها كما يلي: ... volumes: dbdata: احفظ الملف وأغلقه عند الانتهاء من التحرير. سنحتاج، قبل ترجمة تعريفاتنا للخدمة، إلى كتابة ملف env. الذي سيستخدمه kompose لإنشاء خريطة الإعداد ConfigMap بمعلوماتنا غير الحساسة. الرجاء مراجعة الخطوة الثانية من كيفية إعداد تطبيق node.js لسير عملٍ يعتمد على الحاويات باستخدام Docker Compose للمزيد من التوضيح حول هذا الملف. في ذلك الدرس، أضفنا env. إلى ملفنا gitignore. للتأكد من عدم نسخه إلى التحكم في الإصدار. هذا يعني أنه لم يُنسَخ عندما استنسخنا مستودع node-mongo-docker-dev في الخطوة الثانية من هذا الدرس. لذلك سوف نحتاج إلى إعادة إنشائه الآن. أنشئ هذا الملف إذًا: nano .env سوف يستخدم kompose هذا الملف لإنشاء خريطة إعداد ConfigMap لتطبيقنا. ومع ذلك، بدلاً من تعيين كافة المتغيرات من تعريف خدمة nodejs في ملفنا Compose، سنضيف فقط اسم قاعدة البيانات MONGO_DB و MONGO_PORT. سنعيّن اسم المستخدم وكلمة المرور لقاعدة البيانات بشكل منفصل عندما ننشئ يدويًا كائنًا Secret في الخطوة الرابعة. أضف معلومات المنفذ واسم قاعدة البيانات التالية إلى ملف env.. لا تتردد في إعادة تسمية قاعدة بياناتك إذا كنت ترغب في ذلك: MONGO_PORT=27017 MONGO_DB=sharkinfo احفظ الملف وأغلقه عند الانتهاء من التحرير. أنت الآن مستعدّ لإنشاء الملفات اعتمادًا على خصائص كائنك. ويقدم لك kompose خيارات متعددة لترجمة مواردك. إذ تستطيع: إنشاء ملفات yaml بناءً على تعريفات الخدمة في ملفك docker-compose.yaml باستخدام kompose convert. إنشاء كائنات Kubernetes مباشرة باستخدام kompose up. إنشاء مخطط Helm باستخدام kompose convert -c. في الوقت الحالي، سنحوّل تعريفاتنا للخدمة إلى ملفات yaml ثم نضيف الملفات التي ينشئها kompose وننقّحها. استخدام الأمر التالي لتحويل تعريفات الخدمة إلى ملفات yaml: kompose convert يمكنك أيضًا تسمية ملفات تكوين محددة أو متعددة باستخدام الراية f-. بعد تنفيذ هذا الأمر، سيُخرِج kompose معلوماتٍ حول الملفات التي أنشأتها: INFO Kubernetes file "nodejs-service.yaml" created INFO Kubernetes file "db-deployment.yaml" created INFO Kubernetes file "dbdata-persistentvolumeclaim.yaml" created INFO Kubernetes file "nodejs-deployment.yaml" created INFO Kubernetes file "nodejs-env-configmap.yaml" created يتضمّن هذا الإخراج الملفات yaml مع خصائص خدمة ونشر وخريطة إعداد تطبيق Node، وكذلك طلب وحدة التخزين الثابتة ل dbdata ونشر قاعدة بيانات MongoDB. تعدّ هذه الملفات نقطة انطلاق جيدة، ولكن لكي تتطابق وظائف تطبيقنا مع الإعداد الموضح في كيفية إعداد تطبيق node.js لسير عملٍ يعتمد على الحاويات باستخدام Docker Compose، سنحتاج إلى إجراء بعض الإضافات والتغييرات على الملفات التي أنشأها kompose. الخطوة الرابعة: إنشاء أسرار Kubernetes لكي يعمل تطبيقنا بالطريقة التي نتوقعها، سنحتاج إلى إجراء بعض التعديلات على الملفات التي أنشأها kompose. أول هذه التغييرات هو إنشاء سرٍّ لمستخدم قاعدة البيانات وكلمة المرور وإضافتها إلى عمليات نشر التطبيق وقاعدة البيانات. ويقدّم Kubernetes طريقتين للعمل بمتغيرات البيئة: ConfigMap وSecret. وقد أنشأ kompose بالفعل خريطة إعداد ConfigMap بالمعلومات غير السرية التي ضمّنناها في ملفنا env.، لذلك سننشئ الآن سرًّا بمعلوماتنا السرية: اسم المستخدم وكلمة المرور لقاعدة البيانات. ستكون الخطوة الأولى في إنشاء السّرِّ يدويًا هي تحويل اسم المستخدم وكلمة المرور إلى base64، وهو نظام ترميز يسمح لك بنقل البيانات بشكل موحد، بما في ذلك البيانات الثنائية. حوّل أولا اسم مستخدم قاعدة بياناتك: echo -n 'your_database_username' | base64 دوّن القيمة التي تظهر لك في الإخراج. بعد ذلك، حوّل كلمة مرورك: echo -n 'your_database_password' | base64 دوّن القيمة الظاهرة في الإخراج هنا أيضا. افتح ملف السّر: nano secret.yaml ملاحظة: تُحدّد كائنات Kubernetes عادة باستخدام YAML الذي يمنع بصرامةٍ علامات التبويب ويتطلب مسافتين للمسافة البادئة. إذا كنت ترغب في التحقق من تنسيق أيٍّ من ملفاتك yaml ، فيمكنك استخدام linter أو اختبار صياغة (syntax) تركيبك باستخدام kubectl create مع الرايتين dry-run-- و validate--: kubectl create -f your_yaml_file.yaml --dry-run --validate=true يُستحسن بشكل عام التحقق من صحة الصياغة قبل إنشاء الموارد باستخدام kubectl. أضف الشيفرة التالية إلى الملف لإنشاء سرٍّ يحدّد MONGO_USERNAME و MONGO_PASSWORD باستخدام القيم المشفرة التي أنشأتها للتو. تأكد من تعويض القيم الوهمية هنا باسم المستخدم وكلمة المرور المشفرة: الملف ‎~/node_project/secret.yaml: apiVersion: v1 kind: Secret metadata: name: mongo-secret data: MONGO_USERNAME: your_encoded_username MONGO_PASSWORD: your_encoded_password لقد سمّينا كائن السرّ mongo-secret، ولكن يمكنك تسميته كما تشاء. احفظ هذا الملف وأغلقه عند الانتهاء من التحرير. وكما فعلت مع ملفك env. ، تأكد من إضافة secret.yaml إلى ملفك gitignore. لإبقائه خارج نطاق التحكم في الإصدار. بعد تحرير secret.yaml، ستكون خطوتنا التالية هي ضمان استخدام عُلَب كلّ من التطبيق وقاعدة البيانات للقيم التي أضفناها إلى الملف. لنبدأ بإضافة إشارات مرجعية إلى السّرّ Secret في نشر التطبيق. افتح الملف المسمى nodejs-publish.yaml: nano nodejs-deployment.yaml تتضمن مواصفات حاوية الملف متغيرات البيئة التالية المحددة تحت مفتاح env: apiVersion: extensions/v1beta1 kind: Deployment ... spec: containers: - env: - name: MONGO_DB valueFrom: configMapKeyRef: key: MONGO_DB name: nodejs-env - name: MONGO_HOSTNAME value: db - name: MONGO_PASSWORD - name: MONGO_PORT valueFrom: configMapKeyRef: key: MONGO_PORT name: nodejs-env - name: MONGO_USERNAME سنحتاج إلى إضافة إشارات مرجعية إلى السرّ في متغيرات MONGO_USERNAME و MONGO_PASSWORD المدرجة هنا، حتى يتمكن تطبيقنا من الوصول إلى تلك القيم. وبدلاً من تضمين مفتاح configMapKeyRef للإشارة إلى خريطة الإعداد لnodejs-env ، كما هو الحال مع قيم MONGO_DB و MONGO_PORT،، سنُضمِّن مفتاح secretKeyRef للإشارة إلى القيم الموجودة في السرّ secret. أضف إشارات السرّ المرجعية التالية إلى المتغيرين MONGO_USERNAME و MONGO_PASSWORD: apiVersion: extensions/v1beta1 kind: Deployment ... spec: containers: - env: - name: MONGO_DB valueFrom: configMapKeyRef: key: MONGO_DB name: nodejs-env - name: MONGO_HOSTNAME value: db - name: MONGO_PASSWORD valueFrom: secretKeyRef: name: mongo-secret key: MONGO_PASSWORD - name: MONGO_PORT valueFrom: configMapKeyRef: key: MONGO_PORT name: nodejs-env - name: MONGO_USERNAME valueFrom: secretKeyRef: name: mongo-secret key: MONGO_USERNAME احفظ الملف وأغلقه عند الانتهاء من التحرير. بعد ذلك ، سنضيف القيم نفسها إلى ملف db-publish.yaml. افتح الملف للتحرير: nano db-deployment.yaml سنضيف في هذا الملف إشارات مرجعية إلى السرّ في مفاتيح المتغيرات التالية: MONGO_INITDB_ROOT_USERNAME و MONGO_INITDB_ROOT_PASSWORD. تجعل صورة mongo هذه المتغيرات متاحة لتتمكّن من تعديل تهيئة نسخة قاعدة بياناتك. ينشئ MONGO_INITDB_ROOT_USERNAME وMONGO_INITDB_ROOT_PASSWORD معًا مستخدمًا جذرًا في قاعدة بيانات المشرف admin مع التأكد من تفعيل التصديق على الهوية عند بدء تشغيل حاوية قاعدة البيانات. يضمن استخدام القيم التي حدّدناها في السرّ أن يكون لدينا مستخدم للتطبيق يتمتع بأذونات الجذر root في نسخة قاعدة البيانات، مع إمكانية الوصول إلى جميع الأذونات الإدارية والتشغيلية لهذا الدور. عند العمل في الإنتاج، سترغب في إنشاء مستخدم تطبيق مخصّص ذي صلاحيات محددة النطاق. أضف أسفل متغيرات MONGO_INITDB_ROOT_USERNAME وMONGO_INITDB_ROOT_PASSWORD إشارات مرجعية إلى قيم السرّ Secret: apiVersion: extensions/v1beta1 kind: Deployment ... spec: containers: - env: - name: MONGO_INITDB_ROOT_PASSWORD valueFrom: secretKeyRef: name: mongo-secret key: MONGO_PASSWORD - name: MONGO_INITDB_ROOT_USERNAME valueFrom: secretKeyRef: name: mongo-secret key: MONGO_USERNAME image: mongo:4.1.8-xenial … احفظ الملف وأغلقه عند الانتهاء من التحرير. بعد إعداد السر الخاص بك، يمكنك الانتقال إلى إنشاء خدمة قاعدة بياناتك والتأكد من أن حاوية التطبيق تحاول فقط الاتصال بقاعدة البيانات بمجرد إعدادها وتهيئتها بالكامل. الخطوة الخامسة: إنشاء خدمة قاعدة البيانات وحاوية التطبيق الأولية الآن بعد أن أعددنا السرّ، يمكننا الانتقال إلى إنشاء خدمة قاعدة البيانات وحاوية أولية مهمّتها استطلاع هذه الخدمة للتأكد من أن تطبيقنا يحاول فقط الاتصال بقاعدة البيانات بمجرد اكتمال إجراءات بدء تشغيل قاعدة البيانات، بما في ذلك إنشاء مستخدم وكلمة مرور MONGO_INITDB. للمزيد حول كيفية تنفيذ هذه الوظيفة في Compose، يرجى الاطلاع على الخطوة الرابعة من إعداد تطبيق node.js لسير عملٍ يعتمد على الحاويات باستخدام Docker Compose. افتح ملفًا لتحديد مواصفات خدمة قاعدة البيانات: nano db-service.yaml أضف الشيفرة التالية إلى الملف لتعريف الخدمة: الملف ‎~/node_project/db-service.yaml: apiVersion: v1 kind: Service metadata: annotations: kompose.cmd: kompose convert kompose.version: 1.18.0 (06a2e56) creationTimestamp: null labels: io.kompose.service: db name: db spec: ports: - port: 27017 targetPort: 27017 selector: io.kompose.service: db status: loadBalancer: {} سيُطابِق المحدد selector الذي أدرجناه هنا كائن الخدمة هذا مع علب (Pods) قاعدة بياناتنا، والتي عُرِّفت بالتسمية io.kompose.service: db بواسطة kompose في ملف .db-publish.yaml لقد سمّينا أيضًا هذه الخدمة db. احفظ الملف وأغلقه عند الانتهاء من التحرير. بعد ذلك، دعنا نضيف حقلًا لحاوية أولية إلى المصفوفة containers في nodejs-deployment.yaml سيؤدي هذا إلى إنشاء حاوية أوّلية يمكننا استخدامها لتأخير حاوية تطبيقنا عن الاشتغال حتى إنشاء خدمة db ذات علبة يمكن الوصول إليها. وهذا هو أحد الاستخدامات المحتملة للحاويات الأولية. لمعرفة المزيد عن حالات الاستخدام الأخرى، يرجى الاطلاع على التوثيق الرسمي. افتح الآن الملف nodejs-publish.yaml: nano nodejs-deployment.yaml داخل العلبة Pod وبجانب المصفوفة containers، سنضيف حقلًا initContainers بحاوية مهمتها استقصاء الخدمة db. أضف الشيفرة التالية أسفل الحقلين ports وresources وفوق restartPolicy في مصفوفة nodejs containers: الملف ‎~/node_project/nodejs-deployment.yaml: apiVersion: extensions/v1beta1 kind: Deployment ... spec: containers: ... name: nodejs ports: - containerPort: 8080 resources: {} initContainers: - name: init-db image: busybox command: ['sh', '-c', 'until nc -z db:27017; do echo waiting for db; sleep 2; done;'] restartPolicy: Always ... تستخدم هذه الحاوية الأولية صورة BusyBox، وهي صورة خفيفة الوزن تتضمن العديد من أدوات UNIX. في هذه الحالة، سنستخدم الأداة netcat لاستقصاء هل تقبل العلبة المرتبطة بالخدمة db الاتصالات TCP على المنفذ 27017. يكرّر أمر الحاوية command هذا وظيفة السكربت wait-for الذي حذفناه من الملف docker-compose.yaml في الخطوة الثالثة. لمزيد من التوضيح حول كيفية وعلّة استخدام تطبيقنا للسكربت wait-for ، يرجى الاطلاع على الخطوة الرابعة من كيفية إعداد تطبيق node.js لسير عملٍ يعتمد على الحاويات باستخدام Docker Compose. تشتغل الحاويات الأولية وفق نظام run to completion. هذا يعني في حالتنا هذه أن حاوية التطبيق Node لن تبدأ في الاشتغال حتى تبدأ حاوية قاعدة البيانات في الاشتغال وقبول الاتصالات على المنفذ 27017. يسمح لنا تعريف الخدمة db بضمان هذه الوظيفة بغض النظر عن الموقع المحدّد لحاوية قاعدة البيانات، والذي يمكنه أن يتغيّر. احفظ الملف وأغلقه عند الانتهاء من التحرير. بعد إنشائك لخدمة قاعدة البيانات ووضعك لحاوياتك الأولية للتحكم في ترتيب اشتغال الحاويات، يمكنك الانتقال إلى التحقق من متطلبات التخزين في PersistentVolumeClaim وعرض خدمة تطبيقك باستخدام LoadBalancer. الخطوة السّادسة: تعديل PersistentVolumeClaim وعرض الواجهة الأمامية للتطبيق قبل تشغيل التطبيق، سنجري تغييرين أخيرين لضمان توفير تخزين قاعدة البيانات بشكل صحيح، وأنّنا نستطيع عرض الواجهة الأمامية لتطبيقنا باستخدام LoadBalancer. دعنا نعدل أولاً مورد التخزين storage resource المحدد في PersistentVolumeClaim الذي أنشأناه. تتيح لنا هذه المطالبة (claim) توفير مساحة تخزين ديناميكية لإدارة حالة التطبيق. للعمل ب PersistentVolumeClaims، يجب أن يكون لديك صنف تخزين StorageClass أنشأته وأعددته لتوفير موارد التخزين. في حالتنا هذه، ونظرًا لأننا نعمل على DigitalOcean Kubernetes، يعيّن صنف التخزين provisioner الافتراضي على dobs.csi.digitalocean.com. يمكننا التحقق من ذلك عبر كتابة ما يلي: kubectl get storageclass إذا كنت تعمل مع مجموعة DigitalOcean، فسترى الإخراج التالي: NAME PROVISIONER AGE do-block-storage (default) dobs.csi.digitalocean.com 76m إذا كنت لا تعمل على عنقود DigitalOcean، فستحتاج إلى إنشاء StorageClass وإعداد مزوّد من اختيارك. للحصول على تفاصيل حول كيفية القيام بذلك، يرجى الاطلاع على التوثيق الرسمي. عندما ينشئ kompose الملف dbdata-persistentvolumeclaim.yaml، فإنه يعيّن مورد التخزين storage resource على سعةٍ لا تلبي الحدّ المطلوب في مزوّد الخدمة لدينا. لذلك، سنحتاج إلى تعديل PersistentVolumeClaim الخاص بنا لاستخدام السعة الدنيا القابلة للتطبيق لوحدة تخزين DigitalOcean Block المحدّدة في 1 جيجابايت. ويمكنك تعديل هذه القيمة لتلبية متطلبات التخزين الخاصة بك. افتح الملفّ dbdata-persistentvolumeclaim.yaml: nano dbdata-persistentvolumeclaim.yaml عوّض قيمة التخزين storage بـ 1Gi: الملف ‎~/node_project/dbdata-persistentvolumeclaim.yaml: apiVersion: v1 kind: PersistentVolumeClaim metadata: creationTimestamp: null labels: io.kompose.service: dbdata name: dbdata spec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi status: {} لاحظ أيضًا أنّ accessMode: ReadWriteOnce يعني أن الحجم المتوفّر كنتيجة لهذه الطلب سيكون للقراءة والكتابة من خلال عقدة واحدة فقط. يرجى الاطلاع على التوثيق لمزيد من المعلومات حول أوضاع الوصول المختلفة. احفظ الملف وأغلقه عند الانتهاء. بعد ذلك، افتح nodejs-service.yaml: nano nodejs-service.yaml سنعمل على الكشف عن هذه الخدمة خارجيًا باستخدام ‎.DigitalOcean Load Balancer إذا كنت لا تستخدم عنقود DigitalOcean، فيرجى الرجوع إلى التوثيق ذا الصلة من مزوّد الخدمة السحابية للحصول على معلومات حول موازِنات الحمل الخاصة به. بدلاً من ذلك، يمكنك اتباع توثيق Kubernetes الرسمية الخاصة بإعداد عنقود عالي التوفّر باستخدام kubeadm، لكن في هذه الحالة لن تتمكن من استخدام PersistentVolumeClaims لتوفير التخزين. حدّد ضمن مواصفات الخدمة، LoadBalancer في الحقل type الخاصّ بالخدمة: الملف ‎~/node_project/nodejs-service.yaml: apiVersion: v1 kind: Service ... spec: type: LoadBalancer ports: ... عندما ننشئ خدمة nodejs، سيُنشَأ مُوازِن للتحميل تلقائيًا، مما يوفر لنا عنوانًا خارجيًا IP يمكِّننا من الوصول إلى تطبيقنا. احفظ الملف وأغلقه عند الانتهاء من التحرير. بوجود جميع ملفاتنا في مكانها الصحيح، نكون على استعداد لبدء واختبار التطبيق. الخطوة السابعة: بدء التطبيق والوصول إليه لقد حان الوقت لإنشاء كائنات Kubernetes واختبار عمل التطبيق على النحو المتوقع. لإنشاء الكائنات التي حددناها، سنستخدم kubectl create مع الراية f-، والتي سوف تتيح لنا تحديد الملفات التي أنشأها لنا Compose، بالإضافة إلى الملفات التي حرّرناها. نفّذ الأمر التالي لإنشاء تطبيق Node وخدمات قاعدة بيانات MongoDB وعمليات النشر، مع Secret وConfigMap وPersistentVolumeClaim: kubectl create -f nodejs-service.yaml,nodejs-deployment.yaml,nodejs-env-configmap.yaml,db-service.yaml,db-deployment.yaml,dbdata-persistentvolumeclaim.yaml,secret.yaml سيظهر لك الإخراج التالي مشيرًا إلى إنشاء الكائنات: Output service/nodejs created deployment.extensions/nodejs created configmap/nodejs-env created service/db created deployment.extensions/db created persistentvolumeclaim/dbdata created secret/mongo-secret created للتحقق من اشتغال العلب Pods، اكتب ما يلي: kubectl get pods لا تحتاج إلى تحديد فضاء اسم (namespace) هنا، لأننا أنشأنا كائناتنا في مساحة اسمية الافتراضية. إذا كنت تعمل على مساحات اسمية متعددة، فتأكد من تضمين الراية n- عند تنفيذ هذا الأمر، بالإضافة إلى اسم مساحتك الاسمية. سيظهر لك الإخراج التالي أثناء بدء تشغيل الحاوية db وحاوية تطبيقك الأولية: Output NAME READY STATUS RESTARTS AGE db-679d658576-kfpsl 0/1 ContainerCreating 0 10s nodejs-6b9585dc8b-pnsws 0/1 Init:0/1 0 10s بمجرد تشغيل تلك الحاوية وبدء تشغيل حاويات التطبيق وقاعدة البيانات، سيظهر لك هذا الإخراج: Output NAME READY STATUS RESTARTS AGE db-679d658576-kfpsl 1/1 Running 0 54s nodejs-6b9585dc8b-pnsws 1/1 Running 0 54s يشير Running STATUS إلى أن العلب الخاصة بك مرتبطة بالعُقَد وأن الحاويات المرتبطة بتلك العلب تعمل. ويشير READY إلى عدد الحاويات الموجودة في العلبة. لمزيد من المعلومات، يرجى الرجوع إلى توثيق دورة حياة العلبة. ملحوظة: إذا رأيت مراحل غير متوقعة في العمود STATUS، تذكر أنه يمكنك استكشاف الأخطاء وإصلاحها باستخدام الأوامر التالية: kubectl describe pods your_pod kubectl logs your_pod بعد تشغيل حاوياتك، يمكنك الآن الوصول إلى التطبيق. للحصول على العنوان IP الخاص بـ LoadBalancer، اكتب: kubectl get svc سيظهر لك الإخراج التالي: NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE db ClusterIP 10.245.189.250 <none> 27017/TCP 93s kubernetes ClusterIP 10.245.0.1 <none> 443/TCP 25m12s nodejs LoadBalancer 10.245.15.56 your_lb_ip 80:30729/TCP 93s العنوان EXTERNAL_IP المرتبط بخدمة nodejs هو عنوان IP يمكّنك من الوصول إلى التطبيق. إذا رأيت حالة <pending> في عمود EXTERNAL_IP،، فهذا يعني أن مُوازِن التحميل لا يزال قيد الإنشاء. بمجرد رؤية عنوان IP في هذا العمود، انتقل إليه في متصفحك: http://your_lb_ip. ينبغي أن تظهر لك صفحة الهبوط التالية: انقر على زر الحصول على معلومات القرش. ستظهر صفحةّ فيها نموذج إدخال حيث يمكنك إدخال اسم سمك القرش ووصف لسلوكه العام: أضف في النموذج سمكة قرش من اختيارك. للتوضيح، سنضيف Megalodon Shark إلى حقل Shark Name، وAncient لحقل Shark Character: انقر على زر الإرسال. ستظهر صفحة بها معلومات القرش معروضة لك: لديك الآن إعداد نسخة واحدة لتطبيق Node.js بقاعدة بيانات MongoDB تعمل على عنقود Kubernetes. خاتمة تعدّ الملفات التي أنشأتها في هذا الدرس نقطة انطلاق جيدة للبناء عليها أثناء تقدمك نحو الإنتاج. ويمكنك العمل أثناء تطوير تطبيقك على تنفيذ ما يلي: تسجيل الدّخول والمراقبة بشكل مركزي. يرجى الاطلاع على المناقشة ذات الصلة على موقع ديجيتال أوشن حول تحديث تطبيقات Kubernetes للحصول على نظرة أشمل. يمكنك أيضًا الاطلاع على كيفية إعداد حزمة تسجيل دخول Elasticsearch، و Fluentd و Kibana ‏(EFK) على Kubernetes على موقع ديجيتال أوشن. راجع أيضًا مقال مقدمة لشبكات الخدمة للحصول على معلومات حول كيفية تنفيذ شبكات الخدمات مثل Istio لهذه الوظيفة. موارد الولوج لتوجيه حركة المرور إلى عنقودك. يعد هذا بديلاً جيدًا لموزع الحمل LoadBalancer في الحالات التي تشغّل فيها خدمات متعددة، والتي تتطلب كل منها موازِنًا LoadBalancer خاصًّا بها، أو عندما ترغب في تنفيذ استراتيجيات توجيه على مستوى التطبيق (اختبارات A/B & canary، على سبيل المثال). لمزيد من المعلومات، تحقّق من مقال كيفية إعداد Nginx Ingress مع Cert-Manager على DigitalOcean Kubernetes والمناقشة ذات الصلة على ديجيتال أوشن بالتوجيه في سياق شبكة الخدمة في مقدمة لشبكات الخدمة. استراتيجيات النسخ الاحتياطي لكائناتك Kubernetes. للحصول على إرشادات حول تنفيذ النسخ الاحتياطية باستخدام Velero (سابقًا Heptio Ark) مع منتج Kubernetes الخاص بـ DigitalOcean، يرجى الاطلاع على مقال كيفية عمل نسخة احتياطية واستعادة عنقود Kubernetes على DigitalOcean باستخدام Heptio Ark. ترجمة -وبتصرف- للمقال How To Migrate a Docker Compose Workflow to Kubernetes لصاحبته Kathleen Juell
  10. إذا كنت تعمل على تطوير تطبيقك بنشاط، فإن استخدام Docker يمكنه تبسيط سير العمل وجعل عملية نشر التطبيق على الإنتاج سلسةً، إذ يمنح العمل بالحاويات في عمليّة التطوير العديد من المزايا، نذكر منها أنّ: البيئات متناسقة (consistent)، مما يعني أنه يمكنك اختيار اللغات والاعتماديات التي تريدها لمشروعك دون أن تقلق بشأن تعارضات النظام. البيئات معزولة (isolated)، ممّا يسهّل اكتشاف المشكلات وإدماج الأعضاء الجدد في الفريق. البيئات محمولة (portable)، ممّا يسمح لك بتحزيم ومشاركة شيفرتك مع الآخرين. يشرح لك هذا الدرس كيفية إعداد بيئة تطوير لتطبيق Node.js بالاعتماد على Docker. ستنشئ حاويتين، واحدة من أجل تطبيق Node والأخرى لقاعدة البيانات MongoDB، وذلك باستخدام Docker Compose. ونظرًا لأن هذا التطبيق يعمل على Node وMongoDB، فسيقوم برنامج الإعداد بما يلي: *مزامنة شيفرة التطبيق التي على المضيف مع الشيفرة التي في الحاوية لتسهيل التعديلات أثناء التطوير. التحقّق من أن التعديلات في شيفرة التطبيق تعمل دون الحاجة إلى إعادة تشغيل. إنشاء قاعدة بيانات محمية بمستخدم وكلمة مرور من أجل بيانات التطبيق. جعل هذه البيانات ثابتة ومستقرة (persistent). في نهاية هذا الدرس، سيكون لديك تطبيق لمعلومات سمك القرش يعمل بشكل جيّد على حاويات Docker: المتطلبات الأساسية لمتابعة هذا الدرس، ستحتاج إلى العناصر التالية: خادم تطوير يشتغل على نظام أوبونتو 18.04، إضافة إلى مستخدم غير جذري يمتلك صلاحيات sudo وجدار حماية نشط. للحصول على إرشادات حول كيفية إعدادها، يرجى الاطلاع على دليل إعداد الخادم الأولي. تثبيت Docker على خادمك، باتباع الخطوتين الأولى و الثانية لكيفية تثبيت واستخدام Docker على أوبونتو 18.04. تثبيت Compose Docker على خادمك، باتباع الخطوة الأولى من كيفية تثبيت Docker Compose على أوبونتو 18.04. الخطوة الأولى: استنساخ المشروع وتعديل الاعتماديات تتمثل الخطوة الأولى في إنشاء هذا الإعداد في استنساخ شيفرة المشروع وتعديل ملف package.json الخاص به، والذي يتضمن اعتماديات المشروع. سوف نضيف nodemon إلى اعتماديات المشروع devDependencies، مع تحديد أننا سنستخدمها أثناء التطوير. يضمن تشغيل التطبيق باستخدام nodemon أنه سيُعاد تشغيله تلقائيًا عند إجراء تغييرات على الشيفرة. انسخ، في البداية، مستودع nodejs-mongo-mongoose من حساب DigitalOcean Community GitHub. يتضمن هذا المستودع الشيفرة من الإعداد الموضح في كيفية دمج MongoDB مع تطبيقك Node، والذي يشرح كيفية دمج قاعدة بيانات MongoDB مع تطبيق Node موجودٍ سلفًا باستخدام Mongoose. انسخ المستودع في مجلّد يسمى node_project: git clone https://github.com/do-community/nodejs-mongo-mongoose.git node_project انتقل إلى المجلّد node_project: cd node_project افتح ملف package.json الخاص بالمشروع باستخدام nano أو المحرّر الذي تفضّله: nano package.json أسفل اعتماديات المشروع وفوق قوس الإغلاق، أنشئ كائن devDependencies جديد يتضمن nodemon: ... "dependencies": { "ejs": "^2.6.1", "express": "^4.16.4", "mongoose": "^5.4.10" }, "devDependencies": { "nodemon": "^1.18.10" } } احفظ الملف وأغلقه عند الانتهاء من التحرير. بوضعك لشيفرة المشروع في مكانها وتعديل اعتمادياته، يمكنك الانتقال إلى إعادة بناء الشيفرة من أجل سير عملٍ يعتمد على الحاويات. الخطوة الثانية: إعداد تطبيقك للعمل بالحاويات يعني تعديل التطبيق من أجل سير عملٍ يعتمد على الحاويات، جعل الشيفرة أكثر نمطيةً. إذ توفر الحاويات قابلية التنقل بين البيئات، ويجب أن تعكس الشيفرة ذلك من خلال البقاء قدر الإمكان منفصلةً عن نظام التشغيل الأساسي. لتحقيق ذلك، سنعيد تشكيل شيفرتنا للاستفادة بشكل أكبر من خاصية process.env الخاصة ب Node، والتي تعيد كائنًا يحتوي على معلومات حول بيئة المستخدم في وقت التشغيل. يمكننا استخدام هذا الكائن في شيفرتنا لتعيين معلومات التكوين ديناميكيًا في وقت التشغيل باستخدام متغيرات البيئة. لنبدأ بالملف app.js، الذي يمثّل نقطة دخول التطبيق الرئيسية لدينا. افتح هذا الملف: nano app.js سترى فيه تعريفًا للثابتة port، بالإضافة إلى الدالّة listen التي تستخدم هذه الثابتة لتحديد المنفذ الذي سيستمع إليه التطبيق: ... const port = 8080; ... app.listen(port, function () { console.log('Example app listening on port 8080!'); }); دعنا نعيد تعريف ثابتة port للسماح بالتعيين الديناميكي في وقت التشغيل باستخدام الكائن process.env. لذا، أجرِ التعديلات التالية على تعريف الثابتة وعلى الدالّة listen: ... const port = process.env.PORT || 8080; ... app.listen(port, function () { console.log(`Example app listening on ${port}!`); }); يعيّن تعريفنا الثابت الجديد المنفذ port ديناميكيًا باستخدام القيمة الممرّرة في وقت التشغيل أو القيمة 8080. وبالمثل، أعدنا كتابة الدّالة لاستخدام قالب نصّي حديث، والذي سيُقحم قيمة المنفذ عند الاستماع للاتصالات. ونظرًا لأننا سنعيّن منافذنا في أماكن أخرى، فستغنينا هذه المراجعات عن الاضطرار إلى مراجعة هذا الملف بشكل مستمر عند تغيير بيئتنا. احفظ الملف وأغلقه عند الانتهاء من التحرير. بعد ذلك، سنعدّل معلومات اتصال قاعدة البيانات لحذف أي بيانات اعتماد في التكوينات. افتح الملف db.js الذي يحتوي على هذه المعلومات: nano db.js يقوم الملف حاليا بالأمور التالية: يستورد Mongoose، كائن تخطيط المستندات (ODM) الذي نستخدمه لإنشاء مخطّطات ونماذج لبيانات التطبيق. يعيّن بيانات اعتماد قاعدة البيانات كقيم ثابتة، بما في ذلك اسم المستخدم وكلمة المرور. يتصل بقاعدة البيانات باستخدام التابع mongoose.connect. لمزيد من المعلومات حول الملفّ، يرجى الاطلاع على الخطوة الثالثة من كيفية دمج MongoDB مع تطبيق Node الخاص بك. ستكون خطوتنا الأولى في تعديل الملف هي إعادة تحديد الثوابت التي تتضمن معلومات حساسة. تبدو هذه الثوابت حاليًا على النحو التالي: ... const MONGO_USERNAME = 'sammy'; const MONGO_PASSWORD = 'your_password'; const MONGO_HOSTNAME = '127.0.0.1'; const MONGO_PORT = '27017'; const MONGO_DB = 'sharkinfo'; ... يمكنك استخدام الكائن process.env لالتقاط قيم وقت التشغيل لهذه الثوابت بدلاً من الاضطرار لكتابة هذه المعلومات يدويًا. لذا عدّل هذا المقطع من الشيفرة لتبدو على هذا النحو: ... const { MONGO_USERNAME, MONGO_PASSWORD, MONGO_HOSTNAME, MONGO_PORT, MONGO_DB } = process.env; ... احفظ الملف وأغلقه عند الانتهاء من التحرير. في هذه المرحلة، عدّلت الملف db.js للعمل بمتغيرات البيئة في تطبيقك، ولكنك مازلت تحتاج إلى وسيلة لتمرير هذه المتغيرات إلى تطبيقك. لننشئ ملف env. بقيمٍ يمكنك تمريرها إلى التطبيق في وقت التشغيل. افتح هذا الملف: nano .env سيتضمن هذا الملف المعلومات التي حذفتها من db.js وهي اسم المستخدم وكلمة المرور لقاعدة بيانات التطبيق، بالإضافة إلى إعداد المنفذ واسم قاعدة البيانات. لا تنسَ تحديث اسم المستخدم وكلمة المرور واسم قاعدة البيانات المدرجة هنا بمعلوماتك الخاصة: MONGO_USERNAME=sammy MONGO_PASSWORD=your_password MONGO_PORT=27017 MONGO_DB=sharkinfo لاحظ أننا حذفنا إعدادات المضيف التي كانت في الأصل في db.js. والآن، سنعرّف المضيف على مستوى الملف Docker Compose، إلى جانب معلومات أخرى حول الخدمات والحاويات. احفظ هذا الملف وأغلقه عندما تنتهي من التحرير. نظرًا لاحتواء الملف env. على معلومات حساسة، فستحتاج إلى الحرص على تضمينه في ملفات dockerignore. و gitignore. الخاصة بمشروعك حتى لا يُنسخ إلى وحدة إدارة الإصدار أو إلى حاوياتك. افتح ملفك dockerignore.: nano .dockerignore أضف السطر التالي إلى أسفل الملف: ... .gitignore .env احفظ الملف وأغلقه عند الانتهاء من التحرير. يحتوي الملف gitignore. الموجود في هذا المستودع على env. أصلًا، ولكن لا تتردد في التحقق من وجوده: nano .gitignore ... .env ... تمكنت في هذه المرحلة من استخراج المعلومات الحساسة من شيفرة المشروع واتخذت تدابير للتحكم في كيفية ومكان نسخ هذه المعلومات. يمكنك الآن العمل على تمتين الشيفرة الخاصّة بالاتصال بقاعدة البيانات بُغية تحسينها من أجل سير عملٍ يعتمد على الحاويات. الخطوة الثالثة: تعديل إعدادات اتصال قاعدة البيانات ستكون خطوتنا التالية هي جعل طريقة اتصال قاعدة البيانات أكثر متانة عن طريق إضافة شيفرة تعالج الحالات التي يفشل فيها تطبيقنا في الاتصال بقاعدة البيانات. ويُعدّ تقديم هذا المستوى من المرونة لشيفرة التطبيق ممارسةً ينصح بها عند التعامل مع الحاويات باستخدام "Compose". افتح الملفّ db.js لتحريره: nano db.js ستظهر لك الشفرة التي أضفناها سابقًا، إلى جانب ثابتة العنوان url الخاصة بالمعرّف URI لاتصال Mongo والتابع connect فيMongoose: ... const { MONGO_USERNAME, MONGO_PASSWORD, MONGO_HOSTNAME, MONGO_PORT, MONGO_DB } = process.env; const url = `mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}?authSource=admin`; mongoose.connect(url, {useNewUrlParser: true}); يقبل التابع connect حاليًا خيارًا يطلب من Mongoose استخدام مفسّر URL الجديد الخاص بـMongo. دعنا نضيف بعض الخيارات الإضافية إلى هذا التابع لتعريف المعاملات (paramaters) لمحاولات إعادة الاتصال. يمكننا القيام بذلك عبر إنشاء ثابتة options تتضمّن المعلومات ذات الصلة، بالإضافة إلى خيار مفسّر عناوين URL الجديد. أسفل ثوابت Mongo، أضف التعريف التالي للثابتة options: ... const { MONGO_USERNAME, MONGO_PASSWORD, MONGO_HOSTNAME, MONGO_PORT, MONGO_DB } = process.env; const options = { useNewUrlParser: true, reconnectTries: Number.MAX_VALUE, reconnectInterval: 500, connectTimeoutMS: 10000, }; ... يطلب الخيار reconnectTries من Mongoose مواصلة محاولة الاتصال إلى أجل غير محدّد، في حين تحدد reconnectInterval الفترة الفاصلة بين محاولتي اتصال بوحدة الجزء من الألف من الثانية. ويحدّد connectTimeoutMS الفترة التي سينتظرها برنامج التشغيل Mongo قبل إعلان فشل محاولة الاتصال في 10 ثوانٍ. يمكننا الآن استخدام الثابتة الجديدة options في التابع connect لضبط إعدادات اتصال Mongoose بدقّةٍ. سنضيف أيضًا كائن وعدٍ (promise) لمعالجة أخطاء الاتصال المحتملة. يبدو التابع connect الخاصّ ب Mongoose حاليًا على هذا النحو: ... mongoose.connect(url, {useNewUrlParser: true}); احذف التابع connect الموجود حاليًا وعوّضه بالشيفرة التالية التي تتضمن الثابتة options والوعد: ... mongoose.connect(url, options).then( function() { console.log('MongoDB is connected'); }) .catch( function(err) { console.log(err); }); في حالة اتصال ناجح، تسجّل دالّتنا رسالةً مناسبةً. وإلا فإنها تُمسك الخطأ وتسجّله، مما يتيح لنا استكشاف الأخطاء وإصلاحها. سيبدو الملف النهائي كما يلي: const mongoose = require('mongoose'); const { MONGO_USERNAME, MONGO_PASSWORD, MONGO_HOSTNAME, MONGO_PORT, MONGO_DB } = process.env; const options = { useNewUrlParser: true, reconnectTries: Number.MAX_VALUE, reconnectInterval: 500, connectTimeoutMS: 10000, }; const url = `mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}?authSource=admin`; mongoose.connect(url, options).then( function() { console.log('MongoDB is connected'); }) .catch( function(err) { console.log(err); }); احفظ الملف وأغلقه عندما تنتهي من التحرير. لقد أضفت الآن مرونةً إلى شيفرة التطبيق لمعالجة الحالات التي قد يفشل فيها التطبيق في الاتصال بقاعدة البيانات. بوضعك لهذه الشيفرة في مكانها الصحيح، يمكنك الانتقال إلى تعريف خدماتك باستخدام "Compose". الخطوة الرابعة: تعريف الخدمات باستخدام Docker Compose بإعادة تشكيل الشيفرة، تكون مستعدًا لتحرير الملف docker-compose.yml بتعريفات خدماتك. الخدمة في Compose هي عبارة عن حاويةٍ قيد التشغيل، وتحتوي تعريفات الخدمة، التي ستتضمّنها في ملف docker-compose.yml، على معلومات حول كيفية اشتغال كل حاويةٍ صورةٍ (container image). تتيح لك الأداة "Compose" تعريف خدمات متعددة لإنشاء تطبيقات متعددة الحاويات. قبل تعريف خدماتنا، سنضيف، مع ذلك، أداةً إلى مشروعنا تسمى wait-for للتأكد من أن التطبيق لا يحاول الاتصال بقاعدة البيانات فقط بعد اكتمال مهام بدء تشغيل قاعدة البيانات. يستخدم هذا السكربت المجمِّع netcat لاستقصاء ما إذا كان المضيف والمنفذ المعيّنان يقبلان اتصالات TCP أم لا. ويتيح لك استخدامه التحكم في محاولات التطبيق للاتصال بقاعدة البيانات عن طريق اختبار جاهزية قاعدة البيانات لقبول الاتصالات من عدمها. رغم أنّ "Compose" يتيح لك تحديد الاعتماديات بين الخدمات باستخدام الخيار dependson، فإنّ هذا الترتيب يعتمد على ما إذا كانت الحاوية تعمل أم لا وليس على جاهزيتها. لن يكون استخدام dependson هو الأمثل لإعدادنا، لأننا نريد أن يتّصل تطبيقنا بقاعدة البيانات فقط عند اكتمال مهام بدء التشغيل فيها، بما في ذلك إضافة مستخدم وكلمة مرور إلى استيثاق قاعدة البيانات admin. لمزيد من المعلومات حول استخدام wait-for والأدوات الأخرى للتحكم في ترتيب بدء التشغيل، يرجى الاطلاع على التوصيات ذات الصلة في توثيق Compose. افتح ملفًا يسمى wait-for.sh: nano wait-for.sh انسخ الشيفرة التالية في هذا الملف لإنشاء دالة الاستقصاء: #!/bin/sh # original script: https://github.com/eficode/wait-for/blob/master/wait-for TIMEOUT=15 QUIET=0 echoerr() { if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi } usage() { exitcode="$1" cat << USAGE >&2 Usage: $cmdname host:port [-t timeout] [-- command args] -q | --quiet Do not output any status messages -t TIMEOUT | --timeout=timeout Timeout in seconds, zero for no timeout -- COMMAND ARGS Execute command with args after the test finishes USAGE exit "$exitcode" } wait_for() { for i in `seq $TIMEOUT` ; do nc -z "$HOST" "$PORT" > /dev/null 2>&1 result=$? if [ $result -eq 0 ] ; then if [ $# -gt 0 ] ; then exec "$@" fi exit 0 fi sleep 1 done echo "Operation timed out" >&2 exit 1 } while [ $# -gt 0 ] do case "$1" in *:* ) HOST=$(printf "%s\n" "$1"| cut -d : -f 1) PORT=$(printf "%s\n" "$1"| cut -d : -f 2) shift 1 ;; -q | --quiet) QUIET=1 shift 1 ;; -t) TIMEOUT="$2" if [ "$TIMEOUT" = "" ]; then break; fi shift 2 ;; --timeout=*) TIMEOUT="${1#*=}" shift 1 ;; --) shift break ;; --help) usage 0 ;; *) echoerr "Unknown argument: $1" usage 1 ;; esac done if [ "$HOST" = "" -o "$PORT" = "" ]; then echoerr "Error: you need to provide a host and port to test." usage 2 fi wait_for "$@" احفظ الملف وأغلقه عند الانتهاء من إضافة الشيفرة. اجعل السكربتَ قابلًا للتنفيذ: chmod +x wait-for.sh بعد ذلك، افتح الملف docker-compose.yml: nano docker-compose.yml ابدأ بتعريف خدمة تطبيق nodejs عبر إضافة الشيفرة التالية إلى الملف: version: '3' services: nodejs: build: context: . dockerfile: Dockerfile image: nodejs container_name: nodejs restart: unless-stopped env_file: .env environment: - MONGO_USERNAME=$MONGO_USERNAME - MONGO_PASSWORD=$MONGO_PASSWORD - MONGO_HOSTNAME=db - MONGO_PORT=$MONGO_PORT - MONGO_DB=$MONGO_DB ports: - "80:8080" volumes: - .:/home/node/app - node_modules:/home/node/app/node_modules networks: - app-network command: ./wait-for.sh db:27017 -- /home/node/app/node_modules/.bin/nodemon app.js يتضمّن تعريف خدمة nodejs الخيارات التالية: build: هذا يحدد خيارات التكوين، بما في ذلك context وdockerfile، والتي ستُطبّق عندما يبني Docker صورة التطبيق. إذا كنت ترغب في استخدام صورة موجودة من سجلٍّ مثل Docker Hub، فيمكنك استخدام التعليمة image بدلاً من ذلك، مع معلومات حول اسم المستخدم والمستودع ووسم الصورة. context: هذا يحدّد سياق البناء الخاص ببناء الصورة، وهو في هذه الحالة مجلّد المشروع الحالي. dockerfile : هذا يحدّد الملفّ Dockerfile في مجلّد المشروع الحالي لكي يستخدم Compose هذا الملفّ لبناء صورة التطبيق. لمزيد من المعلومات حول هذا الملف، يرجى الاطلاع على [كيفية إنشاء تطبيق Node.js باستخدام Docker](رابط المقال الأول في هذه السلسلة). image و container_name: لإعطاء اسم لكلّ من الصورة والحاوية. restart: هذا يحدّد سياسة إعادة تشغيل الحاوية. تكون القيمة الافتراضية هي no، لكننا أعدنا تعيينها في yes ما لم يتم إيقافها. env_file: هذا يطلب من Compose إضافة متغيرات البيئة من ملفٍّ يسمى env. موجود في سياق البناء. environment: يتيح لك استخدام هذا الخيار إضافة إعدادات اتصال Mongo التي حدّدتها في ملف env. لاحظ أننا لا نعيّن NODEENV على development، لأن هذا هو السلوك الافتراضي لـ Express إذا لم يتم تعيين NODEENV. عند الانتقال إلى الإنتاج، يمكنك تعيين هذا على production لإتاحة التخزين المؤقت للعرض والتقليل من رسائل الخطأ المطوّلة. لاحظ أيضًا أننا حددنا حاوية قاعدة البيانات db كمضيف، كما وضّحناه في الخطوة الثانية. port: هذا يوجّه المنفذ 80 على المضيف إلى المنفذ 8080 على الحاوية. Volumes: نضمّن هنا نوعين من الربط: الأول هو وصل ترابطي (bind mount) يحمّل شيفرة التطبيق على المضيف إلى المجلّد /home/node/app في الحاوية. سيسهل هذا الأمر التطوير بسرعة، إذ سيتم إدخال أي تغييرات تجريها على شيفرة المضيف في الحاوية على الفور. والثاني هو عبارة عن حجمٍ مُسمّى nodemodules. عندما ينفّذ Docker التعليمة npm install المدرجة في Dockerfile، سينشئ npm مجلّدًا جديدًا nodemodules على الحاوية يتضمن الحزم المطلوبة لتشغيل التطبيق. ومع ذلك، سيحجب الوصل الترابطي الذي أنشأناه المجلّدَ nodemodules حديث الإنشاء. ونظرًا لأن nodemodules على المضيف فارغٌ، فسيعمل الوصل على تعيين مجلّدٍ فارغ على الحاوية، مع تحييد المجلّد nodemodules الجديد ومنع التطبيق من بدء التشغيل. ويعمل الحجم nodemodules المسمى على حلّ هذه المشكلة من خلال تثبيت محتويات المجلّد /home/node/app/node_modules وربطه مع الحاوية، مع إخفاء الربط. ضع في حسبانك النقاط التالية عند استخدام هذه الطريقة: سيعمل الربط على تحميل محتويات مجلّد node_modules على الحاوية إلى المضيف وسيكون هذا المجلّد مِلكًا للجذر root، مادام الحجم المسمى أنشئ بواسطة Docker. إذا كان لديك مجلّد nodemodules موجود مسبقًا على المضيف، فسيحيِّد المجلّد nodemodules الذي أنشئ على الحاوية. يفترض الإعداد الذي نبنيه في هذا الدرس أنك لا تملك مجلّدًا node_modules موجود مسبقًا وأنك لن تعمل مع npm على مضيفك. هذا يتماشى مع طريقة اثني عشر عامل لتطوير التطبيقات التي تقلل من الاعتماديات بين بيئات التنفيذ. networks: هذا يحدّد أن خدمة التطبيق ستنضم إلى الشبكة app-network التي سنعرّفها في أسفل الملف. command: يتيح لك هذا الخيار تعيين الأمر الذي يجب تنفيذه عندما يشغّل Compose الصورة. لاحظ أن هذا سوف يحيّد تعليمات CMD التي وضعناها في تطبيق Dockerfile الخاص بنا. نشغِّل هنا التطبيق باستخدام السكربت wait-for، الذي سيستقصي الخدمة db على المنفذ 27017 لاختبار جاهزية خدمة قاعدة البيانات من عدمها. بمجرد نجاح اختبار الاستعداد، سينفذ السكربت الأمر الذي حددناه، ‎/home/node/app/node_modules/.bin/nodemon app.js، لتشغيل التطبيق عبر nodemon. وسيضمن هذا إعادة تحميل أي تعديلات مستقبلية نجريها على شيفرتنا دون الحاجة إلى إعادة تشغيل التطبيق. بعد ذلك، أنشئ خدمة db من خلال إضافة الشيفرة التالية أسفل تعريف خدمة التطبيق: الملف ‎~/node_project/docker-compose.yml: ... db: image: mongo:4.1.8-xenial container_name: db restart: unless-stopped env_file: .env environment: - MONGO_INITDB_ROOT_USERNAME=$MONGO_USERNAME - MONGO_INITDB_ROOT_PASSWORD=$MONGO_PASSWORD volumes: - dbdata:/data/db networks: - app-network تظل بعض الإعدادات التي حددناها لخدمة nodejs كما هي، لكننا أجرينا أيضًا التعديلات التالية على تعريفات image و environment و volumes: image: لإنشاء هذه الخدمة، سيسحب Compose صورة 4.1.8-xenial الخاصة ب Mongo من Docker Hub. نحن نثبّت هنا إصدارًا معينًا لتجنب التعارضات المستقبلية المحتملة عند تغير صورة Mongo. لمزيد من المعلومات حول تثبيت الإصدار، يرجى الاطلاع على توثيق Docker حول أفضل ممارسات Dockerfile. MONGOINITDBROOTUSERNAME و MONGOINITDBROOTPASSWORD: تجعل صورة mongo متغيرات البيئة هذه متاحةً لكي تستطيع تعديل التهيئة الأولية لمثيل قاعدة بياناتك. وينشئ MONGOINITDBROOTUSERNAME و MONGOINITDBROOTPASSWORD معًا مستخدمًا root في استيثاق admin لقاعدة البيانات مع التأكد من تفعيل الاستيثاق عند بدء تشغيل الحاوية. لقد عيّننا MONGOINITDBROOTUSERNAME و MONGOINITDBROOTPASSWORD باستخدام قيم مأخوذة من الملف env.، والتي نمرّرها إلى الخدمة db عبر الخيار env_file. يعني القيام بذلك أن مستخدم التطبيق sammy سيكون مستخدمًا أساسيًا في مثيل قاعدة البيانات، مع إمكانية الوصول إلى جميع الصلاحيات الإدارية والتشغيلية لهذا الدور. عند العمل على الإنتاج، سترغب في إنشاء مستخدم تطبيق مخصص ذي صلاحيات محدّدة بدقّة. dbdata:/data/db: سيثبّت الحجم المسمى dbdata البيانات المخزنة في مجلّد البيانات الافتراضي ‎/data/db الخاص بـ Mongo . سيضمن هذا أنك لن تفقد البيانات في الحالات التي توقف فيها أو تحذف الحاويات. لقد أضفنا أيضًا خدمة db إلى شبكة app-network باستخدام الخيار networks. كخطوة أخيرة، أضف تعريفات الحجم والشبكة إلى أسفل الملف: ... networks: app-network: driver: bridge volumes: dbdata: node_modules: تتيح شبكة المستخدم الجسرية app-network التواصل بين حاوياتنا مادامت موجودة على نفس المضيف العفريت Docker. يعمل هذا على تبسيط حركة المرور والاتصال داخل التطبيق، إذ يفتح جميع المنافذ بين الحاويات على نفس الشبكة الجسرية، دون تعريض أي منفذ للعالم الخارجي. وبالتالي، فإن حاويات db و nodejs تستطيع التواصل مع بعضها البعض، وسنحتاج فقط إلى فتح المنفذ 80 للوصول الأمامي (front-end access) إلى التطبيق. يعرّف مفتاح المستوى الأعلى volumes الحجمين dbdata و node_modules. وعندما ينشئ Docker حجومًا، تُخزّن محتوياتها في جزء من ملفات المضيف النظامية، /var/lib/docker/volumes/ ، التي يديرها Docker. تخزّن محتويات كل حجمٍ في مجلّد تحت /var/lib/docker/volumes/ وتُثبت على أي حاوية تستخدم الحجم. وبهذه الطريقة، ستكون بيانات معلومات سمك القرش التي سيقوم المستخدمون بإنشائها مثبّتة في الحجم dbdata حتى لو أزلنا الحاوية db وأعدنا إنشاءها. سيبدو ملف docker-compose.yml النهائي كما يلي: version: '3' services: nodejs: build: context: . dockerfile: Dockerfile image: nodejs container_name: nodejs restart: unless-stopped env_file: .env environment: - MONGO_USERNAME=$MONGO_USERNAME - MONGO_PASSWORD=$MONGO_PASSWORD - MONGO_HOSTNAME=db - MONGO_PORT=$MONGO_PORT - MONGO_DB=$MONGO_DB ports: - "80:8080" volumes: - .:/home/node/app - node_modules:/home/node/app/node_modules networks: - app-network command: ./wait-for.sh db:27017 -- /home/node/app/node_modules/.bin/nodemon app.js db: image: mongo:4.1.8-xenial container_name: db restart: unless-stopped env_file: .env environment: - MONGO_INITDB_ROOT_USERNAME=$MONGO_USERNAME - MONGO_INITDB_ROOT_PASSWORD=$MONGO_PASSWORD volumes: - dbdata:/data/db networks: - app-network networks: app-network: driver: bridge volumes: dbdata: node_modules: احفظ الملف وأغلقه عند الانتهاء من التحرير. بوضعك لتعريفات الخدمة في مكانها الصحيح، تكون مستعدًّا لبدء تشغيل التطبيق. الخطوة الخامسة: اختبار التطبيق بعد إنشاء وتحرير ملف docker-compose.yml ، يمكنك إنشاء خدماتك باستخدام الأمر docker-compose up. يمكنك أيضًا اختبار ثبات بياناتك عبر إيقاف حاوياتك وحذفها باستخدام Docker. ابدأ أولًا ببناء صور الحاوية وإنشاء الخدمات عبر تنفيذ docker-compose up باستخدام الراية d- ، والذي سيشغّل حاويات nodejs و db في الخلفية: docker-compose up -d سيظهر لك الإخراج الذي يؤكد أن خدماتك أُنشئت فعلًا: ... Creating db ... done Creating nodejs ... done يمكنك أيضًا الحصول على معلومات أكثر تفصيلاً حول عمليات التشغيل بعرضك لإخراج سجل الخدمات: docker-compose logs سيظهر لك إخراج مثل هذا إذا اشتغل كل شيء بشكل صحيح: ... nodejs | [nodemon] starting `node app.js` nodejs | Example app listening on 8080! nodejs | MongoDB is connected ... db | 2019-02-22T17:26:27.329+0000 I ACCESS [conn2] Successfully authenticated as principal sammy on admin يمكنك أيضًا التحقق من حالة حاوياتك باستخدام docker-compose ps: docker-compose ps سيظهر لك الإخراج التالي مشيرًا إلى أن حاوياتك تعمل: Name Command State Ports ---------------------------------------------------------------------- db docker-entrypoint.sh mongod Up 27017/tcp nodejs ./wait-for.sh db:27017 -- ... Up 0.0.0.0:80->8080/tcp بعد تشغيل خدماتك، يمكنك تصفّح: http://yourserverip في المتصفح. ستظهر لك صفحة الهبوط على هذا النحو: انقر على زر الحصول على معلومات القرش. ستظهر لك صفحة بنموذج إدخال يمكنك إدخال الاسم ووصف السلوك العام لسمك القرش: أضف سمكة قرش من اختيارك في النموذج. لأغراض هذا العرض التوضيحي، سنضيف Megalodon Shark إلى حقل Shark Name، وAncient إلى حقل Shark Character: انقر على زر الإرسال. ستظهر لك صفحة بها معلومات القرش معروضة: كخطوة أخيرة، يمكننا اختبار ثبات البيانات التي أدخلتها للتو إذا حذفنا حاوية قاعدة البيانات. ارجع مرة أخرى إلى الطرفية، واكتب الأمر التالي لإيقاف وحذف الحاويات والشبكة: docker-compose down لاحظ أننا لم ندرج الخيار ‎--volumes؛ وبالتالي، لن يُحذف الحجم dbdata. يؤكد الإخراج التالي حذف الحاويات والشبكة: Stopping nodejs ... done Stopping db ... done Removing nodejs ... done Removing db ... done Removing network node_project_app-network أعد الآن إنشاء الحاويات: docker-compose up -d ثم عد إلى نموذج معلومات سمك القرش: أدخل سمكة قرش جديدة من اختيارك. سنستعمل هنا Whale Shark وLarge: بمجرد النقر على زر الإرسال، سترى أن القرش الجديد أُضيف إلى مجموعة سمك القرش في قاعدة بياناتك دون أن تفقد البيانات التي أدخلتها سابقًا: إنّ تطبيقك يشتغل الآن على حاويات Docker مع تفعيل خاصيتي ثبات البيانات ومزامنة الشيفرة. خاتمة باتباع هذا الدرس، أنشأت إعداد تطويرٍ لتطبيقك Node باستخدام حاويات Docker. لقد تمكنت من جعل مشروعك أكثر نمطيّةً وأكثر قابلية للنقل من خلال استخراج المعلومات الحساسة وفصل حالة التطبيق عن شيفرة التطبيق. لقد أعددت أيضًا تكوين ملف الشيفرة المتداولة docker-compose.yml الذي تستطيع مراجعته كلما تغيرت احتياجاتك ومتطلبات التطوير الخاصة بك. قد تكون مهتمًا، أثناء عمليتك التطويرية ، بمعرفة المزيد حول تصميم التطبيقات بسير عمل يعتمد على الحاويات وعلى Cloud Native. يرجى الاطلاع على تصميم التطبيقات لKubernetes وتحديث التطبيقات ل Kubernetes إذا كانت لغتك الإنجليزية جيدة لمزيد من المعلومات حول هذه المواضيع. لمعرفة المزيد حول الشيفرة المستخدم في هذا الدرس، يرجى الاطلاع على كيفية إنشاء تطبيق Node.js باستخدام Docker وكيفية دمج MongoDB مع تطبيق Node الخاص بك. وللحصول على معلومات حول نشر تطبيق Node باستخدام وكيل عكسي Nginx يعتمد على الحاويات، يرجى الاطلاع على كيفية تأمين تطبيق Node.js في حاويات باستخدام Nginx و Let’s Encrypt و Docker Compose. ترجمة -وبتصرف- للمقال Containerizing a Node.js Application for Development With Docker Compose لصاحبته Kathleen Juell
  11. قد تجد نفسك، وأنت تستخدم node.js، أنك بصدد تطوير مشروع يُخزّن البيانات والاستعلام عنها. في هذه الحالة، ستحتاج إلى اختيار حلّ تقني لقاعدة البيانات يكون ملائما لأنواع البيانات والاستعلامات التي يستخدمها تطبيقك. في هذا الدّرس، ستعمل على دمج قاعدة بيانات MongoDB مع تطبيق Node موجودٍ سلفًا. يمكن أن تكون قواعد البيانات NoSQL مثل MongoDB مفيدةً إذا كانت متطلبات بياناتك تتضمن قابلية التوسع والمرونة. ويتكامل MongoDB جيدًا مع Node لأنه مصمم للعمل بشكل غير متزامن مع كائنات JSON. من أجل دمج MongoDB في مشروعك، سوف تستخدم Object Document Mapper) ODM) الخاص ب Mongoose لإنشاء مخططات ونماذج بيانات لتطبيقك. سيتيح لك ذلك تنظيم شيفرة التطبيق وفقًا للنمط الهيكلي MVC، والذي يسمح بفصل المنطق الذي يحكم كيفية معالجة التطبيق لإدخالات المستخدم عن ذلك الذي يتعلق بكيفية هيكلة البيانات وتقديمها إلى المستخدم. ويسهّل الاعتمادُ على هذا النمط الاختبار والتطوير في المستقبل عبر فصل المتعلّقات في قاعدة البيانات الخاصة بك. في نهاية البرنامج التعليمي، سيكون لديك تطبيق جاهز خاصّ بمعلومات سمك القرش يعمل على أخذ مدخلات المستخدم حول أسماك القرش المفضلة لديه وعرض النتائج في المتصفح: المتطلبات الأساسية جهاز أو خادم تطوير محلي يعمل على نظام أوبونتو 18.04، إلى جانب مستخدم غير جذري بصلاحيات sudo وجدار حماية نشط. للحصول على إرشادات حول كيفية إعدادها على خادم 18.04، يرجى الاطلاع على دليل إعداد الخادم الأولي. Node.js و npm مثبتين على الجهاز أو الخادم، باتباع هذه الإرشادات حول التثبيت باستخدام PPA المدار بواسطة NodeSource. تثبيت MongoDB على الجهاز أو الخادم، باتباع الخطوة الأولى من كيفية تثبيت MongoDB في أوبونتو 18.04. الخطوة الأولى: إنشاء مستخدم Mongo قبل أن نبدأ العمل بشيفرة التطبيق، سننشئ مستخدمًا إداريًا يملك صلاحية الوصول إلى قاعدة بيانات التطبيق. سيكون لهذا المستخدم أذونات إدارية على أي قاعدة بيانات، مما يمنحك المرونة في التبديل وإنشاء قواعد بيانات جديدة حسب الحاجة. أولاً، تحقق أن MongoDB يشتغل على خادمك: sudo systemctl status mongodb يدلّ الإخراج التالي على أنّ MongoDB يشتغل: ● mongodb.service - An object/document-oriented database Loaded: loaded (/lib/systemd/system/mongodb.service; enabled; vendor preset: enabled) Active: active (running) since Thu 2019-01-31 21:07:25 UTC; 21min ago ... بعد ذلك، افتح الصدفة الخاصة ب Mongo من أجل إنشاء مستخدمك: Mongo سينقلك هذا الأمر إلى صدفة إدارية: MongoDB shell version v3.6.3 connecting to: mongodb://127.0.0.1:27017 MongoDB server version: 3.6.3 ... > ستظهر لك بعض التحذيرات الإدارية عند فتح الصدفة (shell) نظرًا لغياب قيود على وصولك إلى قاعدة بيانات الخاصة بالمشرف admin. لمعرفة المزيد حول تقييد هذا الوصول يمكنك قراءة كيفية تثبيت وتأمين MongoDB على أوبونتو 18.04، عندما تنتقل إلى إعدادات الإنتاج. تستطيع الآن الاستفادة من وصولك إلى قاعدة بيانات المشرف admin لإنشاء مستخدم يمتلك الأذونات userAdminAnyDatabase التي تتيح الوصول المحمي بكلمة مرور لقواعد بيانات التطبيق. حدّد في الصّدفة shell، أنك تريد استخدام قاعدة بيانات المشرف admin لإنشاء مستخدمك: use admin بعد ذلك، أنشئ دورًا (role) وكلمة مرور عبر إضافة اسم مستخدم وكلمة مرور باستخدام الأمر db.createUser. بعد كتابة هذا الأمر، ستُظهِر الصدفة ثلاث نقاط قبل كل سطر حتى يكتمل تنفيذ الأمر. تأكد من تغيير المستخدم وكلمة المرور المقدمين هنا باسم المستخدم وكلمة المرور الخاصين بك: db.createUser( { user: "sammy", pwd: "your_password", roles: [ { role: "userAdminAnyDatabase", db: "admin" } ] } ) سينشئ هذا الأمر إدخالًا للمستخدم sammy في قاعدة بيانات المشرف admin. وسيعمل اسم المستخدم الذي تحدده وقاعدة بيانات المشرف admin كمحدّدات للمستخدم. وسيبدو إخراج العملية برمّتها كما يلي، بما في ذلك الرسالة التي تشير إلى نجاح الإدخال: > db.createUser( ... { ... user: "sammy", ... pwd: "your_password", ... roles: [ { role: "userAdminAnyDatabase", db: "admin" } ] ... } ...) Successfully added user: { "user" : "sammy", "roles" : [ { "role" : "userAdminAnyDatabase", "db" : "admin" } ] } يمكنك بعد إنشاء المستخدم وكلمة المرور، الخروج من الصّدفة الخاصة ب Mongo: exit وتستطيع كذلك الآن، بعد أن أنشأت مستخدم قاعدة البيانات الخاصة بك، الانتقال إلى استنساخ شيفرة المشروع البدئية وإضافة مكتبة Mongoose، والتي سوف تسمح لك بتنفيذ المخططات والنماذج للمجموعات (collections) في قواعد بياناتك. الخطوة الثانية: إضافة معلومات Mongoose وقاعدة البيانات إلى المشروع ستكون خطوتنا التالية هي استنساخ شيفرة التطبيق البدئية وإضافة معلومات Mongoose وقاعدة بيانات MongoDB إلى المشروع. في المجلّد الرئيسي للمستخدم غير الجذري، استنسخ مستودع nodejs-image-demo من حساب DigitalOcean Community GitHub. ويتضمن مستودع التخزين هذا الشيفرة الخاصة بالإعداد الموضح في كيفية إنشاء تطبيق Node.js باستخدام Docker. انسخ المستودع في مجلّد يسمى node_project: git clone https://github.com/do-community/nodejs-image-demo.git node_project انتقل إلى المجلّد node_project: cd node_project قبل تعديل شيفرة المشروع، دعنا نلقي نظرة على بنية المشروع باستخدام الأمر tree . ملحوظة: يعدّ الأمر tree مفيدًا لعرض بنيات الملفات والمجلّدات من سطر الأوامر. يمكنك تثبيته باستخدام الأمر التالي: sudo apt install tree ومن أجل استخدامه، انتقل بالأمر cd إلى مجلّد معين واكتب tree. يمكنك أيضًا أن تزوّد الأمر بكامل المسار إلى النقطة المحدّدة باستخدام أمر مثل: tree /home/sammy/sammys-project اكتب ما يلي لمعاينة المجلّد node_project: tree تبدو بنية المشروع الحالي كما يلي: ├── Dockerfile ├── README.md ├── app.js ├── package-lock.json ├── package.json └── views ├── css │ └── styles.css ├── index.html └── sharks.html سنضيف كلما تقدمنا في هذا البرنامج التعليمي بعض المجلّدات إلى هذا المشروع، وسيكون الأمر tree مفيدًا لمساعدتنا في تتبع تقدمنا. بعد ذلك، أضف حزمة npm mongoose إلى المشروع باستخدام الأمر npm install: npm install mongoose سينشئ هذا الأمر مجلّد node_modules في مجلّد مشروعك، باستخدام الاعتماديات المدرجة في ملف package.json الخاص بالمشروع، وسيضيف كذلك mongoose إلى هذا المجلّد. سيضيف mongoose أيضًا إلى الاعتماديات المدرجة في ملفك package.json. للحصول على توضيح أكثر تفصيلا عن package.json، يرجى الاطلاع على الخطوة الأولى في كيفية إنشاء تطبيق Node.js باستخدام Docker. قبل إنشاء أي مخططات أو نماذج لMongoose، سنضيف معلومات اتصال قاعدة البيانات حتى يتمكن التطبيق من الاتصال بقاعدة البيانات الخاصة بنا. من أجل فصل العناصر المتعلّقة بالتطبيق قدر الإمكان، أنشئ ملفًا منفصلًا لمعلومات اتصال قاعدة البيانات باسم db.js. ويمكنك فتح هذا الملف باستخدام nano أو المحرر المفضل لديك: nano db.js ستستورد أولاً وحدة mongoose باستخدام الدالّة require: الملف ‎~/node_project/db.js: const mongoose = require('mongoose'); سوف يتيح لك ذلك الوصول إلى توابع Mongoose المدمجة، والتي ستستخدمها لإنشاء اتصال بقاعدة البيانات الخاصة بك. بعد ذلك، أضف الثوابت التالية لتحديد معلومات الاتصال URI الخاصة بMongo. رغم أن اسم المستخدم وكلمة المرور اختياريان، ولكننا سندرجهما حتى نتمكن من طلب مصادقة الهوية على قاعدة البيانات. تأكد من استبدال اسم المستخدم وكلمة المرور بمعلوماتك الخاصة، وتبقى لك الحرية في طلب شيء آخر من قاعدة البيانات غير sharkinfo إذا كنت تفضل ذلك: الملف ‎~/node_project/db.js: const mongoose = require('mongoose'); const MONGO_USERNAME = 'sammy'; const MONGO_PASSWORD = 'your_password'; const MONGO_HOSTNAME = '127.0.0.1'; const MONGO_PORT = '27017'; const MONGO_DB = 'sharkinfo'; نظرًا لأننا نشغّل قاعدة البيانات محليًا، فقد استخدمنا 127.0.0.1 اسمًا للمضيف. قد يكون الأمر مغايرًا في سياقات تطوير أخرى: على سبيل المثال، إذا كنت تستخدم خادم قاعدة بيانات منفصل أو تعمل باستخدام عقد متعدّدة في إطار سير عمل يتضمن حاويات. ختامًا، حدّد قيمة ثابتة لـ URI وأنشئ الاتصال باستخدام التابع ()mongoose.connect: الملف ‎~/node_project/db.js: ... const url = `mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}?authSource=admin`; mongoose.connect(url, {useNewUrlParser: true}); لاحظ أننا في URI حدّدنا authSource لمستخدمنا كمشرف لقاعدة البيانات. ويعدّ هذا الأمر ضروريًا لأننا حددنا اسم مستخدم في سلسلة الاتصال connection string. ويحدّد استخدام الراية useNewUrlParser مع التابع ()mongoose.connect أننا نريد استخدام مفسّر URL الجديد الخاص بـ Mongo. احفظ الملف وأغلقه عند الانتهاء من التحرير. كخطوة أخيرة، أضف معلومات اتصال قاعدة البيانات إلى ملف app.js بحيث يمكن للتطبيق استخدامها. ثم افتح التطبيق. nano app.js ستبدو الأسطر الأولى من الملف كما يلي: الملف ‎~/node_project/app.js: const express = require('express'); const app = express(); const router = express.Router(); const path = __dirname + '/views/'; ... أسفل السّطر الذي يحدّد القيمة الثابتة للكائن router، الموجود قريبًا من أعلى الملف، أضف السطر التالي: الملف ‎~/node_project/app.js: ... const router = express.Router(); const db = require('./db'); const path = __dirname + '/views/'; ... يطلب هذا الأمر من التطبيق استخدام معلومات اتصال قاعدة البيانات المحددة في db.js. احفظ الملف وأغلقه عند الانتهاء من التحرير. بوجود معلومات قاعدة البيانات وبعد إضافتها إلى مشروعك، تكون مستعدًا لإنشاء المخططات والنماذج التي ستشكل البيانات في المجموعة sharks. الخطوة الثالثة: إنشاء مخططات ونماذج Mongoose ستكون خطوتنا التالية هي التفكير في بنية المجموعة sharks التي سينشئها المستخدمون في قاعدة بيانات sharkinfo بواسطة مدخلاتهم. فما هي البنية التي نريد أن تكون لهذه المستندات التي أُنشئت؟ تتضمن صفحة معلومات سمك القرش في تطبيقنا الحالي بعض التفاصيل حول أسماك القرش المختلفة وسلوكياتها: وفقًا لذلك، سيكون بإمكان المستخدمين أن يضيفوا أسماك قرش جديدة مرفقة بتفاصيل حول سلوكها العام. وسيحدّد هذا الهدف كيفية إنشاء مخططنا. من أجل تمييز مخططاتك ونماذجك عن الأجزاء الأخرى لتطبيقك، أنشئ مجلّدا models في مجلّد المشروع الحالي: mkdir models بعد ذلك، افتح ملفًا باسم sharks.js لإنشاء مخططك ونموذجك: nano models/sharks.js استورد وحدة Mongose في الجزء العلوي من الملف: الملف ‎~/node_project/models/sharks.js: const mongoose = require('mongoose'); بعد ذلك، حدّد كائنًا Schema لاستخدامه أساسًا للمخطط الخاصّ بأسماك القرش: الملف ‎~/node_project/models/sharks.js: const mongoose = require('mongoose'); const Schema = mongoose.Schema; يمكنك الآن تحديد الحقول التي تريد تضمينها في مخطّطك. ونظرًا لأننا نريد إنشاء مجموعة تحتوي على أسماك قرش فردية ومعلومات حول سلوكياتها، فلنُضمِّن فيها مفتاحين واحدٌ للاسم name والآخر للسلوك character. أضف مخطط Shark التالي أسفل تعريفات الثوابت: الملف ‎~/node_project/models/sharks.js: ... const Shark = new Schema ({ name: { type: String, required: true }, character: { type: String, required: true }, }); يتضمن هذا التعريف معلومات حول نوع الإدخال الذي نتوقعه من المستخدمين، وهو في هذه الحالة، سلسلة نصّية، وهل هذا الإدخال ضروري أم لا. أخيرًا ، أنشئ نموذج Shark باستخدام دالة Mongoose التالية model()‎. سيتيح لك هذا النموذج الاستعلام عن المستندات من خلال مجموعتك والتحقق من صحة المستندات الجديدة. أضف السطر التالي في أسفل الملف: ... module.exports = mongoose.model('Shark', Shark) يجعل هذا السطر الأخير النموذج Shark متاحًا كوحدة نمطية باستخدام خاصية module.exports. إذ تحدّد هذه الخاصية القيم التي ستصدرها الوحدة النمطية، مما يجعلها متاحة للاستخدام في أي مكان آخر في التطبيق. سيبدو الملف النهائي models/sharks.js على هذا النحو: const mongoose = require('mongoose'); const Schema = mongoose.Schema; const Shark = new Schema ({ name: { type: String, required: true }, character: { type: String, required: true }, }); module.exports = mongoose.model('Shark', Shark) احفظ الملف وأغلقه عند الانتهاء من التحرير. بإنشائك لمخطط Shark ونموذجه، سيكون بإمكانك البدء في العمل على المنطق الذي تريده أن يحكم كيفية تعامل تطبيقك مع مدخلات المستخدم. الخطوة الرابعة: إنشاء وحدات التحكم ستكون خطوتنا التالية هي إنشاء وحدة التحكم التي تحدد كيفية حفظ مدخلات المستخدم في قاعدة البيانات وإعادتها إليه. أنشئ أولاً مجلّدًا لوحدة التحكم: mkdir controllers بعد ذلك، افتح ملفًا في هذا المجلد باسم sharks.js: nano controllers/sharks.js في الجزء العلوي من الملف، سنستورد الوحدة مع النموذج Shark لكي نستطيع استخدامها في منطق وحدة التحكم لدينا. سنستورد أيضًا وحدة المسار path للوصول إلى الأدوات المساعدة التي ستتيح لنا تعيين المسار إلى الاستمارة التي سيدخل فيها المستخدمون معلوماتهم عن أسماك القرش. أضف الدوالّ require التالية إلى بداية الملف: الملف ‎~/node_project/controllers/sharks.js: const path = require('path'); const Shark = require('../models/sharks'); بعد ذلك، سنكتب سلسلة من الدوال التي سنصدّرها مع وحدة التحكم باستخدام اختصارات Node للتصدير exports. ستشمل هذه الدوالّ المهام الثلاثة المتعلقة ببيانات سمك القرش الخاصة بمستخدمنا: إرسال إستمارة إدخال سمك القرش للمستخدمين. إنشاء إدخالٍ لقرش جديد. إعادة عرض أسماك القرش للمستخدمين. من أجل البدء، أنشئ دالةً index لعرض صفحة أسماك القرش مع إستمارة الإدخال. أضف هذه الدّالة أسفل الاستيرادات: ... exports.index = function (req, res) { res.sendFile(path.resolve('views/sharks.html')); }; بعد ذلك أضف، أسفل الدالة index، دالّة بالاسم create لإنشاء إدخال جديد لسمك القرش في المجموعة Sharks: ... exports.create = function (req, res) { var newShark = new Shark(req.body); console.log(req.body); newShark.save(function (err) { if(err) { res.status(400).send('Unable to save shark to database'); } else { res.redirect('/sharks/getshark'); } }); }; سوف تستدعى هذه الدّالة عندما يرسل المستخدم بيانات سمك القرش إلى الإستمارة على الصفحة sharks.html. سننشئ لاحقًا المسار route الخاصّ بهذا الإرسال POST في هذا البرنامج التعليمي عندما ننشئ مسارات التطبيق. باستخدام المحتوى body الخاصّ بالطلب POST، ستنشئ الدالة create كائن ملفّ سمك القرش الجديد، الذي يأخذ هنا الاسم newShark، اعتمادًا على النموذج Shark الذي استوردناه. ولقد أضفنا التابع console.log من أجل إظهار الإدخال الخاصّ بسمك القرش إلى الطرفية من أجل التحقق من أن طريقة POST تعمل على النحو المنشود، ولكن تبقى لك الحرّية في تجاوز هذا الأمر إذا كنت تفضل ذلك. باستخدام الكائنnewShark ، ستستدعي الدالة create بعد ذلك تابع Mongoose المسمّى model.save()‎ من أجل إنشاء ملفّ سمك قرش جديد اعتمادًا على المفاتيح التي حدّدتها في النموذج Shark. تتبع دالّة ردّ النداء هذه نموذج ردّ النداء المعياري في Node وهو: callback(error, results)‎. في حالة وجود خطأ، نرسل رسالة تُبلغ المستخدمين عن الخطأ، وفي حالة نجاح العمليّة، نستخدم التابع res.redirect()‎ لتوجيه المستخدمين إلى نقطة النهاية التي ستعيد معلومات القرش إليهم على المتصفح. في الختام، ستعرض الدّالة list محتويات المجموعة مرة أخرى للمستخدم. أضف الشيفرة التالية أسفل الدالة create: ... exports.list = function (req, res) { Shark.find({}).exec(function (err, sharks) { if (err) { return res.send(500, err); } res.render('getshark', { sharks: sharks }); }); }; تستخدم هذه الدالّة النموذج Shark بالاعتماد على التابع ()model.find الخاصّ بMongoose من أجل إرجاع معلومات أسماك القرش التي أُدخِلت في المجموعة sharks. يتمّ هذا الأمر عبر إرجاع كائن الاستعلام query object، ويمثل في هذه الحالة جميع الإدخالات في المجموعة sharks، باستخدام الدالّة ()exec الخاصّة ب Mongoose. في حالة وجود خطأ، سترسل دالّة رد النّداء خطأً 500. سوف يُعرض كائن الاستعلام الذي أرجعته الدّالة مع المجموعة sharks في الصفحة getshark التي سننشئها في الخطوة التالية باستخدام لغة القوالب EJS. سيبدو الملف النهائي ‎~/node_project/controllers/sharks.js على هذا النحو: const path = require('path'); const Shark = require('../models/sharks'); exports.index = function (req, res) { res.sendFile(path.resolve('views/sharks.html')); }; exports.create = function (req, res) { var newShark = new Shark(req.body); console.log(req.body); newShark.save(function (err) { if(err) { res.status(400).send('Unable to save shark to database'); } else { res.redirect('/sharks/getshark'); } }); }; exports.list = function (req, res) { Shark.find({}).exec(function (err, sharks) { if (err) { return res.send(500, err); } res.render('getshark', { sharks: sharks }); }); }; ضع في اعتبارك أنه رغم أننا لا نستخدم الدّوال السهمية هنا، فقد ترغب في تضمينها أثناء تكرارك لهذه الشيفرة في عمليتك التطويرية. احفظ الملف وأغلقه عند الانتهاء من التحرير. قبل الانتقال إلى الخطوة التالية، يمكنك تنفيذ الأمر tree مرة أخرى من المجلّد nodeproject لعرض بنية المشروع في هذه المرحلة. هذه المرة، ومن أجل الإيجاز، سنطلب من tree استبعاد المجلّد nodemodules عبر الراية ‎-I: tree -I node_modules مع الإضافات التي أجريتها، ستبدو بنية مشروعك على النحو التالي: ├── Dockerfile ├── README.md ├── app.js ├── controllers │ └── sharks.js ├── db.js ├── models │ └── sharks.js ├── package-lock.json ├── package.json └── views ├── css │ └── styles.css ├── index.html └── sharks.html الآن بعد أن أصبحت لديك وحدة تحكم لتوجيه كيفية حفظ مدخلات المستخدم وإعادتها إليه، يمكنك الانتقال إلى إنشاء طرق العرض التي ستبلور منطق وحدة التحكم الخاصة بك. الخطوة الخامسة: استخدام برمجيات EJS و Express الوسيطة لجمع البيانات وعرضها من أجل تمكين التطبيق من العمل ببيانات المستخدم، سنقوم بأمرين مهمّين: سنضمّن أولاً دالة وسيطة Express تسمى ()urlencoded، والتي ستمكن تطبيقنا من تحليل بيانات المستخدم المُدخلَة. ثم سنضيف ثانيا علامات القوالب (template tags) إلى واجهة العرض views لتمكين التفاعل الديناميكي مع بيانات المستخدم في الشيفرة. من أجل استخدام الدالة ()urlencoded الخاصة بـ Express، افتح أولاً الملف app.js: nano app.js أضف السطر التالي فوق الدالة ()express.static في الملف ‎~/node_project/app.js: ... app.use(express.urlencoded({ extended: true })); app.use(express.static(path)); ... ستمكّن إضافة هذه الدّالة الوصول إلى بيانات POST المحلّلة (parsed) في استمارة معلومات سمك القرش. نحدّد القيمة true في الخيار extended لإتاحة قدر أكبر من المرونة في نوع البيانات التي سيحللها التطبيق (بما في ذلك الكائنات مثل الكائنات المتداخلة). يرجى الاطلاع على توثيق الدّالة لمزيد من المعلومات حول الخيارات. احفظ الملف وأغلقه عند الانتهاء من التحرير. بعد ذلك، سنضيف إمكانية استخدام القالب إلى واجهات العرض. ثبّت أولاً حزمة ejs باستخدام instaill npm: npm install ejs بعد ذلك ، افتح ملف sharks.html في المجلد views: nano views/sharks.html لقد ألقينا نظرة من قبل في الخطوة الثالثة على هذه الصفحة لتحديد كيفية كتابة المخطط والنموذج Mongoose: الآن ، بدلاً من وجود تنسيق بعمودين، سنضيف عمودًا ثالثًا يحتوي على استمارة يمكن للمستخدمين إدخال معلومات حول أسماك القرش فيها. كخطوة أولى، عدّل أبعاد الأعمدة الموجودة إلى 4 لإنشاء ثلاثة أعمدة متساوية الحجم. لاحظ أنك ستحتاج إلى إجراء هذا التعديل على السطرين اللذين يقرآن حاليًا <div class="col-lg-6"‎>. سيصبح كلاهما <div class="col-lg-4"‎>: الملف ‎~/node_project/views/sharks.html: ... <div class="container"> <div class="row"> <div class="col-lg-4"> <p> <div class="caption">Some sharks are known to be dangerous to humans, though many more are not. The sawshark, for example, is not considered a threat to humans. </div> <img src="https://assets.digitalocean.com/articles/docker_node_image/sawshark.jpg" alt="Sawshark"> </p> </div> <div class="col-lg-4"> <p> <div class="caption">Other sharks are known to be friendly and welcoming!</div> <img src="https://assets.digitalocean.com/articles/docker_node_image/sammy.png" alt="Sammy the Shark"> </p> </div> </div> </div> </html> للاطلاع على توضيح أكثر لنظام الشبكة في Bootstrap، بما في ذلك تخطيطات الصفوف والأعمدة، يرجى الاطلاع على هذه المقدمة إلى إطار Bootstrap. بعد ذلك، أضف عمودًا آخر يتضمن نقطة النهاية المحددة للطلب POST مع بيانات المستخدم عن سمك القرش ووسوم القالب EJS التي ستلتقط تلك البيانات. سيكون هذا العمود أسفل وسمي الإغلاق <‎/p> و <‎/div> للعمود السابق وفوق وسوم الإغلاق للصف والحاوية والصفحة HTML. وتوجد وسوم الإغلاق هذه بالفعل في الشّيفرة. اتركها في مكانها وأنت تضيف الشيفرة التالية لإنشاء العمود الجديد: ... </p> <!-- closing p from previous column --> </div> <!-- closing div from previous column --> <div class="col-lg-4"> <p> <form action="/sharks/addshark" method="post"> <div class="caption">Enter Your Shark</div> <input type="text" placeholder="Shark Name" name="name" <%=sharks[i].name; %> <input type="text" placeholder="Shark Character" name="character" <%=sharks[i].character; %> <button type="submit">Submit</button> </form> </p> </div> </div> <!-- closing div for row --> </div> <!-- closing div for container --> </html> <!-- closing html tag --> تضيف هنا في الوسم form نقطة نهاية "‎/sharks/addhark" لبيانات سمك القرش الخاصة بالمستخدم وتحدّد الطريقة POST لإرسالها. كما تحدّد في حقول الإدخال، الحقلين "Shark Name" و"Shark Character" ، وفقًا للنموذج Shark الذي حدّدته سابقًا. لإضافة مُدخلات المستخدم إلى المجموعة sharks، فأنت تستخدم وسوم قالب:EJS (<%=, %>) بناءً على قواعد لغة جافاسكربت لتوجيه مدخلات المستخدم إلى الحقول المناسبة في المستند الذي أنشئ حديثًا. لمزيد من المعلومات حول كائنات جافاسكربت، يرجى مراجعة توثيق جافاسكربت في الموسوعة. ولمعرفة المزيد عن وسوم قالبEJS ، يرجى الاطلاع على التوثيق الرسمي لEJS. ستبدو الحاوية بأكملها مع جميع الأعمدة الثلاثة، بما في ذلك العمود الذي يحتوي على استمارة إدخال سمك القرش، على هذا النحو عند الانتهاء: الملف ‎~/node_project/views/sharks.html: ... <div class="container"> <div class="row"> <div class="col-lg-4"> <p> <div class="caption">Some sharks are known to be dangerous to humans, though many more are not. The sawshark, for example, is not considered a threat to humans. </div> <img src="https://assets.digitalocean.com/articles/docker_node_image/sawshark.jpg" alt="Sawshark"> </p> </div> <div class="col-lg-4"> <p> <div class="caption">Other sharks are known to be friendly and welcoming!</div> <img src="https://assets.digitalocean.com/articles/docker_node_image/sammy.png" alt="Sammy the Shark"> </p> </div> <div class="col-lg-4"> <p> <form action="/sharks/addshark" method="post"> <div class="caption">Enter Your Shark</div> <input type="text" placeholder="Shark Name" name="name" <%=sharks[i].name; %> <input type="text" placeholder="Shark Character" name="character" <%=sharks[i].character; %> <button type="submit">Submit</button> </form> </p> </div> </div> </div> </html> احفظ الملف وأغلقه عند الانتهاء من التحرير. الآن بعد أن أصبح لديك طريقة لجمع مدخلات المستخدم، يمكنك إنشاء نقطة نهاية (endpoint) لعرض أسماك القرش المُعادة وكذلك معلومات السلوك المرتبطة بها. انسخ ملف sharks.html المعدّل حديثًا إلى ملف يسمى getshark.html: cp views/sharks.html views/getshark.html افتح الملف getshark.html: nano views/getshark.html داخل الملف، سنعدّل العمود الذي استخدمناه لإنشاء استمارة إدخال أسماك القرش من خلال استبداله بعمود سيعرض أسماك القرش في المجموعة sharks. مرة أخرى، ستمتدّ الشفرة بين وسمي <p/> و <‎/div> للعمود السابق ووسوم الإغلاق للصف والحاوية والصفحة HTML. احرص على ترك هذه الوسوم في مكانها وأنت تضيف الشيفرة التالية لإنشاء العمود: ... </p> <!-- closing p from previous column --> </div> <!-- closing div from previous column --> <div class="col-lg-4"> <p> <div class="caption">Your Sharks</div> <ul> <% sharks.forEach(function(shark) { %> <p>Name: <%= shark.name %></p> <p>Character: <%= shark.character %></p> <% }); %> </ul> </p> </div> </div> <!-- closing div for row --> </div> <!-- closing div for container --> </html> <!-- closing html tag --> هنا تستخدم وسوم قالب EJS والدالّة forEach()‎ لإخراج كل قيمة في المجموعة sharks ، بما في ذلك معلومات حول أحدث أسماك القرش المضافة. ستبدو الحاوية بأكملها التي تحتوي على جميع الأعمدة الثلاثة، بما في ذلك العمود الذي يتضمن المجموعة sharks، على هذا النحو عند الانتهاء: ... <div class="container"> <div class="row"> <div class="col-lg-4"> <p> <div class="caption">Some sharks are known to be dangerous to humans, though many more are not. The sawshark, for example, is not considered a threat to humans. </div> <img src="https://assets.digitalocean.com/articles/docker_node_image/sawshark.jpg" alt="Sawshark"> </p> </div> <div class="col-lg-4"> <p> <div class="caption">Other sharks are known to be friendly and welcoming!</div> <img src="https://assets.digitalocean.com/articles/docker_node_image/sammy.png" alt="Sammy the Shark"> </p> </div> <div class="col-lg-4"> <p> <div class="caption">Your Sharks</div> <ul> <% sharks.forEach(function(shark) { %> <p>Name: <%= shark.name %></p> <p>Character: <%= shark.character %></p> <% }); %> </ul> </p> </div> </div> </div> </html> احفظ الملف وأغلقه عند الانتهاء من التحرير. لكي يستخدم التطبيق القوالب التي أنشأتها، ستحتاج إلى إضافة بضعة أسطر إلى ملفّك app.js. لذا افتحه مرة أخرى: nano app.js فوق المكان الذي أضفت فيه الدالة express.urlencoded()‎، أضف الآن الأسطر التالية: ... app.engine('html', require('ejs').renderFile); app.set('view engine', 'html'); app.use(express.urlencoded({ extended: true })); app.use(express.static(path)); ... تطلب الدّالة app.engine من التطبيق توجيه محرك قوالب EJS إلى ملفات HTML، بينما تحدّد app.set محرك العرض الافتراضي. يجب أن يبدو ملف app.js الآن على النحو التالي: الملف ‎~/node_project/app.js: const express = require('express'); const app = express(); const router = express.Router(); const db = require('./db'); const path = __dirname + '/views/'; const port = 8080; router.use(function (req,res,next) { console.log('/' + req.method); next(); }); router.get('/',function(req,res){ res.sendFile(path + 'index.html'); }); router.get('/sharks',function(req,res){ res.sendFile(path + 'sharks.html'); }); app.engine('html', require('ejs').renderFile); app.set('view engine', 'html'); app.use(express.urlencoded({ extended: true })); app.use(express.static(path)); app.use('/', router); app.listen(port, function () { console.log('Example app listening on port 8080!') }) الآن وبعد أن أنشأت واجهات عرض يمكن أن تعمل ديناميكيًا مع بيانات المستخدم، فقد حان الوقت لإنشاء المسارات لمشروعك من أجل الجمع بين واجهات العرض ومنطق وحدة التحكم الذي تعمل به. الخطوة السادسة: إنشاء المسارات (routes) الخطوة الأخيرة في الجمع بين مكونات التطبيق هي إنشاء المسارات. سنحاول فصل المسارات حسب وظيفتها، بما في ذلك المسار إلى صفحة الهبوط لتطبيقنا ومسار آخر إلى صفحة أسماك القرش. سيكون المسار sharks هو المكان الذي ندمج فيه منطق وحدة التحكم مع واجهات العرض التي أنشأناها في الخطوة السابقة. أنشئ في البداية مجلّدًا باسم routes: mkdir routes بعد ذلك، أنشئ ملفًا يسمى index.js في هذا المجلّد وافتحه: nano routes/index.js سيستورد هذا الملف في البداية كائنات express وrouter وpath، مما يتيح لنا تحديد المسارات التي نريد تصديرها باستخدام الكائن router، وهذا يتيح بدوره العمل ديناميكيًا بمسارات الملفات (file paths). أضف الشيفرة التالية في أعلى الملف: الملف ‎~/node_project/routes/index.js: const express = require('express'); const router = express.Router(); const path = require('path'); بعد ذلك، أضف الدّالة router.use التالية، والتي تُحمّل الدالّة الوسيطة التي ستسجل طلبات الموجه وتمررها إلى مسار التطبيق: ... router.use (function (req,res,next) { console.log('/' + req.method); next(); }); ستُوجَّه الطلبات الخاصّة بجذر التطبيق إلى هنا في البداية، ومن ثم سيُوجّه المستخدمون إلى صفحة الهبوط لتطبيقنا، وهو المسار الذي سنحدّده بعد ذلك. أضف الشيفرة التالية أسفل الدّالة router.use لتحديد المسار نحو صفحة الهبوط: ... router.get('/',function(req,res){ res.sendFile(path.resolve('views/index.html')); }); إن أول ما نريد إرساله إليه المستخدم عندما يزور التطبيق هو صفحة الهبوط index.html الموجودة في المجلّد views. ختامًا، لجعل هذه المسارات قابلة للوصول كوحدات يمكن استيرادها في مكان آخر من التطبيق، أضف عبارة ختامية إلى نهاية الملف لتصدير الكائن router: ... module.exports = router; سوف يبدو الملف النهائي على هذا النحو: const express = require('express'); const router = express.Router(); const path = require('path'); router.use (function (req,res,next) { console.log('/' + req.method); next(); }); router.get('/',function(req,res){ res.sendFile(path.resolve('views/index.html')); }); module.exports = router; احفظ هذا الملف وأغلقه عندما تنتهي من التحرير. بعد ذلك، افتح ملفًا باسم sharks.js لتحديد كيفية استخدام التطبيق لنقاط النهاية وواجهات العرض المختلفة التي أنشأناها للتعامل مع مدخلات المستخدم لأسماك القرش: nano routes/sharks.js في الجزء العلوي من الملف، استورد الكائنات epress وrouter: const express = require('express'); const router = express.Router(); بعد ذلك، استورد وحدة نمطية تسمى shark لتتيح لك العمل بالدّوال المصدرة التي حددتها في وحدة التحكم: const express = require('express'); const router = express.Router(); const shark = require('../controllers/sharks'); يمكنك الآن إنشاء مسارات باستخدام الدّوال index وcreate وlist التي حدّدتها في ملف وحدة التحكم sharks. سيُربط كل مسار بطريقة HTTP المناسبة والتي تكون إما GET في حالة عرض صفحة الهبوط لمعلومات أسماك القرش الرئيسية وإعادة قائمة أسماك القرش إلى المستخدم، وإما POST في حالة إنشاء إدخال جديد لسمك قرش: ... router.get('/', function(req, res){ shark.index(req,res); }); router.post('/addshark', function(req, res) { shark.create(req,res); }); router.get('/getshark', function(req, res) { shark.list(req,res); }); يستخدم كل مسار الدّالة المرتبطة به في controllers/sharks.js، لأننا جعلنا هذه الوحدة قابلة للوصول عن طريق استيرادها في الجزء العلوي من هذا الملف. ختامًا، أغلق الملف بربط هذه المسارات بالكائن routes وتصديرها: ... module.exports = router; سيبدو الملف النهائي على هذا النحو: const express = require('express'); const router = express.Router(); const shark = require('../controllers/sharks'); router.get('/', function(req, res){ shark.index(req,res); }); router.post('/addshark', function(req, res) { shark.create(req,res); }); router.get('/getshark', function(req, res) { shark.list(req,res); }); module.exports = router; احفظ الملف وأغلقه عند الانتهاء من التحرير. ستكون الخطوة الأخيرة في جعل هذه المسارات متاحةً للتطبيق هي إضافتها إلى app.js. افتح هذا الملف مرة أخرى: nano app.js أسفل الثابتة db، أضف الاستيراد التالي لمساراتك: ... const db = require('./db'); const sharks = require('./routes/sharks'); بعد ذلك، عوّض الدّالة app.use التي تُركّب الكائن router بالسطر التالي الذي سيركّب وحدة التوجيه sharks: ... app.use(express.static(path)); app.use('/sharks', sharks); app.listen(port, function () { console.log("Example app listening on port 8080!") }) تستطيع الآن حذف المسارات التي حدّدتها مسبقًا في هذا الملف، نظرًا لأنك تستورد مسارات تطبيقك باستخدام وحدة التوجيه sharks. ستبدو النسخة النهائية للملف app.js على النحو التالي: const express = require('express'); const app = express(); const router = express.Router(); const db = require('./db'); const sharks = require('./routes/sharks'); const path = __dirname + '/views/'; const port = 8080; app.engine('html', require('ejs').renderFile); app.set('view engine', 'html'); app.use(express.urlencoded({ extended: true })); app.use(express.static(path)); app.use('/sharks', sharks); app.listen(port, function () { console.log('Example app listening on port 8080!') }) احفظ الملف وأغلقه عند الانتهاء من التحرير. يمكنك الآن إعادة تنفيذ الأمر tree مرة أخرى لرؤية البنية النهائية لمشروعك: tree -I node_modules ستبدو بنية مشروعك الآن على هذا النحو: ├── Dockerfile ├── README.md ├── app.js ├── controllers │ └── sharks.js ├── db.js ├── models │ └── sharks.js ├── package-lock.json ├── package.json ├── routes │ ├── index.js │ └── sharks.js └── views ├── css │ └── styles.css ├── getshark.html ├── index.html └── sharks.html بإنشائك لجميع مكونات تطبيقك ووضعها في المكان المناسب، فأنت الآن مستعدّ لإضافة سمك قرش للاختبار إلى قاعدة بياناتك! إذا تتبعت درس إعداد الخادم الأولي في المتطلبات الأساسية، فستحتاج إلى تعديل جدار الحماية الخاص بك، لأنه لا يسمح حاليًا سوى بحركة المرور SSH. لذا نفّذ هذا الأمر من أجل السماح بحركة المرور إلى منفذ 8080: sudo ufw allow 8080 شغّل التطبيق: node app.js بعد ذلك، انتقل بمتصفّحك إلى http://yourserverip:8080. ستظهر لك صفحة الهبوط التالية: انقر على زر الحصول على معلومات القرش. ستظهر لك صفحة المعلومات التالية، مع استمارة إدخال سمك القرش المضافة: أضف في الاستمارة، سمكة قرش من اختيارك. لأغراض هذا العرض التوضيحي، سنضيف Megalodon Shark إلى حقل اسم القرش، وAncient إلى حقل سلوك القرش: انقر على زر الإرسال. ستظهر لك صفحة بها معلومات القرش معروضة لك: سترى أيضًا مخرجاتٍ في وحدة التحكم الخاصة بك تشير إلى أنّ سمك القرش أضيف إلى مجموعتك: Example app listening on port 8080! { name: 'Megalodon Shark', character: 'Ancient' } إذا كنت ترغب في إنشاء إدخال جديد لسمك القرش، فارجع إلى صفحة أسماك القرش وكرر عملية إضافة سمكة قرش. لديك الآن تطبيق لمعلومات سمك القرش يتيح للمستخدمين بإضافة معلومات حول أسماك القرش المفضلة لديهم. خاتمة لقد أنشأت في هذا البرنامج التعليمي تطبيق Node من خلال دمج قاعدة بيانات MongoDB وإعادة كتابة منطق التطبيق باستخدام النظام الهيكلي MVC. ويمكن أن يكون هذا التطبيق بمثابة نقطة انطلاق جيدة لتطبيق CRUD مكتمل. لمزيد من التفصيل حول النظام الهيكلي MVC في سياقات أخرى، يرجى الاطلاع على سلسلة مقالات تطوير Django. لمزيد من المعلومات حول العمل على MongoDB، يرجى الاطلاع على سلسلة المقالات حول MongoDB. ترجمة -وبتصرف- للمقال How To Integrate MongoDB with Your Node Application لصاحبته Kathleen Juell
  12. تتيح منصة Docker للمطورين تحزيم وتنفيذ التطبيقات على شكل حاويات (containers). وتعدّ الحاوية عملية (process) منعزلة تعمل على نظام تشغيل مشترك، وتوفر بديلاً أخفّ من الأجهزة الافتراضية (virtual machines) . رغم أن الحاويات ليست جديدة، إلا أن لها فوائد عديدة، من بينها عزل العمليات وتوحيد البيئة، وتزداد أهميتها نظرًا لتزايد إقبال المطورين على استخدام بنية التطبيقات الموزعة. عند بناء تطبيق وتوسيع نطاقه باستخدام Docker، تكون نقطة البداية عادةً هي إنشاء صورة للتطبيق، والتي يمكنك تنفيذها بعد ذلك في حاوية. تتضمن الصورة شيفرة التطبيق والمكتبات وملفات التكوين ومتغيرات البيئة ووقت التشغيل. ويضمن استخدام صورة ما أن تكون البيئة في حاويتك موحدة ومتضمّنة فقط لما هو ضروري لإنشاء التطبيق وتنفيذه. ستنشئ في هذا الدرس صورة تطبيق لموقع ويب ثابت يستخدم إطار عمل Express و Bootstrap. وستنشئ بعد ذلك حاوية باستخدام تلك الصورة ورفعها إلى Docker Hub للاستخدام المستقبلي. أخيرًا، ستتمكّن من سحب الصورة المخزنة من مستودع Docker Hub الخاص بك وتنشئ حاوية أخرى، مع توضيح كيف يمكنك إعادة إنشاء التطبيق وتوسيع نطاقه. المتطلبات الأساسية سوف تحتاج من أجل متابعة هذه السلسلة إلى العناصر التالية: خادم Ubuntu 18.04 يمكنك إعداده باتباع دليل إعداد الخادم الأولي. تثبيت Docker على الخادم الخاص بك، باتباع الخطوتين 1 و 2 في دليل كيفية تثبيت واستخدام Docker على Ubuntu 18.04. تثبيت Node.js و npm، باتباع هذه الإرشادات تحديدًا التّثبيت باستخدام أرشيف الحزم الشّخصي PPA المدار بواسطة NodeSource. حساب Docker Hub. لإلقاء نظرة عامة حول كيفية إعداده، راجع هذه المقدمة عن بدء استخدام Docker Hub. الخطوة الأولى: تثبيت الاعتماديات لتطبيقك لإنشاء الصورة، ستحتاج أولاً إلى إنشاء ملفات التطبيق الخاصة بك والتي يمكنك بعد ذلك نسخها إلى حاويتك. ستتضمن هذه الملفات المحتوى الثابت للتطبيق والشيفرة والاعتماديات. أنشئ في البداية مجلًدًا لمشروعك في المجلّد الرئيسي للمستخدم غير الجذري. سنستدعي node_project الخاص بنا، لكن يمكنك استبداله بشيء آخر: $ mkdir node_project انتقل إلى هذا المجلّد: $ cd node_project سيكون هذا هو المجلّد الرئيسي للمشروع. أنشئ بعد ذلك ملف [package.json](https://docs.npmjs.com/files/package.json) باعتماديات مشروعك ومعلومات التعريف الأخرى. افتح الملف باستخدام nano أو المحرر المفضل لديك: $ nano package.json أضف المعلومات التالية حول المشروع، بما في ذلك اسم المشروع وصاحبه والترخيص ونقطة الدخول والاعتماديات. تأكد من استبدال معلومات صاحب المشروع باسمك ومعلومات الاتصال الخاصة بك: الملف ‎:~/node_project/package.json { "name": "nodejs-image-demo", "version": "1.0.0", "description": "nodejs image demo", "author": "Sammy the Shark <sammy@example.com>", "license": "MIT", "main": "app.js", "keywords": [ "nodejs", "bootstrap", "express" ], "dependencies": { "express": "^4.16.4" } } يحتوي هذا الملف على اسم المشروع وصاحبه والترخيص الذي تتم مشاركته بموجبه. ينصح npm بجعل اسم المشروع قصيرًا وذا دلالة وصفية مع تجنب التكرار في سجل npm. لقد أدرجنا ترخيص MIT في حقل الترخيص، مما يتيح الاستخدام المجاني لشيفرة التطبيق وتوزيعه. بالإضافة إلى ذلك، يحدد الملف العناصر التالية: "main": نقطة دخول التطبيق app.js، ستقوم بإنشاء هذا الملف بعد ذلك. "dependencies": اعتماديات المشروع، في هذه الحالة، Express إصدار 4.16.4 أو أعلى منه. ورغم أن هذا الملف لا يحتوي على مستودعٍ، فيمكنك إضافته باتباع هذه الإرشادات حول إضافة مستودع إلى ملف package.json الخاص بك. وتعدّ هذه إضافةً جيدةً إذا كنت تقوم بإصدار تطبيقك. احفظ الملف وأغلقه عندما تنتهي من إجراء التعديلات. لتثبيت اعتماديات مشروعك، نفّذ الأمر التالي: $ npm install سيؤدي ذلك إلى تثبيت الحزم التي أدرجتها في ملف package.json في مجلّد المشروع الخاص بك. يمكننا الآن الانتقال إلى بناء ملفات التطبيق. الخطوة الثانية: إنشاء ملفات التطبيق سنعمل على إنشاء موقع ويب يقدم للمستخدمين معلومات حول أسماك القرش. سيكون لدى لتطبيق نقطة دخول رئيسية و ملفّ app.js ومجلّد views يتضمن الأصول الثابتة للمشروع. ستوفر صفحة الهبوط index.html للمستخدمين بعض المعلومات الأولية ورابطًا نحو صفحة تحتوي على معلومات أكثر تفصيلًا عن أسماك القرش، sharks.html. سننشئ في مجلّد views كلًّا من صفحة الهبوط و الصفحة sharks.html. في البداية، افتح ملفّ app.js في مجلّد المشروع الرئيسي لتحديد مسارات المشروع: $ nano app.js سيعمل الجزء الأول من الملف على إنشاء تطبيق Express وكائنات الموجِّه، وتحديد المجلّد الأساسي والمنفذ كثوابت: الملف ‎:~/node_project/app.js const express = require('express'); const app = express(); const router = express.Router(); const path = __dirname + '/views/'; const port = 8080; تحمّل الداّلة require الوحدة express التي نستخدمها فيما بعد لإنشاء الكائنات app و router. سيؤدي الكائن router وظيفة التوجيه الخاصة بالتطبيق، وعندما نحدد مسارات طرق HTTP، سنضيفها إلى هذا الكائن لتحديد كيفية تعامل التطبيق مع الطلبات. يعين هذا الجزء من الملف أيضًا ثابتتي المسار path والمنفذ port: *path: يحدد المجلّد الأساسي، والذي سيكون المجلّد الفرعي ل views داخل مجلّد المشروع الحالي. port: يطلب من التطبيق الاستماع على المنفذ 8080. بعد ذلك، حدّد مسارات التطبيق باستخدام الكائن router: الملف ‎~/node_project/app.js: router.use(function (req,res,next) { console.log('/' + req.method); next(); }); router.get('/', function(req,res){ res.sendFile(path + 'index.html'); }); router.get('/sharks', function(req,res){ res.sendFile(path + 'sharks.html'); }); تحمّل الدالّة router.use دالّة وسيطة تعمل على تسجيل طلبات الموجه وتمررها إلى مسارات التطبيق. تُحدّد هذه المسارات في الدوالّ الموالية، والتي تنصّ على أن تنفيذ الأمر GET على عنوان URL للمشروع الأساسي يجب أن يُرجع الصفحة index.html، بينما يجب أن يُرجع تنفيذ الأمر GET على المسار sharks/ الصفحة sharks.html. ختامًا، صل بين البرمجية الوسيطة router والأصول الثابتة للتطبيق واجعل التطبيق يستمع على المنفذ 8080: ~/node_project/app.js ... app.use(express.static(path)); app.use('/', router); app.listen(port, function () { console.log('Example app listening on port 8080!') }) سيبدو ملف app.js النهائي كما يلي: const express = require('express'); const app = express(); const router = express.Router(); const path = __dirname + '/views/'; const port = 8080; router.use(function (req,res,next) { console.log('/' + req.method); next(); }); router.get('/', function(req,res){ res.sendFile(path + 'index.html'); }); router.get('/sharks', function(req,res){ res.sendFile(path + 'sharks.html'); }); app.use(express.static(path)); app.use('/', router); app.listen(port, function () { console.log('Example app listening on port 8080!') }) احفظ الملف وأغلقه عند الانتهاء. بعد ذلك، دعنا نضيف بعض المحتوى الثابت إلى التطبيق. ابدأ بإنشاء المجلد views: $ mkdir views افتح ملف صفحة الهبوط index.html: $ nano views/index.html أضف الشيفرة التالية إلى الملف، الذي سيعمل على استيراد Bootstrap وإنشاء مكوّن jumbotron مع رابط إلى صفحة المعلومات المفصّلة sharks.html: الملف ‎~/node_project/views/index.html: <!DOCTYPE html> <html lang="en"> <head> <title>About Sharks</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"> <link href="css/styles.css" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Merriweather:400,700" rel="stylesheet" type="text/css"> </head> <body> <nav class="navbar navbar-dark bg-dark navbar-static-top navbar-expand-md"> <div class="container"> <button type="button" class="navbar-toggler collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false"> <span class="sr-only">Toggle navigation</span> </button> <a class="navbar-brand" href="#">Everything Sharks</a> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <ul class="nav navbar-nav mr-auto"> <li class="active nav-item"><a href="/" class="nav-link">Home</a> </li> <li class="nav-item"><a href="/sharks" class="nav-link">Sharks</a> </li> </ul> </div> </div> </nav> <div class="jumbotron"> <div class="container"> <h1>Want to Learn About Sharks?</h1> <p>Are you ready to learn about sharks?</p> <br> <p><a class="btn btn-primary btn-lg" href="/sharks" role="button">Get Shark Info</a> </p> </div> </div> <div class="container"> <div class="row"> <div class="col-lg-6"> <h3>Not all sharks are alike</h3> <p>Though some are dangerous, sharks generally do not attack humans. Out of the 500 species known to researchers, only 30 have been known to attack humans. </p> </div> <div class="col-lg-6"> <h3>Sharks are ancient</h3> <p>There is evidence to suggest that sharks lived up to 400 million years ago. </p> </div> </div> </div> </body> </html> يتيح شريط التنقل في الأعلى للمستخدمين المرور بين الصفحة الرئيسية وصفحة أسماك القرش. في المكون الفرعي navbar-nav، نستخدم الصنف active في Bootstrap للإشارة إلى الصفحة الحالية للمستخدم. لقد حددنا أيضًا الطرق المؤدية إلى الصفحات الثابتة لدينا، والتي تتوافق مع الطرق التي حددناها في app.js: الملف ‎~/node_project/views/index.html: ... <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <ul class="nav navbar-nav mr-auto"> <li class="active nav-item"><a href="/" class="nav-link">Home</a> </li> <li class="nav-item"><a href="/sharks" class="nav-link">Sharks</a> </li> </ul> </div> ... بالإضافة إلى ذلك، أنشأنا رابطًا إلى صفحة معلومات سمك القرش في زر jumbotron الخاص بنا: الملف ‎~/node_project/views/index.html: ... <div class="jumbotron"> <div class="container"> <h1>Want to Learn About Sharks?</h1> <p>Are you ready to learn about sharks?</p> <br> <p><a class="btn btn-primary btn-lg" href="/sharks" role="button">Get Shark Info</a> </p> </div> </div> ... يوجد أيضًا رابط إلى ورقة أنماط مخصصة في عنصر الترويسة header: الملف ‎~/node_project/views/index.html: ... <link href="css/styles.css" rel="stylesheet"> ... سننشئ ورقة الأنماط هذه في نهاية هذه الخطوة. احفظ الملف وأغلقه عند الانتهاء. يمكننا، من خلال صفحة الهبوط، إنشاء صفحة معلومات أسماك القرش sharks.html، والتي ستوفر للمستخدمين المهتمين مزيدًا من المعلومات حول أسماك القرش. افتح الملف: $ nano views/sharks.html الملف ‎~/node_project/views/sharks.html: <!DOCTYPE html> <html lang="en"> <head> <title>About Sharks</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous"> <link href="css/styles.css" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Merriweather:400,700" rel="stylesheet" type="text/css"> </head> <nav class="navbar navbar-dark bg-dark navbar-static-top navbar-expand-md"> <div class="container"> <button type="button" class="navbar-toggler collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false"> <span class="sr-only">Toggle navigation</span> </button> <a class="navbar-brand" href="/">Everything Sharks</a> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <ul class="nav navbar-nav mr-auto"> <li class="nav-item"><a href="/" class="nav-link">Home</a> </li> <li class="active nav-item"><a href="/sharks" class="nav-link">Sharks</a> </li> </ul> </div> </div> </nav> <div class="jumbotron text-center"> <h1>Shark Info</h1> </div> <div class="container"> <div class="row"> <div class="col-lg-6"> <p> <div class="caption">Some sharks are known to be dangerous to humans, though many more are not. The sawshark, for example, is not considered a threat to humans. </div> <img src="https://assets.digitalocean.com/articles/docker_node_image/sawshark.jpg" alt="Sawshark"> </p> </div> <div class="col-lg-6"> <p> <div class="caption">Other sharks are known to be friendly and welcoming!</div> <img src="https://assets.digitalocean.com/articles/docker_node_image/sammy.png" alt="Sammy the Shark"> </p> </div> </div> </div> </html> لاحظ أنه في هذا الملف، نستخدم مرة أخرى الصنف active للإشارة إلى الصفحة الحالية. احفظ الملف وأغلقه عند الانتهاء. ختامًا ، أنشئ ورقة الأنماط المخصصة CSS التي ربطتها بالصفحتين index.html و sharks.html، وذلك بإنشاء ملف css أولاً في المجلّد views: $ mkdir views/css افتح ورقة الأنماط: nano views/css/styles.css أضف الشيفرة التالية التي ستحدد اللون والخط المطلوب لصفحاتنا: الملف ‎~/node_project/views/css/styles.css: .navbar { margin-bottom: 0; } body { background: #020A1B; color: #ffffff; font-family: 'Merriweather', sans-serif; } h1, h2 { font-weight: bold; } p { font-size: 16px; color: #ffffff; } .jumbotron { background: #0048CD; color: white; text-align: center; } .jumbotron p { color: white; font-size: 26px; } .btn-primary { color: #fff; text-color: #000000; border-color: white; margin-bottom: 5px; } img, video, audio { margin-top: 20px; max-width: 80%; } div.caption: { float: left; clear: both; } بالإضافة إلى تعيين الخط واللون، يحدّد هذا الملف أيضًا حجم الصور بتحديد عرض أقصى يبلغ 80٪. هذا سيمنعها من شغل مساحة في الصفحة أكبر مما نريد. احفظ الملف وأغلقه عند الانتهاء. بعد إنشاء ملفات التطبيق وتثبيت اعتماديات المشروع، تكون جاهزًا لتشغيل التطبيق. إذا كنت قد تابعت دليل إعداد الخادم الأولي المذكور في المتطلبات الأساسية، فسيكون لديك جدار حماية نشط يسمح فقط بحركة مرور SSH. للسماح بحركة المرور عبر المنفذ 8080، نفّذ الأمر التالي : $ sudo ufw allow 8080 لتشغيل التطبيق، تأكد من وجودك في المجلد الرئيسي للمشروع: $ cd ~/node_project ابدأ تشغيل التطبيق باستخدام node app.js: $ node app.js انتقل بمتصفّحك إلى http://your_server_ip:8080. وستظهر لك صفحة الهبوط التالية: انقر على زر الحصول على معلومات القرش Get shark info. وستظهر لك صفحة المعلومات التالية: لديك الآن تطبيق قيد التشغيل. عندما تكون جاهزًا، أوقف الخادم بكتابة CTRL+C. يمكننا الآن الانتقال إلى إنشاء ملف Dockerfile الذي سيتيح لنا إعادة إنشاء هذا التطبيق وتوسيع نطاقه حسب الرغبة. الخطوة الثالثة: كتابة ملف Dockerfile يحدد ملف Dockerfile الخاص بك ما سيتم تضمينه في حاوية التطبيق عند تنفيذه. ويتيح لك استخدام هذا الملفّ تحديد بيئة الحاوية وتجنب التناقضات مع الاعتماديات أو إصدارات وقت التشغيل. باتباع هذه الإرشادات حول إنشاء حاويات محسّنة، سنجعل صورة التطبيق أكثر فاعلية قدر الإمكان عن طريق تقليل عدد طبقات الصورة وتحديد وظيفة الصورة في غرض واحد هو إعادة إنشاء ملفات التطبيق والمحتوى الثابت. في المجلّد الرئيسي لمشروعك، أنشئ الملف Dockerfile: $ nano Dockerfile تُنشَأ صور Docker باستخدام سلسلة متتالية من الصور ذات الطبقات التي تعتمد على بعضها بعضًا. ستكون خطوتنا الأولى هي إضافة الصورة الأساسية للتطبيق والتي ستشكل نقطة انطلاق إنشاء التطبيق. لنستخدم الصورة node:10-alpine. ويمكننا الحصول على الصورة alpine من مشروع Alpine Linux لكي تساعدنا في تقليل حجم الصور لدينا. أضف تعليمات FROM التالية لتعيين الصورة الأساسية للتطبيق: الملف ‎~/node_project/Dockerfile: FROM node:10-alpine تتضمن هذه الصورة Node.js و npm. ويجب أن يبتدئ كل ملفّ Dockerfile بتعليمات FROM. بشكل افتراضي، تشتمل صورة Docker Node على مستخدم Node غير جذري يمكنك استخدامه لتجنب تشغيل حاوية التطبيق باستخدام الحساب الجذري. إنها ممارسة أمان موصى بها لتجنب تشغيل الحاويات باستخدام root وتقييد الصلاحيات داخل الحاوية في تلك المطلوبة فقط لتنفيذ عملياتها. لذلك، سوف نستخدم المجلّد الرئيسي لمستخدم Node كمجلّد العمل لتطبيقنا ونعيّنه كمستخدم داخل الحاوية. لمزيد من المعلومات حول أفضل الممارسات عند العمل مع صورة Docker Node، راجع دليل أفضل الممارسات هذا إذا كانت لغتك الانجليزية جيدة. لضبط الأذونات على شيفرة تطبيقنا في الحاوية، دعنا ننشئ المجلّد الفرعي node_modules في ‎/home/node رفقة المجلّد app. سيضمن إنشاء هذه المجلّدات أنها تتوفّر على الأذونات التي نريدها، والتي ستكون مهمة عندما ننشئ وحدات node محلية في الحاوية باستخدام npm install. بالإضافة إلى إنشاء هذه المجلّدات، سنعطي ملكيتها للمستخدم node: -الملف ‎~/node_project/Dockerfile: ... RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app عيّن بعد ذلك مجلّد العمل للتطبيق على ‎/home/node/app: الملف ‎~/node_project/Dockerfile : ... WORKDIR /home/node/app إذا لم يتم تعيين مجلّد عمل WORKDIR، فسوف يُنشئ Docker واحدًا افتراضيًا، لذلك من الجيد تعيينه بشكل صريح. بعد ذلك، انسخ ملفات package.json و package-lock.json (بالنسبة لملفات الإصدار الخامس npm فما فوق): الملف ‎~/node_project/Dockerfile: ... COPY package*.json ./ تتيح إضافة تعليمة COPY قبل تنفيذ npm install تثبيت أو نسخ شيفرة التطبيق الاستفادة من آلية التخزين المؤقت لـ Docker. في كل مرحلة من مراحل البناء، سيقوم Docker بالتحقق مما إذا كان يحتوي على طبقة تم تخزينها في ذاكرة التخزين المؤقت لتلك التعليمات المحددة. إذا غيّرنا الحزمة package.json، فسيُعاد بناء هذه الطبقة، لكن إذا لم نفعل ذلك، فسوف تسمح هذا التعليمة لـ Docker باستخدام طبقة الصورة الحالية وتخطي إعادة تثبيت وحدات node. للتأكد من أن جميع ملفات التطبيق مملوكة للمستخدم node غير الجذري، بما في ذلك محتويات المجلّد node_modules، حوّل المستخدم إلى node قبل تنفيذ npm install: الملف ‎-/node_project/Dockerfile: ... USER node بعد نسخ اعتماديات المشروع وتبديل مستخدمنا، يمكننا تنفيذ install npm: الملف ‎~/node_project/Dockerfile: ... RUN npm install بعد ذلك، انسخ شيفرة تطبيقك مع الأذونات المناسبة إلى مجلّد التطبيق على الحاوية: الملف ‎~/node_project/Dockerfile: ... COPY --chown=node:node . . سيضمن هذا أن ملفات التطبيق مملوكة من قبل مستخدم node غير الجذري. أخيرًا، اعرض المنفذ 8080 على الحاوية وابدأ تشغيل التطبيق: الملف ‎~/node_project/Dockerfile: ... EXPOSE 8080 CMD [ "node", "app.js" ] لا ينشر الأمر EXPOSE المنفذَ، ولكنه يعمل بدلاً من ذلك كوسيلة لتوثيق المنافذ على الحاوية التي سيتم نشرها في وقت التشغيل. يقوم CMD بتنفيذ الأمر لبدء التطبيق، في هذه الحالة، node app.js. لاحظ أنه يجب أن يكون هناك تعليمة CMD واحدة فقط في كل Dockerfile. إذا ضمّنت أكثر من واحدة، فستُفعّل الأخيرة فقط. هناك العديد من الأشياء التي يمكنك القيام بها باستخدام Dockerfile. للحصول على قائمة كاملة بالتعليمات، يرجى الرجوع إلى التوثيق المرجعي لملفات Dockerfile. يكون الملفّ Dockerfile الكامل على هذا النحو: الملف ‎~/node_project/Dockerfile: FROM node:10-alpine RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app WORKDIR /home/node/app COPY package*.json ./ USER node RUN npm install COPY --chown=node:node . . EXPOSE 8080 CMD [ "node", "app.js" ] احفظ الملف وأغلقه عند الانتهاء من التحرير. قبل بناء صورة التطبيق، دعنا نضيف ملف ‎.dockerignore ونظرًا لأنه يعمل بطريقة مشابهة لملف .gitignore، فإن ملف ‎.dockerignore يحدّد ماهي الملفات والمجلّدات الموجودة في مجلّد مشروعك والتي لا ينبغي نسخها إلى حاويتك. افتح الملف dockerignore.: $ nano .dockerignore أضف داخل الملف وحدات node المحلية وسجلات npm وملف Dockerfile وملف dockerignore.: الملف ‎~/node_project/.dockerignore: node_modules npm-debug.log Dockerfile .dockerignore إذا كنت تعمل باستخدام Git، فستحتاج أيضًا إلى إضافة مجلّد git. وملف gitignore. احفظ الملف وأغلقه عند الانتهاء. أنت الآن جاهز لإنشاء صورة التطبيق باستخدام الأمر docker build. سيسمح لك استخدام الراية t- مع docker build بتعليم الصورة باسم سهل التذكّر. ونظرًا لأننا سنقوم بنقل الصورة إلى Docker Hub، فلنضمّن اسم مستخدم Docker Hub الخاص بنا في العلامة. سنعلّم الصورة باستخدام nodejs-image-demo، ولكن لا تتردد في استبدالها باسم تختاره. تذكر أيضًا استبدال your_dockerhub_username باسم مستخدم Docker Hub الخاص بك: $ docker build -t your_dockerhub_username/nodejs-image-demo . تحدّد النقطة . أن موضع البناء هو المجلّد الحالي. سوف يستغرق الأمر دقيقة أو دقيقتين لبناء الصورة. بمجرد اكتمال الأمر، يمكنك التحقق من صورك: $ docker images سيظهر لك في المخرجات ما يلي: output REPOSITORY TAG IMAGE ID CREATED SIZE your_dockerhub_username/nodejs-image-demo latest 1c723fb2ef12 8 seconds ago 73MB node 10-alpine f09e7c96b6de 3 weeks ago 70.7MB أصبح الآن من الممكن إنشاء حاوية بهذه الصورة باستخدام docker run. وسنضمّن ثلاث علامات مع هذا الأمر: ‎-p : تنشر هذه الراية المنفذ على الحاوية وتقدّمه كمنفذ على المضيف host. سنستخدم المنفذ 80 على المضيف، ولكن يجب ألا تتردد في تعديل هذا عند الضرورة إذا كان لديك عملية أخرى تعمل على هذا المنفذ. لمزيد من المعلومات حول كيفية عمل هذا، راجع هذه النقاش في توثيقات Docker حول ربط المنافذ. ‎-d : تشغّل هذه العلامة الحاوية في الخلفية. ‎-name : هذا يتيح لنا إعطاء الحاوية اسمًا سهل التذكر. نفّذ الأمر التالي لبناء الحاوية: docker run --name nodejs-image-demo -p 80:8080 -d your_dockerhub_username/nodejs-image-demo بمجرد تشغيل حاويتك، يمكنك فحص قائمة الحاويات قيد التشغيل باستخدام docker ps: $ docker ps سيظهر لك الإخراج التالي: Output CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES e50ad27074a7 your_dockerhub_username/nodejs-image-demo "node app.js" 8 seconds ago Up 7 seconds 0.0.0.0:80->8080/tcp nodejs-image-demo بعد تشغيل حاويتك، يمكنك الآن الولوج إلى تطبيقك بالانتقال بمتصفحك إلى http://your_server_ip. ستظهر لك صفحتك الهبوط مرة أخرى: الآن وبعد بناء صورة لتطبيقك، يمكنك رفعها إلى Docker Hub للاستخدام المستقبلي. الخطوة الرابعة: استخدام مستودع للعمل بالصور برفعك لصورة التطبيق الخاصة بك إلى سجلّ مثل Docker Hub، فأنت تجعلها متاحة للاستخدام المستقبلي عندما تكون بصدد بناء حاوياتك وتوسيع نطاقها. سنوضح كيف يعمل هذا عن طريق رفع صورة التطبيق إلى مستودع ثم استخدام الصورة لإعادة إنشاء حاويتنا. الخطوة الأولى لرفع الصورة هي تسجيل الدخول إلى حساب Docker Hub الذي أنشأته في المتطلبات الأساسية: $ docker login -u your_dockerhub_username أدخل كلمة مرور حساب Docker Hub، وسيؤدي التسجيل بهذه الطريقة إلى إنشاء ملف ‎~/.docker/config.json في المجلّد الرئيسي لمستخدمك بالبيانات الاعتمادية لDocker Hub. يمكنك الآن رفع صورة التطبيق إلى Docker Hub باستخدام العلامة التي أنشأتها مسبقًا، your_dockerhub_username/nodejs-image-demo $ docker push your_dockerhub_username/nodejs-image-demo دعنا نختبر الآن أهمّية سجلّ الصور بتدمير حاوية التطبيق الحالية والصورة وإعادة بنائها باستخدام الصورة التي في مستودعنا. استعرض أولاً حاوياتك التي توجد قيد التشغيل: $ docker ps سيظهر لك الإخراج التالي: Output CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES e50ad27074a7 your_dockerhub_username/nodejs-image-demo "node app.js" 3 minutes ago Up 3 minutes 0.0.0.0:80->8080/tcp nodejs-image-demo باستخدام CONTAINER ID الظاهر في المخرجات، أوقف حاوية التطبيق قيد التشغيل. تأكد من استبدال المعرّف أدناه بمعرّف الحاوية CONTAINER ID: $ docker stop e50ad27074a7 اعرض قائمة صورك باستخدام الراية ‎-a : $ docker images -a سيظهر لك الإخراج التالي مع اسم صورتك، your_dockerhub_username/nodejs-image-demo، إلى جانب صورة node وباقي الصور الأخرى الخاصة ببنائك: Output REPOSITORY TAG IMAGE ID CREATED SIZE your_dockerhub_username/nodejs-image-demo latest 1c723fb2ef12 7 minutes ago 73MB <none> <none> 2e3267d9ac02 4 minutes ago 72.9MB <none> <none> 8352b41730b9 4 minutes ago 73MB <none> <none> 5d58b92823cb 4 minutes ago 73MB <none> <none> 3f1e35d7062a 4 minutes ago 73MB <none> <none> 02176311e4d0 4 minutes ago 73MB <none> <none> 8e84b33edcda 4 minutes ago 70.7MB <none> <none> 6a5ed70f86f2 4 minutes ago 70.7MB <none> <none> 776b2637d3c1 4 minutes ago 70.7MB node 10-alpine f09e7c96b6de 3 weeks ago 70.7MB احذف الحاوية المتوقفة وجميع الصور، بما في ذلك الصور غير المستخدمة أو المعلّقة، باستخدام الأمر التالي: $ docker system prune -a اكتب y في المحثّ عند الإخراج لتأكيد رغبتك في إزالة الحاوية والصور المتوقفة. وينبغي التوضيح أن هذا سيؤدي أيضًا إلى حذف ذاكرة التخزين المؤقت للبناء. لقد حذفت الآن كلًا من الحاوية التي تشغّل صورة التطبيق والصورة نفسها. لمزيد من المعلومات حول إزالة حاويات Docker والصور، يرجى الاطلاع على كيفية التعامل مع الحاويات. بعد حذف كل الصور والحاويات الخاصة بك ، يمكنك الآن سحب صورة التطبيق من Docker Hub: $ docker pull your_dockerhub_username/nodejs-image-demo اعرض قائمة صورك مرة أخرى: $ docker images ستظهر لك صورة تطبيقك: Output REPOSITORY TAG IMAGE ID CREATED SIZE your_dockerhub_username/nodejs-image-demo latest 1c723fb2ef12 11 minutes ago 73MB يمكنك الآن إعادة بناء حاويتك باستخدام الأمر التالي من الخطوة 3: docker run --name nodejs-image-demo -p 80:8080 -d your_dockerhub_username/nodejs-image-demo اعرض قائمة الحاويات قيد التشغيل: $ docker ps Output CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES f6bc2f50dff6 your_dockerhub_username/nodejs-image-demo "node app.js" 4 seconds ago Up 3 seconds 0.0.0.0:80->8080/tcp nodejs-image-demo تصفّح http://your_server_ip مرة أخرى لعرض التطبيق قيد التشغيل. الخلاصة في هذا الدرس، عملت على بناء تطبيق ويب ثابت باستخدام Express و Bootstrap، بالإضافة إلى صورة Docker لهذا التطبيق. استخدمت هذه الصورة لإنشاء حاوية ورفعت الصورة إلى Docker Hub. بعد ذلك، تمكنت من تدمير صورتك وحاوياتك وإعادة إنشائها باستخدام مستودع Docker Hub. ترجمة -وبتصرف- للمقال How To Build a Node.js Application with Docker لصاحبته Kathleen Juell
  13. سيكون من الجيد أن نحدّ من قدرة اللاعب على التحرك للأعلى إلّا من خلال القفز عبر الفجوات أو على الصناديق. ليست هذه هي الخيارات الوحيدة المتاحة. لا يزال يتعين علينا معرفة المزيد عن المصاعد والأدراج والسلالم. لذلك، دعنا نُنشئ سُلّمًا! إليك ما سنعمل عليه في هذه المرحلة: إنشاء أول سلّم السماح للاعبين بتسلق السلالم السماح للاعبين بالوقوف والقفز على السلالم إنشاء أول سلّم لنبدأ إنشاء سلّمنا بنسخ صنف الصندوق Box: class Ladder { constructor(sprite, rectangle) { this.sprite = sprite; this.rectangle = rectangle; } animate(state) { if (this.sprite) { this.sprite.x = this.rectangle.x; this.sprite.y = this.rectangle.y; } } } export default Ladder; import Ladder from "./ladder"; game.addObject( new Ladder( new PIXI.Sprite.fromImage( "ladder.png" ), new PIXI.Rectangle( 200, window.innerHeight - 192, 64, 192 ) ) ); game.addObject( new Box( new PIXI.Sprite.fromImage( "box.png" ), new PIXI.Rectangle( 136, window.innerHeight - 191, 64, 64 ) ) ); ستحتاج إلى إنشاء صورة ladder.png للسلم؛ بالنسبة لي، اخترت استخدام شكلٍ بسيطٍ بمقاسات 64x192 بيكسيل. تأكد من إضافة الصورة إلى اللعبة قبل إضافة اللاعب وإلا سيكون الشكل الخاص بالسلم أمام الشكل الخاص اللاعب. ستلاحظ أن اللاعب يصطدم بالسلم، كما لو كان صندوقًا. سنحتاج إلى إضافة بعض الجوالب إلى الصناديق والسلالم حتى يستطيع كشف التصادمات من تحديد ما إذا كانوا ستستمر بالاصطدام مع بعضها بعضًا … if (me.x < you.x + you.width && me.x + me.width > you.x && me.y < you.y + you.height && me.y + me.height > you.y) { if (object.collides && this.velocityY > 0 && you.y >= me.y) { this.velocityY = 0; this.grounded = true; this.jumping = false; return; } if (object.collides && this.velocityY < 0 && you.y <= me.y) { this.velocityY = this.accelerationY; return; } if (object.collides && this.velocityX < 0 && you.x <= me.x) { this.velocityX = 0; return; } if (object.collides && this.velocityX > 0 && you.x >= me.x) { this.velocityX = 0; return; } } نعطي للصندوق و السلم خاصية الاصطدام بحيث يتمكّن اللاعب من التحرّك في تجاهلٍ للاصطدامات بالكائنات التي يجب ألا يصطدم بها اللاعبون. إذا كنا سنسمح بوجود عدة لاعبين في نفس اللعبة أو المستوى، فسنضيف أيضًا خاصية الاصطدام إلى اللاعب. هذا ما لم نكن نرغب في تصادم لاعبين متعددين مع بعضهم بعضًا. السماح للاعبين بتسلق السلالم لكي يصعد اللاعب السلالم، يجب أن نكون قادرين على معرفة ما إذا كان يحاول تسلق أحدها في لحظة ما. يتعين علينا أيضًا تعليق الجاذبية والحركة الجانبية حتى لا يسقط أو ينزلق: class Player { constructor(sprite, rectangle) { this.sprite = sprite; this.rectangle = rectangle; this.velocityX = 0; this.maximumVelocityX = 12; this.accelerationX = 2; this.frictionX = 0.9; this.velocityY = 0; this.maximumVelocityY = 20; this.accelerationY = 5; this.jumpVelocity = -40; this.climbingSpeed = 10; } animate(state) { if (state.keys[37]) { this.velocityX = Math.max( this.velocityX - this.accelerationX, this.maximumVelocityX * -1 ); } if (state.keys[39]) { this.velocityX = Math.min( this.velocityX + this.accelerationX, this.maximumVelocityX ); } this.velocityX *= this.frictionX; this.velocityY = Math.min( this.velocityY + this.accelerationY, this.maximumVelocityY ); var onLadder = false; state.objects.forEach((object) => { if (object === this) { return; } var me = this.rectangle; var you = object.rectangle; if (me.x < you.x + you.width && me.x + me.width > you.x && me.y < you.y + you.height && me.y + me.height > you.y) { if (object.collides && this.velocityY > 0 && you.y >= me.y) { this.velocityY = 0; this.grounded = true; this.jumping = false; return; } if (object.collides && this.velocityY < 0 && you.y <= me.y) { this.velocityY = this.accelerationY; return; } if (object.collides && this.velocityX < 0 && you.x <= me.x) { this.velocityX = 0; return; } if (object.collides && this.velocityX > 0 && you.x >= me.x) { this.velocityX = 0; return; } if (object.constructor.name === "Ladder") { onLadder = true; if (state.keys[38] || state.keys[40]) { this.grounded = false; this.jumping = false; this.climbing = true; this.velocityY = 0; this.velocityX = 0; } if (state.keys[38]) { this.rectangle.y -= this.climbingSpeed; } if (state.keys[40] && me.y + me.height < you.y + you.height) { this.rectangle.y += this.climbingSpeed; } } } }); if (!onLadder) { this.climbing = false; } if (state.keys[38] && this.grounded) { this.velocityY = this.jumpVelocity; this.jumping = true; this.grounded = false; } this.rectangle.x += this.velocityX; if (!this.climbing) { this.rectangle.y += this.velocityY; } this.sprite.x = this.rectangle.x; this.sprite.y = this.rectangle.y; } } export default Player; نبدأ بإضافة خاصية جديدة سنسميها التسلق (climbing). ستكون قيمتها صحيحة "true" عندما يكون اللاعب في وضع تسلق السلم. ننشِئ أيضًا متغيرًا onLadder محلي، حتى نتمكن من معرفة ما إذا كان لا يزال واقفًا على سلم. أثناء كشف التصادمات المعتاد، نلاحظ ما إذا كان الكائن الذي يصطدم به اللاعب هو سلم أم لا. إذا كان الأمر كذلك، وكان السهم مضغوطًا لأعلى، نبدأ في التسلق. تبدأ عملية التسلق فقط إذا كان السهم مضغوطًا لأعلى، ولهذا السبب نحتاج إلى هذا المتغير المحلي. نعيد ضبط سرعة اللاعب والخصائص المتعلقة بالقفز. ونغير كذلك بشكل مباشر مستطيلَ اللاعب. إذا تم الضغط على السهم للأعلى، فإنّنا نرفع اللاعب لأعلى. إذا تم الضغط لأسفل، فإنّنا نحرّك اللاعب لأسفل حتى يصبح الجزء السفلي من اللاعب أدنى من الجزء السفلي للسلم. هذا يجعل السلالم السفلية لا تتيح للاعب أن يتجاهل التصادم مع الأرض. السماح للاعبين بالوقوف على السلالم لا تزال هناك مشكلة في السلالم. في اللحظة التي نصل فيها إلى القمة، نسقط مرة أخرى. لقد عطلنا تصادمات السلالم، لذلك نحتاج إلى طريقة للوقوف في الجزء العلوي منها حتى نتمكن من القفز أو الانتقال إلى المنصة التالية. دعنا ننقل منطق التصادم إلى الصناديق والسلالم: class Box { collides(state) { var collides = false; state.objects.forEach((object) => { if (object.constructor.name !== "Player") { return; } let edges = this.getEdges(this, object); if (!( edges.boxTop > edges.playerBottom || edges.boxRight < edges.playerLeft || edges.boxBottom < edges.playerTop || edges.boxLeft > edges.playerRight )) { collides = true; return; } }); return collides; } getEdges(box, player) { return { "boxLeft" : box.rectangle.x, "boxRight" : box.rectangle.x + box.rectangle.width, "boxTop" : box.rectangle.y, "boxBottom" : box.rectangle.y + box.rectangle.height, "playerLeft" : player.rectangle.x, "playerRight" : player.rectangle.x + player.rectangle.width, "playerTop" : player.rectangle.y, "playerBottom" : player.rectangle.y + player.rectangle.height }; } collidesInDirection(box, player) { let edges = this.getEdges(box, player); let offsetLeft = edges.playerRight - edges.boxLeft; let offsetRight = edges.boxRight - edges.playerLeft; let offsetTop = edges.playerBottom - edges.boxTop; let offsetBottom = edges.boxBottom - edges.playerTop; if (Math.min(offsetLeft, offsetRight, offsetTop, offsetBottom) === offsetTop) { return "↓"; } if (Math.min(offsetLeft, offsetRight, offsetTop, offsetBottom) === offsetBottom) { return "↑"; } if (Math.min(offsetLeft, offsetRight, offsetTop, offsetBottom) === offsetLeft) { return "→"; } if (Math.min(offsetLeft, offsetRight, offsetTop, offsetBottom) === offsetRight) { return "←"; } return "unknown"; } constructor(sprite, rectangle) { this.sprite = sprite; this.rectangle = rectangle; } animate(state) { if (this.sprite) { this.sprite.x = this.rectangle.x; this.sprite.y = this.rectangle.y; } } } export default Box; يستطيع صنف الصندوق Box الآن حساب ما إذا كان سيتصادم مع اللاعب. بمجرد اكتشاف التصادم بين لاعب وصندوق، يمكننا تنفيذ الدالّة التابع collidesInDirection. سنحصل على سهمٍ أنيقٍ UTF-8 يشير إلى الاتجاه الذي كان اللاعب يتحرك فيه قبل حدوث التصادم. أثناء إجراء هذا التغيير، حدث أنّ صنف السلم لا يقوم بأي شيء مختلف عن صنف الصندوق. يحدث مفهوم التسلق في فئة "اللاعب"، وبالتالي فإن فئة "السلم" يمكن أن تكون امتدادًا لفئة "الصندوق": import Box from "./box"; class Ladder extends Box { } export default Ladder; يبدو صنف اللّاعب أكثر أناقةً وتنظيمًا بعد نقل منطق التصادم خارجه: animate(state) { if (state.keys[37]) { this.velocityX = Math.max( this.velocityX - this.accelerationX, this.maximumVelocityX * -1 ); } if (state.keys[39]) { this.velocityX = Math.min( this.velocityX + this.accelerationX, this.maximumVelocityX ); } this.velocityX *= this.frictionX; this.velocityY = Math.min( this.velocityY + this.accelerationY, this.maximumVelocityY ); var onLadder = false; state.objects.forEach((object) => { if (object === this) { return; } if (object.collides(state)) { let type = object.constructor.name; let direction = object.collidesInDirection(object, this); if (type === "Ladder") { onLadder = true; let player = this.rectangle; let ladder = object.rectangle; if (state.keys[38] || state.keys[40]) { this.grounded = false; this.jumping = false; this.climbing = true; this.velocityY = 0; this.velocityX = 0; } if (state.keys[38]) { let limit = ladder.y - this.rectangle.height + 1; this.rectangle.y = Math.max( this.rectangle.y -= this.climbingSpeed, limit ); if (player.y === limit) { this.grounded = true; this.jumping = false; } } if (state.keys[40] && player.y + player.height < ladder.y + ladder.height) { this.rectangle.y += this.climbingSpeed; } return; } if (type === "Box") { if (direction === "↓") { this.velocityY = 0; this.grounded = true; this.jumping = false; } if (direction === "↑") { this.velocityY = this.accelerationY; } if (direction === "←" && this.velocityX < 0) { this.velocityX = 0; } if (direction === "→" && this.velocityX > 0) { this.velocityX = 0; } } } }); if (!onLadder) { this.climbing = false; } if (state.keys[38] && this.grounded && !this.climbing) { this.velocityY = this.jumpVelocity; this.jumping = true; this.grounded = false; } this.rectangle.x += this.velocityX; if (!this.climbing) { this.rectangle.y += this.velocityY; } this.sprite.x = this.rectangle.x; this.sprite.y = this.rectangle.y; } لم يتغير مفهوم السلم على الأغلب، باستثناء أن اللاعب لن يصل أبدًا إلى أعلى بيكسل في أعلى درجات السلم. هكذا نمنع اللاعب من السقوط بمجرد وصوله إلى القمة. هذا يعني أنك ستحتاج إلى جعل بداية سلالمك في البيكسل الأوّل فوق المنصة التي تتوقع أن يتحرّك عليها اللاعب. وهذا يعني أيضًا أنه لا يمكن للاعب القفز من السلالم. يبدو الآن منطق التصادم المتعلّق بالصندوق أنقى وأبسط بكثير! وماهي النتيجة؟ سلالمٌ تعمل بشكل جيد. ختام اللعبة: ماذا بعد؟ لقد علمتني هذه المقالات القليلة الكثير من الأمور، أو لِنَقُل أنّهما أمران بارزان على وجه التحديد. الأول هو أن هناك قدرًا لا حصر له من الأشياء التي يمكننا إضافتها أو تغييرها في ألعابنا. والثاني هو أن هذا النشاط مربحٌ ومتعبٌ في الوقت ذاته. لقد انطلقت من رغبةٍ في معرفة كيفية صنع لعبة منصة بسيطة، وبعدها قمنا معا بتغطية كل شيء تقريبا نحتاجه لبناء تلك اللعبة. لا أريد التوقف هنا، رغم ذلك. أريد أن أستمر في التعلم وصقل مهاراتي. أريد أن أستمر في مشاركة تلك التجارب معك. ترجمة -وبتصرف- للمقال Ladders لصاحبه Christopher Pitt اقرأ أيضًا المقال السابق: الجاذبية مدخل إلى ألعاب المتصفح
  14. سنعمل هذه المرة على بنية الشيفرة لدينا، ونضيف الجاذبية إلى لعبتنا. ويجدر الذكر أننا قمنا بالفعل بمعظم العمل اللازم لتحقيق الجاذبية. إليك ما سنعمل عليه في هذه المرحلة: تنظيف الشيفرة الموجودة لدينا إضافة الجاذبية إلى لعبتنا السماح للاعبين بالقفز تنظيف الشيفرة الموجودة لدينا نحن بحاجة لتنظيف بعض الأشياء! دعنا في البداية نبدّل موضعي x و y إضافةً إلى العرض والارتفاع (بالنسبة للصندوق وللاعب) مع PIXI.Rectangle. إنّ لديهما هذه الخصائص، ولكنهما يتفاعلان أيضًا مع باقي عناصر PIXI بطرق مثيرة للاهتمام. class Box { constructor(sprite, rectangle) { this.sprite = sprite; this.rectangle = rectangle; } animate(state) { if (this.sprite) { this.sprite.x = this.rectangle.x; this.sprite.y = this.rectangle.y; } } } export default Box; class Player { constructor(sprite, rectangle) { this.sprite = sprite; this.rectangle = rectangle; this.velocityX = 0; this.maximumVelocityX = 12; this.accelerationX = 2; this.frictionX = 0.9; } animate(state) { if (state.keys[37]) { this.velocityX = Math.max( this.velocityX - this.accelerationX, this.maximumVelocityX * -1 ); } if (state.keys[39]) { this.velocityX = Math.min( this.velocityX + this.accelerationX, this.maximumVelocityX ); } this.velocityX *= this.frictionX; var move = true; state.objects.forEach((object) => { if (object === this) { return; } var me = this.rectangle; var you = object.rectangle; if (me.x < you.x + you.width && me.x + me.width > you.x && me.y < you.y + you.height && me.height + me.y > you.y) { if (this.velocityX < 0 && you.x <= me.x) { move = false; } if (this.velocityX > 0 && you.x >= me.x) { move = false; } } }); if (move) { this.rectangle.x += this.velocityX; } this.sprite.x = this.rectangle.x; this.sprite.y = this.rectangle.y; } } export default Player; هل لاحظت هذا المقدار من الشيفرة الذي نستطيع حذفه؟ لقد أصبحت عمليات الموازنة طويلة قليلاً، لكن لا شيء يمكن إصلاحه من خلال متغير محليّ واحد أو اثنين. أدركتُ أيضًا أنه يمكننا تغيير قيم x و y الابتدائية من أجل التحريك. بعد ذلك، أرغب في تغليف (encapsulation) الحدث والعارض والمسرح في صنف (class) جديدة خاصّة باللعبة (Game): class Game { constructor() { this.state = { "keys": {}, "clicks": {}, "mouse": {}, "objects": [] }; } get stage() { if (!this._stage) { this._stage = this.newStage(); } return this._stage; } set stage(stage) { this._stage = stage; } newStage() { return new PIXI.Container(); } get renderer() { if (!this._renderer) { this._renderer = this.newRenderer(); } return this._renderer; } set renderer(renderer) { this._renderer = renderer; } newRenderer() { return new PIXI.autoDetectRenderer( window.innerWidth, window.innerHeight, this.newRendererOptions() ); } newRendererOptions() { return { "antialias": true, "autoResize": true, "transparent": true, "resolution": 2 }; } animate() { var caller = () => { requestAnimationFrame(caller); this.state.renderer = this.renderer; this.state.stage = this.stage; this.state.objects.forEach((object) => { object.animate(this.state); }) this.renderer.render(this.stage); }; caller(); return this; } addEventListenerToElement(element) { element.addEventListener("keydown", (event) => { this.state.keys[event.keyCode] = true; }); element.addEventListener("keyup", (event) => { this.state.keys[event.keyCode] = false; }); element.addEventListener("mousedown", (event) => { this.state.clicks[event.which] = { "clientX": event.clientX, "clientY": event.clientY }; }); element.addEventListener("mouseup", (event) => { this.state.clicks[event.which] = false; }); element.addEventListener("mousemove", (event) => { this.state.mouse.clientX = event.clientX; this.state.mouse.clientY = event.clientY; }); return this; } addRendererToElement(element) { element.appendChild(this.renderer.view); return this; } addObject(object) { this.state.objects.push(object); if (object.sprite) { this.stage.addChild(object.sprite); } return this; } } export default Game; لاحِظ دالّتي الجالب والضابط الخاصتين بصنفٍ ES6. إنهما مفيدتان لملء التبعيات الاختيارية حسب الحاجة. يمكننا تجاهل المصيّر والمسرح إذا كنا بحاجة إلى ذلك، ولكن لديهما قيم افتراضية معقولة أيضًا. هذا، على الأغلب، مجرد نقل للشيفرة التي كانت في main.js إلى game.js. الاختلاف الوحيد الملحوظ هو أننا لم نعد نحتاج إلى إضافة الأشكال إلى المسرح بشكل منفصل عن إضافة الكائنات إلى الحالة (state). أنا محتارٌ حول هذا الموضوع. فمن ناحية، يكون الأمر أكثر وضوحًا إذا أضفناها إلى المسرح يدويًا. ومن ناحية أخرى، هل سنضيف أشكالًا (مرتبطة بالكائنات) دون إضافة الكائنات؟ لا أعتقد ذلك. ربما سنعود ونغير ذلك. في الوقت الحالي، تبدو الشيفرة أنظف قليلاً. إذن، كيف تبدو main.js الآن؟ import Game from "./game"; import Player from "./player"; import Box from "./box"; var game = new Game(); game.addObject( new Box( new PIXI.Sprite.fromImage( "box.png" ), new PIXI.Rectangle( window.innerWidth * 1/4, 152, 64, 64 ) ) ); game.addObject( new Player( new PIXI.Sprite.fromImage( "player.png" ), new PIXI.Rectangle( window.innerWidth * 2/4, 200, 64, 64 ) ) ); game.addObject( new Box( new PIXI.Sprite.fromImage( "box.png" ), new PIXI.Rectangle( window.innerWidth * 3/4, 248, 64, 64 ) ) ); game.addEventListenerToElement(window); game.addRendererToElement(document.body); game.animate(); هذا يبدو أفضل بكثير! لدينا سيطرة كافية لتعيين نقاط الانطلاق لكل كائنٍ في اللعبة. ولكن بعد الإطار الأول، تبدأ الدالّة التابع ()animate في العمل. ستبدأ أشياء مثل الجاذبية والتصادمات في التحكم في كيفية تقدم اللعبة. لقد كان الأمر هكذا دائمًا، لذلك يبدو هذا الملف كذلك أيضًا. كان بإمكاننا أن نحصُر الأحداث والعارض في مجموعة صغيرة من العناصر. كما كنا نستطيع إضافة أي عدد من الكائنات إلى اللعبة من هنا. كل شيء آخر مرتبط باللعبة يوجد داخل فئة اللعبة. وكل شيء آخر مرتبط باللاعب أو الصندوق يوجد داخل الفئة الخاصّة به. هكذا تبدو الأمور أنيقة ومنظمة! إضافة الجاذبية إلى لعبتنا أحد الأشياء التي تجعل ألعاب المنصة ممتعةً هو وجود قدرٍ معتدلٍ من الأجسام في محيط اللعب. دعنا نضيف الجدران والأرضية: var game = new Game(); game.addObject( new Box( null, new PIXI.Rectangle( -10, 0, 10, window.innerHeight ) ) ); game.addObject( new Box( null, new PIXI.Rectangle( 0, window.innerHeight, window.innerWidth, 10 ) ) ); game.addObject( new Box( null, new PIXI.Rectangle( window.innerWidth, 0, 10, window.innerHeight ) ) ); game.addObject( new Player( new PIXI.Sprite.fromImage( "player.png" ), new PIXI.Rectangle( window.innerWidth * 2/4, 200, 64, 64 ) ) ); بدلاً من مربعين أحمرين، لدينا ثلاثة مستطيلات رفيعة. إنها تتصادم بنفس الطريقة. الآن، دعنا نرى كيف نضيف هذه الجاذبية: class Player { constructor(sprite, rectangle) { this.sprite = sprite; this.rectangle = rectangle; this.velocityX = 0; this.maximumVelocityX = 12; this.accelerationX = 2; this.frictionX = 0.9; this.velocityY = 0; this.maximumVelocityY = 30; this.accelerationY = 10; this.jumpVelocity = -50; } animate(state) { if (state.keys[37]) { this.velocityX = Math.max( this.velocityX - this.accelerationX, this.maximumVelocityX * -1 ); } if (state.keys[39]) { this.velocityX = Math.min( this.velocityX + this.accelerationX, this.maximumVelocityX ); } this.velocityX *= this.frictionX; this.velocityY = Math.min( this.velocityY + this.accelerationY, this.maximumVelocityY ); state.objects.forEach((object) => { if (object === this) { return; } var me = this.rectangle; var you = object.rectangle; if (me.x < you.x + you.width && me.x + me.width > you.x && me.y < you.y + you.height && me.y + me.height > you.y) { if (this.velocityY > 0 && you.y >= me.y) { this.velocityY = 0; return; } if (this.velocityY < 0 && you.y <= me.y) { this.velocityY = this.accelerationY; return; } if (this.velocityX < 0 && you.x <= me.x) { this.velocityX = 0; return; } if (this.velocityX > 0 && you.x >= me.x) { this.velocityX = 0; return; } } }); this.rectangle.x += this.velocityX; this.rectangle.y += this.velocityY; this.sprite.x = this.rectangle.x; this.sprite.y = this.rectangle.y; } } export default Player; نبدأ بإنشاء مجموعة من الخصائص لتتناسب مع تلك التي أنشأناها لتتبع الحركة الأفقية. لا نحتاج إلى احتكاك عمودي، إذ أنّ هذا المستوى من التفاصيل يُحذَف غالبا من ألعاب المنصّات. علينا أيضًا أن نتتبع التصادمات العمودية والأفقية. عندما يكون التصادم بين اللاعب والمنصة أو الأرضية، فإننا نوقف حركة الهبوط. وعندما يكون مع السقف، فإننا نستبدل الحركة الصعودية بقوة الجاذبية. السماح للاعبين بالقفز القفز هو ببساطة مقاومة للجاذبية لفترة قصيرة: animate(state) { if (state.keys[37]) { this.velocityX = Math.max( this.velocityX - this.accelerationX, this.maximumVelocityX * -1 ); } if (state.keys[39]) { this.velocityX = Math.min( this.velocityX + this.accelerationX, this.maximumVelocityX ); } this.velocityX *= this.frictionX; this.velocityY = Math.min( this.velocityY + this.accelerationY, this.maximumVelocityY ); state.objects.forEach((object) => { if (object === this) { return; } var me = this.rectangle; var you = object.rectangle; if (me.x < you.x + you.width && me.x + me.width > you.x && me.y < you.y + you.height && me.y + me.height > you.y) { if (this.velocityY > 0 && you.y >= me.y) { this.velocityY = 0; this.grounded = true; this.jumping = false; return; } if (this.velocityY < 0 && you.y <= me.y) { this.velocityY = this.accelerationY; return; } if (this.velocityX < 0 && you.x <= me.x) { this.velocityX = 0; return; } if (this.velocityX > 0 && you.x >= me.x) { this.velocityX = 0; return; } } }); if (state.keys[32] && !this.jumping && this.grounded) { this.velocityY = this.jumpVelocity; this.jumping = true; this.grounded = false; } this.rectangle.x += this.velocityX; this.rectangle.y += this.velocityY; this.sprite.x = this.rectangle.x; this.sprite.y = this.rectangle.y; } لقد قمنا هنا بتعيين مفتاح المسافة من أجل حركة القفز. نضيف فحص لوحة المفاتيح بعد فحص التصادم لأننا نريد لاعبنا أن يقفز فقط إذا كان يقف على منصة أو أرضية. يمكنك الآن إنشاء مستوياتٍ انطلاقًا من الصناديق. تستطيع كذلك إعطاءها بنيةً مرئيةً، والقفز من حولها. تمتع بقضاء بعض الوقت في إنشاء المستوى والقفز من خلاله! ترجمة -وبتصرف- للمقال Gravity لصاحبه Christopher Pitt اقرأ أيضًا المقال التالي: إنشاء السلالم وختام اللعبة المقال السابق: كشف التصادمات
  15. ألعاب المتصفح

    لقد حان الوقت لكي نتحدث عن كشف التصادمات (Detection Collision). إنه يؤثر على الأجزاء الظاهرة في لعبتنا، مثل الجدران التي لا يمكننا المرور عبرها ومثل الأرضيات التي لا يمكننا السقوط من خلالها. كما أنه يؤثر على الأجزاء المخفية في لعبتنا مثل قذائف الأسلحة ونقاط التفتيش. لن نصل إلى مفهوم الجاذبية بعد، ولن ننظر إلى صحة اللاعب أو استرداد حياته (الدم والأرواح). تعدّ الأرضيات والقذائف ونقاط التفتيش مثيرة للاهتمام، ولكنها تستحق أقسامًا خاصة بها. سنعمل على إنشاء كائنات غير نافذة (لا يمكن العبور من خلالها). سنتعلم طرقًا لمعرفة ما إذا كان جسمان يشغلان نفس الحيّز المكانيِّ. لقد قضيت بعض الوقت أبحث في هذا الموضوع. يبدو أن هناك العديد من الطرق لحل مشكلة جسمين يشغلان نفس المكان. بعض هذه الطرائق يسهل شرحها وتنفيذها وهي التي سنلقي نظرة عليها، والطرائق الأخرى ليست سهلةً ولا تزال حديثة العهد رغم ذلك. إليك ما سنعمل عليه في هذه المرحلة: *إنشاء أول صنف (class) غير متعلقة باللاعب. الكشف عن التصادمات المتعلقة بالدائرة. الكشف عن التصادمات المتعلقة بالمستطيل. إنشاء صناديق اللاعب هو كائنٌ من العديد من الكائنات التي سوف تتواجد على الشاشة في آنٍ واحدٍ. ولعبتنا هذه هي لعبة منصة، لذلك يمكننا أن نتوقع منصة واحدة على الأقل على الشاشة. لدى المنصات بعض الخصائص المثيرة للاهتمام. فهي تسمح في بعض الأحيان للاعبين بالسقوط من خلالها كما هو الحال عندما تكون واقفًا على منصة وترجع للخلف وتقفز في الوقت ذاته. تأخذ بعض الألعاب هذه السلسلة على أنك تريد السقوط من خلال المنصّة. تسمح بعض الألعاب للاعبين بالقفز في أسفل المنصات. وهذا يتيح الحركة العمودية دون وجود ثغرات في المنصات العلوية. وفي بعض الأحيان، تكون المنصات متحرّكة! تعتبر المنصّات فريدةً جدًا إلى حدّ أننا سنقضي بضعة أقسام في تنفيذ سلوكياتها المختلفة فقط. سوف نركز الآن على كائن مشترك آخر هو الصندوق الشامل (generic box). فكّر في هذا الصّندوق كمولّد للمنصّة. قد يشترك في بعض وظائفه مع المنصّة، ولكن السبب الرئيسي لوجوده هو الأشياء التي تتصادم معها وبالخصوص الكائنات مثل اللاعب. قد لا تبدو الصناديق التي سنُنشئها مثل الصناديق الحقيقية. عندما نصل إلى تطبيق الجاذبية، سنحتاج إلى صندوق عريضٍ ورفيعٍ لمنع اللاعب من السقوط خارج محيط اللعبة. وسنحتاج إلى صناديق طويلة ورفيعة لمنع اللاعب من الركض على جوانب أرضية اللعبة. سنصنع منها الجدران. وقد نصنع منها أيضًا صناديق فعلية كبيرة وخشبية تصلح للقفز عليها للوصول إلى أشياء أعلى. حسنًا، لقد تحدّثت بما يكفي. class Box { constructor(sprite, x, y, width, height) { this.sprite = sprite; this.x = x; this.y = y; this.width = width; this.height = height; this.sprite.x = this.x; this.sprite.y = this.y; } animate(state) { this.sprite.x = this.x; this.sprite.y = this.y; } } export default Box; لإنشاء هذا الصّنف، نسخت ولصقت فئة "اللّاعب" وحذفت مجموعة من الأشياء. لقد كان عليّ أن أضيف إليها كذلك خاصيتي العرض والارتفاع. سوف نصل إلى ذلك بعد قليلٍ. بعد ذلك، نحتاج إلى إضافة صندوقين إلى مسرح اللّعبة: var player_sprite = new PIXI.Sprite.fromImage( "player.png" ); var player = new Player( player_sprite, window.innerWidth * 2/4, 200, 64, 64 ); var barrel_sprite1 = new PIXI.Sprite.fromImage( "barrel.png" ); var barrel1 = new Box( barrel_sprite1, window.innerWidth * 1/4, 152, 64, 64 ); var barrel_sprite2 = new PIXI.Sprite.fromImage( "barrel.png" ); var barrel2 = new Box( barrel_sprite2, window.innerWidth * 3/4, 248, 64, 64 ); stage.addChild(player_sprite); stage.addChild(barrel_sprite1); stage.addChild(barrel_sprite2); var state = { "renderer": renderer, "stage": stage, "keys": {}, "clicks": {}, "mouse": {}, "objects": [ player, barrel1, barrel2 ] }; هذا غريب، أليس كذلك! لقد أنشأتُ نسختين جديدتين من الصّندوق، وأطلقت عليهما اسم براميل. ذلك لأننا على وشك أن نلقي نظرة على التصادمات المتعلقة بالدائرة. كشف التصادمات المتعلقة بالدائرة أريدك أن تنشئ دوائر لهذه الصناديق القليلة الأولى لأننا سندرس أولاً كشف التصادمات المتعلقة بالدوائر رغم أنّ الصندوق يتميّز بالعرض والارتفاع بدلاً من الشّعاع الذي يُميّز الدائرة. ولكن، لن نستخدم هذا النوع من الكشف عن التصادمات في كثير من الأحيان ما لم تكن في منصة اللعبة الكثير من الدوائر. لنر كيف يعمل هذا النوع من الكشف: class Player { constructor(sprite, x, y, width, height) { this.sprite = sprite; this.x = x; this.y = y; this.width = width; this.height = height; this.velocityX = 0; this.maximumVelocityX = 12; this.accelerationX = 2; this.frictionX = 0.9; this.sprite.x = this.x; this.sprite.y = this.y; } animate(state) { if (state.keys[37]) { // left this.velocityX = Math.max( this.velocityX - this.accelerationX, this.maximumVelocityX * -1 ); } if (state.keys[39]) { // right this.velocityX = Math.min( this.velocityX + this.accelerationX, this.maximumVelocityX ); } this.velocityX *= this.frictionX; var move = true; state.objects.forEach((object) => { if (object === this) { return; } var deltaX = this.x - object.x; var deltaY = this.y - object.y; var distance = Math.sqrt( deltaX * deltaX + deltaY * deltaY ); if (distance < this.width / 2 + object.width / 2) { if (this.velocityX < 0 && object.x <= this.x) { move = false; } if (this.velocityX > 0 && object.x >= this.x) { move = false; } } }); if (move) { this.x += this.velocityX; } this.sprite.x = this.x; this.sprite.y = this.y; } } export default Player; أول شيء يتعين علينا القيام به هو تحديد العرض والارتفاع. رغم أننا ندعي أن اللاعبين والصناديق عبارة عن دوائر، فإننا نحتاج فقط إلى نصف العرض كشعاع للدائرة. بعد ذلك، نتحقق من كل كائن في الطبقة (state). يمكننا تجاهل اللاعب لأننا لسنا بحاجة لمعرفة متى يصطدم شيء ما بنفسه. ولكن سيكون علينا، مع ذلك، أن نتحقق من كلّ الكائنات الأخرى. تصطدم دائرتان عندما تكون المسافة بين مركزيهما أقلّ من مجموع شعاعيهما معًا. تكون نقطتاهما الأصليتان متقاربتان جدًا بحيث تتداخل خطوطهما. نفحص سريعًا لمعرفة ما إذا كان الاتجاه الذي يتحرك فيه اللاعب هو حيث يتواجد الصندوق. إذا كان الأمر كذلك، فإننا نمنع اللاعب من التحرك في هذا الاتجاه. جرّبها، فمن الممتع جدًا أن ترى كيف تعيق الأشكال غير المربّعة بعضها بعضًا. بالطبع يجب أن تكون جميعها دوائر مثالية حتى تعمل هذه الخوارزمية البسيطة. كشف التصادمات المتعلّقة بالمستطيل يكون كشف التصادمات بالنسبة للمستطيلات سهلاً مثل الدوائر. امضِ قُدُمًا واستبدل صورة البرميل بصورة صندوق. يمكنك حتى ضبط شيفرة bootstrap لتعكس شكلا مربّعًا للصناديق. هذه المرة، سنتعامل مع اللاعب باعتباره مستطيلًا. بدلاً من استعمال الشّعاع، سنحتاج إلى التحقق مما إذا كانت هناك فراغات بين المستطيل الذي يمثّل اللاعب وأيّ صندوق. نحن نسمي هذا الكشف عن التصادم في المربع المحيط المحاذي للمحور (axis-aligned bounding box) أو AABB للاختصار. إذا لم يكن هناك فراغ، وكان اللاعب يريد التحرك في اتجاه الصندوق، فإننا نمنع حدوث ذلك: var move = true; state.objects.forEach((object) => { if (object === this) { return; } if (this.x < object.x + object.width && this.x + this.width > object.x && this.y < object.y + object.height && this.y + this.height > object.y) { if (this.velocityX < 0 && object.x <= this.x) { move = false; } if (this.velocityX > 0 && object.x >= this.x) { move = false; } } }); if (move) { this.x += this.velocityX; } هذه بعض الطرق البسيطة للكشف عن التصادمات، ولكن هناك طرق أخرى. هناك طريقة تعتمد على الإسقاط في الرياضيات المتجهية لتحديد التداخل. وهناك طريقة أخرى تفحص كل خطّ في زوجٍ من المضلّعات لمعرفة ما إذا كان هناك تقاطع للخطوط. إنّها طريقة غريبة. ويمكنك كذلك تجربة مجموعات من الدوائر تتصادم معًا. قد يكون الأمر ممتعًا. سوف أشغّل قليلاً هذه الدائرة الخضراء الصغيرة لتتحرّك نحو هذه الصناديق الحمراء الصغيرة. نلتقي فيما بعد… ترجمة -وبتصرف- للمقال Collision Detection لصاحبه Christopher Pitt اقرأ أيضًا المقال التالي: الجاذبية المقال السابق: جلب المدخلات من اللاعب