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

একটি সিঙ্গেল-থ্রেডেড ওয়েব সার্ভার তৈরি

আমরা একটি সিঙ্গেল-থ্রেডেড ওয়েব সার্ভার চালু করার মাধ্যমে কাজ শুরু করব। তবে তার আগে, ওয়েব সার্ভার তৈরির সাথে জড়িত প্রোটোকলগুলো সম্পর্কে সংক্ষেপে জেনে নেওয়া যাক। এই প্রোটোকলগুলোর বিস্তারিত বিবরণ এই বইয়ের আওতার বাইরে, কিন্তু একটি সংক্ষিপ্ত ধারণা আপনাকে প্রয়োজনীয় তথ্য দেবে।

ওয়েব সার্ভারের সাথে প্রধানত দুটি প্রোটোকল জড়িত: Hypertext Transfer Protocol (HTTP) এবং Transmission Control Protocol (TCP)। উভয় প্রোটোকলই request-response প্রোটোকল, যার অর্থ হলো একটি client রিকোয়েস্ট পাঠায় এবং একটি server সেই রিকোয়েস্ট শোনে এবং client-কে একটি রেসপন্স প্রদান করে। এই রিকোয়েস্ট এবং রেসপন্সগুলোর বিষয়বস্তু প্রোটোকল দ্বারা সংজ্ঞায়িত করা হয়।

TCP হলো একটি নিম্ন-স্তরের প্রোটোকল যা বর্ণনা করে কীভাবে তথ্য এক সার্ভার থেকে অন্য সার্ভারে যায়, কিন্তু সেই তথ্যটি কী, তা নির্দিষ্ট করে না। HTTP, TCP-এর উপরে তৈরি করা হয়েছে এবং এটি রিকোয়েস্ট ও রেসপন্সের বিষয়বস্তু নির্ধারণ করে। প্রযুক্তিগতভাবে HTTP অন্য প্রোটোকলের সাথেও ব্যবহার করা সম্ভব, তবে বেশিরভাগ ক্ষেত্রেই HTTP তার ডেটা TCP-এর মাধ্যমে পাঠায়। আমরা TCP এবং HTTP রিকোয়েস্ট ও রেসপন্সের raw বাইট নিয়ে কাজ করব।

TCP কানেকশনে লিসেন করা

আমাদের ওয়েব সার্ভারকে একটি TCP connection শোনার প্রয়োজন, তাই আমরা প্রথমে এই অংশটি নিয়ে কাজ করব। স্ট্যান্ডার্ড লাইব্রেরির std::net মডিউলটি আমাদের এই কাজটি করতে সাহায্য করে। চলুন, স্বাভাবিক পদ্ধতিতে একটি নতুন প্রজেক্ট তৈরি করি:

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

এখন src/main.rs ফাইলে লিস্টিং ২১-১ এর কোডটি লিখুন। এই কোডটি স্থানীয় 127.0.0.1:7878 অ্যাড্রেসে আসা TCP stream-এর জন্য লিসেন করবে। যখন এটি একটি ইনকামিং স্ট্রিম পাবে, তখন 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 ব্যবহার করে আমরা 127.0.0.1:7878 অ্যাড্রেসে TCP connection-এর জন্য লিসেন করতে পারি। এই অ্যাড্রেসে, কোলনের আগের অংশটি হলো আপনার কম্পিউটারকে প্রতিনিধিত্বকারী একটি IP address (এটি প্রতিটি কম্পিউটারে একই এবং নির্দিষ্টভাবে লেখকের কম্পিউটারকে বোঝায় না), এবং 7878 হলো পোর্ট। আমরা এই পোর্টটি দুটি কারণে বেছে নিয়েছি: সাধারণত এই পোর্টে HTTP গ্রহণ করা হয় না, তাই আমাদের সার্ভারটি আপনার মেশিনে চলমান অন্য কোনো ওয়েব সার্ভারের সাথে冲突 করার সম্ভাবনা কম, এবং 7878 একটি টেলিফোনে rust টাইপ করলে যা হয়, তাই।

এক্ষেত্রে bind ফাংশনটি new ফাংশনের মতোই কাজ করে, কারণ এটি একটি নতুন TcpListener ইনস্ট্যান্স রিটার্ন করে। ফাংশনটিকে bind বলা হয় কারণ নেটওয়ার্কিং-এর পরিভাষায়, শোনার জন্য একটি পোর্টের সাথে সংযোগ করাকে "বাইন্ডিং টু এ পোর্ট" বলা হয়।

bind ফাংশনটি একটি Result<T, E> রিটার্ন করে, যা নির্দেশ করে যে বাইন্ডিং ব্যর্থ হতে পারে। উদাহরণস্বরূপ, যদি আমরা আমাদের প্রোগ্রামের দুটি ইনস্ট্যান্স চালাই এবং দুটি প্রোগ্রাম একই পোর্টে লিসেন করে। যেহেতু আমরা শুধুমাত্র শেখার উদ্দেশ্যে একটি বেসিক সার্ভার লিখছি, তাই আমরা এই ধরনের এরর হ্যান্ডলিং নিয়ে চিন্তা করব না; এর পরিবর্তে, যদি কোনো এরর ঘটে, তাহলে প্রোগ্রাম বন্ধ করতে আমরা unwrap ব্যবহার করব।

TcpListener-এর incoming মেথড একটি iterator রিটার্ন করে যা আমাদের একাধিক stream দেয় (আরও নির্দিষ্টভাবে বললে, TcpStream টাইপের স্ট্রিম)। একটি সিঙ্গেল stream ক্লায়েন্ট এবং সার্ভারের মধ্যে একটি খোলা সংযোগের প্রতিনিধিত্ব করে। একটি connection হলো সম্পূর্ণ রিকোয়েস্ট এবং রেসপন্স প্রক্রিয়ার নাম, যেখানে একটি ক্লায়েন্ট সার্ভারের সাথে সংযোগ করে, সার্ভার একটি রেসপন্স তৈরি করে এবং সংযোগটি বন্ধ করে দেয়। সুতরাং, ক্লায়েন্ট কী পাঠিয়েছে তা দেখার জন্য আমরা TcpStream থেকে ডেটা রিড করব এবং ক্লায়েন্টকে ডেটা ফেরত পাঠানোর জন্য আমাদের রেসপন্সটি স্ট্রিমে লিখব। মোটের উপর, এই for লুপটি প্রতিটি কানেকশন একে একে প্রসেস করবে এবং আমাদের হ্যান্ডেল করার জন্য একাধিক স্ট্রিম তৈরি করবে।

আপাতত, স্ট্রিম হ্যান্ডেল করার জন্য আমরা unwrap কল করছি, যাতে স্ট্রিমের কোনো এরর থাকলে আমাদের প্রোগ্রাম বন্ধ হয়ে যায়; যদি কোনো এরর না থাকে, প্রোগ্রাম একটি বার্তা প্রিন্ট করে। পরবর্তী লিস্টিংয়ে আমরা সফলতার ক্ষেত্রে আরও কার্যকারিতা যোগ করব। incoming মেথড থেকে এরর পাওয়ার কারণ হলো, আমরা আসলে কানেকশনগুলোর উপর ইটারেট করছি না, বরং কানেকশন প্রচেষ্টার উপর ইটারেট করছি। কানেকশন বিভিন্ন কারণে সফল নাও হতে পারে, যার মধ্যে অনেকগুলোই অপারেটিং সিস্টেম-নির্দিষ্ট। উদাহরণস্বরূপ, অনেক অপারেটিং সিস্টেমে একই সাথে খোলা কানেকশনের সংখ্যার একটি সীমা থাকে; সেই সংখ্যার বাইরে নতুন কানেকশন প্রচেষ্টা করলে কিছু খোলা কানেকশন বন্ধ না হওয়া পর্যন্ত এরর দেখা দেবে।

চলুন এই কোডটি রান করি! টার্মিনালে cargo run কমান্ডটি চালান এবং তারপর একটি ওয়েব ব্রাউজারে 127.0.0.1:7878 লোড করুন। ব্রাউজারে "Connection reset"-এর মতো একটি এরর মেসেজ দেখানো উচিত, কারণ সার্ভারটি বর্তমানে কোনো ডেটা ফেরত পাঠাচ্ছে না। কিন্তু আপনি যখন আপনার টার্মিনাল দেখবেন, তখন ব্রাউজার সার্ভারের সাথে সংযোগ করার সময় প্রিন্ট হওয়া বেশ কয়েকটি বার্তা দেখতে পাবেন!

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

কখনও কখনও আপনি একটি ব্রাউজার রিকোয়েস্টের জন্য একাধিক প্রিন্ট করা বার্তা দেখতে পাবেন; এর কারণ হতে পারে ব্রাউজারটি পেজের জন্য একটি রিকোয়েস্ট করছে এবং সেই সাথে অন্যান্য রিসোর্সের জন্যও রিকোয়েস্ট করছে, যেমন ব্রাউজার ট্যাবে প্রদর্শিত favicon.ico আইকন।

এমনও হতে পারে যে সার্ভারটি কোনো ডেটা দিয়ে রেসপন্স না করায় ব্রাউজার একাধিকবার সার্ভারের সাথে সংযোগ করার চেষ্টা করছে। যখন লুপের শেষে stream স্কোপের বাইরে চলে যায় এবং ড্রপ হয়ে যায়, তখন drop ইমপ্লিমেন্টেশনের অংশ হিসেবে সংযোগটি বন্ধ হয়ে যায়। ব্রাউজারগুলো কখনও কখনও বন্ধ হয়ে যাওয়া সংযোগ পুনরায় চেষ্টা করে মোকাবেলা করে, কারণ সমস্যাটি অস্থায়ী হতে পারে।

ব্রাউজারগুলো কোনো রিকোয়েস্ট না পাঠিয়েও সার্ভারে একাধিক কানেকশন খুলতে পারে, যাতে পরে যদি তারা রিকোয়েস্ট পাঠায়, তবে সেই রিকোয়েস্টগুলো দ্রুত সম্পন্ন হতে পারে। যখন এমনটা হয়, আমাদের সার্ভার প্রতিটি কানেকশন দেখতে পায়, সেই কানেকশনে কোনো রিকোয়েস্ট থাকুক বা না থাকুক। উদাহরণস্বরূপ, ক্রোম-ভিত্তিক ব্রাউজারগুলোর অনেক সংস্করণেই এমনটা করে থাকে; আপনি প্রাইভেট ব্রাউজিং মোড ব্যবহার করে বা অন্য কোনো ব্রাউজার ব্যবহার করে এই অপটিমাইজেশনটি নিষ্ক্রিয় করতে পারেন।

গুরুত্বপূর্ণ বিষয় হলো আমরা সফলভাবে একটি TCP connection-এর একটি হ্যান্ডেল পেয়েছি!

মনে রাখবেন, কোডের একটি নির্দিষ্ট সংস্করণ চালানো শেষ হলে ctrl-C চেপে প্রোগ্রামটি বন্ধ করতে হবে। এরপর কোডে প্রতিটি পরিবর্তনের পর cargo run কমান্ড দিয়ে প্রোগ্রামটি পুনরায় চালু করুন, যাতে আপনি নতুন কোডটি চালাচ্ছেন তা নিশ্চিত হয়।

রিকোয়েস্ট পড়া

চলুন, ব্রাউজার থেকে রিকোয়েস্ট পড়ার কার্যকারিতা ইমপ্লিমেন্ট করি! প্রথমে একটি কানেকশন পাওয়া এবং তারপর সেই কানেকশন নিয়ে কোনো কাজ করার উদ্বেগগুলো আলাদা করতে, আমরা কানেকশন প্রসেস করার জন্য একটি নতুন ফাংশন শুরু করব। এই নতুন handle_connection ফাংশনে, আমরা TCP স্ট্রিম থেকে ডেটা পড়ব এবং এটি প্রিন্ট করব যাতে আমরা ব্রাউজার থেকে পাঠানো ডেটা দেখতে পারি। কোডটি পরিবর্তন করে লিস্টিং ২১-২ এর মতো করুন।

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::prelude এবং std::io::BufReader স্কোপের মধ্যে নিয়ে এসেছি যাতে আমরা স্ট্রিম থেকে ডেটা পড়া এবং লেখার জন্য প্রয়োজনীয় trait এবং type-গুলিতে অ্যাক্সেস পেতে পারি। main ফাংশনের for লুপে, আমরা কানেকশন তৈরির বার্তা প্রিন্ট করার পরিবর্তে, এখন নতুন handle_connection ফাংশনটি কল করি এবং এতে stream পাস করি।

handle_connection ফাংশনে, আমরা একটি নতুন BufReader ইনস্ট্যান্স তৈরি করি যা stream-এর একটি রেফারেন্সকে wrap করে। BufReader আমাদের জন্য std::io::Read trait মেথড কলগুলো পরিচালনা করে বাফারিং যুক্ত করে।

আমরা http_request নামে একটি ভেরিয়েবল তৈরি করেছি ব্রাউজার থেকে আমাদের সার্ভারে পাঠানো রিকোয়েস্টের লাইনগুলো সংগ্রহ করার জন্য। আমরা Vec<_> টাইপ অ্যানোটেশন যোগ করে নির্দেশ করছি যে আমরা এই লাইনগুলো একটি ভেক্টরে সংগ্রহ করতে চাই।

BufReader, std::io::BufRead ট্রেইটটি ইমপ্লিমেন্ট করে, যা lines মেথড প্রদান করে। lines মেথডটি ডেটা স্ট্রিমকে যখনই একটি নিউলাইন বাইট দেখে, তখনই বিভক্ত করে Result<String, std::io::Error> এর একটি ইটারেটর রিটার্ন করে। প্রতিটি String পাওয়ার জন্য, আমরা প্রতিটি Result কে map এবং unwrap করি। যদি ডেটা বৈধ UTF-8 না হয় বা স্ট্রিম থেকে পড়তে কোনো সমস্যা হয়, তাহলে Result একটি এরর হতে পারে। আবারও বলছি, একটি প্রোডাকশন-লেভেলের প্রোগ্রামে এই এররগুলো আরও ভালোভাবে পরিচালনা করা উচিত, কিন্তু আমরা সরলতার জন্য এরর হলে প্রোগ্রামটি বন্ধ করে দিচ্ছি।

ব্রাউজার একটি HTTP রিকোয়েস্টের শেষ বোঝাতে পরপর দুটি নিউলাইন ক্যারেক্টার পাঠায়, তাই স্ট্রিম থেকে একটি রিকোয়েস্ট পেতে, আমরা লাইন নিতে থাকি যতক্ষণ না একটি খালি স্ট্রিংয়ের লাইন পাই। একবার ভেক্টরে লাইনগুলো সংগ্রহ করার পরে, আমরা সেগুলোকে প্রিটি ডিবাগ ফরম্যাটিং ব্যবহার করে প্রিন্ট করছি যাতে ওয়েব ব্রাউজার আমাদের সার্ভারকে কী নির্দেশ পাঠাচ্ছে তা দেখতে পারি।

চলুন এই কোডটি চেষ্টা করি! প্রোগ্রামটি শুরু করুন এবং একটি ওয়েব ব্রাউজারে আবার একটি রিকোয়েস্ট করুন। মনে রাখবেন যে আমরা এখনও ব্রাউজারে একটি এরর পেজ পাবো, কিন্তু টার্মিনালে আমাদের প্রোগ্রামের আউটপুট এখন এইরকম দেখাবে:

$ cargo run
   Compiling hello v0.1.0 (file:///projects/hello)
    Finished dev [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 রিকোয়েস্টের দিকে আরেকটু গভীর দৃষ্টি

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) এর মতো নয়। এই অধ্যায়ে আমাদের উদ্দেশ্যে URI এবং URL-এর মধ্যে পার্থক্য গুরুত্বপূর্ণ নয়, তবে HTTP স্পেসিফিকেশন URI শব্দটি ব্যবহার করে, তাই আমরা এখানে মানসিকভাবে URI-এর জন্য URL প্রতিস্থাপন করতে পারি।

শেষ অংশটি হলো ক্লায়েন্টের ব্যবহৃত HTTP ভার্সন, এবং তারপর রিকোয়েস্ট লাইনটি একটি CRLF সিকোয়েন্সে শেষ হয়। (CRLF মানে হলো carriage return এবং line feed, যা টাইপরাইটারের দিনের পরিভাষা!) CRLF সিকোয়েন্সটিকে \r\n হিসেবেও লেখা যেতে পারে, যেখানে \r একটি carriage return এবং \n একটি line feed। CRLF sequence রিকোয়েস্ট লাইনটিকে বাকি রিকোয়েস্ট ডেটা থেকে আলাদা করে। লক্ষ্য করুন যে যখন CRLF প্রিন্ট হয়, তখন আমরা \r\n না দেখে একটি নতুন লাইন শুরু হতে দেখি।

আমাদের প্রোগ্রাম চালিয়ে প্রাপ্ত রিকোয়েস্ট লাইন ডেটা দেখলে, আমরা দেখি যে GET হলো মেথড, / হলো রিকোয়েস্ট URI, এবং HTTP/1.1 হলো ভার্সন।

রিকোয়েস্ট লাইনের পরে, Host: থেকে শুরু করে বাকি লাইনগুলো হলো হেডার। GET রিকোয়েস্টে কোনো বডি থাকে না।

অন্য একটি ব্রাউজার থেকে রিকোয়েস্ট করার চেষ্টা করুন বা একটি ভিন্ন অ্যাড্রেস, যেমন 127.0.0.1:7878/test চেয়ে দেখুন, রিকোয়েস্ট ডেটা কীভাবে পরিবর্তিত হয়।

এখন যেহেতু আমরা জানি ব্রাউজার কী চাইছে, চলুন কিছু ডেটা ফেরত পাঠাই!

একটি রেসপন্স লেখা

আমরা এখন ক্লায়েন্টের রিকোয়েস্টের জবাবে ডেটা পাঠানোর কার্যকারিতা ইমপ্লিমেন্ট করব। রেসপন্সগুলোর ফরম্যাট নিচে দেওয়া হলো:

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

প্রথম লাইনটি একটি status line যা রেসপন্সে ব্যবহৃত HTTP ভার্সন, একটি সংখ্যাসূচক স্ট্যাটাস কোড যা রিকোয়েস্টের ফলাফল সংক্ষিপ্তভাবে জানায়, এবং একটি কারণ-বাক্যাংশ যা স্ট্যাটাস কোডের একটি টেক্সট বর্ণনা প্রদান করে। CRLF সিকোয়েন্সের পরে যেকোনো হেডার, আরেকটি CRLF সিকোয়েন্স এবং রেসপন্সের বডি থাকে।

এখানে একটি উদাহরণ রেসপন্স রয়েছে যা HTTP ভার্সন 1.1 ব্যবহার করে, যার স্ট্যাটাস কোড 200, একটি OK কারণ-বাক্যাংশ, কোনো হেডার নেই এবং কোনো বডি নেই:

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

স্ট্যাটাস কোড 200 হলো স্ট্যান্ডার্ড সফল রেসপন্স। টেক্সটটি একটি ক্ষুদ্র সফল HTTP রেসপন্স। চলুন, একটি সফল রিকোয়েস্টের জবাবে এটিকে স্ট্রিমে লিখি! handle_connection ফাংশন থেকে println! যা রিকোয়েস্ট ডেটা প্রিন্ট করছিল তা সরিয়ে দিন এবং লিস্টিং ২১-৩ এর কোড দিয়ে প্রতিস্থাপন করুন।

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 ভেরিয়েবলকে সংজ্ঞায়িত করে যা সফল বার্তার ডেটা ধারণ করে। তারপর আমরা আমাদের response-এর উপর as_bytes কল করে স্ট্রিং ডেটাকে বাইটে রূপান্তর করি। stream-এর write_all মেথডটি একটি &[u8] নেয় এবং সেই বাইটগুলো সরাসরি কানেকশনে পাঠিয়ে দেয়। যেহেতু write_all অপারেশন ব্যর্থ হতে পারে, তাই আমরা আগের মতোই যেকোনো এরর ফলাফলের উপর unwrap ব্যবহার করি। আবারও, একটি বাস্তব অ্যাপ্লিকেশনে আপনার এখানে এরর হ্যান্ডলিং যোগ করা উচিত।

এই পরিবর্তনগুলোর সাথে, চলুন আমাদের কোড চালাই এবং একটি রিকোয়েস্ট করি। আমরা আর টার্মিনালে কোনো ডেটা প্রিন্ট করছি না, তাই আমরা কার্গোর আউটপুট ছাড়া আর কিছুই দেখতে পাব না। যখন আপনি একটি ওয়েব ব্রাউজারে 127.0.0.1:7878 লোড করবেন, তখন আপনি একটি এররের পরিবর্তে একটি খালি পেজ দেখতে পাবেন। আপনি এইমাত্র হাতে-কলমে একটি HTTP রিকোয়েস্ট গ্রহণ এবং একটি রেসপন্স পাঠানো কোড করেছেন!

আসল HTML ফেরত পাঠানো

চলুন একটি খালি পেজের চেয়ে বেশি কিছু ফেরত পাঠানোর কার্যকারিতা ইমপ্লিমেন্ট করি। আপনার প্রজেক্ট ডিরেক্টরির রুটে একটি নতুন ফাইল hello.html তৈরি করুন, src ডিরেক্টরিতে নয়। আপনি যেকোনো HTML ইনপুট করতে পারেন; লিস্টিং ২১-৪ একটি সম্ভাব্য উদাহরণ দেখায়।

<!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 পরিবর্তন করব যাতে এটি 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();
}

আমরা use স্টেটমেন্টে fs যোগ করেছি যাতে স্ট্যান্ডার্ড লাইব্রেরির ফাইল সিস্টেম মডিউলটি স্কোপে আসে। একটি ফাইলের বিষয়বস্তু একটি স্ট্রিংয়ে পড়ার কোডটি পরিচিত মনে হওয়া উচিত; আমরা আমাদের I/O প্রজেক্টে লিস্টিং ১২-৪ এ একটি ফাইলের বিষয়বস্তু পড়ার সময় এটি ব্যবহার করেছি।

এরপরে, আমরা ফাইলের বিষয়বস্তু সফল রেসপন্সের বডি হিসেবে যোগ করতে 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 ফাইলটি ফেরত পাঠাতে চাই।

রিকোয়েস্ট যাচাই এবং বেছে বেছে রেসপন্স করা

এখন, আমাদের ওয়েব সার্ভার ক্লায়েন্টের রিকোয়েস্ট যাই হোক না কেন, ফাইলের HTML ফেরত দেবে। চলুন, ব্রাউজার / রিকোয়েস্ট করছে কিনা তা পরীক্ষা করার জন্য কার্যকারিতা যোগ করি এবং যদি ব্রাউজার অন্য কিছু রিকোয়েস্ট করে তবে একটি এরর ফেরত দিই। এর জন্য আমাদের handle_connection পরিবর্তন করতে হবে, যেমনটি লিস্টিং ২১-৬ এ দেখানো হয়েছে। এই নতুন কোডটি প্রাপ্ত রিকোয়েস্টের বিষয়বস্তু / রিকোয়েস্টের সাথে তুলনা করে এবং রিকোয়েস্টগুলো ভিন্নভাবে ট্রিট করার জন্য 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 হ্যান্ডেল করে এবং লিস্টিং ২১-২-এ যোগ করা map-এর unwrap-এর মতোই কাজ করে।

এরপরে, আমরা request_line পরীক্ষা করি যে এটি / পাথের একটি GET রিকোয়েস্টের রিকোয়েস্ট লাইনের সমান কিনা। যদি তাই হয়, if ব্লকটি আমাদের HTML ফাইলের বিষয়বস্তু ফেরত দেয়।

যদি request_line টি / পাথের GET রিকোয়েস্টের সমান না হয়, তার মানে আমরা অন্য কোনো রিকোয়েস্ট পেয়েছি। আমরা কিছুক্ষণের মধ্যে else ব্লকে কোড যোগ করব অন্যান্য সব রিকোয়েস্টের জবাব দেওয়ার জন্য।

এখন এই কোডটি চালান এবং 127.0.0.1:7878 রিকোয়েস্ট করুন; আপনার hello.html-এর HTML পাওয়া উচিত। আপনি যদি অন্য কোনো রিকোয়েস্ট করেন, যেমন 127.0.0.1:7878/something-else, আপনি একটি কানেকশন এরর পাবেন যেমনটি আপনি লিস্টিং ২১-১ এবং লিস্টিং ২১-২ চালানোর সময় দেখেছিলেন।

এখন চলুন লিস্টিং ২১-৭ এর কোডটি 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। রেসপন্সের বডি হবে 404.html ফাইলের HTML। এরর পেজের জন্য আপনাকে hello.html-এর পাশে একটি 404.html ফাইল তৈরি করতে হবে; আবারও আপনার ইচ্ছামত যেকোনো HTML ব্যবহার করতে পারেন, অথবা লিস্টিং ২১-৮-এর উদাহরণ HTML ব্যবহার করতে পারেন।

<!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, 404.html থেকে এরর HTML ফেরত দেওয়া উচিত।

একটু রিফ্যাক্টরিং

এই মুহূর্তে, if এবং else ব্লকগুলিতে অনেক পুনরাবৃত্তি রয়েছে: তারা উভয়ই ফাইল পড়ছে এবং ফাইলগুলির বিষয়বস্তু স্ট্রিমে লিখছে। একমাত্র পার্থক্য হলো স্ট্যাটাস লাইন এবং ফাইলের নাম। চলুন, কোডটিকে আরও সংক্ষিপ্ত করি এই পার্থক্যগুলোকে আলাদা if এবং else লাইনে নিয়ে এসে, যা স্ট্যাটাস লাইন এবং ফাইলের নামের মানগুলিকে ভেরিয়েবলে অ্যাসাইন করবে; তারপর আমরা ফাইল পড়া এবং রেসপন্স লেখার জন্য সেই ভেরিয়েবলগুলিকে শর্তহীনভাবে ব্যবহার করতে পারি। লিস্টিং ২১-৯ বড় 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 ব্লকগুলো শুধুমাত্র স্ট্যাটাস লাইন এবং ফাইলের নামের জন্য উপযুক্ত মান একটি টাপল-এ রিটার্ন করে; এরপর আমরা let স্টেটমেন্টে একটি প্যাটার্ন ব্যবহার করে status_line এবং filename-এ এই দুটি মান অ্যাসাইন করতে destructuring ব্যবহার করি, যা অধ্যায় ১৯-এ আলোচনা করা হয়েছে।

পূর্বে ডুপ্লিকেট করা কোডটি এখন if এবং else ব্লকের বাইরে এবং status_linefilename ভেরিয়েবল ব্যবহার করে। এটি দুটি ক্ষেত্রের মধ্যে পার্থক্য দেখতে সহজ করে তোলে, এবং এর মানে হলো যদি আমরা ফাইল পড়া এবং রেসপন্স লেখার কাজ পরিবর্তন করতে চাই তবে আমাদের শুধুমাত্র একটি জায়গায় কোড আপডেট করতে হবে। লিস্টিং ২১-৯ এর কোডের আচরণ লিস্টিং ২১-৭ এর মতোই হবে।

অসাধারণ! আমাদের কাছে এখন প্রায় ৪০ লাইনের রাস্ট কোডে একটি সাধারণ ওয়েব সার্ভার রয়েছে যা একটি রিকোয়েস্টে একটি কন্টেন্ট পেজ দিয়ে সাড়া দেয় এবং অন্য সব রিকোয়েস্টে একটি 404 রেসপন্স দিয়ে সাড়া দেয়।

বর্তমানে, আমাদের সার্ভার একটি একক থ্রেডে চলে, যার মানে এটি একবারে শুধুমাত্র একটি রিকোয়েস্ট পরিবেশন করতে পারে। চলুন কিছু ধীরগতির রিকোয়েস্ট সিমুলেট করে দেখি কীভাবে এটি একটি সমস্যা হতে পারে। তারপর আমরা এটি ঠিক করব যাতে আমাদের সার্ভার একবারে একাধিক রিকোয়েস্ট পরিচালনা করতে পারে।