25  التخاطب عبر الشبكة

رأينا في الدرس السابق كيفية التواصل مع خادم عن طريق مكتبة العميل التابعة للخدمة (GeoPy). لكن ما الآلية التي جرَّدتها عنا هذه المكتبة وأخفتها؟

تخضع المراسلات بين البرمجيات التطبيقية لقواعد تواصل تسمى HTTP (Hyper-Text Transfer Protocol)؛ وهي حرفيًّا بمعنى قواعد تناقل النص الفائق. وسيأتي بيان معنى الفائقية عند شرح HTML (Hyper-text Markup Language) لأن هذين المفهومين مرتبطان. لكن الذي يهمنا الآن أنها أحد أكثر لغات التواصل استخدامًا على الشبكة. فالاتصال بين الخادم والمخدوم يكون بها وإن كان البرنامج نفسه مكتوبًا بلغة أخرى مثل بايثون. ومن نظائرها:

وغيرها كثير جدًّا.

فحتى نصل إلى خدمات كثيرة على الشبكة ونستفيد منها؛ لابد أن نفهم هذه الآلية. وهي مبنيَّة على أمور، منها: العنوان، والطلب، والجواب.

عنوان المورِد الموحَّد

إن اصطلاح عنوان المورِد الموحَّد URL (Uniform Resource Locator) هو الذي نقصده عندما نقول رابط (Link أو Hyper-link reference وتختصر href) لأن صياغة الروابط عادةً ما تتبع هذه الصيغة الموحَّدة. وهي التي تتضمَّنُ عادةً جزئيَّة com. أو net. ونحوهما؛ وتتضمن بعدها مسارًا فرعيًّا. ونمثل على ذلك بمثال بسيط:

https://github.com/HassanAlgoz/python

ففيه:

  • الصياغة: https
  • العنوان الأساسي: github.com
  • المسار الفرعي: HassanAlgoz/python/
  • المورِد: python

أما مصطلح المورِد (Resource) فعامٌّ يشمل شيئًا مُجرَّدًا يتم الوصول إليه بعنوان مُصاغٍ بصيغة متفقٍ عليها. سواءٌ كان موجودًا قبل الوصول إليه، أو يتمُّ إنتاجه متى طُلِب. فقد يكون ملفَّ بيانات أو صفحةً تُعرَض أو معالجةً قيِّمة.

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

الطلب والجواب

ومن مفاهيم لغة التخاطب عبر تطبيقات الشبكة أيضًا:

الطلب

الطلب (Request) وهو الرسالة التي تشمل:

  • المحتوى: body / content
  • العميل الذي يمثِّل المستخدم (المُرسِل): user-agent
  • المستضيف الذي عليه الخادم (المُرسَل إليه): Host

نوع الطلب (Method) وأهمها:

  • GET لطلب الحصول على مورِد معيَّن
  • DELETE لطلب حذف مورِد معيَّن

ففيهما يتمُّ تحديد عنوان المورِد.

ثم لدينا:

  • POST لطلب إنشاء مورِد؛ يتمُّ تحديد تفاصيل الإنشاء في محتوى الرسالة
  • PUT لطلب تعديل مورِد؛ يتمُّ تحديد تفاصيل التعديل في محتوى الرسالة

الرؤوس (Headers) هي معلومات عن المعلومات التي في الطلب نفسه أو محتواه؛ بعضها أصلي وبعضها إضافي.

  • مثلاً: Content-Type: text/csv تعني أن البيانات المرسلة عبارة عن ملف بصيغة CSV. وهو أصلي.
  • أما الإضافي فيبدأ بحرف X على هذا النحو: x-api-key: 1234567890 هو مفتاح التطبيق الذي يسمح للطلب بالتعريف بصاحب الحساب لإتاحة الخدمة له.

المحتوى (Body) هي البيانات المُرْسَلة أو المُسْتَلَمة؛ سواءٌ في الطلب أو جوابه.

مثلا بيانات عبارة عن قاموس بصيغة JSON:

{"name": "John", "age": 30}

جواب الطلب

جواب الطلب (Response) وهو مثل الطلب في خصائصه؛ إلا أنَّه بعكس الاتجاه: من الخادم إلى العميل.

رمز حالة الطلب (Status Code) وتنقسم إلى نطاقات، وكثيرٌ منها مُهمَل غير مُستعمل:

  • نطاق 100-199 (فقط للعلم - ولا تهمنا)
  • نطاق 200-299 تعني أن الطلب تمُّ إنجازه بنجاح.
  • نطاق 300-399 إعادة توجيه
  • نطاق 400-499 إشكال من جهة العميل
    • 400 -> Bad Request البيانات المُرسلة ليست صالحة
    • 401 -> Unauthorized المفتاح مفقود أو غير صالح
    • 403 -> Forbidden المفتاح صالح لكن ليس كافيًا للوصول
    • 404 -> Not Found ما طلبته غير موجود
  • نطاق 500-599 إشكال من جهة الخادم

ولمزيد من التفاصيل راجع: HTTP overview.

مثال: خدمة صور القطط

ماذا لو لم تتوفَّر مكتبة خاصَّة بمزوِّد الخدمة؟ في هذه الحالة سنكتب نحن تفاصيل الاتصال بالخادم المزوِّد. وذلك يتطلب معرفة لغة التخاطب بين الخادم والعميل (HTTP).

يجب علينا أولاً تثبيت مكتبة httpx باستعمال uv:

uv add httpx

ونمثل لمزود خدمة معلومات عن القطط (The Cat API)، وقد حصلنا على مفتاح التطبيق (API KEY) من خلال التسجيل في الموقع.

ولمعرفة الاستفادة من أي مزود خدمة، فإننا ندخل إلى صفحة المطورين، وتسمى (API Documentation). ومنها نعرف أن المسار الذي يجب أن نطلبه هو https://api.thecatapi.com/v1/images/search، وهو يعطينا صورة قط عشوائية.

باستعمال مكتبة عامة لعميل HTTP يمكننا الوصول لأي خدمة مقدَّمة من جهة خادم يتخاطب بلغة HTTP. وهو ما يُعرف أيضًا بخادم ويب (Web Server). فها نحن هنا نحدد جميع ما نريد:

  • نوع الطلب: GET
  • المسار: /v1/images/search
  • العنوان الرئيسي: api.thecatapi.com (وهو الموقع الذي يوجد عليه الملف)
  • الرؤوس: x-api-key: 1234567890 (وهو مفتاح التطبيق الذي يسمح للطلب بالوصول إلى الخدمة)

sequenceDiagram
    participant Client
    participant Server

    Client->>Server: GET /v1/images/search HTTP/1.1
    activate Server
    Server->>+Client: HTTP/1.1 200 OK
    deactivate Server

import httpx

client = httpx.Client()

request = client.build_request(
    method="GET",
    url="https://api.thecatapi.com/v1/images/search",
    headers={
        "x-api-key": "live_9Cj8P0h75D5h2D7Y2H8MYEuuTmTXjT412xNlbVBouHxn2sEnAjr1dr4JMfIn4Mr4"
    }
)

وفي الواقع يتم تكوين الطلب كنص (string) بهذا الشكل (ونحن هنا نطبعه بصيغته النصية لغرضٍ تعليمي):

print(f"""
{request.method} {request.url.path} HTTP/1.1
{"\n".join([f"{k}: {v}" for k, v in request.headers.items()])}

""")

GET /v1/images/search HTTP/1.1
host: api.thecatapi.com
accept: */*
accept-encoding: gzip, deflate
connection: keep-alive
user-agent: python-httpx/0.28.1
x-api-key: live_9Cj8P0h75D5h2D7Y2H8MYEuuTmTXjT412xNlbVBouHxn2sEnAjr1dr4JMfIn4Mr4

فأما السطر الأول:

GET /v1/images/search HTTP/1.1

فمكون من ثلاثة أجزاء:

  • نوع الطلب: GET ذلك أننا نريد حصول على معلومات (لا إنشاءها ولا تغييرها)
  • المسار: /v1/images/search هو المسار الفرعي الذي يحدد الخدمة المطلوبة
  • نسخة قوانين التواصل: HTTP/1.1 فشكل الطلب والجواب يعتمد على هذه النسخة

وأما الوُجهة فمحددة بالرأس Host على هذا النحو:

host: api.thecatapi.com

والترويسة الأخيرة x-api-key هي ليست من الترويسات المعرَّفة في HTTP، ولكنه اتفاق بين العميل والخادم:

x-api-key: ...

وقد أضاف العميل رؤسًا لم نعيِّنها وهي: accept, accept-encoding, connection, user-agent، وإليك معناها:

  • accept: */* وتعني أننا نقبل الجواب بأي صيغة؛ سواء كانت بصيغة JSON أو HTML أو أي صيغة أخرى
  • accept-encoding: gzip, deflate تعني أن العميل يتوقع المحتوى مضغوطًا بصيغة ضغط معينة
  • connection: keep-alive تعني أن العميل يريد الحفاظ على الاتصال بالخادم
  • user-agent: python-httpx/0.28.1 هي ترويسة إجبارية ولا يهم ما تكون قيمتها. لكنها تعرِّف بهوية العميل

وبعد ذلك نرسل الطلب بالإجراء send ونحصل على جواب response:

response = client.send(request)

والجواب نفسه له الصيغة النصية التالية:

print(f"""
{response.status_code} {response.reason_phrase}
{"\n".join([f"{k}: {v}" for k, v in response.headers.items()])}

{response.text}
""")

200 OK
x-dns-prefetch-control: off
x-frame-options: SAMEORIGIN
strict-transport-security: max-age=15552000; includeSubDomains
x-download-options: noopen
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
vary: Origin
expires: Tue, 03 Jul 2001 06:00:00 GMT
last-modified: Thu Mar 27 2025 21:03:30 GMT+0000 (Coordinated Universal Time)
cache-control: post-check=0, pre-check=0
authenticated: false
content-type: application/json; charset=utf-8
x-response-time: 236ms
x-cloud-trace-context: 198a37888b0f0aecb61c4481a201abe7
date: Thu, 27 Mar 2025 21:03:30 GMT
server: Google Frontend
content-length: 89

[{"id":"2ml","url":"https://cdn2.thecatapi.com/images/2ml.jpg","width":500,"height":340}]

ولاحظ أول سطر في نص الجواب: 200 OK تعني أن الطلب تمُّ إنجازه بنجاح. وراجع الرؤس (Headers) إن أردت معرفة معنى كل ترويسة هنا (وليست تهمنا الآن). لكن ما يبدأ بحرف x- هو إضافي وليس من أساس لغة HTTP المتفق عليها، ولذلك قد لا تجده في التوثيق العام، وإنما تجده في توثيق مزوِّد للخدمة.

ولاحظ أن آخر سطرٍ هو المحتوى:

[{"id": ... ,"url": ... , ... }]

وإذا نظرت إلى النصّ الموجود في محتوى الرد (response.text) فإنك ستلاحظ أنه نصُّ مقوْلَب بصيغة JSON التي سبق الحديث عنها:

print(type(response.text))
print(response.text)
<class 'str'>
[{"id":"2ml","url":"https://cdn2.thecatapi.com/images/2ml.jpg","width":500,"height":340}]

ولكن هذا النص لا يمكن التعامل معه كما هو، لذلك نستخدم الإجراء json() لتفسيره إلى شيء في بايثون (قائمة):

data = response.json()
print(type(data))
print(data)
<class 'list'>
[{'id': '2ml', 'url': 'https://cdn2.thecatapi.com/images/2ml.jpg', 'width': 500, 'height': 340}]

الآن أصبح في هيكل بيانات يمكن التعامل معه. فنريد استخراج رابط الصورة منه:

image_url = data[0]["url"]
print(image_url)
https://cdn2.thecatapi.com/images/2ml.jpg

فهذا الرابط، لو نسخته وأدخلته في المتصفح فستظهر لك صورة القط.

الكود
from IPython.display import Image, display

display(Image(url=image_url))

مثال: خدمة الطقس

والرابط يشبه استدعاء الإجراء. لاحظ أننا في بايثون نستدعي الإجراء pow من الوحدة math ونمرر العوامل 2, 3 إليه على هذا النحو:

import math

math.pow(2, 3)
8.0

وهكذا نشبِّه ذلك بطريقة العنوان الموحَّد باعتبار أن المورِد هو معالجة؛ وهي سؤالٌ عن الطقس في مدينة لندن:

Origin (الأصل) Path (المسار) Query (المعاملات)
https://api.openweathermap.org /weather ?city=London
math pow 2, 3
  • فكما أننا نطلب الإجراء pow من الوحدة math ونمرر العوامل 2, 3 إليه
  • فكذلك نطلب المورِد /weather من الموقع https://api.openweathermap.org ونمرر العوامل ?city=London إليه
    • العامل يبتدأ بعلامة الاستفهام ? ثم اسم العامل city وقيمته London وهو اسم المدينة

لنأخذ مثالاً آخر على استخدام خدمة برمجية، وهي خدمة الطقس من OpenWeatherMap. هذه الخدمة تتيح لنا معرفة حالة الطقس في أي مدينة في العالم.

أولاً، نحتاج إلى مفتاح API من الموقع (يمكنك الحصول عليه مجاناً بعد التسجيل). ثم نستخدم مكتبة httpx للاتصال بالخدمة.

ففي صفحة التوثيق قالوا إن طريقة الطلب هي على النحو التالي:

نشرح الرابط حتى تتبين أجزاؤه ليسهل عليك بعد ذلك قراءة أية رابط:

https://api.openweathermap.org/data/3.0/onecall?lat={lat}&lon={lon}&exclude={part}&appid={API key}:

  • https://api.openweathermap.org هو الأصل (Origin)
  • /data/3.0/onecall هو المسار الفرعي (Path)
  • ? العلامة الفاصلة بين المسار والعوامل
  • & علامة فاصلة بين العوامل نفسها
  • lat={latitude} هو تعيين للعامل الأوَّل بقيمة latitude أي: خط العرض
  • lon={longitude} هو تعيين للعامل الثاني بقيمة longitude أي: خط الطول
  • exclude={part} اختيار البيانات التي تريد استبعادها في جواب الطلب
  • appid={API key} هو مفتاح التطبيق الذي يسمح للطلب بالوصول إلى الخدمة

ونحن نكتبها في بايثون مع مكتبة httpx على النحو التالي:

import httpx

latitude = 24.7136
longitude = 46.6753

client = httpx.Client()

request = client.build_request(
    method="GET",
    url="https://api.openweathermap.org/data/3.0/onecall",
    params={
        "lat": round(latitude, 4),
        "lon": round(longitude, 4),
        "appid": "4a5417dd3a781b7f64f05178ed423a23"
    }
)

response = client.send(request)
print(response.text)
{"cod":401, "message": "Please note that using One Call 3.0 requires a separate subscription to the One Call by Call plan. Learn more here https://openweathermap.org/price. If you have a valid subscription to the One Call by Call plan, but still receive this error, then please see https://openweathermap.org/faq#error401 for more info."}

أو اختصارًا باستعمال httpx.get مباشرةً هكذا:

import httpx

latitude = 24.7136
longitude = 46.6753

response = httpx.get(
    url="https://api.openweathermap.org/data/3.0/onecall",
    params={
        "lat": round(latitude, 4),
        "lon": round(longitude, 4),
        "appid": "4a5417dd3a781b7f64f05178ed423a23"
    }
)
print(response.text)
{"cod":401, "message": "Please note that using One Call 3.0 requires a separate subscription to the One Call by Call plan. Learn more here https://openweathermap.org/price. If you have a valid subscription to the One Call by Call plan, but still receive this error, then please see https://openweathermap.org/faq#error401 for more info."}

انطلق بالتطبيق

وبهذا تكون قادرًا على التعامل مع أي برمجيَّة توفِّر خدماتها عبر الشبكة. تحتاج فقط أن تُقدِم وتجرِّب حتى تأخذ يدك على الأمر!

انتقل إلى المسائل.فإن أردت البحث عن واجهة برمجية لعمل شيء ما، فاكتب الكلمات المفتاحية + “API” في محرك البحث؛ مثلاً: Google Maps API.

ملاحظة: بعض الواجهات تتطلب التسجيل للحصول على مفتاح API. وبعضها يحتاج إضافة إلى ذلك شحن الحساب برصيد مثل 5 دولارات. وكل ذلك مبين في التوثيق نفسه.