HTTP
HTTP1
HTTP 基本概念
HTTP 是超文本传输协议(HyperText Transfer Protocol),可以分成 3 个部分:
- 超文本
- 传输
- 协议
超文本指不止是文本,还有图片、音视频、文档等,传输是指在两个通信端点间传输。
HTTP 的优点:简单、灵活和易于扩展、应用广泛和跨平台。
简单:由 请求行 + 请求头部 + 请求内容
组成,请求行是文本,头部是 key-value 结构的文本形式,请求内容可多样。
灵活和易于扩展:各类请求方法、 URI/URL 、状态码、头部字段等没有被固定死,允许开发人员自定义和扩充。HTTP 是应用层协议,下层协议可变化。
应用广泛和跨平台:各类设备的浏览器都支持。
HTTP 的缺点:无状态、明文传输不安全。 无状态:服务器不会记录 HTTP 的状态,不需要额外的资源来记录状态信息,能减轻服务器负担,节约 CPU 和内存。但对于关联性操作不友好,所以出现了 Cookie 等用于身份认证的技术。 明文传输不安全:无法防御窃听、假冒、篡改攻击,无保证信息的机密性和完整性。
状态码
状态码 | 含义 | 示例 |
---|---|---|
1xx | 提示信息,表示协议中间状态,还需后续处理 | 101 |
2xx | 成功,报文已经收到并成功处理 | 200, 204, 206 |
3xx | 重定向,资源位置发生变动,需要客户端重新发送请求 | 301, 302, 304 |
4xx | 客户端错误,请求保温有误,服务器无法处理 | 400, 403, 404 |
5xx | 服务器错误,服务器在处理请求时内部发生了错误 | 500, 502, 503, 504 |
常见字段
HTTP 的 header 是 key-value 结构,key 在规范中不允许使用下划线 _
连接两个单词,需使用 -
,不允许使用 @
。
Host: 客户端指定服务器的域名
Content-Length: 服务器表明响应数据的长度
Connection: 客户端要求服务器使用 TCP 持久连接,以便其他请求复用这个 TCP 连接
Content-Type: 服务器表明响应数据的格式,如 text/html, application/json
Accept: 客户端表明自己能接受哪些数据格式,常见 */*
Content-Encoding: 服务器表明响应数据的压缩方法
Accpet-Encoding: 客户端表明自己能接受的压缩方法
请求方法
GET: 从服务器获取指定 URL 的资源,如文本、页面、图片等 POST: 向服务器的指定 URL 提交数据,数据存在于 body 中
GET 是安全且幂等的,GET 是只读的操作,无论操作多少次,结果都是一样的 POST 是不安全且不幂等的,每次操作会修改服务器上的资源,多次操作会累积
HTTP/1.1
长连接 keepalive :即持久连接,减少了 TCP 连接的重复建立和断开而额外造成的开销,减轻服务器负载。 管道网络传输 pipe :客户端可以发起多个请求,不必等待响应,继而发送第二个请求,减少整体的响应时间。
问题: 对头阻塞:服务器还是按照请求顺序进行响应,若前面的请求因某种原因而被阻塞时,后面排队的请求也被阻塞了,导致客户端一直获取不到响应。 冗余:请求/响应 header 没有压缩,只能压缩 body 部分,每次都要重复发送 header 造成了冗余。 其他:没有请求优先级的控制,请求只能从客户端发起,服务器只能被动响应。
局限性: Web 网页越来越大,平均网页长度不断增大。单位动不动就是 MB 。 资源越来越多,很多网页都有 100 个以上的资源。 内容样式多样,图片流量占比越来越大。 实时性要求高,要求更快的速度。
但是延迟难以下降,触底延迟的下限。并发连接有限,浏览器限制了并连接数,每一个连接都要 TCP 和 TLS 握手,会受到 TCP 慢启动过程给流量带来的影响。还有前文存在的问题。
优化: 将小图合成大图传输,在客户端使用 JavaScript 切割,图片 Base64 编码传输;打包 JS 和 CSS 静态文件;将页面资源分散到不同的域名,提升并发连接数上限。
keepalive
长连接,前提条件是我们先得确定请求头与响应体的长度。
HTTP1.0 默认 connection 为 close ; HTTP1.1 默认 connection 为 keep-alive 。
对于请求来说,如果当前请求需要有 body ,如 POST 请求,那就需要客户端在请求头中指定 content-length 来表明 body 的大小,否则返回 400 错误。也就是说,请求体的长度是确定的。
响应体的长度:
- 对于 HTTP1.0 协议来说,如果响应头中 有content-length 头,则以 content-length 的长度就可以知道 body 的长度了,客户端在接收 body 时,就可以依照这个长度来接收数据,接收完后,就表示这个请求完成了。而如果没有 content-length 头,则客户端会一直接收数据,直到服务端主动断开连接,才表示 body 接收完了。
- 对于 HTTP1.1 协议来说,如果响应头中的 Transfer-encoding 为 chunked 传输,则表示 body 是流式输出, body 会被分成多个块,每块的开始会标识出当前块的长度,此时, body 不需要通过长度来指定。如果是非 chunked 传输,而且有 content-length ,则按照 content-length 来接收数据。否则,如果是非 chunked ,并且没有 content-length ,则客户端接收数据,直到服务端主动断开连接。
pipe
pipeline 其实就是流水线作业,它可以看作为 keepalive 的一种升华,因为 pipeline 也基于长连接的,目的就是利用一个连接做多次请求。如果客户端要提交多个请求,对于 keepalive 来说,那么第二个请求,必须要等到第一个请求的响应接收完全后,才能发起,这和 TCP 的停止等待协议是一样的,得到两个响应的时间至少为 2RTT。而对 pipeline 来说,客户端不必等到第一个请求处理完后,就可以马上发起第二个请求。得到两个响应的时间可能能够达到 1RTT。
HTTP/2
HTTP HPack Stream TLS 1.2+ TCP IP
头部压缩:同时发送多个请求,header 是一样或相似的,使用 HPACK 算法在服务器和客户端同时维护一张 header 信息表,字段存入此表,生成索引号,遇到重复字段,只发送索引号。 二进制格式:全面采用二进制格式,header 和 body 统称为帧 (frame):头信息帧和数据帧。这样对于计算机来说,直接解析二进制报文就可以得到信息,提高数据传输效率。 数据流:每个请求或响应的所有数据包,称为一个数据流(stream),数据流有一个独一无二的编号,客户端发出的数据流编号为奇数,服务器发出的数据流编号为偶数。客户端可以指定数据流的优先级,让服务器优先响应。 多路复用:在一个连接中并发多个请求或响应,而不用按顺序一一对应,而不是 HTTP/1.1 那样的串行。 服务器推送:服务器可以提前把要用到的资源文件主动发送给客户端(Server Push/Cache Push)。
问题: 多个 HTTP 请求复用一个 TCP 连接,下层的 TCP 协议不知道有多少个 HTTP 请求,导致一旦传输层发生了丢包,就会出发 TCP 的重传机制,这样在一个 TCP 连接中的所有 HTTP 请求都必须等待这个丢了的包被重传回来。
只改变了应用层,兼容 HTTP/1.1 ,没有改变协议语义,改变了协议语法,相比 HTTP/1.1 改动点: 头部压缩,避免重复信息的多次传输,将 ASCII 编码改成二进制编码,提高效率。
HPack 算法
静态字典;动态字典;Huffman 编码进行压缩。 客户端和服务器都会建立和维护字典,用长度较小的索引号代替 header ,再使用 Huffman 编码压缩数据。 静态字典:HTTP/2 规则有 61 组高频出现的字符串和字段。
Index | Header Name | Header Value |
---|---|---|
1 | :authority | |
2 | :method | GET |
3 | :method | POST |
4 | :path | / |
5 | :path | /index.html |
6 | :scheme | http |
7 | :scheme | https |
8 | :status | 200 |
… | … | … |
54 | server | |
55 | set-cookie | |
56 | strict-transport-security | |
57 | transfer-encoding | |
58 | user-agent | |
59 | vary | |
60 | via | |
61 | www-authenticate |
非固定的 Header Value 是变化的,会经过 Huffman 编码后发送出去。 根据 RFC7541 规范,如果头部字段属于静态表范围,并且 Value 是变化的,那么他的 HTTP/2 头部的前 2 位固定为 01 ,头部格式如图:
0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | 0 | 1 | Index (6+) | +---+---+-----------------------+ | H | Value Length (7+) | +---+---------------------------+ | Value String (Length octets) | +-------------------------------+
HTTP/2 头部因为是二进制编码,不需要冒号空格和末尾的 \r\n
作为分隔符,改用字符串长度来分割 Index 和 Value 。H 标记是否启用 Huffman 编码。HTTP/2 还定义了一张静态 Huffman 表。
动态表编码:Index 从 62 开始,在编码解码时更新。例如若客户端发送了一个 custom-header
字段,双方更新自己的动态表,添加一个 Index 号 62 ,下一次根据规范发送 62 就行了。但动态表生效需要是 同一个连接,重复传输完全相同的 HTTP 头部 。如果字段在一个连接上只发送了一次,或者重复传输时,字段变化了,动态表就无法被充分利用。
服务器一般有 http2_max_requests
限制一个连接能传输的请求数量,避免动态表无线增大。
二进制帧
+-----------------------------------------------+ | Length (24) | +---------------+---------------+---------------+ | Type (8) | Flags (8) | +-+-------------+---------------+-------------------------------+ |R| Stream Identifier (31) | +=+=============================================================+ | Frame Payload (0...) ... +---------------------------------------------------------------+
Frame Type: 数据帧(前 3 种)和控制帧:
Type | Encode | Function |
---|---|---|
DATA | 0x0 | Transfer body |
HEADERS | 0x1 | Transfer header |
PRIORITY | 0x2 | Designate priority |
RST_STREAM | 0x3 | Reset stream |
SETTINGS | 0x4 | Modify config of connection or stream |
PUSH_PROMISE | 0x5 | Describe frame pushed by server |
PING | 0x6 | Heart beat |
GOAWAY | 0x7 | Close gracefully or send error |
WINDOW_UPDATE | 0x8 | Traffic control |
CONTINUATION | 0x9 | Long frame |
Flags 可以携带控制信息如 END_HEADERS, END_STREAM, PRIORITY
;R 为保留位;Stream Identifier 帧标识用于标识该 Frame 属于哪个 Stream 。Payload 存放的是经过 HPack 算法压缩过的 HTTP 头部和包体。
并发传输
1 个 TCP 连接包含 1 个或多个 Stream , Stream 是 HTTP/2 并发的关键技术; Stream 里可以包含 1 个或多个 Message , Message 对应 HTTP/1 中的请求或响应,由 header 和 body 组成; Message 里包含 1 个或多个 Frame , Frame 是 HTTP/2 的最小单位,以二进制压缩格式存放 HTTP/1 中的请求或响应。
HTTP 消息可以由多个 Frame 构成,1 个 Frame 可以由多个 TCP 报文构成。
服务器主动推送
Nginx 主动推送设置:
location /test.html { http2_push /test.css; }
服务器主动的推送,用的是偶数号 Stream ,通过 PUSH_PROMISE 帧传输 HTTP 头部,通过帧的 Promised Stream ID 字段告知客户端,接下来会在哪个偶数号 Stream 中发送包体。
但因为下层 TCP 协议的特性 若 TCP 传输发生丢包,则会造成 队头阻塞 。
HTTP/3
HTTP QPack Stream QUIC TLS 1.3 UDP IP
传输层使用 UDP 协议,当某个流发生丢包时,只会阻塞这一个流,不会影响其他流。使用 TLS 1.3 减少握手的 RTT 消耗,头部压缩算法使用 QPack 。
所解决的 HTTP/2 缺陷:队头阻塞、 TCP 与 TLS 的握手延迟、网络迁移需要重新连接。
UDP 协议的特点:简单、不可靠。无需握手和回收,包之间没有依赖性,比 TCP 快。
HTTP/3 也是使用二进制帧,但不需要 Stream 了, Stream 由 QUIC 实现,结构如下:
+---------------+ | Type (8) | +---------------+ | Length (8) | +---------------+ | Frame Payload | +---------------+
帧也是分为数据帧和控制帧。
QUIC(Quick UDP Internet Connection)协议
无队头阻塞:每个数据包都有一个序号唯一标识,当某个 Stream 流中的一个数据包丢失了,即使该流的其他数据包到达了,数据也无法被 HTTP/3 读取,直到 QUIC 重传丢失的报文。但 这个流不会影响到其他流 。多个 Stream 间是独立的,不像 HTTP/2 那样多个流复用一个 TCP 连接。 更快的连接建立:对于 HTTP/1 和 HTTP/2 , TCP 和 TLS 是分层的,分别属于内核实现的传输层和 OpenSSL 实现的表示/会话层,难以合并在一起,需要分批握手。 HTTP/3 在传输数据前需要 QUIC 握手,需要 1 RTT ,目的是确认双方的 连接ID ,连接 ID 可以用于连接迁移。 QUIC 包含了 TLS 1.3 ,仅需 1 RTT 就可以完成建立连接与密钥协商,在第二次连接的时候,应用数据包可以和 QUIC 握手信息(连接信息 + TLS 信息)一起发送,达到 0-RTT 的效果。 连接迁移:不使用 TCP 的四元组来“绑定”链接,通过 连接ID 来标记通信的两个端点,客户端和服务器可以各自选择一组 ID 来标记自己,当网络发生变化导致 IP 地址改变,只要仍保有上下文信息(连接 ID 、 TLS 密钥),就可以“无缝”地复用原连接,消除重连的成本。
QPack
静态表扩大到 91 项。
HPack的动态表在首次请求-响应后,双方会将未包含在静态表中的 header 项更新到各自的动态表中,接着后续传输时仅用 1 个数字 Index 表示,然后对方可以根据 Index 查到对应的数据,提高传输效率。但在丢包的情况下,会导致双方动态表不同步,造成阻塞。 而 QPack 有两个特殊的 单向 流,用于同步双方的动态表:
- QPack Encoder Stream: 用于将一个字典(key-value)传递给对方,客户端可以通过这个 Stream 发送不属于静态表的 HTTP header
- QPack Decoder Stream: 用于响应对方,通知对方其刚发的字典已经更新
HTTPS = HTTP + SSL/TLS
HTTPS 需要向 CA 申请证书,确保服务器的身份是可信的。 HTTPS 实现的功能有:信息加密、校验机制、身份证书,同时使用非对称加密生成会话密钥,使用对称加密通信保证机密性,使用摘要算法实现完整性,使用数字证书防止假冒。
如何提高 HTTPS 的性能?1️⃣ TLS 握手阶段;2️⃣ 对称加密报文传输阶段。
- 软件升级:升级 Linux 系统内核版本,升级 OpenSSL 。 协议优化:使用 TLS 1.3(合并了 HELLO 和公钥交换消息) 及 ECDHE 密钥交换算法且选择 x25519 曲线。 证书优化:使用 ECDSA 证书,相比 RSA 证书,在同样的安全强度下,ECC 的密钥长度短的多。避免使用 CRT(Certificate Revocation List)验证证书,而使用 OCSP(Online Certificate Status Protocol)向 CA(Certificate Authority)发送证书查询请求以得到证书的有效状态或使用 OCSP Stapling 让服务器缓存一个从 CA 得到的时间戳和签名的响应以证明证书的有效性。 会话复用:(TLS session resumption)Session ID / Session Ticket / Pre-shared Key。
- 使用支持 AES-NI 的 CPU ,对于不支持 AES-NI 的设备,可选择使用 CHACHA20 ,可以适当选择 AES128。
Web
请求行包含请求的方法,URI,协议版本,也可以包含有 host 的。 比如一个请求:
GET http://example.com/uri HTTP/1.0
这样一个请求行也是合法的。
Web 访问过程
Crawler / Netspider2
爬虫、反爬虫、反反爬虫。
URI/URL
URI 包括 URL 和 URN 两个类别,URL 是 URI 的子集,所以 URL 一定是 URI ,而 URI 不一定是 URL 。
URI = Universal Resource Identifier 统一资源标志符,用来标识抽象或物理资源的一个紧凑字符串。
URL = Universal Resource Locator 统一资源定位符,一种定位资源的主要访问机制的字符串,一个标准的URL必须包括:protocol、host、port、path、parameter、anchor。
URN = Universal Resource Name 统一资源名称,通过特定命名空间中的唯一名称或ID来标识资源。
Content-Type
常见 content-type :
application/x-www-form-urlencoded
key1=value1&key2=value2
- multipart/form-data
- application/json
Cache-Control
- max-age 是指定强缓存的时间
- no-cache 是会用本地的缓存但每次都会协商
- no-store 是禁用掉缓存
一般是入口 html 文件设置 no-cache ,资源文件如 js, css 都带有 hash 值,更新 html 后请求的资源文件名不一样,也就相当于刷新了缓存。
Footnotes:
图解网络-小林coding-v3.0.pdf