يُعد بروتوكول HTTP الخيار الأفضل غالبًا عندما يحتاج المطورون إلى إنشاء اتصال بين البرامج. تقدم لغة جو (المعروفة بمكتبتها القياسية الشاملة) دعمًا قويًا لبروتوكول HTTP من خلال حزمة net/http
الموجودة في المكتبة القياسية. لا تتيح هذه الحزمة إنشاء خوادم HTTP فحسب، بل تتيح أيضًا إجراء طلبات HTTP مثل عميل.
سننشئ في هذه المقالة برنامجًا يتفاعل مع خادم HTTP من خلال إجراء أنواع مختلفة من الطلبات. سنبدأ بطلب GET
باستخدام عميل HTTP الافتراضي في لغة جو، ثم سنعمل على تحسين البرنامج ليشمل طلب POST
مع متن الطلب. أخيرًا نُخصص طلب POST
من خلال دمج ترويسة HTTP وتحقيق آلية المهلة، للتعامل مع الحالات التي تتجاوز فيها مدة الطلب حدًا معينًا.
المتطلبات الأولية
لمتابعة هذا المقال التعليمي، سنحتاج إلى:
- إصدار مُثبّت من جو 1.16 أو أعلى، ويمكنك الاستعانة بمقال تثبيت لغة جو Go وإعداد بيئة برمجة محلية على أبونتو Ubuntu لإعداده.
- تثبيت لغة جو وإعداد بيئة برمجة محلية على نظام ماك macOS.
- تثبيت لغة جو وإعداد بيئة برمجة محلية على ويندوز.
- معرفة بكيفية إنشاء خادم HTTP في لغة جو.
- فهم لتنظيمات جو goroutines والقنوات channels. يمكنك الاطلاع على مقالة كيفية تشغيل عدة دوال على التساير في لغة جو Go.
- الإلمام بكيفية إنشاء طلبات HTTP وإرسالها (موصى به).
تقديم طلب GET
نناقش في هذه الفقرة كيفية تقديم طلب GET
باستخدام حزمة net/http
مثل عميل، إذ توفّر لنا هذه الحزمةطرقًا وخيارات متنوعة للتفاعل مع موارد HTTP. أحد الخيارات الشائعة هو استخدام عميل "HTTP عام" مع دوال مثل http.Get
، التي تسمح بإنشاء طلب GET
سريعًا باستخدام عنوان URL ومتن فقط. يمكننا أيضًا إنشاء http.Request
للحصول على مزيد من التحكم وتخصيص جوانب معينة من الطلب. سنبدأ في هذا القسم بإنشاء برنامج أولي يستخدم http.Get
لتقديم طلب HTTP، ونعدّله لاحقًا لاستخدام http.Request
مع عميل HTTP الافتراضي.
استخدام دالة http.Get لتقديم طلب
نستخدم الدالة http.Get
في الإصدار الأولي من البرنامج لإرسال طلب إلى خادم HTTP داخل البرنامج، إذ تُعد هذه الدالة مناسبة لأنها تتطلب الحد الأدنى من التهيئة، أي أنها لا تحتاج إلى الكثير من التفاصيل والإعدادات، وهذا ما يجعلها مثالية لتقديم طلب واحد مباشر. بالتالي، يكون استخدام الدالة http.Get
هو الأسلوب الأنسب عندما نحتاج إلى تنفيذ طلب سريع لمرة واحدة فقط.
كما هو معتاد، سنحتاج لبدء إنشاء برامجنا إلى إنشاء مجلد للعمل ووضع الملفات فيه، ويمكن وضع المجلد في أي مكان على الحاسب، إذ يكون للعديد من المبرمجين عادةً مجلدٌ يضعون داخله كافة مشاريعهم. سنستخدم في هذا المقال مجلدًا باسم "projects"، لذا فلننشئ هذا المجلد وننتقل إليه:
$ mkdir projects
$ cd projects
ننشئ مجلدًا للمشروع وننتقل إليه. لنسميه مثلًا "httpclient":
$ mkdir httpclient
$ cd httpclient
نستخدم الآن محرر نانو nano أو أي محرر آخر تريده لفتح ملف "main.go":
$ nano main.go
نضع بداخله الشيفرة التالية:
package main import ( "errors" "fmt" "net/http" "os" "time" ) const serverPort = 3333 // ضبط منفذ الخادم على 3333
أضفنا في الشيفرة السابقة الحزمة main
لضمان إمكانية تصريف البرنامج وتنفيذه. نستورد أيضًا عدة حزم لاستخدامها في البرنامج. نُعّرف ثابت const
اسمه serverPort
بقيمة 3333
، سيجري استخدامه مثل منفذ لخادم HTTP والعميل.
ننشئ دالة main
ضمن الملف "main.go" ونبدأ بإعداد خادم HTTP مثل تنظيم goroutine:
func main() { // مثل تنظيم جو HTTP نبدأ تشغيل خادم go func() { // نُنشئ جهاز توجيه جديد mux := http.NewServeMux() // معالجة مسار الجذر "/" وطباعة معلومات الطلب mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Printf("Server: %s /\n", r.Method) }) // HTTP تهيئة خادم server := http.Server{ Addr: fmt.Sprintf(":%d", serverPort), Handler: mux, } // بدء تشغيل الخادم، ومعالجة الأخطاء المحتملة if err := server.ListenAndServe(); err != nil { if !errors.Is(err, http.ErrServerClosed) { fmt.Printf("Error running HTTP server: %s\n", err) } } }() // الانتظار لمدة قصيرة للسماح للخادم بالبدء time.Sleep(100 * time.Millisecond) }
تعمل الدالة main
بمثابة نقطة دخول للبرنامج، ونستخدم الكلمة المفتاحية go
للإشارة إلى أن خادم HTTP سيجري تشغيله ضمن تنظيم جو goroutine. نعالج مسار الجذر /
، ونطبع معلومات الطلب باستخدام fmt.Printf
، ونُهيّئ خادم HTTP بالعنوان والمعالج المحددين.
يبدأ الخادم بالاستماع إلى الطلبات باستخدام الدالة ListenAndServe
، ويجري التعامل مع أية أخطاء محتملة؛ فإذا حدث خطأ ولم يكن هذا الخطأ هو http.ErrServerClosed
(إغلاق طبيعي تحدثنا عنه في المقال السابق)، ستُطبع رسالة خطأ. نستخدم الدالة time.Sleep
للسماح للخادم بوقت كافٍ لبدء التشغيل قبل تقديم الطلبات إليه.
نجري الآن بعض التعديلات الإضافية على الدالة mian
من خلال إعداد عنوان URL للطلب، وذلك بدمج اسم المضيف http://localhost
مع قيمة serverPort
باستخدام fmt.Sprintf
. نستخدم بعد ذلك الدالة http.Get
لتقديم طلب إلى عنوان URL هذا:
... requestURL := fmt.Sprintf("http://localhost:%d", serverPort) res, err := http.Get(requestURL) if err != nil { fmt.Printf("error making http request: %s\n", err) os.Exit(1) } fmt.Printf("client: got response!\n") fmt.Printf("client: status code: %d\n", res.StatusCode) }
يرسل البرنامج طلب HTTP باستخدام عميل HTTP الافتراضي إلى عنوان URL المحدد عند استدعاء http.Get
، ويعيد http.Response
في حالة نجاح الطلب أو ظهور قيمة خطأ في حالة فشل الطلب. في حال حدوث خطأ، يطبع البرنامج رسالة الخطأ ويخرج من البرنامج باستخدام os.Exit
مع شيفرة خطأ 1
. إذا نجح الطلب، يطبع البرنامج رسالةً تشير إلى تلقي استجابة، جنبًا إلى جنب مع شيفرة حالة HTTP للاستجابة.
لنُشغّل ملف البرنامج "main.go" بعد حفظه من خلال الأمر go run
:
$ go run main.go
سيكون الخرج على النحو التالي:
server: GET / client: got response! client: status code: 200
يشير السطر الأول من الخرج إلى أن الخادم تلقى طلب GET
من العميل على المسار /
. يشير السطران التاليان إلى أن العميل قد تلقى استجابة من الخادم بنجاح وأن شيفرة حالة الاستجابة كان 200
.
على الرغم من أن الدالة http.Get
ملائمة لإجراء طلبات HTTP سريعة مثل تلك الموضحة في هذا القسم، لكن استخدامhttp.Request
يوفر نطاقًا أوسع من الخيارات لتخصيص الطلب.
استخدام دالة http.Request لتقديم طلب
توفّر لنا http.Request
مزيدًا من التحكم في الطلب أكثر من مجرد استخدام تابع HTTP (على سبيل المثال، GET
و POST
) وعنوان URL فقط. على الرغم من أننا لن نستخدم ميزات إضافية في الوقت الحالي، يسمح لنا استخدام http.Request
بإضافة تخصيصات في أقسام لاحقة من هذا المقال.
يتضمن التحديث الأولي تعديل معالج خادم HTTP لإرجاع استجابة تتضمّن بيانات جسون JSON وهمية باستخدام fmt.Fprintf
. تُنشأ هذه البيانات ضمن خادم HTTP مكتمل باستخدام حزمة encoding/json
. لمعرفة المزيد حول العمل مع بيانات جسون في لغة جو، يمكن الرجوع إلى المقال "كيفية استخدام جسون JSON في لغة جو". نحتاج أيضًا إلى استيراد io/ioutil
لاستخدامه في تحديث لاحق.
نفتح ملف "main.go" لتضمينhttp.Request
كما هو موضح أدناه:
package main import ( ... "io/ioutil" ... ) ... func main() { ... mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Printf("server: %s /\n", r.Method) fmt.Fprintf(w, `{"message": "hello!"}`) }) ...
هدفنا هو استبدال استخدام http.Get
، من خلال استخدام كل من الدالة http.NewRequest
من الحزمة net/http
-لإنشاء http.Request
جديد، والذي يمثل طلب HTTP يمكن إرساله إلى الخادم - و http.DefaultClient
ليكون عميل HTTP افتراضي يوفر لنا استخدام التابع Do
لإرسال http.Request
إلى الخادم واسترداد http.Response
المقابل. يسمح هذا التغيير بمزيد من التحكم في الطلب وإجراء تعديلات عليه قبل الإرسال إلى الخادم:
... requestURL := fmt.Sprintf("http://localhost:%d", serverPort) req, err := http.NewRequest(http.MethodGet, requestURL, nil) if err != nil { fmt.Printf("client: could not create request: %s\n", err) os.Exit(1) } res, err := http.DefaultClient.Do(req) if err != nil { fmt.Printf("client: error making http request: %s\n", err) os.Exit(1) } fmt.Printf("client: got response!\n") fmt.Printf("client: status code: %d\n", res.StatusCode) resBody, err := ioutil.ReadAll(res.Body) if err != nil { fmt.Printf("client: could not read response body: %s\n", err) os.Exit(1) } fmt.Printf("client: response body: %s\n", resBody) }
نبدأ باستخدام الدالة http.NewRequest
لإنشاء قيمة http.Request
. نحدد تابع HTTP للطلب على أنه GET
باستخدام http.MethodGet
. نُنشئ أيضًا عنوان URL للطلب من خلال دمج اسم المضيف http://localhost
مع قيمة serverPort
باستخدام fmt.Sprintf
. نفحص أيضًا متن الطلب إذا كان فارغًا (أي قيمة nil
) لنتعامل أيضًا مع أي خطأ محتمل قد يحدث أثناء إنشاء الطلب.
بخلاف http.Get
الذي يرسل الطلب على الفور، فإنhttp.NewRequest
يُجهّز الطلب فقط ولا يرسله مباشرةً، ويتيح لنا ذلك تخصيص الطلب كما نريد قبل إرساله فعليًا.
بمجرد إعداد http.Request
، نستخدم(http.DefaultClient.Do (req
لإرسال الطلب إلى الخادم باستخدام عميل HTTP الافتراضي http.DefaultClient
. يبدأ التابع Do
الطلب ويعيد الاستجابة المستلمة من الخادم مع أي خطأ محتمل.
بعد الحصول على الرد نطبع معلومات عنه، إذ نعرض أن العميل قد تلقى استجابة بنجاح، إلى جانب شيفرة حالة HTTP للاستجابة.
نقرأ بعد ذلك متن استجابة HTTP باستخدام الدالة ioutil.ReadAll
. يُمثّل متن الاستجابة على أنه io.ReadCloser
، والذي يجمع بين io.Reader
و io.Closer
. نستخدم ioutil.ReadAll
لقراءة جميع البيانات من متن الاستجابة حتى النهاية أو حدوث خطأ. تُعيد الدالة البيانات بقيمة byte[]
، والتي نطبعها مع أي خطأ مصادف.
لنُشغّل ملف البرنامج "main.go" بعد حفظه من خلال الأمر go run
:
$ go run main.go
الخرج مشابه لما سبق مع إضافة بسيطة:
server: GET / client: got response! client: status code: 200 client: response body: {"message": "hello!"}
يُظهر السطر الأول أن الخادم لا يزال يتلقى طلب GET
إلى المسار /
. يتلقى العميل استجابة من الخادم برمز الحالة 200
. يقرأ العميل متن استجابة الخادم ويطبعها أيضًا، والتي تكون في هذه الحالة {" message ":" hello! "}
. يمكننا معالجة استجابة جسون هذه أيضًا باستخدام حزمة encoding/json
، لكن لن ندخل في هذه التفاصيل الآن.
طورّنا في هذا القسم برنامج باستخدام خادم HTTP وقدمنا طلبات HTTP إليه باستخدام طرق مختلفة. استخدمنا في البداية الدالة http.Get
لتنفيذ طلب GET
للخادم باستخدام عنوان URL الخاص بالخادم فقط. حدّثنا بعد ذلك البرنامج لاستخدام http.NewRequest
لإنشاء قيمة http.Request
، ثم استخدمنا التابع Do
لعميل HTTP الافتراضيhttp.DefaultClient
، لإرسال الطلب وطباعة متن الاستجابة.
تُعد طلبات GET
مفيدةً لاسترداد المعلومات من الخادم، لكن بروتوكول HTTP يوفر طرقًا أخرى متنوعة للاتصال بين البرامج. إحدى هذه الطرق هي طريقة POST
، والتي تتيح لك إرسال معلومات من برنامجك إلى الخادم.
إرسال طلب POST
يُستخدم طلب GET
في واجهة برمجة تطبيقات REST، فقط لاسترداد المعلومات من الخادم، لذلك لكي يُشارك البرنامج بالكامل في REST، يحتاج أيضًا إلى دعم إرسال طلبات POST
، وهو عكس طلب GET
تقريبًا، إذ يرسل العميل البيانات إلى الخادم داخل متن الطلب.
سنُعدّل في هذا القسم البرنامج لإرسال طلب POST
بدلُا من طلب GET
، إذ سيتضمن طلب POST
متنًا للطلب، وسنُعدّل الخادم لتوفير معلومات أكثر تفصيلًا حول الطلبات الواردة من العميل.
نفتح ملف "main.go" ونُضمّن الحزم الإضافية التالية:
... import ( "bytes" "errors" "fmt" "io/ioutil" "net/http" "os" "strings" "time" ) ...
ثم نُعدّل دالة المعالجة لطباعة معلومات متنوعة حول الطلب الوارد، مثل قيم سلسلة الاستعلام وقيم الترويسة ومتن الطلب:
... mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Printf("server: %s /\n", r.Method) fmt.Printf("server: query id: %s\n", r.URL.Query().Get("id")) fmt.Printf("server: content-type: %s\n", r.Header.Get("content-type")) fmt.Printf("server: headers:\n") for headerName, headerValue := range r.Header { fmt.Printf("\t%s = %s\n", headerName, strings.Join(headerValue, ", ")) } reqBody, err := ioutil.ReadAll(r.Body) if err != nil { fmt.Printf("server: could not read request body: %s\n", err) } fmt.Printf("server: request body: %s\n", reqBody) fmt.Fprintf(w, `{"message": "hello!"}`) }) ...
أضفنا في هذا التحديث لمعالج طلب HTTP الخاص بالخادم تعليمات fmt.Printf
لتوفير مزيد من المعلومات حول الطلب الوارد. يُستخدم التابع r.URL.Query ().Get
لاسترداد قيمة سلسلة الاستعلام المسماة id
. يُستخدم التابع r.Header.Get
للحصول على قيمة الترويسة content-type
. تتكرر حلقة for
معr.Header
فوق كل ترويسة HTTP يستقبلها الخادم وتطبع اسمها وقيمتها. يمكن أن تكون هذه المعلومات ذات قيمة لأغراض استكشاف الأخطاء وإصلاحها في حالة ظهور أي مشكلات مع سلوك العميل أو الخادم. تُستخدم أيضًا الدالة ioutil.ReadAll
لقراءة متن الطلب من r.Body
.
بعد تحديث دالة المعالجة للخادم، نُعدّل شيفرة الطلب في الدالة main
، بحيث ترسل طلب POST
مع متن الطلب:
... time.Sleep(100 * time.Millisecond) jsonBody := []byte(`{"client_message": "hello, server!"}`) bodyReader := bytes.NewReader(jsonBody) requestURL := fmt.Sprintf("http://localhost:%d?id=1234", serverPort) req, err := http.NewRequest(http.MethodPost, requestURL, bodyReader) ...
في هذا التعديل قُدّم متغيران جديدان: يمثل الأول jsonBody
بيانات جسون مثل قيم من النوع byte[]
بدلًا من سلسلة string
. يُستخدم هذا التمثيل لأنه عند ترميز بيانات جسون باستخدام حزمة encoding/json
، فإنها تُرجع byte[]
بدلًا من سلسلة.
المتغير الثاني bodyReader
، هو bytes.Reader
مُغلّف ببيانات jsonBody
. يتطلب http.Request
أن يكون متن الطلب من النوع io.Reader
. نظرًا لأن قيمة jsonBody
هي byte[]
ولا تُحقق io.Reader
مباشرةً، يُستخدم bytes.Reader
لتوفير واجهة io.Reader
الضرورية، مما يسمح باستخدام قيمة jsonBody
على أنها متن الطلب.
نُعدّل أيضًا المتغير requestURL
لتضمين قيمة سلسلة الاستعلام id = 1234
، وذلك لتوضيح كيف يمكن تضمين سلسلة استعلام في عنوان URL للطلب إلى جانب مكونات عنوان URL القياسية الأخرى. أخيرًا نُعدّل استدعاء الدالة http.NewRequest
لاستخدام التابع POST
مع http.MethodPost
، وضبط متن الطلب على bodyReader
، وهو قارئ بيانات جسون.
لنُشغّل ملف البرنامج "main.go" بعد حفظه من خلال الأمر go run
:
$ go run main.go
سيكون الخرج بالتأكيد أطول من السابق، بسبب طباعة معلومات إضافية:
server: POST / server: query id: 1234 server: content-type: server: headers: Accept-Encoding = gzip User-Agent = Go-http-client/1.1 Content-Length = 36 server: request body: {"client_message": "hello, server!"} client: got response! client: status code: 200 client: response body: {"message": "hello!"}
يُظهر الخرج السلوك المُحدّث للخادم والعميل بعد إجراء التغييرات لإرسال طلب POST
؛ إذ يشير السطر الأول إلى أن الخادم قد تلقى طلب POST
للمسار /
؛ ويعرض السطر الثاني قيمة سلسلة الاستعلام id
، والتي تحمل القيمة 1234
في هذا الطلب؛ بينما يعرض السطر الثالث قيمة ترويسة Content-Type
، وهي فارغة في هذه الحالة.
قد يختلف ترتيب الترويسات المطبوعة من r.Headers
أثناء التكرار عليها باستخدام range
في كل مرة نُشغّل فيها البرنامج، فبدءًا من إصدار جو 1.12، لا يمكن ضمان أن يكون الترتيب الذي يجري فيه الوصول إلى العناصر هو نفس الترتيب الذي تُدرج في العناصر في الرابط map
، وهذا لأن لغة جو تُحقق الروابط باستخدام بنية لا تحافظ على ترتيب الإدراج، لذلك قد يختلف ترتيب طباعة الترويسات عن الترتيب الذي جرى استلامها به فعليًا في طلب HTTP. الترويسات الموضحة في المثال هي Accept-Encoding
و User-Agent
و Content-Length
. قد نرى أيضًا قيمة مختلفة للترويسة User-Agent
عما رأيناه أعلاها اعتمادًا على إصدار جو المُستخدم.
يعرض السطر التالي متن الطلب الذي استلمه الخادم، وهو بيانات جسون التي يرسلها العميل. يمكن للخادم بعد ذلك استخدام حزمة encoding/json
لتحليل بيانات جسون هذه التي أرسلها العميل وصياغة استجابة. تمثل الأسطر التالية خرج العميل، مما يشير إلى أنه تلقى استجابةً مع رمز الحالة 200
. يحتوي متن الاستجابة المعروض في السطر الأخير على بيانات جسون أيضًا.
أجرينا في القسم السابق العديد من التحديثات لتحسين البرنامج. أولًا، استبدلنا طلب GET
بطلب POST
، مما يتيح للعميل إرسال البيانات إلى الخادم في متن الطلب، إذ أنجزنا هذا عن طريق تحديث شيفرة الطلب وتضمين متن الطلب باستخدام byte[]
. عدّلنا أيضًا دالة معالجة الطلب الخاصة بالخادم لتوفير معلومات أكثر تفصيلًا حول الطلبات الواردة، مثل قيمة سلسلة الاستعلام id
وترويسة Content-Type
وجميع الترويسات المستلمة من العميل.
تجدر الإشارة إلى أنه في الخرج المعروض، لم تكن ترويسة Content-Type
موجودة في طلب HTTP، مما يشير عادةً إلى نوع المحتوى المُرسل في المتن. سنتعلم في القسم التالي المزيد عن كيفية تخصيص طلب HTTP، بما في ذلك ضبط ترويسة Content-Type
لتحديد نوع البيانات المُرسلة.
تخصيص طلب HTTP
يسمح تخصيص طلب HTTP بنقل مجموعة واسعة من أنواع البيانات بين العملاء والخوادم. كان بإمكان عملاء HTTP سابقًا افتراض أن البيانات المستلمة من خادم HTTP هي HTML غالبًا، أما اليوم يمكن أن تشمل البيانات تنسيقات مختلفة، مثل جسون والموسيقى والفيديو وغيرهم. لنقل معلومات إضافية متعلقة بالبيانات المرسلة، يشتمل بروتوكول HTTP على العديد من الترويسات، أهمها الترويسة Content-Type
، التي تُخبر الخادم (أو العميل، اعتمادًا على اتجاه البيانات) بكيفية تفسير البيانات التي يتلقاها.
سنعمل في القسم التالي على تحسين البرنامج عن طريق ضبط الترويسة Content-Type
في طلب HTTP للإشارة إلى أن الخادم يجب أن يتوقع بيانات جسون. ستتاح لنا الفرصة أيضًا لاستخدام عميل HTTP مخصص بدلًا من http.DefaultClient
الافتراضي، مما يتيح لنا تخصيص إرسال الطلب وفقًا للمتطلبات المحددة.
نفتح ملف "main.go" لإجراء التعديلات الجديدة على الدالة main
:
... req, err := http.NewRequest(http.MethodPost, requestURL, bodyReader) if err != nil { fmt.Printf("client: could not create request: %s\n", err) os.Exit(1) } req.Header.Set("Content-Type", "application/json") client := http.Client{ Timeout: 30 * time.Second, } res, err := client.Do(req) if err != nil { fmt.Printf("client: error making http request: %s\n", err) os.Exit(1) } ...
بدايةً، جرى النفاذ إلى ترويسات http.Request
باستخدام req.Header
وضبطنا ترويسة Content-Type
على application/json
، إذ يُمثّل ضبط هذه الترويسة إشارةً للخادم إلى أن البيانات الموجودة في متن الطلب مُنسّقة بتنسيق جسون، وهذا يساعد الخادم في تفسير متن الطلب ومعالجته بطريقة صحيحة.
أنشأنا أيضًا نسخة خاصة من البنية http.Client
في المتغير client
. يتيح لنا هذا مزيدًا من التحكم في سلوك العميل. يمكننا في هذه الحالة ضبط قيمة المهلة Timeout
للعميل على 30 ثانية؛ وهذا يعني أنه إذا لم يجري تلقي الرد في غضون 30 ثانية، سيستسلم العميل ويتوقف عن الانتظار. هذا مهم لمنع البرنامج من إهدار الموارد بإبقاء الاتصالات مفتوحة إلى أجل غير مسمى.
من خلال إنشاء http.Client
خاص بنا مع مهلة محددة، فإننا تتأكد من أن البرنامج لديه حد محدد للمدة التي سينتظرها الرد، ويختلف هذا عن استخدام http.DefaultClient
الافتراضي، والذي لا يحدد مهلة وسينتظر إلى أجل غير مسمى. لذلك يعد تحديد المهلة أمرًا بالغ الأهمية لإدارة الموارد بفعالية وتجنب مشكلات الأداء المحتملة.
أخيرًا عدّلنا الطلب لاستخدام التابع Do
من المتغير client
. هذا التغيير واضح ومباشر لأننا كنا نستخدم التابع Do
في جميع أنحاء البرنامج، سواء مع العميل الافتراضي أو العميل المخصص. الاختلاف الوحيد الآن هو أننا أنشأنا صراحةً متغيرًا من http.Client
.
توفر هذه التحديثات مزيدًا من التحكم والمرونة لبرنامجنا، مما يسمح بضبط ترويسات محددة وإدارة المهلات الزمنية بالطريقة المناسبة. من خلال فهم وتخصيص هذه الجوانب لطلب HTTP، يمكننا ضمان الأداء الأمثل والتواصل الموثوق مع الخادم.
لنُشغّل ملف البرنامج "main.go" بعد حفظه من خلال الأمر go run
:
$ go run main.go
يجب أن تكون مخرجاتك مشابهة جدًا للإخراج السابق ولكن مع مزيد من المعلومات حول نوع المحتوى:
server: POST / server: query id: 1234 server: content-type: application/json server: headers: Accept-Encoding = gzip User-Agent = Go-http-client/1.1 Content-Length = 36 Content-Type = application/json server: request body: {"client_message": "hello, server!"} client: got response! client: status code: 200 client: response body: {"message": "hello!"}
يمكننا ملاحظة أن الخادم قد استجاب بقيمة الترويسة Content-Type
المُرسلة من قِبل العميل وهي من النوع application/json
، ونلاحظ القيمة content-type
وهي أيضًا application/json
.
يسمح وجود Content-Type
بالمرونة في التعامل مع أنواع مختلفة من واجهات برمجة التطبيقات في وقت واحد، إذ يمكّن الخادم من التمييز بين الطلبات التي تحتوي على بيانات جسون والطلبات التي تحتوي على بيانات XML. يصبح هذا التمييز مهمًا خاصةً عند التعامل مع واجهات برمجة التطبيقات التي تدعم تنسيقات بيانات مختلفة.
لفهم كيفية عمل مهلة العميل Timeout، يمكننا تعديل الشيفرة لمحاكاة سيناريو يستغرق فيه الطلب وقتًا أطول من المهلة المحددة، وذلك لكي نتمكن من ملاحظة تأثير المهلة.
يمكننا إضافة استدعاء دالة time.Sleep
ضمن دالة المعالجة لخادم HTTP، إذ تؤدي هذه الدالة إلى تأخير مُصطنع في استجابة الخادم. للتأكد من أن التأخير يتجاوز قيمة المهلة التي حددناها، نجعل time.Sleep
يستمر لمدة 35 ثانية، وبالتالي سيظهر الخادم وكأنه غير مستجيب، مما يتسبب في انتظار العميل للاستجابة:
... func main() { go func() { mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { ... fmt.Fprintf(w, `{"message": "hello!"}`) time.Sleep(35 * time.Second) }) ... }() ... }
لنُشغّل ملف البرنامج "main.go" بعد حفظه من خلال الأمر go run
:
$ go run main.go
عند تنفيذ البرنامج مع إضافة (time.Sleep (35 * time.Second
في دالة المعالجة، سيتغير سلوك العميل والخادم، وذلك بسبب التأخير المصطنع الذي حدث في استجابة الخادم. نلاحظ أيضًا أن البرنامج يستغرق وقتًا أطول للخروج مقارنةً بالسابق، وذلك لأن البرنامج ينتظر انتهاء طلب HTTP قبل الإنهاء. الآن مع الانتظار الجديد الذي أضفناه، لن يكتمل طلب HTTP حتى تصل الاستجابة أو نتجاوز المهلة المحددة البالغة 30 ثانية، ونظرًا لأن الخادم متوقف مؤقتًا لمدة 35 ثانية، متجاوزًا مدة المهلة، فسيلغي العميل الطلب بعد 30 ثانية، وستجري عملية معالجة خطأ المهلة الزمنية:
server: POST / server: query id: 1234 server: content-type: application/json server: headers: Content-Type = application/json Accept-Encoding = gzip User-Agent = Go-http-client/1.1 Content-Length = 36 server: request body: {"client_message": "hello, server!"} client: error making http request: Post "http://localhost:3333?id=1234": context deadline exceeded (Client.Timeout exceeded while awaiting headers) exit status 1
نلاحظ تلقي الخادم طلب POST
بطريقة صحيحة إلى المسار /
، والتقط أيضًا معامل الاستعلام id
بقيمة1234
. حدد الخادم نوع محتوى الطلب content-type
على أنه application/json
بناءً على ترويسة نوع المحتوى، كما عرض الخادم الترويسات التي تلقاها. طبع الخادم أيضًا متن الطلب.
نلاحظ حدوث خطأ context deadline exceeded
من جانب العميل أثناء طلب HTTP كما هو متوقع. تشير رسالة الخطأ إلى أن الطلب تجاوز المهلة الزمنية البالغة 30 ثانية (الخادم تلقى الطلب وعالجه، لكنه بسبب وجود تعليمة انتظار من خلال استدعاء دالة time.Sleep
، سيبدو وكأنه لم ينتهي من معالجته). بالتالي سيفشل استدعاء التابع client.Do
، ويخرج البرنامج مع رمز الحالة 1
باستخدام (os.Exit (1
.
يسلط هذا الضوء على أهمية ضبط قيم المهلة الزمنية بطريقة مناسبة لضمان عدم تعليق الطلبات إلى أجل غير مسمى.
أجرينا خلال هذا القسم تعديلات على البرنامج لتخصيص طلب HTTP من خلال إضافة ترويسة Content-Type
إليه. عدّلنا البرنامج أيضًا من خلال إنشاء http.Client
جديد مع مهلة زمنية 30 ثانية، والتي استخدمناها لاحقًا لتنفيذ طلب HTTP. فحصنا أيضًا تأثير المهلة من خلال استخدام التعليمة time.Sleep
داخل معالج طلب HTTP. سمح لنا هذا بملاحظة أن استخدام عميل http.Client
مع المهلات الزمنية المضبوطة بدقة أمرٌ بالغ الأهمية لمنع الطلبات من الانتظار إلى أجل غير مسمى. بالتالي اكتسبنا رؤى حول أهمية ضبط المهلات المناسبة لضمان معالجة الطلب بكفاءة ومنع المشكلات المحتملة مع الطلبات التي قد تظل خاملة.
الخاتمة
اكتسبنا خلال هذا المقال فهمًا شاملًا للعمل مع طلبات HTTP في لغة جو باستخدام حزمة net/http
؛ إذ بدأنا بتقديم طلب GET
إلى خادم HTTP باستخدام كل من دالة http.Get
و http.NewRequest
مع عميل HTTP الافتراضي؛ ثم وسّعنا بعد ذلك معرفتنا عن طريق إجراء طلب POST
مع متن طلب باستخدام bytes.NewReader
.
تعلمنا كيفية تخصيص الطلب عن طريق ضبط ترويسة Content-Type
باستخدام التابع Set
في حقل ترويسة http.Request
.اكتشفنا أهمية ضبط المهلات للتحكم في مدة الطلبات من خلال إنشاء عملي http.Client
.
تجدر الإشارة إلى أن حزمة net/http
توفر دوال أكثر مما غطيناه في هذا المقال، فعلى سبيل المثال يمكن استخدام دالة http.Post
لتقديم طلبات POST
والاستفادة من ميزات مثل إدارة ملفات تعريف الارتباط.
من خلال إتقان هذه المفاهيم، نكون قد زودنا أنفسنا بالمهارات الأساسية للتفاعل مع خوادم HTTP وإنشاء أنواع مختلفة من الطلبات باستخدام لغة جو.
ترجمة -وبتصرف- للمقال How To Make HTTP Requests in Go لصاحبه Kristin Davidson.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.