HTTP之Content-Length



HTTP之Content-Length             



    10 一 12 杂谈Nginx与HTTP协议

    在项目中遇到一个问题,需要详细了解下HTTP协议及其Nginx中对HTTP协议的支持程度。今天一天收集了一些资料,也梳理出最终方案。记录到博客上,方便后续查阅。重点关注以下几个方面:1、Http交互中如何判定内容的长度及其HTTP协议中关于Content-Length的解读。2、Chunk和Gzip在Nginx中的实现及原理。3、Upstream如何和Chunked结合。

    Http协议中关于Content-Length的解读

    在HTTP协议中,有Content-Length的详细解读。Content-Length用于描述HTTP消息实体的传输长度the transfer-length of the message-body。在HTTP协议中,消息实体长度和消息实体的传输长度是有区别,比如说gzip压缩下,消息实体长度是压缩前的长度,消息实体的传输长度是gzip压缩后的长度。

    在具体的HTTP交互中,客户端是如何获取消息长度的呢,主要基于以下几个规则:

    • 响应为1xx,204,304相应或者head请求,则直接忽视掉消息实体内容。
    • 如果有Transfer-Encoding,则优先采用Transfer-Encoding里面的方法来找到对应的长度。比如说Chunked模式。
    • “如果head中有Content-Length,那么这个Content-Length既表示实体长度,又表示传输长度。如果实体长度和传输长度不相等(比如说设置了Transfer-Encoding),那么则不能设置Content-Length。如果设置了Transfer-Encoding,那么Content-Length将被忽视”。这句话翻译的优点饶,其实关键就一点:有了Transfer-Encoding,则不能有Content-Length。
    • Range传输。不关注,没详细看了:)
    • 通过服务器关闭连接能确定消息的传输长度。(请求端不能通过关闭连接来指明请求消息体的结束,因为这样可以让服务器没有机会继续给予响应)。这种情况主要对应为短连接,即非keep-alive模式。
    • HTTP1.1必须支持chunk模式。因为当不确定消息长度的时候,可以通过chunk机制来处理这种情况。
    • 在包含消息内容的header中,如果有content-length字段,那么该字段对应的值必须完全和消息主题里面的长度匹配。
      “The entity-length of a message is the length of the message-body before any transfer-codings have been applied”
      也就是有chunk就不能有content-length 。

    其实后面几条几乎可以忽视,简单总结后如下:

    1、Content-Length如果存在并且有效的话,则必须和消息内容的传输长度完全一致。(经过测试,如果过短则会截断,过长则会导致超时。)

    2、如果存在Transfer-Encoding(重点是chunked),则在header中不能有Content-Length,有也会被忽视。

    3、如果采用短连接,则直接可以通过服务器关闭连接来确定消息的传输长度。(这个很容易懂)

    结合HTTP协议其他的特点,比如说Http1.1之前的不支持keep alive。那么可以得出以下结论:

    1、在Http 1.0及之前版本中,content-length字段可有可无。

    2、在http1.1及之后版本。如果是keep alive,则content-length和chunk必然是二选一。若是非keep alive,则和http1.0一样。content-length可有可无。

    Nginx在Http协议方面的处理

    第一、Nginx的chunk模块

    Nginx的Chunk模块是一个典型的Filter模块,它本身是内置必选的Nginx模块。在0.7.66版本之后,有一个配置项chunked_transfer_encoding可以开启或者关闭chunk模式,默认是开启的。

    首先,先简单了解下在HTTP协议中Chunked相关的知识点。Chunked一种transfer coding方式,在HTTP1.0之前(包含http1.0)的版本是不支持的。在HTTP协议中定义如下:


    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    Chunked-Body  
    = *chunk

                      last-chunk

                      trailer

                      CRLF

     

    chunk         
    = chunk-size [ chunk-extension ] CRLF

                      chunk-data
    CRLF

    chunk-size    
    = 1*HEX

    last-chunk    
    = 1*(
    "0" )
    [ chunk-extension ] CRLF

     

    chunk-extension=
    *(
    ";"

    chunk-ext-name [
    "="

    chunk-ext-val ] )

    chunk-ext-name
    = token

    chunk-ext-val 
    = token | quoted-string

    chunk-data    
    = chunk-size(OCTET)

    trailer       
    = *(entity-header CRLF)

    其中可以看到,每一个chunk都是可以大小自描述的。

    在Nginx的Chunked模块中,header filter函数的流程如下:


    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    //如果没有content或者是head请求,则直接跳过。

    if

    (r->headers_out.content_length_n == -1) {

            if

    (http版本 < http 1.1) {

                 r->keepalive
    = 0;
    //关闭keep
    alive

            }
    else

    {

                 if

    (开启chunk) {

                      r->chunked
    = 1;

                 }
    else

    {

                      r->keepalive
    = 0;
    //也就是,如果关闭了chunk,则也关闭了keep
    alive。

                 }

            }

       }

    对应的body filter的流程,则更加简单,直接对当前输出的buf chain进行一个chunk封装。

    第二、Nginx中Gzip模块和r->headers_out.content_length_n

    r->headers_out.content_length_n :这个在Nginx内部用于表述请求返回内容的长度。但注意这不是完全相等的,只有在 r->headers_out.content_length_n >=0的时候,才有意义。比如说,通常后端的upstream(比如说PHP),如果没有在脚本中强制header输出content-length,则默认在nginx中 r->headers_out.content_length_n = -1。

    Gzip模块也是一个典型的Filter模块。这里简单介绍下,后续可以详细描述。在header filter中会直接清空 r->headers_out.content_length_n和header中输出的content_length。为什么要清空呢?主要是因为gzip要对内容模块进行压缩处理,而在header filter的时候,gzip模块不可能计算出压缩后的内容长度(原因是在nginx中,header 输出和body的输出是完全两个不同的阶段),所以最好的办法就是在清空header中的content-length。这样结合之前的介绍的chunked模块,可以看出:在nginx中,如果采用gzip,如果是keep alive,则必然是chunked模式。

    Upsteam如何和chunked结合

    在前面介绍的Nginx Chunked模式实现中提到,Chunked模块的body filter是对当前处理的buf chain进行封装成一个Chunk。那么在Nginx中是如何实现多个Chunk的呢?这里要从 ngx_event_pipe_write_to_downstream这个函数说起,该函数是upstream处理中非常关键的一个函数,用于把upstream返回的数据直接进行输出。

    分析该函数的伪代码如下:


    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    25

    for ()

    {

          if

    (p->downstream_error) {

                 return

    ngx_event_pipe_drain_chains(p);
    //出错了

          }

          if

    (p->upstream_eof || p->upstream_error || p->upstream_done) {

               //是最后一个节点,或者已经读取upstream数据完毕

               //分类比的flush输出,这里面一般都含有bodyfilter比如

               rc
    = p->output_filter(p->output_ctx, p->in);

               //并且结束循环

               p->downstream_done
    = 1;

               break ;

          }

          /*计算当前BUF的size*/

          if

    (bsize >= (
    size_t )
    p->busy_size) {

               /*超过大小则刷数据*/

               go
    flush;

          }

          for

    ( ;; ) {

               //循环拼装输出buf,如果超过busy_size同样进行flush

          }

    flush:

          //flush输出

          rc
    = p->output_filter(p->output_ctx, out);

    }

    可以看到,有一个关键点,就是busy_size,它决定了一个chunk的大小。这个对应一个配置,追查后如下:在不同的upstream中有不同的定义,比如说proxy模式下,有proxy_busy_buffers_size,该值默认是proxy buffer size * 2 。(proxy buffer size根据操作系统,可以是4K或者8k)。fastcgi模式下,buffer_size默认是ngx_pagesize。配置项是fastcgi_busy_buffers_size。默认情况下是fastcgi的buffer size的2倍。(buffer size个是可配置的,buffer 个个数也是可以配置的,个数不能小于2)。

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值