3.6 处理用户请求
本节介绍如何处理一个实际的HTTP请求。回顾一下上文,在出现mytest配置项时,ngx_http_mytest方法会被调用,这时将ngx_http_core_loc_conf_t结构的handler成员指定为ngx_http_mytest_handler, 另外,HTTP框架在接收完HTTP请求的头部后,会调用handler指向的方法。下面看一下handler成员的原型ngx_http_handler_pt:
typedef ngx_int_t (ngx_http_handler_pt)(ngx_http_request_t r);
从上面这段代码可以看出,实际处理请求的方法ngx_http_mytest_handler将接收一个ngx_http_request_t类型的参数r,返回一个ngx_int_t(参见3.2.1节)类型的结果。下面先探讨一下ngx_http_mytest_handler方法可以返回什么,再看一下参数r包含了哪些Nginx已经解析完的用户请求信息。
3.6.1 处理方法的返回值
这个返回值可以是HTTP中响应包的返回码,其中包括了HTTP框架已经在/src/http/ngx_http_request.h文件中定义好的宏,如下所示。
#define NGX_HTTP_OK 200
#define NGX_HTTP_CREATED 201
#define NGX_HTTP_ACCEPTED 202
#define NGX_HTTP_NO_CONTENT 204
#define NGX_HTTP_PARTIAL_CONTENT 206
#define NGX_HTTP_SPECIAL_RESPONSE 300
#define NGX_HTTP_MOVED_PERMANENTLY 301
#define NGX_HTTP_MOVED_TEMPORARILY 302
#define NGX_HTTP_SEE_OTHER 303
#define NGX_HTTP_NOT_MODIFIED 304
#define NGX_HTTP_TEMPORARY_REDIRECT 307
#define NGX_HTTP_BAD_REQUEST 400
#define NGX_HTTP_UNAUTHORIZED 401
#define NGX_HTTP_FORBIDDEN 403
#define NGX_HTTP_NOT_FOUND 404
#define NGX_HTTP_NOT_ALLOWED 405
#define NGX_HTTP_REQUEST_TIME_OUT 408
#define NGX_HTTP_CONFLICT 409
#define NGX_HTTP_LENGTH_REQUIRED 411
#define NGX_HTTP_PRECONDITION_FAILED 412
#define NGX_HTTP_REQUEST_ENTITY_TOO_LARGE 413
#define NGX_HTTP_REQUEST_URI_TOO_LARGE 414
#define NGX_HTTP_UNSUPPORTED_MEDIA_TYPE 415
#define NGX_HTTP_RANGE_NOT_SATISFIABLE 416
/* The special code to close connection without any response */
#define NGX_HTTP_CLOSE 444
#define NGX_HTTP_NGINX_CODES 494
#define NGX_HTTP_REQUEST_HEADER_TOO_LARGE 494
#define NGX_HTTPS_CERT_ERROR 495
#define NGX_HTTPS_NO_CERT 496
#define NGX_HTTP_TO_HTTPS 497
#define NGX_HTTP_CLIENT_CLOSED_REQUEST 499
#define NGX_HTTP_INTERNAL_SERVER_ERROR 500
#define NGX_HTTP_NOT_IMPLEMENTED 501
#define NGX_HTTP_BAD_GATEWAY 502
#define NGX_HTTP_SERVICE_UNAVAILABLE 503
#define NGX_HTTP_GATEWAY_TIME_OUT 504
#define NGX_HTTP_INSUFFICIENT_STORAGE 507
注意 以上返回值除了RFC2616规范中定义的返回码外,还有Nginx自身定义的HTTP返回码。例如,NGX_HTTP_CLOSE就是用于要求HTTP框架直接关闭用户连接的。
在ngx_http_mytest_handler的返回值中,如果是正常的HTTP返回码,Nginx就会按照规范构造合法的响应包发送给用户。例如,假设对于PUT方法暂不支持,那么,在处理方法中发现方法名是PUT时,返回NGX_HTTP_NOT_ALLOWED,这样Nginx也就会构造类似下面的响应包给用户。
http/1.1 405 Not Allowed
Server: nginx/1.0.14
Date: Sat, 28 Apr 2012 06:07:17 GMT
Content-Type: text/html
Content-Length: 173
Connection: keep-alive
<html>
<head><title>405 Not Allowed</title></head>
<body bgcolor="white">
<center><h1>405 Not Allowed</h1></center>
<hr><center>nginx/1.0.14</center>
</body>
</html>
在处理方法中除了返回HTTP响应码外,还可以返回Nginx全局定义的几个错误码,包括:
#define NGX_OK 0
#define NGX_ERROR -1
#define NGX_AGAIN -2
#define NGX_BUSY -3
#define NGX_DONE -4
#define NGX_DECLINED -5
#define NGX_ABORT -6
这些错误码对于Nginx自身提供的大部分方法来说都是通用的。所以,当我们最后调用ngx_http_output_filter(参见3.7节)向用户发送响应包时,可以将ngx_http_output_filter的返回值作为ngx_http_mytest_handler方法的返回值使用。例如:
static ngx_int_t ngx_http_mytest_handler(ngx_http_request_t *r)
{
...
ngx_int_t rc = ngx_http_send_header(r);
if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) {
return rc;
}
return ngx_http_output_filter(r, &out);
}
当然,直接返回以上7个通用值也是可以的。在不同的场景下,这7个通用返回值代表的含义不尽相同。在mytest的例子中,HTTP框架在NGX_HTTP_CONTENT_PHASE阶段调用ngx_http_mytest_handler后,会将ngx_http_mytest_handler的返回值作为参数传给ngx_http_finalize_request方法,如下所示。
if (r->content_handler) {
r->write_event_handler = ngx_http_request_empty_handler;
ngx_http_finalize_request(r, r->content_handler(r));
return NGX_OK;
}
上面的r->content_handler会指向ngx_http_mytest_handler处理方法。也就是说,事实上ngx_http_finalize_request决定了ngx_http_mytest_handler如何起作用。本章不探讨ngx_http_finalize_request的实现(详见11.10节),只简单地说明一下4个通用返回码,另外,在11.10节中介绍这4个返回码引发的Nginx一系列动作。
NGX_OK:表示成功。Nginx将会继续执行该请求的后续动作(如执行subrequest或撤销这个请求)。
NGX_DECLINED:继续在NGX_HTTP_CONTENT_PHASE阶段寻找下一个对于该请求感兴趣的HTTP模块来再次处理这个请求。
NGX_DONE:表示到此为止,同时HTTP框架将暂时不再继续执行这个请求的后续部分。事实上,这时会检查连接的类型,如果是keepalive类型的用户请求,就会保持住HTTP连接,然后把控制权交给Nginx。这个返回码很有用,考虑以下场景:在一个请求中我们必须访问一个耗时极长的操作(比如某个网络调用),这样会阻塞住Nginx,又因为我们没有把控制权交还给Nginx,而是在ngx_http_mytest_handler中让Nginx worker进程休眠了(如等待网络的回包),所以,这就会导致Nginx出现性能问题,该进程上的其他用户请求也得不到响应。可如果我们把这个耗时极长的操作分为上下两个部分(就像Linux内核中对中断处理的划分),上半部分和下半部分都是无阻塞的(耗时很少的操作),这样,在ngx_http_mytest_handler进入时调用上半部分,然后返回NGX_DONE,把控制交还给Nginx,从而让Nginx继续处理其他请求。在下半部分被触发时(这里不探讨具体的实现方式,事实上使用upstream方式做反向代理时用的就是这种思想),再回调下半部分处理方法,这样就可以保证Nginx的高性能特性了。如果需要彻底了解NGX_DONE的意义,那么必须学习第11章内容,其中还涉及请求的引用计数内容。
NGX_ERROR:表示错误。这时会调用ngx_http_terminate_request终止请求。如果还有POST子请求,那么将会在执行完POST请求后再终止本次请求。
3.6.2 获取URI和参数
请求的所有信息(如方法、URI、协议版本号和头部等)都可以在传入的ngx_http_request_t类型参数r中取得。ngx_http_request_t结构体的内容很多,本节不会探讨ngx_http_request_t中所有成员的意义(ngx_http_request_t结构体中的许多成员只有HTTP框架才感兴趣,在11.3.1节会更详细的说明),只介绍一下获取URI和参数的方法,这非常简单,因为Nginx提供了多种方法得到这些信息。下面先介绍相关成员的定义。
typedef struct ngx_http_request_s ngx_http_request_t;
struct ngx_http_request_s {
…
ngx_uint_t method;
ngx_uint_t http_version;
ngx_str_t request_line;
ngx_str_t uri;
ngx_str_t args;
ngx_str_t exten;
ngx_str_t unparsed_uri;
ngx_str_t method_name;
ngx_str_t http_protocol;
u_char *uri_start;
u_char *uri_end;
u_char *uri_ext;
u_char *args_start;
u_char *request_start;
u_char *request_end;
u_char *method_end;
u_char *schema_start;
u_char *schema_end;
…
};
在对一个用户请求行进行解析时,可以得到下列4类信息。
(1)方法名
method的类型是ngx_uint_t(无符号整型),它是Nginx忽略大小写等情形时解析完用户请求后得到的方法类型,其取值范围如下所示。
#define NGX_HTTP_UNKNOWN 0x0001
#define NGX_HTTP_GET 0x0002
#define NGX_HTTP_HEAD 0x0004
#define NGX_HTTP_POST 0x0008
#define NGX_HTTP_PUT 0x0010
#define NGX_HTTP_DELETE 0x0020
#define NGX_HTTP_MKCOL 0x0040
#define NGX_HTTP_COPY 0x0080
#define NGX_HTTP_MOVE 0x0100
#define NGX_HTTP_OPTIONS 0x0200
#define NGX_HTTP_PROPFIND 0x0400
#define NGX_HTTP_PROPPATCH 0x0800
#define NGX_HTTP_LOCK 0x1000
#define NGX_HTTP_UNLOCK 0x2000
#define NGX_HTTP_TRACE 0x4000
当需要了解用户请求中的HTTP方法时,应该使用r->method这个整型成员与以上15个宏进行比较,这样速度是最快的(如果使用method_name成员与字符串做比较,那么效率会差很多),大部分情况下推荐使用这种方式。除此之外,还可以用method_name取得用户请求中的方法名字符串,或者联合request_start与method_end指针取得方法名。method_name是ngx_str_t类型,按照3.2.2节中介绍的方法使用即可。
request_start与method_end的用法也很简单,其中request_start指向用户请求的首地址,同时也是方法名的地址,method_end指向方法名的最后一个字符(注意,这点与其他xxx_end指针不同)。获取方法名时可以从request_start开始向后遍历,直到地址与method_end相同为止,这段内存存储着方法名。
注意 Nginx中对内存的控制相当严格,为了避免不必要的内存开销,许多需要用到的成员都不是重新分配内存后存储的,而是直接指向用户请求中的相应地址。例如,method_name.data、request_start这两个指针实际指向的都是同一个地址。而且,因为它们是简单的内存指针,不是指向字符串的指针,所以,在大部分情况下,都不能将这些u_char*指针当做字符串使用。
(2)URI
ngx_str_t类型的uri成员指向用户请求中的URI。同理,u_char类型的uri_start和uri_end也与request_start、method_end的用法相似,唯一不同的是,method_end指向方法名的最后一个字符,而uri_end指向URI结束后的下一个地址,也就是最后一个字符的下一个字符地址(HTTP框架的行为),这是大部分u_char类型指针对“xxx_start”和“xxx_end”变量的用法。
ngx_str_t类型的extern成员指向用户请求的文件扩展名。例如,在访问“GET /a.txt HTTP/1.1”时,extern的值是{len = 3, data = "txt"},而在访问“GET /a HTTP/1.1”时,extern的值为空,也就是{len = 0, data = 0x0}。
uri_ext指针指向的地址与extern.data相同。
unparsed_uri表示没有进行URL解码的原始请求。例如,当uri为“/a b”时,unparsed_uri是“/a%20b”(空格字符做完编码后是%20)。
(3)URL参数
arg指向用户请求中的URL参数。
args_start指向URL参数的起始地址,配合uri_end使用也可以获得URL参数。
(4)协议版本
http_protocol指向用户请求中HTTP的起始地址。
http_version是Nginx解析过的协议版本,它的取值范围如下:
#define NGX_HTTP_VERSION_9 9
#define NGX_HTTP_VERSION_10 1000
#define NGX_HTTP_VERSION_11 1001
建议使用http_version分析HTTP的协议版本。
最后,使用request_start和request_end可以获取原始的用户请求行。
3.6.3 获取HTTP头部
在ngx_http_request_t* r中就可以取到请求中的HTTP头部,比如使用下面的成员:
struct ngx_http_request_s {
…
ngx_buf_t *header_in;
ngx_http_headers_in_t headers_in;
…
};
其中,header_in指向Nginx收到的未经解析的HTTP头部,这里暂不关注它(在第11章中可以看到,header_in就是接收HTTP头部的缓冲区)。ngx_http_headers_in_t 类型的headers_in则存储已经解析过的HTTP头部。下面介绍ngx_http_headers_in_t结构体中的成员。
typedef struct {
/所有解析过的HTTP头部都在headers链表中,可以使用3.2.3节中介绍的遍历链表的方法来获取所有的HTTP头部。注意,这里headers链表的每一个元素都是3.2.4节介绍过的ngx_table_elt_t成员/
ngx_list_t headers;
/以下每个ngx_table_elt_t成员都是RFC1616规范中定义的HTTP头部, 它们实际都指向headers链表中的相应成员。注意,当它们为NULL空指针时,表示没有解析到相应的HTTP头部/
ngx_table_elt_t *host;
ngx_table_elt_t *connection;
ngx_table_elt_t *if_modified_since;
ngx_table_elt_t *if_unmodified_since;
ngx_table_elt_t *user_agent;
ngx_table_elt_t *referer;
ngx_table_elt_t *content_length;
ngx_table_elt_t *content_type;
ngx_table_elt_t *range;
ngx_table_elt_t *if_range;
ngx_table_elt_t *transfer_encoding;
ngx_table_elt_t *expect;
#if (NGX_HTTP_GZIP)
ngx_table_elt_t *accept_encoding;
ngx_table_elt_t *via;
#endif
ngx_table_elt_t *authorization;
ngx_table_elt_t *keep_alive;
#if (NGX_HTTP_PROXY || NGX_HTTP_REALIP || NGX_HTTP_GEO)
ngx_table_elt_t *x_forwarded_for;
#endif
#if (NGX_HTTP_REALIP)
ngx_table_elt_t *x_real_ip;
#endif
#if (NGX_HTTP_HEADERS)
ngx_table_elt_t *accept;
ngx_table_elt_t *accept_language;
#endif
#if (NGX_HTTP_DAV)
ngx_table_elt_t *depth;
ngx_table_elt_t *destination;
ngx_table_elt_t *overwrite;
ngx_table_elt_t *date;
#endif
/user和passwd是只有ngx_http_auth_basic_module才会用到的成员,这里可以忽略/
ngx_str_t user;
ngx_str_t passwd;
/cookies是以ngx_array_t数组存储的,本章先不介绍这个数据结构,感兴趣的话可以直接跳到7.3节了解ngx_array_t的相关用法/
ngx_array_t cookies;
//server名称
ngx_str_t server;
//根据ngx_table_elt_t *content_length计算出的HTTP包体大小
off_t content_length_n;
time_t keep_alive_n;
/HTTP连接类型,它的取值范围是0、NGX_http_CONNECTION_CLOSE或者NGX_HTTP_CONNECTION_KEEP_ALIVE/
unsigned connection_type:2;
/以下7个标志位是HTTP框架根据浏览器传来的“useragent”头部,它们可用来判断浏览器的类型,值为1时表示是相应的浏览器发来的请求,值为0时则相反/
unsigned msie:1;
unsigned msie6:1;
unsigned opera:1;
unsigned gecko:1;
unsigned chrome:1;
unsigned safari:1;
unsigned konqueror:1;
} ngx_http_headers_in_t;
获取HTTP头部时,直接使用r->headers_in的相应成员就可以了。这里举例说明一下如何通过遍历headers链表获取非RFC2616标准的HTTP头部,读者可以先回顾一下ngx_list_t链表和ngx_table_elt_t结构体的用法。前面3.2.3节中已经介绍过,headers是一个ngx_list_t链表,它存储着解析过的所有HTTP头部,链表中的元素都是ngx_table_elt_t类型。下面尝试在一个用户请求中找到“Rpc-Description”头部,首先判断其值是否为“uploadFile”,再决定后续的服务器行为,代码如下。
ngx_list_part_t *part = &r->headers_in.headers.part;
ngx_table_elt_t *header = part->elts;
//开始遍历链表
for (i = 0; /* void */; i++) {
//判断是否到达链表中当前数组的结尾处
if (i >= part->nelts) {
//是否还有下一个链表数组元素
if (part->next == NULL) {
break;
}
/* part设置为next来访问下一个链表数组;header也指向下一个链表数组的首地址;i设置为0时,表示从头开始遍历新的链表数组*/
part = part->next;
header = part->elts;
i = 0;
}
//hash为0时表示不是合法的头部
if (header[i].hash == 0) {
continue;
}
/判断当前的头部是否是“Rpc-Description”。如果想要忽略大小写,则应该先用header[i].lowcase_key代替header[i].key.data,然后比较字符串/
if (0 == ngx_strncasecmp(header[i].key.data,
(u_char*) "Rpc-Description",
header[i].key.len))
{
//判断这个HTTP头部的值是否是“uploadFile”
if (0 == ngx_strncmp(header[i].value.data,
"uploadFile",
header[i].value.len))
{
//找到了正确的头部,继续向下执行
}
}
}
对于常见的HTTP头部,直接获取r->headers_in中已经由HTTP框架解析过的成员即可,而对于不常见的HTTP头部,需要遍历r->headers_in.headers链表才能获得。
3.6.4 获取HTTP包体
HTTP包体的长度有可能非常大,如果试图一次性调用并读取完所有的包体,那么多半会阻塞Nginx进程。HTTP框架提供了一种方法来异步地接收包体:
ngx_int_t ngx_http_read_client_request_body(ngx_http_request_t *r, ngx_http_client_body_handler_pt post_handler);
ngx_http_read_client_request_body是一个异步方法,调用它只是说明要求Nginx开始接收请求的包体,并不表示是否已经接收完,当接收完所有的包体内容后,post_handler指向的回调方法会被调用。因此,即使在调用了ngx_http_read_client_request_body方法后它已经返回,也无法确定这时是否已经调用过post_handler指向的方法。换句话说,ngx_http_read_client_request_body返回时既有可能已经接收完请求中所有的包体(假如包体的长度很小),也有可能还没开始接收包体。如果ngx_http_read_client_request_body是在ngx_http_mytest_handler处理方法中调用的,那么后者一般要返回NGX_DONE,因为下一步就是将它的返回值作为参数传给ngx_http_finalize_request。NGX_DONE的意义在3.6.1节中已经介绍过,这里不再赘述。
下面看一下包体接收完毕后的回调方法原型ngx_http_client_body_handler_pt是如何定义的:
typedef void (*ngx_http_client_body_handler_pt)(ngx_http_request_t *r);
其中,有参数ngx_http_request_t r,这个请求的信息都可以从r中获得。这样可以定义一个方法void func(ngx_http_request_t r),在Nginx接收完包体`
时调用它,另外,后续的流程也都会写在这个方法中,例如:
void ngx_http_mytest_body_handler(ngx_http_request_t *r)
{
…
}
注意 ngx_http_mytest_body_handler的返回类型是void,Nginx不会根据返回值做一些收尾工作,因此,我们在该方法里处理完请求时必须要主动调用ngx_http_finalize_request方法来结束请求。
接收包体时可以这样写:
ngx_int_t rc = ngx_http_read_client_request_body(r, ngx_http_mytest_body_handler);
if (rc >= NGX_http_SPECIAL_RESPONSE) {
return rc;
}
return NGX_DONE;
Nginx异步接收HTTP请求的包体的内容将在11.8节中详述。
如果不想处理请求中的包体,那么可以调用ngx_http_discard_request_body方法将接收自客户端的HTTP包体丢弃掉。例如:
ngx_int_t rc = ngx_http_discard_request_body(r);
if (rc != NGX_OK) {
return rc;
}
ngx_http_discard_request_body只是丢弃包体,不处理包体不就行了吗?何必还要调用ngx_http_discard_request_body方法呢?其实这一步非常有意义,因为有些客户端可能会一直试图发送包体,而如果HTTP模块不接收发来的TCP流,有可能造成客户端发送超时。
接收完请求的包体后,可以在r->request_body->temp_file->file中获取临时文件(假定将r->request_body_in_file_only标志位设为1,那就一定可以在这个变量获取到包体。更复杂的接收包体的方式本节暂不讨论)。file是一个ngx_file_t类型,在3.8节会详细介绍它的用法。这里,我们可以从r->request_body->temp_file->file.name中获取Nginx接收到的请求包体所在文件的名称(包括路径)。