25  تواصل البرمجيات عبر الشبكة

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

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

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

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

  1. العنوان (URL)
  2. الطلب (Request)
  3. الجواب (Response)

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

إن اصطلاح عنوان المورِد الموحَّد 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
date: Sat, 07 Jun 2025 14:39:24 GMT
content-type: application/json; charset=utf-8
transfer-encoding: chunked
connection: keep-alive
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: Sat Jun 07 2025 14:39:23 GMT+0000 (Coordinated Universal Time)
cache-control: post-check=0, pre-check=0
authenticated: false
x-response-time: 213ms
x-cloud-trace-context: b205032c1ba9c2c97f3e9b757d69bb25
server: cloudflare
cf-cache-status: DYNAMIC
nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
report-to: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=CljQEVsVv5fZiMXOK0ZiPWVjX18SDfxqaU0JNAs4N8uq5LJFa8m6SDOL14Ty6dCcyyU51h1kZEaDRqwqBJX3SyqqYX0gQCrOAHz1VzQOZCFg4fV4j6rcX0ESvB2A"}]}
content-encoding: gzip
cf-ray: 94c0e60c9d46ba92-MXP
alt-svc: h3=":443"; ma=86400

[{"id":"cg9","url":"https://cdn2.thecatapi.com/images/cg9.jpg","width":300,"height":200}]

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

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

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

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

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

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

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

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

image_url = data[0]["url"]
print(image_url)
https://cdn2.thecatapi.com/images/cg9.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 دولارات. وكل ذلك مبين في المرجع نفسه.