একটি সিঙ্গেল-থ্রেডেড ওয়েব সার্ভার তৈরি করা (Building a Single-Threaded Web Server)
আমরা একটি সিঙ্গেল-থ্রেডেড ওয়েব সার্ভার চালু করে শুরু করব। শুরু করার আগে, ওয়েব সার্ভার তৈরিতে জড়িত প্রোটোকলগুলির একটি সংক্ষিপ্ত বিবরণ দেখা যাক। এই প্রোটোকলগুলির বিশদ বিবরণ এই বইয়ের সুযোগের বাইরে, তবে একটি সংক্ষিপ্ত বিবরণ আপনাকে প্রয়োজনীয় তথ্য দেবে।
ওয়েব সার্ভারগুলিতে জড়িত দুটি প্রধান প্রোটোকল হল হাইপারটেক্সট ট্রান্সফার প্রোটোকল (HTTP) এবং ট্রান্সমিশন কন্ট্রোল প্রোটোকল (TCP)। উভয় প্রোটোকলই রিকোয়েস্ট-রেসপন্স প্রোটোকল, অর্থাৎ একজন ক্লায়েন্ট রিকোয়েস্ট শুরু করে এবং একজন সার্ভার রিকোয়েস্টগুলি শোনে এবং ক্লায়েন্টকে একটি রেসপন্স প্রদান করে। সেই রিকোয়েস্ট এবং রেসপন্সগুলির বিষয়বস্তু প্রোটোকল দ্বারা সংজ্ঞায়িত করা হয়।
TCP হল নিম্ন-স্তরের প্রোটোকল যা বর্ণনা করে কিভাবে তথ্য এক সার্ভার থেকে অন্য সার্ভারে যায়, কিন্তু সেই তথ্যটি কী তা নির্দিষ্ট করে না। HTTP, TCP-এর উপরে তৈরি হয়ে রিকোয়েস্ট এবং রেসপন্সগুলির বিষয়বস্তু সংজ্ঞায়িত করে। টেকনিক্যালি HTTP অন্যান্য প্রোটোকলের সাথে ব্যবহার করা সম্ভব, কিন্তু বেশিরভাগ ক্ষেত্রে, HTTP তার ডেটা TCP-এর মাধ্যমে পাঠায়। আমরা TCP এবং HTTP রিকোয়েস্ট এবং রেসপন্সের raw bytes নিয়ে কাজ করব।
TCP কানেকশন শোনা (Listening to the TCP Connection)
আমাদের ওয়েব সার্ভারকে একটি TCP কানেকশন শুনতে হবে, তাই এটিই প্রথম অংশ যা নিয়ে আমরা কাজ করব। স্ট্যান্ডার্ড লাইব্রেরি একটি std::net
মডিউল সরবরাহ করে যা আমাদের এটি করতে দেয়। চলুন যথারীতি একটি নতুন প্রোজেক্ট তৈরি করি:
$ cargo new hello
Created binary (application) `hello` project
$ cd hello
এখন শুরু করার জন্য src/main.rs-এ Listing 21-1-এর কোডটি লিখুন। এই কোডটি লোকাল অ্যাড্রেস 127.0.0.1:7878
-এ আগত TCP স্ট্রিমগুলির জন্য শুনবে। যখন এটি একটি আগত স্ট্রিম পাবে, তখন এটি 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 কানেকশন শুনতে পারি। অ্যাড্রেসে, কোলনের আগের অংশটি আপনার কম্পিউটারকে উপস্থাপন করে এমন একটি IP অ্যাড্রেস (এটি প্রতিটি কম্পিউটারে একই এবং লেখকদের কম্পিউটারের প্রতিনিধিত্ব করে না), এবং 7878
হল পোর্ট। আমরা দুটি কারণে এই পোর্টটি বেছে নিয়েছি: HTTP সাধারণত এই পোর্টে গৃহীত হয় না তাই আমাদের সার্ভারটি আপনার মেশিনে চলমান অন্য কোনও ওয়েব সার্ভারের সাথে বিরোধ করার সম্ভাবনা কম, এবং 7878 হল টেলিফোনে rust টাইপ করা।
এই পরিস্থিতিতে bind
ফাংশনটি new
ফাংশনের মতোই কাজ করে, যেখানে এটি একটি নতুন TcpListener
ইনস্ট্যান্স রিটার্ন করবে। ফাংশনটির নাম bind
কারণ, নেটওয়ার্কিং-এ, শোনার জন্য একটি পোর্টের সাথে কানেক্ট করাকে "বাইন্ডিং টু এ পোর্ট" বলা হয়।
bind
ফাংশনটি একটি Result<T, E>
রিটার্ন করে, যা নির্দেশ করে যে বাইন্ডিং ব্যর্থ হওয়া সম্ভব। উদাহরণস্বরূপ, পোর্ট 80-তে কানেক্ট করার জন্য অ্যাডমিনিস্ট্রেটরের বিশেষাধিকার প্রয়োজন (নন-অ্যাডমিনিস্ট্রেটররা শুধুমাত্র 1023-এর চেয়ে বেশি পোর্টে শুনতে পারে), তাই আমরা যদি অ্যাডমিনিস্ট্রেটর না হয়ে পোর্ট 80-তে কানেক্ট করার চেষ্টা করি, তাহলে বাইন্ডিং কাজ করবে না। উদাহরণস্বরূপ, বাইন্ডিং কাজ করবে না যদি আমরা আমাদের প্রোগ্রামের দুটি ইনস্ট্যান্স চালাই এবং তাই একই পোর্টে দুটি প্রোগ্রাম শুনি। যেহেতু আমরা শুধুমাত্র শেখার উদ্দেশ্যে একটি বেসিক সার্ভার লিখছি, তাই আমরা এই ধরণের ত্রুটিগুলি পরিচালনা করার বিষয়ে চিন্তা করব না; পরিবর্তে, ত্রুটি ঘটলে প্রোগ্রাম বন্ধ করতে আমরা unwrap
ব্যবহার করি।
TcpListener
-এর incoming
মেথড একটি ইটারেটর রিটার্ন করে যা আমাদের স্ট্রিমের একটি সিকোয়েন্স দেয় (আরও নির্দিষ্টভাবে, TcpStream
টাইপের স্ট্রিম)। একটি একক স্ট্রিম ক্লায়েন্ট এবং সার্ভারের মধ্যে একটি খোলা কানেকশন উপস্থাপন করে। একটি কানেকশন হল সম্পূর্ণ রিকোয়েস্ট এবং রেসপন্স প্রক্রিয়ার নাম যেখানে একজন ক্লায়েন্ট সার্ভারের সাথে কানেক্ট করে, সার্ভার একটি রেসপন্স তৈরি করে এবং সার্ভার কানেকশন বন্ধ করে দেয়। যেমন, ক্লায়েন্ট কী পাঠিয়েছে তা দেখতে আমরা 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 কানেকশনের হ্যান্ডেল পেয়েছি!
আপনি যখন কোডের একটি নির্দিষ্ট সংস্করণ চালানো শেষ করবেন তখন ctrl-c টিপে প্রোগ্রামটি বন্ধ করতে ভুলবেন না। তারপর আপনি প্রতিটি কোড পরিবর্তনের সেট করার পরে cargo run
কমান্ডটি ব্যবহার করে প্রোগ্রামটি পুনরায় চালু করুন যাতে আপনি নতুন কোড চালাচ্ছেন তা নিশ্চিত করতে পারেন।
রিকোয়েস্ট পড়া (Reading the Request)
আসুন ব্রাউজার থেকে রিকোয়েস্ট পড়ার কার্যকারিতা ইমপ্লিমেন্ট করি! প্রথমে একটি কানেকশন পাওয়া এবং তারপর কানেকশনের সাথে কিছু কাজ করার বিষয়গুলিকে আলাদা করার জন্য, আমরা কানেকশন প্রসেস করার জন্য একটি নতুন ফাংশন শুরু করব। এই নতুন handle_connection
ফাংশনে, আমরা TCP স্ট্রিম থেকে ডেটা পড়ব এবং ব্রাউজার থেকে পাঠানো ডেটা দেখতে এটি প্রিন্ট করব। কোডটিকে Listing 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::prelude
এবং std::io::BufReader
-কে স্কোপে নিয়ে আসি যাতে আমরা স্ট্রিম থেকে পড়তে এবং লিখতে পারি এমন ট্রেইট এবং টাইপগুলিতে অ্যাক্সেস পেতে পারি। main
ফাংশনের for
লুপে, আমরা একটি কানেকশন তৈরি করেছি এমন একটি মেসেজ প্রিন্ট করার পরিবর্তে, এখন আমরা নতুন handle_connection
ফাংশনটি কল করি এবং stream
পাস করি।
handle_connection
ফাংশনে, আমরা একটি নতুন BufReader
ইনস্ট্যান্স তৈরি করি যা stream
-এর একটি রেফারেন্সকে র্যাপ করে। BufReader
আমাদের জন্য std::io::Read
ট্রেইট মেথডগুলিতে কল পরিচালনা করে বাফারিং যোগ করে।
আমরা ব্রাউজার আমাদের সার্ভারে যে রিকোয়েস্ট পাঠায় তার লাইনগুলি সংগ্রহ করার জন্য http_request
নামে একটি ভেরিয়েবল তৈরি করি। আমরা নির্দেশ করি যে আমরা এই লাইনগুলিকে একটি ভেক্টরে সংগ্রহ করতে চাই Vec<_>
টাইপ অ্যানোটেশন যোগ করে।
BufReader
, std::io::BufRead
ট্রেইট ইমপ্লিমেন্ট করে, যা lines
মেথড সরবরাহ করে। lines
মেথড ডেটার স্ট্রিমটিকে যখনই একটি নতুন লাইনের বাইট দেখতে পায় তখনই বিভক্ত করে Result<String, std::io::Error>
-এর একটি ইটারেটর রিটার্ন করে। প্রতিটি String
পেতে, আমরা প্রতিটি Result
-কে ম্যাপ করি এবং unwrap
করি। ডেটা বৈধ UTF-8 না হলে বা স্ট্রিম থেকে পড়তে সমস্যা হলে Result
একটি error হতে পারে। আবারও, একটি প্রোডাকশন প্রোগ্রামের এই ত্রুটিগুলিকে আরও সুন্দরভাবে পরিচালনা করা উচিত, কিন্তু আমরা সরলতার জন্য ত্রুটির ক্ষেত্রে প্রোগ্রামটি বন্ধ করা বেছে নিচ্ছি।
ব্রাউজার পরপর দুটি নতুন লাইন অক্ষর পাঠিয়ে একটি HTTP রিকোয়েস্টের সমাপ্তি নির্দেশ করে, তাই স্ট্রিম থেকে একটি রিকোয়েস্ট পেতে, আমরা লাইনগুলি ততক্ষণ নিই যতক্ষণ না আমরা একটি লাইন পাই যা খালি স্ট্রিং। একবার আমরা লাইনগুলিকে ভেক্টরে সংগ্রহ করার পরে, আমরা সেগুলিকে সুন্দর ডিবাগ ফরম্যাটিং ব্যবহার করে প্রিন্ট করি যাতে আমরা ওয়েব ব্রাউজার আমাদের সার্ভারে যে নির্দেশাবলী পাঠাচ্ছে তা দেখতে পারি।
আসুন এই কোডটি চেষ্টা করি! প্রোগ্রামটি শুরু করুন এবং আবার একটি ওয়েব ব্রাউজারে একটি রিকোয়েস্ট করুন। লক্ষ্য করুন যে আমরা এখনও ব্রাউজারে একটি error পেজ পাব, কিন্তু টার্মিনালে আমাদের প্রোগ্রামের আউটপুট এখন এইরকম দেখতে হবে:
$ 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 রিকোয়েস্টের আরও বিশদ পর্যালোচনা (A Closer Look at an HTTP Request)
HTTP হল একটি টেক্সট-ভিত্তিক প্রোটোকল, এবং একটি রিকোয়েস্ট এই ফর্ম্যাট নেয়:
Method Request-URI HTTP-Version CRLF
headers CRLF
message-body
প্রথম লাইনটি হল রিকোয়েস্ট লাইন যা ক্লায়েন্ট কী রিকোয়েস্ট করছে সে সম্পর্কে তথ্য রাখে। রিকোয়েস্ট লাইনের প্রথম অংশটি ব্যবহৃত মেথড, যেমন GET
বা POST
নির্দেশ করে, যা বর্ণনা করে যে ক্লায়েন্ট কীভাবে এই রিকোয়েস্টটি করছে। আমাদের ক্লায়েন্ট একটি GET
রিকোয়েস্ট ব্যবহার করেছে, যার মানে এটি তথ্য চাইছে।
রিকোয়েস্ট লাইনের পরবর্তী অংশটি হল /, যা ক্লায়েন্ট যে ইউনিফর্ম রিসোর্স আইডেন্টিফায়ার (URI) রিকোয়েস্ট করছে তা নির্দেশ করে: একটি URI প্রায়, কিন্তু সম্পূর্ণরূপে নয়, একটি ইউনিফর্ম রিসোর্স লোকেটর (URL)-এর মতোই। URI এবং URL-এর মধ্যে পার্থক্য এই চ্যাপ্টারে আমাদের উদ্দেশ্যের জন্য গুরুত্বপূর্ণ নয়, তবে HTTP স্পেক URI শব্দটি ব্যবহার করে, তাই আমরা এখানে URI-এর জন্য মানসিকভাবে URL প্রতিস্থাপন করতে পারি।
শেষ অংশটি হল ক্লায়েন্ট যে HTTP ভার্সন ব্যবহার করছে এবং তারপর রিকোয়েস্ট লাইনটি একটি CRLF সিকোয়েন্স দিয়ে শেষ হয়। (CRLF মানে ক্যারেজ রিটার্ন এবং লাইন ফিড, যা টাইপরাইটারের দিনের শব্দ!) CRLF সিকোয়েন্সটিকে \r\n
হিসাবেও লেখা যেতে পারে, যেখানে \r
হল একটি ক্যারেজ রিটার্ন এবং \n
হল একটি লাইন ফিড। CRLF সিকোয়েন্স রিকোয়েস্ট লাইনটিকে রিকোয়েস্টের বাকি ডেটা থেকে আলাদা করে। লক্ষ্য করুন যে যখন CRLF প্রিন্ট করা হয়, তখন আমরা \r\n
-এর পরিবর্তে একটি নতুন লাইন শুরু হতে দেখি।
এখন পর্যন্ত আমাদের প্রোগ্রাম চালানোর ফলে প্রাপ্ত রিকোয়েস্ট লাইনের ডেটা দেখলে, আমরা দেখতে পাই যে GET
হল মেথড, / হল রিকোয়েস্ট URI এবং HTTP/1.1
হল ভার্সন।
রিকোয়েস্ট লাইনের পরে, Host:
থেকে শুরু করে বাকি লাইনগুলি হল হেডার। GET
রিকোয়েস্টের কোনও বডি নেই।
একটি ভিন্ন ব্রাউজার থেকে একটি রিকোয়েস্ট করার চেষ্টা করুন বা একটি ভিন্ন অ্যাড্রেস, যেমন 127.0.0.1:7878/test, রিকোয়েস্ট করার চেষ্টা করুন যাতে রিকোয়েস্ট ডেটা কীভাবে পরিবর্তিত হয় তা দেখা যায়।
এখন আমরা জানি ব্রাউজার কী চাইছে, আসুন কিছু ডেটা ফেরত পাঠানো যাক!
একটি রেসপন্স লেখা (Writing a Response)
ক্লায়েন্ট রিকোয়েস্টের রেসপন্সে ডেটা পাঠানোর কাজটি আমরা ইমপ্লিমেন্ট করব। রেসপন্সগুলির নিম্নলিখিত ফর্ম্যাট রয়েছে:
HTTP-Version Status-Code Reason-Phrase CRLF
headers CRLF
message-body
প্রথম লাইনটি হল একটি স্ট্যাটাস লাইন যাতে রেসপন্সে ব্যবহৃত HTTP ভার্সন, রিকোয়েস্টের ফলাফলের সংক্ষিপ্ত বিবরণ দেয় এমন একটি সংখ্যাসূচক স্ট্যাটাস কোড এবং স্ট্যাটাস কোডের একটি টেক্সট বিবরণ প্রদান করে এমন একটি কারণ বাক্যাংশ রয়েছে। CRLF সিকোয়েন্সের পরে যেকোনও হেডার, আরেকটি CRLF সিকোয়েন্স এবং রেসপন্সের বডি থাকে।
এখানে একটি উদাহরণ রেসপন্স রয়েছে যা HTTP ভার্সন 1.1 ব্যবহার করে এবং যার স্ট্যাটাস কোড 200, একটি OK কারণ বাক্যাংশ, কোনও হেডার নেই এবং কোনও বডি নেই:
HTTP/1.1 200 OK\r\n\r\n
স্ট্যাটাস কোড 200 হল স্ট্যান্ডার্ড সাফল্যের রেসপন্স। টেক্সটটি একটি ক্ষুদ্র সফল HTTP রেসপন্স। আসুন এটিকে একটি সফল রিকোয়েস্টের রেসপন্স হিসাবে স্ট্রীমে লিখি! handle_connection
ফাংশন থেকে, println!
সরিয়ে দিন যা রিকোয়েস্ট ডেটা প্রিন্ট করছিল এবং এটিকে Listing 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
ভেরিয়েবলটিকে সংজ্ঞায়িত করে যা সাফল্যের মেসেজের ডেটা ধারণ করে। তারপর আমরা আমাদের response
-এ as_bytes
কল করি যাতে স্ট্রিং ডেটাকে বাইটে রূপান্তর করা যায়। stream
-এর write_all
মেথডটি একটি &[u8]
নেয় এবং সেই বাইটগুলিকে সরাসরি কানেকশনে পাঠায়। যেহেতু write_all
অপারেশনটি ব্যর্থ হতে পারে, তাই আমরা আগের মতোই যেকোনো error ফলাফলের উপর unwrap
ব্যবহার করি। আবারও, একটি বাস্তব অ্যাপ্লিকেশনে আপনি এখানে error হ্যান্ডলিং যুক্ত করবেন।
এই পরিবর্তনগুলির সাথে, আসুন আমাদের কোডটি চালাই এবং একটি রিকোয়েস্ট করি। আমরা আর টার্মিনালে কোনও ডেটা প্রিন্ট করছি না, তাই আমরা Cargo থেকে আউটপুট ছাড়া অন্য কোনও আউটপুট দেখতে পাব না। আপনি যখন একটি ওয়েব ব্রাউজারে 127.0.0.1:7878 লোড করবেন, তখন আপনি একটি error-এর পরিবর্তে একটি ফাঁকা পেজ পাবেন। আপনি এইমাত্র একটি HTTP রিকোয়েস্ট গ্রহণ এবং একটি রেসপন্স পাঠানোর হ্যান্ডকোড করেছেন!
রিয়েল HTML রিটার্ন করা (Returning Real HTML)
আসুন একটি ফাঁকা পেজের চেয়ে বেশি কিছু রিটার্ন করার কার্যকারিতা ইমপ্লিমেন্ট করি। আপনার প্রোজেক্ট ডিরেক্টরির রুটে, src ডিরেক্টরিতে নয়, hello.html নামে নতুন ফাইল তৈরি করুন। আপনি যেকোনো HTML ইনপুট করতে পারেন; Listing 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 ডকুমেন্ট। একটি রিকোয়েস্ট পেলে সার্ভার থেকে এটি রিটার্ন করার জন্য, আমরা Listing 21-5-এ দেখানো 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
যোগ করেছি যাতে স্ট্যান্ডার্ড লাইব্রেরির ফাইল সিস্টেম মডিউলটিকে স্কোপে আনা যায়। একটি ফাইলের বিষয়বস্তু একটি স্ট্রিং-এ পড়ার কোডটি পরিচিত হওয়া উচিত; Listing 12-4-এ আমরা আমাদের 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 ফাইলটি ফেরত পাঠাতে চাই।
রিকোয়েস্ট ভ্যালিডেট করা এবং বেছে বেছে রেসপন্স করা (Validating the Request and Selectively Responding)
এই মুহূর্তে, আমাদের ওয়েব সার্ভার ক্লায়েন্ট যাই রিকোয়েস্ট করুক না কেন ফাইলের HTML রিটার্ন করবে। আসুন ব্রাউজারটি / রিকোয়েস্ট করছে কিনা তা পরীক্ষা করার জন্য কার্যকারিতা যুক্ত করি এবং HTML ফাইলটি রিটার্ন করার আগে এবং ব্রাউজার অন্য কিছু রিকোয়েস্ট করলে একটি error রিটার্ন করি। এর জন্য আমাদের handle_connection
পরিবর্তন করতে হবে, যেমনটি Listing 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
হ্যান্ডেল করে এবং Listing 21-2-এ যোগ করা 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, তাহলে আপনি Listing 21-1 এবং Listing 21-2-এর কোড চালানোর সময় যে কানেকশন error গুলি দেখেছিলেন সেরকম একটি error পাবেন।
এখন আসুন Listing 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
সহ একটি স্ট্যাটাস লাইন রয়েছে। রেসপন্সের বডি হবে 404.html ফাইলের HTML। আপনাকে error পেজের জন্য hello.html-এর পাশে একটি 404.html ফাইল তৈরি করতে হবে; আবার আপনার ইচ্ছামতো যেকোনো HTML ব্যবহার করতে পারেন বা Listing 21-8-এর উদাহরণ 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 থেকে error HTML রিটার্ন করা উচিত।
রিফ্যাক্টরিং-এর একটি ছোঁয়া (A Touch of Refactoring)
এই মুহূর্তে, if
এবং else
ব্লকগুলিতে অনেক পুনরাবৃত্তি রয়েছে: উভয়ই ফাইল পড়ছে এবং ফাইলের কনটেন্টগুলি স্ট্রীমে লিখছে। শুধুমাত্র পার্থক্য হল স্ট্যাটাস লাইন এবং ফাইলের নাম। আসুন সেই পার্থক্যগুলিকে আলাদা if
এবং else
লাইনে বের করে কোডটিকে আরও সংক্ষিপ্ত করি যা স্ট্যাটাস লাইন এবং ফাইলের নামের মানগুলিকে ভেরিয়েবলে অ্যাসাইন করবে; তারপর আমরা ফাইলটি পড়তে এবং রেসপন্স লিখতে কোডে শর্তহীনভাবে সেই ভেরিয়েবলগুলি ব্যবহার করতে পারি। Listing 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
ব্লকগুলি শুধুমাত্র একটি টাপলে স্ট্যাটাস লাইন এবং ফাইলের নামের উপযুক্ত মানগুলি রিটার্ন করে; তারপর আমরা Chapter 19-এ আলোচনা করা let
স্টেটমেন্টের একটি প্যাটার্ন ব্যবহার করে এই দুটি মানকে status_line
এবং filename
-এ অ্যাসাইন করতে ডিস্ট্রাকচারিং ব্যবহার করি।
পূর্বে ডুপ্লিকেট করা কোডটি এখন if
এবং else
ব্লকের বাইরে এবং status_line
এবং filename
ভেরিয়েবল ব্যবহার করে। এটি দুটি ক্ষেত্রের মধ্যে পার্থক্য দেখা সহজ করে তোলে এবং এর মানে হল যে আমরা যদি ফাইল রিডিং এবং রেসপন্স রাইটিং কীভাবে কাজ করে তা পরিবর্তন করতে চাই তবে আমাদের কোড আপডেট করার জন্য শুধুমাত্র একটি জায়গা রয়েছে। Listing 21-9-এর কোডের আচরণ Listing 21-7-এর মতোই হবে।
অসাধারণ! আমাদের কাছে এখন প্রায় 40 লাইনের Rust কোডে একটি সহজ ওয়েব সার্ভার রয়েছে যা একটি কনটেন্টের পেজ সহ একটি রিকোয়েস্টের রেসপন্স করে এবং অন্য সমস্ত রিকোয়েস্টের জন্য একটি 404 রেসপন্স করে।
বর্তমানে, আমাদের সার্ভার একটি একক থ্রেডে চলে, অর্থাৎ এটি একবারে শুধুমাত্র একটি রিকোয়েস্ট পরিবেশন করতে পারে। আসুন কিছু স্লো রিকোয়েস্ট সিমুলেট করে পরীক্ষা করি কিভাবে এটি একটি সমস্যা হতে পারে। তারপর আমরা এটিকে ঠিক করব যাতে আমাদের সার্ভার একবারে একাধিক রিকোয়েস্ট হ্যান্ডেল করতে পারে।