أرسل استجابة ناجحة واحدة، واكتب جسمها تدريجيًا، وأنهِ الرسالة بالطريقة المعتادة. يوضح هذا الدليل العملي عبر طرفية Mac كيفية إرسال مستند اصطناعي كامل دون التعامل مع كل دفعة على أنها استجابة منفصلة أو استخدام 206 Partial Content.
المطلوب هو تقديم صفحة HTML مخصصة للاستضافة العامة، ثم التحقق من أن العميل يتلقى محتواها كاملًا. يجري العرض محليًا ولا ينشر أي شيء. تحتاج إلى الطرفية وcurl وإصدار LTS مدعوم من Node.js من صفحة تنزيل Node.js الرسمية.
لماذا لا تعني الدفعات استجابات جزئية؟
يجيب معنى الاستجابة وطريقة تأطير تسليمها عن سؤالين مختلفين. في استجابة ناجحة عادية من GET، تعني 200 OK نجاح الاسترجاع. أما 206 Partial Content فتعني أن الخادم ينفذ طلب نطاق بنقل أجزاء محددة من التمثيل. تقسيم الإرسال إلى عدة عمليات كتابة لا ينشئ طلب نطاق. RFC 9110: دلالات HTTP، 206 Partial Content.
على سبيل المثال، يمكن أن يصل دليل اصطناعي يتضمن مقدمة ووصفًا وفقرة ختامية تدريجيًا، مع بقائه مستندًا واحدًا. أرسل ترويسات الاستجابة مرة واحدة، ثم أقسام الجسم بالترتيب. لا تضع سطر حالة HTTP جديدًا قبل كل قسم.
يغلّف ترميز النقل المقسّم إلى دفعات في HTTP/1.1 بايتات الجسم بأطوال سداسية عشرية، وينتهي بدفعة طولها صفر وسطر فارغ ختامي مطلوب. تحدد بايتات التأطير هذه طريقة التسليم؛ وليست جزءًا من محتوى المقالة. يجب ألا يجمع المرسل بين Content-Length وTransfer-Encoding. RFC 9112: ترميز النقل المقسّم إلى دفعات، Content-Length.
القاعدة العملية هي ترك مكتبة HTTP تتولى تأطير الرسالة. يوفّر تطبيقك محتوى المستند ويشير إلى انتهاء ذلك المحتوى.
ما آلية البث التي ينبغي استخدامها؟
تعتمد الآلية المدعومة على إصدار HTTP. يظل بث جسم كامل ممكنًا عبر الإصدارات الثلاثة أدناه، لكن أساليب التأطير تختلف بينها.
| إصدار HTTP | كيفية نقل محتوى الجسم | آلية الإكمال |
|---|---|---|
| HTTP/1.1 مع الترميز المقسّم إلى دفعات | دفعات مسبوقة بأطوالها | دفعة نهائية طولها صفر واكتمال قسم التذييل |
| HTTP/2 | إطارات DATA |
END_STREAM في الإطار الختامي، بما في ذلك اكتمال أي كتلة ترويسة |
| HTTP/3 | إطارات DATA |
اكتمال الاستجابة وإغلاق اتجاه الإرسال في الدفق |
يحظر HTTP/2 صراحةً ترميز النقل المقسّم إلى دفعات الخاص بـ HTTP/1.1. ولا يعرّف HTTP/3 ترميزات النقل ويحظر ترويسة Transfer-Encoding. دع الخادم يتولى تأطير البروتوكول الذي يستخدمه بدلًا من فرض Transfer-Encoding: chunked على كل اتصال. RFC 9113: تبادلات رسائل HTTP، RFC 9114: تأطير رسائل HTTP.
إذا كان المستند الصغير مُعدًا بالفعل، فعادةً ما يكون إرساله بالطريقة المعتادة أسهل تنفيذًا. يصبح إجراء عمليات الكتابة التدريجية مفيدًا عندما يُنتج المحتوى بمرور الوقت. يتعمد المثال أدناه الفصل بين ثلاث عمليات كتابة بفواصل زمنية كي تتمكن من فحص السلوك؛ هذه التأخيرات للتوضيح وليست توصية للنشر.
ابدأ مستندًا كاملًا متدفقًا على جهاز Mac
افتح الطرفية وتحقق من المتطلبات الأساسية:
node --version
curl --version
إذا لم يكن node متاحًا، فثبّت إصدار LTS مدعومًا من صفحة تنزيل Node.js، ثم افتح نافذة طرفية جديدة. يستخدم البرنامج وحدات مضمّنة فقط ولا يحتاج إلى حزم npm.
الصق الكتلة الكاملة أدناه في الطرفية. ستبدأ خادم HTTP/1.1 على عنوان العرض http://127.0.0.1:8080/document. تشكّل السلاسل الثلاث معًا صفحة اصطناعية كاملة واحدة.
node <<'JS'
const http = require('node:http');
const parts = [
'<!doctype html>\n<html lang="en"><head><meta charset="utf-8">' +
'<title>Paper Shapes</title></head><body><main>\n' +
'<h1>Paper Shapes</h1>\n',
'<p>A circle, a square, and a triangle make this sample collection.</p>\n',
'<p>End of the paper shapes guide.</p>\n</main></body></html>\n'
];
const server = http.createServer((req, res) => {
res.on('error', error => console.error(error.message));
if (req.method !== 'GET' || req.url !== '/document') {
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('No sample document at this path.\n');
return;
}
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
let index = 0;
const timer = setInterval(() => {
if (res.destroyed) {
clearInterval(timer);
return;
}
res.write(parts[index++]);
if (index === parts.length) {
clearInterval(timer);
res.end();
}
}, 300);
res.on('close', () => clearInterval(timer));
});
server.on('error', error => {
console.error(error.message);
process.exitCode = 1;
});
server.listen(8080, '127.0.0.1', () => {
console.log('Sample: http://127.0.0.1:8080/document');
});
JS
يستدعي التطبيق writeHead() مرة واحدة، وwrite() ثلاث مرات، وend() مرة واحدة. توثّق Node عمليات الكتابة المتتالية بوصفها أجزاءً من جسم الاستجابة نفسه، وتتطلب end() لإكمال كل استجابة. تتولى Node تأطير HTTP؛ فلا تضف أطوال الدفعات أو دفعة نهائية طولها صفر إلى السلاسل. توثيق HTTP في Node.js.
هذا عرض محدود بثلاث سلاسل صغيرة. بالنسبة إلى منتِج كبير أو مستمر، راعِ الضغط العكسي: عندما تُرجع write() القيمة false، أوقف الإنتاج مؤقتًا حتى drain، مع التعامل أيضًا مع الإغلاق والأخطاء. توثيق كتابة الاستجابة في Node.js.
اترك نافذة الطرفية هذه قيد التشغيل. إذا أظهرت EADDRINUSE، فهذا يعني أن المنفذ 8080 مشغول؛ اختر منفذًا آخر متاحًا في الخادم وغيّر المنفذ في كل أوامر العميل أدناه. أوقف العرض باستخدام Control-C عند الانتهاء.
افحص المحتوى بعد فك ترميزه وتأطير دفعات HTTP
في نافذة طرفية ثانية، اطلب المستند:
curl --http1.1 --include --no-buffer --max-time 5 \
http://127.0.0.1:8080/document
توقع استجابة واحدة بحالة 200 OK، ونوع محتوى text/html; charset=utf-8، ومحتوى الصفحة المجمّع. ينبغي أن تستخدم الاستجابة في مثال HTTP/1.1 هذا Transfer-Encoding: chunked. يفك curl تأطير النقل عادةً، لذا يعرض ناتجه المعتاد المستند بدلًا من أحجام الدفعات السداسية عشرية.
لفحص التأطير، كرر الطلب باستخدام --raw:
curl --http1.1 --include --no-buffer --raw --max-time 5 \
http://127.0.0.1:8080/document
يوثّق curl الخيار --raw بأنه يعطّل فك ترميز المحتوى وترميزات النقل داخليًا. يعطّل --no-buffer تخزين خرج curl مؤقتًا، ويضع --max-time 5 حدًا زمنيًا للعملية. تجعل هذه الخيارات الأمر أداة تشخيصية قصيرة بدلًا من أمر ينتظر إلى أجل غير مسمى. دليل curl.
في الخرج الخام، تحيط أسطر الأحجام بالمحتوى، ثم تأتي الدفعة النهائية ذات الحجم صفر. من دون هذا المُنهي، يكون جسم HTTP/1.1 المقسّم إلى دفعات غير مكتمل. وفي التأطير ذي الطول الثابت، يكون استلام بايتات أقل من الطول المعلن غير مكتمل. RFC 9112: التعامل مع الرسائل غير المكتملة.
استخدم الخرج الخام لفحص النقل فقط. لا تنسخ أسطر الأحجام إلى ملف صفحة أو تتعامل معها كأقسام من المستند.
استلم المستند كاملًا قبل معالجته
إذا كانت خطوتك التالية تتطلب الصفحة كاملة، فاستهلك الجسم بأكمله قبل تحليله أو حفظه أو قبوله بأي طريقة أخرى. يعرّف WHATWG جسم الاستجابة على أنه تدفق؛ وتقرأ طريقته text() ذلك الجسم كاملًا وتعيده كسلسلة نصية، وتُرفض العملية إذا فشل استهلاك الجسم. لا يثبت استلام ترويسات الاستجابة وحده أن الجسم قد اكتمل. معيار WHATWG Fetch: مزج Body، معالجة الاستجابات.
مع استمرار تشغيل الخادم، الصق هذا العميل الكامل في نافذة الطرفية الثانية:
node --input-type=module <<'JS'
try {
const response = await fetch('http://127.0.0.1:8080/document', {
signal: AbortSignal.timeout(5000)
});
if (response.status !== 200) {
throw new Error(`Expected 200; received ${response.status}`);
}
const html = await response.text();
const marker = 'End of the paper shapes guide.';
if (!html.includes(marker) || !html.endsWith('</html>\n')) {
throw new Error('Expected sample ending is missing');
}
console.log(html);
console.log('Complete sample document received.');
} catch (error) {
console.error('Document not accepted:', error.message);
process.exitCode = 1;
}
JS
توفر Node تنفيذًا لـ fetch() متوافقًا مع المتصفحات. تنشئ AbortSignal.timeout() إشارة تُجهض العملية بعد عدد المللي ثانية المحدد؛ هنا، خمس ثوانٍ حد توضيحي لهذا المثال المحلي الصغير. توثيق الكائنات العامة في Node.js، توثيق مهلة AbortSignal.
يتحقق العميل من أمرين مختلفين: نجاح استهلاك الجسم والنهاية المتوقعة للعينة. الفحص الثاني تشخيص خاص بالتطبيق. يمكن لـ HTTP نقل مستند بنجاح رغم أن تطبيقك قصّره عن طريق الخطأ قبل استدعاء end(). تساعد علامة الإكمال في اكتشاف هذا الخطأ تحديدًا، لكنها لا تتحقق من كل فقرة ولا تُعد تحققًا عامًا من HTML.
يخزن هذا العميل الصفحة كاملة في الذاكرة المؤقتة. للمستندات الكبيرة جدًا، اختر المعالجة التدريجية باستخدام تنسيق مستند صريح وقواعد إكمال واضحة. لا تفترض أن كل قراءة توفر فقرة كاملة أو استجابة مستقلة ذات معنى.
شخّص الإكمال قبل تغيير رمز الحالة
استخدم التسلسل التالي لتحديد موضع المشكلة. فهو يفصل اكتمال النقل عن المحتوى الذي يقصده التطبيق، كي لا يخفي تنزيل يبدو ناجحًا نهايةً محذوفة.
| الملاحظة | الإجراء التالي |
|---|---|
| تعذّر على الأمر المحلي الاتصال | تأكد من أن الخادم يعمل وأن المنفذ متطابق. |
| انتهت مهلة curl | تحقق من أن المنتِج ينتهي ويستدعي end()؛ واحتفظ بمخرجات الخطأ. |
| اكتمل النقل لكن فشل فحص النهاية | افحص المحتوى المُنشأ والموضع الذي ينهي فيه التطبيق الاستجابة. |
| وصل المستند كاملًا دفعة واحدة | قارن الخرج الخام والمفكوك؛ ولا تستنتج وجود استجابات متعددة من توقيت العرض. |
تلقى طلب عادي لصفحة كاملة 206 |
تحقق مما إذا كان التعامل مع النطاقات قد طُبّق دون قصد؛ فالتسليم التدريجي وحده لا يبرره. |
فحوص الإكمال توصيات مستمدة من البروتوكول وسلوك Fetch، وليست ميزة HTTP منفصلة. إذا فشلت عملية قراءة الجسم، يبلغ العميل أعلاه عن الفشل وينهي التنفيذ بحالة غير ناجحة بدلًا من طباعة رسالة نجاح للمحتوى الذي وصل حتى ذلك الحين.
ما أثر ذلك على صفحة عامة وبحث Google؟
أبقِ محتوى المستند المفيد ضمن استجابة HTML نفسها. توضح Google أن الزحف إلى الصفحات وتحليلها يعمل جيدًا عندما تتضمن استجابة HTML كل محتواها، وتوصي بالعرض على جانب الخادم أو بالعرض المسبق لأن بعض برامج الروبوت لا تنفذ JavaScript. يتبع المثال هذا النهج: يصل نصه مباشرةً ضمن جسم المستند. مركز بحث Google: فهم أساسيات تحسين محركات البحث لـ JavaScript.
تقول وثائق زاحف Google إن استجابة 200 تمرر المحتوى المستلم إلى مرحلة المعالجة التالية؛ وهذا لا يضمن الفهرسة. لا تقدم الوثائق المشار إليها حجم دفعة أو تأخيرًا خاصًا يضمن فهرسة الصفحة. تعامل مع البث بوصفه آلية تسليم، وتحقق من المحتوى النهائي بشكل منفصل. Google: كيف تؤثر رموز حالة HTTP في برامج زحف Google.
عند تكييف هذا العرض للاستضافة العامة، أعد الفحوص على نقطة نهاية المستند الفعلية. تحقق من الحالة الناجحة، واستهلك الجسم كاملًا، وقارن محتواه النهائي بالصفحة المقصودة. اجعل فحص القبول مستقلًا عن عدد عمليات الكتابة أو الإطارات أو التحديثات المرئية التي نقلت المستند.
المصادر
- RFC 9110: دلالات HTTP — محرر RFC / IETF.
- RFC 9112: HTTP/1.1 — محرر RFC / IETF.
- RFC 9113: HTTP/2 — محرر RFC / IETF.
- RFC 9114: HTTP/3 — محرر RFC / IETF.
- معيار Fetch — WHATWG.
- HTTP — Node.js.
- الكائنات العامة — Node.js.
- تنزيل Node.js — Node.js.
- curl — كيفية الاستخدام — curl.
- فهم أساسيات تحسين محركات البحث لـ JavaScript — مركز بحث Google.
- كيف تؤثر رموز حالة HTTP في برامج زحف Google — Google للمطورين.