يكرّس العديد من المطورين جزءًا من وقتهم لبناء خوادم تُسهّل توزيع المحتوى عبر الإنترنت. يُعد بروتوكول النقل التشعبي Hypertext Transfer Protocol-أو اختصارًا HTTP- من أهم الوسائل المستخدمة لتوزيع المحتوى مهما كان نوع البيانات عبر الإنترنت. تتضمّن مكتبة لغة جو القياسية وظائفًا مدمجة لإنشاء خادم HTTP لتخديّم محتوى الويب، أو إنشاء طلبات HTTP للتواصل مع هذه الخوادم.
سنتعلم في هذا المقال كيفية إنشاء خادم HTTP باستخدام مكتبة لغة جو القياسية، وكيفية توسيع وظائف الخادم لاستخراج البيانات من أجزاء مختلفة من طلب HTTP، مثل سلسلة الاستعلام والمتن Body وبيانات النموذج في الطلب. سنتعلّم أيضًا كيفية تعديل استجابة الخادم عن طريق إضافة ترويسات HTTP ورموز حالة مخصصة status codes، وبالتالي السماح للمطورين بتخصيص سلوك الخادم الخاص بهم.
توضيح بعض المصطلحات:
في سياق إنشاء خادم HTTP باستخدام مكتبة جو القياسية، تشير المصطلحات "سلسلة استعلام الطلب" و"المتن" و"بيانات النموذج" إلى أجزاء مختلفة من طلب HTTP.
-
سلسلة الاستعلام: هي جزء من عنوان URL الذي يأتي بعد رمز علامة الاستفهام (
?
). يحتوي عادةً على أزواج ذات قيمة مفتاح مفصولة بعلامات&
. -
المتن أو النص الأساسي: يحتوي متن طلب HTTP على البيانات التي يرسلها العميل إلى الخادم، مثل JSON أو XML أو النص العادي. يُستخدم بكثرة في طلبات من نوع
POST
وPUT
وPATCH
لإرسال البيانات إلى الخادم. -
بيانات النموذج: تُرسل عادةً بيانات النموذج مثل جزء من طلب
POST
مع ضبط ترويسة "نوع المحتوى" على "application/x-www-form-urlencoded" أو "multipart/form-data". وهو يتألف من أزواج مفتاح - قيمة على غرار سلسلة الاستعلام، ولكن يُرسل في متن الطلب.
بروتوكول HTTP
هو بروتوكول يعمل على مستوى التطبيقات، ويستخدم لنقل مستندات الوسائط التشعبية، مثل صفحات HTML عبر الإنترنت. يعمل HTTP بمثابة أساس لاتصالات البيانات على شبكة الويب العالمية.
يتبع HTTP نموذج خادم-العميل، إذ يرسل العميل (عادةً متصفح ويب) طلبًا إلى الخادم، ويستجيب الخادم بالمعلومات المطلوبة. تُستخدم بعض الدوال، مثل GET
و POST
و PUT
و DELETE
لتسهيل عملية الاتصال بين العميل والخادم.
يمكنك الاطلاع على مقال مدخل إلى HTTP على أكاديمية حسوب لمزيدٍ من المعلومات حول بروتوكول HTTP.
المتطلبات الأولية
لمتابعة هذا المقال التعليمي، سنحتاج إلى:
- إصدار مُثبّت من جو 1.16 أو أعلى، ويمكنك الاستعانة بمقال تثبيت لغة جو Go وإعداد بيئة برمجة محلية على أبونتو Ubuntu لإعداده.
- تثبيت لغة جو وإعداد بيئة برمجة محلية على نظام ماك macOS.
- تثبيت لغة جو وإعداد بيئة برمجة محلية على ويندوز.
- القدرة على استخدام أداة curl لإجراء طلبات ويب.
- إلمام بكيفية استخدام جسون JSON في لغة جو.
- معرفة بكيفية استخدام السياقات Contexts في لغة جو Go.
- فهم لتنظيمات جو goroutines والقنوات channels. يمكنك الاطلاع على مقالة كيفية تشغيل عدة دوال على التساير في لغة جو Go.
- الإلمام بكيفية إنشاء طلبات 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
، يمكننا الوصول إلى قيم سلسلة الاستعلام المرتبطة بالطلب. هناك طريقتان يمكن استخدامهما للتفاعل مع بيانات سلسلة الاستعلام:
-
يتحقق التابع
Has
ما إذا كانت سلسلة الاستعلام تحتوي على قيمة بمفتاح معين، مثل"first" أو "second". يعيد التابع قيمة بوليانيةbool
تشير إلى وجود المفتاح. -
يسترد التابع
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.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.