Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

بناء خادم ويب أحادي المسار (Building a Single-Threaded Web Server)

سنبدأ بتشغيل (خادم ويب أحادي المسار) single-threaded web server. قبل أن نبدأ، دعنا نلقي نظرة سريعة على (البروتوكولات) protocols المتضمنة في بناء خوادم الويب. تفاصيل هذه البروتوكولات خارج نطاق هذا الكتاب، لكن نظرة عامة موجزة ستعطيك المعلومات التي تحتاجها.

البروتوكولان الرئيسيان المتضمنان في خوادم الويب هما (بروتوكول نقل النص التشعبي) Hypertext Transfer Protocol (HTTP) و (بروتوكول التحكم في الإرسال) Transmission Control Protocol (TCP). كلا البروتوكولين هما بروتوكولات (طلب-استجابة) request-response ، مما يعني أن (العميل) client يبدأ الطلبات و (الخادم) server يستمع إلى الطلبات ويقدم استجابة للعميل. يتم تحديد محتويات تلك الطلبات والاستجابات بواسطة البروتوكولات.

TCP هو البروتوكول من المستوى الأدنى الذي يصف تفاصيل كيفية انتقال المعلومات من خادم إلى آخر ولكنه لا يحدد ماهية تلك المعلومات. يبني HTTP فوق TCP من خلال تحديد محتويات الطلبات والاستجابات. من الممكن تقنياً استخدام HTTP مع بروتوكولات أخرى، ولكن في الغالبية العظمى من الحالات، يرسل HTTP بياناته عبر TCP. سنعمل مع (البايتات الخام) raw bytes لطلبات واستجابات TCP و HTTP.

الاستماع إلى اتصال TCP (Listening to the TCP Connection)

يحتاج خادم الويب الخاص بنا إلى الاستماع إلى (اتصال TCP) TCP connection ، لذا فهذا هو الجزء الأول الذي سنعمل عليه. توفر المكتبة القياسية وحدة std::net التي تتيح لنا القيام بذلك. لننشئ مشروعاً جديداً بالطريقة المعتادة:

$ cargo new hello
     Created binary (application) `hello` project
$ cd hello

الآن أدخل الكود الموجود في القائمة 21-1 في src/main.rs للبدء. سيستمع هذا الكود عند العنوان المحلي 127.0.0.1:7878 لـ (تدفقات TCP) TCP streams القادمة. عندما يتلقى تدفقاً قادماً، سيطبع Connection established!.

use std::net::TcpListener;

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        println!("Connection established!");
    }
}

باستخدام TcpListener ، يمكننا الاستماع لاتصالات TCP عند العنوان 127.0.0.1:7878. في العنوان، الجزء الذي يسبق النقطتين هو (عنوان IP) IP address يمثل جهاز الكمبيوتر الخاص بك (هذا هو نفسه على كل جهاز كمبيوتر ولا يمثل جهاز المؤلفين تحديداً)، و 7878 هو (المنفذ) port. لقد اخترنا هذا المنفذ لسببين: لا يتم قبول HTTP عادةً على هذا المنفذ، لذا فمن غير المرجح أن يتعارض خادمنا مع أي خادم ويب آخر قد يكون قيد التشغيل على جهازك، و 7878 هي كلمة rust المكتوبة على الهاتف.

تعمل دالة bind في هذا السيناريو مثل دالة new حيث ستعيد مثيلاً جديداً من TcpListener. تسمى الدالة bind لأنه في الشبكات، يُعرف الاتصال بمنفذ للاستماع إليه باسم “(الربط بمنفذ) binding to a port”.

تعيد دالة bind نوع Result<T, E> ، مما يشير إلى أنه من الممكن أن يفشل الربط، على سبيل المثال، إذا قمنا بتشغيل مثيلين من برنامجنا وبالتالي كان هناك برنامجان يستمعان إلى نفس المنفذ. لأننا نكتب خادماً أساسياً لأغراض التعلم فقط، فلن نقلق بشأن التعامل مع هذه الأنواع من الأخطاء؛ بدلاً من ذلك، نستخدم unwrap لإيقاف البرنامج في حالة حدوث أخطاء.

تعيد دالة incoming على TcpListener (مكرراً) iterator يعطينا سلسلة من التدفقات (بشكل أكثر تحديداً، تدفقات من النوع TcpStream). يمثل (التدفق) stream الواحد اتصالاً مفتوحاً بين العميل والخادم. (الاتصال) Connection هو الاسم لعملية الطلب والاستجابة الكاملة التي يتصل فيها العميل بالخادم، ويقوم الخادم بإنشاء استجابة، ويغلق الخادم الاتصال. على هذا النحو، سنقرأ من TcpStream لنرى ما أرسله العميل ثم نكتب استجابتنا إلى التدفق لإرسال البيانات مرة أخرى إلى العميل. بشكل عام، ستقوم حلقة for هذه بمعالجة كل اتصال بدوره وإنتاج سلسلة من التدفقات لنتعامل معها.

في الوقت الحالي، تتكون معالجتنا للتدفق من استدعاء unwrap لإنهاء برنامجنا إذا كان التدفق يحتوي على أي أخطاء؛ إذا لم تكن هناك أخطاء، يطبع البرنامج رسالة. سنضيف المزيد من الوظائف لحالة النجاح في القائمة التالية. السبب في أننا قد نتلقى أخطاء من دالة incoming عندما يتصل عميل بالخادم هو أننا لا نتنقل فعلياً عبر الاتصالات. بدلاً من ذلك، نحن نتنقل عبر (محاولات الاتصال) connection attempts. قد لا يكون الاتصال ناجحاً لعدد من الأسباب، وكثير منها خاص بنظام التشغيل. على سبيل المثال، تمتلك العديد من أنظمة التشغيل حداً لعدد الاتصالات المفتوحة المتزامنة التي يمكنها دعمها؛ ستؤدي محاولات الاتصال الجديدة التي تتجاوز هذا العدد إلى حدوث خطأ حتى يتم إغلاق بعض الاتصالات المفتوحة.

دعنا نحاول تشغيل هذا الكود! استدعِ cargo run في (الطرفية) terminal ثم قم بتحميل 127.0.0.1:7878 في متصفح الويب. يجب أن يظهر المتصفح رسالة خطأ مثل “Connection reset” لأن الخادم لا يرسل حالياً أي بيانات. ولكن عندما تنظر إلى الطرفية الخاصة بك، يجب أن ترى عدة رسائل تمت طباعتها عندما اتصل المتصفح بالخادم!

     Running `target/debug/hello`
Connection established!
Connection established!
Connection established!

أحياناً سترى رسائل متعددة مطبوعة لطلب متصفح واحد؛ قد يكون السبب هو أن المتصفح يقدم طلباً للصفحة بالإضافة إلى طلب لموارد أخرى، مثل أيقونة favicon.ico التي تظهر في علامة تبويب المتصفح.

قد يكون السبب أيضاً هو أن المتصفح يحاول الاتصال بالخادم عدة مرات لأن الخادم لا يستجيب بأي بيانات. عندما يخرج stream عن النطاق ويتم (إسقاطه) dropped في نهاية الحلقة، يتم إغلاق الاتصال كجزء من تنفيذ drop. تتعامل المتصفحات أحياناً مع الاتصالات المغلقة عن طريق إعادة المحاولة، لأن المشكلة قد تكون مؤقتة.

تفتح المتصفحات أيضاً أحياناً اتصالات متعددة بالخادم دون إرسال أي طلبات بحيث إذا أرسلت طلبات لاحقاً، يمكن أن تتم تلك الطلبات بسرعة أكبر. عندما يحدث هذا، سيرى خادمنا كل اتصال، بغض النظر عما إذا كانت هناك أي طلبات عبر ذلك الاتصال. تقوم العديد من إصدارات المتصفحات المستندة إلى Chrome بذلك، على سبيل المثال؛ يمكنك تعطيل هذا (التحسين) optimization باستخدام وضع التصفح الخاص أو استخدام متصفح مختلف.

العامل المهم هو أننا حصلنا بنجاح على (مقبض) handle لاتصال TCP!

تذكر إيقاف البرنامج بالضغط على ctrl-C عندما تنتهي من تشغيل إصدار معين من الكود. ثم أعد تشغيل البرنامج باستدعاء أمر cargo run بعد إجراء كل مجموعة من تغييرات الكود للتأكد من أنك تقوم بتشغيل أحدث كود.

قراءة الطلب (Reading the Request)

لننفذ الوظيفة لقراءة الطلب من المتصفح! لفصل (الاهتمامات) concerns بين الحصول على اتصال أولاً ثم اتخاذ إجراء ما مع الاتصال، سنبدأ دالة جديدة لمعالجة الاتصالات. في دالة handle_connection الجديدة هذه، سنقرأ البيانات من تدفق TCP ونطبعها حتى نتمكن من رؤية البيانات التي يتم إرسالها من المتصفح. قم بتغيير الكود ليبدو مثل القائمة 21-2.

use std::{
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&stream);
    let http_request: Vec<_> = buf_reader
        .lines()
        .map(|result| result.unwrap())
        .take_while(|line| !line.is_empty())
        .collect();

    println!("Request: {http_request:#?}");
}

نقوم بجلب std::io::BufReader و std::io::prelude إلى النطاق للوصول إلى traits والأنواع التي تتيح لنا القراءة من التدفق والكتابة إليه. في حلقة for في دالة main ، بدلاً من طباعة رسالة تقول إننا أجرينا اتصالاً، نستدعي الآن دالة handle_connection الجديدة ونمرر stream إليها.

في دالة handle_connection ، ننشئ مثيلاً جديداً من BufReader يغلف (مرجعاً) reference للتدفق. يضيف BufReader (تخزيناً مؤقتاً) buffering عن طريق إدارة الاستدعاءات لطرق std::io::Read trait من أجلنا.

ننشئ متغيراً باسم http_request لجمع أسطر الطلب الذي يرسله المتصفح إلى خادمنا. نشير إلى أننا نريد جمع هذه الأسطر في (متجه) vector عن طريق إضافة توضيح النوع Vec<_>.

ينفذ BufReader السمة std::io::BufRead ، التي توفر دالة lines. تعيد دالة lines مكرراً من Result<String, std::io::Error> عن طريق تقسيم تدفق البيانات كلما رأت بايت (سطر جديد) newline. للحصول على كل String ، نقوم بـ map و unwrap لكل Result. قد يكون Result خطأ إذا لم تكن البيانات (ترميز UTF-8) UTF-8 صالحاً أو إذا كانت هناك مشكلة في القراءة من التدفق. مرة أخرى، يجب أن يتعامل برنامج الإنتاج مع هذه الأخطاء بشكل أكثر لباقة، لكننا نختار إيقاف البرنامج في حالة الخطأ للتبسيط.

يشير المتصفح إلى نهاية طلب HTTP عن طريق إرسال حرفي سطر جديد متتاليين، لذا للحصول على طلب واحد من التدفق، نأخذ الأسطر حتى نحصل على سطر عبارة عن سلسلة فارغة. بمجرد جمع الأسطر في المتجه، نقوم بطباعتها باستخدام (تنسيق التصحيح الجميل) pretty debug formatting حتى نتمكن من إلقاء نظرة على التعليمات التي يرسلها متصفح الويب إلى خادمنا.

دعنا نجرب هذا الكود! ابدأ البرنامج وقدم طلباً في متصفح الويب مرة أخرى. لاحظ أننا سنظل نحصل على صفحة خطأ في المتصفح، لكن مخرج برنامجنا في الطرفية سيبدو الآن مشابهاً لهذا:

$ cargo run
   Compiling hello v0.1.0 (file:///projects/hello)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
     Running `target/debug/hello`
Request: [
    "GET / HTTP/1.1",
    "Host: 127.0.0.1:7878",
    "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:99.0) Gecko/20100101 Firefox/99.0",
    "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
    "Accept-Language: en-US,en;q=0.5",
    "Accept-Encoding: gzip, deflate, br",
    "DNT: 1",
    "Connection: keep-alive",
    "Upgrade-Insecure-Requests: 1",
    "Sec-Fetch-Dest: document",
    "Sec-Fetch-Mode: navigate",
    "Sec-Fetch-Site: none",
    "Sec-Fetch-User: ?1",
    "Cache-Control: max-age=0",
]

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

دعنا نحلل بيانات الطلب هذه لفهم ما يطلبه المتصفح من برنامجنا.

نظرة فاحصة على طلب HTTP (Looking More Closely at an HTTP Request)

HTTP هو بروتوكول يعتمد على النص، ويأخذ الطلب هذا التنسيق:

Method Request-URI HTTP-Version CRLF
headers CRLF
message-body

السطر الأول هو (سطر الطلب) request line الذي يحتوي على معلومات حول ما يطلبه العميل. الجزء الأول من سطر الطلب هو (الطريقة) method المستخدمة، مثل GET أو POST ، والتي تصف كيفية تقديم العميل لهذا الطلب. استخدم عميلنا طلب GET ، مما يعني أنه يطلب معلومات.

الجزء التالي من سطر الطلب هو / ، والذي يشير إلى (معرف المورد الموحد) uniform resource identifier (URI) الذي يطلبه العميل: URI هو تقريباً، ولكن ليس تماماً، نفس (محدد موقع الموارد الموحد) uniform resource locator (URL). الفرق بين URIs و URLs ليس مهماً لأغراضنا في هذا الفصل، لكن مواصفات HTTP تستخدم المصطلح URI ، لذا يمكننا فقط استبدال URL بـ URI ذهنياً هنا.

الجزء الأخير هو إصدار HTTP الذي يستخدمه العميل، ثم ينتهي سطر الطلب بتسلسل CRLF. (يرمز CRLF إلى carriage return و line feed (رجوع العربة وتغذية السطر)، وهي مصطلحات من أيام الآلة الكاتبة!) يمكن أيضاً كتابة تسلسل CRLF كـ \r\n ، حيث \r هو رجوع العربة و \n هو تغذية السطر. يفصل (تسلسل CRLF) CRLF sequence سطر الطلب عن بقية بيانات الطلب. لاحظ أنه عند طباعة CRLF ، نرى بداية سطر جديد بدلاً من \r\n.

بالنظر إلى بيانات سطر الطلب التي تلقيناها من تشغيل برنامجنا حتى الآن، نرى أن GET هي الطريقة، و / هو request URI ، و HTTP/1.1 هو الإصدار.

بعد سطر الطلب، الأسطر المتبقية بدءاً من Host: فصاعداً هي (رؤوس) headers. طلبات GET ليس لها (جسم) body.

حاول تقديم طلب من متصفح مختلف أو طلب عنوان مختلف، مثل 127.0.0.1:7878/test ، لترى كيف تتغير بيانات الطلب.

الآن بعد أن عرفنا ما يطلبه المتصفح، دعنا نرسل بعض البيانات!

كتابة استجابة (Writing a Response)

سنقوم بتنفيذ إرسال البيانات استجابة لطلب العميل. تأخذ الاستجابات التنسيق التالي:

HTTP-Version Status-Code Reason-Phrase CRLF
headers CRLF
message-body

السطر الأول هو (سطر الحالة) status line الذي يحتوي على إصدار HTTP المستخدم في الاستجابة، و (رمز حالة) status code رقمي يلخص نتيجة الطلب، و (عبارة سبب) reason phrase تقدم وصفاً نصياً لرمز الحالة. بعد تسلسل CRLF توجد أي رؤوس، وتسلسل CRLF آخر، وجسم الاستجابة.

إليك مثال على استجابة تستخدم إصدار HTTP 1.1 ولديها رمز حالة 200، وعبارة سبب OK ، ولا توجد رؤوس، ولا يوجد جسم:

HTTP/1.1 200 OK\r\n\r\n

رمز الحالة 200 هو استجابة النجاح القياسية. النص هو استجابة HTTP ناجحة صغيرة جداً. لنكتب هذا في التدفق كاستجابتنا لطلب ناجح! من دالة handle_connection ، قم بإزالة println! التي كانت تطبع بيانات الطلب واستبدلها بالكود الموجود في القائمة 21-3.

use std::{
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&stream);
    let http_request: Vec<_> = buf_reader
        .lines()
        .map(|result| result.unwrap())
        .take_while(|line| !line.is_empty())
        .collect();

    let response = "HTTP/1.1 200 OK\r\n\r\n";

    stream.write_all(response.as_bytes()).unwrap();
}

يحدد السطر الجديد الأول متغير response الذي يحمل بيانات رسالة النجاح. بعد ذلك، نستدعي as_bytes على استجابتنا لتحويل بيانات السلسلة النصية إلى بايتات. تأخذ دالة write_all على stream نوع &[u8] وترسل تلك البايتات مباشرة عبر الاتصال. لأن عملية write_all قد تفشل، نستخدم unwrap على أي نتيجة خطأ كما فعلنا سابقاً. مرة أخرى، في تطبيق حقيقي، ستضيف معالجة الأخطاء هنا.

مع هذه التغييرات، دعنا نشغل الكود الخاص بنا ونقدم طلباً. لم نعد نطبع أي بيانات في الطرفية، لذا لن نرى أي مخرج بخلاف المخرج من Cargo. عندما تقوم بتحميل 127.0.0.1:7878 في متصفح الويب، يجب أن تحصل على صفحة فارغة بدلاً من خطأ. لقد قمت للتو ببرمجة استلام طلب HTTP وإرسال استجابة يدوياً!

إرجاع HTML حقيقي (Returning Real HTML)

لننفذ الوظيفة لإرجاع أكثر من صفحة فارغة. أنشئ الملف الجديد hello.html في جذر دليل مشروعك، وليس في دليل src. يمكنك إدخال أي HTML تريده؛ تعرض القائمة 21-4 أحد الاحتمالات.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Hello!</title>
  </head>
  <body>
    <h1>Hello!</h1>
    <p>Hi from Rust</p>
  </body>
</html>

هذا مستند HTML5 بسيط مع عنوان وبعض النص. لإرجاع هذا من الخادم عند استلام طلب، سنقوم بتعديل handle_connection كما هو موضح في القائمة 21-5 لقراءة ملف HTML ، وإضافته إلى الاستجابة كجسم، وإرساله.

use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};
// --snip--

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&stream);
    let http_request: Vec<_> = buf_reader
        .lines()
        .map(|result| result.unwrap())
        .take_while(|line| !line.is_empty())
        .collect();

    let status_line = "HTTP/1.1 200 OK";
    let contents = fs::read_to_string("hello.html").unwrap();
    let length = contents.len();

    let response =
        format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");

    stream.write_all(response.as_bytes()).unwrap();
}

لقد أضفنا fs إلى عبارة use لجلب وحدة نظام الملفات في المكتبة القياسية إلى النطاق. يجب أن يبدو كود قراءة محتويات ملف إلى سلسلة نصية مألوفاً؛ استخدمناه عندما قرأنا محتويات ملف لمشروع الإدخال/الإخراج الخاص بنا في القائمة 12-4.

بعد ذلك، نستخدم format! لإضافة محتويات الملف كجسم لاستجابة النجاح. لضمان استجابة HTTP صالحة، نضيف رأس Content-Length ، والذي يتم ضبطه على حجم جسم الاستجابة الخاص بنا - في هذه الحالة، حجم hello.html.

شغل هذا الكود باستخدام cargo run وقم بتحميل 127.0.0.1:7878 في متصفحك؛ يجب أن ترى HTML الخاص بك معروضاً!

حالياً، نحن نتجاهل بيانات الطلب في http_request ونرسل فقط محتويات ملف HTML دون قيد أو شرط. هذا يعني أنه إذا حاولت طلب 127.0.0.1:7878/something-else في متصفحك، فستظل تحصل على نفس استجابة HTML هذه. في الوقت الحالي، خادمنا محدود للغاية ولا يفعل ما تفعله معظم خوادم الويب. نريد تخصيص استجاباتنا اعتماداً على الطلب وإرسال ملف HTML فقط لطلب جيد التنسيق إلى /.

التحقق من الطلب والاستجابة بشكل انتقائي (Validating the Request and Selectively Responding)

في الوقت الحالي، سيعيد خادم الويب الخاص بنا HTML الموجود في الملف بغض النظر عما طلبه العميل. لنضف وظيفة للتحقق من أن المتصفح يطلب / قبل إرجاع ملف HTML وإرجاع خطأ إذا طلب المتصفح أي شيء آخر. لهذا نحتاج إلى تعديل handle_connection ، كما هو موضح في القائمة 21-6. يتحقق هذا الكود الجديد من محتوى الطلب المستلم مقابل ما نعرف أن طلباً لـ / يبدو عليه ويضيف كتل if و else لمعالجة الطلبات بشكل مختلف.

use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}
// --snip--

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();

    if request_line == "GET / HTTP/1.1" {
        let status_line = "HTTP/1.1 200 OK";
        let contents = fs::read_to_string("hello.html").unwrap();
        let length = contents.len();

        let response = format!(
            "{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
        );

        stream.write_all(response.as_bytes()).unwrap();
    } else {
        // some other request
    }
}

سننظر فقط في السطر الأول من طلب HTTP ، لذا بدلاً من قراءة الطلب بالكامل في متجه، نستدعي next للحصول على العنصر الأول من المكرر. يتولى unwrap الأول التعامل مع Option ويوقف البرنامج إذا لم يكن المكرر يحتوي على عناصر. يتعامل unwrap الثاني مع Result وله نفس تأثير unwrap الذي كان في map المضاف في القائمة 21-2.

بعد ذلك، نتحقق من request_line لنرى ما إذا كان يساوي سطر طلب لطلب GET إلى مسار /. إذا كان الأمر كذلك، فإن كتلة if تعيد محتويات ملف HTML الخاص بنا.

إذا كان request_line لا يساوي طلب GET إلى مسار / ، فهذا يعني أننا تلقينا طلباً آخر. سنضيف كوداً إلى كتلة else بعد قليل للاستجابة لجميع الطلبات الأخرى.

شغل هذا الكود الآن واطلب 127.0.0.1:7878 ؛ يجب أن تحصل على HTML في hello.html. إذا قمت بإجراء أي طلب آخر، مثل 127.0.0.1:7878/something-else ، فستحصل على خطأ في الاتصال مثل تلك التي رأيتها عند تشغيل الكود في القائمة 21-1 والقائمة 21-2.

الآن لنضف الكود الموجود في القائمة 21-7 إلى كتلة else لإرجاع استجابة برمز الحالة 404، والذي يشير إلى أن المحتوى المطلوب لم يتم العثور عليه. سنعيد أيضاً بعض HTML لصفحة يتم عرضها في المتصفح تشير إلى الاستجابة للمستخدم النهائي.

use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();

    if request_line == "GET / HTTP/1.1" {
        let status_line = "HTTP/1.1 200 OK";
        let contents = fs::read_to_string("hello.html").unwrap();
        let length = contents.len();

        let response = format!(
            "{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
        );

        stream.write_all(response.as_bytes()).unwrap();
    // --snip--
    } else {
        let status_line = "HTTP/1.1 404 NOT FOUND";
        let contents = fs::read_to_string("404.html").unwrap();
        let length = contents.len();

        let response = format!(
            "{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
        );

        stream.write_all(response.as_bytes()).unwrap();
    }
}

هنا، استجابتنا لها سطر حالة برمز الحالة 404 وعبارة السبب NOT FOUND. سيكون جسم الاستجابة هو HTML الموجود في الملف 404.html. ستحتاج إلى إنشاء ملف 404.html بجوار hello.html لصفحة الخطأ؛ مرة أخرى، لا تتردد في استخدام أي HTML تريده، أو استخدم عينة HTML في القائمة 21-8.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Hello!</title>
  </head>
  <body>
    <h1>Oops!</h1>
    <p>Sorry, I don't know what you're asking for.</p>
  </body>
</html>

مع هذه التغييرات، شغل خادمك مرة أخرى. يجب أن يؤدي طلب 127.0.0.1:7878 إلى إرجاع محتويات hello.html ، وأي طلب آخر، مثل 127.0.0.1:7878/foo ، يجب أن يعيد خطأ HTML من 404.html.

إعادة الهيكلة (Refactoring)

في الوقت الحالي، تحتوي كتل if و else على الكثير من التكرار: كلاهما يقرأ الملفات ويكتب محتويات الملفات في التدفق. الاختلافات الوحيدة هي سطر الحالة واسم الملف. لنحول الكود ليكون أكثر إيجازاً عن طريق استخراج تلك الاختلافات في أسطر if و else منفصلة ستقوم بتعيين قيم سطر الحالة واسم الملف إلى متغيرات؛ يمكننا بعد ذلك استخدام تلك المتغيرات دون قيد أو شرط في الكود لقراءة الملف وكتابة الاستجابة. تعرض القائمة 21-9 الكود الناتج بعد استبدال كتل if و else الكبيرة.

use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        handle_connection(stream);
    }
}
// --snip--

fn handle_connection(mut stream: TcpStream) {
    // --snip--
    let buf_reader = BufReader::new(&stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();

    let (status_line, filename) = if request_line == "GET / HTTP/1.1" {
        ("HTTP/1.1 200 OK", "hello.html")
    } else {
        ("HTTP/1.1 404 NOT FOUND", "404.html")
    };

    let contents = fs::read_to_string(filename).unwrap();
    let length = contents.len();

    let response =
        format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");

    stream.write_all(response.as_bytes()).unwrap();
}

الآن تعيد كتل if و else فقط القيم المناسبة لسطر الحالة واسم الملف في (مجموعة) tuple ؛ ثم نستخدم (تفكيك البنية) destructuring لتعيين هاتين القيمتين لـ status_line و filename باستخدام نمط في عبارة let ، كما تمت مناقشته في الفصل 19.

أصبح الكود الذي كان مكرراً سابقاً الآن خارج كتل if و else ويستخدم متغيرات status_line و filename. هذا يسهل رؤية الفرق بين الحالتين، ويعني أن لدينا مكاناً واحداً فقط لتحديث الكود إذا أردنا تغيير كيفية عمل قراءة الملف وكتابة الاستجابة. سيكون سلوك الكود في القائمة 21-9 هو نفسه الموجود في القائمة 21-7.

رائع! لدينا الآن خادم ويب بسيط في حوالي 40 سطراً من كود Rust يستجيب لطلب واحد بصفحة من المحتوى ويستجيب لجميع الطلبات الأخرى باستجابة 404.

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