最新消息:

Nginx教程:nginx 中处理 http header

未分类 admin 5237浏览 0评论

这里主要的头的处理是放在filter中做的,我们首先来看config(默认情况)后,Nginx的obj目录下的ngx_modules.c这个文件中的关于filter的部分:

ngx_module_t *ngx_modules[] = {
................................................
&ngx_http_write_filter_module,
&ngx_http_header_filter_module,
&ngx_http_chunked_filter_module,
&ngx_http_range_header_filter_module,
&ngx_http_gzip_filter_module,
&ngx_http_postpone_filter_module,
&ngx_http_ssi_filter_module,
&ngx_http_charset_filter_module,
&ngx_http_userid_filter_module,
&ngx_http_headers_filter_module,
&ngx_http_copy_filter_module,
&ngx_http_range_body_filter_module,
&ngx_http_not_modified_filter_module,
NULL
};

nginx处理filter是严格按照顺序的(以前的blog有描述过),其中越后面的filter越前处理,也就是说这里第一个处理的filter是not_modified_filter,最后一个是write filter,write filter最终会调用发送数据的方法(sendfile or writev)来讲数据发送出去。

接下来就要按照顺序来一个个的分析这些filter的处理流程.

第一个是not_modified_header_filter,这个filter主要是为了处理If-Modified-Since这个头的,这个头的含义很简单,就是一个标记用来标记服务端上次取得这个请求内容的时间的,服务端看到这个时间就来和请求文件的修改时间进行比较,从而知道client的内容是不是过期的,如果是过期的则发送新的内容,否则返回304。

而一般来说客户端的If-Modified-Since这个时间值是server通过Last-Modified这个头传递过去的,这里要注意,只有http 1.1才要求无论如何都要传递这个头。

下面是RFC中对于这个头如何执行的一个描述:

a) If the request would normally result in anything other than a 200 (OK) status, or if the passed If-Modified-
Since date is invalid, the response is exactly the same as for a normal GET. A date which is later than the
server’s current time is invalid.
b) If the variant has been modified since the If-Modified-Since date, the response is exactly the same as for
a normal GET.
c) If the variant has not been modified since a valid If-Modified-Since date, the server SHOULD return a
304 (Not Modified) response.

接下来就来看nginx是如何做的,这个filter只有header的,通过上面的描述我们能看到他是不需要body filter的。

这里有一个要注意的变量就是last_modified_time,这个值是保存了请求文件的最后修改时间。

还有就是nginx也有一个叫做if_modified_since的命令,这个命令用来设置如何比较if_modified_since的头的时间,这个默认是精确匹配,也就是修改时间等于if_modified_since的时间。

static ngx_int_t 
ngx_http_not_modified_header_filter(ngx_http_request_t *r) 
{ 
 time_t                     ims; 
 ngx_http_core_loc_conf_t *clcf; 
 
//如果if_modified_since没有设置或者last_modified_time为-1则直接去下一个filter. 
 if (r->headers_out.status != NGX_HTTP_OK 
 || r != r->main 
 || r->headers_in.if_modified_since == NULL 
 || r->headers_out.last_modified_time == -1) 
 { 
 return ngx_http_next_header_filter(r); 
 } 
 
 clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); 
 
//这里的if_modified_since 是指nginx的if_modified_since命令,如果它是关闭的,则直接进入下一个filter 
 if (clcf->if_modified_since == NGX_HTTP_IMS_OFF) { 
 return ngx_http_next_header_filter(r); 
 } 
//取得对应的if_modified_since的时间值 
 ims = ngx_http_parse_time(r->headers_in.if_modified_since->value.data, 
 r->headers_in.if_modified_since->value.len); 
 
 ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 
 "http ims:%d lm:%d", ims, r->headers_out.last_modified_time); 
//如果不等于,则说明有可能文件被改变过了。 
 if (ims != r->headers_out.last_modified_time) { 
//如果设置的是精确匹配或者时间小于文件修改的时间则直接调用下一个filter,因为这说明文件被修改过了,需要发送新的给client 
 if (clcf->if_modified_since == NGX_HTTP_IMS_EXACT 
 || ims < r->headers_out.last_modified_time) 
 { 
 return ngx_http_next_header_filter(r); 
 } 
 } 
//到达这里说明文件没有被修改(在上次请求之后).然后需要发送304,因此这里设置对应的头。 
 r->headers_out.status = NGX_HTTP_NOT_MODIFIED; 
//接下来就是清理工作,由于不需要发送内容了,因此清理掉很多域. 
 r->headers_out.status_line.len = 0; 
 r->headers_out.content_type.len = 0; 
 ngx_http_clear_content_length(r); 
 ngx_http_clear_accept_ranges(r); 
 
 if (r->headers_out.content_encoding) { 
 r->headers_out.content_encoding->hash = 0; 
 r->headers_out.content_encoding = NULL; 
 } 
 
 return ngx_http_next_header_filter(r); 

最后还有一个就是if-range和if_modified_since一起使用的时候会有些小变化,等下面分析range filter时候再来看这个。

第二个是 range filter ,它主要是用来处理If-Range和range这两个头的,他们主要是用于断点下载的,其中client请求使用range这个头,用于表示请求的内容的范围,而If-Range主要来实现range请求的if_modified_since的功能,下面代码我将会分析到.

而这里server生成Content-Range用于表示返回给client的信息。具体头的格式可以去看rfc文档。如果请求执行成功,server返回的是206,而如果请求的range有误,则会返回一个416. 而当请求有If-Range这个头时,则返回值有所不同的,当If-Range对应的etag或者date被匹配成功时,也就说明在上次请求之后还没被修改,此时返回206,而当不匹配时,则直接返回一个200.并返回整个文件.还有一个要注意的地方就是server发送给client有可能会一次返回多个部分.

下面这段是nginx里面的注释,描述的很清晰,对于多个部分和单个部分。不过nginx里面只有当整个body都在一个buf里面才支持多个range的。

/*
* the single part format:
*
* "HTTP/1.0 206 Partial Content" CRLF
* ... header ...
* "Content-Type: image/jpeg" CRLF
* "Content-Length: SIZE" CRLF
* "Content-Range: bytes START-END/SIZE" CRLF
* CRLF
* ... data ...
*
*
* the mutlipart format:
*
* "HTTP/1.0 206 Partial Content" CRLF
* ... header ...
* "Content-Type: multipart/byteranges; boundary=0123456789" CRLF
* CRLF
* CRLF
* "--0123456789" CRLF
* "Content-Type: image/jpeg" CRLF
* "Content-Range: bytes START0-END0/SIZE" CRLF
* CRLF
* ... data ...
* CRLF
* "--0123456789" CRLF
* "Content-Type: image/jpeg" CRLF
* "Content-Range: bytes START1-END1/SIZE" CRLF
* CRLF
* ... data ...
* CRLF
* "--0123456789--" CRLF
*/

然后来看nginx的实现.

这里要注意的就是当nginx的If-Range头只支持date而不支持ETAG,并且当If-Range设置后,如果不匹配,则nginx会返回200并且发送内容。而且Accept-Ranges就是在这个filter里面设置的。

static ngx_int_t 
ngx_http_range_header_filter(ngx_http_request_t *r) 
{ 
 time_t                        if_range; 
 ngx_int_t rc; 
 ngx_http_range_filter_ctx_t *ctx; 
 
//判断版本号以及是否允许range等 
 if (r->http_version < NGX_HTTP_VERSION_10 
 || r->headers_out.status != NGX_HTTP_OK 
 || r != r->main 
 || r->headers_out.content_length_n == -1 
 || !r->allow_ranges) 
 { 
 return ngx_http_next_header_filter(r); 
 } 
 
//判断range是否设置以及格式是否正确 
 if (r->headers_in.range == NULL 
 || r->headers_in.range->value.len < 7 
 || ngx_strncasecmp(r->headers_in.range->value.data, 
 (u_char *) "bytes=", 6) 
 != 0) 
 { 
 goto next_filter; 
 } 
 
//处理if_range头 
 if (r->headers_in.if_range && r->headers_out.last_modified_time != -1) { 
 
 if_range = ngx_http_parse_time(r->headers_in.if_range->value.data, 
 r->headers_in.if_range->value.len); 
 
 ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 
 "http ir:%d lm:%d", 
 if_range, r->headers_out.last_modified_time); 
//如果不等于则说明需要发送全新的内容. 
 if (if_range != r->headers_out.last_modified_time) { 
 goto next_filter; 
 } 
 } 
 
 ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_range_filter_ctx_t)); 
 if (ctx == NULL) { 
 return NGX_ERROR; 
 } 
//初始化ranges数组,这是因为有可能会发送多个段给client 
 if (ngx_array_init(&ctx->ranges, r->pool, 1, sizeof(ngx_http_range_t)) 
 != NGX_OK) 
 { 
 return NGX_ERROR; 
 } 
//解析range 
 rc = ngx_http_range_parse(r, ctx); 
//解析成功 
 if (rc == NGX_OK) { 
 
 ngx_http_set_ctx(r, ctx, ngx_http_range_body_filter_module); 
//设置返回值为206 
 r->headers_out.status = NGX_HTTP_PARTIAL_CONTENT; 
 r->headers_out.status_line.len = 0; 
//多个part和单个分开处理 
 if (ctx->ranges.nelts == 1) { 
 return ngx_http_range_singlepart_header(r, ctx); 
 } 
 return ngx_http_range_multipart_header(r, ctx); 
 } 
 
 if (rc == NGX_HTTP_RANGE_NOT_SATISFIABLE) { 
//返回416 
 return ngx_http_range_not_satisfiable(r); 
 } 
 
 /* rc == NGX_ERROR */
 
 return rc; 
 
next_filter: 
//发送客户端Accept-Ranges头。 
 r->headers_out.accept_ranges = ngx_list_push(&r->headers_out.headers); 
 if (r->headers_out.accept_ranges == NULL) { 
 return NGX_ERROR; 
 } 
 
 r->headers_out.accept_ranges->hash = 1; 
 r->headers_out.accept_ranges->key.len = sizeof("Accept-Ranges") - 1; 
 r->headers_out.accept_ranges->key.data = (u_char *) "Accept-Ranges"; 
 r->headers_out.accept_ranges->value.len = sizeof("bytes") - 1; 
 r->headers_out.accept_ranges->value.data = (u_char *) "bytes"; 
 
 return ngx_http_next_header_filter(r); 
}

接下来是headers_filter,这个filter主要是处理expires头以及添加一些自定义的头,这里我们主要来看expires的头。

expire主要是为了防止client发起没必要的请求,server端发送一个expire头,然后客户端通过这个expire时间来计算是否需要发送请求给server取得内容。这里还有一个新的较头是Cache-Control,他的max-age 域能够实现expire的相同功能,并且max-age优先级高于expire的。

在nginx中如果设置expires命令的话,nginx会同时设置expire头和cache-control头.这里有一个要注意的就是cache-control的max-age=0和no-cache的区别。

其中no-cache表示客户端不需要cache当前的网页,也就是每次访问访问都要直接去请求源服务器。而max-age=0则表示每次都要到源服务器去确认当前的文件是否有更新。因此这里一般都是配合if-modify-since一起使用的.

这里主要来看nginx如何设置expire的,这里nginx的expires命令可以设置4类参数,分别为:epoch,max,off以及超时时间.对应的cache-control的值为no-cache,10y. off的话直接跳过这个filter,数值的话就是赋值给max-age.并且nginx是同时设置expire和cache-control.这是为了防止有的http 1.0没有实现cache-control.

而且nginx的expire命令的date也分好几类,这些具体可以去看nginx的wiki的描述。

static ngx_int_t 
ngx_http_set_expires(ngx_http_request_t *r, ngx_http_headers_conf_t *conf) 
{ 
 size_t            len; 
 time_t            now, expires_time, max_age; 
 ngx_uint_t i; 
 ngx_table_elt_t *expires, *cc, **ccp; 
 
 expires = r->headers_out.expires; 
 
 if (expires == NULL) { 
 
 expires = ngx_list_push(&r->headers_out.headers); 
 if (expires == NULL) { 
 return NGX_ERROR; 
 } 
 
 r->headers_out.expires = expires; 
//设置Expires的name 
 expires->hash = 1; 
 expires->key.len = sizeof("Expires") - 1; 
 expires->key.data = (u_char *) "Expires"; 
 } 
//时间格式 
 len = sizeof("Mon, 28 Sep 1970 06:00:00 GMT"); 
 expires->value.len = len - 1; 
//开始设置cache-control 
 ccp = r->headers_out.cache_control.elts; 
 
 if (ccp == NULL) { 
 
 if (ngx_array_init(&r->headers_out.cache_control, r->pool, 
 1, sizeof(ngx_table_elt_t *)) 
 != NGX_OK) 
 { 
 return NGX_ERROR; 
 } 
 
 ccp = ngx_array_push(&r->headers_out.cache_control); 
 if (ccp == NULL) { 
 return NGX_ERROR; 
 } 
 
 cc = ngx_list_push(&r->headers_out.headers); 
 if (cc == NULL) { 
 return NGX_ERROR; 
 } 
//设置cache-control的name 
 cc->hash = 1; 
 cc->key.len = sizeof("Cache-Control") - 1; 
 cc->key.data = (u_char *) "Cache-Control"; 
 
 *ccp = cc; 
 
 } else { 
 for (i = 1; i < r->headers_out.cache_control.nelts; i++) { 
 ccp[i]->hash = 0; 
 } 
 
 cc = ccp[0]; 
 } 
 
//根据设置的不同参数来设置不同的头的值 
 if (conf->expires == NGX_HTTP_EXPIRES_EPOCH) { 
 expires->value.data = (u_char *) "Thu, 01 Jan 1970 00:00:01 GMT"; 
//设置no-cache 
 cc->value.len = sizeof("no-cache") - 1; 
 cc->value.data = (u_char *) "no-cache"; 
 
 return NGX_OK; 
 } 
 
 if (conf->expires == NGX_HTTP_EXPIRES_MAX) { 
 expires->value.data = (u_char *) "Thu, 31 Dec 2037 23:55:55 GMT"; 
//max的话设置时间为10年. 
 /* 10 years */
 cc->value.len = sizeof("max-age=315360000") - 1; 
 cc->value.data = (u_char *) "max-age=315360000"; 
 
 return NGX_OK; 
 } 
 
 expires->value.data = ngx_pnalloc(r->pool, len); 
 if (expires->value.data == NULL) { 
 return NGX_ERROR; 
 } 
 
 if (conf->expires_time == 0) { 
 ngx_memcpy(expires->value.data, ngx_cached_http_time.data, 
 ngx_cached_http_time.len + 1); 
//超时时间为0 
 cc->value.len = sizeof("max-age=0") - 1; 
 cc->value.data = (u_char *) "max-age=0"; 
 
 return NGX_OK; 
 } 
 
 now = ngx_time(); 
//开始设置超时时间 
................................................................................................ 
 
 cc->value.data = ngx_pnalloc(r->pool, 
 sizeof("max-age=") + NGX_TIME_T_LEN + 1); 
 if (cc->value.data == NULL) { 
 return NGX_ERROR; 
 } 
 
 cc->value.len = ngx_sprintf(cc->value.data, "max-age=%T", max_age) 
 - cc->value.data; 
 
 return NGX_OK;

然后是charset filter,这个主要是处理Nginx内部的charset命令,转换为设置的编码。这个filter就不介绍了,主要是一个解码的过程。

再接下来是chunk filter,它主要是生成chunk数据,这里要注意nginx只支持服务端生成chunk,而不支持客户端发送的chunk数据。chunk的格式很简单,简单的来说就是大小+数据内容。

先来看chunk的header filter,在filter中,主要是用来判断是否需要chunk数据,然后设置相关标记位,以便于后面的body filter处理.

static ngx_int_t 
ngx_http_chunked_header_filter(ngx_http_request_t *r) 
{ 
 ngx_http_core_loc_conf_t *clcf; 
//如果是304,204或者是head方法,则直接跳过chunk filter。 
 if (r->headers_out.status == NGX_HTTP_NOT_MODIFIED 
 || r->headers_out.status == NGX_HTTP_NO_CONTENT 
 || r != r->main 
 || (r->method & NGX_HTTP_HEAD)) 
 { 
 return ngx_http_next_header_filter(r); 
 } 
 
//如果content_length_n为-1 则进入chunk处理,下面我们会看到这个值在那里设置,也就是nginx中如何打开chunk编码. 
 if (r->headers_out.content_length_n == -1) { 
 if (r->http_version < NGX_HTTP_VERSION_11) { 
 r->keepalive = 0; 
 
 } else { 
 clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); 
//chunked_transfer_encoding命令对应的参数,这个值默认是1,因此默认chunk是打开的。 
 if (clcf->chunked_transfer_encoding) { 
 
 r->chunked = 1; 
 
 } else {
 r->keepalive = 0;
 }
 }
 }
 
 return ngx_http_next_header_filter(r);

然后来看content_length_n何时被改变为-1,也就是准备chunk编码,这个值是在ngx_http_clear_content_length中被改变了。也就是如果希望chunk编码的话,必须调用这个函数。

#define ngx_http_clear_content_length(r)  
  
//设置长度为-1.
 r->headers_out.content_length_n = -1; 
 if (r->headers_out.content_length) { 
 r->headers_out.content_length->hash = 0; 
 r->headers_out.content_length = NULL; 
 }

然后来看body filter是如何处理的。这里的处理其实很简单,只不过特殊处理下last buf.大体流程是这样子的,首先计算chunk的大小,然后讲将要发送的buf串联起来,然后将大小插入到数据buf之前,最后设置tail buf,如果是last buf,则结尾是:

CRLF "0" CRLF CRLF

如果不是last buf,则结尾就是一个CRLF,这些都是严格遵守rfc2616。

来看详细的代码:

 if (size) {
 b = ngx_calloc_buf(r->pool);
 if (b == NULL) {
 return NGX_ERROR;
 }
 
 /* the "0000000000000000" is 64-bit hexadimal string */
//分配大小
 chunk = ngx_palloc(r->pool, sizeof("0000000000000000" CRLF) - 1);
 if (chunk == NULL) {
 return NGX_ERROR;
 }
 b->temporary = 1;
 b->pos = chunk;
//设置chunk第一行的数据,也就是大小+回车换行
 b->last = ngx_sprintf(chunk, "%xO" CRLF, size);

 out.buf = b; 
 } 
//如果是最后一个buf。 
 if (cl->buf->last_buf) { 
 b = ngx_calloc_buf(r->pool); 
 if (b == NULL) { 
 return NGX_ERROR; 
 }
 b->memory = 1; 
 b->last_buf = 1; 
//设置结尾 
 b->pos = (u_char *) CRLF "0" CRLF CRLF; 
 .......................................................... 
 
 } else {
........................................................ 
 b->memory = 1;
//否则则是中间的chunk块,因此结尾为回车换行。
 b->pos = (u_char *) CRLF;
 b->last = b->pos + 2;
 }
 
 tail.buf = b;
 tail.next = NULL;
 *ll = &tail;

然后是gzip filter,它主要是处理gzip的压缩.其中在header filter中,判断accept-encoding头,来看客户端是否支持gzip压缩,然后设置Content-Encoding为gzip,以便与client解析。然后核心的处理都在body filter里面。

先来介绍下filter的主要流程,这里有一个要强调的,那就是nginx里面所有的filter处理基本都是流式的,也就是有多少处理多少。由于是gzip压缩,因此这里会有一个输入,一个输出,因此这里就分为3步,第一步取得输入buf,第二步设置输出buf,第三步结合前两步取得的buf,交给zlib库去压缩,然后输出到前面设置的buf。

 for ( ;; ) { 
 
 /* cycle while we can write to a client */
 
 for ( ;; ) { 
 
 /* cycle while there is data to feed zlib and ... */
//设置正确的输入buf 
 rc = ngx_http_gzip_filter_add_data(r, ctx); 
 
 if (rc == NGX_DECLINED) { 
 break; 
 } 
 
 if (rc == NGX_AGAIN) { 
 continue; 
 } 
//设置输出buf 
 rc = ngx_http_gzip_filter_get_buf(r, ctx); 
 
 if (rc == NGX_DECLINED) { 
 break; 
 } 
 
 if (rc == NGX_ERROR) { 
 goto failed; 
 } 
//开始进行压缩 
 rc = ngx_http_gzip_filter_deflate(r, ctx); 
 
 if (rc == NGX_OK) { 
 break; 
 } 
 
 if (rc == NGX_ERROR) { 
 goto failed; 
 } 
 /* rc == NGX_AGAIN */
 }

这里有一个小细节要注意的,就是在ngx_http_gzip_filter_add_data中,在nginx中会一个chain一个chain进行gzip压缩,压缩完毕后,输入chain也就可以free掉了,可是nginx不是这么做的,他会在当所有的chain都被压缩完毕后再进行free,这是因为gzip压缩对于cpu Cache很敏感,而当你free buf的时候,有可能会导致cache trashing,也就是会将一些cache的数据换出去。

 if (ctx->copy_buf) { 
//这里注释很详细 
 /* 
 * to avoid CPU cache trashing we do not free() just quit buf, 
 * but postpone free()ing after zlib compressing and data output 
 */
 
 ctx->copy_buf->next = ctx->copied; 
 ctx->copied = ctx->copy_buf; 
//简单的赋为NULL,而不是free掉. 
 ctx->copy_buf = NULL; 
 }

最终在ngx_http_gzip_filter_free_copy_buf中free所有的gzip压缩的数据。从这里我们能看到nginx对于细节已经抓到什么地步了.

最后一个是header filter,也就是发送前最后一个head filter,这个filter里面设置对应的头以及status_lines,并且根据对应的status code设置对应的变量。所以这个filter是只有head filter的。这里的处理都没什么难的地方,就是简单的设置对应的头,因此就不详细的分析代码。它的流程大体就是先计算size,然后分配空间,最后copy对应的头。

就看一段代码,关于keepalive的,我们知道http1.1 keepalive是默认开启的,而http1.0它是默认关闭的,而nginx的keepalive_timeout命令则只是用来设置keepalive timeout的.对应clcf->keepalive_header。

//是否打开了keepalive,如果是1.1则默认是开启的 
 if (r->keepalive) { 
//设置Connection的头. 
 len += sizeof("Connection: keep-alive" CRLF) - 1; 
//不同浏览器行为不一样的。 
 /* 
 * MSIE and Opera ignore the "Keep-Alive: timeout=<N>" header. 
 * MSIE keeps the connection alive for about 60-65 seconds. 
 * Opera keeps the connection alive very long. 
 * Mozilla keeps the connection alive for N plus about 1-10 seconds. 
 * Konqueror keeps the connection alive for about N seconds. 
 */
 
 if (clcf->keepalive_header) { 
//设置timeout 
 len += sizeof("Keep-Alive: timeout=") - 1 + NGX_TIME_T_LEN + 2; 
 } 
 
 } else { 
 len += sizeof("Connection: closed" CRLF) - 1; 
 }

转载请注明:爱开源 » Nginx教程:nginx 中处理 http header

您必须 登录 才能发表评论!