रूबी मैजिक श्रृंखला में हम सॉफ़्टवेयर को अलग करके यह सीखना पसंद करते हैं कि यह हुड के नीचे कैसे कार्य करता है। यह सब प्रक्रिया के बारे में है; अंतिम परिणाम ऐसा कुछ नहीं है जिसका आप उत्पादन में उपयोग करेंगे, हम रूबी भाषा और इसके लोकप्रिय पुस्तकालयों के आंतरिक कामकाज के बारे में सीखते हैं। हम महीने में लगभग एक बार एक नया लेख प्रकाशित करते हैं, इसलिए यदि आप भी इस तरह की चीज़ों में रुचि रखते हैं तो हमारे न्यूज़लेटर की सदस्यता लेना सुनिश्चित करें।
रूबी मैजिक के पहले के संस्करण में हमने रूबी में 30-लाइन HTTP सर्वर लागू किया था। बिना बहुत सारा कोड लिखे, हम HTTP GET अनुरोधों को संभालने और एक साधारण Rack एप्लिकेशन की सेवा करने में सक्षम थे। इस बार, हम अपने घर के बने सर्वर को थोड़ा आगे ले जाएंगे। जब हम कर लेंगे, तो हमारे पास एक वेब सर्वर होगा जो रेल्स के प्रसिद्ध पंद्रह मिनट के ब्लॉग की सेवा कर सकता है जो आपको पोस्ट बनाने, अपडेट करने और हटाने की अनुमति देता है।
जहां हम छोड़ गए थे
पिछली बार, हमने Rack::Lobster को उदाहरण एप्लिकेशन के रूप में सेवा करने के लिए सर्वर का सिर्फ पर्याप्त कार्यान्वयन किया था।
- हमारे कार्यान्वयन ने एक TCP सर्वर खोला और अनुरोध आने का इंतजार किया।
- जब ऐसा हुआ, तो अनुरोध-लाइन (
GET /?flip=left HTTP/1.1\r\n) को पार्स किया गया ताकि अनुरोध मेथड (GET), पथ (/), और क्वेरी पैरामीटर (flip=left) प्राप्त हो सकें। - अनुरोध मेथड, पथ और क्वेरी स्ट्रिंग को Rack ऐप को पास किया गया, जिसने एक स्थिति, कुछ प्रतिक्रिया हेडर और प्रतिक्रिया बॉडी के साथ एक ट्रिपलेट लौटाया।
- उनका उपयोग करके, हम ब्राउज़र को वापस भेजने के लिए एक 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 और रेल्स
रेल्स और सिनात्रा जैसे रूबी फ्रेमवर्क 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 के बजाय रेल्स एप्लिकेशन की एनवायरनमेंट फाइल को require करें, और Rack::Lobster.new के बजाय Rails.application को app वेरिएबल में रखें।
# 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 `<main>'
Rack एनवायरनमेंट
जब हमने अपना सर्वर लागू किया था, तब हम Rack एनवायरनमेंट पर चमक गए और Rack एप्लिकेशन को ठीक से सेवा करने के लिए आवश्यक अधिकांश वेरिएबल को नजरअंदाज कर दिया। हमने केवल REQUEST_METHOD, PATH_INFO, और QUERY_STRING वेरिएबल को लागू किया, क्योंकि वे हमारे सरल Rack ऐप के लिए पर्याप्त थे।
जैसा कि हमने पहले ही अपवाद से देखा जब हमने अपने नए एप्लिकेशन को शुरू करने का प्रयास किया, रेल्स को rack.input की आवश्यकता होती है, जिसका उपयोग कच्चे HTTP POST डेटा के लिए इनपुट स्ट्रीम के रूप में किया जाता है। इसके अलावा, कुछ और वेरिएबल हैं जिन्हें हमें पास करने की आवश्यकता है, जैसे सर्वर का पोर्ट नंबर, और अनुरोध कुकी डेटा।
सौभाग्य से, Rack सभी वेरिएबल को मौजूद और मान्य सुनिश्चित करने में मदद के लिए Rack::Lint प्रदान करता है। हम अपने रेल्स ऐप को इसमें 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 `<main>'
प्रत्येक त्रुटि को ठीक करके जो हम पर फेंकी जाती है, हम वेरिएबल जोड़ते रह सकते हैं जब तक Rack::Lint हमारे सर्वर को क्रैश करना बंद नहीं कर देता। आइए Rack::Lint द्वारा आवश्यक प्रत्येक वेरिएबल पर जाएं।
SERVER_NAME: सर्वर का होस्टनाम। हम अभी केवल इस सर्वर को स्थानीय रूप से चला रहे हैं, इसलिए हम "localhost" का उपयोग करेंगे।SERVER_PORT: वह पोर्ट जिस पर हमारा सर्वर चल रहा है। हमने पोर्ट नंबर (5678) को हार्डकोड किया है, इसलिए हम बस उसे Rack एनवायरनमेंट में पास करेंगे।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 समर्थन नहीं, इसलिए इसे "https" के बजाय "http" पर सेट किया जा सकता है।
सभी लापता वेरिएबल जोड़ने के बाद, 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 `<main>'
जब अनुरोध में कोई क्वेरी स्ट्रिंग नहीं होती, तो हम अब QUERY_STRING के रूप में nil पास करेंगे, जिसकी अनुमति नहीं है। उस स्थिति में, 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 अपवाद का सामना करना पड़ता है।
प्रामाणिकता टोकन फॉर्म पोस्ट करते समय साथ भेजा जाता है और यह जांचने के लिए उपयोग किया जाता है कि अनुरोध किसी विश्वसनीय स्रोत से आया है। हमारा सर्वर अभी पूरी तरह से POST डेटा को नजरअंदाज कर रहा है, इसलिए टोकन नहीं भेजा जाता, और अनुरोध सत्यापित नहीं किया जा सकता।
जब हमने पहली बार अपना HTTP सर्वर लागू किया था, तब हमने पहली लाइन (जिसे Request-Line कहा जाता है) प्राप्त करने के लिए session.gets का उपयोग किया था, और उससे HTTP मेथड और पथ को पार्स किया था। Request-Line को पार्स करने के अलावा, हमने अनुरोध के बाकी हिस्से को नजरअंदाज कर दिया।
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,
# ...
})
# ...
वहां हम हैं। यदि हम सर्वर को पुनः आरंभ करते हैं और फिर से फॉर्म सबमिट करने का प्रयास करते हैं, तो आप देखेंगे कि हमने अपने ब्लॉग पर पहली पोस्ट सफलतापूर्वक बनाई!
हमने इस बार अपने वेब सर्वर को गंभीरता से अपग्रेड किया है। केवल Rack ऐप से GET अनुरोध स्वीकार करने के बजाय, हम अब POST अनुरोधों को संभालने वाला एक पूर्ण रेल्स ऐप सेवा कर रहे हैं। और हमने अभी भी कुल पचास लाइनों से अधिक कोड नहीं लिखा है!
यदि आप हमारे नए और बेहतर सर्वर के साथ खेलना चाहते हैं, तो यहां कोड है। @AppSignal पर हमें बताएं यदि आप अधिक जानना चाहते हैं, या कोई विशिष्ट प्रश्न है।