require ‘webrick'
root = File.expand_path 'public_html'
server = WEBrick::HTTPServer.new :Port => 8000, :DocumentRoot => root
# trap 'INT’作用是当收到’control+c’的中断信号就退出server,用过'rails s' 的应该都清楚
trap 'INT' do server.shutdown end
server.start
代码 so 简单,效果如下:
➜ rake ruby webrick.rb
[2017-08-24 10:36:45] INFO WEBrick 1.3.1
[2017-08-24 10:36:45] INFO ruby 2.3.0 (2015-12-25) [x86_64-darwin15]
[2017-08-24 10:36:45] INFO WEBrick::HTTPServer#start: pid=11803 port=8000
在 public_html 文件夹下放入 index.html,这就算你的首页了。 这里涉及的类就很少了,只有一个 WEBrick::HTTPServer,和它的父类::WEBrick::GenericServer。
def initialize(config={}, default=Config::General)
@config = default.dup.update(config)
@status = :Stop
@config[:Logger] ||= Log::new
@logger = @config[:Logger]
# 初始化最大连接数
@tokens = Thread::SizedQueue.new(@config[:MaxClients])
@config[:MaxClients].times{ @tokens.push(nil) }
webrickv = WEBrick::VERSION
rubyv = "#{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"
@logger.info("WEBrick #{webrickv}")
@logger.info("ruby #{rubyv}")
@listeners = []
@shutdown_pipe = nil
unless @config[:DoNotListen]
if @config[:Listen]
warn(":Listen option is deprecated; use GenericServer#listen")
end
# 监听端口和IP,最终返回一个TCPServer。
listen(@config[:BindAddress], @config[:Port])
if @config[:Port] == 0
@config[:Port] = @listeners[0].addr[1]
end
end
end
整个初始化,就是返回一个 TCPServer 的 socket。TCP/IP stream 型连接的服务器端套接字的类。accept 实例方法会受理客户端的连接请求,返回已连接的 TCPSocket 的实例。
# Creates a new HTTP server according to +config+
#
# An HTTP server uses the following attributes:
#
# :AccessLog:: An array of access logs. See WEBrick::AccessLog
# :BindAddress:: Local address for the server to bind to
# :DocumentRoot:: Root path to serve files from
# :DocumentRootOptions:: Options for the default HTTPServlet::FileHandler
# :HTTPVersion:: The HTTP version of this server
# :Port:: Port to listen on
# :RequestCallback:: Called with a request and response before each
# request is serviced.
# :RequestTimeout:: Maximum time to wait between requests
# :ServerAlias:: Array of alternate names for this server for virtual
# hosting
# :ServerName:: Name for this server for virtual hosting
def initialize(config={}, default=Config::HTTP)
super(config, default)
@http_version = HTTPVersion::convert(@config[:HTTPVersion])
@mount_tab = MountTable.new
if @config[:DocumentRoot]
mount("/", HTTPServlet::FileHandler, @config[:DocumentRoot],
@config[:DocumentRootOptions])
end
unless @config[:AccessLog]
@config[:AccessLog] = [
[ $stderr, AccessLog::COMMON_LOG_FORMAT ],
[ $stderr, AccessLog::REFERER_LOG_FORMAT ]
]
end
@virtual_hosts = Array.new
end
之前有人问 mount 的意思,这里就写的很清楚了
##
# Mounts +servlet+ on +dir+ passing +options+ to the servlet at creation
# time
def mount(dir, servlet, *options)
@logger.debug(sprintf("%s is mounted on %s.", servlet.inspect, dir))
@mount_tab[dir] = [ servlet, options ]
end
mount 就是把不同 servlet 和不同的 dir 对应起来。 webrick 自带 4 个 servlet,分别处理 CGI scripts, ERB pages, Ruby blocks,directory HTTPServlet::FileHandler,就不言而喻了。
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]
# 多路复用IO。参数列表前三项为输入/输出/异常的IO(或者子类)的实例数组。第四个参数是timeout。返回一个包含3个元素的数组,这3个元素分别是等待输入/输
# 出/异常的对象的数组(指定数组的子集)
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.
# 本质就是调用socket的accept
if sock = accept_client(svr)
unless config[:DoNotReverseLookup].nil?
sock.do_not_reverse_lookup = !!config[:DoNotReverseLookup]
end
# start_thread下面会做进一步解释
th = start_thread(sock, &block)
th[:WEBrickThread] = true
thgroup.add(th)
else
@tokens.push(nil)
end
}
end
rescue Errno::EBADF, Errno::ENOTSOCK, IOError => ex
# if the listening socket was closed in GenericServer#shutdown,
# IO::select raise it.
rescue StandardError => ex
msg = "#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"
@logger.error msg
rescue Exception => ex
@logger.fatal ex
raise
end
end
ensure
cleanup_shutdown_pipe(shutdown_pipe)
cleanup_listener
@status = :Shutdown
@logger.info "going to shutdown ..."
thgroup.list.each{|th| th.join if th[:WEBrickThread] }
call_callback(:StopCallback)
@logger.info "#{self.class}#start done."
@status = :Stop
end
}
end
tokens 的作用类似信号量,初始化 server 的时候,会把 tokens 用 nil 填充满,只有能从 token 获取到信号的时候,才可以创建线程,获取不到信号的时候,会阻塞主线程,以此控制并发数量,具体看 SizedQueue 的定义吧
##
# Accepts a TCP client socket from the TCP server socket +svr+ and returns
# the client socket.
def accept_client(svr)
sock = nil
begin
sock = svr.accept
sock.sync = true
Utils::set_non_blocking(sock)
rescue Errno::ECONNRESET, Errno::ECONNABORTED,
Errno::EPROTO, Errno::EINVAL
rescue StandardError => ex
msg = "#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"
@logger.error msg
end
return sock
end
其中 accept_client 会不断返回一个连接好的 socket,针对每个与客户端通信的 socket,webrick 会创建一个线程,在 start_thread 中处理,start_thread 里只要一行主要代码,执行
run(sock)
def start_thread(sock, &block)
...
block ? block.call(sock) : run(sock)
...
end
所以,对作者 GenericServer 类的注释,只有这一行,可见 run 方法很重要
##
# Base TCP server class. You must subclass GenericServer and provide a #run
# method.
##
# Processes requests on +sock+
def run(sock)
while true
res = HTTPResponse.new(@config)
req = HTTPRequest.new(@config)
server = self
begin
timeout = @config[:RequestTimeout]
while timeout > 0
break if sock.to_io.wait_readable(0.5)
break if @status != :Running
timeout -= 0.5
end
raise HTTPStatus::EOFError if timeout <= 0 || @status != :Running
raise HTTPStatus::EOFError if sock.eof?
# Parses a request from +socket+. 先是解析请求行,再是请求报文头部解析,最后确#定keep_alive
req.parse(sock)
res.request_method = req.request_method
res.request_uri = req.request_uri
res.request_http_version = req.http_version
res.keep_alive = req.keep_alive?
server = lookup_server(req) || self
if callback = server[:RequestCallback]
callback.call(req, res)
elsif callback = server[:RequestHandler]
msg = ":RequestHandler is deprecated, please use :RequestCallback"
@logger.warn(msg)
callback.call(req, res)
end
# 调用get_instance实例化对应的servlet,并且调用servlet的service方法,组装res.body等等
server.service(req, res)
rescue HTTPStatus::EOFError, HTTPStatus::RequestTimeout => ex
res.set_error(ex)
rescue HTTPStatus::Error => ex
@logger.error(ex.message)
res.set_error(ex)
rescue HTTPStatus::Status => ex
res.status = ex.code
rescue StandardError => ex
@logger.error(ex)
res.set_error(ex, true)
ensure
if req.request_line
if req.keep_alive? && res.keep_alive?
req.fixup()
end
res.send_response(sock)
server.access_log(@config, req, res)
end
end
break if @http_version < "1.1"
break unless req.keep_alive?
break unless res.keep_alive?
end
end
只需要继承 WEBrick::HTTPServlet::AbstractServlet,并且提供相应的 do_GET 方法,就基本能实现一个简单的 servlet。可以对 request 和 response 做一些处理。
require 'webrick'
class Simple < WEBrick::HTTPServlet::AbstractServlet
def do_GET request, response
status, content_type, body = do_stuff_with request
response.status = status
response['Content-Type'] = content_type
response.body = "#{body} Hello, World!"
end
def do_stuff_with request
return 200, 'text/plain', 'you got a page'
end
end
class Configurable < Simple
def initialize server, color, size
super server
@color = color
@size = size
end
def do_stuff_with request
content = "<p " +
%q{style="color: #{@color}; font-size: #{@size}"} +
">Hello, World!"
return 200, "text/html", content
end
end
server = WEBrick::HTTPServer.new :Port => 8000
server.mount '/simple', Simple
server.mount '/configurable', Configurable, 'red', '2em'
server.start
除了提到的,webrick 里还涉及到 log,cookie,auth,https,proxy,cgi,HTTPRequest 和 HTTPResponse 相关的处理,可以具体看代码,其实已经不算很复杂了