Nginx与HTTP协议,content-length

本文深入探讨了HTTP协议中Content-Length字段的作用及其与Chunked传输编码的关系,并解析了Nginx中Chunked模式和Gzip模块的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在项目中遇到一个问题,基于Android的apache httpdclient 无法获取到响应的content-length,通过排查确定是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协议中定义如下:
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函数的流程如下:
//如果没有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返回的数据直接进行输出。
分析该函数的伪代码如下:
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)。

<think>嗯,用户问的是关于HTTP/1.1 400 Bad Request错误,特别是提到“Missing or invalid Sec-WebSocket-Key header”。我需要先理解这个错误的具体含义和常见场景。首先,400错误通常表示客户端发送了一个服务器无法理解的请求,可能因为语法错误或无效信息。而Sec-WebSocket-Key头是WebSocket协议握手过程中必须的一部分,所以这个问题可能出现在尝试建立WebSocket连接的时候。 接下来,我需要分解这个错误的各个部分。HTTP响应头里的Connection: close说明服务器在处理完这个请求后会关闭连接,而不是保持长连接。Content-Type是text/html,意味着返回的内容可能是简单的HTML错误页面,但Content-Length只有43字节,可能是一个简短的错误信息。 用户可能需要知道的是,这个错误发生在客户端尝试升级到WebSocket协议时,但请求头中缺少Sec-WebSocket-Key或者它的值不符合规范。根据RFC 6455,WebSocket握手需要客户端发送带有Sec-WebSocket-Key头的HTTP请求,该头部的值必须是一个随机生成的16字节的Base64编码字符串。如果这个头缺失或无效,服务器会返回400错误,拒绝连接。 接下来,我应该考虑用户可能的背景。他们可能是开发者,正在调试WebSocket连接的问题,或者运维人员检查服务器日志时遇到这个错误。需要解释清楚错误的原因,可能的解决方案,以及如何预防。此外,用户可能希望了解如何生成有效的Sec-WebSocket-Key,或者检查现有的实现是否存在问题。 需要确保回答结构清晰,分步骤说明问题根源、解决方案和预防措施。同时,注意使用正确的中文术语,避免技术术语错误。比如,解释Base64编码、握手过程等,可能需要用简明的例子,比如客户端如何生成这个密钥,服务器如何验证它。还要提醒检查HTTP头是否正确,比如Upgrade和Connection头是否正确设置,因为WebSocket握手需要这些头来升级协议。 另外,可能需要提到常见的工具或库是否自动处理这些头信息,比如使用JavaScript的WebSocket API时,浏览器会自动生成正确的Sec-WebSocket-Key,但如果是自定义客户端,可能需要手动处理。这样用户可以根据自己的开发环境排查问题。 最后,确保回答符合之前的系统级指令,数学公式用$...$,如果有的话,但这个问题可能不涉及数学表达式。不过,要检查是否有需要强调的代码或命令,使用正确的格式。比如,展示一个示例请求头,或者生成Sec-WebSocket-Key的代码片段,可能需要用代码块或引用的方式。</think>### 问题分析:HTTP/400 + `Sec-WebSocket-Key` 错误 当服务器返回以下响应时: ``` HTTP/1.1 400 Bad Request Connection: close Content-Type: text/html Content-Length: 43 Missing or invalid Sec-WebSocket-Key header ``` 表明客户端尝试建立 **WebSocket 连接**时,未按协议规范提供合法的 `Sec-WebSocket-Key` 请求头。 --- ### 问题根源 1. **WebSocket 握手要求** WebSocket 连接需通过 HTTP 协议完成握手,客户端需发送包含以下关键头的请求: ```http GET /ws-endpoint HTTP/1.1 Host: example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== # 必须的随机 Base64 字符串 Sec-WebSocket-Version: 13 ``` - `Sec-WebSocket-Key` 必须为 **16 字节随机值的 Base64 编码**。 - 若缺失此头或值格式错误,服务器会拒绝连接并返回 HTTP/400。 2. **服务器验证逻辑** 服务器会根据 RFC 6455 规范检查 `Sec-WebSocket-Key`: - 是否存在? - 是否是合法的 Base64 字符串? - 解码后是否为 16 字节长度? --- ### 解决方案 1. **客户端修复** - **自动生成密钥**:若使用浏览器 WebSocket API(如 JavaScript 的 `new WebSocket()`),浏览器会自动生成合法的 `Sec-WebSocket-Key`。 - **自定义客户端**:需确保生成 16 字节随机值并 Base64 编码。例如: ```python import base64 import os key = base64.b64encode(os.urandom(16)).decode() headers = {"Sec-WebSocket-Key": key} ``` 2. **检查请求头** 用抓包工具(如 Wireshark 或浏览器开发者工具)确认请求头是否包含: - `Upgrade: websocket` - `Connection: Upgrade` - `Sec-WebSocket-Key: [valid-base64-string]` - `Sec-WebSocket-Version: 13` 3. **服务器配置** 若服务器为 Nginx/Apache,需确保反向代理配置允许 WebSocket 升级: ```nginx # Nginx 示例配置 location /ws/ { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; proxy_set_header Host $host; } ``` --- ### 预防措施 - **使用标准库**:避免手动实现 WebSocket 握手,优先使用成熟库(如 Python 的 `websockets`、Node.js 的 `ws`)。 - **协议兼容性**:确认客户端和服务端均支持 **WebSocket 版本 13**(RFC 6455)。 - **测试工具**:用 `wscat` 或 Postman 的 WebSocket 功能验证服务端是否正常响应。 --- ### 总结 此错误本质是 **WebSocket 握手阶段的协议不匹配**。通过规范生成 `Sec-WebSocket-Key` 并检查握手头字段,可快速修复问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值