理解 Rack 應用及其中間件
理解 Rack 應用及其中間件
大多數 web 開發者都是基于高度抽象出來的接口基礎上編碼,很多時候我們知其然但不知其所以然,特別是使用 Rails 框架開發時。
你是否研究過 Rails 內部對請求/響應周期是如何運作的?近期我意識到對于 Rack 和 middlewares 內部機制知之甚少,所以我花了一點時間來研究它。在這篇文章中分享了我的研究成果。
什么是 Rack
你知道 Rails 是一個 Rack 應用嗎?同時 Sinatra 亦然。那么請問什么是 Rack 呢?總而言之 Rack 就是對 Ruby 的 Net::HTTP 庫的封裝為一個 Ruby 包,這個包能夠讓開發者方便易用 Net::HTTP。
使用 Rack 能夠快速新建一個簡單的 web 應用。
首先,你需要一個能夠響應 call 方法的對象,這個對象以一個環境變量哈希作為參數并且返回一個數組,返回的數組元素中包含 HTTP 響應碼,響應頭已經響應體。此時使用一個Ruby 服務器(例如Rack::Handler::WEBrick)即可啟動服務端代碼;或者你也可以把它放到一個單獨的 config.ru 文件中,然后通過 rackup config.ru 命令啟動服務。
很酷吧?那么 Rack 內部到底做了些什么呢?
Rack 的工作機制
Rack 實際上是為開發者提供開發服務器應用的一種途徑,避免編寫 boilerplate code,否則需要應用 Net::HTTP 底層庫。如果你編寫符合 Rack 規范的代碼,那么可以通過 Ruby 服務器(WEBrick,Mongrel,Thin)來啟動服務,以此來接收請求和響應請求。
Rack 提供多個方法啟動,你可以在 config.ru 文件直接調用這些方法。
run
run 方法以一個應用程序(響應 call 方法的對象)為參數,下面這段代碼是 Rack 官網中的例子
run Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['get rack\'d']] }
譯者注:拷貝上述代碼到 config.ru 文件中,然后在運行 rackup config.ru。同時 ruby 默認的服務器是 WEBrick,服務端口是 9292。服務啟動之后運行 curl -X GET localhost:9292,啟動的服務即能接收到請求并響應。
map
map 方法能處理一個指定的請求路徑,如果請求路徑符合指定路徑,那么塊中 Rack 應用程序代碼將會執行。
map '/posts' do run Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['first_post', 'second_post', 'third_post']] } end
譯者注:服務啟動方法如上,此時請求路徑為 curl -X GET localhost:9292/posts 服務能接收請求并響應。
use
use 方法告訴 Rack 使用指定的 middleware。
所以接下來你需要了解一些什么樣的知識點呢?讓我們接下來具體了解環境哈希和響應數組。
環境哈希
Rack 服務對象接收一個環境哈希,包含如下部分:
-
REQUEST_METHOD:HTTP 請求方法
-
PATH_INFO:相對于應用程序的請求路徑
-
QUERY_STRING:請求URL中"?"問號后面的字符串
-
SERVER_NAME 和 SERVER_PORT:服務器地址和端口
-
rack.version:使用的 rack 版本號
-
rack.url_scheme:是 http 或者是 https?
-
rack.input:一個包含原生 HTTP POST 數據的 IO-like 對象
-
rack.errors:一個能夠響應 puts,write 和 flush 的對象
-
rack.session:一個保存請求會話的健值對
-
rack.logger:一個提供打印日志接口的對象。包含 info,debug,ware,error 和 fatal 方法。
很多基于 rack 的框架把 env 哈希封裝在 Rack::Request 對象中。這個對象提供了很多便于使用的方法,例如,request_method,query_string,session 和 logger,這些方法都返回上述列表列出來鍵的值。同時還允許開發者獲取用戶請求中的一些有用信息,例如請求參數,HTTP scheme 或者后臺服務使用開啟了 ssl? 查看源碼https://github.com/rack/rack/blob/master/lib/rack/request.rb 可以完整的方法。
響應數組
Rack 服務器對象響應一個請求,必須包含三個部分:響應狀態,響應頭和響應體。正如請求一樣,Rack 內置的 Rack::Response對象同樣也提供了方便易用的方法,譬如 write,set_cookie,finish 等方法。或者你也可以使用一個數組包含這三個必要元素。
響應狀態
就是 HTTP 狀態碼,例如 200,404
響應頭
響應頭的格式必須能夠被 each 方法遍歷,被 each 遍歷出來的值應為一個健值對,鍵必須遵循 RFC7230 標準。例如在響應頭中可以設置 Content-Type 和 Content-Length。
譯者注:可參考上文 rack 工作機制的示例代碼 {'Content-Type' => 'text/html'}
響應體
響應體就是服務器對用戶請求發送的數據。響應體的格式必須能夠被 each 方法遍歷,并且 each 遍歷出來的值應為字符串。
譯者注:可參考上文 rack 工作機制中的示例代碼 ['first_post', 'second_post', 'third_post']
讓 RACK 運行起來!
現在我們已經可以創建一個 Rack app 了,那我們該怎么去做讓它起作用呢?第一步就要考慮添加一些中間件。
什么是中間件?
Rack 這么好是因為其易于添加一個連鎖的中間件組件,它是在 web 服務器和 app 間通過你自定義的 request/response 方式添加的。但是什么是中間件組件呢?
中間件組件被設置在客戶端和服務器之間,處理入站的請求和出站的回應。為什么你會想要做這些呢?Rack 有很多可用的中間件組件,比如推測可用的緩存,驗證,捕獲垃圾郵件等其他功能。
使用 Rack 中間件
在 Rack 應用中使用中間件,你所需要做的僅僅是告訴 Rack 使用它。使用多個中間件時,每個中間件將會改變請求或響應體,然后傳遞到下一個中間件。這一系列的中間件稱之為中間件堆棧。
Warden
我們來看看如何增加 Warden 到一個項目中。Warden 在中間件堆棧中是在某種會話中間件之后被調用的位置,因此,我們在 Warden 之前使用 Rack::Session::Cookie 這個會話中間件。首先,增加代碼: gem "warden" 到你的項目等 Gemfile 文件中,然后執行 bundle install。然后再添加以下代碼到你的 config.ru 文件中。
require "warden" use Rack::Session::Cookie, secret: "MY_SECRET" failure_app = Proc.new { |env| ['401', {'Content-Type' => 'text/html'}, ["UNAUTHORIZED"]] } use Warden::Manager do |manager| manager.default_strategies :password, :basic manager.failure_app = failure_app end run Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['get rack\'d']] }
最后,執行命令 rackup 啟動你的rack服務。Rack 將會找到你的 config.ru 文件啟動服務,默認監聽 9292 端口。
注意,想要使用 Warden 來作為應用的身份驗證還需要更多的步驟,這里只是舉例說明如何添加中間件到 Rack 程序的中間件堆棧中。想要查看更多典型的 Warden 集成的例子可以查看 代碼片段 。
除了在 config.ru 文件中直接調用 use 命令來定義中間件堆棧,還有另一種方法。你可以使用 Rack::Builder 來包裹一系列的中間件或者代碼塊來生成一個應用。例如:
failure_app = Proc.new { |env| ['401', {'Content-Type' => 'text/html'}, ["UNAUTHORIZED"]] } app = Rack::Builder.new do use Rack::Session::Cookie, secret: "MY_SECRET" use Warden::Manager do |manager| manager.default_strategies :password, :basic manager.failure_app = failure_app end end run app
Rack 基本認證
一個很有用的中間件是 Rack::Auth::Basic,你可以通過它來使用 HTTP basic authentication 保護任何的 Rack 應用。它非常輕量級,非常便利。例如,Ryan Bates 就是使用它來保護 Resque 服務。參考:this episode of Railscasts.
以下是非常簡單的配置代碼:
use Rack::Auth::Basic, "Restricted Area" do |username, password| [username, password] == ['admin', 'abc123'] end
在 rails 中使用中間件
現在,那又怎樣, Rack 是相當酷,并且我們知道 rails 是基于 rack 構建的。但是我們僅僅知道它是什么,又不會在中實際中使用它寫生產的應用程序。
在 rails 怎么使用 rack
你有沒有注意到在 rails 項目文件中的根目錄下有個名叫 config.ru 的文件。你有沒有看過里面的內容,下面代碼是它內容:
# This file is used by Rack-based servers to start the application. require ::File.expand_path('../config/environment', __FILE__) run Rails.application
很簡單的幾句代碼。它只是加載 config/environment 文件,然后啟動 rails 程序。等等, 那是什么?看一下 config/environment 里面的內容,我們可以看見它已經定義在 config/application.rb 文件里。config/environment 文件只是調用 initialize! 方法。
接下來 config/application.rb 文件又是干什么的呢?如果我們看了代碼,它加載了 從config/boot.rb 文件里讀取已經 bundled 的 gem 包,加載 rails 所有包,加載當前程序運行的環境(測試,開發,生產,等等),還定義了應用程序命名空間的版本號。它看起來像這樣的:
module MyApplication class Application < Rails::Application ... end end
那按照我的理解那意味著 rails 程序一定是 rack 應用了?果然是的,如果我們檢出 rails 的源碼。它響應 call!方法。
接下來是怎么使用中間件?我看它是自動加載了 rails/application/default_middleware_stack
這個文件,把這個文件拉下來,它看起來已經定義了在 ActionDispatch 模塊里。ActionDispatch 是從哪來的呢?ActionPack 包嗎?
Action Dispatch
Action Pack 是處理請求響應的 Rails 框架。它是Rails中為數不多的非常精密的組件,類似的還有:routing,虛擬控制器,頁面渲染器。
大多數AP相關的討論在這里 Action Dispatch。它提供了一系列的中間件來處理類似 ssl,cookies,調試,靜態文件的問題。
去了解每一個 Action Dispatch 中間件,你就會發現它們都遵循著Rack規范:它們都提供 call 方法,接受 app 請求,返回 status, headers, 以及body。它們中的大部分還會使用Rack::Request,以及Rack::Response 對象。
通過閱讀這些Rails組件的源碼,揭開了Rails程序的神秘面紗。當我意識到Rails框架只是一群的遵循Rack規范的Ruby對象彼此間傳遞著請求和響應實體,這么看來Rails也就沒有這么神秘了。
現在我們已經了解了Rack中間件的一些原理,下面我們來看看如何在Rails程序中引入自定義的中間件。
添加自定義中間件
假設你在 Engine Yard 上部署了一個應用。你有一套 Rails API 跑在一個服務器上,基于 JavaScript 的客戶端跑在另一個服務器。API 的地址為: https://api.example.com,客戶端的是: https://app.example.com。
這時你將面臨一個問題,根據 同源策略 你的 JS 客戶端無法訪問 api.example.com 的資源。你也許知道,這個問題的解決方案是開啟 跨域資源共享 (CORS)。有很多種方法可以在你的應用中開啟 CORS,最簡單的莫過于使用 Rack::Cors middleware 這個 Gem。
在 Gemfile 中指定:
gem "rack-cors", require: "rack/cors"
Rails 提供了非常簡單的方式去加載中間件。雖然我們也可以如前文所訴,在 config.ru 文件中用 Rack::Builder 塊來加載,然而 Rails 的約定是寫在 config/application.rb 文件中。代碼如下:
module MyApp class Application < Rails::Application config.middleware.insert_before 0, "Rack::Cors" do allow do origins '*' resource '*', :headers => :any, :expose => ['X-User-Authentication-Token', 'X-User-Id'], :methods => [:get, :post, :options, :patch, :delete] end end end end
注意,這里我們使用 insert_before 來確保 Rack::Cors 在 ActionPack 引入的中間件(以及你使用到的其他中間件)之前被調用。
重啟服務之后,你的客戶端應用就可以正常訪問 api.example.com。
如果你希望了解更多關于 Rack in Rails 如何路由 HTTP 請求,我建議你看看這個部分 Rails 代碼 ,這里很詳細地說明 Rails 如何處理請求。
結論
在這篇博文中,我們深入 Rack 的內部結構,并且擴展,請求(request)/回應(response)基于幾個 Ruby 的 Web 框架,這也包括 Rails。
幸運的是,理解當一個請求到達服務器并且應用程序接收響應這個過程,你就會覺得這個過程少了一些魔法(magical)。我不了解你是怎么做的,但當我的事情出錯,故障排除時,我明白發生了什么,這涉及到魔法(magical)。在那種情況下,我會說“哦,那就是 Rack 的回應”,并且靠這個來修復 bug。
曾經我這樣做過我的工作,讀這篇文章會讓你獲得類似的經驗。
附言:一個簡單的 Rack 應用是怎么滿足您的業務需求的? 在你的更大的應用中有什么其他方式集成進 Rack 應用。我們希望聽你的戰斗故事!給我們評論!