在 Ruby 中使用 HTTP 請求
有時候我們需要通過原生的 HTTP 調用來同 API 進行連接,而有時候我們又只需要進行普通的 HTTP 調用。那么在 Ruby 中有哪些可供我們選用呢?
一個 API 不會因為它自己而存在,它總會涉及到兩方: 客戶端 和 服務端。
在 Rails 中,我們的應用程序常常是扮演這其中的服務端, 而我們通常都指導如何來處理那些不可避免的問題。我們可以輸出日志來查看進入的請求到底是什么(路徑、消息頭、參數), 如何做出響應,諸如此類。但隨著后臺開發越來越趨向于微服務架構,我們的 Rails 應用會更多地扮演除了服務端之外的客戶端這一角色。
作為客戶端很可能意味著要進行HTTP請求和解析JSON類型的響應數據。更具體的說,這意味著使用正確的URL,身份認證所需HTTP消息頭,頁數信息,響應格式,和請求消息體。請求消息體通常是表單( application/x-www-form-urlencoded )或者是JSON。
得益于APIs的日益普及,例如推ter, Slack, and Stripe,已經有相關客戶端類庫對底層HTTP請求的構建和響應解析進行封裝。
但是,當你需要連接自己內部服務或者知名度較低的APIs時,可能還沒有構建相關客戶端類庫,我們只有構建我們自己的類庫。 這篇文章的余下部分對Ruby進行探索——如何以多種可用的方式處理HTTP請求。當出現問題或者你希望可視化更多細節時,我們將使用一些技術去設計實現代碼和排除故障。
在Ruby中可使用哪些HTTP客戶端?
在Ruby生態系統中,有一些HTTP庫可用作HTTP請求,我無法每個都提及到。下面我列出幾個較為流行的:
Net::HTTP : Ruby標準庫的一部分。當它可以獲取你所需要的所有東西時,你就會明白為什么會有那么多第三方庫存在了。個人認為,它的工作方式并不簡單,所以我傾向于在此列表的其他選項。
Curb : 這個精簡庫提供 libcurl 編程的綁定(與你使用 curl 命令的命令行方式效果一樣)。運行速度快,主要是因為,在Ruby外部做了大量繁雜工作,并且 libcurl 編程效率很高。
HTTParty : 一個非常流行的庫(2200萬的下載量)封裝了 Net::HTTP 代碼,提供一個更加簡單的API以供調用。
HTTP : 為什么不如其他庫一樣流行呢,當它與APIs聯系起來,最近它變成我的最愛。它提供一個鏈式接口來構建HTTP請求,提供你所期待一個HTTP庫擁有的所有常用功能的支持。
Excon : 另一個非常流行的庫(2500萬下載量)使用純Ruby編寫。它有一個簡潔API并且簡單調用。
我不會在這里展示每個范例,來教大家如何使用這些庫,你可以在它們的網站主頁自行查看。在下一個章節,我們將討論,如何 使用 HTTP 庫,以及我們如何封裝和組織我們的代碼。
對 HTTP 客戶端進行包裝
最好是給與API端點或者服務的交互提供某些形式的接口,隱藏比較底層的HTTP請求和響應細節。其它的編程工作應該不需要展示執行的細節, 而對于同一個執行應該要能夠在可以進行修改/重構的同時,無需對公開的API進行修改。
讓我們來看看針對一個API我們應該怎么做。我們將會使用 lcboapi.com API。它提供了一個漂亮小巧的接口來訪問跟LCBO的飲料和店鋪相關的信息 (加拿大安大略省的一家企業,負責面向全省的酒精飲料零售和批發業務)。
我們會用到4個類:
-
Lcbo::Products: 我們使用這個公共接口來獲取特定產品的詳細信息。
-
Lcbo::ProductRequest: 處理針對特定產品ID的HTTP請求。
-
Lcbo::ProductResponse: 處理HTTP響應并且知道如何去構建一個 Lcbo::Product.
-
Lcbo::Product: 實際我們要獲取的一個產品詳情。
在深入實現細節之前,讓我們先看看如何使用它:
require_relative 'lib/lcbo'
key = ENV.fetch('LCBO_API_KEY')
product = Lcbo::Products.new(key).fetch(438457)
puts product.name
# Hopsta La Vista
puts product.tags.inspect
# ["hopsta", "la", "vista", "beer", "ale", "canada", "ontario", "longslice", "brewery", "inc", "can"]
控制器
這個類作為公共接口獲取關于一個或多個LCBO產品細節。在這一點上,我只實現了獲取方法,該方法將返回一個產品的細節。這類的工作是構建請求并處理響應;它控制流和知道訂單必須在API調用。
module Lcbo
require_relative 'product_request'
require_relative 'product_response'
class Products
attr_accessor :key
def initialize(key)
@key = key
end
def fetch(product_id)
connection = HTTP
product_response = ProductRequest.new(key, product_id, connection).response
fail LcboError, product_response.error_message unless product_response.success?
product_response.product
end
end
end
請求
每個請求類知道如何使一個單一的請求API(基本上一個端點)。構建HTTP請求,填寫標題的細節和任何其他信息需要包含在請求。這也有可能,你可以引入緩存,如果注意到你有一個本地版本的反應已經。它將回應響應對象。
module Lcbo
class ProductRequest
attr_reader :key, :product_id, :connection
def initialize(key, product_id, connection)
@key = key
@product_id = product_id
@connection = connection
end
def response
http_response = connection
.headers('Authorization' => "Token #{key}")
.get(url)
ProductResponse.new(http_response)
end
def url
"https://lcboapi.com/products/#{product_id}"
end
end
end
響應
響應對象知道如何處理從一個API端點的響應。可以返回響應的具體細節和/或構建其他對象的響應。
module Lcbo
class ProductResponse
attr_reader :http_response
DEFAULT_ERROR_MESSAGE = 'There was an error retrieving product details.'.freeze
def initialize(http_response)
@http_response = http_response
end
def success?
http_response.status == 200
end
def error_message
data.fetch('message', DEFAULT_ERROR_MESSAGE)
end
def product
Product.new(data.fetch('result'))
end
private
def data
http_response.parse(:json)
end
end
end
產品類
最后一個類我們將關注Lcbo::Product 類。它由ProductResponse構成,并在LCBO API中代表一個單一的產品。
module Lcbo
class Product
attr_accessor :details
def initialize(details)
@details = details
end
def name
details['name']
end
def tags
details.fetch('tags', '').split(' ')
end
end
end
日志輸入通信
編程很容易當一切工作第一次嘗試(提示:永遠不會發生)。api可能會非常棘手,因為HTTP請求可能會以一個非常具體的方式格式化,這頭或身體。可以棘手的Ruby代碼僅僅通過觀察找出最終的HTTP請求和響應實際的樣子。
伴隨httplog
httplog 是一個很棒的精華,monkey解決了大部分HTTP庫能夠記錄傳入和傳出的交通到控制臺($stdout或無論你想要的什么)。它所提供的信息可以幫助你意識到你拼寫的標題錯了,丟失了一些東西。
如下面例子:
D, [2016-09-11T22:05:11.353063 #5345] DEBUG -- : [httplog] Sending: GET https://lcboapi.com/products/438457
D, [2016-09-11T22:05:11.353158 #5345] DEBUG -- : [httplog] Header: Authorization: Token MY_API_KEY
D, [2016-09-11T22:05:11.353184 #5345] DEBUG -- : [httplog] Header: Connection: close
D, [2016-09-11T22:05:11.353202 #5345] DEBUG -- : [httplog] Header: Host: lcboapi.com
D, [2016-09-11T22:05:11.353234 #5345] DEBUG -- : [httplog] Header: User-Agent: http.rb/2.0.3
D, [2016-09-11T22:05:11.353268 #5345] DEBUG -- : [httplog] Data:
D, [2016-09-11T22:05:11.353320 #5345] DEBUG -- : [httplog] Connecting: lcboapi.com:443
D, [2016-09-11T22:05:11.502537 #5345] DEBUG -- : [httplog] Status: 200
D, [2016-09-11T22:05:11.502658 #5345] DEBUG -- : [httplog] Benchmark: 0.1491872170008719 seconds
D, [2016-09-11T22:05:11.502815 #5345] DEBUG -- : [httplog] Header: Server: nginx/1.6.2
etc...
使用 mitmproxy
用代理服務器監視出站請求和入站響應則是一種更為有效的工具。 mitmproxy 是一個用于觀測app(或者手機或者電腦)產生HTTP流量的優秀工具。
使用mitmproxy 的第一步是安裝并且下載一個pem證書文件。之后,便可以在啟動Rails服務器或者運行Ruby腳本時,添加指向你SSL證書的ENV變量。如下:
-
Rails 應用: SSL_CERT_FILE=/Users/leighhalliday/mitmproxy-ca-cert.pem bundle exec rails s -p 3000
-
Rails 控制臺: SSL_CERT_FILE=/Users/leighhalliday/mitmproxy-ca-cert.pem bundle exec rails c
-
Ruby 腳本: SSL_CERT_FILE=/Users/leighhalliday/mitmproxy-ca-cert.pem ruby demo.rb
通過mitmproxy命令啟動mitmproxy并監聽localhost的8080(默認)端口。
然后就可以讓HTTP庫將流量路由到mitmproxy,這樣就能看到傳進來的信息了。mitmproxy可以通過規則過濾請求、重試請求、以及導出這些信息以方便共享或后續查看。它能讓我聯想到Chrome或者火狐的網絡監視標簽。
使用函數庫的時候,需要將connect = HTTP改為connect = HTTP.via('localhost', 8080)。
結論
找出與API 的 抽象交互 或者微服務十分重要。它簡化測試,封裝了底層細節,并且提供了可重用的代碼片段。為深入了解 httplogger 和 mitmproxy 做出的請求和響應,我們需要給予他們更充分的時間做出嘗試。
來自:https://www.oschina.net/translate/http-calls-in-ruby