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

يكرّس العديد من المطورين جزءًا من وقتهم لبناء خوادم تُسهّل توزيع المحتوى عبر الإنترنت. يُعد بروتوكول النقل التشعبي Hypertext Transfer Protocol-أو اختصارًا HTTP- من أهم الوسائل المستخدمة لتوزيع المحتوى مهما كان نوع البيانات عبر الإنترنت. تتضمّن مكتبة لغة جو القياسية وظائفًا مدمجة لإنشاء خادم HTTP لتخديّم محتوى الويب، أو إنشاء طلبات HTTP للتواصل مع هذه الخوادم.

سنتعلم في هذا المقال كيفية إنشاء خادم HTTP باستخدام مكتبة لغة جو القياسية، وكيفية توسيع وظائف الخادم لاستخراج البيانات من أجزاء مختلفة من طلب HTTP، مثل سلسلة الاستعلام والمتن Body وبيانات النموذج في الطلب. سنتعلّم أيضًا كيفية تعديل استجابة الخادم عن طريق إضافة ترويسات HTTP ورموز حالة مخصصة status codes، وبالتالي السماح للمطورين بتخصيص سلوك الخادم الخاص بهم.

توضيح بعض المصطلحات:

في سياق إنشاء خادم HTTP باستخدام مكتبة جو القياسية، تشير المصطلحات "سلسلة استعلام الطلب" و"المتن" و"بيانات النموذج" إلى أجزاء مختلفة من طلب HTTP.

  1. سلسلة الاستعلام: هي جزء من عنوان URL الذي يأتي بعد رمز علامة الاستفهام (?). يحتوي عادةً على أزواج ذات قيمة مفتاح مفصولة بعلامات &.
  2. المتن أو النص الأساسي: يحتوي متن طلب HTTP على البيانات التي يرسلها العميل إلى الخادم، مثل JSON أو XML أو النص العادي. يُستخدم بكثرة في طلبات من نوع POSTو PUT و PATCH لإرسال البيانات إلى الخادم.
  3. بيانات النموذج: تُرسل عادةً بيانات النموذج مثل جزء من طلب POST مع ضبط ترويسة "نوع المحتوى" على "application/x-www-form-urlencoded" أو "multipart/form-data". وهو يتألف من أزواج مفتاح - قيمة على غرار سلسلة الاستعلام، ولكن يُرسل في متن الطلب.

بروتوكول HTTP

هو بروتوكول يعمل على مستوى التطبيقات، ويستخدم لنقل مستندات الوسائط التشعبية، مثل صفحات HTML عبر الإنترنت. يعمل HTTP بمثابة أساس لاتصالات البيانات على شبكة الويب العالمية.

يتبع HTTP نموذج خادم-العميل، إذ يرسل العميل (عادةً متصفح ويب) طلبًا إلى الخادم، ويستجيب الخادم بالمعلومات المطلوبة. تُستخدم بعض الدوال، مثل GET و POSTو PUT و DELETE لتسهيل عملية الاتصال بين العميل والخادم.

يمكنك الاطلاع على مقال مدخل إلى HTTP على أكاديمية حسوب لمزيدٍ من المعلومات حول بروتوكول HTTP.

المتطلبات الأولية

لمتابعة هذا المقال التعليمي، سنحتاج إلى:

إعداد المشروع

تتوفّر معظم وظائف HTTP التي تسمح لنا بإجراء الطلبات من خلال حزمة net/http الموجودة في المكتبة القياسية في لغة جو، بينما تتولى حزمة net بقية عمليات الاتصال بالشبكة. توفّر حزمة net/http أيضًا خادم HTTP يمكن استخدامه لمعالجة تلك الطلبات.

سننشئ في هذا القسم برنامجًا يستخدم الدالة http.ListenAndServe لتشغيل خادم HTTP يستجيب للطلبات ذات المسارات / و hello/، ثم سنوسّع البرنامج لتشغيل خوادم HTTP متعددة في نفس البرنامج.

كما هو معتاد، سنحتاج لبدء إنشاء برامجنا إلى إنشاء مجلد للعمل ووضع الملفات فيه، ويمكن وضع المجلد في أي مكان على الحاسب، إذ يكون للعديد من المبرمجين عادةً مجلدٌ يضعون داخله كافة مشاريعهم. سنستخدم في هذا المقال مجلدًا باسم "projects"، لذا فلننشئ هذا المجلد وننتقل إليه:

$ mkdir projects
$ cd projects

الآن من داخل هذا المجلد، سنشغّل الأمر mkdir لإنشاء مجلد "httpserver" ثم سنستخدم cd للانتقال إليه:

$ mkdir httpserver
$ cd httpserver

الآن، بعد أن أنشأنا مجلدًا للبرنامج وانتقلنا إليه، يمكننا البدء في تحقيق خادم HTTP.

الاستماع إلى الطلبات وتقديم الردود

يتضمّن مخدّم HTTP في لغة جو مكونين رئيسيين: المخدّم الذي يستمع إلى الطلبات القادمة من العميل الذي يرسل طلبات HTTP (عميل HTTP أو عملاء HTTP) ومُعالج طلبات (أو أكثر) يستجيب لتلك الطلبات. سنبدأ حاليًا في استخدام الدالة http.HandleFunc التي تخبر المُخدّم بالدالة التي يجب استدعاؤها لمعالجة الطلب، ثم سنستخدم الدالة http.ListenAndServe لتشغيل الخادم وإخباره بالتحضير للاستماع إلى طلب HTTP جديد وتخديمه من خلال معالجته بدوال المعالجة Handler functions التي نُنشئها مُسبقًا.

بما أننا الآن داخل مجلد "httpserver"، يمكننا فتح ملف "main.go" باستخدام محرر نانو nano أو أي محرر آخر تريده:

$ nano main.go

سننشئ داخل هذا الملف دالتين getRoot و getHello سيمثلان دوال المعالجة الخاصة بنا، ثم سننشئ دالةً رئيسية main لاستخدامها في إعداد معالجات الطلبات من خلال الدالة http.HandleFunc، وذلك بتمرير المسار / الخاص بالدالة getRoot والمسار hello/ الخاص بالدالة getHello. بمجرد أن ننتهي من إعداد دوال المعالجة الخاصة بنا، يمكننا استدعاء http.ListenAndServe لبدء تشغيل المخدّم والاستماع للطلبات. دعنا نضيف التعليمات البرمجية التالية إلى الملف لبدء تشغيل البرنامج وإعداد المعالجات:

package main

import (
    "errors"
    "fmt"
    "io"
    "net/http"
    "os"
)

func getRoot(w http.ResponseWriter, r *http.Request) {
    fmt.Printf("got / request\n")
    io.WriteString(w, "This is my website!\n")
}
func getHello(w http.ResponseWriter, r *http.Request) {
    fmt.Printf("got /hello request\n")
    io.WriteString(w, "Hello, HTTP!\n")
}

أعددنا في البداية الحزمة الخاصة بالبرنامج package مع استيراد الحزم المطلوبة من خلال تعليمة import، كما أنشأنا الدالتين getRoot و getHello، ونلاحظ أن لهما نفس البصمة Signature، إذ تقبلان نفس الوسيطين، هما: قيمة http.ResponseWriter وقيمة http.Request*. هاتان الدالتان لهما بصمة تطابق النوع http.HandlerFunc، والذي يشيع استخدامه لتعريف دوال معالجة HTTP. عند تقديم طلب إلى الخادم، فإنه يُزوِّد هاتين القيمتين بمعلومات حول الطلب الحالي، ثم يستدعي دالة المعالج التي تتوافق مع هذه القيم.

تُستخدم القيمة http.ResponseWriter (االمُسماة w) ضمن http.HandlerFunc للتحكّم بمعلومات الاستجابة التي يُعاد كتابتها إلى العميل الذي قدّم الطلب، مثل متن الاستجابة response body (جزء من استجابة HTTP ويحمل الحمولة الفعلية أو المعلومات التي طلبها العميل أو التي يوفرها الخادم) أو رموز الحالة status codes، وهي معلومات حول نتيجة الطلب والحالة الحالية للخادم، وهي جزء من بروتوكول HTTP يجري تضمينها في ترويسة الاستجابة، وتشير إلى ما إذا كان الطلب ناجحًا أو واجه خطأً أو يتطلب إجراءً إضافيًا. بعد ذلك، تُستخدم القيمة http.Request* (المسماة r) للحصول على معلومات حول الطلب الذي جاء إلى الخادم، مثل المتن المُرسل في حالة طلب POST أو معلومات حول العميل الذي أجرى الطلب.

في كل من معالجات HTTP التي أنشأناها، يمكننا استخدام الدالة fmt.Printf للطباعة، وذلك عندما يأتي طلب لدالة المعالجة، ثم نستخدم http.ResponseWriter لإرسال نص ما إلى متن الاستجابة. http.ResponseWriter هي واجهة في حزمة http تمثل الاستجابة التي سترسل مرةً أخرى إلى العميل عند تقديم طلب إلى الخادم، وهي io.Writer مما يعني أنها توفر إمكانية كتابة البيانات. نستخدم http.ResponseWriterفي الشيفرة السابقة مثل وسيطw في دوال المعالجة (getRoot و getHello)، للسماح بالتحكم في الرد المرسل إلى العميل، وبالتالي إمكانية كتابة متن الاستجابة، أو ضبط الترويسات headers، أو تحديد رمز الحالة باستخدامها. نستخدم الدالة io.WriteString لكتابة الاستجابة ضمن متن الرسالة.

لنضيف الآن الدالة main إلى الشيفرة السابقة:

...
func main() {
    http.HandleFunc("/", getRoot)
    http.HandleFunc("/hello", getHello)

    err := http.ListenAndServe(":3333", nil)
...

تكون الدالة الرئيسية main في جزء الشيفرة السابق، مسؤولةً عن إعداد خادم HTTP وتحديد معالجات الطلب. هناك استدعاءان إلى الدالة http.HandleFunc، بحيث يربط كل استدعاء لها دالة معالجة من أجل مسار طلب محدد ضمن مجمّع الخادم الافتراضي default server multiplexer. يتطلب الأمر وسيطين: الأول هو مسار الطلب (في هذه الحالة / ثم hello/) ودالة المعالجة (getRoot ثم getHello على التوالي). الدالتان getRoot و getHello هما دوال المعالجة التي سيجري استدعاؤها عند تقديم طلب إلى المسارات المقابلة (/ و hello/). للدالتين توقيع مماثل للدالة http.HandlerFunc التي تقبلhttp.ResponseWriter و http.Request* مثل وسطاء.

تُستخدم الدالة http.ListenAndServe لبدء تشغيل خادم HTTP للاستماع إلى الطلبات الواردة. يتطلب الأمر وسيطين، هما: عنوان الشبكة للاستماع عليه (في هذه الحالة 3333:) ومعالج اختياري http.Handler. يحدد 3333: في برنامجنا أن الخادم يجب أن يستمع إلى المنفذ 3333، ونظرًا لعدم تحديد عنوان IP، سيستمع إلى جميع عناوين IP المرتبطة بالحاسب. يمثّل منفذ الشبكة network port -مثل "3333"- طريقةً تمكّن جهاز الحاسوب من أن يكون لديه عدة برامج تتواصل مع بعضها بنفس الوقت، بحيث يستخدم كل برنامج منفذه المخصص، وبالتالي عند اتصال العميل مع منفذ معين يعلم الحاسوب إلى أي منفذ سيُرسل. إذا كنت تريد قصر الاتصالات على المضيف المحلي localhost فقط، فيمكنك استخدام ‎127.0.0.1:3333.

تمرّر الدالة http.ListenAndServe قيمة nil من أجل المعامل http.Handler، وهذا يخبر دالة ListenAndServe بأنك تريد استخدام مجمّع الخادم الافتراضي وليس أي مجمّع ضبطه سابقًا.

الدالة ListenAndServe هي استدعاء "حظر"، مما يعني أنها ستمنع تنفيذ التعليمات البرمجية الأخرى حتى يُغلق الخادم. تُعيد هذه الدالة خطًأ إذا فشلت عملية بدء تشغيل الخادم أو إذا حدث خطأ أثناء التشغيل. من المهم تضمين عملية معالجة الأخطاء بعد استدعاء http.ListenAndServe، وذلك لأن الدالة يمكن أن تفشل، بالتالي من الضروري التعامل مع الأخطاء المحتملة. يُستخدم المتغير err لالتقاط أي خطأ يُعاد بواسطة ListenAndServe. يمكننا إضافة شيفرة معالجة الأخطاء بعد هذا السطر لمعالجة أي أخطاء محتملة قد تحدث أثناء بدء تشغيل الخادم أو تشغيله كما سنرى.

لنضيف الآن شيفرة معالجة الأخطاء إلى دالة ListenAndServe ضمن دالة main الرئيسية كما يلي:

...

func main() {
    ...
    err := http.ListenAndServe(":3333", nil)
  if errors.Is(err, http.ErrServerClosed) {
        fmt.Printf("server closed\n")
    } else if err != nil {
        fmt.Printf("error starting server: %s\n", err)
        os.Exit(1)
    <^>}
}

بعد استدعاء http.ListenAndServe، يجري تخزين الخطأ المُعاد في المتغير err. تجري عملية فحص الخطأ الأولى باستخدام (errors.Is(err, http.ErrServerClosed، إذ يجري التحقق ما إذا كان الخطأ هو http.ErrServerClosed، والذي يُعاد عندما يُغلق الخادم أو يجري إيقاف تشغيله. يعني ظهور هذا الخطأ أن الخادم قد أُغلق بطريقة متوقعة، بالتالي طباعة الرسالة "server closed".

يُنجز فحص الخطأ الثاني باستخدام Err != nil. يتحقق هذا الشرط مما إذا كان الخطأ ليس nil، مما يشير إلى حدوث خطأ أثناء بدء تشغيل الخادم أو تشغيله. إذا تحقق الشرط، فهذا يعني حدوث خطأ غير متوقع، وبالتالي طباعة رسالة خطأ مع تفاصيل الخطأ باستخدام fmt.Printf، كما يجري إنهاء البرنامج مع شيفرة الخطأ 1 باستخدام (os.Exit (1 للإشارة إلى حدوث خطأ.

تجدر الإشارة إلى أن أحد الأخطاء الشائعة التي قد تواجهها هو أن العنوان قيد الاستخدام فعلًا address already in use. يحدث هذا عندما تكون الدالة ListenAndServe غير قادرة على الاستماع إلى العنوان أو المنفذ المحدد للخادم لأنه قيد الاستخدام فعلًا من قبل برنامج آخر، أي إذا كان المنفذ شائع الاستخدام أو إذا كان برنامج آخر يستخدم نفس العنوان أو المنفذ.

ملاحظة: عند ظهور هذا الخطأ، يجب التأكد من إيقاف أي مثيلات سابقة للبرنامج ثم محاولة تشغيله مرة أخرى. إذا استمر الخطأ يجب علينا محاولة استخدام رقم منفذ مختلف لتجنب التعارضات، فمن المحتمل أن برنامجًا آخر يستخدم المنفذ المحدد. يمكن اختيار رقم منفذ مختلف (أعلى من 1024 وأقل من 65535) وتعديل الشيفرة وفقًا لذلك.

على عكس برامج لغة جو الأخرى؛ لن يُنهى البرنامج فورًا من تلقاء نفسه. لنُشغّل ملف البرنامج "main.go" بعد حفظه من خلال الأمر go run:

$ go run main.go

بما أن البرنامج يبقى قيد التشغيل في الطرفية الحالية، فسنحتاج إلى فتح نافذة أخرى للطرفية لكي نتفاعل مع الخادم (ستظهر الأوامر بلون مختلف عن الطرفية الأولى).

ضمن الطرفية الثانية التي فتحناها، نستخدم الأمر curl لتقديم طلب HTTP إلى خادم HTTP الخاص بنا. curl هي أداة مُثبّتة افتراضيًا على العديد من الأنظمة التي يمكنها تقديم طلبات للخوادم من أنواع مختلفة، وسنستخدمها في هذا المقال لإجراء طلبات HTTP. يستمع الخادم إلى الاتصالات على المنفذ 3333 لجهاز الحاسوب، لذا يجب تقديم الطلب للمضيف المحلي على نفس المنفذ:

$ curl http://localhost:3333

سيكون الخرج على النحو التالي:

This is my website!

العبارة This is my website ناتجة عن الدالة getRoot، وذلك لأنك استخدمت المسار / على خادم HTTP. دعونا الآن نستخدم المسار hello/ على نفس المضيف والمنفذ، وذلك بإضافة المسار إلى نهاية أمر curl:

$ curl http://localhost:3333/hello

ليكون الخرج هذه المرة:

Hello, HTTP!

نلاحظ أن الخرج السابق كان نتيجةً لاستدعاء الدالة getHello. إذا عدنا إلى المحطة الطرفية الأولى التي يعمل عليها خادم HTTP، نلاحظ وجود سطرين أنتجهما الخادم الخاص بنا: واحد للطلب / والآخر للطلب hello/.

got / request
got /hello request

سيستمر البرنامج بالعمل، لذا يجب علينا إيقافه يدويًا من خلال الضغط على المفتاحين "Ctrl+C".

لقد أنشأنا برنامجًا يمثل خادم HTTP، لكنه يستخدم مجمّع خادم افتراضي وخادم HTTP افتراضي أيضًا. يمكن أن يؤدي الاعتماد على القيم الافتراضية أو العامة Global إلى حدوث أخطاء يصعب تكرارها أو يصعب إنتاجها باستمرار Reproduce consistently، إذ يمكن أن تُعدِّل أجزاءً مختلفة من البرنامج هذه القيم العامة في أوقات مختلفة، مما يؤدي إلى حالة غير صحيحة أو غير متسقة. يصبح تحديد مثل هذه الأخطاء أمرًا صعبًا لأنها قد تحدث فقط في ظل ظروف معينة أو إذا جرى استدعاء وظائف معينة بترتيب معين.

معالجات طلبات التجميع

عند بدء تشغيل خادم HTTP سابقًا؛ استخدمنا مجمّع خادم افتراضي عن طريق تمرير قيمة صفرية (أي nil) للمعامل http.Handler في دالة ListenAndServe. رأينا أيضًا أن هناك بعض المشاكل التي قد تطرأ في حالة استخدام المعاملات الافتراضية. بما أن http.Handler هو واجهة interface، فهذا يعني أنه لدينا الخيار لإنشاء بنية مخصصة تحقق هذه الواجهة. هناك طبعًا حالات نحتاج فيها فقط إلى http.Handler الافتراضي الذي يستدعي دالة واحدة لمسار طلب معين، أي كما في حالة مجمّع الخادم الافتراضي، لكن هناك حالات قد تتطلّب أكثر من ذلك؛ هذا ما نناقشه تاليًا.

لنعدّل البرنامج الآن لاستخدام http.ServeMux، إنها أداة تعمل مثل مجمّع للخادم، وهي مسؤولة عن التوجيه والتعامل مع طلبات HTTP الواردة بناءً على مساراتها. تحقّقhttp.ServeMux الواجهة http.Handler المؤمنة من قِبل حزمة net/http، مما يعني قدرتها على التعامل مع طلبات HTTP وإنشاء الاستجابات المناسبة. بالتالي مزيد من التحكم في التوجيه والتعامل مع مسارات الطلبات المختلفة، وإتاحة الفرصة لتحديد دوال أو معالجات محددة لكل مسار. بالتالي نكون قد اتبعنا نهجًا أكثر تنظيمًا وقابلية للتخصيص للتعامل مع طلبات HTTP في البرنامج.

يمكن تهيئة بنية http.ServeMux بطريقة مشابهة للمجمّع الافتراضي، لذا لن نحتاج إلى إجراء العديد من التغييرات على البرنامج لبدء استخدام مجمّع الخادم المخصّص بدلًا من الافتراضي. لتحديث البرنامج وفقًا لذلك، نفتح ملف "main.go" مرةً أخرى ونجري التعديلات اللازمة لاستخدام http.ServeMux:

...

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", getRoot)
    mux.HandleFunc("/hello", getHello)

    err := http.ListenAndServe(":3333", mux)

    ...
}

أنشأنا http.ServeMux جديد باستخدام باني http.NewServeMux وأسندناه إلى المتغير mux، ثم عدّلنا استدعاءات http.HandleFunc لاستخدام المتغير mux بدلًا من استدعاء حزمة http مباشرة. أخيرًا، عدّلنا استدعاء http.ListenAndServe لتزويده بالمعالج http.Handler الذي أنشأنه mux بدلًا من nil.

لنُشغّل ملف البرنامج "main.go" بعد حفظه من خلال الأمر go run:

$ go run main.go

سيستمر البرنامج في العمل كما في المرة السابقة، لذا نحتاج إلى تشغيل أوامر في طرفية أخرى للتفاعل مع الخادم. أولًا، نستخدم curl لطلب المسار /:

$ curl http://localhost:3333

سيكون الخرج على النحو التالي:

This is my website!

الخرج كما هو في المرة السابقة. دعونا الآن نستخدم المسار hello/ على نفس المضيف والمنفذ، وذلك بإضافة المسار إلى نهاية أمر curl:

$ curl http://localhost:3333/hello

سيكون الخرج كما يلي:

Hello, HTTP!

نلاحظ أن الخرج السابق كان كما المرة السابقة أيضًا.

إذا عدنا الآن إلى الطرفية الأولى، فسنرى مخرجات كل من / و hello / كما كان من قبل:

got / request
got /hello request

نلاحظ أن التعديلات التي أجريناها لا تغيّر في وظيفة البرنامج، وإنما فقط بدلنا مجمّع الخادم الافتراضي بآخر مخصص.

سيستمر البرنامج بالعمل، لذا يجب علينا إيقافه يدويًا من خلال الضغط على المفتاحين "Ctrl+C".

تشغيل عدة خوادم في وقت واحد

سنجري خلال هذا القسم تعديلات على البرنامج لاستخدام عدة خوادم HTTP في نفس الوقت باستخدام http.Server التي توفرها حزمة net/http. يمكننا بالحالة الافتراضية تشغيل خادم HTTP واحد فقط في البرنامج، لكن قد تكون هناك سيناريوهات نحتاج فيها إلى تخصيص سلوك الخادم أو تشغيل عدة خوادم في نفس الوقت، مثل استضافة موقع ويب عام Public website وموقع ويب إداري خاص Private admin website داخل نفس البرنامج. لأجل ذلك سنعدّل ملف "main.go" لإنشاء نسخ متعددة من http.Server لأغراض مختلفة، إذ سيكون لكل خادم التهيئة والإعدادات الخاصة به. يتيح لك هذا مزيدًا من التحكم في سلوك الخادم ويمكّنك من التعامل مع وظائف خادم متعددة داخل نفس البرنامج.

سنعدّل أيضًا دوال المعالجة لتحقيق إمكانية الوصول إلى Context.Context المرتبط مع http.Request*؛ أي إمكانية الوصول إلى سياق الطلبات الواردة، إذ يمكننا من خلال هذا السياق تمييز الخادم الذي يأتي الطلب منه. إذًا من خلال تخزين هذه المعلومات في متغير السياق، يصبح بمقدورنا استخدامها داخل دالة المعالجة لتنفيذ إجراءات محددة أو تخصيص الاستجابة بناءً على الخادم الذي أنشأ الطلب.

لنفتح ملف "main.go" ونعدّله بالتالي:

package main

import ( 
// os لاحظ أننا حذفنا استيراد 
    "context"
    "errors"
    "fmt"
    "io"
    "net"
    "net/http"
)

const keyServerAddr = "serverAddr"

func getRoot(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    fmt.Printf("%s: got / request\n", ctx.Value(keyServerAddr))
    io.WriteString(w, "This is my website!\n")
}
func getHello(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    fmt.Printf("%s: got /hello request\n", ctx.Value(keyServerAddr))
    io.WriteString(w, "Hello, HTTP!\n")
}

عدّلنا بيان الاستيراد import لتضمين الحزم المطلوبة، ثم أنشأنا سلسلة نصية ثابتة const string تسمى keyServerAddr لتعمل مثل مفتاح لقيمة عنوان خادم HTTP في سياق http.Request، ثم عدّلنا دوال المعالجة getRoot و getHello للوصول إلى قيمة Context.Context التابعة إلى http.Request. بعد الحصول على القيمة يمكننا تضمين عنوان خادم HTTP في خرج fmt.Printf حتى نتمكن من معرفة أي من الخادمين تعامل مع طلب HTTP.

لنعدّل الآن الدالة main بإضافة قيمتي http.Server:

...
func main() {
    ...
    mux.HandleFunc("/hello", getHello)

    ctx, cancelCtx := context.WithCancel(context.Background())
    serverOne := &http.Server{
        Addr:    ":3333",
        Handler: mux,
        BaseContext: func(l net.Listener) context.Context {
            ctx = context.WithValue(ctx, keyServerAddr, l.Addr().String())
            return ctx
        },
    }

التغيير الأول الذي أجريناه هو إنشاء قيمة Context.Context جديدة مع دالة متاحة هي الدالة `cancelCtx'. هذا يسمح لنا بإلغاء السياق عند الحاجة.

عرّفنا أيضًا نسخةً تسمى serverOne من http.Server، وهو مشابه لخادم HTTP الذي كنا نستخدمه، ولكن بدلًا من تمرير العنوان والمعالج مباشرةً إلى http.ListenAndServe، يمكن إسنادهما مثل قيم Addr و Handler في بنية http.Server.

تعديل آخر كان بإضافة دالة BaseContext، وهي دالة تسمح بتعديل أجزاء من Context.Context الذي جرى تمريره إلى دوال المعالجة عند استدعاء التابع Context من http.Request*. أضفنا في الشيفرة السابقة عنوان الاستماع الخاص بالخادم إلى السياق باستخدام المفتاح serverAddr، وهذا يعني أن العنوان الذي يستمع فيه الخادم للطلبات الواردة مرتبط بمفتاح serverAddr في السياق. عندما نستدعي الدالة BaseContext، فإنها تتلقى net.Listener، والذي يمثل مستمع الشبكة الأساسي الذي يستخدمه الخادم.

بالنسبة لبرنامجنا: من خلال استدعاء ()l.Addr(). String، نكون قد حصلنا على عنوان شبكة المستمع. يتضمن هذا عادةً عنوان IP ورقم المنفذ الذي يستمع الخادم عليه. بعد ذلك نضيف العنوان الذي حصلنا عليه إلى السياق باستخدام الدالة Context.WithValue، والتي تتيح لنا تخزين أزواج المفتاح والقيمة في السياق. في هذه الحالة يكون المفتاح هو serverAddr، والقيمة المرتبطة به هي عنوان الاستماع الخاص بالخادم.

لنعرّف الآن الخادم الثاني serverTwo:

...

func main() {
    ...
    serverOne := &http.Server {
        ...
    }

    serverTwo := &http.Server{
        Addr:    ":4444",
        Handler: mux,
        BaseContext: func(l net.Listener) context.Context {
            ctx = context.WithValue(ctx, keyServerAddr, l.Addr().String())
            return ctx
        },
    }

نعرّف الخادم الثاني بنفس طريقة تعريف الأول، لكن نضع حقل العنوان على 4444: بدلًا من 3333:، وذلك لكي يستمع الخادم الأول على للاتصالات على المنفذ 3333 ويستمع الخادم الثاني على المنفذ 4444.

لنعدّل الآن البرنامج من أجل استخدام الخادم الأول serverOne وجعله يعمل مثل تنظيم جو goroutine:

...

func main() {
    ...
    serverTwo := &http.Server {
        ...
    }

    go func() {
        err := serverOne.ListenAndServe()
        if errors.Is(err, http.ErrServerClosed) {
            fmt.Printf("server one closed\n")
        } else if err != nil {
            fmt.Printf("error listening for server one: %s\n", err)
        }
        cancelCtx()
    }()

نستخدم تنظيم goroutine لبدء تشغيل الخادم الأول serverOne باستخدام الدالة ListenAndServe كما فعلنا سابقًا، لكن هذه المرة دون أي معاملات لأن قيم http.Server جرى تهيئتها مسبقًا باستخدام العنوان والمعالج المطلوبين.

تجري عملية معالجة الأخطاء كما في السابق؛ فإذا كان الخادم مغلقًا، فإنه يطبع رسالة تشير إلى أن "الخادم الأول" أصبح مغلقًا؛ وإذا كان هناك خطأ آخر بخلاف http.ErrServerClosed، فستظهر رسالة خطأ.

تُستدعى أخيرًا الدالة CancelCtx لإلغاء السياق الذي قدمناه لمُعالجات HTTP ودوال BaseContext لكلا الخادمين. بالتالي إنهاء أي عمليات جارية تعتمد عليه بأمان. هذا يضمن أنه إذا انتهى الخادم لأي سبب، فسيُنهى أيضًا السياق المرتبط به.

لنعدّل الآن البرنامج لاستخدام الخادم الثاني، بحيث يعمل مثل تنظيم جو :

...

func main() {
    ...
    go func() {
        ...
    }()
    go func() {
        err := serverTwo.ListenAndServe()
        if errors.Is(err, http.ErrServerClosed) {
            fmt.Printf("server two closed\n")
        } else if err != nil {
            fmt.Printf("error listening for server two: %s\n", err)
        }
        cancelCtx()
    }()

    <-ctx.Done()
}

تنظيم جو هنا هو نفسه الأول من الناحية الوظيفية، فهو يُشغّل serverTwo فقط بدلًا من serverOne. بعد بدء تشغيل serverTwo، يصل التنظيم الخاص بالدالة main إلى السطر ctx.Done. ينتظر هذا السطر إشارةً من قناة ctx.Done، والتي تُعاد عند إلغاء السياق أو الانتهاء منه. من خلال هذا الانتظار نكون قد منعنا تنظيم الدالة الرئيسية من الخروج من الدالة main حتى تنتهي تنظيمات جو لكلا الخادمين من العمل. إذًا، الغرض من هذا الأسلوب هو التأكد من استمرار تشغيل البرنامج حتى انتهاء كلا الخادمين أو مواجهة خطأ.

لنُشغّل ملف البرنامج "main.go" بعد حفظه من خلال الأمر go run:

$ go run main.go

نُشغّل الآن أوامر curl (في الطرفية الثانية) لطلب المسار / والمسارhello/ من الخادم الذي يستمع على 3333، مثل الطلبات السابقة:

$ curl http://localhost:3333
$ curl http://localhost:3333/hello

سيكون الخرج كما في المرات السابقة:

This is my website!
Hello, HTTP!

لنشغّل الأوامر نفسها مرة أخرى، ولكن هذه المرة مع المنفذ 4444 الذي يتوافق مع serverTwo:

$ curl http://localhost:4444
$ curl http://localhost:4444/hello

سيبقى الخرج نفسه كما في المرة السابقة أيضًا:

This is my website!
Hello, HTTP!

لنلقي نظرةً الآن على الطرفية الأولى حيث يعمل الخادم:

[::]:3333: got / request
[::]:3333: got /hello request
[::]:4444: got / request
[::]:4444: got /hello request

الخرج مشابه لما رأيناه من قبل، لكنه يعرض هذه المرة الخادم الذي استجاب للطلب. يظهِر الطلبان الأولان أنهما جاءا من الخادم الذي يستمع على المنفذ 3333 أي serverOne، والطلبان الثانيان جاءا من الخادم الذي يستمع على المنفذ 4444 أي serverTwo. إنها القيم التي جرى استردادها من قيمة serverAddr في BaseContext. قد يكون الخرج مختلفًا قليلًا عن الخرج أعلاه اعتمادًا على ما إذا كان جهاز الحاسب المُستخدم يستخدم IPv6 أم لا. إذا كان الحاسب يستخدم IPv6 فسيكون الخرج كما أعلاه، وإلا سنرى 0.0.0.0 بدلًا من [::]. السبب في ذلك هو أن جهاز الحاسب سيتواصل مع نفسه عبر IPv6، و [::] هو تدوين IPv6 والذي يُقابل 0.0.0.0 في IPv4. بعد الانتهاء نضغط "CONTROL+C" لإنهاء الخادم.

تعرفنا في هذا القسم على عملية إنشاء برنامج خادم HTTP وتحسينه تدريجيًا للتعامل مع السيناريوهات المختلفة. بدأنا بتهيئة الخادم الافتراضي باستخدام http.HandleFunc و http.ListenAndServe. بعد ذلك عدّلناه لاستخدام http.ServeMux مثل مجمّع خادم، مما يسمح لنا بالتعامل مع معالجات طلبات متعددة لمسارات مختلفة. وسّعنا البرنامج أيضًا لاستخدام http.Server، مما يمنحنا مزيدًا من التحكم في تهيئة الخادم ويسمح لنا بتشغيل خوادم HTTP متعددة في نفس الوقت داخل نفس البرنامج.

على الرغم من أن البرنامج يعمل، إلا أنه يفتقر إلى التفاعل الذي يتجاوز تحديد المسارات المختلفة. لمعالجة هذا القيد، يركز القسم التالي على دمج قيم سلسلة الاستعلام query string values في وظائف الخادم، مما يتيح للمستخدمين التفاعل مع الخادم باستخدام معاملات الاستعلام.

فحص سلسلة الاستعلام الخاصة بالطلب

ينصب التركيز في هذا القسم على دمج قيم سلسلة الاستعلام في وظائف خادم HTTP. سلسلة الاستعلام هي مجموعة من القيم الملحقة بنهاية عنوان URL، تبدأ بالمحرف ? وتستخدم المُحدّد & للقيم الإضافية. توفر قيم سلسلة الاستعلام وسيلة للمستخدمين للتأثير على الاستجابة التي يتلقونها من خادم HTTP عن طريق تخصيص النتائج أو تصفيتها، فمثلًا قد يستخدم أحد الخوادم قيمة results للسماح للمستخدم بتحديد شيء مثل results = 10 ليقول إنه يرغب في رؤية 10 عناصر في قائمة النتائج.

لتحقيق هذه الميزة نحتاج إلى تحديث دالة المعالجة getRoot في ملف "main.go" للوصول إلى قيم سلسلة الاستعلام http.Request* باستخدام التابع r.URL.Query، ثم طباعتها بعد ذلك على الخرج. نزيل أيضًا serverTwo وكل الشيفرات المرتبطة به من الدالة main، لأنها لم تعد مطلوبة للتغييرات القادمة:

...

func getRoot(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    hasFirst := r.URL.Query().Has("first")
    first := r.URL.Query().Get("first")
    hasSecond := r.URL.Query().Has("second")
    second := r.URL.Query().Get("second")

    fmt.Printf("%s: got / request. first(%t)=%s, second(%t)=%s\n",
        ctx.Value(keyServerAddr),
        hasFirst, first,
        hasSecond, second)
    io.WriteString(w, "This is my website!\n")
}
...

يمكننا في دالة getRoot المحدَّثة استخدام الحقل r.URL الذي يتبع إلى http.Request* للوصول إلى الخصائص المتعلقة بعنوان URL المطلوب. باستخدام التابع Query في الحقل r.URL، يمكننا الوصول إلى قيم سلسلة الاستعلام المرتبطة بالطلب. هناك طريقتان يمكن استخدامهما للتفاعل مع بيانات سلسلة الاستعلام:

  1. يتحقق التابع Has ما إذا كانت سلسلة الاستعلام تحتوي على قيمة بمفتاح معين، مثل"first" أو "second". يعيد التابع قيمة بوليانية bool تشير إلى وجود المفتاح.
  2. يسترد التابع Get القيمة المرتبطة بمفتاح معين من سلسلة الاستعلام وتكون من نوع string، وإذا لم يعثر على المفتاح، يعيد عادةً سلسلة فارغة.

نظريًّا يمكننا دائمًا استخدام Get لاسترداد قيم سلسلة الاستعلام لأنها ستعيد دائمًا إما القيمة الفعلية للمفتاح المحدد أو سلسلة فارغة إذا كان المفتاح غير موجود. هذا جيد في كثير من الحالات، ولكن في بعض الحالات الأخرى، قد نرغب في معرفة الفرق بين المستخدم الذي يقدم قيمة فارغة أو عدم تقديم قيمة إطلاقًا. إذًا، بناءً على حالة الاستخدام الخاصة بنا، قد يكون من المهم التمييز بين المستخدم الذي يقدم قيمة فارغة للمرشّح أو عدم تقديم مرشح filter إطلاقًا:

  • يُقدّم المستخدم قيمة فارغة للمرشح filter: هنا يحدد المستخدم صراحة قيمة المرشح، ولكنه يضبطها على قيمة فارغة. قد يشير هذا إلى اختيار متعمد لاستبعاد نتائج معينة أو تطبيق شرط مرشحfilter معين. مثلًا، إذا كانت لدينا دالة بحث وقدم المستخدم قيمة فارغة للمرشح، فيمكننا تفسيرها على أنها "إظهار كافة النتائج".
  • لا يوفر المستخدم مرشّح إطلاقًا: هنا لم يقدّم المستخدم مرشّحًا في الطلب. هذا يعني عادةً أن المستخدم يريد استرداد جميع النتائج دون تطبيق أي تصفية محددة. يمكن عدّه سلوكًا افتراضيًا، إذ لا تُطبّق شروط ترشيح محددة.

يسمح لك استخدام Has و Get بالتمييز بين الحالات التي يقدم فيها المستخدم صراحة قيمةً فارغة والحالات التي لا تُقدّم فيها قيمة إطلاقًا. بالتالي إمكانية التعامل مع السيناريوهات المختلفة اعتمادًا على حالة الاستخدام المحددة الخاصة بنا.

يمكنك تحديث دالة getRoot لعرض قيم Has و Get من أجل قيمتي سلسلة الاستعلام first و second.

لنعدّل الدالة main بحيث نستخدم خادم واحد مرة أخرى:

...

func main() {
    ...
    mux.HandleFunc("/hello", getHello)

    ctx := context.Background()
    server := &http.Server{
        Addr:    ":3333",
        Handler: mux,
        BaseContext: func(l net.Listener) context.Context {
            ctx = context.WithValue(ctx, keyServerAddr, l.Addr().String())
            return ctx
        },
    }

    err := server.ListenAndServe()
    if errors.Is(err, http.ErrServerClosed) {
        fmt.Printf("server closed\n")
    } else if err != nil {
        fmt.Printf("error listening for server: %s\n", err)
    }
}

نلاحظ ضمن الدالة main إزالة المراجع والشيفرات المرتبطة بالخادم الثاني serverTwo، لأننا لم نعد بحاجة إلى خوادم متعددة. نقلنا أيضًا تنفيذ الخادم (serverOne سابقًا) خارج التنظيم goroutine وإلى الدالة main. هذا يعني أنه سيجري بدء تشغيل الخادم بطريقة متزامنة، و ينتظر التنفيذ حتى يُغلق الخادم قبل المتابعة.

تغيير آخر كان باستخدام الدالة server.ListenAndServe؛ فبدلًا من استخدام http.ListenAndServe، يمكن الآن استخدامserver.ListenAndServe لبدء الخادم. يتيح ذلك الاستفادة من تهيئة http.Server وأي تخصيصات أجريناها. كذلك أضفنا شيفرة لمعالجة الأخطاء، وذلك للتحقق ما إذا كان الخادم مغلقًا أو واجه أي خطأ آخر أثناء الاستماع. إذا كان الخطأ هو http.ErrServerClosed، فهذا يعني أن عملية الإغلاق عن قصد (إغلاق طبيعي)، وخلاف ذلك ستجري طباعة الخطأ. بإجراء هذه التغييرات سيُشغّل برنامجنا الآن خادم HTTP واحد باستخدام وفقًا لتهيئةhttp.Server ليبدأ في الاستماع للطلبات الواردة، ولن تحتاج إلى تحديثات أخرى من أجل تخصيصات للخادم مستقبلًا.

لنُشغّل ملف البرنامج "main.go" بعد حفظه من خلال الأمر go run:

$ go run main.go

نُشغّل الآن أوامر curl (في الطرفية الثانية). هنا نحتاج إلى إحاطة عنوان URL بعلامات اقتباس مفردة (')، وإلا فقد تُفسر صدفة الطرفية (أي Shell) الرمز & في سلسلة الاستعلام على أنه ميزة "تشغيل الأمر في الخلفية". ضمن عنوان URL نضيف first=1 إلى first و =second إلى second:

$ curl 'http://localhost:3333?first=1&second='

سيكون الخرج على النحو التالي:

This is my website!

لاحظ أن الخرج لم يتغير عن المرة السابقة، لكن إذا عدنا إلى خرج برنامج الخادم، فسنرى أن الخرج الجديد يتضمن قيم سلسلة الاستعلام:

[::]:3333: got / request. first(true)=1, second(true)=

يظهِر خرج قيمة سلسلة الاستعلام first أن التابع Has أعاد true لأن first لها قيمة، وأيضًا التابع Get أعاد القيمة 1. يُظهر ناتج second أنه قد أعاد true لأننا ضمّنّا second، لكن التابع Get لم يُعيد أي شيء إلا سلسلة فارغة. يمكننا أيضًا محاولة إجراء طلبات مختلفة عن طريق إضافة وإزالة first و second أو إسناد قيم مختلفة لنرى كيف تتغير النتائج.

سيستمر البرنامج بالعمل، لذا يجب علينا إيقافه يدويًا من خلال الضغط على المفتاحين "Ctrl+C".

حدّثنا في هذا القسم البرنامج لاستخدام http.Server واحد فقط مرةً أخرى، لكن أضفنا أيضًا دعمًا لقراءة قيم first و second من سلسلة الاستعلام لدالة المعالجة getRoot.

ليس استخدام سلسلة الاستعلام الطريقة الوحيدة للمستخدمين لتقديم مدخلات إلى خادم HTTP، فهناك طريقة أخرى شائعة لإرسال البيانات إلى الخادم وهي تضمين البيانات في متن الطلب. سنعدّل في القسم التالي البرنامج لقراءة نص الطلب من بيانات http.Request*.

قراءة متن الطلب

عند إنشاء واجهة برمجة تطبيقات مبنية على HTTP، مثل واجهة برمجة تطبيقات REST، قد تكون هناك حالات تتجاوز فيها البيانات المُرسلة قيود تضمينها في عنوان URL نفسه، مثل الطول الأعظمي للعنوان. قد نحتاج أيضًا إلى تلقي بيانات لا تتعلق بكيفية تفسير البيانات، وهذا يشير إلى الحالات التي لا ترتبط فيها البيانات المرسلة في متن الطلب ارتباطًا مباشرًا بالمحتوى نفسه. بمعنى آخر: لا توفر هذه البيانات الإضافية إرشادات أو بيانات وصفية حول المحتوى، ولكنها تتضمن معلومات تكميلية تحتاج إلى المعالجة بطريقة منفصل. تخيل صفحة بحث، يمكن فيها للمستخدمين إدخال كلمات للبحث عن عناصر محددة. تمثل كلمات البحث نفسها تفسير المحتوى المقصود، ومع ذلك قد تكون هناك بيانات إضافية في متن الطلب، مثل تفضيلات المستخدم أو الإعدادات، والتي لا ترتبط مباشرةً باستعلام البحث نفسه ولكنها لا تزال بحاجة إلى النظر فيها أو معالجتها بواسطة الخادم. للتعامل مع مثل هذه السيناريوهات، يمكننا تضمين البيانات في متن طلب HTTP باستخدام توابع مثل POST أو PUT.

تُستخدم قيمة http.Request* في http.HandlerFunc للوصول إلى معلومات متعلقة بالطلب الوارد، بما في ذلك متن الطلب، والذي يمكن الوصول إليه من خلال حقل Body.

سنعدّل في هذا القسم دالة المعالجة getRoot لقراءة نص الطلب. لنفتح ملف main.go ونعدّل getRoot لاستخدام ioutil.ReadAll لقراءة حقلr.Body للطلب:

package main

import (
    ...
    "io/ioutil"
    ...
)

...

func getRoot(w http.ResponseWriter, r *http.Request) {
    ...
    second := r.URL.Query().Get("second")

    body, err := ioutil.ReadAll(r.Body)
    if err != nil {
        fmt.Printf("could not read body: %s\n", err)
    }

    fmt.Printf("%s: got / request. first(%t)=%s, second(%t)=%s, body:\n%s\n",
        ctx.Value(keyServerAddr),
        hasFirst, first,
        hasSecond, second,
        body)
    io.WriteString(w, "This is my website!\n")
}

...

تُستخدم الدالة ioutil.ReadAll لقراءة r.Body من http.Request* لاسترداد بيانات متن الطلب. ioutil.ReadAll هي أداة مساعدة تقرأ البيانات من io.Reader حتى تنتهي من القراءة أو ظهور خطأ. نظرًا لأن r.Body هو io.Reader، فيمكن استخدامه لقراءة متن الطلب. نعدّل العبارة fmt.Printf بعد قراءة النص لتضمين محتوى المتن في الخرج.

لنُشغّل ملف البرنامج "main.go" بعد حفظه من خلال الأمر go run:

$ go run main.go

نُشغّل الآن أوامر curl (في الطرفية الثانية)، لتقديم طلب POST مع خيار X POST- وتحديد أن طريقة الطلب يجب أن تكون POST، ومتن طلب باستخدام الخيار b-. يُضبط متن الطلب على السلسلة النصيّة المقدمة، والتي في هذه الحالة هي "This is the body":

$ curl -X POST -d 'This is the body' 'http://localhost:3333?first=1&second='

ليكون الخرج:

This is my website!

خرج دالة المعالجة هو نفسه، لكن سنرى تسجيلات الخادم الخاصة بك قد حُدّثت. سيُرسل طلب POST عندما نشغّل هذا الأمر إلى الخادم المحلي الذي يعمل على المنفذ 3333 مع محتوى المتن المحدد ومعلمات سلسلة الاستعلام. بالتالي يعالج الخادم الطلب ونرى الخرج في سجلات الطرفية للخادم، بما في ذلك قيم سلسلة الاستعلام ومتن الطلب.

[::]:3333: got / request. first(true)=1, second(true)=, body:
This is the body

سيستمر البرنامج بالعمل، لذا يجب علينا إيقافه يدويًا من خلال الضغط على المفتاحين "Ctrl+C".

عدّلنا في هذا القسم البرنامج لقراءة وطباعة متن الطلب. تفتح هذه الإمكانية إمكانيات التعامل مع أنواع مختلفة من البيانات، مثل جسون encoding/json، وتتيح إنشاء واجهات برمجة تطبيقات يمكنها التفاعل مع بيانات المستخدم بطريقة مألوفة.

تجدر الملاحظة إلى أنه لا تأتي جميع بيانات المستخدم في شكل واجهات برمجة التطبيقات. تتضمن العديد من مواقع الويب نماذج يملؤها المستخدمون، والتي ترسل البيانات إلى الخادم مثل بيانات نموذج Form. سنعمل على تحسين البرنامج في القسم التالي ليشمل القدرة على قراءة بيانات النموذج ومعالجتها، بالإضافة إلى جسم الطلب وبيانات سلسلة الاستعلام التي كنا نعمل معها سابقًا.

استرجاع بيانات النموذج

لطالما كان إرسال البيانات باستخدام النماذج أو الاستمارات هو الطريقة القياسية للمستخدمين لإرسال البيانات إلى خادم HTTP والتفاعل مع مواقع الويب، وعلى الرغم من انخفاض شعبية النماذج بمرور الوقت، إلا أنها لا تزال تخدم أغراضًا مختلفة لتقديم البيانات. توفر قيمة http.Request* في http.HandlerFunc طريقةً للوصول إلى هذه البيانات، بطريقة مشابهة للطريقة التي توفر بها الوصول إلى سلسلة الاستعلام ومتن الطلب. سنعدّل في هذا القسم الدالة getHello لتلقي اسم مستخدم من نموذج والرد بتحية شخصية.

لنفتح ملف "main.go" ونعدّل getHello لاستخدام التابع PostFormValue من http.Request*:

...

func getHello(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    fmt.Printf("%s: got /hello request\n", ctx.Value(keyServerAddr))

    myName := r.PostFormValue("myName")
    if myName == "" {
        myName = "HTTP"
    }
    io.WriteString(w, fmt.Sprintf("Hello, %s!\n", myName))
}

...

الآن في دالة getHello المحدَّثة، تجري قراءة قيم النموذج المرسلة إلى دالة المعالجة والبحث عن قيمة تسمى myName. إذا لم يُعثر على القيمة أو كانت سلسلة فارغة، تُضبط القيمة الافتراضية HTTP إلى المتغير myName لمنع عرض اسم فارغ على الصفحة. نعدّل أيضًا الخرج المرسل إلى المستخدم لكي يعرض الاسم الذي قدمه أو HTTP إذا لم يُقدّم أي اسم.

لنُشغّل ملف البرنامج "main.go" بعد حفظه من خلال الأمر go run:

$ go run main.go

نُشغّل الآن أوامر curl (في الطرفية الثانية)، لتقديم طلب POST مع خيار X POST-، وبدلًا من استخدام الخيار b-، نستخدم الخيار 'F 'myName=Sammy لتضمين بيانات النموذج مع حقل myName والقيمة Sammy:

curl -X POST -F 'myName=Sammy' 'http://localhost:3333/hello'

سيكون الخرج:

Hello, Sammy!

يُستخدم التابع r.PostFormValue في getHello لاسترداد قيمة حقل النموذج myName. يبحث هذا التابع تحديدًا عن القيم المنشورة (البيانات المرسلة من العميل إلى الخادم) في متن الطلب. يمكن أيضًا استخدام التابع r.FormValue، والذي يتضمن كلًا من متن النموذج وأي قيم أخرى في سلسلة الاستعلام. إذا استخدمنا ("r.FormValue("myName وأزلنا الخيار F-، فيمكن تضمين myName = Sammy في سلسلة الاستعلام لرؤية القيمة Sammy.

يوصى عمومًا بأن نكون أكثر صرامة وأن نستخدم الدالة r.PostFormValue إذا كنا نريد تحديدًا استرداد القيم من متن النموذج، إذ تتجنب هذه الدالة التعارضات أو الأخطاء المحتملة التي قد تنشأ عن خلط قيم النموذج من مصادر مختلفة. إذًا، تعد الدالة r.PostFormValue خيارًا أكثر أمانًا عند التعامل مع بيانات النموذج، لكن إذا كنا نحتاج إلى المرونة في وضع البيانات في كل من متن النموذج وسلسلة الاستعلام، فربما نضطر إلى الدالة الأخرى.

عند النظر إلى سجلات الخادم، سنرى أن طلب hello/ قد جرى تسجيله بطريقة مشابهة للطلبات السابقة:

[::]:3333: got /hello request

سيستمر البرنامج بالعمل، لذا يجب علينا إيقافه يدويًا من خلال الضغط على المفتاحين "CONTROL+C".

عدّلنا في هذا القسم البرنامج لقراءة اسم من بيانات النموذج المنشورة على الصفحة ثم إعادة هذا الاسم إلى المستخدم.

يمكن أن تسوء بعض الأشياء في هذه المرحلة من البرنامج، لا سيما عند التعامل مع أحد الطلبات، ولن يجري إعلام المستخدمين. لنعدّل في القسم التالي دوال المعالجة لإرجاع رموز حالة HTTP والترويسات headers.

الرد باستجابة تتضمن الترويسات ورمز الحالة

هناك بعض الميزات المستخدمة خلف الكواليس ضمن بروتوكول HTTP لتسهيل الاتصال الفعّال بين المتصفحات والخوادم. إحدى هذه الميزات هي "رموز الحالة"، والتي تعمل مثل وسيلة للخادم لتوضّح للعميل ما إذا كان الطلب ناجحًا أو واجه أي مشكلات في أي من الطرفين.

آلية اتصال أخرى تستخدمها خوادم وعملاء HTTP هي استخدام "حقول الترويسة header fields"، التي تتكون من أزواج ذات قيمة مفتاح يجري تبادلها بين العميل والخادم لنقل المعلومات عن أنفسهم. يمتلك بروتوكول HTTP عدة ترويسات معرّفة مسبقًا، مثل ترويسة Accept، التي يستخدمها العميل لإعلام الخادم بنوع البيانات التي يمكنه التعامل معها. يمكن أيضًا تعريف ترويسات خاصة باستخدام البادئة x- متبوعة بالاسم المطلوب.

سنعمل في هذا القسم على تحسين البرنامج بجعل حقل النموذج myName في دالة المعالجة getHello حقلًا إلزاميًا. بالتالي، إذا لم تُعطى قيمة للحقل myName، فسيستجيب الخادم للعميل برمز الحالة "Bad Request طلب غير صالح" وتضمين الترويسة x-missing-field في الاستجابة، والتي تُعلم العميل بالحقل المفقود من الطلب.

لنفتح ملف "main.go" ونعدّل الدالة getHello وفقًا لما ذُكر:

...

func getHello(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    fmt.Printf("%s: got /hello request\n", ctx.Value(keyServerAddr))

    myName := r.PostFormValue("myName")
    if myName == "" {
        w.Header().Set("x-missing-field", "myName")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    io.WriteString(w, fmt.Sprintf("Hello, %s!\n", myName))
}

سابقًا: إذا كان حقل myName فارغًا، كان يُسند إليه قيمة افتراضية، أما الآن ضمن الدالة getHello المُحدَّثة، نُرسل رسالة خطأ إلى العميل. يُستخدم بدايةً التابع w.Header().Set لضبط الترويسة x-missing-field بقيمة myName في ترويسة الاستجابة، ثم التابع w.WriteHeader لكتابة ترويسات الاستجابة ورمز الحالة "طلب غير صالح" إلى العميل. أخيرًا تُستخدم تعليمة return لضمان إنهاء الدالة وعدم إرسال استجابة إضافية.

من الضروري التأكد من ضبط الترويسات وإرسال رمز الحالة بالترتيب الصحيح، إذ يجب إرسال جميع الترويسات في بروتوكول HTTP قبل الجسم، مما يعني أنه يجب إجراء أي تعديلات على ()w.Header قبل استدعاء w.WriteHeader. عند استدعاء w.WriteHeader يُرسل رمز الحالة والترويسات، ويمكن كتابة المتن بعد ذلك حصرًا. يجب اتباع هذا الأمر لضمان حسن سير استجابة HTTP.

لنُشغّل ملف البرنامج "main.go" بعد حفظه من خلال الأمر go run:

$ go run main.go

نُشغّل الآن الأمر curl -X POST (في الطرفية الثانية) مع المسار hello/ وبدون تضمين F- لإرسال بيانات النموذج. نحتاج أيضًا إلى تضمين الخيار v- لإخبار curl بإظهار الخرج المطوّل حتى نتمكن من رؤية جميع الترويسات والخرج للطلب:

curl -v -X POST 'http://localhost:3333/hello'

سنرى هذه المرة الكثير من المعلومات بسبب استخدامنا الخيار v-:

*   Trying ::1:3333...
* Connected to localhost (::1) port 3333 (#0)
> POST /hello HTTP/1.1
> Host: localhost:3333
> User-Agent: curl/7.77.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 400 Bad Request
< X-Missing-Field: myName
< Date: Wed, 02 Mar 2022 03:51:54 GMT
< Content-Length: 0
< 
* Connection #0 to host localhost left intact

تشير الأسطر الأولى إلى أن curl تحاول إنشاء اتصال بالخادم عند منفذ المضيف المحلي 3333. الأسطر المسبوقة بالرمز < تمثل الطلب المُرسل بواسطة curl، إذ يُرسل طلب POST إلى العنوان أو المسار hello/ باستخدام بروتوكول HTTP 1.1. يتضمن الطلب ترويسات مثل User-Agent و Accept و Host. والجدير بالذكر أن الطلب لا يتضمن متنًا، كما هو موضح في السطر الفارغ.

تظهر استجابة الخادم بالسابقة > بعد إرسال الطلب. يشير السطر الأول إلى أن الخادم قد استجاب برمز حالة "طلب غير صالح" (المعروفة برمز الحالة 400)، كما تُضمّن الترويسة X-Missing-Field التي جرى ضبطها في ترويسة استجابة الخادم، مع تحديد أن الحقل المفقود هو myName. ينتهي الرد بدون أي محتوى في المتن، وهذا ما يتضح من طول المحتوى 0.

إذا نظرنا مرةً أخرى إلى خرج الخادم، فسنرى طلب hello/ للخادم الذي جرت معالجته في الخرج:

[::]:3333: got /hello request

سيستمر البرنامج بالعمل، لذا يجب علينا إيقافه يدويًا من خلال الضغط على المفتاحين "CONTROL+C".

عدّلنا في هذا القسم خادم HTTP ليشمل التحقق من صحة إرسال الطلب hello/. إذا لم يُقدّم اسم في في الطلب، تُضبط ترويسة باستخدام التابع w.Header(). Set للإشارة إلى الحقل المفقود. يُستخدم التابع w.WriteHeader بعد ذلك، لكتابة الترويسات إلى العميل، جنبًا إلى جنب مع رمز الحالة التي تشير إلى "طلب غير صالح". إذًا يُخبر الخادم العميل بوجود مشكلة في الطلب من خلال ضبط الترويسة ورمز الحالة. يسمح هذا الأسلوب بمعالجة الأخطاء بطريقة صحيحة ويوفر ملاحظات للعميل فيما يتعلق بحقل النموذج المفقود.

الخاتمة

تعلمنا كيفية إنشاء خادم HTTP في لغة جو باستخدام حزمة net/http، وطوّرنا الخادم باستخدام مجمّع مخصص للخادم واستخدام نسخhttp.Server متعددة. اسكتشفنا أيضًا طرقًا مختلفة للتعامل مع مُدخلات المستخدم، مثل قيم سلسلة الاستعلام ومتن الطلب وبيانات النموذج. تحققنا أيضًا من صحة الطلب من خلال إعادة ترويسات HTTP مخصصة ورموز حالة إلى العميل.

تتمثل إحدى نقاط القوة في نظام بروتوكول HTTP في توافقه مع العديد من الأطر التي تتكامل بسلاسة مع حزمة net/http. توضح مشاريع مثل github.com/go-chi/chi ذلك من خلال توسيع وظائف خادم http القياسي من خلال تحقيق الواجهة http.Handler. يتيح ذلك للمطورين الاستفادة من البرامج الوسيطة والأدوات الأخرى دون الحاجة لإعادة كتابة المنطق الخاص الخادم، وهذا يسمح بالتركيز على إنشاء برمجيات وسيطة middleware وأدوات أخرى لتحسين الأداء بدلًا من التعامل مع الوظائف الأساسية فقط.

توفر حزمة net/http المزيد من الإمكانات التي لم نتناولها في هذا المقال، مثل العمل مع ملفات تعريف الارتباط وخدمة حركة مرور HTTPS.

ترجمة -وبتصرف- للمقال How To Make an HTTP Server in Go لصاحبه Kristin Davidson.

اقرأ أيضًا


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

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

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



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

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

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

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

  Only 75 emoji are allowed.

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

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

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


×
×
  • أضف...