理解 Rack 應用及其中間件

jopen 9年前發布 | 21K 次閱讀 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 應用。我們希望聽你的戰斗故事!給我們評論!

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!