SELFLAB الستاك الكامل · ١١ إقليم SYSTEM ONLINE
الإقليم 00منهجٌ يُدرَس وحدك

الإقليم ٠ — الجسر: HTTP

يبني فوق: منهج الشبكات (TCP/sockets/ports، وإقليم ٧ فيه "تكلّم HTTP بأصابعك")، ومنهج بنية الويب (HTTP/HTTPS فوق TCP، تتبّع طلب). يفتح: كل ما بعده — لأن كل شيء في هذا الستاك يتنفّس HTTP.

النبذة

في منهج الشبكات بنيتَ الإنترنت من الإلكترون: MAC ثم IP ثم TCP — قناةُ بايتاتٍ موثوقةٌ ومرتّبةٌ بين طرفين. وفي الإقليم السابع "كنتَ الـ socket": فتحتَ nc وكتبتَ بأصابعك سطراً مثل GET / HTTP/1.1 ورجعت لك بايتات. لكن وقتها مرّرناها مرور الكرام — أداةٌ لاختبار السلك.

الآن نتوقّف عندها بالضبط. لأن هذا السطر الذي كتبته بأصابعك هو اللغة التي يتكلّمها الستاك كلّه. كل نقرةٍ في React، وكل دالةٍ في Express، وكل ما بينهما — ليست إلا بايتاتٍ من هذا الشكل تسري في قناة TCP التي تعرفها. من يتقن هذا الإقليم، يرى الستاك كلّه شفّافاً.


اللغز المستفزّ

عندك TCP: قناةُ بايتاتٍ موثوقة. هذا كل ما تعطيك إياه الشبكة. لا "طلبات"، ولا "ردود"، ولا "ملفّات" — بايتاتٌ تسري كالماء، بلا بداية ولا نهاية لرسالة.

افتح طرفين بـ nc على جهازك:

sh
# الطرف ١ (الخادم): يستمع على المنفذ 8080 ويطبع ما يصله حرفياً nc -l 8080 # الطرف ٢ (العميل): يتّصل ويكتب أنت بيدك nc localhost 8080

الآن جاوب — قبل أن تقرأ أي شيء بعد هذا السطر:

  1. كتبتَ في العميل سطرين ثم سطراً فارغاً. كيف يعرف الخادم أين انتهت "رسالتك" وبدأت الثانية؟ TCP لا يخبره — فهو بايتاتٌ بلا حدود. فمن يرسم الحدود؟
  2. لو أرسلتَ صورةً (بايتات ثنائية فيها صفرٌ هنا وسطرٌ جديدٌ هناك صدفةً) — كيف يميّز الخادمُ بايتةَ "نهاية الرسالة" عن بايتةٍ من داخل الصورة نفسها؟
  3. اتّصلتَ بالخادم، أرسلتَ، أُغلق. ثم اتّصلتَ ثانيةً. هل يعرف الخادمُ أنك نفس الشخص؟ ولو افترضنا أنه لا يعرف — كيف تعمل أي صفحةٍ "مسجَّل الدخول" في الدنيا؟

هذه الأسئلة الثلاثة هي HTTP كلّه: التأطير (أين تبدأ الرسالة وتنتهي)، والمعنى (ماذا تعني البايتات)، وانعدام الذاكرة (الخادم لا يتذكّرك). لا تكمل وأنت غير مستوعبٍ لمَ هي ألغاز. اقعد معها.


ليش: لماذا يوجد HTTP أصلاً؟

عندك TCP موثوق. لماذا لم يكتفِ الناس به؟

لأن TCP بروتوكول نقل (transport): يضمن أن البايتات تصل كاملةً ومرتّبة. لكنه أجوف من المعنى تماماً. تخيّل خطّ هاتفٍ نقيّ: يوصّل صوتك بأمانة، لكنه لا يعرف أتطلب بيتزا أم تحجز موعداً. تحتاج لغةً متّفقاً عليها فوق الخط. الطرفان لازم يتّفقان مسبقاً: "أول سطرٍ يقول ماذا أريد، ثم أسطرٌ تصف الطلب، ثم سطرٌ فارغٌ يعني ‹خلصت رأسي›، ثم المحتوى". هذا الاتّفاق اسمه بروتوكول التطبيق (application-layer protocol). HTTP واحدٌ منها (وSMTP للبريد، وFTP للملفّات...).

نموذج ذهني

النموذج الذهني (احفظه): HTTP ليس "تقنيةً". هو اتّفاقٌ نصّيٌّ على شكل رسالةٍ تُكتب داخل قناة TCP. لو اختفت كل برامج الويب وبقي اتّفاقٌ بشريٌّ على هذا الشكل، فأنت وأنا نقدر نتبادل HTTP بقلمٍ وورقة. الخادم ليس سحراً — هو برنامجٌ يقرأ هذا الشكل ويكتب شكلاً مثله. تذكّر هذا في كل إقليمٍ قادم.

الجواب على اللغز ١ و٢: التأطير فوق التيّار (framing over a stream)

TCP يعطيك تيّار بايتات بلا حدود. أي بروتوكولٍ فوقه لازم يرسم حدود الرسائل بنفسه. هذه مشكلةٌ قديمةٌ تعرف نظيرها في C: عندك read() يرجّع بايتات، فكيف تعرف أين تنتهي "السجلّة"؟ لك حلّان كلاسيكيّان:

HTTP يستعمل الاثنين معاً، كلٌّ في موضعه:

ملاحظة

هذه النقطة وحدها تشرح ٩٠٪ من أعطال الويب الغامضة: لو كذبتَ في Content-Length، أو نسيتَ السطر الفارغ، تجمّد الطرف الآخر ينتظر بايتاتٍ لن تأتي، أو يقطع رسالةً ناقصة. التأطير ليس تفصيلاً — هو العقد.

(فيه طريقةٌ ثالثة Transfer-Encoding: chunked حين لا تعرف الطول مسبقاً — تأطيرٌ بقطعٍ كلٌّ منها مسبوقٌ بطوله. بذرةٌ، نفتحها حين نبثّ بياناتٍ متدفّقة.)


كيف: تشريح رسالة HTTP

الطلب (request)

ثلاث طبقات، بنفس ترتيب التأطير أعلاه:

http
POST /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 وحده: نهاية الرأس.)

  1. سطر الطلب (request-line): METHOD SP target SP version.
    • method = الفعل: ماذا أفعل؟ (POST).
    • target = الاسم/المورد: على ماذا؟ (/api/v1/auth/register).
    • version = أي لهجةٍ من HTTP (HTTP/1.1).
    • فعلٌ + اسم. هذا قلب HTTP: افعل كذا على المورد كذا.
  2. الترويسات (headers): Key: Value لكلٍّ سطر. بياناتٌ عن الطلب: من أين (Host)، نوع الجسم (Content-Type)، طوله (Content-Length)، الهوية (Authorization)... ميتاداتا.
  3. السطر الفارغ ثم الجسم: الحمولة. اختياريّ (لا جسم في GET عادةً).

الرد (response)

نفس البنية، بسطرٍ أوّلَ مختلف:

http
HTTP/1.1 201 Created⏎ Content-Type: application/json⏎ Content-Length: 38⏎ ⏎ {"success":true,"data":{"id":"..."}}

ليش: دلالات الأفعال (methods) — ولماذا تهمّك عملياً

ليست الأفعال أسماءً عشوائية. لكلٍّ عقدٌ دلاليٌّ يعتمد عليه كل شيءٍ في الطريق (المتصفّح، الـ proxy، الـ cache، مكتبتك). كسرُ العقد يكسر أشياءً لا تتوقّعها.

الفعلالمعنىآمن (safe)؟عديم الأثر بالتكرار (idempotent)؟
GETاقرأ مورداً✅ لا يغيّر شيئاً
POSTأنشئ/نفّذ فعلاً جديداً❌ (مرّتان = سجلّان)
PUTاستبدل المورد كاملاً✅ (نفس النتيجة مهما كرّرت)
PATCHعدّل جزءاًليس بالضرورة
DELETEاحذف المورد✅ (محذوفٌ محذوف)
ملاحظة

الترتيب "ليش قبل كيف" هنا حرفيّ: لا تحفظ "POST للإنشاء". افهم أن عدم الـ idempotency هو سبب كون POST للإنشاء، وأن العقد الدلاليّ يخدم لا-موثوقية الشبكة التي بنيتها بنفسك في منهج الشبكات.


ليش: عائلات رموز الحالة — من المُلام؟

الرقم مقسومٌ لعائلاتٍ ليجيب فوراً عن سؤالٍ واحد: من سبّب هذه النتيجة؟

العائلةالمعنىالمُلام
1xxمعلوماتيّ، انتظر— (نادر)
2xxنجحالجميع سعيد
3xxاذهب لمكانٍ آخر (redirect)لا أحد
4xxطلبك أنت خاطئالعميل
5xxالخادم انهار وهو يعالج طلباً سليماًالخادم

الفرق بين 4xx و5xx ليس تجميلاً — هو توزيع المسؤولية، وهو ما تبني عليه مراقبتك (تذكّر QPS والـ monitoring من منهج بنية الويب): ارتفاع 5xx يعني كودُك مكسور؛ ارتفاع 4xx يعني عملاؤك يسيئون الاستعمال أو واجهتك مربكة. الرموز التي ستستعملها فعلياً في مشروعك:

ملاحظة

فخٌّ شائع وجذره: ردّ 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?... (أفعالٌ في الأسماء، فوضى لا نهاية لها)، تقول:

GET /api/v1/users/5 ← اقرأ المستخدم 5 DELETE /api/v1/users/5 ← احذفه GET /api/v1/curricula ← اقرأ المناهج (مجموعة) POST /api/v1/curricula ← أنشئ منهجاً GET /api/v1/curricula/9/sprints ← سبرنتات المنهج 9 (تداخل = علاقة)

ليش أفضل؟ لأن "الفعل" لم يعد جزءاً من اسمك — صار في الـ 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)

كل ما قرأته أعلاه ليس تجريداً — هو حرفياً عقد مشروعك:

حين تفتح أول ملفٍّ في backend/ وترى app.use("/auth", routes) و res.status(201).json(...) — لن ترى سحراً. سترى الرسالةَ التي كتبتها بأصابعك في nc، وقد أُتمتت. هذا هو الجسر.

بذرة الإقليم التالي

البذرة التالية: كتبتَ الخادمَ بيدك في nc. لكن nc لا يعرف يقرأ ألفَ طلبٍ في الثانية ولا يردّ على كلٍّ بمنطق. تحتاج برنامجاً. ستكتبه بلغةٍ غريبة: بلا main يدوم، بلا انتظارٍ يحجب، بخيطٍ واحدٍ يخدم الآلاف. لغةٌ مبنيّةٌ على فكرةٍ أنت تعرفها من C باسمٍ آخر: epoll. ننزل للإقليم ١ نتعلّم اللغة، ثم الإقليم ٢ نفهم كيف لا تنام.

الستاك الكامل · من باب الخادم إلى داخله ليش قبل كيف · صُمّم لـ يزيد