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

ylt · 2015年03月15日 · 最后由 ylt 回复于 2016年07月03日 · 6045 次阅读
本帖已被设为精华帖!

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,那么代表解析完成。

共收到 6 条回复

你好,能介绍一篇如何阅读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
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册