分享 Puma 源代码分析 - HTTP 协议解析

ylt · 2015年03月15日 · 最后由 ylt 回复于 2016年07月03日 · 7425 次阅读
本帖已被管理员设置为精华贴

puma 的 http 协议解析

总览

对一个 web 服务器来说,http 协议解析模块是性能关键的部分。为了性能考虑,puma 的 http 协议解析器是用 C 语言写 ruby 扩展实现的。这部分代码原来是 Zed A. Shaw 为 Mongrel 所写,后来被移植到了 Unicorn、Thin 和 Puma。

上一节分析 socket 的时候已经看到,当客户端连接的 socket 有数据可读的时候,解析 http 协议部分的代码是

require 'puma/puma_http11'

@parser = HttpParser.new
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
if @parser.finished?
  return ...
end

这里 parser 相关的方法就是 C 语言实现的。其涉及到的关键文件有:http11_parser.rl, http11_parser.h, http11_parser.c, puma_http11.c。其中 http11_parser.c 其实是由 http11_parser.rl 生成的。这里使用到了一个工具 Ragel,网址ragel。它是一个类似 lex 的正则表达式分析器生成工具。在 puma 中使用了 ragel 的格式描述 http 协议。这一章的代码可能是最难懂的,因为涉及到了 ragel/c/ruby 扩展等很多知识点。

http 协议解析的接口

Http 协议解析的关键部分是理解 puma_parser 结构,这是 ragel/c/ruby 之间进行数据交换的关键结构。

typedef struct puma_parser {
  int cs;               ragel解析的当前状态
  size_t body_start;    body开始
  int content_len;      content长度
  size_t nread;         #已读
  size_t mark;          
  size_t field_start;   http字段开始
  size_t field_len;     http字段长度
  size_t query_start;   query开始

  VALUE request;        ruby对象requestc表示
  VALUE body;           ruby对象bodyc表示

  field_cb http_field;          #解析时碰到httpfield时的回调
  element_cb request_method;    #解析时碰到request_method时的回调
  element_cb request_uri;
  element_cb fragment;
  element_cb request_path;
  element_cb query_string;
  element_cb http_version;
  element_cb header_done;

  char buf[BUFFER_LEN];

} puma_parser;

typedef void (*element_cb)(struct puma_parser* hp,
                           const char *at, size_t length);

typedef void (*field_cb)(struct puma_parser* hp,
                         const char *field, size_t flen,
                         const char *value, size_t vlen);

该结构首先被 ragel 使用,其中的 cs 是 ragel 预定义的一个变量,代表当前状态。当 ragel 分析输入时,碰到符合的规则会执行对应的动作(action)。动作中会给 field_start/field_len 之类的变量赋值,也会调用 http_field 等回调。而这些回调则是 ruby 的 c 扩展在其 init 方法中注入的。

http 协议的 ragel 描述

先看看 ragel 描述的 http 协议的部分代码:

%%{

  machine puma_parser_common;

  CRLF = "\r\n";

# character types
  CTL = (cntrl | 127);
  safe = ("$" | "-" | "_" | ".");
  extra = ("!" | "*" | "'" | "(" | ")" | ",");
  reserved = (";" | "/" | "?" | ":" | "@" | "&" | "=" | "+");
  unsafe = (CTL | " " | "\"" | "#" | "%" | "<" | ">");
  national = any -- (alpha | digit | reserved | extra | safe | unsafe);
  unreserved = (alpha | digit | safe | extra | national);
  escape = ("%" xdigit xdigit);
  uchar = (unreserved | escape);
  pchar = (uchar | ":" | "@" | "&" | "=" | "+");
  tspecials = ("(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\\" | "\"" | "/" | "[" | "]" | "?" | "=" | "{" | "}" | " " | "\t");

# elements
  token = (ascii -- (CTL | tspecials));

# URI schemes and absolute paths
  scheme = ( alpha | digit | "+" | "-" | "." )* ;
  absolute_uri = (scheme ":" (uchar | reserved )*);

  path = ( pchar+ ( "/" pchar* )* ) ;
  query = ( uchar | reserved )* %query_string ;
  param = ( pchar | "/" )* ;
  params = ( param ( ";" param )* ) ;
  rel_path = ( path? %request_path (";" params)? ) ("?" %start_query query)?;
  absolute_path = ( "/"+ rel_path );

  Request_URI = ( "*" | absolute_uri | absolute_path ) >mark %request_uri;
  Fragment = ( uchar | reserved )* >mark %fragment;
  Method = ( upper | digit | safe ){1,20} >mark %request_method;

  http_number = ( digit+ "." digit+ ) ;
  HTTP_Version = ( "HTTP/" http_number ) >mark %http_version ;
  Request_Line = ( Method " " Request_URI ("#" Fragment){0,1} " " HTTP_Version CRLF ) ;

  field_name = ( token -- ":" )+ >start_field $snake_upcase_field %write_field;

  field_value = any* >start_value %write_value;

  message_header = field_name ":" " "* field_value :> CRLF;

  Request = Request_Line ( message_header )* ( CRLF @done );

main := Request;

}%%

其中的 machine 代表一个有限状态机器,它使用 ragel 定制化的正则表达式来表述,然后包含一些 action 的描述。以field_name = ( token -- ":" )+ >start_field $snake_upcase_field %write_field;为例,这一行的意思是 field_name 是任意的 token 但是不包含":",这里的--就是 ragel 自定义的操作符,表示“Strong Difference”。这一行后面的三个字段则是 ragel 的用户动作(User Action)。其中>是进入动作(Enter Action), $是全部转换动作 (All Transition Action), %是离开动作(Leave Action)。所以这个规则匹配的开始会执行 start_field,中间每个字符都会执行 snake_upcase_field,匹配完成执行 write_field。

  machine puma_parser;
  action start_field { MARK(field_start, fpc); }
  action snake_upcase_field { snake_upcase_char((char *)fpc); }
  action write_field { 
    parser->field_len = LEN(field_start, fpc);
  }

  action start_value { MARK(mark, fpc); }
  action write_value {
    parser->http_field(parser, PTR_TO(field_start), parser->field_len, PTR_TO(mark), LEN(mark, fpc));
  }
  action request_method { 
    parser->request_method(parser, PTR_TO(mark), LEN(mark, fpc));
  }

  include puma_parser_common "http11_parser_common.rl";

#define LEN(AT, FPC) (FPC - buffer - parser->AT)
#define MARK(M,FPC) (parser->M = (FPC) - buffer)  

static void snake_upcase_char(char *c)
{
    if (*c >= 'a' && *c <= 'z')
      *c &= ~0x20;
    else if (*c == '-')
      *c = '_';
}

在 ragel 的 action 里执行的是一些 c 函数。可以看到前面提到的 start_field/write_field 动作其实都是设置了 puma_parser 这个结构里一些字段,而 request_method 动作则会执行 puma_parser 里设置好的回调,这里的代码并不关心回调函数是什么。代码中的 fpc 是 ragel 预定义的变量,是指向当前字符的指针。

http 协议的解析

Http 协议的解析需要实现的接口函数如下:

int puma_parser_init(puma_parser *parser);
int puma_parser_finish(puma_parser *parser);
size_t puma_parser_execute(puma_parser *parser, const char *data,
                           size_t len, size_t off);
int puma_parser_has_error(puma_parser *parser);
int puma_parser_is_finished(puma_parser *parser);
define puma_parser_nread(parser) (parser)->nread

首先是初始化函数,这个函数很简单,主要就是初始化 puma_parser 结构体。其中的%% write init;是 ragel 提供的初始化语句,在生成的 c 代码中会被替换为 ragel 的初始化代码。

int puma_parser_init(puma_parser *parser)  {
  int cs = 0;
  %% write init;
  parser->cs = cs;
  parser->body_start = 0;
  parser->content_len = 0;
  parser->mark = 0;
  parser->nread = 0;
  parser->field_len = 0;
  parser->field_start = 0;
  parser->request = Qnil;
  parser->body = Qnil;

  return 1;
}

实现 http 协议解析的代码也很简单,真正的工作都是由 ragel 包办了,也就是%% write exec这一行。其中的 p/pe/cs 则都是 ragel 要求的预定义变量,分别代表"data pointer", "data end pointer", "current state"。

size_t puma_parser_execute(puma_parser *parser, const char *buffer, size_t len, size_t off)  {
  const char *p, *pe;
  int cs = parser->cs;
  p = buffer+off;
  pe = buffer+len;

  %% write exec;

  if (!puma_parser_has_error(parser))
    parser->cs = cs;
  parser->nread += p - (buffer + off);
  return(parser->nread);
}

然后是判断解析是否完成,是否有错误等的实现代码,也都比较简单。

int puma_parser_finish(puma_parser *parser)
{
  if (puma_parser_has_error(parser) ) {
    return -1;
  } else if (puma_parser_is_finished(parser) ) {
    return 1;
  } else {
    return 0;
  }
}

int puma_parser_has_error(puma_parser *parser) {
  return parser->cs == puma_parser_error;
}

int puma_parser_is_finished(puma_parser *parser) {
  return parser->cs >= puma_parser_first_final;
}

ruby 对象的 c 接口

最后看看如何在 ruby 中使用上面提供的 c 语言的 http 解析代码。关于如何编写 ruby 的 c 语言扩展,可以看看这个系列的文章

首先来看看入口函数,它首先定义了模块 Puma 和类 HttpParser,然后定义了一些 ruby 中的全局的变量如 request_method 等,然后给 HttpParser 对象定义了一些方法如 new/execute/finish 等。

void Init_puma_http11()
{

  VALUE mPuma = rb_define_module("Puma");
  VALUE cHttpParser = rb_define_class_under(mPuma, "HttpParser", rb_cObject);

  DEF_GLOBAL(request_method, "REQUEST_METHOD");
  DEF_GLOBAL(request_uri, "REQUEST_URI");
  DEF_GLOBAL(fragment, "FRAGMENT");
  DEF_GLOBAL(query_string, "QUERY_STRING");
  DEF_GLOBAL(http_version, "HTTP_VERSION");
  DEF_GLOBAL(request_path, "REQUEST_PATH");

  eHttpParserError = rb_define_class_under(mPuma, "HttpParserError", rb_eIOError);
  rb_global_variable(&eHttpParserError);

  rb_define_alloc_func(cHttpParser, HttpParser_alloc);
  rb_define_method(cHttpParser, "initialize", HttpParser_init, 0);
  rb_define_method(cHttpParser, "reset", HttpParser_reset, 0);
  rb_define_method(cHttpParser, "finish", HttpParser_finish, 0);
  rb_define_method(cHttpParser, "execute", HttpParser_execute, 3);
  rb_define_method(cHttpParser, "error?", HttpParser_has_error, 0);
  rb_define_method(cHttpParser, "finished?", HttpParser_is_finished, 0);
  rb_define_method(cHttpParser, "nread", HttpParser_nread, 0);
  rb_define_method(cHttpParser, "body", HttpParser_body, 0);
  init_common_fields();

  Init_io_buffer(mPuma);
  Init_mini_ssl(mPuma);
}

define DEF_GLOBAL(N, val)   global_##N = rb_str_new2(val); rb_global_variable(&global_##N)

然后我们看看 HttpParser 的初始化实现,也就是在执行 ruby 代码HttpParser.new时,底层到底发生了什么。可以看到,当执行 new 的时候,先给 puma_parser 结构体分配了内存,然后设置了一些回调函数。前面提高的 ragel 的动作执行的时候,会执行这里设置的回调。ALLOC_N 是 ruby 提供的内存分配函数,相比于 malloc 直接分配,ALLOC_N 的功能更强,比如在内存不够的时候它会尝试先执行一次垃圾收集然后再分配内存。ALLOC_N 分配出去的内存必须使用 xfree 回收。

VALUE HttpParser_alloc(VALUE klass)
{
  puma_parser *hp = ALLOC_N(puma_parser, 1);
  TRACE();
  hp->http_field = http_field;
  hp->request_method = request_method;
  hp->request_uri = request_uri;
  hp->fragment = fragment;
  hp->request_path = request_path;
  hp->query_string = query_string;
  hp->http_version = http_version;
  hp->header_done = header_done;
  hp->request = Qnil;

  puma_parser_init(hp);

  return Data_Wrap_Struct(klass, HttpParser_mark, HttpParser_free, hp);
}

然后看看实际执行 http 协议解析的函数,这里的关键部分还是调用前面提到的 puma_parser_execute 函数,其它代码都是为了提供给 ruby 层使用而增加的封装。

VALUE HttpParser_execute(VALUE self, VALUE req_hash, VALUE data, VALUE start)
{
  puma_parser *http = NULL;
  int from = 0;
  char *dptr = NULL;
  long dlen = 0;

  DATA_GET(self, puma_parser, http);

  from = FIX2INT(start);
  dptr = rb_extract_chars(data, &dlen);

  if(from >= dlen) {
    rb_free_chars(dptr);
    rb_raise(eHttpParserError, "%s", "Requested start is after data buffer end.");
  } else {
    http->request = req_hash;
    puma_parser_execute(http, dptr, dlen, from);

    rb_free_chars(dptr);
    VALIDATE_MAX_LENGTH(puma_parser_nread(http), HEADER);

    if(puma_parser_has_error(http)) {
      rb_raise(eHttpParserError, "%s", "Invalid HTTP format, parsing fails.");
    } else {
      return INT2FIX(puma_parser_nread(http));
    }
  }
}

VALUE HttpParser_is_finished(VALUE self)
{
  puma_parser *http = NULL;
  DATA_GET(self, puma_parser, http);

  return puma_parser_is_finished(http) ? Qtrue : Qfalse;
}

最后看看一些 ragel 回调的实现。函数 http_field 就是把解析出来的 field 和 value 对设置到 ruby 的 hash 中。前面看到的 ragel 代码action write_value {parser->http_field(......)}会调用这里的 c 代码。


void http_field(puma_parser* hp, const char *field, size_t flen,
                                 const char *value, size_t vlen)
{
  VALUE v = Qnil;
  VALUE f = Qnil;

  VALIDATE_MAX_LENGTH(flen, FIELD_NAME);
  VALIDATE_MAX_LENGTH(vlen, FIELD_VALUE);

  v = rb_str_new(value, vlen);

  f = find_common_field_value(field, flen);

  if (f == Qnil) {
    /*
     * We got a strange header that we don't have a memoized value for.
     * Fallback to creating a new string to use as a hash key.
     */

    size_t new_size = HTTP_PREFIX_LEN + flen;
    assert(new_size < BUFFER_LEN);

    memcpy(hp->buf, HTTP_PREFIX, HTTP_PREFIX_LEN);
    memcpy(hp->buf + HTTP_PREFIX_LEN, field, flen);

    f = rb_str_new(hp->buf, new_size);
  }

  rb_hash_aset(hp->request, f, v);
}

其它一些 ragel 回调则都很简单,比如函数 request_method 则设置了 ruby 中 global_request_method 的值。

void request_method(puma_parser* hp, const char *at, size_t length)
{
  VALUE val = Qnil;

  val = rb_str_new(at, length);
  rb_hash_aset(hp->request, global_request_method, val);
}

void request_path(puma_parser* hp, const char *at, size_t length)
{
  VALUE val = Qnil;

  VALIDATE_MAX_LENGTH(length, REQUEST_PATH);

  val = rb_str_new(at, length);
  rb_hash_aset(hp->request, global_request_path, val);
}

void query_string(puma_parser* hp, const char *at, size_t length)
{
  VALUE val = Qnil;

  VALIDATE_MAX_LENGTH(length, QUERY_STRING);

  val = rb_str_new(at, length);
  rb_hash_aset(hp->request, global_query_string, val);
}

在 ruby 中通过调用 HttpParser.execute 解析输入的字符流,通过 parser.finished?判断是否解析完成,如果 ragel 的状态是 puma_parser_first_final,那么代表解析完成。

你好,能介绍一篇如何阅读 ruby gem 源代码的文章吗?

比如说:
  • 按什么顺序来读
  • 怎样调试
  • ruby 代码通常不超过 10 行,尤其是 famous gem,里面的方法通常互相调用,模块混入,类扩展混入等等。那么看到一个方法后如何跳过,如何查找源代码。

期待 ing.

#1 楼 @flowerwrong 看一些 gem 的源代码,要有目的性。我一般不会为了读源代码而读源代码。比如在使用 gem 的过程中碰到了 bug,那么会追踪到 gem 的源码中去。这个时候因为带着目的,就会有主线,不会因为很多方法跳来跳去而迷茫了。至于调试,和普通的代码调试一样啊。至于查找源代码的关键点,有一些小方法,一个是用 grep/ack 等搜索关键字,还有就是 ruby 提供的 source_location 方法,在查找第三方 gem 的某个方法的实现时很有用。比如想要知道 rails 里面 1.days.ago 是怎么实现的,可以执行 1.method(:days).source_location,就可以知道实现的代码在哪里了。 ["/mnt/.rvm/gems/ruby-2.1.2/gems/activesupport-4.0.9/lib/active_support/core_ext/numeric/time.rb", 49]

def days ActiveSupport::Duration.new(self * 24.hours, [[:days, self]]) end

👍 楼主写的很不错 看来 Puma 之所以号称性能好关键还是 C 代码承包了服务器中最需要的性能的一部分 期待下一篇

感谢。前段时间我也看到这部分 http parser,可是没找到这个 .rl 是什么,这下好了 😄

ylt Puma 源代码分析 - 概述 提及了此话题。 07月03日 22:43
需要 登录 后方可回复, 如果你还没有账号请 注册新账号