在ngx_http_process_request_line函数中,解析完请求行之后,如果请求行的uri里面包含了域名部分,则将其保持在请求结构的headers_in成员的server字段,headers_in用来保存所有请求头,它的类型为ngx_http_headers_in_t:

typedef struct {ngx_list_t                        headers;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;
#endifngx_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;
#endifngx_str_t                         user;ngx_str_t                         passwd;ngx_array_t                       cookies;ngx_str_t                         server;off_t                             content_length_n;time_t                            keep_alive_n;unsigned                          connection_type:2;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;

接着,该函数会检查进来的请求是否使用的是http0.9,如果是的话则使用从请求行里得到的域名,调用ngx_http_find_virtual_server()函数来查找用来处理该请求的虚拟服务器配置,之前通过端口和地址找到的默认配置不再使用,找到相应的配置之后,则直接调用ngx_http_process_request()函数处理该请求,因为http0.9是最原始的http协议,它里面没有定义任何请求头,显然就不需要读取请求头的操作。

 if (r->host_start && r->host_end) {host = r->host_start;n = ngx_http_validate_host(r, &host,r->host_end - r->host_start, 0);if (n == 0) {ngx_log_error(NGX_LOG_INFO, c->log, 0,"client sent invalid host in request line");ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);return;}if (n < 0) {ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);return;}r->headers_in.server.len = n;r->headers_in.server.data = host;}if (r->http_version < NGX_HTTP_VERSION_10) {if (ngx_http_find_virtual_server(r, r->headers_in.server.data,r->headers_in.server.len)== NGX_ERROR){ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);return;}ngx_http_process_request(r);return;}

当然,如果是1.0或者更新的http协议,接下来要做的就是读取请求头了,首先nginx会为请求头分配空间,ngx_http_headers_in_t结构的headers字段为一个链表结构,它被用来保存所有请求头,初始为它分配了20个节点,每个节点的类型为ngx_table_elt_t,保存请求头的name/value值对,还可以看到ngx_http_headers_in_t结构有很多类型为ngx_table_elt_t*的指针成员,而且从它们的命名可以看出是一些常见的请求头名字,nginx对这些常用的请求头在ngx_http_headers_in_t结构里面保存了一份引用,后续需要使用的话,可以直接通过这些成员得到,另外也事先为cookie头分配了2个元素的数组空间,做完这些内存准备工作之后,该请求对应的读事件结构的处理函数被设置为ngx_http_process_request_headers,并随后马上调用了该函数。

  if (ngx_list_init(&r->headers_in.headers, r->pool, 20,sizeof(ngx_table_elt_t))!= NGX_OK){ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);return;}if (ngx_array_init(&r->headers_in.cookies, r->pool, 2,sizeof(ngx_table_elt_t *))!= NGX_OK){ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);return;}c->log->action = "reading client request headers";rev->handler = ngx_http_process_request_headers;ngx_http_process_request_headers(rev);

ngx_http_process_request_headers函数循环的读取所有的请求头,并保存和初始化和请求头相关的结构,下面详细分析一下该函数:
因为nginx对读取请求头有超时限制,ngx_http_process_request_headers函数作为读事件处理函数,一并处理了超时事件,如果读超时了,nginx直接给该请求返回408错误:

    if (rev->timedout) {ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT, "client timed out");c->timedout = 1;ngx_http_close_request(r, NGX_HTTP_REQUEST_TIME_OUT);return;}

读取和解析请求头的逻辑和处理请求行差不多,总的流程也是循环的调用ngx_http_read_request_header()函数读取数据,然后再调用一个解析函数来从读取的数据中解析请求头,直到解析完所有请求头,或者发生解析错误为主。当然由于涉及到网络io,这个流程可能发生在多个io事件的上下文中。

接着来细看该函数,先调用了ngx_http_read_request_header()函数读取数据,如果当前连接并没有数据过来,再直接返回,等待下一次读事件到来,如果读到了一些数据则调用ngx_http_parse_header_line()函数来解析,同样的该解析函数实现为一个有限状态机,逻辑很简单,只是根据http协议的解析一个请求头的name/vale对,每次调用该函数最多解析出一个请求头,该函数返回4种不同返回值,表示不同解析结果:

1,返回NGX_OK,表示解析出了一行请求头,这时还要判断解析出的请求头名字里面是否有非法字符,名字里面合法的字符包括字母,数字和连字符(-),另外如果设置了underscores_in_headers指令为on,则下划线也是合法字符,但是nginx默认下划线不合法,当请求头里面包含了非法的字符,nginx默认只是忽略这一行请求头;如果一切都正常,nginx会将该请求头及请求头名字的hash值保存在请求结构体的headers_in成员的headers链表,而且对于一些常见的请求头,如Host,Connection,nginx采用了类似于配置指令的方式,事先给这些请求头分配了一个处理函数,当解析出一个请求头时,会检查该请求头是否有设置处理函数,有的话则调用之,nginx所有有处理函数的请求头都记录在ngx_http_headers_in全局数组中:

typedef struct {ngx_str_t                         name;ngx_uint_t                        offset;ngx_http_header_handler_pt        handler;
} ngx_http_header_t;ngx_http_header_t  ngx_http_headers_in[] = {{ ngx_string("Host"), offsetof(ngx_http_headers_in_t, host),ngx_http_process_host },{ ngx_string("Connection"), offsetof(ngx_http_headers_in_t, connection),ngx_http_process_connection },{ ngx_string("If-Modified-Since"),offsetof(ngx_http_headers_in_t, if_modified_since),ngx_http_process_unique_header_line },{ ngx_string("If-Unmodified-Since"),offsetof(ngx_http_headers_in_t, if_unmodified_since),ngx_http_process_unique_header_line },{ ngx_string("User-Agent"), offsetof(ngx_http_headers_in_t, user_agent),ngx_http_process_user_agent },{ ngx_string("Referer"), offsetof(ngx_http_headers_in_t, referer),ngx_http_process_header_line },{ ngx_string("Content-Length"),offsetof(ngx_http_headers_in_t, content_length),ngx_http_process_unique_header_line },{ ngx_string("Content-Type"),offsetof(ngx_http_headers_in_t, content_type),ngx_http_process_header_line },{ ngx_string("Range"), offsetof(ngx_http_headers_in_t, range),ngx_http_process_header_line },{ ngx_string("If-Range"),offsetof(ngx_http_headers_in_t, if_range),ngx_http_process_unique_header_line },{ ngx_string("Transfer-Encoding"),offsetof(ngx_http_headers_in_t, transfer_encoding),ngx_http_process_header_line },{ ngx_string("Expect"),offsetof(ngx_http_headers_in_t, expect),ngx_http_process_unique_header_line },#if (NGX_HTTP_GZIP){ ngx_string("Accept-Encoding"),offsetof(ngx_http_headers_in_t, accept_encoding),ngx_http_process_header_line },{ ngx_string("Via"), offsetof(ngx_http_headers_in_t, via),ngx_http_process_header_line },
#endif{ ngx_string("Authorization"),offsetof(ngx_http_headers_in_t, authorization),ngx_http_process_unique_header_line },{ ngx_string("Keep-Alive"), offsetof(ngx_http_headers_in_t, keep_alive),ngx_http_process_header_line },#if (NGX_HTTP_PROXY || NGX_HTTP_REALIP || NGX_HTTP_GEO){ ngx_string("X-Forwarded-For"),offsetof(ngx_http_headers_in_t, x_forwarded_for),ngx_http_process_header_line },
#endif#if (NGX_HTTP_REALIP){ ngx_string("X-Real-IP"),offsetof(ngx_http_headers_in_t, x_real_ip),ngx_http_process_header_line },
#endif#if (NGX_HTTP_HEADERS){ ngx_string("Accept"), offsetof(ngx_http_headers_in_t, accept),ngx_http_process_header_line },{ ngx_string("Accept-Language"),offsetof(ngx_http_headers_in_t, accept_language),ngx_http_process_header_line },
#endif#if (NGX_HTTP_DAV){ ngx_string("Depth"), offsetof(ngx_http_headers_in_t, depth),ngx_http_process_header_line },{ ngx_string("Destination"), offsetof(ngx_http_headers_in_t, destination),ngx_http_process_header_line },{ ngx_string("Overwrite"), offsetof(ngx_http_headers_in_t, overwrite),ngx_http_process_header_line },{ ngx_string("Date"), offsetof(ngx_http_headers_in_t, date),ngx_http_process_header_line },
#endif{ ngx_string("Cookie"), 0, ngx_http_process_cookie },{ ngx_null_string, 0, NULL }
};

ngx_http_headers_in数组当前包含了25个常用的请求头,每个请求头都设置了一个处理函数,当前其中一部分请求头设置的是公共的处理函数,这里有2个公共的处理函数,ngx_http_process_header_line和ngx_http_process_unique_header_line。

先来看一下处理函数的函数指针定义:
typedef ngx_int_t (*ngx_http_header_handler_pt)(ngx_http_request_t *r,
    ngx_table_elt_t *h, ngx_uint_t offset);
它有3个参数,r为对应的请求结构,h为该请求头在headers_in.headers链表节点的指针,offset为该请求头的引用在ngx_http_headers_in_t结构中的偏移。
再来看ngx_http_process_header_line函数:

static ngx_int_t
ngx_http_process_header_line(ngx_http_request_t *r, ngx_table_elt_t *h,ngx_uint_t offset)
{ngx_table_elt_t  **ph;ph = (ngx_table_elt_t **) ((char *) &r->headers_in + offset);if (*ph == NULL) {*ph = h;}return NGX_OK;
}

这个函数只是简单将该请求头在ngx_http_headers_in_t结构中保存一份引用。ngx_http_process_unique_header_line功能类似,不同点在于该函数会检查这个请求头是否是重复的,如果是的话,则给该请求返回400错误。

ngx_http_headers_in数组中剩下的请求头都有自己特殊的处理函数,这些特殊的函数根据对应的请求头有一些特殊的处理,下面我们拿Host头的处理函数ngx_http_process_host做一下介绍:

static ngx_int_t
ngx_http_process_host(ngx_http_request_t *r, ngx_table_elt_t *h,ngx_uint_t offset)
{u_char   *host;ssize_t   len;if (r->headers_in.host == NULL) {r->headers_in.host = h;}host = h->value.data;len = ngx_http_validate_host(r, &host, h->value.len, 0);if (len == 0) {ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,"client sent invalid host header");ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);return NGX_ERROR;}if (len < 0) {ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);return NGX_ERROR;}if (r->headers_in.server.len) {return NGX_OK;}r->headers_in.server.len = len;r->headers_in.server.data = host;return NGX_OK;
}

此函数的目的也是保存Host头的快速引用,它会对Host头的值做一些合法性检查,并从中解析出域名,保存在headers_in.server字段,实际上前面在解析请求行时,headers_in.server可能已经被赋值为从请求行中解析出来的域名,根据http协议的规范,如果请求行中的uri带有域名的话,则域名以它为准,所以这里需检查一下headers_in.server是否为空,如果不为空则不需要再赋值。
其他请求头的特殊处理函数,不再做介绍,大致都是根据该请求头在http协议中规定的意义及其值设置请求的一些属性,必备后续使用。

对一个合法的请求头的处理大致为如上所述;

2,返回NGX_AGAIN,表示当前接收到的数据不够,一行请求头还未结束,需要继续下一轮循环。在下一轮循环中,nginx首先检查请求头缓冲区header_in是否已满,如够满了,则调用ngx_http_alloc_large_header_buffer()函数分配更多缓冲区,下面分析一下ngx_http_alloc_large_header_buffer函数:

static ngx_int_t
ngx_http_alloc_large_header_buffer(ngx_http_request_t *r,ngx_uint_t request_line)
{u_char                    *old, *new;ngx_buf_t                 *b;ngx_http_connection_t     *hc;ngx_http_core_srv_conf_t  *cscf;ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,"http alloc large header buffer");/** 在解析请求行阶段,如果客户端在发送请求行之前发送了大量回车换行符将* 缓冲区塞满了,针对这种情况,nginx只是简单的重置缓冲区,丢弃这些垃圾* 数据,不需要分配更大的内存。*/if (request_line && r->state == 0) {/* the client fills up the buffer with "\r\n" */r->request_length += r->header_in->end - r->header_in->start;r->header_in->pos = r->header_in->start;r->header_in->last = r->header_in->start;return NGX_OK;}/* 保存请求行或者请求头在旧缓冲区中的起始地址 */old = request_line ? r->request_start : r->header_name_start;cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);/* 如果一个大缓冲区还装不下请求行或者一个请求头,则返回错误 */if (r->state != 0&& (size_t) (r->header_in->pos - old)>= cscf->large_client_header_buffers.size){return NGX_DECLINED;}hc = r->http_connection;/* 首先在ngx_http_connection_t结构中查找是否有空闲缓冲区,有的话,直接取之 */if (hc->nfree) {b = hc->free[--hc->nfree];ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,"http large header free: %p %uz",b->pos, b->end - b->last);/* 检查给该请求分配的请求头缓冲区个数是否已经超过限制,默认最大个数为4个 */} else if (hc->nbusy < cscf->large_client_header_buffers.num) {if (hc->busy == NULL) {hc->busy = ngx_palloc(r->connection->pool,cscf->large_client_header_buffers.num * sizeof(ngx_buf_t *));if (hc->busy == NULL) {return NGX_ERROR;}}/* 如果还没有达到最大分配数量,则分配一个新的大缓冲区 */b = ngx_create_temp_buf(r->connection->pool,cscf->large_client_header_buffers.size);if (b == NULL) {return NGX_ERROR;}ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,"http large header alloc: %p %uz",b->pos, b->end - b->last);} else {/* 如果已经达到最大的分配限制,则返回错误 */return NGX_DECLINED;}/* 将从空闲队列取得的或者新分配的缓冲区加入已使用队列 */hc->busy[hc->nbusy++] = b;/** 因为nginx中,所有的请求头的保存形式都是指针(起始和结束地址),* 所以一行完整的请求头必须放在连续的内存块中。如果旧的缓冲区不能* 再放下整行请求头,则分配新缓冲区,并从旧缓冲区拷贝已经读取的部分请求头,* 拷贝完之后,需要修改所有相关指针指向到新缓冲区。* status为0表示解析完一行请求头之后,缓冲区正好被用完,这种情况不需要拷贝*/if (r->state == 0) {/** r->state == 0 means that a header line was parsed successfully* and we do not need to copy incomplete header line and* to relocate the parser header pointers*/r->request_length += r->header_in->end - r->header_in->start;r->header_in = b;return NGX_OK;}ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,"http large header copy: %d", r->header_in->pos - old);r->request_length += old - r->header_in->start;new = b->start;/* 拷贝旧缓冲区中不完整的请求头 */ngx_memcpy(new, old, r->header_in->pos - old);b->pos = new + (r->header_in->pos - old);b->last = new + (r->header_in->pos - old);/* 修改相应的指针指向新缓冲区 */if (request_line) {r->request_start = new;if (r->request_end) {r->request_end = new + (r->request_end - old);}r->method_end = new + (r->method_end - old);r->uri_start = new + (r->uri_start - old);r->uri_end = new + (r->uri_end - old);if (r->schema_start) {r->schema_start = new + (r->schema_start - old);r->schema_end = new + (r->schema_end - old);}if (r->host_start) {r->host_start = new + (r->host_start - old);if (r->host_end) {r->host_end = new + (r->host_end - old);}}if (r->port_start) {r->port_start = new + (r->port_start - old);r->port_end = new + (r->port_end - old);}if (r->uri_ext) {r->uri_ext = new + (r->uri_ext - old);}if (r->args_start) {r->args_start = new + (r->args_start - old);}if (r->http_protocol.data) {r->http_protocol.data = new + (r->http_protocol.data - old);}} else {r->header_name_start = new;r->header_name_end = new + (r->header_name_end - old);r->header_start = new + (r->header_start - old);r->header_end = new + (r->header_end - old);}r->header_in = b;return NGX_OK;
}

当ngx_http_alloc_large_header_buffer函数返回NGX_DECLINED)时,表示客户端发送了过大的一行请求头,或者是整个请求头部超过了限制,nginx会返回494错误,注意到nginx再返回494错误之前将请求的lingering_close标识置为了1,这样做的目的是在返回响应之丢弃掉客户端发过来的其他数据;

3,返回NGX_HTTP_PARSE_INVALID_HEADER,表示请求头解析过程中遇到错误,一般为客户端发送了不符合协议规范的头部,此时nginx返回400错误。

4,返回NGX_HTTP_PARSE_HEADER_DONE,表示所有请求头已经成功的解析,这时请求的状态被设置为NGX_HTTP_PROCESS_REQUEST_STATE,意味着结束了请求读取阶段,正式进入了请求处理阶段,但是实际上请求可能含有请求体,nginx在请求读取阶段并不会去读取请求体,这个工作交给了后续的请求处理阶段的模块,这样做的目的是nginx本身并不知道这些请求体是否有用,如果后续模块并不需要的话,一方面请求体一般较大,如果全部读取进内存,则白白耗费大量的内存空间,另一方面即使nginx将请求体写进磁盘,但是涉及到磁盘io,会耗费比较多时间。所以交由后续模块来决定读取还是丢弃请求体是最明智的办法。

读取完请求头之后,nginx调用了ngx_http_process_request_header()函数,这个函数主要做了两个方面的事情,一是调用ngx_http_find_virtual_server()函数查找虚拟服务器配置;二是对一些请求头做一些协议的检查。比如对那些使用http1.1协议但是却没有发送Host头的请求,nginx给这些请求返回400错误。还有nginx现在的版本并不支持chunked格式的输入,如果某些请求申明自己使用了chunked格式的输入(请求带有值为chunked的transfer_encoding头部),nginx给这些请求返回411错误。等等。
最后调用ngx_http_process_request()函数处理请求;
至此,nginx接收请求接收流程就介绍完毕。
--------------------- 
作者:fengmo_q 
来源:CSDN 
原文:https://blog.csdn.net/fengmo_q/article/details/7736695 
版权声明:本文为博主原创文章,转载请附上博文链接!

nginx的请求接收流程(二)相关推荐

  1. nginx的请求接收流程(一)

    今年我们组计划写一本nginx模块开发以及原理解析方面的书,整本书是以open book的形式在网上会定时的更新,网址为http://tengine.taobao.org/book/index.htm ...

  2. nginx 子请求接收响应_Nginx详解其原理

    Nginx进程模型分析  在介绍Nginx的进程模型之前我们先来给大家解释下一些常见的名词,这能辅助我们更好的了解Nginx的进程模型.作为Web服务器,设计的初衷就是为了能够处理更多的客户端的请求, ...

  3. tcp/ip 协议栈Linux内核源码分析15 udp套接字接收流程二

    内核版本:3.4.39 上篇我们分析了UDP套接字如何接收数据的流程,最终它是在内核套接字的接收队列里取出报文,剩下的问题就是谁会去写入这个队列,当然,这部分工作由内核来完成,本篇剩下的文章主要分析内 ...

  4. Spring cloud Gateway(二) 一个Http请求的流程解析

    Spring cloud Gateway(二) 一个Http请求的流程解析 简介     通过一个简单示例,debug出Spring Cloud Gateway的一个HTTP请求的处理流程 思路整理 ...

  5. Nginx 解决WebApi跨域二次请求以及Vue单页面问题

    Nginx 解决WebApi跨域二次请求以及Vue单页面问题 参考文章: (1)Nginx 解决WebApi跨域二次请求以及Vue单页面问题 (2)https://www.cnblogs.com/ji ...

  6. Nginx 架构——【核心流程+模块介绍】

    1. Nginx简介以及特点 Nginx简介: Nginx (engine x) 是一个高性能的Web服务器和反向代理服务器,也是一个IMAP/POP3/SMTP服务器 俄罗斯程序员Igor Syso ...

  7. 【面试题】一个Http请求的流程

    文章目录 一.一个Http请求的流程(+4分) 二.HTTP与HTTPS的特点和区别(+2分) 2.1.HTTP 2.2.HTTPS 2.2.1.https是什么 2.2.2.https 耗性能吗 ? ...

  8. 深入操作系统底层分析nginx网络请求及响应过程

    0. 网络传输阶段 比如说主机A是家里windows的一台笔记本电脑,主机B是linux服务器上的一个nginx,其监听80或443等web端口. 在笔记本的浏览器发送了一个http get请求,其数 ...

  9. Nginx HTTP请求的11个阶段

    前言 上一篇文章(https://blog.csdn.net/zzhongcy/article/details/86086369)简单说明了nginx的11个阶段, 今天网上看到一篇详细介绍的文章,这 ...

最新文章

  1. ora-01033:oracle initialization or shutdown in progress 解决方法
  2. json知识中的数据契约
  3. 关于清除浮动那些事儿~
  4. Linux的硬盘分区
  5. ABAP常见面试问题
  6. C#Socket开发TCP详解(二)
  7. DDD战略篇:架构设计的响应力
  8. Android PopupWindow的使用和分析
  9. 取消centOS7虚拟机锁屏
  10. Qfarm /load命令
  11. 使用阿里云容器监控服务与第三方监控框架集成搭建自己的容器看板
  12. 《算法:C语言实现》阅读笔记
  13. C语言如何调用REFPROP软件,coolprop调用refprop计算流体的热力学性质和传输特性
  14. 【第168期】游戏策划:草率了,这简历像极了脱缰的马儿
  15. 35岁开发转测试,能度过中年危机吗?
  16. Google Play关键词选词方法及常用选词工具
  17. java实现网页评分_Java web分级测试评分C级感受
  18. 世界杯最野蛮大战,巴西付出惨重代价步入四强
  19. 达梦数据库dm7同构数据库的双向同步
  20. 华为发布21年财报 孟晚舟出席

热门文章

  1. 【转载】JAVA内存模型和线程安全
  2. Java数据结构与算法(20) - ch08树
  3. fatal error LNK1561: 必须定义入口点问题的我是这样解决的
  4. 对于***win2000 精彩过程实例!不可错过_出自:痞菜
  5. CodeForces - 1480D1 Painting the Array I(贪心)
  6. HihoCoder - 1445 后缀自动机二·重复旋律5(后缀自动机)
  7. HYSBZ - 3676 回文串(回文自动机)
  8. string类型比较_redis存json数据时选择string还是hash
  9. Spaly_Tree 模版
  10. WinDbg学习笔记(一)--认识WinDbg