Rails Ruby webrick

ane · August 24, 2017 · 2770 hits

写在开始前,一个简易的 webrick 服务

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。

初始化

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 的实例。

HTTPServer 的初始化

# 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

自定义 servlet

只需要继承 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 相关的处理,可以具体看代码,其实已经不算很复杂了

No Reply at the moment.
You need to Sign in before reply, if you don't have an account, please Sign up first.