Nginx教程:nginx 中处理 http header 详解(1)

这里主要的头的处理是放在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;
, 相关的文章: