ওয়েব সার্ভার, এবং HTTP সাধারণত বুঝতে কঠিন লাগতে পারে। ব্রাউজার কীভাবে একটি রিকোয়েস্ট ফরম্যাট করে, এবং রেসপন্স কীভাবে ইউজারের কাছে পাঠানো হয়? এই রুবি ম্যাজিক এপিসোডে আমরা শিখব কীভাবে একটি রুবি HTTP সার্ভার ৩০ লাইন কোডে বানাতে হয়। যখন আমরা শেষ করব, আমাদের সার্ভার HTTP GET রিকোয়েস্ট হ্যান্ডল করবে এবং আমরা সেটি একটি Rack অ্যাপ সার্ভ করতে ব্যবহার করব।
HTTP এবং TCP কীভাবে একসাথে কাজ করে
TCP একটি ট্রান্সপোর্ট প্রোটোকল যা বর্ণনা করে কীভাবে একটি সার্ভার এবং একটি ক্লায়েন্ট ডেটা এক্সচেঞ্জ করে।
HTTP একটি রিকোয়েস্ট-রেসপন্স প্রোটোকল যা স্পেসিফিক্যালি বর্ণনা করে কীভাবে ওয়েব সার্ভার HTTP ক্লায়েন্ট বা ওয়েব ব্রাউজারের সাথে ডেটা এক্সচেঞ্জ করে। HTTP সাধারণত TCP ব্যবহার করে তার ট্রান্সপোর্ট প্রোটোকল হিসেবে। সारत所周知, একটি HTTP সার্ভার হলো একটি TCP সার্ভার যা "বোলতে" পারে HTTP।
# tcp_server.rb
require 'socket'
server = TCPServer.new 5678
while session = server.accept
session.puts "Hello world! The time is #{Time.now}"
session.close
end
এই TCP সার্ভার উদাহরণে, সার্ভার পোর্ট 5678-এ বাইন্ড করে এবং একটি ক্লায়েন্ট কানেক্ট হওয়ার জন্য অপেক্ষা করে। যখন সেটি হয়, সেটি ক্লায়েন্টকে একটি মেসেজ পাঠায়, এবং তারপর কানেকশন ক্লোজ করে। প্রথম ক্লায়েন্টের সাথে কথা বলা শেষ হলে, সার্ভার অন্য একটি ক্লায়েন্ট কানেক্ট হওয়ার জন্য অপেক্ষা করে আবার তার মেসেজ পাঠাতে।
# tcp_client.rb
require 'socket'
server = TCPSocket.new 'localhost', 5678
while line = server.gets
puts line
end
server.close
আমাদের সার্ভারের সাথে কানেক্ট করতে, আমাদের একটি TCP ক্লায়েন্ট দরকার। এই উদাহরণ ক্লায়েন্ট একই পোর্ট (5678)-এ কানেক্ট করে এবং server.gets ব্যবহার করে সার্ভার থেকে ডেটা রিসিভ করে, যেটি তারপর প্রিন্ট হয়। যখন সেটি ডেটা রিসিভ করা বন্ধ করে, সেটি সার্ভারের সাথে কানেকশন ক্লোজ করে এবং প্রোগ্রাম এক্সিট করে।
যখন সার্ভার সার্ভার রান করছে ($ ruby tcp_server.rb), আপনি একটি আলাদা ট্যাবে ক্লায়েন্ট শুরু করতে পারেন সার্ভারের মেসেজ রিসিভ করতে।
$ ruby tcp_client.rb
Hello world! The time is 2016-11-23 15:17:11 +0100
$
একটু কাল্পনার সাথে, আমাদের TCP সার্ভার এবং ক্লায়েন্ট কিছুটা ওয়েব সার্ভার এবং ব্রাউজারের মতো কাজ করে। ক্লায়েন্ট একটি রিকোয়েস্ট পাঠায়, সার্ভার রেসপন্ড করে, এবং কানেকশন ক্লোজ হয়। এটাই হলো রিকোয়েস্ট-রেসপন্স প্যাটার্ন যা কাজ করে, যা ঠিক আমাদের একটি HTTP সার্ভার বানাতে দরকার।
আসল অংশে যাওয়ার আগে, চলুন দেখি HTTP রিকোয়েস্ট এবং রেসপন্স কীভাবে দেখে।
একটি বেসিক HTTP GET রিকোয়েস্ট
সবচেয়ে বেসিক HTTP GET রিকোয়েস্ট হলো একটি রিকোয়েস্ট-লাইন কোনো অ্যাডিশনাল হেডার বা রিকোয়েস্ট বডি ছাড়া।
GET / HTTP/1.1\r\n
Request-Line চারটি অংশ নিয়ে গঠিত:
- একটি মেথড টোকেন (
GET, এই উদাহরণে) - The Request-URI (
/) - প্রোটোকল ভার্সন (
HTTP/1.1) - একটি CRLF (একটি ক্যারিজ রিটার্ন:
\r, লাইন ফিড অনুসরণ:\n) লাইন এন্ড ইন্ডিকেট করতে
সার্ভার একটি HTTP রেসপন্স দিয়ে রেসপন্ড করবে, যেটি এমন দেখতে পারে:
HTTP/1.1 200\r\nContent-Type: text/html\r\n\r\n\Hello world!
এই রেসপন্স ধারণ করে:
- একটি স্ট্যাটাস লাইন: প্রোটোকল ভার্সন ("HTTP/1.1"), একটি স্পেস, রেসপন্সের স্ট্যাটাস কোড ("200"), এবং একটি CRLF (
\r\n) দিয়ে টার্মিনেটেড - অপশনাল হেডার লাইন। এই কেসে, শুধু একটি হেডার লাইন আছে ("Content-Type: text/html"), কিন্তু একাধিক হতে পারে (CRLF দিয়ে সেপারেটেড:
\r\n) - একটি নিউলাইন (বা ডবল CRLF) স্ট্যাটাস লাইন এবং হেডারকে বডি থেকে আলাদা করতে: (
\r\n\r\n) - বডি: "Hello world!"
একটি মিনিমাল রুবি HTTP সার্ভার
पर্যাপ্ত কথা। এখন যেহেতু আমরা জানি কীভাবে রুবিতে একটি TCP সার্ভার বানাতে হয় এবং কিছু HTTP রিকোয়েস্ট এবং রেসপন্স কীভাবে দেখে, আমরা একটি মিনিমাল HTTP সার্ভার বানাতে পারি। আপনি লক্ষ্য করবেন ওয়েব সার্ভারটা প্রায় একই দেখছে TCP সার্ভারের মতো যা আমরা আগে আলোচনা করেছিলাম। জেনারেল আইডিয়া একই, আমরা শুধু HTTP প্রোটোকল ব্যবহার করছি আমাদের মেসেজ ফরম্যাট করার জন্য। এছাড়া, যেহেতু আমরা একটি ব্রাউজার ব্যবহার করব রিকোয়েস্ট পাঠাতে এবং রেসপন্স পার্স করতে, আমাদের এইবার ক্লায়েন্ট ইমপ্লিমেন্ট করতে হবে না।
# http_server.rb
require 'socket'
server = TCPServer.new 5678
while session = server.accept
request = session.gets
puts request
session.print "HTTP/1.1 200\r\n" # 1
session.print "Content-Type: text/html\r\n" # 2
session.print "\r\n" # 3
session.print "Hello world! The time is #{Time.now}" #4
session.close
end
সার্ভার একটি রিকোয়েস্ট রিসিভ করার পর, আগের মতো, এটা session.print ব্যবহার করে ক্লায়েন্টকে একটি মেসেজ পাঠায়: শুধু আমাদের মেসেজের বদলে, এটা রেসপন্সকে প্রিফিক্স করে একটি স্ট্যাটাস লাইন, একটি হেডার এবং একটি নিউলাইন দিয়ে:
- স্ট্যাটাস লাইন (
HTTP 1.1 200\r\n) ব্রাউজারকে বলতে HTTP ভার্সন 1.1 এবং রেসপন্স কোড "200" - একটি হেডার ইন্ডিকেট করতে রেসপন্সের text/html কন্টেন্ট টাইপ (
Content-Type: text/html\r\n) - নিউলাইন (
\r\n) - বডি: "Hello world! …"
আগের মতো, মেসেজ পাঠানোর পর সেটি কানেকশন ক্লোজ করে। আমরা রিকোয়েস্ট পড়ছি না এখনো, তাই সেটি শুধু কনসোলে প্রিন্ট করছে।
আপনি সার্ভার শুরু করলে এবং https://localhost:5678 আপনার ব্রাউজারে খুললে, আপনি "Hello world! …"-লাইনটি কারেন্ট টাইমসহ দেখতে পাবেন, যেমন আমরা TCP ক্লায়েন্ট থেকে আগে পেয়েছিলাম। 🎉
একটি Rack অ্যাপ সার্ভ করা
এখন পর্যন্ত, আমাদের সার্ভার প্রতিটি রিকোয়েস্টের জন্য একটি সিঙ্গেল রেসপন্স রিটার্ন করছিল। এটাকে একটু আরো উপকারী করতে, আমরা আমাদের সার্ভারে আরও রেসপন্স যোগ করতে পারি। সেগুলো সার্ভারে সরাসরি যোগ করার বদলে, আমরা একটি Rack অ্যাপ ব্যবহার করব। আমাদের সার্ভার HTTP রিকোয়েস্ট পার্স করবে এবং সেগুলো Rack অ্যাপে পাস করবে, যেটি তারপর সার্ভারের জন্য একটি রেসপন্স রিটার্ন করবে ক্লায়েন্টের কাছে পাঠাতে।
Rack হলো রুবি সাপোর্ট করা ওয়েব সার্ভারের মাঝপথে এবং অধিকাংশ রুবি ওয়েব ফ্রেমওয়ার্ক যেমন রেইলস এবং সিনাত্রার ইন্টারফেস। এর সিম্পলেস্ট ফর্মে, একটি Rack অ্যাপ হলো একটি অবজেক্ট যা call-এ রেসপন্ড করে এবং একটি "টিপলেট" রিটার্ন করে, তিনটি আইটেমের একটি অ্যারে: একটি HTTP রেসপন্স কোড, HTTP হেডাররের একটি হ্যাশ এবং একটি বডি।
app = Proc.new do |env|
['200', {'Content-Type' => 'text/html'}, ["Hello world! The time is #{Time.now}"]]
end
এই উদাহরণে, রেসপন্স কোড "200", আমরা "text/html" কন্টেন্ট টাইপ হেডার হিসেবে পাস করছি, এবং বডি একটি স্ট্রিং সহ একটি অ্যারে।
আমাদের সার্ভারকে এই অ্যাপ থেকে রেসপন্স সার্ভ করতে দিতে, আমাদের রিটার্নড ট্রিপলেটকে একটি HTTP রেসপন্স স্ট্রিং-এ টার্ন করতে হবে। সবসময় একটি স্ট্যাটিক রেসপন্স রিটার্ন করার বদলে, যেটি আমরা আগে করতাম, আমরা এখন রেক অ্যাপ tarafından রিটার্নড ট্রিপলেট থেকে রেসপন্স বিল্ড করব।
# http_server.rb
require 'socket'
app = Proc.new do
['200', {'Content-Type' => 'text/html'}, ["Hello world! The time is #{Time.now}"]]
end
server = TCPServer.new 5678
while session = server.accept
request = session.gets
puts request
# 1
status, headers, body = app.call({})
# 2
session.print "HTTP/1.1 #{status}\r\n"
# 3
headers.each do |key, value|
session.print "#{key}: #{value}\r\n"
end
# 4
session.print "\r\n"
# 5
body.each do |part|
session.print part
end
session.close
end
Rack অ্যাপ থেকে প্রাপ্ত রেসপন্স সার্ভ করতে, আমাদের সার্ভারে কিছু পরিবর্তন আনব:
app.callদ্বারা রিটার্নড ট্রিপলেট থেকে স্ট্যাটাস কোড, হেডার, এবং বডি পান।- স্ট্যাটাস কোড ব্যবহার করে স্ট্যাটাস লাইন বিল্ড করুন
- হেডারগুলোর ওপর লুপ করুন এবং হ্যাশের প্রতিটি কী-ভ্যালু পেয়ারের জন্য একটি হেডার লাইন যোগ করুন
- স্ট্যাটাস লাইন এবং হেডারকে বডি থেকে আলাদা করতে একটি নিউলাইন প্রিন্ট করুন
- বডির ওপর লুপ করুন এবং প্রতিটি পার্ট প্রিন্ট করুন। যেহেতু আমাদের বডি অ্যারেতে শুধু একটি পার্ট আছে, সেটি সিম্পলি আমাদের "Hello world"-মেসেজ সেশনে প্রিন্ট করবে ক্লোজ করার আগে।
রিকোয়েস্ট পড়া
এখন পর্যন্ত, আমাদের সার্ভার request ভেরিয়েবল ইগনোর করছিল। আমাদের দরকার নয় কারণ আমাদের Rack অ্যাপ সবসময় একই রেসপন্স রিটার্ন করত।
Rack::Lobster একটি উদাহরণ অ্যাপ যা Rack-এর সাথে শিপ করা থাকে এবং ফাংশন করার জন্য রিকোয়েস্ট URL প্যারামিটার ব্যবহার করে। আগে যেই Proc আপনাকে অ্যাপ হিসেবে ব্যবহার করতাম, তার বদলে আমরা সেটি আমাদের টেস্টিং অ্যাপ হিসেবে ব্যবহার করব।
# http_server.rb
require 'socket'
require 'rack'
require 'rack/lobster'
app = Rack::Lobster.new
server = TCPServer.new 5678
while session = server.accept
# ...
ব্রাউজার খুললে এখন একটি লবস্টার দেখাবে আগের বোরিং স্ট্রিং-এর বদলে। লবস্টারিশাস!
"flip!" এবং "crash!" লিংক লিংক করে /?flip=left এবং /?flip=crash রেসপেক্টিভলি। যাইহোক, লিংক ফলো করলে, লবস্টার ফ্লিপ করছে না এবং কিছু ক্র্যাশ করছে না এখনো। এটা কারণ আমাদের সার্ভার কোয়ারী স্ট্রিং হ্যান্ডল করছে না এখনো। যেই request ভেরিয়েবল আমরা আগে ইগনোর করেছিলাম সেটি মনে করুন? আমাদের সার্ভারের লগে তাকালে, আমরা প্রতিটি পেজের জন্য রিকোয়েস্ট স্ট্রিং দেখতে পাব।
GET / HTTP/1.1
GET /?flip=left HTTP/1.1
GET /?flip=crash HTTP/1.1
HTTP রিকোয়েস্ট স্ট্রিং ইনক্লুড করে রিকোয়েস্ট মেথড ("GET"), রিকোয়েস্ট পাথ (/, /?flip=left এবং /?flip=crash), এবং HTTP ভার্সন। আমরা এই ইনফরমেশন ব্যবহার করতে পারি নির্ধারণ করতে আমাদের কী সার্ভ করতে হবে।
# http_server.rb
require 'socket'
require 'rack'
require 'rack/lobster'
app = Rack::Lobster.new
server = TCPServer.new 5678
while session = server.accept
request = session.gets
puts request
# 1
method, full_path = request.split(' ')
# 2
path, query = full_path.split('?')
# 3
status, headers, body = app.call({
'REQUEST_METHOD' => method,
'PATH_INFO' => path,
'QUERY_STRING' => query
})
session.print "HTTP/1.1 #{status}\r\n"
headers.each do |key, value|
session.print "#{key}: #{value}\r\n"
end
session.print "\r\n"
body.each do |part|
session.print part
end
session.close
end
রিকোয়েস্ট পার্স করতে এবং রিকোয়েস্ট প্যারামিটার Rack অ্যাপে পাঠাতে, আমরা রিকোয়েস্ট স্ট্রিং স্প্লিট করব এবং Rack অ্যাপে পাঠাব:
- রিকোয়েস্ট স্ট্রিংকে একটি মেথড এবং একটি ফুল পাথে স্প্লিট করুন
- ফুল পাথে কে একটি পাথ এবং একটি কোয়ারী-এ স্প্লিট করুন
- সেগুলো একটি Rack এনভায়রনমেন্ট হ্যাশে অ্যাপে পাস করুন।
উদাহরণস্বরূপ, GET /?flip=left HTTP/1.1\r\n এই রিকোয়েস্টকে অ্যাপে এভাবে পাস করা হবে:
{
'REQUEST_METHOD' => 'GET',
'PATH_INFO' => '/',
'QUERY_STRING' => '?flip=left'
}
আমাদের সার্ভার রিস্টার্ট করলে, https://localhost:5678 ভিজিট করলে, এবং "flip!"-লিংক ক্লিক করলে, এখন লবস্টার ফ্লিপ করবে, এবং "crash!" লিংক ক্লিক করলে আমাদের ওয়েব সার্ভার ক্র্যাশ করবে।
আমরা HTTP সার্ভার ইমপ্লিমেন্ট করার সারফেস শুধু স্ক্র্যাচ করেছি, এবং আমাদের সার্ভার শুধু ৩০ লাইন কোড, কিন্তু এটা বেসিক আইডিয়া এক্সপ্লেন করে। এটা GET রিকোয়েস্ট এক্সেপ্ট করে, রিকোয়েস্টের অ্যাট্রিবিউটগুলো একটি Rack অ্যাপে পাস করে, এবং ব্রাউজারের কাছে রেসপন্স পাঠায়। যদিও এটা রিকোয়েস্ট স্ট্রিমিং এবং POST রিকোয়েস্ট হ্যান্ডল করে না, আমাদের সার্ভার থিওরেটিক্যালি অন্যান্য Rack অ্যাপও সার্ভ করতে পারে।
রুবিতে HTTP সার্ভার বানানোর আমাদের কুইক লুক এখানেই সমাপ্তি। আপনি যদি আমাদের সার্ভারের সাথে এক্সপেরিমেন্ট করতে চান, এখানে কোডটি। @AppSignal-এ আমাদের জানান আপনি আরও জানতে চান, বা কোনো স্পেসিফিক প্রশ্ন আছে।
আপনি যদি এই আর্টিকেল উপভোগ করেন, Ruby Magic নিউজলেটার সাবস্ক্রাইব করুন: একটি (প্রায়) মাসিক পো(র)শন অফ রুবি।