webrick源码分析——http请求

2010-02-23 22:52:19 +0800

http服务器的主要工作就是解析http请求,然后返回http应答。http请求从socket读入,就是一段特定格式的字符串,下面是访问huangzhimn.com首页的http请求

GET / HTTP/1.1\r\n
Host: www.huangzhimin.com\r\n
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
Accept-Language: en-us,en;q=0.5\r\n
Accept-Encoding: gzip,deflate\r\n
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n
Keep-Alive: 300\r\n
Connection: keep-alive\r\n
\r\n

那么webrick是解析这段字符串的呢?之前在分析webrick主要流程的时候讲到,在http服务器从socket读取到数据时,立刻交给WEBrick::HTTPRequest类来解析,解析方法如下

def parse(socket=nil)
  @socket = socket
  begin
    @peeraddr = socket.respond_to?(:peeraddr) ? socket.peeraddr : []
    @addr = socket.respond_to?(:addr) ? socket.addr : []
  rescue Errno::ENOTCONN
    raise HTTPStatus::EOFError
  end

  read_request_line(socket)
  if @http_version.major > 0
    read_header(socket)
    @header['cookie'].each{|cookie|
      @cookies += Cookie::parse(cookie)
    }
    @accept = HTTPUtils.parse_qvalues(self['accept'])
    @accept_charset = HTTPUtils.parse_qvalues(self['accept-charset'])
    @accept_encoding = HTTPUtils.parse_qvalues(self['accept-encoding'])
    @accept_language = HTTPUtils.parse_qvalues(self['accept-language'])
  end
  return if @request_method == "CONNECT"
  return if @unparsed_uri == "*"

  begin
    @request_uri = parse_uri(@unparsed_uri)
    @path = HTTPUtils::unescape(@request_uri.path)
    @path = HTTPUtils::normalize_path(@path)
    @host = @request_uri.host
    @port = @request_uri.port
    @query_string = @request_uri.query
    @script_name = ""
    @path_info = @path.dup
  rescue
    raise HTTPStatus::BadRequest, "bad URI `#{@unparsed_uri}'."
  end

  if /close/io =~ self["connection"]
    @keep_alive = false
  elsif /keep-alive/io =~ self["connection"]
    @keep_alive = true
  elsif @http_version < "1.1"
    @keep_alive = false
  else
    @keep_alive = true
  end
end

第3-8行,读取对方和自己的地址信息(host, port, id)

第10行,解析http请求的第一行数据,内容为“GET / HTTP/1.1\r\n”,具体解析方法如下

def read_request_line(socket)
  @request_line = read_line(socket) if socket
  @request_time = Time.now
  raise HTTPStatus::EOFError unless @request_line
  if /^(\S+)\s+(\S+)(?:\s+HTTP\/(\d+\.\d+))?\r?\n/mo =~ @request_line
    @request_method = $1
    @unparsed_uri   = $2
    @http_version   = HTTPVersion.new($3 ? $3 : "0.9")
  else
    rl = @request_line.sub(/\x0d?\x0a\z/o, '')
    raise HTTPStatus::BadRequest, "bad Request-Line `#{rl}'."
  end
end

读取http请求的第一行,读取之后通过正则匹配获取@request_method为'GET',@unparsed_url为'/',@http_version为‘1.1’

第11-20行,当http版本为1.0或1.1时,对http头部进行处理

首先,读取http头,读取方法如下:

def read_header(socket)
  if socket
    while line = read_line(socket)
      break if /\A(#{CRLF}|#{LF})\z/om =~ line
      @raw_header << line
    end
  end
  begin
    @header = HTTPUtils::parse_header(@raw_header)
  rescue => ex
    raise  HTTPStatus::BadRequest, ex.message
  end
end

从socket一行一行地读取数据,直到一行为”\r\n“,并通过HTTPUTils::parse_header方法将字符串数组@raw_header转换为散列@header

接着,读取cookies,将cookie字符串解析为Cookie对象

然后是读取accept, accept-charset, accept-encoding, accept-language值,这些值都是多选的,比如“Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n”,所以通过HTTPUtils::parse_qvalues解析出来的结果是一个数组,而且按照q值来排序

第21行,当request_method为CONNECT时(用于http代理,http1.1协议新增的),不再继续

第25行,将字符串@unparsed_uri转换成正规的URI实例@parsed_uri

第26-30行,通过@parsed_uri获取@path, @host, @port和@query_string

最后,第37-45行,设置keep-alive值。

 

WEBrick::HTTPRequest类另外一个重要的方法是body

def body(&block)
  block ||= Proc.new{|chunk| @body << chunk }
  read_body(@socket, block)
  @body.empty? ? nil : @body
end
def read_body(socket, block)
  return unless socket
  if tc = self['transfer-encoding']
    case tc
    when /chunked/io then read_chunked(socket, block)
    else raise HTTPStatus::NotImplemented, "Transfer-Encoding: #{tc}."
    end
  elsif self['content-length'] || @remaining_size
    @remaining_size ||= self['content-length'].to_i
    while @remaining_size > 0 
      sz = BUFSIZE < @remaining_size ? BUFSIZE : @remaining_size
      break unless buf = read_data(socket, sz)
      @remaining_size -= buf.size
      block.call(buf)
    end
    if @remaining_size > 0 && @socket.eof?
      raise HTTPStatus::BadRequest, "invalid body size."
    end
  elsif BODY_CONTAINABLE_METHODS.member?(@request_method)
    raise HTTPStatus::LengthRequired
  end
  return @body
end

如果http body为空,则返回nil

http body分为两种,一种是数据一次性全部传入,另一种是一段一段分批传输(chunked)。

第8-18行就是处理一次性全部传入的数据,根据header中content-length来读取指定长度的数据。

第3-7行读取chunked分段数据,读取方法为

def read_chunked(socket, block)
  chunk_size, = read_chunk_size(socket)
  while chunk_size > 0
    data = ""
    while data.size < chunk_size
      tmp = read_data(socket, chunk_size-data.size) # read chunk-data
      break unless tmp
      data << tmp
    end
    if data.nil? || data.size != chunk_size
      raise BadRequest, "bad chunk data size."
    end
    read_line(socket)                    # skip CRLF
    block.call(data)
    chunk_size, = read_chunk_size(socket)
  end
  read_header(socket)                    # trailer + CRLF
  @header.delete("transfer-encoding")
  @remaining_size = 0
end

chunked分段数据,第一行表明这一段数据的长度,用十六进制表示,第二行开始为需要读取的分段数据。所以读取chunked数据就是读一行chunk_size,读一行chunk data,直到读完为止。

 

最后看看WEBrick::HTTPRequest的meta方法,对CGI的理解很有帮助

def meta_vars
  # This method provides the metavariables defined by the revision 3
  # of ``The WWW Common Gateway Interface Version 1.1''.
  # (http://Web.Golux.Com/coar/cgi/)

  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

 

webrick源码分析──路由

2010-01-25 22:53:29 +0800

webrick的路由是由WEBrick::HTTPServer::MountTable定义的

MountTable由@tab和@scanner组成,@tab是一个由script_name到Servlet的Hash,@scanner一个可以匹配所有script_name的正则表达式。其定义如下:

class MountTable
  def initialize
    @tab = Hash.new
    compile
  end

  def [](dir)
    dir = normalize(dir)
    @tab[dir]
  end

  def []=(dir, val)
    dir = normalize(dir)
    @tab[dir] = val
    compile
    val
  end

  def delete(dir)
    dir = normalize(dir)
    res = @tab.delete(dir)
    compile
    res
  end

  def scan(path)
    @scanner =~ path
    [ $&, $' ]
  end
end

MountTable只提供了四个方法:

[] 根据script_name获取相应的Servlet
[]= 定义scrpt_name与Servlet的对应关系
delete 删除script_name到Servlet的映射
scan 根据request的path返回相应的script_name和path_info

另外normalize和compile是MountTable的私有方法,normalize会删除url最后的'/',compile生成可以匹配所有script_name的正则表达式

看完定义之后,先来看看我们是如何定义路由的

1. 定义根目录

doc_root = '/home/flyerhzm'
server.mount("/", WEBrick::HTTPServlet::FileHandler, doc_root, {:FancyIndexing=>true})

2. 定义任意目录

cgi_dir = '/home/flyerhzm/cgi-bin'
server.mount("/cgi-bin", WEBrick::HTTPServlet::FileHandler, cgi_dir, {:FancyIndexing=>true})

上面定义了两个由FileHandler处理的路由,当path为‘/'时,在'/home/flyerhzm'目录下查找相应的文件,当path为'/cgi-bin'时,在'/home/flyerhzm/cgi-bin'目录下查找相应的文件,选项:FancyIndexing=>true表示,在path对应为某个目录时,显示目录下的所有文件。对应到MountTable的@tab为

""=>[WEBrick::HTTPServlet::FileHandler, ["/home/flyerhzm", {:FancyIndexing=>true}]],
"/cgi-bin"=>[WEBrick::HTTPServlet::FileHandler, ["/home/flyerhzm/cgi-bin", {:FancyIndexing=>true}]]

3. 定义Servlet路径

class GreetingServlet < WEBrick::HTTPServlet::AbstractServlet
  def do_GET(req, resp)
    if req.query['name']
      resp.body = "#{@options[0]} #{req.query['name']}. #{@options[1]}"
      raise WEBrick::HTTPStatus::OK
    else
      raise WEBrick::HTTPStatus::PreconditionFailed.new("missing attribute: 'name'")
    end
  end
  alias do_POST do_GET
end
server.mount('/greet', GreetingServlet, 'Hi', 'Are you having a nice day?')

当path为'/greet'时,由GreetingServlet来处理,选项options = ['Hi', 'Are you having a nice day?'],其对应到MountTable的@tab为

"/greet"=>[GreetingServlet, ["Hi", "Are you having a nice day?"]]

4. webrick还可以mount一个proc

server.mount_proc('/myblock') {|req, resp|
  resp.body = "a block mounted at #{req.script_name}"
}

当path为'/myblock'时,执行这个proc,其对应到MountTable的@tab为

"/myproc"=>[#<WEBrick::HTTPServlet::ProcHandler:0x5ce54 @proc=#<Proc:0x00026c8c@webrick_test.rb:18>>, []]

接下来,让我们看看webrick是如何执行mount操作的

在httpserver初始化的时候,执行

@mount_tab = MountTable.new
if @config[:DocumentRoot]
  mount("/", HTTPServlet::FileHandler, @config[:DocumentRoot],
        @config[:DocumentRootOptions])
end

初始化MountTable,同时检查DocumentRoot参数是否设置,如果设置的话,就mount到根目录

mount, mount_proc和unmount方法定义如下

def mount(dir, servlet, *options)
  @logger.debug(sprintf("%s is mounted on %s.", servlet.inspect, dir))
  @mount_tab[dir] = [ servlet, options ]
end

def mount_proc(dir, proc=nil, &block)
  proc ||= block
  raise HTTPServerError, "must pass a proc or block" unless proc
  mount(dir, HTTPServlet::ProcHandler.new(proc))
end

def unmount(dir)
  @logger.debug(sprintf("unmount %s.", dir))
  @mount_tab.delete(dir)
end
alias umount unmount

非常简单,只是调用MountTable提供的方法。

然后来看看webrick是如何根据url来找到相应的servlet。其关键是search_servlet方法

def search_servlet(path)
  script_name, path_info = @mount_tab.scan(path)
  servlet, options = @mount_tab[script_name]
  if servlet
    [ servlet, options, script_name, path_info ]
  end
end

参数path就是request的path,经过MountTable#scan解析,分解为script_name和path_info,而通过script_name就能从MountTable中获取servlet类型和选项,WEBrick再根据这个servlet类型和选项,实例化一个servlet,执行用户请求。

webrick源码分析──主要流程

2010-01-10 22:10:19 +0800

webrick作为ruby自带的一个http server,很适合拿来作为学习之用。首先来看看最简单的使用webrick的示例吧

require 'webrick'

server = WEBrick::HTTPServer.new({:Port => 3000, :DocumentRoot => '/home/flyerhzm/public_html'})

['INT', 'TERM'].each { |signal|
   trap(signal) { server.shutdown }
}

server.start

 这段代码主要是定义了http服务器监听3000端口,根目录在/home/flyerhzm/public_html下,在接收INT或TERM信号时,关闭服务器,然后启动服务器。

我们分两部分来看,首先看看服务器初始化时做了些什么

class GenericServer
  attr_reader :status, :config, :logger, :tokens, :listeners

  def initialize(config={}, default=Config::General)
    @config = default.dup.update(config)
    @status = :Stop
    @config[:Logger] ||= Log::new
    @logger = @config[:Logger]

    @tokens = 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 = []
    unless @config[:DoNotListen]
      if @config[:Listen]
        warn(":Listen option is deprecated; use GenericServer#listen")
      end
      listen(@config[:BindAddress], @config[:Port])
      if @config[:Port] == 0
        @config[:Port] = @listeners[0].addr[1]
      end
    end
  end
end

class HTTPServer < ::WEBrick::GenericServer
  def initialize(config={}, default=Config::HTTP)
    super
    @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
end

WEBrick::HTTPServer继承自WEBrick::GenericServer

WEBrick::GenericServer初始化时

首先记录所有的配置信息,预定义的WEBrick::Config::HTTP和用户定义的配置信息,包括监听端口,请求超时时间,文档根目录等等。

接着生成一个定长的队列SizedQueue,用来控制最大的客户端连接数。注意这里的SizedQueue放入的并不是一个线程,而是nil。

然后打印当前的WEBrick版本号和Ruby版本号。

最后调用listen方法,生成TCPServer,监听端口。这里可能生成两个TCPServer,一个是IPv4的,一个是IPv6的。

WEBrick::HTTPServer初始化时主要是定义了http版本号,根据配置信息mount根目录,这里将http://localhost/映射到/home/flyerhzm/public_html/目录,默认DirectoryIndex为["index.html","index.htm","index.cgi","index.rhtml"],即请求为目录时,显示目录下的index.html, index.htm, index.cgi或者index.rhtml,DocumentRootOptions为{ :FancyIndexing => true },即请求为目录且目录下没有DirectoryIndex定义的文件时,显示目录下的所有文件。这些都是在WEBrick::HTTPServlet::FileHandler中定义的,我会在之后的文章介绍。

介绍完初始化,下面来看看start方法是如何实现的

def start(&block)
  raise ServerError, "already started." if @status != :Stop
  server_type = @config[:ServerType] || SimpleServer

  server_type.start{
    @logger.info \
      "#{self.class}#start: pid=#{$} port=#{@config[:Port]}"
    call_callback(:StartCallback)

    thgroup = ThreadGroup.new
    @status = :Running
    while @status == :Running
      begin
        if svrs = IO.select(@listeners, nil, nil, 2.0)
          svrs[0].each{|svr|
            @tokens.pop          # blocks while no token is there.
            if sock = accept_client(svr)
              th = start_thread(sock, &block)
              th[:WEBrickThread] = true
              thgroup.add(th)
            else
              @tokens.push(nil)
            end
          }
        end
      rescue Errno::EBADF, IOError => ex
        # if the listening socket was closed in GenericServer#shutdown,
        # IO::select raise it.
      rescue Exception => ex
        msg = "#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"
        @logger.error msg
      end
    end

    @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

首先,根据不同的ServerType执行不同的start方法,定义有SimpleServer和Daemon两种,Daemon方式会在以后的文章中介绍,默认为SimpleServer

class SimpleServer
  def SimpleServer.start
    yield
  end
end

非常简单,就是直接执行传过来的block

在这个block中生成一个线程组,用来存放处理http请求的线程。

IO.select(@listeners, nil, nil, 2.0)方法监听@listeners(就是tcp server),一旦有数据进入就返回,并设置2秒超时,防止进程被挂死。

对于客户端连接的socket请求,创建一个新的线程来处理,并把这个线程放入线程组中。这里用了一个小技巧来控制线程组中线程的数量。一般我们是将线程插入到SizedQueue来控制线程的数量,而这里SizedQueue插入满nil,每次创建一个线程之前,先从SizedQueue pop一个nil,每次线程处理完在push一个nil,这样,当创建了一定数量的线程时,SizedQueue就为空,无法再pop数据,只有等待一个线程处理完后才能继续。

接着先来看看如何关闭服务器。webrick提供了两种方法:

def stop
  if @status == :Running
    @status = :Shutdown
  end
end

一是stop,它只是简单地将服务器的状态由Running改为Shutdown,这样就可以从start方法中的循环跳出来,不过由于start方法最后有这么一句话:thgroup.list.each{|th| th.join if th[:WEBrickThread] },这表示服务器并不会马上关闭,它会等到线程组中所有的线程都执行完毕之后再关闭。

def shutdown
  stop
  @listeners.each{|s|
    if @logger.debug?
      addr = s.addr
      @logger.debug("close TCPSocket(#{addr[2]}, #{addr[1]})")
    end
    s.close
  }
  @listeners.clear
end

二是shutdown,它首先执行stop方法,然后遍历所有的sockets并关闭,这样所有的线程都会买上结束,服务器也会马上停止。

再来看看每个线程都做了些什么

def start_thread(sock, &block)
  Thread.start{
    begin
      Thread.current[:WEBrickSocket] = sock
      begin
        addr = sock.peeraddr
        @logger.debug "accept: #{addr[3]}:#{addr[1]}"
      rescue SocketError
        @logger.debug "accept: <address unknown>"
        raise
      end
      call_callback(:AcceptCallback, sock)
      block ? block.call(sock) : run(sock)
    rescue Errno::ENOTCONN
      @logger.debug "Errno::ENOTCONN raised"
    rescue ServerError => ex
      msg = "#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"
      @logger.error msg
    rescue Exception => ex
      @logger.error ex
    ensure
      @tokens.push(nil)
      Thread.current[:WEBrickSocket] = nil
      if addr
        @logger.debug "close: #{addr[3]}:#{addr[1]}"
      else
        @logger.debug "close: <address unknown>"
      end
      sock.close
    end
  }
end

如果传入一个block,就执行这个block,不然就执行run方法,run方法的定义在WEBrick::HTTPServer下

def run(sock)
  while true 
    res = HTTPResponse.new(@config)
    req = HTTPRequest.new(@config)
    server = self
    begin
      timeout = @config[:RequestTimeout]
      while timeout > 0
        break if IO.select([sock], nil, nil, 0.5)
        timeout = 0 if @status != :Running
        timeout -= 0.5
      end
      raise HTTPStatus::EOFError if timeout <= 0 || sock.eof?
      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] || server[:RequestHandler]
        callback.call(req, res)
      end
      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
        req.fixup()
        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

run方法中,首先,根据配置信息实例化HTTPResponse和HTTPRequest,在设置的timeout之内读取socket数据,不然停止执行。request对象读取socket数据并根据HTTP协议进行解析(关于http请求和应答的解析将在后文进行介绍),将部分内容(request_method, request_uri等等)赋值给response对象。调用service方法,根据request进行操作,并返回相应的response。最后,通过socket将response发送给客户端。需要注意的是,如果http版本是1.1而且keep_alive为true的话,run方法的循环将一直执行,来保持与客户端的长连接。

最后看看service方法的代码

def service(req, res)
  if req.unparsed_uri == "*"
    if req.request_method == "OPTIONS"
      do_OPTIONS(req, res)
      raise HTTPStatus::OK
    end
    raise HTTPStatus::NotFound, "`#{req.unparsed_uri}' not found."
  end

  servlet, options, script_name, path_info = search_servlet(req.path)
  raise HTTPStatus::NotFound, "`#{req.path}' not found." unless servlet
  req.script_name = script_name
  req.path_info = path_info
  si = servlet.get_instance(self, *options)
  @logger.debug(format("%s is invoked.", si.class.name))
  si.service(req, res)
end

对于OPTIONS请求是需要特殊处理,返回可以处理的请求(GET, HEAD, POST, OPTIONS),根据请求的path返回相应的servlet,options, script_name和path_info,并获取到servlet实例(一般是用户定义的Servlet类,WEBrick默认有FileHandler, CGIHandler和ProcHandler),然后由具体的servlet实例来处理http请求。

这就是WEBrick的主要流程,写得比较乱,之后的文章根据WEBrick的功能一部分一部分详细介绍。