Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Behind the Scenes: Ruby HTTP Servers Thực Thi Rails Applications Như Thế Nào

Trong series Ruby Magic chúng tôi love take software apart học cách nó function under the hood. All about the process; end result không phải dùng production, chúng tôi learn internal workings Ruby language và popular libraries. Chúng tôi publish new article ~monthly, subscribe newsletter nếu bạn into this sort of thing.

Trong earlier edition Ruby Magic chúng tôi implement 30-line HTTP server Ruby. Không viết nhiều code, ta handle HTTP GET requests serve simple Rack application. Lần này, take home made server further. Khi xong, ta có web server serve Rails' famous fifteen minute blog cho create, update, delete posts.

Where We Left Off

Last time, implement just enough server serve Rack::Lobster as example application.

  1. Implementation mở TCP server wait request đến.
  2. Khi xảy ra, request-line (GET /?flip=left HTTP/1.1\r\n) parsed lấy request method (GET), path (/), query parameters (flip=left).
  3. Request method, path, query string pass cho Rack app, return triplet status, response headers, response body.
  4. Dùng those, build HTTP response gửi về browser, close connection wait new request.
# 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

Continue với code viết last time. Muốn follow along, đây code ended up with.

Rack và Rails

Ruby frameworks như Rails và Sinatra built trên Rack interface. Giống instance Rack::Lobster test server, Rails.application của Rails là Rack application object. Theory, server đã serve được Rails application.

Test đó, tôi prepared simple Rails application. Clone vào same directory server.

$ 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 

Trong server, require Rails application environment file thay vì rack và rack/lobster, đặt Rails.application vào app thay vì Rack::Lobster.new.

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

Start server (ruby http_server.rb) mở https://localhost:5678 show chưa xong. Server không crash, nhưng internal server error browser.

Check server logs, thấy missing rack.input. Tiết lộ chúng tôi lazy implement server last time, còn work trước serve Rails app.

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

Rack Environment

Khi implement server, ta glossed over Rack environment ignore most variables required properly serve Rack applications. Chỉ implement REQUEST_METHOD, PATH_INFO, QUERY_STRING, đủ cho simple Rack app.

Như đã thấy exception khi thử start new application, Rails cần rack.input, dùng as input stream raw HTTP POST data. Ngoài ra, cần thêm variables như server port, request cookie data.

May mắn Rack cung cấp Rack::Lint help ensure all variables Rack environment present và valid. Dùng test server bằng wrap Rails app trong nó gọi Rack::Lint.new pass Rails.application.

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

Rack::Lint throw exception khi variable environment missing/invalid. Restart server mở https://localhost:5678 crash server, Rack::Lint notify first error: SERVER_NAME variable not set.

~/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>' 

Fix từng error thrown, keep adding variables cho đến Rack::Lint stop crash server. Xem từng variable Rack::Lint require.

  • SERVER_NAME: server hostname. Chỉ run locally, dùng "localhost".
  • SERVER_PORT: port server chạy. Hardcoded port (5678), pass đó cho Rack environment.
  • rack.version: targeted Rack protocol version number as array integers. [1,3] time of writing.
  • rack.input: input stream chứa raw HTTP post data. Xem sau, pass empty StringIO instance (ASCII-8BIT encoding) for now.
  • rack.errors: error stream cho Rack::Logger write. Dùng $stderr.
  • rack.multithread: server single-threaded, set false.
  • rack.multiprocess: server single process, set false.
  • rack.run_once: server handle multiple sequential requests one process, false.
  • rack.url_scheme: no SSL support, set "http" thay vì "https".

Sau add all missing variables, Rack::Lint notify one more problem environment.

$ 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>' 

Khi no query string request, ta pass nil as QUERY_STRING, not allowed. Rack expect empty string instead. Sau implement missing variables và update query string, environment trông như:

# 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" # ...

Restart server visit https://localhost:5678, greeted Rails "You're on Rails!"-page, meaning running actual Rails app trên home made server!

Parsing HTTP POST Bodies

Application hơn index page. Visit https://localhost:5678/posts hiển thị empty list posts. Thử create new post fill form new post press "Create post", gặp ActionController::InvalidAuthenticityToken exception.

Authenticity token sent along khi post form dùng check request từ trusted source. Server ignore POST data, token không sent, request không verify.

Khi first implement HTTP server, dùng session.gets lấy first line (Request-Line), parse HTTP method và path. Bên cạnh parse Request-Line, ignore rest request.

Extract POST data, first cần hiểu HTTP request structured. Xem example, structure giống HTTP response:

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 

Giống response, HTTP request gồm:

  • Request-Line (POST /posts HTTP/1.1\r\n), gồm method token (POST), request URI (/posts/), HTTP version (HTTP/1.1), CRLF kết thúc line
  • Header lines (Host: localhost:5678\r\n). Header key, colon, value, CRLF.
  • Newline (double CRLF) separate request line/headers từ body: (\r\n\r\n)
  • URL encoded POST body

Sau dùng session.gets lấy first line request (Request-Line), còn header lines và body. Lấy header lines, retrieve lines từ session cho đến newline (\r\n).

Mỗi header line, split on first colon. Trước colon là key, sau là value. #strip value remove newline cuối.

Biết bao nhiêu bytes read từ request lấy body, dùng header "Content-Length", browser tự include khi gửi request.

# 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) # ...

Thay vì gửi empty object, gửi StringIO instance với body nhận via request. Vì parse cookies từ request header, thêm vào Rack environment variable HTTP_COOKIE pass request authenticity check.

# 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, # ... }) # ...

Restart server submit form again, successfully created first post trên blog!

Seriously upgraded web server. Thay vì chỉ accept GET requests từ Rack app, now serve complete Rails app handle POST requests. Vẫn chưa viết hơn 50 lines code total!

Muốn play around new improved server, đây code. Liên hệ @AppSignal nếu muốn biết thêm, hoặc specific question.