nginx中request请求的解析 博客分类: 服务器设计 nginxCC++C#UI
程序员文章站
2024-03-22 14:00:16
...
ngx_http_init_request 中初始化event 的handler 为ngx_http_process_request_line,然后首先调用ngx_http_read_request_header来读取头部,然后就是开始调用函数ngx_http_parse_request_line对request line进行解析。随后如果解析的url是complex的话,就进入complex的解析,最后进入headers的解析。
static void ngx_http_process_request_line(ngx_event_t *rev) { ................................................................ for ( ;; ) { if (rc == NGX_AGAIN) { //读取request头部 n = ngx_http_read_request_header(r); if (n == NGX_AGAIN || n == NGX_ERROR) { return; } } //开始解析request请求头部。 rc = ngx_http_parse_request_line(r, r->header_in); ............................................................................................................. if (r->complex_uri || r->quoted_uri) { ................................ //解析complex uri。 rc = ngx_http_parse_complex_uri(r, cscf->merge_slashes); ............................................ } ...................................................... //执行request header并且解析headers。 ngx_http_process_request_headers(rev); }
这里nginx将request的解析分为三个部分,第一个是request-line部分,第二个是complex ui部分,第三个是request header部分。
在看nginx的处理之前,我们先来熟悉一下http的request-line的格式。
首先来看request 的格式:
引用
<method> <request-URL> <version>
<headers>
<entity-body>
其中第一行就是request-line,我们先来看
然后我们一个个来看,首先是method:
引用
Method = "OPTIONS"
| "GET"
| "HEAD"
| "POST"
| "PUT"
| "DELETE"
| "TRACE"
| "CONNECT"
| extension-method
extension-method = token
| "GET"
| "HEAD"
| "POST"
| "PUT"
| "DELETE"
| "TRACE"
| "CONNECT"
| extension-method
extension-method = token
然后是URL,这里要注意这个是URL的完整格式,而我们如果和server直接通信的话,一般只需要path就够了。
引用
<scheme>://<user>:<password>@<host>:<port>/<path>;<params>?<query>#<frag>
然后是version的格式:
引用
HTTP/<major>.<minor>
整个request-line解析的一个状态,在ngx_http_parse.c中定义,这个状态保存在ngx_http_request_t 中的state中,它表示当前的request line解析到那一步了,其实也就是个状态机。
enum { sw_start = 0, sw_method, sw_spaces_before_uri, sw_schema, sw_schema_slash, sw_schema_slash_slash, sw_host, sw_port, sw_after_slash_in_uri, sw_check_uri, sw_uri, sw_http_09, sw_http_H, sw_http_HT, sw_http_HTT, sw_http_HTTP, sw_first_major_digit, sw_major_digit, sw_first_minor_digit, sw_minor_digit, sw_spaces_after_digit, sw_almost_done } state;
而这里nginx这些状态就是为了解析这些东西。
接下来来通过代码片断看这些状态的含义。不过在看之前一定要
首先是start状态,也就是初始状态,我们刚开始解析Request的时候,就是这个状态。
case sw_start: r->request_start = p; //如果是回车换行则跳出switch,然后继续解析 if (ch == CR || ch == LF) { break; } //不是A到Z的字母(大小写敏感的),并且不是_则返回错误 if ((ch < 'A' || ch > 'Z') && ch != '_') { return NGX_HTTP_PARSE_INVALID_METHOD; } //到达这里说明下一步改解析方法了。因此下一个状态就是method state = sw_method; break;
然后是method状态,这个状态表示我们正在解析请求的method。
下面就是http的请求方法:
引用
Method = "OPTIONS"
| "GET"
| "HEAD"
| "POST"
| "PUT"
| "DELETE"
| "TRACE"
| "CONNECT"
| extension-method
extension-method = token
| "GET"
| "HEAD"
| "POST"
| "PUT"
| "DELETE"
| "TRACE"
| "CONNECT"
| extension-method
extension-method = token
由于METHOD比较多,而且代码都比较重复,因此这里就看看几个代码片断.
由于
case sw_method: //如果再次读到空格则说明我们已经准备解析request-URL,此时我们就能得到请求方法了。 if (ch == ' ') { //先得到method的结束位置 r->method_end = p - 1; //开始位置前面已经保存。 m = r->request_start; //得到方法的长度,通过长度来得到具体不同的方法,然后给request的method赋值。 switch (p - m) { case 3: if (ngx_str3_cmp(m, 'G', 'E', 'T', ' ')) { r->method = NGX_HTTP_GET; break; } if (ngx_str3_cmp(m, 'P', 'U', 'T', ' ')) { r->method = NGX_HTTP_PUT; break; } break; ............................................... } //下一个状态准备开始解析URI state = sw_spaces_before_uri; break; .......................................................................
然后是sw_spaces_before_uri状态,这里由于uri会有两种情况,一种是带schema的,一种是直接相对路径的(可以看前面的uri格式).
case sw_spaces_before_uri: //如果是以/开始,则进入sw_after_slash_in_uri if (ch == '/' ){ r->uri_start = p; state = sw_after_slash_in_uri; break; } c = (u_char) (ch | 0x20); //如果是字母,则进入sw_schema处理 if (c >= 'a' && c <= 'z') { r->schema_start = p; state = sw_schema; break; } switch (ch) { //空格的话继续这个状态。 case ' ': break; default: return NGX_HTTP_PARSE_INVALID_REQUEST; } break;
sw_schema状态主要是用来解析协议类型。等到协议类型解析完毕则进入sw_schema_slash状态.
case sw_schema: c = (u_char) (ch | 0x20); //如果是字母则break,然后继续这个状态的处理。 if (c >= 'a' && c <= 'z') { break; } //到这里说明schema已经结束。 switch (ch) { //这里必须是:,如果不是冒号则直接返回错误。 case ':': //设置schema_end,而start我们在上面已经设置过了 r->schema_end = p; //设置下一个状态。 state = sw_schema_slash; break; default: return NGX_HTTP_PARSE_INVALID_REQUEST; } break;
sw_schema_slash和sw_schema_slash_slash是两个很简单的状态,第一个是得到schema的第一个/,然后进入sw_schema_slash_slash,而sw_schema_slash_slash则是得到了第二个/.然后进入sw_host。
case sw_schema_slash: switch (ch) { case '/': //进入slash_slash state = sw_schema_slash_slash; break; default: return NGX_HTTP_PARSE_INVALID_REQUEST; } break; case sw_schema_slash_slash: switch (ch) { case '/': //设置host的开始指针 r->host_start = p + 1; //设置下一个状态为sw_host. state = sw_host; break; default: return NGX_HTTP_PARSE_INVALID_REQUEST; } break;
然后是sw_host状态,这个状态用来解析host。
case sw_host: c = (u_char) (ch | 0x20); //这里很奇怪,不知道为什么不把判断写在一起。 if (c >= 'a' && c <= 'z') { break; } if ((ch >= '0' && ch <= '9') || ch == '.' || ch == '-') { break; } //到达这里说明host已经得到,因此设置end指针。 r->host_end = p; switch (ch) { //冒号说明host有跟端口的,因此进入port状态。 case ':': state = sw_port; break; //这个说明要开始解析path了。因此设置uri的start,然后进入slash_in_uri case '/': r->uri_start = p; state = sw_after_slash_in_uri; break; //如果是空格,则设置uri的start和end然后进入http_09 case ' ': r->uri_start = r->schema_end + 1; r->uri_end = r->schema_end + 2; state = sw_http_09; break; default: return NGX_HTTP_PARSE_INVALID_REQUEST; } break;
接下来是sw_port,这个状态用来解析协议端口。
case sw_port: if (ch >= '0' && ch <= '9') { break; } //如果到达这里说明端口解析完毕, 然后就来判断下一步需要的状态。 switch (ch) { //如果紧跟着/,则说明后面是uri,因此进入uri解析,并设置port_end case '/': r->port_end = p; r->uri_start = p; state = sw_after_slash_in_uri; break; //如果是空格则设置port end,并进入http_09状态。 case ' ': r->port_end = p; r->uri_start = r->schema_end + 1; r->uri_end = r->schema_end + 2; state = sw_http_09; break; default: return NGX_HTTP_PARSE_INVALID_REQUEST; } break;
接下来是sw_after_slash_in_uri,sw_check_uri 这两个状态都是解析uri之前的状态,主要用于检测uri,比如complex uri等。
这里要对uri的格式比较熟悉,这里可以去看rfc3986,里面对uri的格式有比较清楚的描述。
因此我们主要来看sw_uri状态,这个状态就是开始解析uri。这里可以看到对http 0.9是特殊处理的,如果直接是回车或者换行的话,就进入http 0.9的处理。
case sw_uri: if (usual[ch >> 5] & (1 << (ch & 0x1f))) { break; } switch (ch) { //下面三种情况都说明是http 0.9 case ' ': r->uri_end = p; state = sw_http_09; break; case CR: r->uri_end = p; r->http_minor = 9; state = sw_almost_done; break; case LF: r->uri_end = p; r->http_minor = 9; goto done; //要对段进行解析。因此设置complex uri case '#': r->complex_uri = 1; break; case '\0': r->zero_in_uri = 1; break; } break;
接下来的sw_http_09,sw_http_H,sw_http_HT,sw_http_HTT,sw_http_HTTP, sw_first_major_digit,sw_major_digit,sw_first_minor_digit,sw_minor_digit,这几个状态主要是用来解析http的版本号的,都比较简单,这里就不仔细分析了。
然后来看最后两个状态sw_spaces_after_digit和sw_almost_done。
第一个状态表示已经解析完http状态了,然后发现有空格。
case sw_spaces_after_digit: switch (ch) { case ' ': break; //如果是回车,则进入almost_done,然后等待最后一个换行。 case CR: state = sw_almost_done; break; //如果是换行则说明request-line解析完毕 case LF: goto done; default: return NGX_HTTP_PARSE_INVALID_REQUEST; } break;
最后是almost_done状态,也就是等待最后的换行。
case sw_almost_done: r->request_end = p - 1; switch (ch) { case LF: goto done; default: return NGX_HTTP_PARSE_INVALID_REQUEST; } }
接下来是complex_uri的解析,这里比如段(#),比如win下的\\,等等,这里这个函数就不分析了,方法和request-line的差不多。
这里主要来看一下header的实现。
headers的格式我们知道一个name紧跟着一个冒号:,然后紧跟着一个可选的空格,然后是一个value,最后以一个CRLF结束,而headers的结束是一个CRLF。
下面是解析header时的状态列表:
enum { sw_start = 0, sw_name, sw_space_before_value, sw_value, sw_space_after_value, sw_ignore_line, sw_almost_done, sw_header_almost_done } state;
解析是ngx_http_parse_header_line来做的,这个函数一次只能解析一个header,如果传递进来的buf会有多个header,则是它只处理一个header,然后设置好对应的buf的域,等待下次再进行解析。
而这个函数能够返回三个值,第一个是NGX_OK,这个表示一个header解析完毕,第二个是NGX_AGAIN,表示header没有解析完毕,也就是说buf只有一部分的数据。这个时候,下次进来的数据会继续没有完成的解析。第三个是NGX_HTTP_PARSE_HEADER_DONE;,表示整个header已经解析完毕。
而对应的goto DONE表示NGX_OK,goto HEADER_DONE表示NGX_HTTP_PARSE_HEADER_DONE,而默认则是NGX_AFAIN.
这里有几个request的值需要先说明一下。
先来看request的域:
header_name_start 这个表示header_name的起始位置。
header_name_end 这个表示当前header_name的结束位置
header_start 这个是value的起始位置
header_end 这个是value的结束位置
header_hash 这个是header name的hash值。这个主要用来保存name和value到hash中。
lowcase_index 这个是索引值。
和上面一样,我们跟着代码来看这些状态的意义。
首先来看sw_start状态,这个是起始状态:
case sw_start: // 设置header开始指针。 r->header_name_start = p; r->invalid_header = 0; //通过第一个字符的值来判断下一个状态。 switch (ch) { //回车的话,说明没有header,因此设置状态为almost_done,然后期待最后的换行 case CR: r->header_end = p; state = sw_header_almost_done; break; case LF: //如果换行则直接进入header_done,也就是整个header解析完毕 r->header_end = p; goto header_done; default: //默认进入sw_name状态,进行name解析 state = sw_name; //这里做了一个表,来进行大小写转换 c = lowcase[ch]; if (c) { //得到hash值,然后设置lowcase_header,后面我会解释这两个操作的原因。 hash = ngx_hash(0, c); r->lowcase_header[0] = c; i = 1; break; } r->invalid_header = 1; break; } break;
然后是sw_name状态,这个状态进行解析name。
case sw_name: //小写。 c = lowcase[ch]; //开始计算hash,然后保存header name if (c) { hash = ngx_hash(hash, c); r->lowcase_header[i++] = c; i &= (NGX_HTTP_LC_HEADER_LEN - 1); break; } //如果存在下划线,则通过传递进来的参数来判断是否允许下划线,比如fastcgi就允许。 if (ch == '_') { if (allow_underscores) { hash = ngx_hash(hash, ch); r->lowcase_header[i++] = ch; i &= (NGX_HTTP_LC_HEADER_LEN - 1); } else { r->invalid_header = 1; } break; } //如果是冒号,则进入value的处理,由于value有可能前面有空格,因此先处理这个。 if (ch == ':') { //设置header name的end。 r->header_name_end = p; state = sw_space_before_value; break; } //如果是回车换行则说明当前header解析已经结束,因此进入最终结束处理。 if (ch == CR) { //设置对应的值。 r->header_name_end = p; r->header_start = p; r->header_end = p; state = sw_almost_done; break; } if (ch == LF) { //设置对应的值,然后进入done r->header_name_end = p; r->header_start = p; r->header_end = p; goto done; } ....................................................... r->invalid_header = 1; break;
sw_space_before_value状态就不分析了,这里它主要是解析value有空格的情况,并且保存value的指针。
case sw_space_before_value: switch (ch) { //跳过空格 case ' ': break; case CR: r->header_start = p; r->header_end = p; state = sw_almost_done; break; case LF: r->header_start = p; r->header_end = p; goto done; default: //设置header_start也就是value的开始指针。 r->header_start = p; state = sw_value; break; } break;
我们主要来看sw_value状态,也就是解析value的状态。
case sw_value: switch (ch) { //如果是空格则进入sw_space_after_value处理 case ' ': r->header_end = p; state = sw_space_after_value; break; //会车换行的话,说明header解析完毕进入done或者almost_done.也就是最终会返回NGX_OK case CR: r->header_end = p; state = sw_almost_done; break; case LF: r->header_end = p; goto done; } break;
最后来看两个结束状态
//当前的header解析完毕 case sw_almost_done: switch (ch) { case LF: goto done; case CR: break; default: return NGX_HTTP_PARSE_INVALID_HEADER; } break; //整个header解析完毕 case sw_header_almost_done: switch (ch) { case LF: goto header_done; default: return NGX_HTTP_PARSE_INVALID_HEADER; } }
最后来看这几个标记:
首先是默认,也就是当遍历完buf后,header仍然没有结束的情况:
此时设置对应的hash值,以及保存当前状态,以及buf的位置
b->pos = p; r->state = state; r->header_hash = hash; r->lowcase_index = i; return NGX_AGAIN;
然后是done,也就是当前的header已经解析完毕,此时设置状态为start,以及buf位置为p+1.
done: b->pos = p + 1; r->state = sw_start; r->header_hash = hash; r->lowcase_index = i; return NGX_OK;
最后是header全部解析完毕,此时是得到了最后的回车换行,因此不需要hash值。
header_done: b->pos = p + 1; r->state = sw_start; return NGX_HTTP_PARSE_HEADER_DONE;