الإقليم ٠ — الجسر: HTTP
النبذة
في منهج الشبكات بنيتَ الإنترنت من الإلكترون: MAC ثم IP ثم TCP — قناةُ بايتاتٍ موثوقةٌ ومرتّبةٌ بين طرفين. وفي الإقليم السابع "كنتَ الـ socket": فتحتَ nc وكتبتَ بأصابعك سطراً مثل GET / HTTP/1.1 ورجعت لك بايتات. لكن وقتها مرّرناها مرور الكرام — أداةٌ لاختبار السلك.
الآن نتوقّف عندها بالضبط. لأن هذا السطر الذي كتبته بأصابعك هو اللغة التي يتكلّمها الستاك كلّه. كل نقرةٍ في React، وكل دالةٍ في Express، وكل ما بينهما — ليست إلا بايتاتٍ من هذا الشكل تسري في قناة TCP التي تعرفها. من يتقن هذا الإقليم، يرى الستاك كلّه شفّافاً.
اللغز المستفزّ
عندك TCP: قناةُ بايتاتٍ موثوقة. هذا كل ما تعطيك إياه الشبكة. لا "طلبات"، ولا "ردود"، ولا "ملفّات" — بايتاتٌ تسري كالماء، بلا بداية ولا نهاية لرسالة.
افتح طرفين بـ nc على جهازك:
sh# الطرف ١ (الخادم): يستمع على المنفذ 8080 ويطبع ما يصله حرفياً nc -l 8080 # الطرف ٢ (العميل): يتّصل ويكتب أنت بيدك nc localhost 8080
الآن جاوب — قبل أن تقرأ أي شيء بعد هذا السطر:
- كتبتَ في العميل سطرين ثم سطراً فارغاً. كيف يعرف الخادم أين انتهت "رسالتك" وبدأت الثانية؟ TCP لا يخبره — فهو بايتاتٌ بلا حدود. فمن يرسم الحدود؟
- لو أرسلتَ صورةً (بايتات ثنائية فيها صفرٌ هنا وسطرٌ جديدٌ هناك صدفةً) — كيف يميّز الخادمُ بايتةَ "نهاية الرسالة" عن بايتةٍ من داخل الصورة نفسها؟
- اتّصلتَ بالخادم، أرسلتَ، أُغلق. ثم اتّصلتَ ثانيةً. هل يعرف الخادمُ أنك نفس الشخص؟ ولو افترضنا أنه لا يعرف — كيف تعمل أي صفحةٍ "مسجَّل الدخول" في الدنيا؟
هذه الأسئلة الثلاثة هي HTTP كلّه: التأطير (أين تبدأ الرسالة وتنتهي)، والمعنى (ماذا تعني البايتات)، وانعدام الذاكرة (الخادم لا يتذكّرك). لا تكمل وأنت غير مستوعبٍ لمَ هي ألغاز. اقعد معها.
ليش: لماذا يوجد HTTP أصلاً؟
عندك TCP موثوق. لماذا لم يكتفِ الناس به؟
لأن TCP بروتوكول نقل (transport): يضمن أن البايتات تصل كاملةً ومرتّبة. لكنه أجوف من المعنى تماماً. تخيّل خطّ هاتفٍ نقيّ: يوصّل صوتك بأمانة، لكنه لا يعرف أتطلب بيتزا أم تحجز موعداً. تحتاج لغةً متّفقاً عليها فوق الخط. الطرفان لازم يتّفقان مسبقاً: "أول سطرٍ يقول ماذا أريد، ثم أسطرٌ تصف الطلب، ثم سطرٌ فارغٌ يعني ‹خلصت رأسي›، ثم المحتوى". هذا الاتّفاق اسمه بروتوكول التطبيق (application-layer protocol). HTTP واحدٌ منها (وSMTP للبريد، وFTP للملفّات...).
النموذج الذهني (احفظه): HTTP ليس "تقنيةً". هو اتّفاقٌ نصّيٌّ على شكل رسالةٍ تُكتب داخل قناة TCP. لو اختفت كل برامج الويب وبقي اتّفاقٌ بشريٌّ على هذا الشكل، فأنت وأنا نقدر نتبادل HTTP بقلمٍ وورقة. الخادم ليس سحراً — هو برنامجٌ يقرأ هذا الشكل ويكتب شكلاً مثله. تذكّر هذا في كل إقليمٍ قادم.
الجواب على اللغز ١ و٢: التأطير فوق التيّار (framing over a stream)
TCP يعطيك تيّار بايتات بلا حدود. أي بروتوكولٍ فوقه لازم يرسم حدود الرسائل بنفسه. هذه مشكلةٌ قديمةٌ تعرف نظيرها في C: عندك read() يرجّع بايتات، فكيف تعرف أين تنتهي "السجلّة"؟ لك حلّان كلاسيكيّان:
- فاصلٌ مميّز (delimiter): اتّفق على بايتةٍ/تسلسلٍ يعني "انتهى". مثل
\0في سلاسل C، أو السطر الجديد في النصوص. عيبه: ماذا لو ظهر الفاصل داخل البيانات؟ (لغز ٢ بالضبط — صورةٌ فيها بايتةٌ تساوي الفاصل صدفةً.) - طولٌ مُعلَن مسبقاً (length-prefix): قل أولاً "المحتوى ١٢٣ بايت"، ثم اقرأ ١٢٣ بايتاً عدّاً، لا تبحث عن فاصل. آمنٌ مع أي بايتاتٍ كانت.
HTTP يستعمل الاثنين معاً، كلٌّ في موضعه:
- الرأس (headers) نصٌّ مؤطَّرٌ بفاصل: كل سطرٍ ينتهي بـ
CRLF(أي البايتتان\r\n= 13 ثم 10)، وسطرٌ فارغٌ (CRLFلوحده) يعني "انتهى الرأس". الرأس نصٌّ مضمونٌ خلوُّه من بايتاتٍ ثنائيةٍ خبيثة، فالفاصل آمنٌ هنا. - الجسم (body) ثنائيٌّ مؤطَّرٌ بالطول: ترويسةٌ في الرأس اسمها
Content-Length: 123تقول كم بايتاً يأتي بعد السطر الفارغ. الخادم يقرأ ١٢٣ بايتاً عدّاً — فلو كان الجسم صورةً فيها\r\n، لا يهمّ، لأننا نعدّ لا نبحث.
هذه النقطة وحدها تشرح ٩٠٪ من أعطال الويب الغامضة: لو كذبتَ في Content-Length، أو نسيتَ السطر الفارغ، تجمّد الطرف الآخر ينتظر بايتاتٍ لن تأتي، أو يقطع رسالةً ناقصة. التأطير ليس تفصيلاً — هو العقد.
(فيه طريقةٌ ثالثة Transfer-Encoding: chunked حين لا تعرف الطول مسبقاً — تأطيرٌ بقطعٍ كلٌّ منها مسبوقٌ بطوله. بذرةٌ، نفتحها حين نبثّ بياناتٍ متدفّقة.)
كيف: تشريح رسالة HTTP
الطلب (request)
ثلاث طبقات، بنفس ترتيب التأطير أعلاه:
httpPOST /api/v1/auth/register HTTP/1.1⏎ Host: localhost:3000⏎ Content-Type: application/json⏎ Content-Length: 54⏎ ⏎ {"username":"yazeed","email":"y@x.com","password":"..."}
(⏎ = CRLF = \r\n. السطر الفارغ الخامس هو CRLF وحده: نهاية الرأس.)
- سطر الطلب (request-line):
METHOD SP target SP version.- method = الفعل: ماذا أفعل؟ (
POST). - target = الاسم/المورد: على ماذا؟ (
/api/v1/auth/register). - version = أي لهجةٍ من HTTP (
HTTP/1.1). - فعلٌ + اسم. هذا قلب HTTP: افعل كذا على المورد كذا.
- method = الفعل: ماذا أفعل؟ (
- الترويسات (headers):
Key: Valueلكلٍّ سطر. بياناتٌ عن الطلب: من أين (Host)، نوع الجسم (Content-Type)، طوله (Content-Length)، الهوية (Authorization)... ميتاداتا. - السطر الفارغ ثم الجسم: الحمولة. اختياريّ (لا جسم في
GETعادةً).
الرد (response)
نفس البنية، بسطرٍ أوّلَ مختلف:
httpHTTP/1.1 201 Created⏎ Content-Type: application/json⏎ Content-Length: 38⏎ ⏎ {"success":true,"data":{"id":"..."}}
- سطر الحالة (status-line):
version SP code SP reason. الرقم للآلة، النصّ للبشر (Createdمجرّد مجاملة؛ الآلة تقرأ201).
ليش: دلالات الأفعال (methods) — ولماذا تهمّك عملياً
ليست الأفعال أسماءً عشوائية. لكلٍّ عقدٌ دلاليٌّ يعتمد عليه كل شيءٍ في الطريق (المتصفّح، الـ proxy، الـ cache، مكتبتك). كسرُ العقد يكسر أشياءً لا تتوقّعها.
| الفعل | المعنى | آمن (safe)؟ | عديم الأثر بالتكرار (idempotent)؟ |
|---|---|---|---|
GET | اقرأ مورداً | ✅ لا يغيّر شيئاً | ✅ |
POST | أنشئ/نفّذ فعلاً جديداً | ❌ | ❌ (مرّتان = سجلّان) |
PUT | استبدل المورد كاملاً | ❌ | ✅ (نفس النتيجة مهما كرّرت) |
PATCH | عدّل جزءاً | ❌ | ليس بالضرورة |
DELETE | احذف المورد | ❌ | ✅ (محذوفٌ محذوف) |
- safe = للقراءة فقط، لا يغيّر حالة الخادم. لهذا يجوز للمتصفّح أن يسبق ويجلب روابط
GETمسبقاً، وللـ proxy أن يخزّنها. لا تضع تغييراً خلفGETأبداً — ستجد سجلّاتٍ تُحذف لأن زاحفاً فتح الرابط. - idempotent = تكرارُه يعطي نفس النتيجة. لماذا يهمّ؟ تذكّر الشبكة: قد ينقطع الردّ في الطريق فلا يدري العميل أنجح أم لا، فيعيد المحاولة. لو كان
PUTأوDELETE— التكرار آمن. لو كانPOST(تسجيل مستخدم) — تكرارُه يخلق مستخدمين مكرّرين. هنا يولد سؤالٌ حقيقيٌّ في مشروعك: كيف تحميPOST /registerمن النقر المزدوج؟ (بذرة: قيدُ تفرّدٍ في قاعدة البيانات — إقليم ٤.)
الترتيب "ليش قبل كيف" هنا حرفيّ: لا تحفظ "POST للإنشاء". افهم أن عدم الـ idempotency هو سبب كون POST للإنشاء، وأن العقد الدلاليّ يخدم لا-موثوقية الشبكة التي بنيتها بنفسك في منهج الشبكات.
ليش: عائلات رموز الحالة — من المُلام؟
الرقم مقسومٌ لعائلاتٍ ليجيب فوراً عن سؤالٍ واحد: من سبّب هذه النتيجة؟
| العائلة | المعنى | المُلام |
|---|---|---|
1xx | معلوماتيّ، انتظر | — (نادر) |
2xx | نجح | الجميع سعيد |
3xx | اذهب لمكانٍ آخر (redirect) | لا أحد |
4xx | طلبك أنت خاطئ | العميل |
5xx | الخادم انهار وهو يعالج طلباً سليماً | الخادم |
الفرق بين 4xx و5xx ليس تجميلاً — هو توزيع المسؤولية، وهو ما تبني عليه مراقبتك (تذكّر QPS والـ monitoring من منهج بنية الويب): ارتفاع 5xx يعني كودُك مكسور؛ ارتفاع 4xx يعني عملاؤك يسيئون الاستعمال أو واجهتك مربكة. الرموز التي ستستعملها فعلياً في مشروعك:
200 OK(نجح، وفي الجسم نتيجة) ·201 Created(أنشأتُ مورداً) ·204 No Content(نجح، لا جسم — مثل DELETE).400 Bad Request(طلبٌ مشوَّه/فشل تحقّق Zod) ·401 Unauthorized(من أنت؟ لم تثبت هويتك) ·403 Forbidden(عرفتُك، لكن ممنوع) ·404 Not Found(لا مورد) ·409 Conflict(تعارض، مثل username مأخوذ) ·422(مفهومٌ نحوياً لكن غير صالحٍ دلالياً).500 Internal Server Error(انفجر كودك) ·503(مشغول/معطّل مؤقتاً).
فخٌّ شائع وجذره: ردّ 200 ثم تضع {"error": "..."} في الجسم. لماذا يبدو مقنعاً؟ لأنك تفكّر بمنطق الجسم وحده. لكن نصف العالم بينك وبين العميل (proxies, caches, fetch, monitoring) يقرأ الرقم فقط ولا يفتح الجسم. فيخزّن "النجاح" ويحسبه في إحصاء الصحّة. الرمز ليس زينة — هو الحقيقة التي تقرؤها الآلات.
ليش: انعدام الذاكرة (statelessness) — الجواب على اللغز ٣
اتّصلت بـ nc، أرسلت، أُغلق، ثم اتّصلت ثانيةً — والخادم لا يعرف أنك نفس الشخص. هذه ليست نقيصة؛ إنها قرار تصميميٌّ مقصود: HTTP بروتوكولٌ عديم الحالة — كل طلبٍ مستقلٌّ تماماً، يحمل كل ما يلزم لفهمه، والخادم لا يحتفظ بأي ذاكرةٍ بين الطلبات.
لماذا اختاروا هذا عمداً؟ ارجع لمنهج بنية الويب: وضعتَ load balancer يوزّع الطلبات على عدّة خوادمَ متطابقة. هذا التوزيع مستحيلٌ إلا إذا كان كل طلبٍ مستقلاً بذاته. لو احتاج الطلبُ الثاني أن "يتذكّر" الخادمُ الأول الطلبَ الأول، لانهار التوزيع (لازم تعود لنفس الخادم دائماً = sticky = هشّ). انعدام الحالة هو الثمن الذي يُشترى به التوسّع الأفقيّ الذي تعلّمته. الحالةُ تُدفع للأسفل، إلى الـ database (الذي يحفظ الحالة)، لا إلى الخادم.
لكن... صفحة "مسجَّل الدخول" تتذكّرك! كيف، والخادمُ بلا ذاكرة؟ الحيلة: العميل يحمل إثبات هويته في كل طلب (ترويسة Authorization)، فيبقى كل طلبٍ مستقلاً ومع ذلك معروفَ المُرسِل. كيف نصنع إثباتاً لا يمكن تزويره دون أن يحفظ الخادمُ شيئاً؟ هذا الإقليم ٧ بالكامل. اتركها بذرةً تؤرّقك.
ليش: REST — لماذا "موارد" و"أفعال"؟
عندك الآن: أفعالٌ قياسية + رموزٌ قياسية + ترويسات. REST مجرّد انضباطٍ في استعمالها: تنمذِج نظامك كـ موارد (أسماء) لكلٍّ عنوانُ URL، وتتعامل معها بالأفعال القياسية، وتنقل تمثيلاً لها (JSON عادةً). بدل أن تخترع /getUserById?id=5 و/deleteUser?id=5 و/updateUserEmail?... (أفعالٌ في الأسماء، فوضى لا نهاية لها)، تقول:
ليش أفضل؟ لأن "الفعل" لم يعد جزءاً من اسمك — صار في الـ method القياسيّ الذي يفهمه كلُّ شيءٍ في الطريق. فتقلّ المفاجآت: أي مطوّرٍ يرى DELETE /users/5 يعرف دلالتها دون توثيق. تأمّل تداخل عنوان مشروعك: CURRICULUM ─< SPRINT ─< PROJECT ─< TASK ─< TEST_CASE. هذا التداخل في الـ URL (/curricula/:id/sprints/:id/...) هو نفسه علاقات قاعدة بياناتك — وستراه يتجسّد جدولاً ومفتاحاً أجنبياً في الإقليم ٤. (REST ليس قانوناً مقدّساً؛ هو انضباطٌ نافع. لا تتعصّب له حدّ تعذيب نموذجك.)
مظروف الرد الموحَّد
مشروعك اتّفق على شكلٍ ثابتٍ لكل جسم رد:
json{ "success": true, "message": "...", "data": { } }
ليش مظروفٌ موحّد؟ كي يكتب العميل (React) شيفرةَ معالجةٍ واحدة لكل الردود بدل أن يخمّن شكل كل endpoint. اتّساقُ الحافة يبسّط الطرف الآخر — ستلمس قيمته حين تبني React في الإقليم ٨.
اللغز / البناء من الصفر
الهدف: أن ترى بعينك أن الخادم برنامجٌ يقرأ بايتاتٍ بشكلٍ متّفقٍ عليه ويكتب بايتاتٍ بشكلٍ مثله — لا أكثر. أدواتك: nc فقط (الذي أتقنته في منهج الشبكات). ممنوع curl، وممنوع أي مكتبة HTTP، وممنوع متصفّحٌ في الأجزاء التي تكتب فيها بيدك. اكتب البايتات بأصابعك.
⚠️ دقّةٌ تقتل المبتدئين: أسطر HTTP تنتهي بـ CRLF (\r\n)، لا بـ \n وحده. الإدخال اليدويّ في nc قد يرسل \n فقط حسب جهازك. إن "تجمّد" طرفٌ، فالـ framing أو الـ line-ending هو المتّهم الأول — وهذا جزءٌ من الدرس، لا عطلٌ خارجه.
اللغز أ — اقرأ ما يقوله متصفّحٌ حقيقيّ. شغّل nc -l 8080، ثم افتح http://localhost:8080/api/v1/health في المتصفّح. لن تُحمّل الصفحة (طبيعيّ — لن تردّ)، لكن في طرفيّتك ستنطبع رسالةُ الطلب الخام التي بعثها المتصفّح. حلّلها سطراً سطراً: أين سطر الطلب؟ ما الأفعال والترويسات التي أرسلها المتصفّح من تلقاء نفسه (Host, User-Agent, Accept...)؟ أين السطر الفارغ؟
اللغز ب — كن الخادم بيدك. بينما المتصفّح ما زال ينتظر، اكتب في نفس جلسة nc -l 8080 ردّاً كاملاً وصحيحاً يجعل المتصفّح يعرض جملةً. القيود: لازم سطرُ حالةٍ صحيح، وContent-Type مناسب، وContent-Length يطابق بالضبط عدد بايتات جسمك، وسطرٌ فارغٌ قبل الجسم. لو غلطتَ في الطول بايتةً واحدة، إما يتجمّد المتصفّح أو يقطع النصّ. اضبطه حتى يظهر نصُّك في المتصفّح. (تلميح وحيدٌ مسموح: عُدّ بايتات جسمك يدوياً، ولا تنسَ أن السطر الجديد بايتة.)
اللغز ج — كن العميل بيدك. شغّل أي خادمٍ بسيطٍ تثق به (أو استعمل خادمَ زميل، أو أبقِ على nc -l في طرفٍ آخر كـ"خادمٍ صامت" يطبع ما يصله). من طرفٍ ثانٍ: nc localhost <port>، ثم اكتب بيدك طلب POST كاملاً يسجّل مستخدماً: سطر طلبٍ بفعلٍ ومسارٍ ونسخة، ثم Host، ثم Content-Type: application/json، ثم Content-Length الصحيح، ثم سطرٌ فارغ، ثم جسم JSON. راقب: هل وصلت الرسالة كاملةً حين أطعمتَ الطول الصحيح؟ جرّب تنقص الطول واحداً — ماذا ينتظر الطرف الآخر، ولماذا؟
اللغز د (تجريديّ، بلا كود): عندك في الإقليم القادم ستكتب خادماً بلغةٍ أعلى. صف بالكلمات — كأنك تكتب دالةً في C — الخطوات التي يجب أن يفعلها أي خادم HTTP بعد accept() على الـ socket: كيف يقرأ حتى يجد نهاية الرأس؟ كيف يعرف أين يبدأ الجسم وكم يقرأ منه؟ كيف يقرّر بأي رمزِ حالةٍ يردّ؟ هذا "السودوكود" هو بالضبط ما يفعله Node و Express تحت الغطاء — وستكتشف في الإقليم ٣ أنك سبقتَهما إليه.
لا تنتقل قبل أن يعرض المتصفّحُ نصَّك في اللغز ب، وتصل رسالةُ اللغز ج كاملةً. حين ينجح ب، توقّف لحظة: أنت كتبتَ "خادم ويب" بيدك. كل خادمٍ في الدنيا يفعل هذا، بأتمتةٍ فقط.
الخلاصة — أين تتّصل هذه العقدة بالشجرة
- تحت: HTTP يجلس فوق TCP من منهج الشبكات. كل ما بنيته هناك (الموثوقية، الترتيب، الـ socket = ip:port) هو القناة؛ HTTP هو اللغة داخلها. وانعدامُ الحالة هو ثمن التوسّع الأفقيّ الذي تعلّمته في بنية الويب.
- العقدة الجديدة: رسالةٌ مؤطَّرةٌ ذاتُ معنى — سطرُ طلب/حالة + ترويسات (فاصل CRLF) + جسم (طول مُعلَن)، بأفعالٍ ورموزٍ لها عقدٌ دلاليّ، منمذَجةً كموارد (REST)، عديمةِ الحالة.
- فوق: كل إقليمٍ قادمٍ ليس إلا برنامجاً يقرأ هذه الرسالة ويكتب مثلها. Node (٢) يقرأ بايتات الـ socket. Express (٣) يحوّل سطر الطلب إلى استدعاء دالة. Zod (٦) يحرس الجسم. الهوية (٧) تحلّ بذرة انعدام الحالة. React (٨) يولّد هذه الرسائل من الطرف الآخر.
الأبواب التي فتحتها: كيف يجد الخادمُ نهاية الرأس؟ (٢/٣). كيف يصير جسمُ JSON كائناً نتعامل معه؟ (١ ثم ٦). كيف يصير POST /register صفّاً في جدول؟ (٤/٥). كيف يتذكّرك خادمٌ بلا ذاكرة؟ (٧).
الوصلة للواقع (نمط SelfLab)
كل ما قرأته أعلاه ليس تجريداً — هو حرفياً عقد مشروعك:
Base URL: /api/v1— بادئةُ نسخةٍ على كل الموارد (GET /api/v1/health).POST /auth/register,POST /auth/login,GET /me— أفعالٌ على موارد، تماماً كاللغز ج.- المظروف
{ success, message, data }— الشكل الموحَّد الذي تبنيه يدوياً الآن وستولّده Express تلقائياً لاحقاً. Authorization: Bearer <token>— حلُّ بذرة انعدام الحالة، تبنيه في الإقليم ٧.- التداخل
/curricula/:id/sprints/...— انعكاسُ علاقات الـ ERD التي تفكّكها في ٤.
حين تفتح أول ملفٍّ في backend/ وترى app.use("/auth", routes) و res.status(201).json(...) — لن ترى سحراً. سترى الرسالةَ التي كتبتها بأصابعك في nc، وقد أُتمتت. هذا هو الجسر.
البذرة التالية: كتبتَ الخادمَ بيدك في nc. لكن nc لا يعرف يقرأ ألفَ طلبٍ في الثانية ولا يردّ على كلٍّ بمنطق. تحتاج برنامجاً. ستكتبه بلغةٍ غريبة: بلا main يدوم، بلا انتظارٍ يحجب، بخيطٍ واحدٍ يخدم الآلاف. لغةٌ مبنيّةٌ على فكرةٍ أنت تعرفها من C باسمٍ آخر: epoll. ننزل للإقليم ١ نتعلّم اللغة، ثم الإقليم ٢ نفهم كيف لا تنام.