[Ruby] Rack — Ruby 框架與 Web Server 的翻譯員

Rack 用於連結”支援Ruby的Web伺服器”和”Web架構的程式庫”,或稱爲微架構。Rack 將 HTTP 請求與響應,盡可能包裝成最簡單的形式,給Web架構、伺服器以及Web架構與伺服器之間的軟體(Middleware)提供了一個統一的 API 接口,call 方法。而 call 方法需要回傳一個包含3個元素的陣列:狀態碼、HTTP標頭資訊以及內容。

Rack包含了不同的Handler來和Web伺服器連結,如WEBrick,Mongrel等,這些Web伺服器類似Apache httpd,但又可以支援Ruby。Rack透過Adapters來和Web架構的程式庫(Sinatra、Rails等)溝通。

用 rack gem 撰寫第一個 rack 應用

Rack 的gem中包含了許多的幫手函數,讓我們很快可以建立出Web應用程式。首先,在Gemfile中增加rack,然後 bundle install。

gem 'rack'

安裝好之後就可以來撰寫第一個簡單的Rack應用了。在irb中輸入:

require 'rack'
rack_proc = lambda { |env| [200, {"Content-type" => "text/plain"}, ["Hello, world"]] }
# 根據Rack的標準,call方法接收一個參數(env)(一個Hash)。而call方法需回傳一個包含3個元素的陣列:狀態碼、HTTP標頭資訊以及內容。
Rack::Handler::WEBrick.run rack_proc
# 啓動一個HTTP伺服器,在8080通訊埠監聽。當請求到達時呼叫 rack_proc 函數

透過命令列來檢視回應

$ curl http://localhost:8080
Hello, world

rackup

Rack gem中提供了一個很好用的工具 rackup,可以用來啓動符合 Rack 標準的應用程式。rackup 需要一個設定檔,一般由 .ru 結尾。

建立 config.ru 檔案:

require 'rack'
rack_proc = lambda { |env| [200, {"Content-type" => "text/plain"}, ["Hello, world"]] }
run rack_proc
# rackup自己會知道需要執行在何種環境,預設即爲 WEBrick。

儲存之後即可啓動應用程式了:

$ rackup config.ru

簡化config.ru

將應用程式從檔案中移出去:

# app.rb
require 'rack'
require 'json'
class App
def call env
[200, {"Content-type" => "application/json"}, [env.to_json]]
# 改爲傳回用戶端的請求中的所有HTTP標頭資訊
end
end
# config.ru
require './app'
run App.new

使用 `rackup config.ru` 啓動服務之後,透過命令列的curl來進行測試,並透過jq指令將輸出的json格式化,就可以看到env中包含了用戶端請求的所有資訊。

$ curl http://localhost:9292 | jq . 
{
"rack.version": [
1,
3
],
"rack.errors": "#<Rack::Lint::ErrorWrapper:0x007fd579241250>",
"rack.multithread": true,
"rack.multiprocess": false,
"rack.run_once": false,
"SCRIPT_NAME": "",
"QUERY_STRING": "",
"SERVER_PROTOCOL": "HTTP/1.1",
"SERVER_SOFTWARE": "puma 3.8.2 Sassy Salamander",
"GATEWAY_INTERFACE": "CGI/1.2",
"REQUEST_METHOD": "GET",
"REQUEST_PATH": "/",
"REQUEST_URI": "/",
"HTTP_VERSION": "HTTP/1.1",
"HTTP_HOST": "localhost:9292",
"HTTP_USER_AGENT": "curl/7.51.0",
"HTTP_ACCEPT": "*/*",
"SERVER_NAME": "localhost",
"SERVER_PORT": "9292",
"PATH_INFO": "/",
"REMOTE_ADDR": "::1",
"puma.socket": "#<TCPSocket:0x007fd579268dc8>",
"rack.hijack?": true,
"rack.hijack": "#<Proc:0x007fd579241778@/Users/dd/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/rack-2.0.1/lib/rack/lint.rb:525>",
"rack.input": "#<Rack::Lint::InputWrapper:0x007fd579241278>",
"rack.url_scheme": "http",
"rack.after_reply": [],
"puma.config": "#<Puma::Configuration:0x007fd5792d8178>",
"rack.tempfiles": []
}

Rack還提供了Request 解析功能

class App
def call env
req = Rack::Request.new(env)
p req.request_method
p req.params['name']
[200, {"Content-type" => "application/json"}, [env.to_json]]
end
end

透過命令列的curl

$ curl http://localhost:9292?name=dosmanthus

在伺服器執行的視窗會看到輸出

"GET"
"dosmanthus"
::1 - - [29/Apr/2017:10:27:14 +0800] "GET /?name=dosmanthus HTTP/1.1" 200 - 0.0007

相同的,Rack 也提供對回應的包裝物件 Response,透過對 Response 的修改,可以產生最後應用程式的回應:

class App
def call env
resp = Rack::Response.new
resp["Content-type"] = "application/json"
# 可加入不止一個標頭資訊
resp.write "{}" # 寫入內容
resp.status = 200
resp.finish
end
end
# [200, {"Content-type" => "application/json"}, [{}]]

Rack Middleware

Rack中介軟體,本質上就是Rack應用程式。我們的每個request其實都是經過很多層的middleware,從一個中介軟體處理後,傳遞給下一個中介軟體,以此類推。Rack middleware的功能是用來處理應用邏輯之外針對 http 本身的相關處理。

定義一個簡單的middleware

這個middleware會爲經過它的應用程式加上頭尾資訊:

class MyMiddleware
def initailize app
@app = app
end
  def call env
stauts, headers, body = @app.call(env)
new_body = []
new_body << "prefix..."
new_body << body.to_s
new_body << "...suffix"
[status, headers, new_body]
end
end

使用時只需要在 config.ru 中加入 use:

require './app'
require './my_middleware'
use MyMiddleware # 可使用多個不同的middleware
run App.new

rackup 實際上是在背景將 config.ru 中的的內容包裝成一個大的 Rack 應用程式:

app = App.new
builder = Rack::Builder.new do
use MyMiddleware
run app
end
Rack::Handler::WEBrick.run builder, :port => 9292

Rackup 的路由功能

rackup 的路由功能會將比對到某個路徑的請求分發到對應的Rack應用程式上

#config.ru
require './app'
require './my_middleware.rb'
use MyMiddleware
map '/' do
run lambda { |env| [200, {}, ["root"]] }
end
map '/todos' do
run lambda { |env| [200, {}, ["todo list"]] }
end

在命令列測試:

$ curl http://localhost:9292/
prefix...["root"]...suffix%
$ curl http://localhost:9292/todos
prefix...["todo list"]...suffix%

Rack::Cascade

Rack::Cascade可以將多個應用程式串聯起來。Rack::Cascade 接受一個陣列,陣列的每個元素都是一個Rack應用程式,請求會首先被第一個元素處理後轉發到第二個元素,以此類推,直到404。

require './app'
StaticApp = Rack::File.new("static")
# 將當前目錄下的 static 目錄變爲 HTTP 伺服器的根目錄
MyApp = App.new
run Rack::Cascade.new[StaticApp, MyApp]

如果透過瀏覽器造訪 http://localhost:9292/file.html,事實上存取的就是 static/file.html。如果存取的檔案存在,則傳回檔案內容,否則使用我們的App來回應。

參考:

  • 「還在寫PHP?大師才用輕量級Ruby、JavaScript開發Web」- 邱俊濤 / 佳魁資訊
One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.