http报文是奠定其成为应用层大哥的核心,恕我HTTP直言,在座的应用层协议都是辣鸡~

HTTP的核心

基于前面的基础知识复习,我发现所谓的HTTP协议其实“名不副实”。

因为它压根儿不管”传输“的过程,这部分的工作被TCP/IP老哥任劳任怨的干了。其工作模式很简单,“请求 - 应答”,但其就强在简单,奠定了现代web一哥的地位。

那么它的核心内容,是那些被传输的内容:http报文。

HTTP 协议在规范文档里详细定义了报文的格式,规定了组成部分,解析规则,还有处理策略。

所以可以在 TCP/IP 层之上实现更灵活丰富的功能,例如连接控制,缓存管理、数据编码、内容协商等等。

报文结构

和TCP/UDP一样,都需要在实际传输的数据前加一些“料”(附加头数据),但它是一个“纯文本”的协议,所以头数据都是 ASCII 码的文本,可以很容易地用肉眼阅读,不用借助程序解析也能够看懂。

很常见的一个浏览器优化过的请求头例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
General
Request URL: https://blog.csdn.net/weixin_44163780/article/uvc/90693902
Request Method: GET
Status Code: 200
Remote Address: 127.0.0.1:7890
Referrer Policy: unsafe-url
Response Headers
Access-Control-Allow-Origin: *
cache-control: no-cache
Content-Security-Policy: script-src 'self' 'sha256-nl+LCdUZCsURbeBC1buBlAcvBhCD2/SswpeCH2ZaR34=' 'sha256-nuF8drPPKUM4+b3urtYHupidizytc1BonZF1t/vSzs0'; object-src 'self';
Content-Type: text/css
ETag: "7UTBv1mKEz8B8cXsi14YOLWk94U="
Request Headers
Referer: https://blog.csdn.net/weixin_44163780/article/details/90693902
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36
Body
body>div#blockbyte-bs-indicator>div{opacity:0;pointer-events:none}body>iframe#blockbyte-bs-sidebar.blockbyte-bs-visible,body>iframe#blockbyte-bs-overlay.blockbyte-bs-visible{opacity:1;pointer-events:auto}body.blockbyte-bs-noscroll{overflow:hidden !important}body>div#blockbyte-bs-indicator>div{position:absolute;transform:translate3d(-"%indicatorWidth", 0, 0);top:0;left:0;width:"%indicatorWidth" !important;height:100%;background:"%indicatorColor";border-radius:0 10px 10px 0;transition:opacity 0.3s, transform 0.3s;z-index:2}body>div#blockbyte-bs-indicator>div>span{-webkit-mask:no-repeat center/"%indicatorIconSize";-webkit-mask-image:url(chrome-extension://jdbnofccmhefkmjbkkdkfiicjkgofkdh/img/icon-bookmark.svg);background-color:"%indicatorIconColor";position:absolute;display:block;top:0;left:0;width:100%;height:100%}body>div#blockbyte-bs-indicator[data-pos='right']{left:auto;right:0}body>div#blockbyte-bs-indicator[data-pos='right']>div{transform:translate3d("%indicatorWidth", 0, 0);left:auto;right:0;border-radius:10px 0 0 10px}body>div#blockbyte-bs-indicator.blockbyte-bs-fullHeight>div{border-radius:0}body>div#blockbyte-bs-indicator.blockbyte-bs-hover>div{transform:translate3d(0, 0, 0);opacity:1}body>div#blockbyte-bs-indicator[data-pos='left'].blockbyte-bs-has-lsb{height:100% !important;top:0 !important}body>div#blockbyte-bs-indicator[data-pos='left'].blockbyte-bs-has-lsb>div{background:transparent}body>div#blockbyte-bs-indicator[data-pos='left'].blockbyte-bs-has-lsb>div>span{-webkit-mask-position-y:20px}body>iframe#blockbyte-bs-sidebar{width:"%sidebarWidth";max-width:none;height:0;z-index:2147483646;background-color:"%sidebarMaskColor" !important;border:none;display:block !important;transform:translate3d(-"%sidebarWidth", 0, 0);transition:width 0s 0.3s, height 0s 0.3s, opacity 0.3s, transform 0.3s}body>iframe#blockbyte-bs-sidebar[data-pos='right']{left:auto;right:0;transform:translate3d("%sidebarWidth", 0, 0)}body>iframe#blockbyte-bs-sidebar.blockbyte-bs-visible{width:calc(100% + %sidebarWidth);height:100%;transform:translate3d(0, 0, 0);transition:opacity 0.3s, transform 0.3s}body>iframe#blockbyte-bs-sidebar.blockbyte-bs-hideMask{background:none !important}body>iframe#blockbyte-bs-sidebar.blockbyte-bs-hideMask:not(.blockbyte-bs-hover){width:calc(%sidebarWidth + 50px)}body>iframe#blockbyte-bs-overlay{width:100%;max-width:none;height:100%;z-index:2147483647;border:none;background:"%overlayMaskColor" !important;transition:opacity 0.3s}

HTTP 协议的请求报文和响应报文的结构基本相同,由三大部分组成:

  1. 起始行(start line): 描述请求或相应的基本信息;

  2. 头部字段集合(header):使用 key-value 形式更详细地说明报文;

  3. 消息正文(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# curl -I www.baidu.com

// 输出
HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform
Connection: keep-alive
Content-Length: 277
Content-Type: text/html
Date: Fri, 29 May 2020 04:55:28 GMT
Etag: "575e1f71-115"
Last-Modified: Mon, 13 Jun 2016 02:50:25 GMT
Pragma: no-cache
Server: bfe/1.0.8.18

// 状态行示例
status-line= HTTP-version SP status-code SP reason-phrase CRLF

HTTP/1.1 200 OK
HTTP/1.1 400 Bad Request
................

可见用这种元语言来描述HTTP的消息很方便,虽然有一些规定的理解成本,但有了约束会理解的更加清晰。

常用头字段

HTTP 协议规定了非常多的头部字段,实现各种各样的功能,但基本上可以分为四大类:

  1. 通用字段:在请求头和响应头里都可以出现;
  2. 请求字段:仅能出现在请求头里,进一步说明请求信息或者额外的附加条件;
  3. 响应字段:仅能出现在响应头里,补充说明响应报文的信息;
  4. 实体字段:它实际上属于通用字段,但专门描述 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××:服务器错误,服务器在处理请求时内部发生了错误。

从这五类的具体含义可以看出,状态码并不单单表明这次请求的客户端或者服务端的“反应结果”,而是表明双方进行处理后的结果,可能是客户端的请求报文有错误,也可能是服务端内部处理时发生了错误等等。

常见的状态码:

  1. 101 Switching Protocols: 它的意思是客户端使用 Upgrade 头字段,要求在 HTTP 协议的基础上改成其他的协议继续通信,比如 WebSocket。而如果服务器也同意变更协议,就会发送状态码 101,但这之后的数据传输就不会再使用 HTTP 了
  2. 206 Partial Content: 是 HTTP 分块下载或断点续传的基础,在客户端发送“范围请求”、要求获取资源的部分数据时出现,它与 200 一样,也是服务器成功处理了请求,但 body 里的数据不是资源的全部,而是其中的一部分。状态码 206 通常还会伴随着头字段“Content-Range”,表示响应报文里 body 数据的具体范围,供客户端确认,例如“Content-Range: bytes 0-99/2000”,意思是此次获取的是总计 2000 个字节的前 100 个字节。
  3. 301 Moved Permanently: 俗称“永久重定向”,含义是此次请求的资源已经不存在了,需要改用改用新的 URI 再次访问。与302(Moved Temporarily)临时重定向,一词之差,应用场景却有很大不同。如果你的服务从http升级到https,就需要配置301,永久重定向后的跳转链接;如果今天夜里网站后台要系统维护,服务暂时不可用,这就属于“临时”的,可以配置成 302 跳转,把流量临时切换到一个静态通知页面,浏览器看到这个 302 就知道这只是暂时的情况,不会做缓存优化,第二天还会访问原来的地址。
  4. 304 Not Modified: 是一个比较有意思的状态码,它用于 If-Modified-Since 等条件请求,表示资源未修改,用于缓存控制。它不具有通常的跳转含义,但可以理解成“重定向已到缓存的文件”(即“缓存重定向”)。
  5. 400 Bad Request: 是一个通用的错误码,表示请求报文有错误,但具体是数据格式错误、缺少请求头还是 URI 超长它没有明确说,只是一个笼统的错误,客户端看到 400 只会是“一头雾水”“不知所措”。所以,在开发 Web 应用时应当尽量避免给客户端返回 400,而是要用其他更有明确含义的状态码。
  6. 403 Forbidden: 实际上不是客户端的请求出错,而是表示服务器禁止访问资源。原因可能多种多样,例如信息敏感、法律禁止等,如果服务器友好一点,可以在 body 里详细说明拒绝请求的原因,不过现实中通常都是直接给一个“闭门羹”。
  7. 405 Method Not Allowed:不允许使用某些方法操作资源,例如不允许 POST 只能 GET;
  8. 406 Not Acceptable:资源无法满足客户端请求的条件,例如请求中文但只有英文;
  9. 408 Request Timeout:请求超时,服务器等待了过长的时间;
  10. 409 Conflict:多个请求发生了冲突,可以理解为多线程并发时的竞态;
  11. 413 Request Entity Too Large:请求报文里的 body 太大;
  12. 414 Request-URI Too Long:请求行里的 URI 太大;
  13. 429 Too Many Requests:客户端发送了太多的请求,通常是由于服务器的限连策略;
  14. 431 Request Header Fields Too Large:请求头某个字段或总体太大;
  15. 500 Internal Server Error:与 400 类似,也是一个通用的错误码,服务器究竟发生了什么错误我们是不知道的。不过对于服务器来说这应该算是好事,通常不应该把服务器内部的详细信息,例如出错的函数调用栈告诉外界。虽然不利于调试,但能够防止黑客的窥探或者分析。
  16. 500 Internal Server Error:与 400 类似,也是一个通用的错误码,服务器究竟发生了什么错误我们是不知道的。不过对于服务器来说这应该算是好事,通常不应该把服务器内部的详细信息,例如出错的函数调用栈告诉外界。虽然不利于调试,但能够防止黑客的窥探或者分析。
  17. 502 Bad Gateway:通常是服务器作为网关或者代理时返回的错误码,表示服务器自身工作正常,访问后端服务器时发生了错误,但具体的错误原因也是不知道的
  18. 503 Service Unavailable:表示服务器当前很忙,暂时无法响应服务,我们上网时有时候遇到的“网络服务正忙,请稍后重试”的提示信息就是状态码 503,很可能过几秒钟后服务器就不那么忙了,可以继续提供服务,所以 503 响应报文里通常还会有一个“Retry-After”字段,指示客户端可以在多久以后再次尝试发送请求

内容协商

假如 HTTP 没有告知数据类型的功能,服务器把“一大坨”数据发给了浏览器,浏览器看到的是一个“黑盒子”,面对茫茫多的数据类型,猜不太可能,于是就有了两端的内容协商。

内容的数据类型

HTTP用一部分的“MIME”来标记body中的数据类型,叫“MIME type”。用“Encoding type”来表明数据的压缩类型(编码格式)。还有语言类型,编码类型…

常见的MIME type:

  1. text:即文本格式的可读数据,我们最熟悉的应该就是 text/html 了,表示超文本文档,此外还有纯文本 text/plain、样式表 text/css 等。
  2. image:即图像文件,有 image/gif、image/jpeg、image/png 等。
  3. audio/video:音频和视频数据,例如 audio/mpeg、video/mp4 等。
  4. application:数据格式不固定,可能是文本也可能是二进制,必须由上层应用程序来解释。常见的有 application/json,application/javascript、application/pdf 等,另外,如果实在是不知道数据是什么类型,像刚才说的“黑盒”,就会是 application/octet-stream,即不透明的二进制数据。

常见的Encoding type:

  1. gzip:GNU zip 压缩格式,也是互联网上最流行的压缩格式;
  2. deflate:zlib(deflate)压缩格式,流行程度仅次于 gzip;
  3. br:一种专门为 HTTP 优化的新压缩算法(Brotli)。

常见的Language type: zh-CN, zh, en

常见的Charset type: gbk, utf-8

上述的是“内容协商”的键值对的值,HTTP 协议为此定义了两个 Accept 请求头字段和两个 Content 实体头字段作为“内容协商”的键值对的键。

也就是说,客户端用 Accept 头告诉服务器希望接收什么样的数据,而服务器用 Content 头告诉客户端实际发送了什么样的数据。

例如上文中举例的报文:

1
2
3
4
5
Accept-Ranges: bytes
Cache-Control: private, no-cache, no-store, proxy-revalidate, no-transform
Connection: keep-alive
Content-Length: 277
Content-Type: text/html

可以用“,”做分隔符列出多个类型,例如下面:

1
2
3
4
5
Accept-Encoding: gzip, deflate, br
Content-Encoding: gzip
Accept-Language: zh-CN, zh, en
Accept-Charset: gbk, utf-8
Content-Type: text/html; charset=utf-8

说完了表示数据类型的主体内容,还有一些辅助内容。

内容的选择权重

在 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 : resreq中表示缓存策略

    1. 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-ModifiedIf-Modified-SinceETagIf-None-Match是在每次的res,req中配对使用的。

评论