Skip to content

تشريح معاملة HTTP

الغرض من هذا الدليل هو نقل فهم راسخ لعملية معالجة HTTP في Node.js. سنفترض أنك تعرف، بمعنى عام، كيف تعمل طلبات HTTP، بغض النظر عن اللغة أو بيئة البرمجة. سنفترض أيضًا بعض الإلمام بـ Node.js EventEmitters و Streams. إذا لم تكن على دراية تامة بها، فمن الجدير بالاطلاع السريع على وثائق API لكل منهما.

إنشاء الخادم

أي تطبيق خادم ويب node سيتعين عليه في مرحلة ما إنشاء كائن خادم ويب. يتم ذلك باستخدام createServer.

javascript
const http = require('node:http');
const server = http.createServer((request, response) => {
    // السحر يحدث هنا!
});

يتم استدعاء الدالة التي يتم تمريرها إلى createServer مرة واحدة لكل طلب HTTP يتم إجراؤه ضد هذا الخادم، لذلك يطلق عليها معالج الطلبات. في الواقع، كائن Server الذي يتم إرجاعه بواسطة createServer هو EventEmitter، وما لدينا هنا هو مجرد اختصار لإنشاء كائن خادم ثم إضافة المستمع لاحقًا.

javascript
const server = http.createServer();
server.on('request', (request, response) => {
    // يحدث نفس النوع من السحر هنا!
});

عندما يضرب طلب HTTP الخادم، تستدعي node دالة معالج الطلبات مع بعض الكائنات المفيدة للتعامل مع المعاملة والطلب والاستجابة. سنتناول هذه الأمور قريبًا. من أجل تقديم الطلبات فعليًا، يجب استدعاء طريقة listen على كائن الخادم. في معظم الحالات، كل ما ستحتاج إلى تمريره إلى listen هو رقم المنفذ الذي تريد أن يستمع إليه الخادم. هناك بعض الخيارات الأخرى أيضًا، لذا راجع مرجع API.

الطريقة وعنوان URL والرؤوس

عند التعامل مع طلب، فإن أول شيء قد ترغب في القيام به هو إلقاء نظرة على الطريقة وعنوان URL، بحيث يمكن اتخاذ الإجراءات المناسبة. تجعل Node.js هذا الأمر غير مؤلم نسبيًا عن طريق وضع خصائص مفيدة على كائن الطلب.

javascript
const { method, url } = request;

كائن الطلب هو نسخة من IncomingMessage. ستكون الطريقة هنا دائمًا طريقة/فعل HTTP عاديًا. عنوان URL هو عنوان URL الكامل بدون الخادم أو البروتوكول أو المنفذ. بالنسبة لعنوان URL النموذجي، هذا يعني كل شيء بعد الشرطة المائلة الأمامية الثالثة بما في ذلك.

الرؤوس ليست بعيدة أيضًا. إنها في كائنها الخاص على الطلب يسمى headers.

javascript
const { headers } = request;
const userAgent = headers['user-agent'];

من المهم ملاحظة هنا أن جميع الرؤوس ممثلة بأحرف صغيرة فقط، بغض النظر عن كيفية إرسالها العميل فعليًا. هذا يبسط مهمة تحليل الرؤوس لأي غرض كان.

إذا تم تكرار بعض الرؤوس، فسيتم استبدال قيمها أو ضمها معًا كسلاسل مفصولة بفواصل، اعتمادًا على الرأس. في بعض الحالات، قد يكون هذا إشكاليًا، لذلك يتوفر أيضًا rawHeaders.

جسم الطلب

عند استقبال طلب POST أو PUT، قد يكون جسم الطلب مهمًا لتطبيقك. الوصول إلى بيانات الجسم أكثر تعقيدًا قليلاً من الوصول إلى رؤوس الطلب. يقوم كائن الطلب الذي يتم تمريره إلى المعالج بتطبيق واجهة ReadableStream. يمكن الاستماع إلى هذا التدفق أو توجيهه إلى مكان آخر تمامًا مثل أي تدفق آخر. يمكننا الحصول على البيانات مباشرة من التدفق من خلال الاستماع إلى أحداث 'data' و 'end' الخاصة بالتدفق.

الكتلة المنبعثة في كل حدث 'data' هي Buffer. إذا كنت تعلم أنها ستكون بيانات سلسلة، فإن أفضل ما يمكنك فعله هو جمع البيانات في مصفوفة، ثم في 'end'، قم بدمجها وتحويلها إلى سلسلة.

javascript
let body = [];
request.on('data', chunk => {
    body.push(chunk);
});
request.on('end', () => {
    body = Buffer.concat(body).toString();
    // في هذه المرحلة، يحتوي 'body' على جسم الطلب بأكمله المخزن فيه كسلسلة
});

NOTE

قد يبدو هذا مملًا بعض الشيء، وفي كثير من الحالات يكون كذلك. لحسن الحظ، توجد وحدات مثل concat-stream و body على npm يمكن أن تساعد في إخفاء بعض هذا المنطق. من المهم أن يكون لديك فهم جيد لما يحدث قبل سلوك هذا الطريق، وهذا هو سبب وجودك هنا!

شيء سريع عن الأخطاء

نظرًا لأن كائن الطلب هو ReadableStream، فهو أيضًا EventEmitter ويتصرف مثله عندما يحدث خطأ.

يظهر خطأ في تدفق الطلب عن طريق إصدار حدث 'error' على التدفق. إذا لم يكن لديك مستمع لهذا الحدث، فسيتم طرح الخطأ، مما قد يؤدي إلى تعطل برنامج Node.js الخاص بك. لذلك يجب عليك إضافة مستمع 'error' على تدفقات الطلب الخاصة بك، حتى إذا قمت بتسجيله ومتابعة طريقك. (على الرغم من أنه ربما يكون من الأفضل إرسال نوع من استجابة خطأ HTTP. المزيد عن ذلك لاحقًا.)

javascript
request.on('error', err => {
    // يطبع هذا رسالة الخطأ وتتبع المكدس إلى stderr.
    console.error(err.stack);
});

هناك طرق أخرى للتعامل مع هذه الأخطاء مثل التجريدات والأدوات الأخرى، ولكن كن دائمًا على دراية بأن الأخطاء يمكن أن تحدث وتحدث بالفعل، وعليك التعامل معها.

ما توصلنا إليه حتى الآن

حتى هذه اللحظة، قمنا بتغطية إنشاء خادم، واستخراج الطريقة وعنوان URL والرؤوس والجسم من الطلبات. عندما نجمع كل ذلك معًا، قد يبدو الأمر كما يلي:

javascript
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); // يقوم بتنشيط هذا الخادم، والاستماع على المنفذ 8080.

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

حتى الآن لم نتطرق إلى كائن الاستجابة على الإطلاق، وهو مثيل لـ ServerResponse، وهو عبارة عن WritableStream. يحتوي على العديد من الطرق المفيدة لإرسال البيانات مرة أخرى إلى العميل. سنغطي ذلك بعد ذلك.

رمز حالة HTTP

إذا لم تكلف نفسك عناء تعيينه، فسيكون رمز حالة HTTP في الاستجابة دائمًا 200. بالطبع، لا يضمن كل رد HTTP ذلك، وفي مرحلة ما سترغب بالتأكيد في إرسال رمز حالة مختلف. للقيام بذلك، يمكنك تعيين الخاصية statusCode.

javascript
response.statusCode = 404; // أخبر العميل أنه لم يتم العثور على المورد.

هناك بعض الاختصارات الأخرى لذلك، كما سنرى قريبًا.

تعيين رؤوس الاستجابة

يتم تعيين الرؤوس من خلال طريقة ملائمة تسمى setHeader.

javascript
response.setHeader('Content-Type', 'application/json');
response.setHeader('X-Powered-By', 'bacon');

عند تعيين الرؤوس في استجابة، يكون حالة الأحرف غير حساسة لأسماءها. إذا قمت بتعيين رأس بشكل متكرر، فإن القيمة الأخيرة التي قمت بتعيينها هي القيمة التي يتم إرسالها.

إرسال بيانات الرأس بشكل صريح

تفترض طرق تعيين الرؤوس ورمز الحالة التي ناقشناها بالفعل أنك تستخدم "رؤوسًا ضمنية". هذا يعني أنك تعتمد على Node لإرسال الرؤوس نيابة عنك في الوقت الصحيح قبل أن تبدأ في إرسال بيانات النص الأساسي.

إذا كنت تريد، يمكنك كتابة الرؤوس بشكل صريح إلى دفق الاستجابة. للقيام بذلك، هناك طريقة تسمى writeHead، والتي تكتب رمز الحالة والرؤوس إلى الدفق.

إرسال بيانات الرأس بشكل صريح

javascript
response.writeHead(200, {
    'Content-Type': 'application/json',
    'X-Powered-By': 'bacon',
});

بمجرد تعيين الرؤوس (سواء ضمنيًا أو صريحًا)، فأنت على استعداد لبدء إرسال بيانات الاستجابة.

إرسال نص الاستجابة

نظرًا لأن كائن الاستجابة هو WritableStream، فإن كتابة نص استجابة إلى العميل هي مجرد مسألة استخدام طرق الدفق المعتادة.

javascript
response.write('<html>');
response.write('<body>');
response.write('<h1>Hello, World!</h1>');
response.write('</body>');
response.write('</html>');
response.end();

يمكن لوظيفة end الموجودة على التدفقات أيضًا أن تأخذ بعض البيانات الاختيارية لإرسالها كآخر جزء من البيانات الموجودة على الدفق، لذلك يمكننا تبسيط المثال أعلاه على النحو التالي.

javascript
response.end('<html><body><h1>hello,world!</h1></body></html>');

NOTE

من المهم تعيين الحالة والرؤوس قبل البدء في كتابة أجزاء من البيانات إلى النص الأساسي. هذا منطقي، لأن الرؤوس تأتي قبل النص الأساسي في استجابات HTTP.

شيء سريع آخر حول الأخطاء

يمكن لدفق الاستجابة أيضًا إصدار أحداث "خطأ"، وفي مرحلة ما سيتعين عليك التعامل مع ذلك أيضًا. تظل جميع النصائح المتعلقة بأخطاء دفق الطلب سارية هنا.

ضع كل شيء معًا

الآن بعد أن تعلمنا كيفية إنشاء استجابات HTTP، دعنا نضع كل شيء معًا. بالاعتماد على المثال السابق، سنقوم بإنشاء خادم يرسل مرة أخرى جميع البيانات التي أرسلها إلينا المستخدم. سنقوم بتنسيق هذه البيانات بتنسيق JSON باستخدام JSON.stringify.

javascript
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();
        // BEGINNING OF NEW STUFF
        response.on('error', err => {
          console.error(err);
        });
        response.statusCode = 200;
        response.setHeader('Content-Type', 'application/json');
        // Note: the 2 lines above could be replaced with this next one:
        // response.writeHead(200, {'Content-Type': 'application/json'})
        const responseBody = { headers, method, url, body };
        response.write(JSON.stringify(responseBody));
        response.end();
        // Note: the 2 lines above could be replaced with this next one:
        // response.end(JSON.stringify(responseBody))
        // END OF NEW STUFF
      });
  })
  .listen(8080);

مثال خادم الصدى

دعنا نبسط المثال السابق لإنشاء خادم صدى بسيط، والذي يرسل ببساطة أي بيانات يتم تلقيها في الطلب مرة أخرى في الاستجابة. كل ما نحتاج إلى القيام به هو الحصول على البيانات من دفق الطلب وكتابة هذه البيانات إلى دفق الاستجابة، على غرار ما فعلناه سابقًا.

javascript
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.

javascript
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 لتوجيه البيانات من أحدهما إلى الآخر. هذا بالضبط ما نريده لخادم صدى!

javascript
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.

javascript
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.

من هذه الأساسيات، يمكن بناء خوادم Node.js HTTP للعديد من حالات الاستخدام النموذجية. هناك الكثير من الأشياء الأخرى التي توفرها واجهات برمجة التطبيقات هذه، لذا تأكد من قراءة وثائق واجهة برمجة التطبيقات لـ EventEmitters وStreams و HTTP.