世界上最伟大的投资就是投资自己的教育
rack 介绍与原理
1. 什么是 rack?
把rails
应该部署到线上生产环境时,我们可能会选择unicorn
或puma
,姑且将unicorn
或puma
称为应用容器,它是用来运行 ruby 代码的。而rails
或sinatra
称为 ruby 的 web 框架。我们在本地跑rails
应用的时候,也就是执行rails c
命令,可以是用webrick
来跑的,在线上却又是unicorn
或puma
,之所以如此灵活,就是因为rack
。因为rails
或sinatra
也是实现了rack
的机制,而unicorn
,puma
等也是,所以它们轻易地换,而不影响整个应用。rack
也可以称为一套协议,只要你的 web 应用容器都实现了rack
的机制,而rails
本身也是实现了rack
的机制的,所以可以用任意一套实现rack
机制的应用容器来跑rails
应用,例如thin
,unicorn
,puma
等。
rack本身是一个 gem,我们可以跑一个 hello world。
# rack.rb
require 'rack'
app = Proc.new do |env|
['200', {'Content-Type' => 'text/html'}, ['A barebones rack app.']]
end
Rack::Handler::WEBrick.run app
我们先来运行这个程序。
$ ruby rack.rb
[2016-01-24 12:32:04] INFO WEBrick 1.3.1
[2016-01-24 12:32:04] INFO ruby 2.2.2 (2015-04-13) [x86_64-darwin14]
[2016-01-24 12:32:04] INFO WEBrick::HTTPServer#start: pid=3204 port=8080
WEBrick是一个应用容器,是 ruby 标准库提供的功能,它提供了一个 http server 的功能,用来可以来跑 rack 应用。
可以看到,程序监听在 8080 端口。
我们用浏览器测试一下。
['200', {'Content-Type' => 'text/html'}, ['A barebones rack app.']]
是 rack 返回的数据,它包括三个内容:
- 状态码 200
- 响应头部信息
- 响应内容
我们用curl
工具来测试它返回的响应的头部信息。
$ curl -i http://localhost:8080
HTTP/1.1 200 OK
Content-Type: text/html
Server: WEBrick/1.3.1 (Ruby/2.2.2/2015-04-13)
Date: Sun, 24 Jan 2016 04:50:36 GMT
Content-Length: 21
Connection: Keep-Alive
A barebones rack app.%
除了上述这种方法,还有另外一种方法可以运行 rack。
# config.ru
run Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['get rack\'d']] }
要运行的话可以这样:
$ rackup config.ru
[2016-01-24 13:26:50] INFO WEBrick 1.3.1
[2016-01-24 13:26:50] INFO ruby 2.2.2 (2015-04-13) [x86_64-darwin14]
[2016-01-24 13:26:50] INFO WEBrick::HTTPServer#start: pid=5975 port=9292
2. 原理
可以看到,rackup
也是用webrick
来加载 rack 程序的。上面的代码是指定了用webrick
,而这里没有指定,它也会默认去找 webrick。
为什么呢?这个可以来研究一下 rack 的源码。
像上述所说的webrick
,unicorn
,puma
,thin
等,在官方有个称谓,叫handlers
。
而默认情况下rack
支持的handlers
有下面几种:
WEBrick
FCGI
CGI
SCGI
LiteSpeed
Thin
相关的源码可见于https://github.com/rack/rack/tree/master/lib/rack/handler
。
而为什么没有指定 handler 的情况下,会去找webrick
,可以看下面的代码:
# https://github.com/rack/rack/blob/master/lib/rack/handler.rb
def self.pick(server_names)
server_names = Array(server_names)
server_names.each do |server_name|
begin
return get(server_name.to_s)
rescue LoadError, NameError
end
end
raise LoadError, "Couldn't find handler for: #{server_names.join(', ')}."
end
def self.default
# Guess.
if ENV.include?("PHP_FCGI_CHILDREN")
Rack::Handler::FastCGI
elsif ENV.include?(REQUEST_METHOD)
Rack::Handler::CGI
elsif ENV.include?("RACK_HANDLER")
self.get(ENV["RACK_HANDLER"])
else
pick ['thin', 'puma', 'webrick']
end
end
主要是这一句在发挥作用:pick ['thin', 'puma', 'webrick']
。
也就是说系统会优先选择thin
,之后是puma
,最后才是webrick
。
现在来验证一下我们的想法。
先来安装一下puma
。
$ gem install puma
然后运行 rack 程序。
rackup config.ru
Puma 2.15.3 starting...
* Min threads: 0, max threads: 16
* Environment: development
* Listening on tcp://localhost:9292
果然,rack 程序用 puma 来跑了,而不选择webrick
,因为webrick
排在puma
之后。
回到刚才的问题,为什么指定webrick
的时候就跑在 8080 端口,没指定的时候就跑在 9292 端口呢?
指定webrick
的时候,是这样指定的:
Rack::Handler::WEBrick.run app
来看相关的 rack 源码:
# https://github.com/rack/rack/blob/master/lib/rack/handler/webrick.rb
module Rack
module Handler
class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet
def self.run(app, options={})
environment = ENV['RACK_ENV'] || 'development'
default_host = environment == 'development' ? 'localhost' : nil
options[:BindAddress] = options.delete(:Host) || default_host
options[:Port] ||= 8080
@server = ::WEBrick::HTTPServer.new(options)
@server.mount "/", Rack::Handler::WEBrick, app
yield @server if block_given?
@server.start
end
end
end
end
上面的options[:Port] ||= 8080
显示的就是 8080 端口的。
而没有指定的时候就去跑 9292 端口,也来看下源码:
# https://github.com/rack/rack/blob/028438ffffd95ce1f6197d38c04fa5ea6a034a85/lib/rack/server.rb#L203
def default_options
environment = ENV['RACK_ENV'] || 'development'
default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
{
:environment => environment,
:pid => nil,
:Port => 9292,
:Host => default_host,
:AccessLog => [],
:config => "config.ru"
}
end
也就是说,用rackup
运行的时候会去跑 9292 端口。
先来分析一下为什么执行rackup config.ru
的时候,会发生这么神的事。
# bin/rackup
#!/usr/bin/env ruby
require "rack"
Rack::Server.start
rackup 程序的源码只有几行,我们找到Rack::Server.start
的部分。
第一步,要先解析那个config.ru
文件的内容。
# https://github.com/rack/rack/blob/028438ffffd95ce1f6197d38c04fa5ea6a034a85/lib/rack/server.rb#L313
def build_app_and_options_from_config
if !::File.exist? options[:config]
abort "configuration #{options[:config]} not found"
end
app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
@options.merge!(options) { |key, old, new| old }
app
end
主要是这一行Rack::Builder.parse_file
,这里可以放一放。
先来看看Rack::Server.start
做了什么事?
# https://github.com/rack/rack/blob/028438ffffd95ce1f6197d38c04fa5ea6a034a85/lib/rack/server.rb#L257
def start &blk
if options[:warn]
$-w = true
end
if includes = options[:include]
$LOAD_PATH.unshift(*includes)
end
if library = options[:require]
require library
end
if options[:debug]
$DEBUG = true
require 'pp'
p options[:server]
pp wrapped_app
pp app
end
check_pid! if options[:pid]
# Touch the wrapped app, so that the config.ru is loaded before
# daemonization (i.e. before chdir, etc).
wrapped_app
daemonize_app if options[:daemonize]
write_pid if options[:pid]
trap(:INT) do
if server.respond_to?(:shutdown)
server.shutdown
else
exit
end
end
server.run wrapped_app, options, &blk
end
def server
@_server ||= Rack::Handler.get(options[:server])
unless @_server
@_server = Rack::Handler.default
# We already speak FastCGI
@ignore_options = [:File, :Port] if @_server.to_s == 'Rack::Handler::FastCGI'
end
@_server
end
看到start
方法的最后一行server.run wrapped_app, options, &blk
,就是去执行server
方法,而server
会通过Rack::Handler.get
去找 handler,例如webrick
,就是上面所说的。
找到之后会运行run
方法,比如我们以webrick
为例。
# https://github.com/rack/rack/blob/master/lib/rack/handler/webrick.rb
module Rack
module Handler
class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet
def self.run(app, options={})
environment = ENV['RACK_ENV'] || 'development'
default_host = environment == 'development' ? 'localhost' : nil
options[:BindAddress] = options.delete(:Host) || default_host
options[:Port] ||= 8080
@server = ::WEBrick::HTTPServer.new(options)
@server.mount "/", Rack::Handler::WEBrick, app
yield @server if block_given?
@server.start
end
end
end
end
::WEBrick::HTTPServer.new(options)
这一行是关键,默认情况下,webrick
是开启 tcp 服务的,::WEBrick::HTTPServer.new(options)
开启的是 http 服务,我们来看下webrick
相关的源码:
require 'socket'
module WEBrick
class GenericServer
def start(&block)
raise ServerError, "already started." if @status != :Stop
server_type = @config[:ServerType] || SimpleServer
setup_shutdown_pipe
server_type.start{
@logger.info \
"#{self.class}#start: pid=#{$$} port=#{@config[:Port]}"
call_callback(:StartCallback)
shutdown_pipe = @shutdown_pipe
thgroup = ThreadGroup.new
@status = :Running
begin
while @status == :Running
begin
sp = shutdown_pipe[0]
if svrs = IO.select([sp, *@listeners], nil, nil, 2.0)
if svrs[0].include? sp
# swallow shutdown pipe
buf = String.new
nil while String ===
sp.read_nonblock([sp.nread, 8].max, buf, exception: false)
break
end
svrs[0].each{|svr|
@tokens.pop # blocks while no token is there.
if sock = accept_client(svr)
unless config[:DoNotReverseLookup].nil?
sock.do_not_reverse_lookup = !!config[:DoNotReverseLookup]
end
th = start_thread(sock, &block)
th[:WEBrickThread] = true
thgroup.add(th)
else
@tokens.push(nil)
end
}
end
...
end
ensure
...
end
}
end
end
end
可见,webrick
的是基于 socket 开发的,IO.select([sp, *@listeners], nil, nil, 2.0)
和sock = accept_client(svr)
接收了客户的链接,并把 socket 放到 sock 变量中。
module WEBrick
class HTTPServer < ::WEBrick::GenericServer
def run(sock)
while true
res = HTTPResponse.new(@config)
req = HTTPRequest.new(@config)
server = self
...
end
end
end
end
为什么会接收到 socket 之后去运行run
方法呢,可以查看start_thread
方法中的这一行block ? block.call(sock) : run(sock)
。
HTTPServer
还会对请求的信息进行处理和封装,比如请求的参数,头部信息等,就是这行HTTPRequest.new(@config)
代码发挥的作用。
我们可以来验证一下。
require 'rack'
require 'pp'
app = Proc.new do |env|
pp "*" * 50
pp env
pp "*" * 50
pp env["rack.input"].string
pp "*" * 50
['200', {'Content-Type' => 'text/html'}, ['A barebones rack app.']]
end
Rack::Handler::WEBrick.run app
然后我们给这个 rack 程序发送请求,并指定参数和头部信息。
$ curl -v localhost:8080 -X POST \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"movie": {"title": "Star Wars: A New Hope"}}'
在 log 上看到的输出大约是下面这样的。
$ ruby rack.rb
[2016-01-24 15:47:56] INFO WEBrick 1.3.1
[2016-01-24 15:47:56] INFO ruby 2.2.2 (2015-04-13) [x86_64-darwin14]
[2016-01-24 15:47:56] INFO WEBrick::HTTPServer#start: pid=26645 port=8080
"**************************************************"
{"CONTENT_LENGTH"=>"45",
"CONTENT_TYPE"=>"application/json",
"GATEWAY_INTERFACE"=>"CGI/1.1",
"PATH_INFO"=>"/",
"QUERY_STRING"=>"",
"REMOTE_ADDR"=>"::1",
"REMOTE_HOST"=>"localhost",
"REQUEST_METHOD"=>"POST",
"REQUEST_URI"=>"http://localhost:8080/",
"SCRIPT_NAME"=>"",
"SERVER_NAME"=>"localhost",
"SERVER_PORT"=>"8080",
"SERVER_PROTOCOL"=>"HTTP/1.1",
"SERVER_SOFTWARE"=>"WEBrick/1.3.1 (Ruby/2.2.2/2015-04-13)",
"HTTP_USER_AGENT"=>"curl/7.37.1",
"HTTP_HOST"=>"localhost:8080",
"HTTP_ACCEPT"=>"application/json",
"rack.version"=>[1, 3],
"rack.input"=>#<StringIO:0x007fafa29c3de0>,
"rack.errors"=>#<IO:<STDERR>>,
"rack.multithread"=>true,
"rack.multiprocess"=>false,
"rack.run_once"=>false,
"rack.url_scheme"=>"http",
"rack.hijack?"=>true,
"rack.hijack"=>
#<Proc:0x007fafa29c3c78@/Users/macintosh1/.rbenv/versions/2.2.2/lib/ruby/gems/2.2.0/gems/rack-1.6.4/lib/rack/handler/webrick.rb:76 (lambda)>,
"rack.hijack_io"=>nil,
"HTTP_VERSION"=>"HTTP/1.1",
"REQUEST_PATH"=>"/"}
"**************************************************"
"{\"movie\": {\"title\": \"Star Wars: A New Hope\"}}"
"**************************************************"
它能把我使用的请求协议,端口,地址,请求参数,还有头部信息全部得到,这又是如何办到的呢?
所有的一切都是在这个文件里实现的https://github.com/ruby/ruby/blob/3e92b635fb5422207b7bbdc924e292e51e21f040/lib/webrick/httprequest.rb
。
比如:
def meta_vars
meta = Hash.new
cl = self["Content-Length"]
ct = self["Content-Type"]
meta["CONTENT_LENGTH"] = cl if cl.to_i > 0
meta["CONTENT_TYPE"] = ct.dup if ct
meta["GATEWAY_INTERFACE"] = "CGI/1.1"
meta["PATH_INFO"] = @path_info ? @path_info.dup : ""
#meta["PATH_TRANSLATED"] = nil # no plan to be provided
meta["QUERY_STRING"] = @query_string ? @query_string.dup : ""
meta["REMOTE_ADDR"] = @peeraddr[3]
meta["REMOTE_HOST"] = @peeraddr[2]
#meta["REMOTE_IDENT"] = nil # no plan to be provided
meta["REMOTE_USER"] = @user
meta["REQUEST_METHOD"] = @request_method.dup
meta["REQUEST_URI"] = @request_uri.to_s
meta["SCRIPT_NAME"] = @script_name.dup
meta["SERVER_NAME"] = @host
meta["SERVER_PORT"] = @port.to_s
meta["SERVER_PROTOCOL"] = "HTTP/" + @config[:HTTPVersion].to_s
meta["SERVER_SOFTWARE"] = @config[:ServerSoftware].dup
self.each{|key, val|
next if /^content-type$/i =~ key
next if /^content-length$/i =~ key
name = "HTTP_" + key
name.gsub!(/-/o, "_")
name.upcase!
meta[name] = val
}
meta
end
和输出的内容很相似吧!
完结。
本站文章均为原创内容,如需转载请注明出处,谢谢。
© 汕尾市求知科技有限公司 | Rails365 Gitlab | 知乎 | b 站 | csdn
粤公网安备 44152102000088号 | 粤ICP备19038915号
Top