Rack学习笔记
缘起
最初学习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.start
,Rack::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之后加上表示修改时间的值 - 资源文件。
如果文档过期了,必须使用服务器进行重新确认,确认后存在两个种情况:改变或未改变。重新确认的方法:
- 基于Last-Modified - 适用方便计算出文档最后修改的时间情况,响应头为Last-Modified,例子是:
Last-Modified: 20 Sep 2008 18:23:00 GMT
- 基于ETag - 依赖文档的内容,同同异异,响应头为If-None-Match,例子是:
If-None-Match: 4135cda4de5f
- 组合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所做的工作:
- 提取请求cookie中session数据,设置env[ rack.session ]
- 处理请求
- 将session数据写入到Set-Cookie响应
- ID session
- Memcache Session
- Pool Session
备注: Session部分的处理一直觉得难以理解,现阶段只是知道存在session这个变量,以及session是一个类Hash的存在的变量。
后记
结果,就粗略的看了一遍,花了一个多星期时间,大多数是上班时间,我还真是相当的闲散,呵呵呵。
Rails是2.3时,引入的Rack,Rack和Rails在这些年里都在不断的发展。Rails的server就是继承自Rack的server。
参考文献
傲娇的使用Disqus