http报文是奠定其成为应用层大哥的核心,恕我HTTP直言,在座的应用层协议都是辣鸡~
HTTP的核心
基于前面的基础知识复习,我发现所谓的HTTP协议其实“名不副实”。
因为它压根儿不管”传输“的过程,这部分的工作被TCP/IP老哥任劳任怨的干了。其工作模式很简单,“请求 - 应答”,但其就强在简单,奠定了现代web一哥的地位。
那么它的核心内容,是那些被传输的内容:http报文。
HTTP 协议在规范文档里详细定义了报文的格式,规定了组成部分,解析规则,还有处理策略。
所以可以在 TCP/IP 层之上实现更灵活丰富的功能,例如连接控制,缓存管理、数据编码、内容协商等等。
报文结构
和TCP/UDP一样,都需要在实际传输的数据前加一些“料”(附加头数据),但它是一个“纯文本”的协议,所以头数据都是 ASCII 码的文本,可以很容易地用肉眼阅读,不用借助程序解析也能够看懂。
很常见的一个浏览器优化过的请求头例子:
1 | General |
HTTP 协议的请求报文和响应报文的结构基本相同,由三大部分组成:
起始行(start line): 描述请求或相应的基本信息;
头部字段集合(header):使用 key-value 形式更详细地说明报文;
消息正文(body/entity):实际传输的数据,它不一定是纯文本,可以是图片、视频等二进制数据。
这其中前两部分起始行和头部字段经常又合称为“请求头”或“响应头”,消息正文又称为“实体”,但与“header”对应,很多时候就直接称为“body”。
HTTP 协议规定报文必须有 header,但可以没有 body,而且在 header 之后必须要有一个“空行”,也就是“CRLF”,十六进制的“0D0A”。
一个真正的报文 = start line + header + CRLF(空行) + (body/entity)
由于“快递式”的包装,拆包装的工作方式,我们在TCP/IP上传输了太多对于真实数据无用的信息,header里面各种token,cookie验证的一大堆字符串泛滥。TCP/IP老哥,我太难了…
使用ABNF(扩充巴科斯-瑙尔范式)描述HTTP报文
上面提到报文的结构,那么怎么科学严谨的描述它,就要使用ABNF了。
范式两个字是不是回想起大学时间老师教SQL数据库时的范式
巴科斯范式的英文缩写为BNF,它是以美国人巴科斯(Backus)和丹麦人(Naur)的名字命名的一种形式化的语法表示方法,用来描述语法的一种形式体系,是一种典型的元语言。又称巴科斯-诺尔形式(Backus-Naur form)。它不仅能严格的表示语法规则,而且所描述的语法与上下文无关的。它具有语法简单,表示明确,便于语法分析和编译的特点。
ABNF规则
空白字符( SP ):用来分隔定义中的各个元素
- method SP request-target SP HTTP-version CRLF
选择( / ):表示多个规则都是可供选择的规则
- start-line=request-line / status-line
值范围 ( %c##-## ):
- OCTAL=”0” / “1” / “2” / “3” / “4” / “5” / ”6“ / ”7“ 与 OCTAL=%x30-37 等价
序列组合():将规则组合起来,视为单个元素
不定量重复 ( m*n ):
- * 元素表示零个或更多元素:*(header-field CRLF)
- 1* 元素表示一个或更多元素,2*4 元素表示两个至四个元素
可选序列( [] ):
- [ message-body ]
规则 | 形式定义 | 意义 |
---|---|---|
ALPHA | %x41-5A / %x61-7A | 大小写字母(A-z) |
DIGIT | %x30-39 | 数字(0-9) |
HEXDIG | DIGIT/“A”/“B”/“C”/“D”/“E”/“F” | 十六进制数(0-9,A-F,a-f) |
DQUOTE | %x22 | 双引号 |
SP | %x20 | 空格 |
HTAB | %x09 | 横向制表符 |
WSP | SP/HTAB | 空格或横向制表符 |
LWSP | *(WSP/CRLF WSP) | 直线空白 |
VCHAR | %x21-7E | 可见字符 |
CHAR | %x01-7F | 任何7-位US-ASCII字符,不包括NUL |
OCTET | %x00-FF | 8位数据 |
CTL | %x00-1F/%x7F | 空值字符 |
CR | %x0D | 回车 (mac) |
LF | %x0A | 换行 (Linux) |
CRLF | CR LF | 标准换行(windows) |
BIT | “0”/“1” | 二进制数字 |
ABNF表示
HTTP-message=start-line ( *header-filed** CRLF ) CRLF [ message-body ]
start-line= request-line / status-line
request-line= method SP resquest-status SP HTTP-version CRLF
status-line= HTTP-version SP status-code SP reason-phrase CRLF
header-filed= field-name “:” OWS field-value OWS
OWS= *(SP / HTAB)
field-name = token
field-value = *(field-content / obs-fold )
*message-body *= *OCTET
示例:
1 | # curl -I www.baidu.com |
可见用这种元语言来描述HTTP的消息很方便,虽然有一些规定的理解成本,但有了约束会理解的更加清晰。
常用头字段
HTTP 协议规定了非常多的头部字段,实现各种各样的功能,但基本上可以分为四大类:
- 通用字段:在请求头和响应头里都可以出现;
- 请求字段:仅能出现在请求头里,进一步说明请求信息或者额外的附加条件;
- 响应字段:仅能出现在响应头里,补充说明响应报文的信息;
- 实体字段:它实际上属于通用字段,但专门描述 body 的额外信息。
1.Host:
它属于请求字段,只能出现在请求头里,它同时也是唯一一个 HTTP/1.1 规范里要求必须出现的字段,也就是说,如果请求头里没有 Host,那这就是一个错误的报文
2.User-Agent:
是请求字段,只出现在请求头里。它使用一个字符串来描述发起 HTTP 请求的客户端,服务器可以依据它来返回最合适此浏览器显示的页面。
但由于历史的原因,User-Agent 非常混乱,每个浏览器都自称是“Mozilla”“Chrome”“Safari”,企图使用这个字段来互相“伪装”,导致 User-Agent 变得越来越长,最终变得毫无意义。
不过有的比较“诚实”的爬虫会在 User-Agent 里用“spider”标明自己是爬虫,所以可以利用这个字段实现简单的反爬虫策略
3.Date:
字段是一个通用字段,但通常出现在响应头里,表示 HTTP 报文创建的时间,客户端可以使用这个时间再搭配其他字段决定缓存策略
4.Server:
是响应字段,只能出现在响应头里。它告诉客户端当前正在提供 Web 服务的软件名称和版本号。
Server 字段也不是必须要出现的,因为这会把服务器的一部分信息暴露给外界,如果这个版本恰好存在 bug,那么黑客就有可能利用 bug 攻陷服务器。所以,有的网站响应头里要么没有这个字段,要么就给出一个完全无关的描述信息。
状态码
描述 状态行的 范式:status-line= HTTP-version SP status-code SP reason-phrase CRLF的tatus-code是状态行中信息量最多的部分,HTTP-version大多都是HTTP/1.1,reason-phrase则是写很常见的短语:“OK”,“Not Found”,“Bad Request” 等等。
目前 RFC 标准里规定的状态码是三位数,所以取值范围就是从 000 到 999。但如果把代码简单地从 000 开始顺序编下去就显得有点太“low”,不灵活、不利于扩展,所以状态码也被设计成有一定的格式。
RFC 标准把状态码分成了五类,用数字的第一位表示分类,而 099 不用,这样状态码的实际可用范围就大大缩小了,由 000999 变成了 100~599。
总共有 41 个状态码,但状态码的定义是开放的,允许自行扩展。所以 Apache、Nginx 等 Web 服务器都定义了一些专有的状态码。如果你自己开发 Web 应用,也完全可以在不冲突的前提下定义新的代码。
这五类的具体含义是:
- 1××:提示信息,表示目前是协议处理的中间状态,还需要后续的操作;
- 2××:成功,报文已经收到并被正确处理;
- 3××:重定向,资源位置发生变动,需要客户端重新发送请求;
- 4××:客户端错误,请求报文有误,服务器无法处理;
- 5××:服务器错误,服务器在处理请求时内部发生了错误。
从这五类的具体含义可以看出,状态码并不单单表明这次请求的客户端或者服务端的“反应结果”,而是表明双方进行处理后的结果,可能是客户端的请求报文有错误,也可能是服务端内部处理时发生了错误等等。
常见的状态码:
- 101 Switching Protocols: 它的意思是客户端使用 Upgrade 头字段,要求在 HTTP 协议的基础上改成其他的协议继续通信,比如 WebSocket。而如果服务器也同意变更协议,就会发送状态码 101,但这之后的数据传输就不会再使用 HTTP 了
- 206 Partial Content: 是 HTTP 分块下载或断点续传的基础,在客户端发送“范围请求”、要求获取资源的部分数据时出现,它与 200 一样,也是服务器成功处理了请求,但 body 里的数据不是资源的全部,而是其中的一部分。状态码 206 通常还会伴随着头字段“Content-Range”,表示响应报文里 body 数据的具体范围,供客户端确认,例如“Content-Range: bytes 0-99/2000”,意思是此次获取的是总计 2000 个字节的前 100 个字节。
- 301 Moved Permanently: 俗称“永久重定向”,含义是此次请求的资源已经不存在了,需要改用改用新的 URI 再次访问。与302(Moved Temporarily)临时重定向,一词之差,应用场景却有很大不同。如果你的服务从http升级到https,就需要配置301,永久重定向后的跳转链接;如果今天夜里网站后台要系统维护,服务暂时不可用,这就属于“临时”的,可以配置成 302 跳转,把流量临时切换到一个静态通知页面,浏览器看到这个 302 就知道这只是暂时的情况,不会做缓存优化,第二天还会访问原来的地址。
- 304 Not Modified: 是一个比较有意思的状态码,它用于 If-Modified-Since 等条件请求,表示资源未修改,用于缓存控制。它不具有通常的跳转含义,但可以理解成“重定向已到缓存的文件”(即“缓存重定向”)。
- 400 Bad Request: 是一个通用的错误码,表示请求报文有错误,但具体是数据格式错误、缺少请求头还是 URI 超长它没有明确说,只是一个笼统的错误,客户端看到 400 只会是“一头雾水”“不知所措”。所以,在开发 Web 应用时应当尽量避免给客户端返回 400,而是要用其他更有明确含义的状态码。
- 403 Forbidden: 实际上不是客户端的请求出错,而是表示服务器禁止访问资源。原因可能多种多样,例如信息敏感、法律禁止等,如果服务器友好一点,可以在 body 里详细说明拒绝请求的原因,不过现实中通常都是直接给一个“闭门羹”。
- 405 Method Not Allowed:不允许使用某些方法操作资源,例如不允许 POST 只能 GET;
- 406 Not Acceptable:资源无法满足客户端请求的条件,例如请求中文但只有英文;
- 408 Request Timeout:请求超时,服务器等待了过长的时间;
- 409 Conflict:多个请求发生了冲突,可以理解为多线程并发时的竞态;
- 413 Request Entity Too Large:请求报文里的 body 太大;
- 414 Request-URI Too Long:请求行里的 URI 太大;
- 429 Too Many Requests:客户端发送了太多的请求,通常是由于服务器的限连策略;
- 431 Request Header Fields Too Large:请求头某个字段或总体太大;
- 500 Internal Server Error:与 400 类似,也是一个通用的错误码,服务器究竟发生了什么错误我们是不知道的。不过对于服务器来说这应该算是好事,通常不应该把服务器内部的详细信息,例如出错的函数调用栈告诉外界。虽然不利于调试,但能够防止黑客的窥探或者分析。
- 500 Internal Server Error:与 400 类似,也是一个通用的错误码,服务器究竟发生了什么错误我们是不知道的。不过对于服务器来说这应该算是好事,通常不应该把服务器内部的详细信息,例如出错的函数调用栈告诉外界。虽然不利于调试,但能够防止黑客的窥探或者分析。
- 502 Bad Gateway:通常是服务器作为网关或者代理时返回的错误码,表示服务器自身工作正常,访问后端服务器时发生了错误,但具体的错误原因也是不知道的
- 503 Service Unavailable:表示服务器当前很忙,暂时无法响应服务,我们上网时有时候遇到的“网络服务正忙,请稍后重试”的提示信息就是状态码 503,很可能过几秒钟后服务器就不那么忙了,可以继续提供服务,所以 503 响应报文里通常还会有一个“Retry-After”字段,指示客户端可以在多久以后再次尝试发送请求
内容协商
假如 HTTP 没有告知数据类型的功能,服务器把“一大坨”数据发给了浏览器,浏览器看到的是一个“黑盒子”,面对茫茫多的数据类型,猜不太可能,于是就有了两端的内容协商。
内容的数据类型
HTTP用一部分的“MIME”来标记body中的数据类型,叫“MIME type”。用“Encoding type”来表明数据的压缩类型(编码格式)。还有语言类型,编码类型…
常见的MIME type:
- text:即文本格式的可读数据,我们最熟悉的应该就是 text/html 了,表示超文本文档,此外还有纯文本 text/plain、样式表 text/css 等。
- image:即图像文件,有 image/gif、image/jpeg、image/png 等。
- audio/video:音频和视频数据,例如 audio/mpeg、video/mp4 等。
- application:数据格式不固定,可能是文本也可能是二进制,必须由上层应用程序来解释。常见的有 application/json,application/javascript、application/pdf 等,另外,如果实在是不知道数据是什么类型,像刚才说的“黑盒”,就会是 application/octet-stream,即不透明的二进制数据。
常见的Encoding type:
- gzip:GNU zip 压缩格式,也是互联网上最流行的压缩格式;
- deflate:zlib(deflate)压缩格式,流行程度仅次于 gzip;
- br:一种专门为 HTTP 优化的新压缩算法(Brotli)。
常见的Language type: zh-CN, zh, en
常见的Charset type: gbk, utf-8
上述的是“内容协商”的键值对的值,HTTP 协议为此定义了两个 Accept 请求头字段和两个 Content 实体头字段作为“内容协商”的键值对的键。
也就是说,客户端用 Accept 头告诉服务器希望接收什么样的数据,而服务器用 Content 头告诉客户端实际发送了什么样的数据。
例如上文中举例的报文:
1 | Accept-Ranges: bytes |
可以用“,”做分隔符列出多个类型,例如下面:
1 | Accept-Encoding: gzip, deflate, br |
说完了表示数据类型的主体内容,还有一些辅助内容。
内容的选择权重
在 HTTP 协议里用 Accept、Accept-Encoding、Accept-Language 等请求头字段进行内容协商的时候,还可以用一种特殊的“q”参数表示权重来设定优先级,这里的“q”是“quality factor”的意思。
权重的最大值是 1,最小值是 0.01,默认值是 1,如果值是 0 就表示拒绝。具体的形式是在数据类型或语言代码后面加一个“;”,然后是“q=value”。
这里要提醒的是“;”的用法,在大多数编程语言里“;”的断句语气要强于“,”,而在 HTTP 的内容协商里却恰好反了过来,“;”的意义是小于“,”的。
例如下面的 Accept 字段:
1 | Accept: text/html,application/xml;q=0.9,*/*;q=0.8 |
它表示浏览器最希望使用的是 HTML 文件,权重是 1,其次是 XML 文件,权重是 0.9,最后是任意数据类型,权重是 0.8。服务器收到请求头后,就会计算权重,再根据自己的实际情况优先输出 HTML 或者 XML。
内容协商的结果
内容协商的过程是不透明的,每个 Web 服务器使用的算法都不一样。但有的时候,服务器会在响应头里多加一个 Vary 字段,记录服务器在内容协商时参考的请求头字段,给出一点信息,例如:
1 | Vary: Accept-Encoding,User-Agent,Accept |
这个 Vary 字段表示服务器依据了 Accept-Encoding、User-Agent 和 Accept 这三个头字段,然后决定了发回的响应报文。
Vary 字段可以认为是响应报文的一个特殊的“版本标记”。每当 Accept 等请求头变化时,Vary 也会随着响应报文一起变化。也就是说,同一个 URI 可能会有多个不同的“版本”,主要用在传输链路中间的代理服务器实现缓存服务。
Http缓存常用字段
约定req
为请求头,res
响应头,C
客户端,S
服务端
Expires :
res
中为资源过期时间Last-Modified:
res
中为资源最近修改时间ETag:
res
中资源的唯一标识符(hash算法生成)If-Modified-Since :
req
中的资源最近修改时间If-None-Match :
req
中的资源标识Cache-Control :
res
,req
中表示缓存策略- req中常用指令
字段名称 | 说明 |
---|---|
max-age= |
设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。与Expires 相反,时间是相对于请求的时间 |
max-stale[= |
C 可接收一个已经过期的资源。设置一个可选的秒数,不接受超过给定时间的资源 |
min-fresh= |
C 希望获取一个能在指定的秒数内保持其最新状态的res |
no-cache | 在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证 |
no-store | 不缓存有关客户端请求或服务器响应的任何内容 |
2. res中常用指令(req中重复的不列举,详见MDN)
字段名称 | 说明 |
---|---|
public | 表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存,即使是通常不可缓存的内容 |
private | 表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)。私有缓存可以缓存响应内容 |
must-revalidate | 一旦资源过期(比如已经超过max-age ),在成功向原始服务器验证之前,缓存不能用该资源响应后续请求 |
proxy-revalidate | 与must-revalidate 作用相同,但它仅适用于共享缓存(例如代理),并被私有缓存忽略 |
s-maxage= |
覆盖max-age 或者Expires 头,但是仅适用于共享缓存(比如各个代理),私有缓存会忽略它 |
其中Last-Modified
与If-Modified-Since
,ETag
与If-None-Match
是在每次的res,req中配对使用的。