在 Ruby 中使用 HTTP 请求
zhang89jt
8年前
<p>有时候我们需要通过原生的 HTTP 调用来同 API 进行连接,而有时候我们又只需要进行普通的 HTTP 调用。那么在 Ruby 中有哪些可供我们选用呢?</p> <p>一个 API 不会因为它自己而存在,它总会涉及到两方: <strong>客户端</strong> 和 <strong>服务端。</strong></p> <p>在 Rails 中,我们的应用程序常常是扮演这其中的服务端, 而我们通常都指导如何来处理那些不可避免的问题。我们可以输出日志来查看进入的请求到底是什么(路径、消息头、参数), 如何做出响应,诸如此类。但随着后台开发越来越趋向于微服务架构,我们的 Rails 应用会更多地扮演除了服务端之外的客户端这一角色。</p> <p>作为客户端很可能意味着要进行HTTP请求和解析JSON类型的响应数据。更具体的说,这意味着使用正确的URL,身份认证所需HTTP消息头,页数信息,响应格式,和请求消息体。请求消息体通常是表单( application/x-www-form-urlencoded )或者是JSON。</p> <p>得益于APIs的日益普及,例如推ter, Slack, and Stripe,已经有相关客户端类库对底层HTTP请求的构建和响应解析进行封装。</p> <p>但是,当你需要连接自己内部服务或者知名度较低的APIs时,可能还没有构建相关客户端类库,我们只有构建我们自己的类库。 这篇文章的余下部分对Ruby进行探索——如何以多种可用的方式处理HTTP请求。当出现问题或者你希望可视化更多细节时,我们将使用一些技术去设计实现代码和排除故障。</p> <h2><strong>在Ruby中可使用哪些HTTP客户端?</strong></h2> <p>在Ruby生态系统中,有一些HTTP库可用作HTTP请求,我无法每个都提及到。下面我列出几个较为流行的:</p> <p><a href="/misc/goto?guid=4959716589815512087" rel="nofollow,noindex">Net::HTTP</a> : Ruby标准库的一部分。当它可以获取你所需要的所有东西时,你就会明白为什么会有那么多第三方库存在了。个人认为,它的工作方式并不简单,所以我倾向于在此列表的其他选项。</p> <p><a href="/misc/goto?guid=4959716589908028874" rel="nofollow,noindex">Curb</a> : 这个精简库提供 libcurl 编程的绑定(与你使用 curl 命令的命令行方式效果一样)。运行速度快,主要是因为,在Ruby外部做了大量繁杂工作,并且 libcurl 编程效率很高。</p> <p><a href="/misc/goto?guid=4959716589992056056" rel="nofollow,noindex">HTTParty</a> : 一个非常流行的库(2200万的下载量)封装了 Net::HTTP 代码,提供一个更加简单的API以供调用。</p> <p><a href="/misc/goto?guid=4959716590078598180" rel="nofollow,noindex">HTTP</a> : 为什么不如其他库一样流行呢,当它与APIs联系起来,最近它变成我的最爱。它提供一个链式接口来构建HTTP请求,提供你所期待一个HTTP库拥有的所有常用功能的支持。</p> <p><a href="/misc/goto?guid=4959616197567460686" rel="nofollow,noindex">Excon</a> : 另一个非常流行的库(2500万下载量)使用纯Ruby编写。它有一个简洁API并且简单调用。</p> <p>我不会在这里展示每个范例,来教大家如何使用这些库,你可以在它们的网站主页自行查看。在下一个章节,我们将讨论,如何 使用 HTTP 库,以及我们如何封装和组织我们的代码。</p> <h2><strong>对 HTTP 客户端进行包装</strong></h2> <p>最好是给与API端点或者服务的交互提供某些形式的接口,隐藏比较底层的HTTP请求和响应细节。其它的编程工作应该不需要展示执行的细节, 而对于同一个执行应该要能够在可以进行修改/重构的同时,无需对公开的API进行修改。</p> <p>让我们来看看针对一个API我们应该怎么做。我们将会使用 <a href="/misc/goto?guid=4959716590189725493" rel="nofollow,noindex">lcboapi.com</a> API。它提供了一个漂亮小巧的接口来访问跟LCBO的饮料和店铺相关的信息 (加拿大安大略省的一家企业,负责面向全省的酒精饮料零售和批发业务)。</p> <p>我们会用到4个类:</p> <ul> <li> <p>Lcbo::Products: 我们使用这个公共接口来获取特定产品的详细信息。</p> </li> <li> <p>Lcbo::ProductRequest: 处理针对特定产品ID的HTTP请求。</p> </li> <li> <p>Lcbo::ProductResponse: 处理HTTP响应并且知道如何去构建一个 Lcbo::Product.</p> </li> <li> <p>Lcbo::Product: 实际我们要获取的一个产品详情。</p> </li> </ul> <p>在深入实现细节之前,让我们先看看如何使用它:</p> <pre> <code class="language-ruby">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"]</code></pre> <p><strong>控制器</strong></p> <p>这个类作为公共接口获取关于一个或多个LCBO产品细节。在这一点上,我只实现了获取方法,该方法将返回一个产品的细节。这类的工作是构建请求并处理响应;它控制流和知道订单必须在API调用。</p> <pre> <code class="language-ruby">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</code></pre> <p>请求</p> <p>每个请求类知道如何使一个单一的请求API(基本上一个端点)。构建HTTP请求,填写标题的细节和任何其他信息需要包含在请求。这也有可能,你可以引入缓存,如果注意到你有一个本地版本的反应已经。它将回应响应对象。</p> <pre> <code class="language-ruby">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</code></pre> <p>响应</p> <p>响应对象知道如何处理从一个API端点的响应。可以返回响应的具体细节和/或构建其他对象的响应。</p> <pre> <code class="language-ruby">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</code></pre> <p>产品类</p> <p>最后一个类我们将关注Lcbo::Product 类。它由ProductResponse构成,并在LCBO API中代表一个单一的产品。</p> <pre> <code class="language-ruby">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</code></pre> <p><strong>日志输入通信</strong></p> <p>编程很容易当一切工作第一次尝试(提示:永远不会发生)。api可能会非常棘手,因为HTTP请求可能会以一个非常具体的方式格式化,这头或身体。可以棘手的Ruby代码仅仅通过观察找出最终的HTTP请求和响应实际的样子。</p> <p>伴随httplog</p> <p><a href="/misc/goto?guid=4959616199418239565" rel="nofollow,noindex">httplog</a> 是一个很棒的精华,monkey解决了大部分HTTP库能够记录传入和传出的交通到控制台($stdout或无论你想要的什么)。它所提供的信息可以帮助你意识到你拼写的标题错了,丢失了一些东西。</p> <p>如下面例子:</p> <pre> <code class="language-ruby">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...</code></pre> <p><strong>使用 mitmproxy</strong></p> <p>用代理服务器监视出站请求和入站响应则是一种更为有效的工具。 <a href="/misc/goto?guid=4959625687265452225" rel="nofollow,noindex">mitmproxy</a> 是一个用于观测app(或者手机或者电脑)产生HTTP流量的优秀工具。</p> <p>使用mitmproxy 的第一步是安装并且下载一个pem证书文件。之后,便可以在启动Rails服务器或者运行Ruby脚本时,添加指向你SSL证书的ENV变量。如下:</p> <ul> <li> <p>Rails 应用: SSL_CERT_FILE=/Users/leighhalliday/mitmproxy-ca-cert.pem bundle exec rails s -p 3000</p> </li> <li> <p>Rails 控制台: SSL_CERT_FILE=/Users/leighhalliday/mitmproxy-ca-cert.pem bundle exec rails c</p> </li> <li> <p>Ruby 脚本: SSL_CERT_FILE=/Users/leighhalliday/mitmproxy-ca-cert.pem ruby demo.rb</p> </li> </ul> <p>通过mitmproxy命令启动mitmproxy并监听localhost的8080(默认)端口。</p> <p>然后就可以让HTTP库将流量路由到mitmproxy,这样就能看到传进来的信息了。mitmproxy可以通过规则过滤请求、重试请求、以及导出这些信息以方便共享或后续查看。它能让我联想到Chrome或者火狐的网络监视标签。</p> <p>使用函数库的时候,需要将connect = HTTP改为connect = HTTP.via('localhost', 8080)。</p> <p>结论</p> <p>找出与API 的 抽象交互 或者微服务十分重要。它简化测试,封装了底层细节,并且提供了可重用的代码片段。为深入了解 httplogger 和 mitmproxy 做出的请求和响应,我们需要给予他们更充分的时间做出尝试。</p> <p> </p> <p>来自:https://www.oschina.net/translate/http-calls-in-ruby</p> <p> </p>