25 October 2014

缘起


最初学习Ruby和Rails时,看到过Rack,顺便还不知从拿收集一个《Rack编程》的PDF文档。最来,学习Grape,顺带学习一下Rack,加强加强基础。

正文

最简单的Rack程序就是一个包含call方法调用的Ruby对象(lambda/proc,method,包含call方法的类),接受参数(HTTP请求),返回包含三个元素的数组(HTTP响应)。

Rack是Ruby应用服务器和Rack应用程序之间的一个接口。应用服务器,反向代理,Rack::Handler名称空间:

Rack::Handler::CGI
Rack::Handler::EventedMongrel
Rack::Handler::FastCGI
Rack::Handler::LSWS
Rack::Handler::Mongrel
Rack::Handler::SCGI
Rack::Handler::SwiftipliedMongrel
Rack::Handler::Thin
Rack::Handler::WEBrick

所有的Rack Handler都有一个run方法 Rails和Sinatra都是Rack程序。 Rack spec文档

中间件 - 最大程序的模块化,web应用程序部件。

可被调用的对象:环境对象参数,返回的三个数组(状态,头,体)

符合条件的Rack应用程序:

irb> rack_app = lambda {|env| [200,{},["hello from lambda"]]}
irb> Rack::Handler::WEBrick.run rack_app ,:Port=>3000 # 运行之后,我的IRB进程就挂了
irb> Rack::Handler::Thin.run rack_app ,:Port=>3000

Rack的实例,具体介绍如下:

#!/usr/bin/env ruby
require "rubygems"
require "rack"
def pp(hash)
  hash.map {|key,value|  "#{key} => #{value}" }.sort.join("\n")
end

Rack::Handler::WEBrick.run lambda {|env| [200,{},[pp(env)]]} , :Port=>8000

输入: http://localhost:8000/xiajin%20e%20we

响应结果:

GATEWAY_INTERFACE => CGI/1.1
HTTP_ACCEPT => text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
HTTP_ACCEPT_ENCODING => gzip, deflate
HTTP_ACCEPT_LANGUAGE => en-US,en;q=0.5
HTTP_CACHE_CONTROL => max-age=0
HTTP_CONNECTION => keep-alive  # HTTP连接的类型
HTTP_COOKIE => optimizelySegments=%7B%22326765800%22%3A%22false%22%2C%22326352497%22%3A%22direct%22%2C%22327219662%22%3A%22ff%22%2C%22532690232%22%3A%22none%22%7D; optimizelyEndUserId=oeu1405064745904r0.9964926734400492; optimizelyBuckets=%7B%7D; CNZZDATA17872=cnzz_eid%3D1643098070-1408757496-%26ntime%3D1408757496; __atuvc=1%7C34
HTTP_DNT => 1
HTTP_HOST => localhost:8000
HTTP_USER_AGENT => Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:33.0) Gecko/20100101 Firefox/33.0
HTTP_VERSION => HTTP/1.1
PATH_INFO => /xiajin%20e%20we  #实现路由算法的关键
QUERY_STRING =>  #查询字符串
REMOTE_ADDR => 127.0.0.1
REMOTE_HOST => 127.0.0.1
REQUEST_METHOD => GET  # HTTP请求
REQUEST_PATH => /xiajin%20e%20we
REQUEST_URI => http://localhost:8000/xiajin%20e%20we
SCRIPT_NAME => 
SERVER_NAME => localhost
SERVER_PORT => 8000
SERVER_PROTOCOL => HTTP/1.1
SERVER_SOFTWARE => WEBrick/1.3.1 (Ruby/1.9.3/2014-05-14)
rack.errors => #<IO:0x00000000bcc258>
rack.input => #<StringIO:0x00000000ea77c0>
rack.multiprocess => false
rack.multithread => true
rack.run_once => false
rack.url_scheme => http
rack.version => [1, 1]

中间件

Rack middleware是简单的拦截器(filter),可以被组织成middleware chain(middleware stack)。比如,日志,执行返回的时间。

中间件就是Ruby应用服务器和Rack应用程序之间执行的代码。用来分离耦合,将处理和包装进行分离,可以实现通用逻辑和业务逻辑分离。

call方法:

状态码: HTTP状态,to_i产生整数,大于100

响应头: 必须能响应each方法,产生key/value,key和value必须是字符串,任何时候必须包含的key: Content-Type 内容类型, Content-Length 内容长度。

响应体: 响应each方法,产生字符串。1.9不支持each方法。

用户身份认证的中间件,应用到任何Rack应用程序中,Web框架-Rack应用程序-中间件。中间件包装中间件,单片实现的框架分离成多个中间件的好处: 1. 中间件独立发展 2. 不同的方式组合中间件 - 过滤器

装配中间件

  • 方法一:使用new方法 - Rack::Handler::XXXX.run Middleware1.new(Middleware2.new(rack_app[,options2])[,options1])

  • 方法二:使用DSL进行描述, 比如定义Builder类:

class Builder
  def use
    # ...
  end

  def run
    # ...
  end
end

使用DSL的方式:

Builder.new {
  use Middleware1
  use Middleware2
  run Rack Application
}

尝试编写了一些加载中间件的DSL,实现不是特别的困难:

class Builder
  def initialize(&block)
    @middlewares = []
    self.instance_eval(&block)  # 对传递过来的代码进行求值
  end

  def use(middleware_class, *options, &block)  # 在数组中保存lambda
    @middlewares << lambda { |app| middleware_class.new(app, *options, &block)}
  end

  def run(app)
    @app = app
  end

  def to_app
    @middlewares.reverse.inject(@app) { |app,middleware| middleware.call(app) }
  end
end

Rack中提供了类似上面描述的Builder,其中还利用了Rack::URLMap处理路由。

最简web框架

Rack为web程序及框架提供了很多有用的设施。Request和Response的存取,路由处理,cookie信息,Session信息,日志。Rack自带最简WEB框架- rackup。

路由

web程序使用不同的代码处理不同的URL,对应关系处理 - 路由 - 路径和代码块的一一对应。\

路由的实现: use和run方法,map的定义:

def map(path, &block)
  if @ins.last.kind_of? Hash
    @ins.last[path] = self.class.new(&block).to_app
  else
    @ins << {}
    map(path, &block)  # 注意这里的递归调用,递归意味着了嵌套
  end
end

所以,如下就是一个嵌套的例子:

#!/usr/bin/env ruby
require "rubygems"
require 'rack'

app = Rack::Builder.new do
  use Rack::ContentLength
  map '/hello' do
    use Rack::CommonLogger
    map '/ketty' do
      run lambda { |env| [200, {"Content-Type" => "text/html"} , 
                          ["from hello-ketty", 
                           "SCRIPT_NAME=#{env['SCRIPT_NAME']}",
                           "PATH_INFO=#{env['PATH_INFO']}"]]}
    end
    map '/everyone' do 
      run lambda { |env| [200, {"Content-Type" => "text/html"},
                          ["from hello-everyone",
                           "SCRIPT_NAME=#{env['SCRIPT_NAME']}",
                           "PATH_INFO=#{env['PATH_INFO']}"]] }
    end
    
    map '/' do
      run lambda { |env| [200, {"Content-Type" => "text/html"}, 
                          ["from hello catch all",
                           "SCRIPT_NAME=#{env['SCRIPT_NAME']}",
                           "PATH_INFO=#{env['PATH_INFO']}"]] }
    end
  end
  map '/world' do 
    run lambda { |env| [200, {"Content-Type" => "text/html"}, ["world"]] }
  end
  map '/' do 
    run lambda { |env| [200, {"Content-Type" => "text/html"}, ["here"]] }
  end
end

Rack::Handler::Thin.run app, :Port => 5000  # 运行应用的处理器

Rackup

rackup命令,使用配置文件(config.ru)运行应用程序。rack config.ru命令所做的事情相当于:

app = Rack::Builder.new { ... 配置文件 ... }.to_app

rackup运行web服务器和端口的方式: rackup -s thin -p 3000

Rackup实现很简单: Rack::Server.startRack::Server是重点,其接口实现如下:

module Rack
  class Server
    def self.start                 # 类方法, Rack::Server的入口,调用实例的start方法
    def self.middleware            # 装配一些缺省的中间件, begin .. end 代码块,开发者环境会装载ShowExceptions和Lint中间件

    def initialize(options = nil)  # 构造函数
    def options                    # 解析命令行参数-ARGV,转化成:server => 'Thin'形式
    def app
    def middleware
    def start                      # 最重要的方法
    def server                     # 获取对应的Rack::Handler
  end
end

备注: 因为这里在讨论Rack::Server的内部实现,所以,特地去研究一下其内部的实现。使用VIM打开命令的方式: cd bundle show rack && vi

中间件是Hash,key是环境名,value为预加载的中间件的数组。

app中存在这样的语句: !::File.exist? options[:config], 这是和作用域有关的,::File表示顶层作用域的类,作用域和名称空间的概念用来组织程序还真是不可小觑。

看到config =~ /\.ru$/后,想到=~之后的表达式中可以嵌套正则语法,为了验证自己的想法,所以使用IRB尝试一下,"String is awesome thing!! =~ /^S.*!$/",觉得REPL(Read-Eval-Print Loop)的编程环境相当的完美。

Server中App的实现:

def app
  @app ||= begin
    if !::File.exist? options[:config]  # options[:config]包含了配置文件名
      abort "configuration #{options[:config]} not found"
    end
    # Rack::Builder读取配置文件
    app, options = Rack::Builder.parse_file(Self,options[:config], opt_parser)
    self.options.merge! options
    app
  end
end

Rack::Builder中的实现, 值得注意是,代码中关于eval "Rack::Builder.new {( " + cfgfile + "\n )}.to_app"的使用,eval,instance_eval,class_eval,分别代表在不同环境下对代码进行求值的语义。

config.ru文件的中,配置行以 #\ 开头,例如: #\ -w -p 8765

def build_app(app)  # app是利用Rack::Builder构造出来的应用程序
  middleware[options[:environment]].reverse_each do |middleware|
    middleware = middleware.call(self) if middleware.respond_to?(:call)
    next unless middleware
    klass = middleware.shift
    app = klass.new(app, *middleware)
  end
  app
end

start方法中,值得关注的有这样的一些:daemonize_app , 在if的条件判断中执行语句。

Rack自带中间件

Rack自身中间件的种类:

  • HTTP协议相关中间件,web框架基石
  • 程序开发相关,代码重载、日志、规格检查等等
  • web应用程序处理的问题,session和文件

熟悉中间件可以深入理解web框架。

HTTP协议相关中间件

HTTP协议相关的中间件有: Rack::Chunked,Rack::ConditionalGet,Rack::ContentType,Rack::Deflater,Rack::Etag,Rack::Head,Rack::MethodOverride

  • Rack::Chunked : HTTP协议的分块传输编码机制(Chunked Transfer Encoding), Transfer-Encoding头字段,需要HTTP 1.1以上的版本,chunk方法调用。
  • Rack::ConditionalGet : 资源未修改时使用客户端缓存内容,Conditional GET请求。服务器的缓存控制(Cache-Control请求头)。智能URL,即在URL之后加上表示修改时间的值 - 资源文件。

如果文档过期了,必须使用服务器进行重新确认,确认后存在两个种情况:改变或未改变。重新确认的方法:

  1. 基于Last-Modified - 适用方便计算出文档最后修改的时间情况,响应头为Last-Modified,例子是: Last-Modified: 20 Sep 2008 18:23:00 GMT
  2. 基于ETag - 依赖文档的内容,同同异异,响应头为If-None-Match,例子是: If-None-Match: 4135cda4de5f
  3. 组合Etag和Last_Modified - 同时包含两个响应头,If-Modified-Since 和 If-None-Match

Rack::ConditionalGet判断当前响应是否代表一个未修改文档的方法:

  • Etag匹配:请求头中的 HTTP_IF_NONE_MATCH 值等于响应头中的 Etag
  • Last-Modified匹配:请求头中的 HTTP_IF_MODIFIED_SINCE 值等于响应头中的 Last-Modified

备注: 尝试了一下Rack编程中提供的关于ETag的例子,结果在尝试Telnet时失败了,话说telnet还真是难用的命令。

  • Rack::ContentLength: Rack程序,多个中间件,HTTP协议-正确的ContentLength,Rack::ContentLength最外层use。

Rack::ContentLength的call方法中实现中例子,判断语句很长,需要判断的条件如下:

if !STATUS_WITH_NO_ENTITY_BODY.include?(status) &&
   !headers['Content-Length'] &&
   !headers['Transfer-Encoding'] &&
   (body.respond_to?(:to_ary) || body.respond_to?(:to_str))  # to_ary或to_str

String类型的size方法和bytesize方法,UTF8字符个数的计算。

传输长度:client和server根据消息长度决定请求/响应的接受完毕,是一种分界规则。如果无法确认消息的长度,可以使用三种方式: chunked传输编码,关闭连接,multipart/byteranges。

!注意:不能确定长度时,一定不能设置Content-Length,或大或小都会引起问题。

  • Rack::ContentType : 设置内容类型(MIME类型注册)。默认为何种类型?
  • Rack::Deflater : HTTP支持对传输内容进行压缩,减少传输量,提高吞吐。涉及的请求头:Accept-Encoding(包含多种格式)/Content-Encoding, RFC包含的编码: gzip,compress,deflate,identity。

内容协商 : 编码的选择,客户端的喜好,服务器端的可用编码格式,协商过程。同一实体响应的多种表现形式,语言编码之类。

删除Content-Length头信息,传输压缩文件,使用chunked传输编码方式。

  • Rack::Etag : 实现HTTP缓存。该中间件只处理头字段中尚未包含”Etag”的情况。将响应转换成字符串,利用Digest::MD5.hexdigest计算Etag(内容很大不可行)。Etag和Rack::ConditionalGet配合使用。
  • Rack::Head : HEAD请求响应体必须为空,这就是Rack::Head所完成的功能
  • Rack::MethodOverride : REST风格,浏览器和web服务器不直接支持PUT和DELETE,POST模拟PUT和DELETE: 表单隐藏的input,HTTP中的X_HTTP_METHOD_OVERRIDE扩展请求头,测试例子:
use Rack::MethodOverride

map '/' do
  # 利用HERE文档插入html文本,如果没有隐藏的input,并且去掉Rack::MethodOverride的引用。<F2>就只是简单的post请求
  form = <<-HERE
  <form action="/user" method="post">
    <input name="_method" type="hidden" value="put" />
    <input name="name" type="text" value="" />
    <input type="submit" value="Modify!">
  </form>
  HERE
  run lambda { |env| [200, {"Content-Type" => "text/html"}, [form]] }
end

map '/user' do
  run lambda { |env|
    req = Rack::Request.new env
    res = Rack::Response.new
    if req.put?
      res.write("you modify user name to #{req.params['name']}")
    else
      res.write("we only support put method to modify user, yours is #{req.request_method}")
    end
    res.finish
  }
end

# 结果
# 127.0.0.1 - - [06/Nov/2014 13:20:03] "GET / HTTP/1.1" 200 - 0.0004
# 127.0.0.1 - - [06/Nov/2014 13:21:22] "PUT /user HTTP/1.1" 200 40 0.0005

程序开发中间件

程序开发的类似模式:写日志、评测性能、检查是否符合规范等。Rack提供了的程序相关的中间件如下:

  • Rack::CommonLogger: Apache Common log格式,合法的错误流: puts, write和flush方法
  • Rack::Lint: 检查请求和响应是否符合Rack规范。Rack检查: 静态检查-call方法实现,动态方法-each方法实现。assert方法,_call方法,check_env(主要涉及session和logger)

Rack要求Ruby应用服务器提供的 env必须包含这些关键字:

%w[REQUEST_METHOD SERVER_NAME SERVER_PORT QUERY_STRING
rack.version rack.input rack.errors
rack.multithread rack.multiprocess rack.run_once].each { |header|
   assert("env missing required key #{header}") { env.include? header }
}

env中的两类关键字: 来自HTTP请求,类CGI的头-全部大写,又分两类(HTTP_开头的HTTP请求头和不以HTTP_开头的) ; 不是从HTTP请求得到,小写,以.分隔。

  • Rack::Reloader: development和production这两个不同的环境,重新加载代码。请求到来时检查,多个线程存在,在临界区执行reload!
  • Rack::Runtime: 计算请求的处理时间,X-Runtime响应头。
  • Rack::Sendfile: URL映射静态文件,X-Sendfile机制,设置响应头。

应用配置和组合中间件

  • Rack::Cascade: 挂载多个应用程序,请求到来时,尝试所有的应用程序,直到404。 这样做的意义: 在Rails应用成嵌入sinatra程序,隔离缓存处理。
  • Rack::Lock: 同一进程的多个并发线程,Rails框架是单线程的。Rack::Lock对请求做互斥锁定

会话管理

HTTP的无状态,各种用户识别的技术:HTTP头识别用户信息,IP地址识别,用户登录,URL嵌入,Cookie。

HTTP Cookies

第一次访问服务器,响应中设置Set-Cookie,然后将Cookie保存到cookie数据库中。cookie也可分为会话cookie和持久cookie(维持应用程序会话-session)。

客户端保存Cookies: domain, path, secure, expiration, name, value。 想看Cookie,不知道怎么看,突然,看到FireBug中提供Cookie选项。这些保存的请求可以发送给对应的服务器。

  • Rack::Session::Cookie: 提供一个基于cookie的会话管理,session-Ruby的Hash对象,采用base64编码保存。

Rack::Session::Cookie所做的工作:

  1. 提取请求cookie中session数据,设置env[ rack.session ]
  2. 处理请求
  3. 将session数据写入到Set-Cookie响应
  • ID session
  • Memcache Session
  • Pool Session

备注: Session部分的处理一直觉得难以理解,现阶段只是知道存在session这个变量,以及session是一个类Hash的存在的变量。

后记

结果,就粗略的看了一遍,花了一个多星期时间,大多数是上班时间,我还真是相当的闲散,呵呵呵。

Rails是2.3时,引入的Rack,Rack和Rails在这些年里都在不断的发展。Rails的server就是继承自Rack的server。

参考文献

  1. Rack 官方库
  2. Rack 插件库



傲娇的使用Disqus