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

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

سننشئ في هذه المقالة برنامجًا يتفاعل مع خادم HTTP من خلال إجراء أنواع مختلفة من الطلبات. سنبدأ بطلب GET باستخدام عميل HTTP الافتراضي في لغة جو، ثم سنعمل على تحسين البرنامج ليشمل طلب POST مع متن الطلب. أخيرًا نُخصص طلب POST من خلال دمج ترويسة 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.

اقرأ أيضًا


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

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

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



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

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

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

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   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.


×
×
  • أضف...