《深入理解Nginx:模块开发与架构解析》一3.6 处理用户请求

简介: 本节书摘来自华章出版社《深入理解Nginx:模块开发与架构解析》一书中的第3章,第3.6节,作者 陶辉,更多章节内容可以访问云栖社区“华章计算机”公众号查看

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接收到的请求包体所在文件的名称(包括路径)。

相关文章
|
18天前
|
Linux 编译器 开发者
Linux设备树解析:桥接硬件与操作系统的关键架构
在探索Linux的庞大和复杂世界时🌌,我们经常会遇到许多关键概念和工具🛠️,它们使得Linux成为了一个强大和灵活的操作系统💪。其中,"设备树"(Device Tree)是一个不可或缺的部分🌲,尤其是在嵌入式系统🖥️和多平台硬件支持方面🔌。让我们深入了解Linux设备树是什么,它的起源,以及为什么Linux需要它🌳。
Linux设备树解析:桥接硬件与操作系统的关键架构
|
1月前
|
存储 搜索推荐 数据挖掘
ElasticSearch架构介绍及原理解析
ElasticSearch架构介绍及原理解析
106 0
|
1月前
|
JSON 数据格式
第三方系统或者工具通过 HTTP 请求发送给 ABAP 系统的数据,应该如何解析试读版
第三方系统或者工具通过 HTTP 请求发送给 ABAP 系统的数据,应该如何解析试读版
27 0
|
1月前
|
网络协议 Linux
Linux DNS服务详解——DNS主从架构配置
Linux DNS服务详解——DNS主从架构配置
408 4
|
2月前
|
架构师 安全 Java
资深架构师带你解析Synchronize关键字原理
众所周知 Synchronize 关键字是解决并发问题常用解决方案,有以下三种使用方式:
28 0
|
2月前
|
应用服务中间件 PHP 开发工具
Nginx解析环境搭建及实战
Nginx解析环境搭建及实战
26 0
|
1月前
|
消息中间件 Cloud Native Java
【Spring云原生系列】SpringBoot+Spring Cloud Stream:消息驱动架构(MDA)解析,实现异步处理与解耦合
【Spring云原生系列】SpringBoot+Spring Cloud Stream:消息驱动架构(MDA)解析,实现异步处理与解耦合
|
2月前
|
供应链 Java
云HIS技术架构:Angular+Nginx+Java+Spring,SpringBoot
标准数据维护 用户信息:维护用户的基本信息,所在科室以及各个系统所具体的权限。 科室信息:维护医院的科室信息。 数据字典:标准字典信息的维护。 药品/诊疗目录维护:维护药品和诊疗目录的基本信息。
33 2
|
4天前
|
前端开发 Java
SpringBoot之三层架构的详细解析
SpringBoot之三层架构的详细解析
20 0
|
4天前
|
Web App开发 前端开发 Java
SpringBoot之请求的详细解析
SpringBoot之请求的详细解析
21 0

推荐镜像

更多