تشريح معاملة HTTP
يهدف هذا الدليل إلى إعطاء فهم متين لعملية معالجة HTTP في Node.js. سنفترض أنك تعرف، بشكل عام، كيف تعمل طلبات HTTP، بغض النظر عن اللغة أو بيئة البرمجة. سنفترض أيضًا معرفة مسبقة بـ Node.js EventEmitters و Streams. إذا لم تكن على دراية بها تمامًا، فمن المفيد قراءة سريعة لوثائق API لكل منهما.
إنشاء الخادم
ستحتاج أي تطبيق خادم ويب عقدة في مرحلة ما إلى إنشاء كائن خادم ويب. يتم ذلك باستخدام createServer
.
const http = require('node:http')
const server = http.createServer((request, response) => {
// تحدث السحرة هنا!
})
يتم استدعاء الدالة التي تم تمريرها إلى createServer
مرة واحدة لكل طلب HTTP يتم إجراؤه على هذا الخادم، لذلك يطلق عليه مُعالِج الطلب. في الواقع، فإن كائن Server الذي تم إرجاعه بواسطة createServer
هو EventEmitter، وما لدينا هنا هو مجرد اختصار لإنشاء كائن خادم ثم إضافة المُستمع لاحقًا.
const server = http.createServer()
server.on('request', (request, response) => {
// يحدث نفس نوع السحر هنا!
})
عندما يصل طلب HTTP إلى الخادم، فإن العقدة تستدعي دالة مُعالِج الطلب مع بعض الكائنات المفيدة للتعامل مع المعاملة، والطلب، والاستجابة. سنتطرق إلى ذلك قريبًا. من أجل تقديم الطلبات بالفعل، يجب استدعاء طريقة listen
على كائن الخادم. في معظم الحالات، كل ما ستحتاج إلى تمريره إلى listen
هو رقم المنفذ الذي تريد أن يستمع عليه الخادم. هناك بعض الخيارات الأخرى أيضًا، لذا راجع مرجع API.
الطريقة وعنوان URL والرؤوس
عند معالجة طلب، فإن أول شيء سترغب في القيام به على الأرجح هو النظر إلى الطريقة وعنوان URL، بحيث يمكن اتخاذ الإجراءات المناسبة. يجعل Node.js هذا سهلاً نسبيًا من خلال وضع خصائص مفيدة على كائن الطلب.
const { method, url } = request
كائن الطلب هو مثيل لـ IncomingMessage
. ستكون الطريقة هنا دائمًا طريقة/فعل HTTP عادية. عنوان url هو عنوان URL الكامل بدون الخادم أو البروتوكول أو المنفذ. بالنسبة لعنوان URL نموذجي، هذا يعني كل شيء بعد وبعد تضمين الشرطة المائلة الثالثة للأمام.
الرؤوس ليست بعيدة أيضًا. إنها موجودة في كائنها الخاص على الطلب يسمى headers
.
const { headers } = request
const userAgent = headers['user-agent']
من المهم ملاحظة هنا أن جميع الرؤوس ممثلة بحروف صغيرة فقط، بغض النظر عن كيفية إرسال العميل لها بالفعل. هذا يبسط مهمة تحليل الرؤوس لأي غرض.
إذا تم تكرار بعض الرؤوس، فسيتم استبدال قيمها أو ضمها معًا كسلاسل مفصولة بفاصلات، حسب الرأس. في بعض الحالات، قد يكون هذا أمرًا إشكاليًا، لذلك تتوفر أيضًا rawHeaders
.
جسم الطلب
عند استقبال طلب POST أو PUT، قد يكون جسم الطلب مهمًا لتطبيقك. إن الحصول على بيانات الجسم أكثر تعقيدًا بعض الشيء من الوصول إلى رؤوس الطلب. كائن الطلب الذي يتم تمريره إلى مُعالِج ينفذ واجهة ReadableStream
. يمكن الاستماع إلى هذا التدفق أو نقله إلى مكان آخر تمامًا مثل أي تدفق آخر. يمكننا الحصول على البيانات مباشرة من التدفق من خلال الاستماع إلى حدثي 'data'
و 'end'
الخاصين بالتدفق.
الجزء المُصدر في كل حدث 'data'
هو Buffer
. إذا كنت تعلم أنه سيكون بيانات نصية، فإن أفضل ما يمكنك فعله هو جمع البيانات في مصفوفة، ثم عند 'end'
، دمجها وتحويلها إلى نص.
let body = []
request.on('data', chunk => {
body.push(chunk)
})
request.on('end', () => {
body = Buffer.concat(body).toString()
// في هذه المرحلة، يحتوي 'body' على كامل جسم الطلب المخزّن فيه كنص
})
ملاحظة
قد يبدو هذا مملًا بعض الشيء، وفي كثير من الحالات، يكون كذلك. لحسن الحظ، توجد وحدات مثل concat-stream
و body
على npm والتي يمكنها المساعدة في إخفاء بعض هذه المنطق. من المهم أن يكون لديك فهم جيد لما يحدث قبل اتباع هذا المسار، وهذا هو سبب وجودك هنا!
شيء سريع حول الأخطاء
بما أن كائن الطلب هو ReadableStream
، فهو أيضًا EventEmitter
ويتصرف مثله عندما يحدث خطأ.
يظهر خطأ في تدفق الطلب من خلال إصدار حدث 'error'
على التدفق. إذا لم يكن لديك مستمع لهذا الحدث، فسيتم طرح الخطأ، مما قد يؤدي إلى تعطل برنامج Node.js الخاص بك. لذلك يجب عليك إضافة مستمع 'error'
إلى تيارات الطلبات الخاصة بك، حتى لو قمت فقط بتسجيله والاستمرار في طريقك. (على الرغم من أنه من الأفضل على الأرجح إرسال نوع من استجابة خطأ HTTP. المزيد عن ذلك لاحقًا.)
request.on('error', err => {
// هذا يُطبع رسالة الخطأ ومسار المكدس إلى stderr.
console.error(err.stack)
})
هناك طرق أخرى لـ معالجة هذه الأخطاء مثل التجريدات والأدوات الأخرى، ولكن كن على دراية دائمًا بأن الأخطاء يمكن أن تحدث وتحدث، وسيتعين عليك التعامل معها.
ما وصلنا إليه حتى الآن
في هذه المرحلة، غطينا إنشاء خادم، والحصول على الطريقة، وعنوان URL، والرؤوس، والهيكل من الطلبات. عندما نجمع كل ذلك معًا، قد يبدو الأمر كما يلي:
const http = require('node:http');
http.createServer((request, response) => {
const { headers, method, url } = request;
let body = [];
request.on('error', err => console.error(err));
request.on('data', chunk => {
body.push(chunk);
});
request.on('end', () => {
body = Buffer.concat(body).toString();
// في هذه المرحلة، لدينا الرؤوس، والطريقة، وعنوان URL، والهيكل، ويمكننا الآن
// القيام بأي شيء نحتاج إليه من أجل الاستجابة لهذا الطلب.
});
});
.listen(8080); // ينشط هذا الخادم، ويستمع على المنفذ ٨٠٨٠.
إذا قمنا بتشغيل هذا المثال، سنتمكن من تلقي الطلبات، ولكن ليس الرد عليها. في الواقع، إذا قمت بتشغيل هذا المثال في متصفح ويب، فسيستنفد طلبك مهلة الوقت، حيث لا يتم إرسال أي شيء مرة أخرى إلى العميل.
حتى الآن لم نلمس كائن الاستجابة على الإطلاق، وهو مثيل لـ ServerResponse
، وهو WritableStream
. يحتوي على العديد من الطرق المفيدة لإرسال البيانات مرة أخرى إلى العميل. سنغطي ذلك لاحقًا.
رمز حالة HTTP
إذا لم تكلف نفسك عناء تعيينه، فستكون حالة رمز HTTP دائمًا 200. بالطبع، ليس كل استجابة HTTP تستحق هذا، وفي مرحلة ما سترغب بالتأكيد في إرسال رمز حالة مختلف. للقيام بذلك، يمكنك تعيين خاصية statusCode
.
response.statusCode = 404 // أخبر العميل أن المورد لم يتم العثور عليه.
هناك بعض الاختصارات الأخرى لهذا، كما سنرى قريبًا.
تعيين رؤوس الاستجابة
يتم تعيين الرؤوس من خلال طريقة مريحة تسمى setHeader
.
response.setHeader('Content-Type', 'application/json')
response.setHeader('X-Powered-By', 'bacon')
عند تعيين الرؤوس على الاستجابة، فإن الحرف كبير أو صغير لا يهم في أسمائها. إذا قمت بتعيين رأس بشكل متكرر، فإن القيمة الأخيرة التي قمت بتعيينها هي القيمة التي سيتم إرسالها.
إرسال بيانات الرأس صراحةً
تفترض طرق تعيين رؤوس الاستجابة ورمز الحالة التي ناقشناها بالفعل أنك تستخدم "رؤوس ضمنية". وهذا يعني أنك تعتمد على العقدة لإرسال الرؤوس نيابةً عنك في الوقت المناسب قبل البدء في إرسال بيانات الجسم.
إذا أردت، يمكنك كتابة الرؤوس صراحةً في دفق الاستجابة. للقيام بذلك، هناك طريقة تسمى writeHead
، والتي تكتب رمز الحالة والرؤوس في الدفق.
إرسال بيانات الرأس صراحةً
response.writeHead(200, {
'Content-Type': 'application/json',
'X-Powered-By': 'bacon',
})
بمجرد تعيين الرؤوس (ضمنيًا أو صراحةً)، ستكون جاهزًا لبدء إرسال بيانات الاستجابة.
إرسال جسم الاستجابة
بما أن كائن الاستجابة هو WritableStream
، فإن كتابة جسم الاستجابة إلى العميل هي مجرد مسألة استخدام طرق الدفق المعتادة.
response.write('<html>')
response.write('<body>')
response.write('<h1>Hello, World!</h1>')
response.write('</body>')
response.write('</html>')
response.end()
يمكن لدالة end
في الدفقات أيضًا أن تتلقى بعض البيانات الاختيارية لإرسالها كآخر جزء من البيانات في الدفق، لذلك يمكننا تبسيط المثال أعلاه على النحو التالي.
response.end('<html><body><h1>hello,world!</h1></body></html>')
ملاحظة
من المهم تعيين الحالة والرؤوس قبل البدء في كتابة أجزاء البيانات في الجسم. هذا منطقي، لأن الرؤوس تأتي قبل الجسم في استجابات HTTP.
شيء آخر سريع حول الأخطاء
يمكن لدفق الاستجابة أيضًا إصدار أحداث "خطأ"، وفي مرحلة ما سيتعين عليك التعامل مع ذلك أيضًا. جميع النصائح المتعلقة بأخطاء دفق الطلب لا تزال تنطبق هنا.
ضع كل شيء معًا
الآن بعد أن تعلمنا عن إنشاء استجابات HTTP، دعونا نضع كل شيء معًا. بناءً على المثال السابق، سنقوم بإنشاء خادم يُعيد جميع البيانات التي أرسلها المستخدم إلينا. سنقوم بتنسيق هذه البيانات بتنسيق JSON باستخدام JSON.stringify
.
const http = require('node:http')
http
.createServer((request, response) => {
const { headers, method, url } = request
let body = []
request
.on('error', err => {
console.error(err)
})
.on('data', chunk => {
body.push(chunk)
})
.on('end', () => {
body = Buffer.concat(body).toString()
// بداية الأشياء الجديدة
response.on('error', err => {
console.error(err)
})
response.statusCode = 200
response.setHeader('Content-Type', 'application/json')
// ملاحظة: يمكن استبدال السطرين أعلاه بالسطر التالي:
// response.writeHead(200, {'Content-Type': 'application/json'})
const responseBody = { headers, method, url, body }
response.write(JSON.stringify(responseBody))
response.end()
// ملاحظة: يمكن استبدال السطرين أعلاه بالسطر التالي:
// response.end(JSON.stringify(responseBody))
// نهاية الأشياء الجديدة
})
})
.listen(8080)
مثال لخادم الصدى
دعونا نبسط المثال السابق لننشئ خادم صدى بسيطًا، والذي يُرسل فقط أي بيانات تم استلامها في الطلب مرة أخرى في الاستجابة. كل ما نحتاج إلى فعله هو الحصول على البيانات من دفق الطلب وكتابة تلك البيانات في دفق الاستجابة، على غرار ما قمنا به سابقًا.
const http = require('node:http');
http.createServer((request, response) => {
let body = [];
request.on('data', chunk => {
body.push(chunk);
});
request.on('end', () => {
body = Buffer.concat(body).toString();
response.end(body);
});
});
.listen(8080);
الآن دعونا نعدل هذا. نريد فقط إرسال صدى في ظل الظروف التالية:
- طريقة الطلب هي POST.
- عنوان URL هو /echo.
في أي حالة أخرى، نريد ببساطة الرد برمز 404.
const http = require('node:http')
http
.createServer((request, response) => {
if (request.method === 'POST' && request.url === '/echo') {
let body = []
request
.on('data', chunk => {
body.push(chunk)
})
.on('end', () => {
body = Buffer.concat(body).toString()
response.end(body)
})
} else {
response.statusCode = 404
response.end()
}
})
.listen(8080)
ملاحظة
من خلال التحقق من عنوان URL بهذه الطريقة، نقوم بنوع من "التوجيه". يمكن أن تكون أشكال التوجيه الأخرى بسيطة مثل عبارات switch
أو معقدة مثل أطر عمل كاملة مثل express
. إذا كنت تبحث عن شيء يقوم بالتوجيه ولا شيء آخر، فجرّب router
.
رائع! الآن دعونا نجرب تبسيط هذا. تذكر، أن كائن الطلب هو ReadableStream
وكائن الاستجابة هو WritableStream
. هذا يعني أنه يمكننا استخدام pipe
لتوجيه البيانات من أحدهما إلى الآخر. هذا هو بالضبط ما نريده لخادم الصدى!
const http = require('node:http')
http
.createServer((request, response) => {
if (request.method === 'POST' && request.url === '/echo') {
request.pipe(response)
} else {
response.statusCode = 404
response.end()
}
})
.listen(8080)
تيارات رائعة!
لسنا قد انتهينا تمامًا بعد. كما ذكرنا عدة مرات في هذا الدليل، يمكن أن تحدث الأخطاء وتحدث، ونحتاج إلى التعامل معها.
للتعامل مع الأخطاء في دفق الطلب، سنقوم بتسجيل الخطأ في stderr
وإرسال رمز حالة 400 للإشارة إلى طلب خاطئ
. ومع ذلك، في تطبيق في العالم الحقيقي، سنرغب في فحص الخطأ لمعرفة رمز الحالة والرسالة الصحيحين. كالمعتاد مع الأخطاء، يجب عليك الرجوع إلى وثائق الخطأ.
في الاستجابة، سنقوم فقط بتسجيل الخطأ في stderr
.
const http = require('node:http')
http
.createServer((request, response) => {
request.on('error', err => {
console.error(err)
response.statusCode = 400
response.end()
})
response.on('error', err => {
console.error(err)
})
if (request.method === 'POST' && request.url === '/echo') {
request.pipe(response)
} else {
response.statusCode = 404
response.end()
}
})
.listen(8080)
لقد غطينا الآن معظم أساسيات التعامل مع طلبات HTTP. في هذه المرحلة، يجب أن تكون قادرًا على:
- إنشاء خادم HTTP باستخدام دالة مُعالجة
request
، وجعله يستمع على منفذ. - الحصول على الرؤوس وعنوان URL والطريقة وبيانات الجسم من كائنات
request
. - اتخاذ قرارات التوجيه بناءً على عنوان URL أو بيانات أخرى في كائنات
request
. - إرسال الرؤوس وأكواد حالة HTTP وبيانات الجسم عبر كائنات
response
. - توجيه البيانات من كائنات
request
وإلى كائنات الاستجابة. - معالجة أخطاء الدفق في كل من تيارات
request
وresponse
.
من هذه الأساسيات، يمكن إنشاء خوادم HTTP Node.js لكثير من حالات الاستخدام النموذجية. هناك الكثير من الأشياء الأخرى التي توفرها هذه واجهات برمجة التطبيقات، لذا تأكد من قراءة وثائق واجهة برمجة التطبيقات الخاصة بـ EventEmitters
و Streams
و HTTP
.