10 خادم الويب
السؤال: كيف نوصِّل العميل بالخادم؟
تسمى العنصار التي تشبع الصفحة بالمحتوى الخارجي، أو تتفاعل مع المستخدم بالضغط أو الإدخال ونحوه، عناصر تحكم (Hypermedia Controls)، فهي تعمل وِفق بروتوكول HTTP للتواصل مع الخادم، عن طريق العناوين المحددة (URL) في كل منها. ونلخص نتيجة تفاعل المستخدم معها في هذا الجدول:
| العنصر | تفاعل المستخدم | النتيجة |
|---|---|---|
<img> |
فتح الصفحة. | طلب ثم جواب ثم عرض. |
<a> |
الضغط على الرابط. | طلب ثم جواب ثم انتقال. |
<form> |
الضغط على الزر. | طلب ثم جواب ثم انتقال. |

سنتوجه الآن للجهة الأخرى من التواصل: خادم الويب (Web Server). ونرى كيف يتصل بالعميل؛ تحديدًا المتصفِّح (Browser)، وفق آليات بروتوكول HTTP.
خادم الويب
إن uvicorn برمجيَّة خادم ويب لا متزامن لبايثون (ASGI Web Server for Python)؛ تتلقى رسائل HTTP وتحوِّلُها إلى العالم البياثوني، ثم تأخذ نتيجة المعالجة بعد إخراج بايثون لها، لتصيغها كجواب على طريقة HTTP ثم ترسلها إلى العميل الذي أنشأ الطلب منذ البداية.
أما اللاتزامن (Async)؛ فيأتي بيانه في وقته -إن شاء الله-.
ونحتاج إطارًا مثل starlette؛ يعطينا لغة وقوالب معالجة مخصصة في بايثون. إذْ أن uvicorn مجرَّد وسيط (Proxy) بين الخدمة (Service) وبين العميل (Client) / المتصفِّح.
ويمكن أن نقارن هذين بما سبق في نظم قواعد البيانات:
خادم uvicorn هو المقابل لخادم قاعدة البيانات، مثل: PostgreSQL و SQLIte و MySQL؛ فكذلك لدينا خيارات في خادم الويب غير uvicorn مثل: daphneو hypercorn.
وحزمة starlette هي بمثابة psycopg؛ هي مكتبة تسوق طريقة الاتصال بقاعدة البيانات فقط. ثم رأينا أن SQLAlchemy إطار أكثر تجريدًا منه؛ لكنا تستعمله (أو غيره).
أما إطار FastAPI فهو مبنيٌّ على starlette لتخصيصه للتعامل مع بيانات JSON تحديدًا. وهو معروفٌ أكثر من starlette نفسها الذي هو مبني عليها. فالعلاقة بين psycopg و SQLAlchemy هي علاقة تجريد، أما العلاقة بين FastAPI و starlette فهي علاقة تخصيص.
فائدة: وكما أن لدينا DB-API في بايثون لوصف شكل موحَّد لتظهره الطرائق المختلفة التي تتعامل مع قواعد البيانات المختلفة. فكذلك لدينا ASGI لوصف شكل موحَّد للتعامل مع الخوادم وتطبيقات الويب غير المتزامنة؛ على اختلافها في التفاصيل.
إنشاء التطبيق وتشغيل الخادم
تثبيت الحزمتين:
- حزمة البرمجة:
starlette - الخادم الوسيط:
uvicorn
uv add starlette uvicornلاحظ أن كلا المكتبتين تحتاجان لخمس تبعيات فقط وهو عدد قليل:
Installed 5 packages in 9ms
+ anyio==4.12.0
+ h11==0.16.0
+ idna==3.11
+ starlette==0.50.0
+ uvicorn==0.38.0
نكتب النص في بايثون في ملف main.py مثلاً:
from starlette.applications import Starlette
from starlette.responses import HTMLResponse
from starlette.routing import Route
async def homepage(request):
return HTMLResponse('<p>Hello, <b>World!</b></p>')
routes = [
Route('/', homepage),
]
app = Starlette(debug=True, routes=routes)لاحظ بعد الاستيرادات، أننا عرفنا دالة تأخذ معطىً وهو request وترجع بمخرج من النوع: HTMLResponse مستوردٍ من وحدة starlette.responses، يتضمَّنُ نصَّ HTML فيه فقرة فيها كلمتان، الثانية بالخط السميك (لوجود العنصر <b>).
من سمات الإطار أنه هو الذي يستدعي الدوال التي نكتبها، وذلك يحتاج منا إلى ربطها أولاً؛ وذلك باستعمال المتغير القائمة: routes وتعريف المسارات بالمعيَّن Route حيث تم تمرير المسار (/) وما يقابله المدبِّر (Handler) وهي الدالة: homepage. ولعلك لاحظت أن تعريف الدالة كان بزيادة كلمة async وسيأتي بيانه عند الكلام عن اللاتزامن.
أخيرًا يتمُّ جمع كل ذلك في معيَّن يمثِّلُ البرمجيَّة ككل؛ وهي app حيث تم تعيينها من كائن Starlette بإعطائها المسارات routes وجعلها تعمل بالطبيعة التي تساعدنا في التدقيق: debug=True.
ثم نشغِّل الخادم الوسيط uvicorn وندله على مكان هذا الملف ليحوِّل إليه طلبات HTTP الآتية إليه:
uvicorn path.to.file.main:app --reloadلاحظ أن path.to.file.main هو مسار للوصول إلى الملف main.py؛ وهو يستعمل النقط (.) لا الشرطة المائلة (/) كما هو معروف في نظام المجلدات.
أما main:app فلا بد أن توافق app اسم المتغيِّر الذي يحمل هو معيَّن من كائن Starlette.
ثم العلامة --reload ليراقب أي تعديل جديد يتم حفظه للتطبيق، فيحدِّثَ نفسه بالنص البرمجي البايثوني الجديد مباشرةً من غير أن نوقفه بأنفسنا (Ctrl+c) ثم نشغله مرة ثانية. وهذا يسرع العمل كثيرًا أثناء التطوير.
جرب الآن زيارة الصفحة localhost:8000 لترى النتيجة: صفحة موقع فيها الذي كتبناه في HTML.
الموقع الثابت
سنحفظ نص HTML في ملف نسميه index.html تحت المجلَّد static، ثم نقوم بقراءته وعرضه حين يتم زيارة المسار:
from starlette.applications import Starlette
from starlette.responses import HTMLResponse
from starlette.routing import Route
async def homepage(request):
with open("static/index.html", "r") as f:
html = f.read()
return HTMLResponse(html)
routes = [
Route("/", homepage),
]
app = Starlette(debug=True, routes=routes)يمكن أتمتة هذه الآلية باستعمال StaticFiles الموجودة في إحدى وحدات الحزمة، بعد استيرادها، واستيراد وحدة أعم من الكائن Route وهو الكائن Mount:
from starlette.staticfiles import StaticFiles
from starlette.routing import Mountثم يتم ربطها على هذا النحو، ليتم ترجمة المسار المبتدأ بـ(/xyz) كما حددنا إلى قراءة الملف من المجلَّد ابتداءً من مسار الملفات static نزولاً:
from starlette.applications import Starlette
from starlette.responses import HTMLResponse
from starlette.routing import Route, Mount # NEW
from starlette.staticfiles import StaticFiles # NEW
async def homepage(request):
with open("static/index.html", "r") as f:
html = f.read()
return HTMLResponse(html)
routes = [
Route("/", homepage),
Mount("/xyz", StaticFiles(directory="static")), # NEW
]
app = Starlette(debug=True, routes=routes)جرب أن تضيف ملفات أخرى سواءٌ كانت HTML أو صور أو أي شيء آخر. ويفضَّل ألا يكون في اسم الملف مسافات، بل يكون على نحو:
- مثل:
report.pdf - أو مثل:
file_name.png - أو مثل:
soundfile.wav - أو مثل:
my-page.html
ثم قم بزيارة المسار ووضع اسم الملف بعد xyz، نحو:
localhost:8000/xyz/my-page.html
وسيقوم الخادم بإرسال الملف ليصل للمتصفِّح ويتم عرضه فيه، وليس كل الصيغ يستطيع المتصفح فتحها لكن كثيرٌ مما نتعامل معه يوميًّا يمكنه فتحه.