কম্পিউটার টিউটোরিয়াল

পর্দার পিছনে: রুবি HTTP সার্ভার কীভাবে রেইলস অ্যাপ্লিকেশন এক্সিকিউট করে

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

রুবি ম্যাজিকের একটি আগের এডিশনে আমরা ৩০-লাইন HTTP সার্ভার রুবিতে ইমপ্লিমেন্ট করেছিলাম। অনেক কোড লিখতে না পারেও, আমরা HTTP GET রিকোয়েস্ট হ্যান্ডল করতে পারলাম এবং একটি সিম্পল Rack অ্যাপ্লিকেশন সার্ভ করতে পারলাম। এইবার, আমরা আমাদের হোম মেড সার্ভারকে একটু আরো এগিয়ে নেব। যখন আমরা শেষ করব, আমাদের একটি ওয়েব সার্ভার থাকবে যা রেইলসের বিখ্যাত পনেরো মিনিট ব্লগ সার্ভ করতে পারবে যেটি আপনাকে পোস্ট তৈরি, আপডেট এবং ডিলিট করতে দেবে।

আমরা কোথায় ছেড়েছিলাম

গতবার, আমরা শুধু যতটা সার্ভার ইমপ্লিমেন্ট করেছিলাম সেটি Rack::Lobster সার্ভ করতে পারত উদাহরণ অ্যাপ্লিকেশন হিসেবে।

  1. আমাদের ইমপ্লিমেন্টেশন একটি TCP সার্ভার খুলেছিল এবং একটি রিকোয়েস্ট আসার জন্য অপেক্ষা করত।
  2. যখন সেটি হতো, রিকোয়েস্ট-লাইন (GET /?flip=left HTTP/1.1\r\n) পার্স করা হতো রিকোয়েস্ট মেথড (GET), পাথ (/), এবং কোয়ারী প্যারামিটার (flip=left) পেতে।
  3. রিকোয়েস্ট মেথড, পাথ এবং কোয়ারী স্ট্রিং Rack অ্যাপে পাস করা হতো, যেটি একটি ট্রিপলেট রিটার্ন করত একটি স্ট্যাটাস, কিছু রেসপন্স হেডার এবং রেসপন্স বডি।
  4. সেগুলো ব্যবহার করে, আমরা একটি HTTP রেসপন্স বিল্ড করতে পারতাম ব্রাউজারের কাছে পাঠাতে, নতুন রিকোয়েস্ট আসার জন্য অপেক্ষা করার আগে কানেকশন ক্লোজ করে।
# http_server.rb
require 'socket'
require 'rack'
require 'rack/lobster'
app = Rack::Lobster.new
server = TCPServer.new 5678
#1
while session = server.accept
  request = session.gets
  puts request
  #2
  method, full_path = request.split(' ')
  path, query = full_path.split('?')
  #3
  status, headers, body = app.call({
    'REQUEST_METHOD' => method,
    'PATH_INFO' => path,
    'QUERY_STRING' => query
  })
  #4
  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 এবং Rails

রুবি ফ্রেমওয়ার্ক যেমন রেইলস এবং সিনাত্রা Rack ইন্টারফেসের ওপর বিল্ট। ঠিক যেমন Rack::Lobster-এর ইনস্ট্যান্স আমরা আমাদের সার্ভার টেস্ট করার জন্য ব্যবহার করছি, রেইলসের Rails.application একটি Rack অ্যাপ্লিকেশন অবজেক্ট। থিওরেটিক্যালি, এর মানে আমাদের সার্ভার ইতিমধ্যে একটি রেইলস অ্যাপ্লিকেশন সার্ভ করতে পারতে হবে।

সেটি টেস্ট করতে, আমি একটি সিম্পল রেইলস অ্যাপ্লিকেশন প্রস্তুত করেছি। চলুন সেটি আমাদের সার্ভারের সাথে একই ডিরেক্টরিতে ক্লোন করি।

$ ls
http_server.rb
$ git clone https://github.com/jeffkreeftmeijer/wups.git blog
Cloning into 'blog'...
remote: Counting objects: 162, done.
remote: Compressing objects: 100% (112/112), done.
remote: Total 162 (delta 32), reused 162 (delta 32), pack-reused 0
Receiving objects: 100% (162/162), 29.09 KiB | 0 bytes/s, done.
Resolving deltas: 100% (32/32), done.
Checking connectivity... done.
$ ls
blog http_server.rb

তারপর, আমাদের সার্ভারে, rack এবং rack/lobster-এর বদলে রেইলস অ্যাপ্লিকেশনের এনভায়রনমেন্ট ফাইল রিকোয়ার করুন, এবং Rails.application-কে app ভেরিয়েবলে রাখুন Rack::Lobster.new-এর বদলে।

# http_server.rb
require 'socket'
require_relative 'blog/config/environment'
app = Rails.application
server = TCPServer.new 5678
# ...

সার্ভার শুরু করলে (ruby http_server.rb) এবং https://localhost:5678 খুললে দেখতে পাবো আমরা এখনো সেখানে নই। সার্ভার ক্র্যাশ করছে না, কিন্তু ব্রাউজারে আমরা একটি ইন্টারন্যাল সার্ভার এরর দেখছি।

আমাদের সার্ভারের লগ চেক করলে, আমরা দেখতে পাই আমাদের কিছু কলড rack.input মিসিং। লাগছে গতবার আমরা সার্ভার ইমপ্লিমেন্ট করার সময় অলস ছিলাম, তাই এই রেইলস অ্যাপ্লিকেশন কাজ করার আগে আরও কিছু কাজ বাকি।

$ ruby http_server.rb
GET / HTTP/1.1
Error during failsafe response: Missing rack.input
...
http_server.rb:15:in `
'

Rack এনভায়রনমেন্ট

গতবার আমরা আমাদের সার্ভার ইমপ্লিমেন্ট করার সময়, Rack এনভায়রনমেন্টের ওপর গ্লস ওভার করলাম এবং প্রপারলি Rack অ্যাপ্লিকেশন সার্ভ করতে যা ভেরিয়েবল দরকার সেগুলোর অধিকাংশ ইগনোর করলাম। আমরা শেষে শুধু REQUEST_METHOD, PATH_INFO, এবং QUERY_STRING ভেরিয়েবল ইমপ্লিমেন্ট করেছিলাম, যেগুলো আমাদের সিম্পল Rack অ্যাপের জন্য পর্যাপ্ত ছিল।

যেমন আমরা ইতিমধ্যে দেখেছি এক্সেপশন থেকে যখন আমরা আমাদের নতুন অ্যাপ্লিকেশন শুরু করার চেষ্টা করলাম, রেইলসকে rack.input দরকার, যেটি র HTTP POST ডেটার জন্য একটি ইনপুট স্ট্রিম হিসেবে ব্যবহৃত হয়। সেটির পাশাপাশি, আরও কিছু ভেরিয়েবল দরকার যা আমাদের পাস করতে হবে, যেমন সার্ভারের পোর্ট নম্বর, এবং রিকোয়েস্ট কুকি ডেটা।

সৌভাগ্যক্রমে, Rack Rack::Lint প্রদান করে যাতে Rack এনভায়রনমেন্টের সব ভেরিয়েবল প্রেজেন্ট এবং ভ্যালিড কিনা সেটা নিশ্চিত করতে সাহায্য করে। আমরা আমাদের সার্ভার টেস্ট করতে এটা ব্যবহার করতে পারি আমাদের রেইলস অ্যাপকে এতে র‍্যাপ করে Rack::Lint.new কল করে এবং Rails.application পাস করে।

# http_server.rb
require 'socket'
require_relative 'blog/config/environment'
app = Rack::Lint.new(Rails.application)
server = TCPServer.new 5678
# ...

Rack::Lint এক্সেপশন থ্রো করবে যখন এনভায়রনমেন্টে একটি ভেরিয়েবল মিসিং বা ইনভ্যালিড। এখন, আমাদের সার্ভার আবার শুরু করলে এবং https://localhost:5678 খুললে সার্ভার ক্র্যাশ করবে এবং Rack::Lint আমাদের প্রথম এরর নোটিফাই করবে: SERVER_NAME ভেরিয়েবল সেট করা হয়নি।

~/Appsignal/http-server (master) $ ruby http_server.rb
GET / HTTP/1.1
/Users/jeff/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-2.0.1/lib/rack/lint.rb:20:in `assert': env missing required key SERVER_NAME (Rack::Lint::LintError)
...
from http_server.rb:15:in `
'

প্রতিটি এরর যা আমাদের থ্রো করা হচ্ছে সেগুলো ফিক্স করে, আমরা ভেরিয়েবল যোগ করতে থাকব যতক্ষণ না Rack::Lint আমাদের সার্ভার ক্র্যাশ করা বন্ধ করে। চলুন Rack::Lint যা যা ভেরিয়েবল রিকোয়ার করে সেগুলো দেখি।

  • SERVER_NAME: সার্ভারের হোস্টনেম। আমরা এখনো শুধু লোকালি এই সার্ভার রান করছি, তাই আমরা "localhost" ব্যবহার করব।
  • SERVER_PORT: আমাদের সার্ভার যেই পোর্টে রান করছে। আমরা পোর্ট নম্বর হার্ডকোড করেছি (৫৬৭৮), তাই আমরা সেটি রেক এনভায়রনমেন্টে পাস করব।
  • rack.version: টার্গেটেড Rack প্রোটোকল ভার্সন নম্বর ইন্টিজার অ্যারে হিসেবে। লেখার সময়ে [1,3]।
  • rack.input: কাঁচা HTTP POST ডেটা ধারণকারী ইনপুট স্ট্রিম। আমরা পরে এটার কাছে যাব, কিন্তু এখন একটি খালি StringIO ইনস্ট্যান্স পাস করব (ASCII-8BIT এনকোডিংসহ)।
  • rack.errors: Rack::Logger লিখতে যেই এরর স্ট্রিম। আমরা $stderr ব্যবহার করছি।
  • rack.multithread: আমাদের সার্ভার সিঙ্গেল-থ্রেডেড, তাই এটা false সেট করা যাবে।
  • rack.multiprocess: আমাদের সার্ভার একটি সিঙ্গেল প্রসেসে রান করছে, তাই এটাও false সেট করা যাবে।
  • rack.run_once: আমাদের সার্ভার এক প্রসেসে একাধিক সিকোয়েনশিয়াল রিকোয়েস্ট হ্যান্ডেল করতে পারে, তাই এটাও false।
  • rack.url_scheme: SSL সাপোর্ট নেই, তাই এটা "http" সেট করা যাবে "https"-এর বদলে।

সব মিসিং ভেরিয়েবল যোগ করার পর, Rack::Lint আমাদের এনভায়রনমেন্টে একটা আরো সমস্যা নোটিফাই করবে।

$ ruby http_server.rb
GET / HTTP/1.1
/Users/jeff/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-2.0.1/lib/rack/lint.rb:20:in `assert': env variable QUERY_STRING has non-string value nil (Rack::Lint::LintError)
...
from http_server.rb:18:in `
'

যখন রিকোয়েস্টে কোনো কোয়ারী স্ট্রিং নেই, আমরা এখন nil পাস করছি QUERY_STRING হিসেবে, যেটি এলাউড নয়। সেটি কেসে, Rack একটি খালি স্ট্রিং এক্সপেক্ট করে। মিসিং ভেরিয়েবল ইমপ্লিমেন্ট করার পর এবং কোয়ারী স্ট্রিং আপডেট করার পর, আমাদের এনভায়রনমেন্ট এভাবে দেখে:

# http_server.rb
# ...
method, full_path = request.split(' ')
path, query = full_path.split('?')
input = StringIO.new
input.set_encoding 'ASCII-8BIT'
status, headers, body = app.call({
  'REQUEST_METHOD' => method,
  'PATH_INFO' => path,
  'QUERY_STRING' => query || '',
  'SERVER_NAME' => 'localhost',
  'SERVER_PORT' => '5678',
  'rack.version' => [1,3],
  'rack.input' => input,
  'rack.errors' => $stderr,
  'rack.multithread' => false,
  'rack.multiprocess' => false,
  'rack.run_once' => false,
  'rack.url_scheme' => 'http'
})
session.print "HTTP/1.1 #{status}\r\n"
# ...

সার্ভার রিস্টার্ট করলে এবং https://localhost:5678 ভিজিট করলে, আমরা রেইলসের "You're on Rails!"-পেজ देखकर স্বাগত হবে, মানে আমরা আমাদের হোম মেড সার্ভারে একটি আসল রেইলস অ্যাপ্লিকেশন রান করছি!

HTTP POST বডি পার্স করা

এই অ্যাপ্লিকেশন সেটি ইনডেক্স পেজের চেয়ে বেশি। https://localhost:5678/posts ভিজিট করলে একটি খালি পোস্ট লিস্ট দেখাবে। যদি আমরা নতুন পোস্ট ফর্ম ফিল আপ করে "Create post" চাপি, আমরা একটি ActionController::InvalidAuthenticityToken এক্সেপশন দ্বারা স্বাগত হবে।

অথেন্সিটি টোকেন ফর্ম পোস্ট করার সময় পাঠানো হয় এবং ব্যবহার হয় চেক করতে রিকোয়েস্ট একটি ট্রাস্টেড সোর্স থেকে এসেছে কিনা। আমাদের সার্ভার এখনো পোস্ট ডেটা সম্পূর্ণ ইগনোর করছে, তাই টোকেন পাঠানো হয়নি, এবং রিকোয়েস্ট ভেরিফাই করা যায় না।

গতবার আমরা প্রথম আমাদের HTTP সার্ভার ইমপ্লিমেন্ট করেছিলাম, আমরা session.gets ব্যবহার করেছিলাম ফাস্ট লাইন (যাকে Request-Line বলা হয়) পেতে, এবং সেখান থেকে HTTP মেথড এবং পাথ পার্স করলাম। রিকোয়েস্টের বাকি অংশ ইগনোর করলাম।

POST ডেটা এক্সট্রাক্ট করতে পারতে, আমরা প্রথম বুঝতে হবে কীভাবে একটি HTTP রিকোয়েস্ট স্ট্রাকচার্ড। একটি উদাহরণ দেখলে, আমরা দেখতে পাই স্ট্রাকচারটি একটি HTTP রেসপন্সের সাথে সাদৃশ্য রেখে:

POST /posts HTTP/1.1\r\n
Host: localhost:5678\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
Accept-Encoding: gzip, deflate\r\n
Accept-Language: en-us\r\n
Content-Type: application/x-www-form-urlencoded\r\n
Origin: https://localhost:5678\r\n
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/602.2.14 (KHTML, like Gecko) Version/10.0.1 Safari/602.2.14\r\n
Cookie: _wups_session=LzE0Z2hSZFNseG5TR3dEVEwzNE52U0lFa0pmVGlQZGtZR3AveWlyMEFvUHRPeXlQUzQ4L0xlKzNLVWtqYld2cjdiWkpmclZIaEhJd1R6eDhaZThFbVBlN2p6QWpJdllHL2F4Z3VseUZ6NU1BRTU5Y1crM2lLRVY0UzdSZkpwYkt2SGFLZUQrYVFvaFE0VjZmZlIrNk5BPT0tLUpLTHQvRHQ0T3FycWV0ZFZhVHZWZkE9PQ%3D%3D--4ef4508c936004db748da10be58731049fa190ee\r\n
Connection: keep-alive\r\n
Upgrade-Insecure-Requests: 1\r\n
Referer: https://localhost:5678/posts/new\r\n
Content-Length: 369\r\n
\r\n
utf8=%E2%9C%93&authenticity_token=3fu7e8v70K0h9o%2FGNiXxaXSVg3nZ%2FuoL60nlhssUEHpQRz%2BM4ZIHjQduQMexvXrNoC2pjmhNPI4xNNA0Qkh5Lg%3D%3D&post%5Btitle%5D=My+first+post&post%5Bcreated_at%281i%29%5D=2017&post%5Bcreated_at%282i%29%5D=1&post%5Bcreated_at%283i%29%5D=23&post%5Bcreated_at%284i%29%5D=18&post%5Bcreated_at%285i%29%5D=47&post%5Bbody%5D=It+works%21&commit=Create+Post

রেসপন্সের মতো, একটি HTTP রিকোয়েস্ট ধারণ করে:

  • একটি Request-Line (POST /posts HTTP/1.1\r\n), যেটি ধারণ করে একটি মেথড টোকেন (POST), একটি রিকোয়েস্ট URI (/posts/), এবং HTTP ভার্সন (HTTP/1.1), একটি CRLF (একটি ক্যারিজ রিটার্ন: \r, লাইন ফিড অনুসরণ: \n) দিয়ে টার্মিনেটেড লাইন এন্ড ইন্ডিকেট করতে
  • হেডার লাইন (Host: localhost:5678\r\n)। হেডার কী, একটি কোলন, তারপর ভ্যালু, এবং একটি CRLF।
  • একটি নিউলাইন (বা ডবল CRLF) রিকোয়েস্ট লাইন এবং হেডারকে বডি থেকে আলাদা করতে: (\r\n\r\n)
  • URL এনকোডেড POST বডি

session.gets ব্যবহার করে রিকোয়েস্টের প্রথম লাইন (Request-Line) নেওয়ার পর, আমাদের কিছু হেডার লাইন এবং একটি বডি থাকে। হেডার লাইনগুলো পেতে, আমাদের সেশন থেকে লাইন রিট্রিভ করতে হবে যতক্ষণ না আমরা একটি নিউলাইন পাই (\r\n)।

প্রতিটি হেডার লাইনের জন্য, আমরা প্রথম কোলনে স্প্লিট করব। কোলনের আগে যা সেটি কী, এবং পরে যা সেটি ভ্যালু। আমরা ভ্যালু #strip করব নিউলাইন শেষ থেকে রিমুভ করতে।

রিকোয়েস্ট থেকে বডি পেতে আমাদের কত বাইট পড়তে হবে সেটা জানতে, আমরা "Content-Length" হেডার ব্যবহার করব, যেটি ব্রাউজার অটোমেটিক্যালি ইনক্লুড করে রিকোয়েস্ট পাঠাতে।

# http_server.rb
# ...
headers = {}
while (line = session.gets) != "\r\n"
  key, value = line.split(':', 2)
  headers[key] = value.strip
end
body = session.read(headers["Content-Length"].to_i)
# ...

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

# http_server.rb
# ...
status, headers, body = app.call({
  # ...
  'REMOTE_ADDR' => '127.0.0.1',
  'HTTP_COOKIE' => headers['Cookie'],
  'rack.version' => [1,3],
  'rack.input' => StringIO.new(body),
  'rack.errors' => $stderr,
  # ...
})
# ...

আর আছে। সার্ভার রিস্টার্ট করলে এবং আবার ফর্ম সাবমিট করলে, আপনি দেখব আমরা সফলভাবে আমাদের ব্লগে প্রথম পোস্ট তৈরি করেছি!

এইবার আমরা সিরিয়াসলি আমাদের ওয়েব সার্ভার আপগ্রেড করেছি। শুধু GET রিকোয়েস্ট এক্সেপ্ট করার বদলে একটি Rack অ্যাপ থেকে, আমরা এখন একটি কমপ্লিট রেইলস অ্যাপ সার্ভ করছি যা POST রিকোয়েস্ট হ্যান্ডল করে। এবং আমরা এখনো মোট পঞ্চাশ লাইনের চেয়ে বেশি কোড লিখিনি!

আপনি যদি আমাদের নতুন এবং উন্নত সার্ভারের সাথে এক্সপেরিমেন্ট করতে চান, এখানে কোডটি। @AppSignal-এ আমাদের জানান আপনি আরও জানতে চান, বা কোনো স্পেসিফিক প্রশ্ন আছে।

  1. Mac-এ Windows অ্যাপ্লিকেশন চালান: সহজ, দ্রুত ও নিরাপদ পদ্ধতি

  2. পিসিতে আইওএস অ্যাপ চালান: ধাপে ধাপে গাইড

  3. macOS-এ অনভেরিফায়েড অ্যাপ সফলভাবে ইনস্টল করার উপায়

  4. উইন্ডোজ ১১-এ অ্যান্ড্রয়েড অ্যাপ চালান: ধাপে ধাপে গাইড