يرسل المتصفح طلب HTTP في صورة بايتات متتالية، وعلى الخادم أن يستخرج منها ما يريده العميل بدقة: أي مورد يطلب، وما البيانات الوصفية التي تحدد معالم طلبه، وأين تبدأ البيانات المرسلة إن وجدت. ولا يستقيم هذا الفهم إلا لأن رسالة HTTP/1.1 تصاغ بقالب حرفي ثابت، يعرف الطرفان فيه موضع كل جزء وما يعين علامة نهايته.
وتفاصيل هذه الصيغة ليست قيودا شكلية عابرة؛ فمسافة واحدة تفصل بين اسم الترويسة والنقطتين الرأسيتين تجعل الخادم يرفض الطلب كله، وسطر فارغ يسقط من نهاية الترويسات يترك الخادم معلقا ينتظر بقية رسالة لم تكتمل في نظره.

البايتات تصل متصلة بلا فواصل
يعمل بروتوكول HTTP فوق قناة نقل توصل البايتات كاملة وبترتيبها، لكنها تسلمها مجردة من أي تقسيم دلالي. فالطرف المستقبل يتلقى البايتات متتابعة كأنها شريط واحد طويل، ولا يجد فيها أي علامة تضعها القناة لتقول: هنا انتهى السطر الأول، وهنا بدأت البيانات.
لذلك كان لزاما أن تحمل الرسالة حدود أجزائها في بنيتها الداخلية. والبرنامج الذي يتولى قراءة الرسالة الواصلة وتفكيك أجزائها يسمى المحلل (parser). ولما كان المحلل لا يرى أمامه سوى شريط البايتات، فهو يحتاج إلى قواعد دقيقة يتفق عليها الطرفان مسبقا ليعرف أين ينتهي كل جزء. وتحدد مواصفة HTTP/1.1 هذه القواعد بدقة، ليفصل المحلل أقسام الرسالة بيقين لا تخمين فيه.
ثلاثة أجزاء بترتيب ثابت
خذ طلبا يرسله المتصفح ليقرأ صفحة مقال في مدونة:
GET /articles/http-message?lang=ar HTTP/1.1
Host: blog.example.com
User-Agent: Mozilla/5.0
Accept: text/htmlفي هذا الطلب سطر أول يفتتح بكلمة GET، تليه ثلاثة أسطر يتألف كل منها من اسم ونقطتين رأسيتين وقيمة. وبعد السطر الأخير يأتي سطر فارغ تماما لا يظهر في هذا العرض، لكنه جزء أصيل من بنية الرسالة لا يكتمل الطلب بدونه.
ويرد الخادم باستجابة تقوم على الترتيب الهيكلي نفسه:
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 21
<h1>HTTP message</h1>يسمى السطر الأول في كل رسالة سطر البدء (start line)، ووظيفته تلخيص الرسالة: ففي الطلب يحدد ماذا يريد العميل وأي مورد يقصد، وفي الاستجابة يبين نتيجة معالجة الطلب. وتلي سطر البدء سطور الترويسات (headers)، حيث يمثل كل سطر ترويسة مستقلة تحمل معلومة وصفية واحدة عن الرسالة. ثم يأتي سطر فارغ يعلن اكتمال الترويسات ونهايتها. وما بعد هذا السطر الفارغ هو الجسم (body)، أي المحتوى الفعلي أو البيانات التي تنقلها الرسالة: مستند HTML قصير في استجابة هذا المثال، في حين خلا الطلب السابق من أي جسم.
ولكل طريقة ورمز حالة وترويسة وردت في المثالين دلالة تفصيلية ستفرد لها الدروس اللاحقة حقها في الدورة، ولا يتوقف استيعاب البنية الهيكلية هنا على تلك المعاني؛ إذ يكفي الآن إدراك موضع كل جزء، وصيغة سطوره، وما يحدد نقطة نهايته.
نهاية السطر: الحرفان CR وLF
الأسطر المستقلة التي رأيناها في العرضين السابقين وسيلة لتيسير قراءة الرسالة على الشاشة، أما على أسلاك الشبكة فلا توجد أسطر مرئية، بل سيل متتابع من البايتات. والذي يعين نهاية كل سطر للمستقبل حرفان غير مرئيين يتعاقبان مباشرة: حرف الرجوع إلى أول السطر (carriage return, CR)، يليه حرف السطر الجديد (line feed, LF). وتطلق المواصفة القياسية على هذين الحرفين مجتمعين اسم CRLF، ويكتبان في معظم لغات البرمجة بالصيغة \r\n.
وهذا هو طلب قراءة المقال نفسه معروضا في سطر واحد متصل، تماما كما تتدفق بايتاته عبر الشبكة، مبينا فيه كل زوج من الحرفين بصيغته البرمجية \r\n:
GET /articles/http-message?lang=ar HTTP/1.1\r\nHost: blog.example.com\r\nUser-Agent: Mozilla/5.0\r\nAccept: text/html\r\n\r\nينتهي كل سطر من سطور الطلب بالزوج \r\n. وعند ختام الترويسات يتجاور زوجان متعاقبان: الزوج الأول ينهي سطر ترويسة Accept، والثاني يمثل السطر الفارغ ذاته، أي سطر لا يحتوي على أي محتوى قبل علامة ختامه. ومن هنا، فإن تتابع الرمزين \r\n\r\n هو العلامة الحاسمة التي يستدل بها المحلل على اكتمال قسم الترويسات.
وتعتمد مواصفة البروتوكول الزوج CRLF نهاية قياسية إلزامية لسطر البدء وكل سطر من سطور الترويسات. ومع أن المواصفة تسمح للطرف المستقبل بالتسامح وقبول حرف LF بمفرده نهاية للسطر، إلا أنها لا تلزمه بهذا القبول. ولهذا قد ينجح طلب كتب يدويا بنهاية \n مع خادم متساهل، في حين يرفضه خادم آخر يلتزم نص المعيار؛ أما الطلب المنضبط الذي يلتزم بصيغة \r\n فلا يرهن نجاحه بتسامح المستقبل.
سطر الطلب: الطريقة والهدف والنسخة
سطر البدء في رسالة الطلب يسمى سطر الطلب (request line)، وهو مبني من ثلاث قطع تفصل بين كل قطعة وتاليتها مسافة واحدة، ويختم بـCRLF:
GET /articles/http-message?lang=ar HTTP/1.1- الطريقة (method): الكلمة
GETفي هذا المثال، وهي الفعل الذي يحدد طبيعة العملية المطلوبة على المورد. ولكل طريقة دلالات وخصائص تفرد لها الدورة درسا مستقلا. - هدف الطلب (request target): الجزء
/articles/http-message?lang=ar، وهو المسار والاستعلام مدمجين كما استخلصهما المتصفح من الرابط. فالرابط لا يدرج بكامله في هذا السطر؛ إذ ينتقل اسم المضيف مستقلا داخل ترويسةHost، ويعرف المخطط تلقائيا من نوع القناة التي حملت الاتصال، في حين يستبعد ما بعد علامة#محليا ولا يرسل قط. - النسخة (HTTP version): الصيغة
HTTP/1.1، وتخبر المستقبل بقواعد أي نسخة صيغت بها الرسالة ليعاملها بموجبها.
ويستطيع المحلل تفكيك هذا السطر بأمان عند المسافتين الفاصلتين دون أدنى لبس في تعيين حدود هدف الطلب؛ فالهدف لا يحتوي على أي مسافة حرفية، لأن أي مسافة يتطلبها المسار أو الاستعلام تخضع للترميز الإلزامي قبل الإرسال، كأن تكتب %20 وفق قواعد الترميز بالنسبة المئوية.
وتتسم الطريقة ورقم النسخة بحساسية تامة لحالة الأحرف؛ فالصيغة get ليست الطريقة القياسية GET، وhttp/1.1 ليست النسخة المعتمدة HTTP/1.1.
سطر الحالة: النسخة والرمز وعبارة السبب
يقابل سطر الطلب في الاستجابة سطر بدء خاص يسمى سطر الحالة (status line)، ويتألف هو الآخر من ثلاث قطع تفصل بينها مسافات، لكن بترتيب مختلف:
HTTP/1.1 200 OK- النسخة:
HTTP/1.1، وتتصدر أول السطر هذه المرة لتبين نسخة البروتوكول المتبعة في صياغة الجواب. - رمز الحالة: الرقم
200، ثلاثة أرقام يقرؤها برنامج العميل آليا ليعرف نتيجة طلبه. - عبارة السبب (reason phrase): النص
OK، وهو وصف بشري موجز يوضح معنى الرمز لمن يقرأ الرسالة من البشر.
وتنفرد عبارة السبب بين عناصر سطر البدء بأنها لا تحمل أي وزن وظيفي في منطق البرمجيات؛ فالمواصفة تجعلها اختيارية، وتوصي العميل بتجاهل محتواها لأنها ليست وسيلة موثوقة لنقل المعلومات. وقد يرسل خادم 200 OK ويرسل خادم آخر 200 Success، والنتيجة عند العميل واحدة لأن الرمز واحد. وإذا غابت العبارة، بقي على الخادم أن يرسل المسافة التي تلي الرمز، فيصير السطر HTTP/1.1 200 مختوما بمسافة في آخره.
الترويسات: سطر لكل حقل
بعد سطر البدء مباشرة تبدأ الترويسات. وكل ترويسة سطر مستقل تسميه المواصفة حقل ترويسة (header field). ويتكون هذا السطر من اسم الحقل (field name)، ثم نقطتين، ثم قيمة الحقل (field value)، وينتهي بـCRLF:
Content-Type: text/htmlالاسم في هذا السطر Content-Type، والقيمة text/html. ولكتابة سطر الترويسة وقراءته ثلاث قواعد:
- اسم الحقل لا يتأثر بحالة الأحرف.
Content-Typeوcontent-typeوCONTENT-TYPEاسم واحد، بخلاف الطريقة والنسخة في سطر البدء. ولذلك قد تحمل استجابة حقيقية واحدة أسماء تبدأ بحروف كبيرة مثلContent-Type، وأخرى مكتوبة كلها بحروف صغيرة مثلlast-modified، دون أن يختلف معناها. - المسافات حول القيمة ليست منها.
Content-Type:text/htmlوContent-Type: text/htmlيحملان القيمة نفسها، لأن المحلل يحذف المسافات في أول القيمة وآخرها. والمعتاد مسافة واحدة بعد النقطتين. - لا مسافة بين الاسم والنقطتين. السطر
Content-Type : text/htmlمخالف للمواصفة، وعلى الخادم أن يرفض الطلب الذي يحمله برمز 400. وسبب هذا التشدد أن البرامج اختلفت في الماضي في تفسير هذه المسافة، ففتح هذا الاختلاف ثغرات أمنية.
السطر الفارغ الذي ينهي الترويسات
لا يحدد HTTP عددا ثابتا للترويسات؛ فقد تحمل الرسالة ثلاث ترويسات أو ثلاثين. لذلك لا يعرف المحلل نهايتها من عدد السطور، بل من علامة: سطر فارغ، أي زوج CRLF يأتي مباشرة بعد الزوج الذي أنهى السطر السابق. فيقرأ المحلل سطرا بعد سطر، ويعد كل سطر حقل ترويسة، حتى يصل إلى السطر الفارغ فيعرف أن قسم الترويسات انتهى.
والسطر الفارغ واجب في كل رسالة، حتى في طلب لا جسم له. فلو أرسل العميل سطر الطلب وترويساته ولم يرسل السطر الفارغ، لظل الخادم ينتظر، لأن البايتات التي وصلته لا تقول إن الترويسات اكتملت، وقد يكون سطر ترويسة آخر في الطريق. ويستمر هذا الانتظار حتى تنقضي مهلة يحددها الخادم فيقطع الطلب، وهي ستون ثانية في الإعداد الافتراضي لخادم nginx.
ويظهر وجوب السطر الفارغ في أصغر طلب صحيح في HTTP/1.1، وهو ثلاثة أسطر: سطر الطلب، وترويسة Host، ثم السطر الفارغ، مع أن الطلب لا جسم له:
GET / HTTP/1.1\r\nHost: example.com\r\n\r\nوأما ترويسة Host فلا يكتمل الطلب في HTTP/1.1 من دونها. فالمواصفة تلزم العميل بإرسالها في كل طلب، وتلزم الخادم بأن يرفض الطلب الذي يخلو منها برمز الحالة 400، وهو الرمز الذي يرد به الخادم على طلب يراه خطأ من جهة العميل.
الجسم: ما بعد السطر الفارغ
لم يحمل طلب قراءة المقال جسما، لأن العميل لم يرسل فيه بيانات، بل طلب موردا فحسب. أما حين يرسل العميل بيانات، كتعليق يكتبه القارئ تحت المقال، فتنتقل هذه البيانات في جسم الطلب:
POST /articles/http-message/comments HTTP/1.1
Host: blog.example.com
Content-Type: application/json
Content-Length: 31
{"comment":"Clear explanation"}يبدأ الجسم بأول بايت بعد السطر الفارغ، وهو { في هذا المثال. والجسم اختياري: طلب القراءة بـGET يأتي بلا جسم عادة، وطلب مثل POST يرسل بيانات فيحمله، والاستجابة تحمله في أغلب الحالات لأنها تنقل المورد المطلوب.
ولا تسري على الجسم قواعد السطور التي تحكم الترويسات. فهو بايتات تنقل كما هي: قد يكون نصا فيه أسطر فارغة كثيرة، كمستند HTML طويل، وقد يكون صورة لا علاقة لبايتاتها بالنص أصلا، فقد تتجاور فيها البايتات \r\n\r\n في أي موضع. ولا يلزم كذلك أن تنتهي آخر بايتات الجسم بالزوج \r\n.
لذلك لا يستطيع المحلل أن يعرف نهاية الجسم بالطريقة التي عرف بها نهاية الترويسات. فلو بحث عن سطر فارغ آخر، لقطع الجسم عند أول سطر فارغ في مستند HTML، أو عند أول موضع في صورة تتجاور فيه هذه البايتات. وإنما تعلن الترويسات كيف يعرف طول الجسم: في هذا المثال تقول Content-Length: 31 إن الجسم 31 بايتا، وهو طول {"comment":"Clear explanation"} بالضبط. وحين لا تعلن ترويسات الطلب عن جسم، ينتهي الطلب عند السطر الفارغ نفسه. أما طرق إعلان الطول، وما يحدث في الاستجابات التي لا تعلنه، فلها درس مستقل.
كيف يقرأ الخادم طلبا وصله
حين تصل بايتات طلب التعليق إلى الخادم، يمر بها المحلل بهذا الترتيب:
- يقرأ حتى أول CRLF، فيحصل على سطر الطلب، ويقطعه عند المسافتين إلى الطريقة
POST، والهدف/articles/http-message/comments، والنسخةHTTP/1.1. - يقرأ السطور التالية واحدا بعد واحد، ويقسم كل سطر عند أول نقطتين إلى اسم وقيمة:
HostثمContent-TypeثمContent-Length. ويقسم عند أول نقطتين تحديدا لأن القيمة نفسها قد تحتوي نقطتين، كما فيHost: localhost:3000. - يصل إلى السطر الفارغ، فيعرف أن الترويسات انتهت، وأن ما بعده جسم.
- يرى أن
Content-Lengthتعلن جسما طوله 31 بايتا، فيقرأ 31 بايتا بالضبط، ثم يعد الطلب مكتملا.
ولا يبحث المحلل داخل الجسم عن نقطتين أو عن أسطر. فالبايتات التي تلي السطر الفارغ، بالطول الذي أعلنته الترويسات، كلها من الجسم، ولو بدا جزء منها كأنه سطر ترويسة. وبعد أن تنفصل الأجزاء، يسلمها المحلل إلى تطبيق الخادم: الطريقة والهدف ليعرف ما يفعله، والترويسات لتصف الطلب، والجسم ليحمل نص التعليق نفسه. ويقرأ المتصفح الاستجابة بخطوات مماثلة، مع فرق في الخطوة الأولى: السطر الذي يقطعه هناك سطر حالة.
الأجزاء نفسها في HTTP/2 وHTTP/3
الشكل النصي الذي سبق هو شكل HTTP/1.1 وحده. أما HTTP/2 وHTTP/3 فلا ينقلان الرسالة سطورا من النص، بل يضعانها في إطارات ثنائية (binary frames)، أي وحدات من البايتات لا تكتب نصا يقرأ بالعين، ولها تفصيل مستقل في الدورة. ومع ذلك تبقى أجزاء الرسالة كما هي: في كل طلب طريقة وهدف، وفي كل استجابة رمز حالة، وفي الرسالتين ترويسات وجسم اختياري. الذي يتغير هو طريقة حمل هذه الأجزاء.
فلا سطر بدء في النسختين. وتنتقل قطعه إلى حقول خاصة تسمى حقول الترويسة الزائفة (pseudo-header fields)، يبدأ اسم كل منها بنقطتين، وتأتي قبل الترويسات العادية. وهذا طلب قراءة المقال نفسه معروضا بحقوله كما يحملها HTTP/2:
:method: GET
:scheme: https
:authority: blog.example.com
:path: /articles/http-message?lang=ar
user-agent: Mozilla/5.0
accept: text/htmlيقابل :method الطريقة، و:path هدف الطلب، و:authority المضيف الذي كانت تحمله ترويسة Host، و:scheme المخطط الذي لم يكن سطر الطلب في HTTP/1.1 يذكره. أما الاستجابة فلها حقل زائف واحد هو :status، ويحمل رمز الحالة وحده؛ فلا مكان في النسختين لعبارة السبب ولا لرقم النسخة. وتشترط النسختان كذلك أن تكتب أسماء الحقول كلها بحروف صغيرة.
وعرض هذه الحقول في سطور تيسير للقراءة فقط؛ أما على الشبكة فتنتقل مضغوطة داخل إطار ثنائي. ولهذا تعرض بعض الأدوات رسائل HTTP/2 في صورة قريبة من شكل HTTP/1.1: فأداة curl تكتب أول سطر في الاستجابة HTTP/2 200 بلا عبارة سبب، وتعرض أسماء الترويسات بعده بحروف صغيرة. وهذا سطر تصنعه الأداة لتسهيل القراءة، لا سطر مر على الشبكة.
رسالة تحمل حدودها معها
يقوم الجزء النصي من رسالة HTTP/1.1 على اتفاق حول بايتات قليلة: مسافة تفصل قطع سطر البدء، ونقطتان تفصلان اسم الحقل عن قيمته، وCRLF تنهي السطر، وسطر فارغ ينهي الترويسات. وبفضل هذه البساطة يقرأ المطور الرسالة بعينه، ويستطيع أن يكتب طلبا صحيحا في ثلاثة أسطر.
والاتفاق نفسه يفرض الدقة. فالمحلل لا يرى غير البايتات، وإذا اختلف برنامجان على موضع علامة واحدة، قرأ كل منهما من البايتات نفسها رسالة مختلفة. لذلك تمنع المواصفة المسافة قبل النقطتين منعا صارما، وتلزم الخادم برفض الطلب الذي يحملها: الخلاف في مثل هذه التفاصيل خلاف على حدود الرسالة نفسها، لا على طريقة كتابتها.
اختبر فهمك
ثمانية أسئلة قصيرة تغطي أفكار الدرس. اختر إجابة ثم اضغط «تحقق» لترى إن كانت صحيحة ولماذا. وفي آخر الاختبار تظهر نتيجتك، ومع كل سؤال أخطأت فيه زر يعيدك إلى القسم الذي يشرحه.
\n وحده. ماذا تقول المواصفة عن هذا الطلب؟\n هي نهاية السطر المعتمدة في HTTP/1.1\r\n، ويجوز للخادم أن يقبل \n لكنه غير ملزم\n إلى \r\n قبل قراءة الطلب، فلا فرق بينهما\n وحدهPOST /search?q=http&page=2 HTTP/1.1؟POST /search، لأن الهدف يبدأ بالطريقة/search وحده، لأن الاستعلام يرسل في ترويسة مستقلةHTTP/1.1، لأنه آخر قطعة في السطر/search?q=http&page=2، أي المسار والاستعلام معاPOST فهو الطريقة، وHTTP/1.1 هو النسخة، ولا يفصل الاستعلام في ترويسة خاصة.HTTP/1.1 200 Not Found. كيف يفهم البرنامج نتيجة طلبه؟200 ويتجاهل عبارة السببNot Found200 هنا هو ما يحدد النتيجة، مهما قالت العبارة بعده.accept: text/htmlAccept:text/htmlAccept : text/htmlACCEPT: text/htmlGET / HTTP/1.1\r\nHost: example.com\r\n ثم توقف دون أن يغلق الاتصال. ماذا يحدث عند الخادم؟GET لا جسم لهHost جسما للطلب، لأنه آخر سطر وصلHostHost، فيبقى الخادم ينتظر سطر ترويسة آخر أو السطر الفارغ، ولا سبب للرمز 400 لأن ترويسة Host موجودة.\r\n\r\n في أي موضعHTTP/1.1 200 OK\r\nContent-Length: 7\r\n\r\nsaved:1. كيف يعامل المحلل النص saved:1؟saved وقيمته 1، لأن فيه نقطتين\r\nContent-Length: 7 أن الجسم سبعة بايتات، وهي بالضبط saved:1، ولا يلزم الجسم أن ينتهي بالزوج \r\n.HTTP/2 200 OK في أول الرسالةStatus بين بقية الترويسات:status، ولا تحمل الرسالة عبارة سبب:status الذي يسبق الترويسات العادية، ولا مكان فيه لعبارة السبب. أما السطر HTTP/2 200 الذي تعرضه أداة مثل curl فتصنعه الأداة للقراءة، ولم يمر على الشبكة.
