文章

HTTP

HTTP

HTTP 协议发展史

48twx

HTTP1.x

HTTP 请求报文结构

http1 中奠定了 http 协议的基本语义:由请求行/状态行、body 和 header 构成

HTTP 请求协议
ecg6l

HTTP 响应协议
8v8iv

HTTP1.1 管道化(没有解决 HTTP1.x 的队头阻塞)

什么是 HTTP1.1 管道化
HTTP1.1 允许在持久连接上可选的使用请求管道,这是相对于 keep-alive 连接的又一次性能优化。在相应请求到达之前,可以将多条请求放入队列,当第一条请求发往服务器的时候,第二第三条请求也可以开始发送了,在高延时的网络条件下,这样可以降低网络的 RTT,提高性能。
a6tks

  • 非管道化:一个请求一个响应,请求串行
  • 管道化:请求可以并发发出,但是响应必须串行返回,后一个响应必须在前一个响应之后。原因是没有序号标明顺序,只能串行接收。即客户端可以并行,服务端串行,客户端可以不用等待前一个请求返回,发送请求,但服务器必须顺序的返回客户端的请求响应结果。

HTTP 管道化的限制

  1. 管道化要求服务器按照请求发送的顺序返回响应(FIFO),原因很简单,HTTP 请求和响应并没有序号标识,无法将乱序的响应和请求关联起来
  2. 客户端需要保持未收到响应的请求的连接,当连接意外中断时,需要重新发送这部分请求
  3. 只有幂等的请求才能进行管道化,即 GET 和 HEAD 请求才能管道化,否则可能会出现意料之外的结果
  4. HTTP 管道化没有解决 HTTP 队头阻塞:HTTP 管道化要求服务端必须按照请求发送的顺序返回响应,那如果一个响应返回延迟了,那么其后续的响应都会被延迟,直到队头的响应送达。

HTTP1.1 协议缺点

HTTP1.1 有两个主要的缺点:安全不足和性能不高

队头阻塞 (Head-Of-Line Blocking) — 高延迟,性能差

网络延迟问题主要由于队头阻塞导致带宽无法被充分利用。
队头阻塞:当顺序发送的请求序列中的一个请求因为原因被阻塞时,在后面排队的所有请求也一并被阻塞,会导致客户端迟迟收不到数据。
HTTP1.1 缓解队头阻塞问题通过:并发连接和域名分片,本质还是提高一个域名下的 TCP 连接数,并没有本质上解决 HTTP 队头阻塞问题
具体解决方案见 HTTP相关面试题→TCP队头阻塞和HTTP队头阻塞怎么解决?

明文传输 — 不安全性

HTTP1.1 传输数据时,所有传输的内容都是明文,客户端和服务器都无法验证对方的身份,无法保证数据的安全性

无状态特性 — 阻碍交互

HTTP特点→无状态性

不支持服务端推送消息

HTTP2.x

SPDY

由于 HTTP1.x 的缺陷,引入了雪碧图、将小图内联、使用多个域名等等的方式来提高性能,不过这些优化都绕开了 HTTP 协议。直到 2009,Google 公开了自研的 SPDY 协议,主要解决了 HTTP1.1 效率不高的问题,SPDY 的推出算正式改造 HTTP 协议本身。降低延迟、压缩 Header 等,最终也带来了 HTTP2 的诞生。

yuq36

HTTP2 简介

2015 年,HTTP2 发布,兼容 HTTP1.1,HTTP2 基于 SPDY,专注于性能,最大的一个目标是在用户和网站间只用一个连接。使用 HTTP2 能带来 20%~60% 的效率提升。

HTTP2 新特性

为了解决 HTTP1.x 的问题,HTTP2 诞生了,HTTP2 性能提升主要有两点:

  • 头部压缩
  • 多路复用(二进制帧)

还有一些颠覆性的功能实现:

  • 设置请求优先级
  • 服务器推送

HTTP/2 传输数据量的大幅减少,主要有两个原因:以二进制方式传输和 Header 压缩

二进制传输 (二进制分帧)

HTTP2 把报文从文本全部换成二进制格式了,二进制协议解析起来更高效,HTTP2 将请求和响应数据分割为更小的帧,并且它们采用二进制编码。
把 TCP 协议的部分特性摞到了应用层,把原来的 Headers+Body 的报文消息打散为数个小片的二进制帧 (Frame)。用HEADERS 帧存放头部字段,DATA 帧存放请求体数据。分帧之后,服务器看到的不再是一个个完整的 HTTP 请求报文,而是一堆乱序的二进制帧。这些二进制帧不存在先后关系,因此也就不会排队等待,也就没有了 HTTP 的队头阻塞问题。
通信双方都可以给对方发送二进制帧,这种二进制帧的双向传输的序列,也叫做流,HTTP2 用流来在一个 TCP 连接上来进行多个数据帧的通信,这就是多路复用的概念。
如何保证乱序的数据帧?
乱序指的是不同 ID 的 Stream 是乱序的,同一个 StreamID 的帧一定是按顺序传输的,二进制帧到达后对方会将 StreamID 相同的二进制帧组装成完成的请求报文和响应报文。

头部编码 (Header 压缩)-HPack 算法

HTTP1.x 时代,请求体一般有响应的压缩编码过程,通过 Content-Encoding 头部字段来指定。HTTP2 针对头部字段,并没有使用传统的压缩算法,而是开发了专门的压缩算法——HPACK 算法,对请求头进行压缩
HPack 算法:服务器和客户端之间建立哈希表 (字典),将用到的字段存放在这张表中,那么在传输的时候对于之前出现过的值,只需要把索引传给对方即可,对方拿到索引查表即可,这种传索引的方式,让请求头字段得到极大程度的精简和复用;还采用哈夫曼编码来压缩整数和字符串,可以达到 50%~90% 的高压缩率。
具体来说:

  • 在客户端和服务端使用 首部表 来跟踪和存储之前发送的键值对,对于相同的数据,不再通过每次请求和响应发送
  • 首部表在 HTTP2 的连接存续期内始终存在,由客户端和服务器共同渐进地更新
  • 每个新的首部键值对要么被追加到当前表的末尾,要么替换表中之前的值

8028t

多路复用

HTTP2 有了二进制分帧之后,HTTP2 不再依赖 TCP 连接去实现多流并行了:

  • 同域名下所有通信都在单个 TCP 连接上完成

同个域名只需要占用一个 TCP 连接,使用一个 TCP 连接并行发送多个请求和响应,这样整个页面资源的下载过程只需要一次慢启动,同时也避免了多个 TCP 连接竞争带宽所带来的问题

  • 单个 TCP 连接可以承载任意数量的双向数据流 Stream

并行交错地发送多个请求/响应,请求/响应之间互不影响,不像 HTTP1.x 那样排队阻塞

  • 数据流以消息的形式发送,而消息又由一个或多个帧组成,多个帧之间可以乱序发送,因为根据帧首部的流标识可以重新组装
  • HTTP2 中,每个请求都可以带一个 31bit 的优先级,0 表示最高优先级,数值越大优先级越低。有了这个优先级,客户端和服务器就可以处理不同的流时采取不同的策略,以最优的方式发送流、消息和帧。

多路复用的技术可以只通过一个 TCP 连接就可以传输所有的请求数据:
wrp86

服务器推送 (Server Push)

HTTP2 还在一定程度上改变了传统的 “ 请求 - 应答 “ 工作模式,服务器不再是完全被动地响应请求,也可以新建 “ 流 “ 主动向客户端发送消息。比如,在浏览器刚请求 HTML 的时候就提前把可能会用到的 JS、CSS 文件发给客户端,减少等待的延迟,这被称为 “ 服务器推送 “( Server Push,也叫 Cache push)

另外需要补充的是,服务端可以主动推送,客户端也有权利选择是否接收。如果服务端推送的资源已经被浏览器缓存过,浏览器可以通过发送 RST_STREAM 帧来拒收。主动推送也遵守同源策略,换句话说,服务器不能随便将第三方资源推送给客户端,而必须是经过双方确认才行。

HTTP2 缺点

虽然 HTTP2 解决了很多之前旧版本的问题,但是它还是存在一个巨大的问题,主要是底层支撑的 TCP 协议造成的。HTTP2 的主要缺点:

  1. TCP 以及 TCP+TLS 建立连接慢,延时
  2. TCP 的队头阻塞并没有彻底解决
  3. 多路复用导致服务器压力上升
  4. 多路复用容易 timeout

建立连接慢,延时(还是和 HTTP1.x 一样,未优化)

HTTP2 还是使用 TCP 协议来传输的,如果使用 HTTPS 的话,还需要使用 TLS 协议进行安全传输,而 TLS 还有一个握手过程,这样就需要两个握手延迟过程

  • 建立 TCP 连接时,需要和服务器进行三次握手来确认连接成功,需要消耗 1.5 个 RTT 才能进行数据传输
  • 进行 TLS 连接,TLS 分不同的版本,1.2 和 1.3,每个版本建立连接所花费时间不同,大致需要 0~2 个 RTT(利用 SessionID/Session Ticket 可达到 1-RTT、TLS1.3 用 PSK 可达到 0-RTT)

总之,在传输数据之前,需要花费 2~4 个 RTT。

RTT(Round-Trip Time):往返时延,表示从发送端发送数据开始,到发送端收到来自接收端的确认(接收端收到数据后立即发送确认),总共经历的时延。

TCP 队头阻塞未解决

HTTP2 只解决了 HTTP 的队头阻塞,未解决底层 TCP 队头阻塞问题。
HTTP2 多个请求是跑在一个 TCP 连接中,当出现了丢包时,HTTP2 的表现反倒不如 HTTP1.x 了。因为 TCP 为了保证可靠传输,有个 丢包重传 机制,丢失的包必须要等待重新传输确认,HTTP2 出现丢包时,整个 TCP 都要开始等待重传,那么就会阻塞该 TCP 连接上的所有请求,而对于 HTTP1.1 来说,可以开启多个 TCP 连接,出现这种情况反倒只会影响其中一个连接,剩余的 TCP 连接还可以正常传输数据。
8wh4l

因为 http2 使用的是多路复用的流模型,一个 tcp 连接的发送数据过程中可能会把一个个请求分割成多个流发送到服务器,因为 tcp 的 tls 加密是一个 record 的加密,也就是接近 10stream 大小进行加密,如果其中在某一个流丢失了,整一串都会解密失败。这就是 http2 最为严重的队头阻塞问题。

多路复用压力大,容易 timeout

多路复用没有限制同时请求数,请求的平均数量与往常相同,但实际会有许多请求的短暂爆发,导致瞬时 QPS 暴增。
大批量的请求同时发送,由于 HTTP2 连接上存在多个并行的流,而网络带宽和服务器资源优先,每个流的资源都会被稀释,虽然它们开始时间相差更短,但却都可能超时。

网络切换导致四元组变化,需要重建连接

  1. 基于 TCP 四元组确定一个链接,在移动互联网中表现不佳。因为移动设备经常移动,可能在公交地铁等地方,出现了基站变换,Wi-Fi 变化等状态。导致四元组发声变化,而需要重新建立连接

QUIC/HTTP3.x

QUIC 章节

HTTP 协议

HTTP 协议组成

HTTP1.x 报文结构

请求报文
z8j1u
响应报文
0k6w9

HTTP 请求方法

http/1.1 规定了以下请求方法 (注意,都是大写):

  1. GET 通常用来获取资源,没有 body,幂等性
  2. POST 增加或修改资源 (上传数据),有 body
  3. PUT 修改资源,有 body,幂等性
  4. DELETE 删除资源,幂等性
  5. HEAD 获取资源的元信息
  6. CONNECT 建立连接隧道,用于代理服务器
  7. OPTIONS 列出可对资源实行的请求方法,用来跨域请求
  8. TRACE 追踪请求 - 响应的传输路径

HTTP 状态码

RFC 规定 HTTP 的状态码为三位数,被分为五类:

  • 1xx: 表示目前是协议处理的中间状态,还需要后续操作。
  • 2xx: 表示成功状态。
  • 3xx: 重定向状态,资源位置发生变动,需要重新请求。
  • 4xx: 请求报文有误。
  • 5xx: 服务器端发生错误。

1xx

  • 101 Switching Protocols。在 HTTP 升级为 WebSocket 的时候,如果服务器同意变更,就会发送状态码 101。

2xx

  • 200 OK是见得最多的成功状态码。通常在响应体中放有数据。
  • 204 No Content含义与 200 相同,但响应头后没有 body 数据。
  • 206 Partial Content顾名思义,表示部分内容,它的使用场景为 HTTP 分块下载和断点续传,当然也会带上相应的响应头字段 Content-Range。

3xx

  • 301 Moved Permanently即永久重定向,对应着302 Found,即临时重定向。
  1. 比如你的网站从 HTTP 升级到了 HTTPS 了,以前的站点再也不用了,应当返回 301,这个时候浏览器默认会做缓存优化,在第二次访问的时候自动访问重定向的那个地址。
  2. 如果只是暂时不可用,那么直接返回 302 即可,和 301 不同的是,浏览器并不会做缓存优化。
  • 304 Not Modified: 当协商缓存命中时会返回这个状态码。详见浏览器缓存

4xx

  • 400 Bad Request: 开发者经常看到一头雾水,只是笼统地提示了一下错误,并不知道哪里出错了。
  • 403 Forbidden: 这实际上并不是请求报文出错,而是服务器禁止访问,原因有很多,比如法律禁止、信息敏感。
  • 404 Not Found: 资源未找到,表示没在服务器上找到相应的资源。
  • 405 Method Not Allowed: 请求方法不被服务器端允许。
  • 406 Not Acceptable: 资源无法满足客户端的条件。
  • 408 Request Timeout: 服务器等待了太长时间。
  • 409 Conflict: 多个请求发生了冲突。
  • 413 Request Entity Too Large: 请求体的数据过大。
  • 414 Request-URI Too Long: 请求行里的 URI 太大。
  • 429 Too Many Request: 客户端发送的请求过多。
  • 431 Request Header Fields Too Large请求头的字段内容太大。

5xx

  • 500 Internal Server Error: 仅仅告诉你服务器出错了,出了啥错咱也不知道。
  • 501 Not Implemented: 表示客户端请求的功能还不支持。
  • 502 Bad Gateway: 服务器自身是正常的,但访问的时候出错了,啥错误咱也不知道。
  • 503 Service Unavailable: 表示服务器当前很忙,暂时无法响应服务。

HTTP 请求头有哪些?

数据类型、压缩格式、语言和字符集(Content-Type、Content-Encoding、Content-Languages)

6e1op

Content-Type/Accept 发送端发送的数据 MIME 类型

发送端发送的数据格式 Content-Type,接收端对应 Accept

  • 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
Content-Encoding/Accept-Encoding 发送端压缩格式

Accept-Encoding 和 Content-Encoding 是 HTTP 中用来对采用哪种编码格式传输正文进行协定的一对头部字段;发送端数据的压缩格式 Content-Encoding,接收端 Accept-Encoding

  • gzip: 当今最流行的压缩格式
  • deflate: 另外一种著名的压缩格式
  • br: 一种专门为 HTTP 发明的压缩算法
Content-Language/Accept-Language 支持的语言

发送端的语言 Content-Language,接收端 Accept-Language

字符集

发送端的字符集 Content-Type: text/html; charset=utf-8,接收端 Accept-Charset: charset=utf-8

定长和分块传输(Content-Length 和 chunked)

Content-Length 定长

响应头 Content-Length 指明内容的固定大小

Transfer-Encoding 不定长

响应头 Transfer-Encoding: chunked,表示分块传输数据,设置这个 header 后有两个效果:

  • Content-Length 会被忽略
  • 基于长链接持续推送动态内容

大文件传输、分块传输(Range 和 Content-Range)

客户端请求用 Range,服务端返回用 Content-Range

Range

对于客户端而言,它需要指定请求哪一部分,通过 Range 这个请求头字段确定,格式为 Range: bytes=x-y,x1-y1

  • 0-499表示从开始到第 499 个字节
  • 500- 表示从第 500 字节到文件终点。
  • -100表示文件的最后 100 个字节。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 请求单段数据
Range: bytes=0-9
// 请求多段数据
Range: bytes=0-9, 30-39

1. HTTP请求头字段
Range头指示服务器只传输一部分Web资源。这个头可以用来实现断点续传功能。Range字段可以通过三种格式设置要传输的字节范围:
Range:bytes=1000-2000    传输范围从10002000字节。
Range:bytes=1000-   传输Web资源中第1000个字节以后的所有内容。
Range bytes=1000   传输最后1000个字节。

2. HTTP响应消息头字段
Accept-Ranges:这个字段说明Web服务器是否支持Range支持,则返回Accept-Ranges:bytes,如果不支持,则返回Accept-Ranges:none.
Content-Range:指定了返回的Web资源的字节范围。这个字段值的格式是:例子: Content-Range1000-3000/5000

可用于实现断点下载续传功能

Content-Range
  • 单段数据
1
2
3
4
5
6
HTTP/1.1 206 Partial Content
Content-Length: 10
Accept-Ranges: bytes
Content-Range: bytes 0-9/100 // 0-9表示请求的返回,100表示资源的总大小

i am xxxxx
  • 多段数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=00000010101 // 多段数据、响应体中的分隔符
Content-Length: 189
Connection: keep-alive
Accept-Ranges: bytes


--00000010101
Content-Type: text/plain
Content-Range: bytes 0-9/96

i am xxxxx
--00000010101
Content-Type: text/plain
Content-Range: bytes 20-29/96

eex jspy e
--00000010101-- // 分隔末尾添上--表示结束
http 分块传输原理

http 协议定义的分块传输的响应 header 字段,具体是否支持取决于 server 的实现,通过在请求头加上 range 字段来验证服务器是否支持分块传输:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
curl -H "Range: bytes=0-10" http://download.dcloud.net.cn/HBuilder.9.0.2.macosx_64.dmg -v
# 请求头
> GET /HBuilder.9.0.2.macosx_64.dmg HTTP/1.1
> Host: download.dcloud.net.cn
> User-Agent: curl/7.54.0
> Accept: */*
> Range: bytes=0-10
# 响应头
< HTTP/1.1 206 Partial Content
< Content-Type: application/octet-stream
< Content-Length: 11
< Connection: keep-alive
< Date: Thu, 21 Feb 2019 06:25:15 GMT
< Content-Range: bytes 0-10/233295878

在请求头中添加 “Range: bytes=0-10” 的作用是,告诉服务器本次请求我们只想获取文件 0-10(包括 10,共 11 字节) 这块内容。如果服务器支持分块传输,则响应状态码为 206,表示 “ 部分内容 “,并且同时响应头中包含 “Content-Range” 字段,如果不支持则不会包含

Content-Range: bytes 0-10/233295878
0-10 表示本次返回的区块,233295878 代表文件的总长度,单位都是 byte, 也就是该文件大概 233M 多一点。
简单的设计一个多线程的文件分块下载器:

  1. 先检测是否支持分块传输,如果不支持,直接下载;如果支持,将剩余内容分块下载
  2. 各个分块下载时保存到各自临时文件,等待所有分块下载完成后合并临时文件
  3. 删除临时文件

HTTP 表单数据

在 HTTP 中,有两种主要的表单提交的方式,体现在两种不同的 Content-Type 取值:

  • application/x-www-form-urlencoded
  • multipart/form-data
application/x-www-form-urlencoded URL 编码
  • 其中的数据会被编码成以 & 分隔的键值对
  • 字符以 URL编码 方式编码

如 a=1&b=2,转换为 a%3D1%26b%3D2

multipart/form-data
  • 请求头中的 Content-Type 字段会包含 boundary,且 boundary 的值有浏览器默认指定。例: Content-Type: multipart/form-data;boundary=----WebkitFormBoundaryRRJKeWfHPGrS4LKe
  • 数据会分为多个部分,每两个部分之间通过分隔符来分隔,每部分表述均有 HTTP 头部描述子包体,如 Content-Type,在最后的分隔符会加上 -- 表示结束。
1
2
3
4
5
6
7
8
Content-Disposition: form-data;name="data1";
Content-Type: text/plain
data1
----WebkitFormBoundaryRRJKeWfHPGrS4LKe
Content-Disposition: form-data;name="data2";
Content-Type: text/plain
data2
----WebkitFormBoundaryRRJKeWfHPGrS4LKe--

HTTP 缓存

HTTP 缓存相关 Header

Expires
在 HTTP1.0,响应使用 Expires 头标识缓存的有效期,其值是一个绝对时间,当客户端再次发出网络请求时可比较当前时间和上次响应的 Expires 时间进行比较,来决定是使用缓存还是发起新的请求

⽐如 Expires:Thu,31 Dec 2020 23:59:59 GMT。使用 Expires 最大的问题是它依赖客户端的本地时间,如果用户自己修改了本地时间,就会导致无法准确的判断缓存是否过期。

Cache-Control
HTTP1.1 推出,优先级高于 Expires。主要值有:

  1. private 默认值,标识那些私有的业务逻辑数据,比如根据用户行为下发的推荐数据。该模式下网络链路中的代理服务器等节点不应该缓存这部分数据,因为没有实际意义。
  2. public 内容是公开的,中间节点可缓存。public 和 private 相反,用于标识那些通用的业务数据,比如获取新闻列表,所有人看到的都是同一份数据,因此客户端、代理服务器都可以缓存
  3. no-cache 可进行缓存,但在客户端使用缓存前必须去服务器进行缓存资源有效性的验证
  4. max-age 表示缓存时长,单位为秒,指一个时间段,比如一年,通常用于不经常变化的静态资源
  5. no-store 任何节点禁止使用缓存

HTTP 缓存原理

强制缓存
网络请求响应 header 标识了 Expires 或 Cache-Control 带了 max-age 信息,客户端计算缓存并未过期,则可以直接使用本地缓存内容,而不用真正的发起一次网络请求

强制缓存缺点:一旦服务器有资源更新,直到缓存时间截止前,客户端无法获取到最新的资源(除非请求时手动添加 no-store 头)

协商缓存
几种头来实现协商缓存:

  1. Last-Modified/If-Modified-Since 头
  • 在服务器响应头添加 Last-Modified 头标识资源的最后修改时间,单位为秒,当客户端再次发起网络请求时添加 If-Modified-Since 头并赋值为上次请求拿到的 Last-Modified 头的值
  • 服务器收到请求后自行判断资源是否仍然有效,如果有效则返回状态码 304 同时 body 体位空,否则下发最新的资源数据;客户端如果发现响应状态码是 304,则取出本地的缓存数据作为响应内容。

这套方案问题:

  • 资源文件使用最后修改时间有一定局限性,Last-Modified 单位为秒,如果某些文件在一秒内被修改则并不能准确的标识修改时间;
  • 资源修改时间并不能作为资源是否修改的唯一依据(比如资源文件是 Daily Build 的,每天都会生成新的,但是其实实际内容可能并未改变)
  1. If-None-Match/Etag 头

标签,类似指纹。流程和 Last-Modified 一样,只是把服务器响应头换成 ETag,客户端发出请求的头变成了 If-None-Match。
ETag 是资源的唯一标识,服务端资源变化一定会导致 ETag 变化。ETag 具体的生成方式由服务端控制,常见的影响因素包括:文件最终修改时间、文件大小、文件编号等。

URI 和 URL?

URI, 全称为 (Uniform Resource Identifier), 也就是统一资源标识符,它的作用很简单,就是区分互联网上不同的资源。但是,它并不是我们常说的网址, 网址指的是 URL, 实际上 URI 包含了 URN 和 URL 两个部分,由于 URL 过于普及,就默认将 URI 视为 URL 了。
URI 只能使用 ASCII, ASCII 之外的字符是不支持显示的,需要编码。
URI 结构
vra0p

  • scheme 表示协议名,比如 http, https, file 等等。后面必须和://连在一起。
  • user:passwd@ 表示登录主机时的用户信息,不过很不安全,不推荐使用,也不常用。
  • **host:port **表示主机名和端口。
  • **path **表示请求路径,标记资源所在位置。
  • **query **表示查询参数,为 key=val 这种形式,多个键值对之间用&隔开。
  • fragment **表示 URI 所定位的资源内的一个锚点**,浏览器可以根据这个锚点跳转到对应的位置。

https://www.baidu.com/s?wd=HTTP&rsv_spt=1

HTTP 特点

灵活可扩展

HTTP 协议只规定了基本格式;另一个是传输形式的多样性,不仅仅可以传输文本,还能传输图片、视频等任意数据,非常方便。

可靠传输

HTTP 基于 TCP/IP,因此把这一特性继承了下来

请求 - 应答

也就是一发一收、有来有回

无状态性

无状态性指的是 HTTP 协议对于事物处理没有记忆能力,每一次请求之间是没有联系的,都是独立的,服务器不知道请求两次之间是否是同一个用户。

  • HTTP/1.0 时代每个 TCP 连接只能发送一个请求,因此无法在连接中产生会话的概念,属于无状态协议
  • HTTP/1.1 时代引入了 keep-alive 即 TCP 连接默认不关闭,可以被多个请求复用。但此时的连接复用仅仅是为了提高传输效率,keep-alive 默认情况下,它的若干个请求会排队串行化单线程处理,即后面的请求等待前面请求的返回才能获得执行机会,它们之间没有什么关联,也属于无状态协议
  • 至于 HTTP/2,它应该算是一个有状态的协议了(有握手和 GOAWAY 消息,有类似于 TCP 的流控),所以以后说 “HTTP 是无状态的协议 “ 就不太对了,最好说 “HTTP 1.x 是无状态的协议 “

TCP 有状态吗?
TCP 是有状态的,在一次 TCP 连接中,每一次的数据交换都和上一次紧密相关的,TCP 报文存在 ACK 字段用于确认上次接收的报文,每一次 TCP 数据交换双方都是能够确切知道对方的信息。
为什么基于 TCP 的 HTTP 是无状态的?
HTTP 协议利用 TCP 协议来进行数据传输的,因为它们本身并没有强关联,它们处于 OSI 不同层次(HTTP 应用层协议,TCP 属于传输层协议),不能说 TCP 是有状态,HTTP 协议就有状态
HTTP1.1 后支持了 keep-alive,那么每次 HTTP 请求还是无状态吗?
也是无状态,不同的请求还是按队列来响应的,它们之间没有任何关联
为什么不改进 http 协议使之有状态?
最初的 http 协议只是用来浏览静态文件的,无状态协议已经足够,这样实现的负担也很轻(相对来说,实现有状态的代价是很高的,要维护状态,根据状态来操作。)引入了 Cookie 和 Session 来保证有状态。

HTTP 缺点

  1. 无状态

在需要长连接的场景中,需要保存大量的上下文信息,以免传输大量重复的信息,那么这时候无状态就是 http 的缺点了。 但与此同时,另外一些应用仅仅只是为了获取一些数据,不需要保存连接上下文信息,无状态反而减少了网络开销,成为了 http 的优点。

  1. 明文传输 数据不安全
  2. 队头阻塞问题

当 HTTP 开启长连接时,共用一个 TCP 连接,同一时刻只能处理一个请求,那么当前请求耗时过长的情况下,其它的请求只能处于阻塞状态,也就是著名的HTTP 队头阻塞问题。

HTTP 和 HTTPS 的区别?

HTTPS 相比 HTTP 最大的不同就是多了一层 SSL (Secure Sockets Layer 安全套接层) 或 TLS (Transport Layer Security 安全传输层协议)。

Cookie、Session 和 Token

背景
HTTP 协议是无状态的,无状态意味着,服务器无法给不同的客户端响应不同的信息。这样一些交互业务就无法支撑了。Cookie 应运而生。

什么是 Cookie?
Cookie 是浏览器里存储的一个很小的文本文件,内部以键值对的方式来存储;Cookie 是客户端保存用户信息的一种机制,用来记录用户的一些信息。
Cookie 最大的作用就是存储 sessionID 用来唯一标识用户。
Cookie 不可跨域名

Cookie 流程

  1. Client 发送 HTTP 请求给 Server
  2. Server 响应,并附带 set-cookie 的头信息
  3. Client 保存 Cookie,之后请求 Server 会覆盖 cookie:xxx 的头信息
  4. Server 从 Cookie 知道 Client 是谁了,返回相应的响应

Cookie 缺点

  1. 安全缺陷 Cookie 很容易被非法用户截获,然后进行一系列的篡改,最后在 Cookie 的有效期内重新发送给服务器
  2. 容量缺陷:体积上限只有 4K,只能用来存储少量的信息
  3. 性能缺陷:Cookie 紧跟域名,因此域名下的请求都会携带上完整的 Cookie,造成性能浪费,因为请求携带了很多不必要的内容。

Session

什么是 Session?

  • Session 是另一种记录客户状态的机制,不同的是 Cookie 保存在客户端浏览器中,而 Session 保存在服务器上
  • 客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。这就是 Session。客户端浏览器再次访问时只需要从该 Session 中查找该客户的状态就可以了
  • 如果说 Cookie 机制是通过检查客户身上的 “ 通行证 “ 来确定客户身份的话,那么 Session 机制就是通过检查服务器上的 “ 客户明细表 “ 来确认客户身份。
  • Session 相当于程序在服务器上建立的一份客户档案,客户来访的时候只需要查询客户档案表就可以了。

Session 缺点

  1. Session 信息保存在服务器,会话多了后,服务器压力增大
  2. SessionID 存储在 Cookie,还是有暴露的风险,比如 CSRF(Cross-Site Request Forgery,跨站请求伪造)
  • cookie 是一个实际存在的、具体的东西,HTTP 协议中定义在 header 中的字段
  • session 是一个抽象概念,开发者为了实现中断和继续等操作,将 Client 和 Server 之间一对一的交互,抽象为会话,进行衍生出会话状态,这就 session 的概念
  • session 描述的是一种通信会话机制,而 Cookie 只是目前实现这种机制的主流方案里面的一个参与者,它一般用于保存 session ID

Token 令牌

Token 就是一段字符串,Token 传递的过程跟 Cookie 类似,只是传递对象变成了 Token。用户使用用户名、密码请求服务器后,服务器就生成 Token,在响应中返给客户端,客户端再次请求时附带上 Token,服务器就用这个 Token 进行认证鉴权。
Token 一般放 header,不放 body 或 url query,否则和请求业务的参数混合在一起,导致代码混乱

Http 错误

线上问题 -ERR_CONNECTION_CLOSED

问题:线上一个链接在不登录时能正常打开,登录后就打不开报错 ERR_CONNECTION_CLOSED
分析:登录后有很多 ABT 参数会被设置到 request header,导致 header 过大,报错
解决:header 数据不要过大

HTTP 相关面试题

HTTP 队头阻塞和 TCP 队头阻塞?怎么解决?

  • HTTP1.0/HTTP1.1 存在 HTTP 队头阻塞
    • 并发连接:一个域名允许多个 TCP 长连接,不会阻塞一个队列的所有任务;RFC2616 标准是 2 个,Chrome 是 6 个
    • 域名分片: 一个顶级域名划分为多个二级域名,多个二级域名都指向同一台服务器,能并发的连接就更多了
    • pipeline:请求管道化,Client 可以并行发送请求,由于 HTTP 的无状态性,请求没有序号,Server 必须串行的一个个处理客户端请求,如果前一个请求慢,那就会阻塞后面请求的响应
    • 并发连接和域名分片,本质还是提高一个域名下的 TCP 连接数,并没有本质上解决 HTTP 队头阻塞问题,多条 TCP 连接还会竞争有限的带宽资源
  • HTTP2.0 解决 HTTP 队头阻塞,还存在 TCP 队头阻塞
    • 多路复用: 二进制分帧,报文由文本变为了二进制,请求/响应报文拆分成若干个帧:Header 帧,Data 帧,每个帧有序号,一个 TCP 存在多个流,一个帧在一个流传输,流互相独立,也就不存在 HTTP 队头阻塞问题了
    • HTTP2.0 通过多路复用同一个 TCP 连接解决了队头阻塞问题,前提是 TCP 协议不出现任何数据包阻塞的前提
    • HTTP2.0 还是基于 TCP,如果网络环境不好,数据包超时确认或丢失导致的重传,导致 TCP 的滑动窗口阻塞了后续数据包的发送,就是 TCP 队头阻塞
  • HTTP3.0(基于 UDP 的 QUIC)解决了 TCP 队头阻塞
    • 需要重传的数据包不阻塞在当前窗口,而是转移到最后的当成新的数据包发送:QUIC 使用 Packet Number 单调递增的设计,可以让数据包不再像 TCP 那样必须有序确认,QUIC 支持乱序确认,当数据包 Packet N 丢失后,只要有新的已接收数据包确认,当前窗口就会继续向右滑动

HTTP1.x 队头阻塞问题

HTTP 传输是基于请求 - 响应的模型进行的,报文必须是一发一收,里面的任务被放在一个任务队列中串行执行,一旦队首的请求处理太慢,就会阻塞后面请求的处理,这就是HTTP 队头阻塞问题。
HTTP1.1 如何解决队头阻塞问题?(未基于 HTTP 本身解决队头阻塞问题)

  • 并发连接:一个域名允许分配多个长连接,相当于增加了任务队列,不至于一个队列的任务阻塞其他所有任务。在 RFC2616 规定客户端最多并发 2 个连接,只不过事实上浏览器标准中,这个上限要多,Chrome 是 6 个。即使提高了并发连接,还是不能满足对性能的需求。
  • 域名分片:如 content1.haket.me、content2.hacket.me,这样 hacket.me 域名下可以分出非常多的二级域名,它们都指向同一台服务器,能够并发的长连接数更多了,事实上也更好地缓解了 HTTP 队头阻塞的问题。

HTTP 管道化没有解决 HTTP 队头阻塞问题, 原因是 HTTP 管道化,客户端可以并行的发送请求,但服务器必须串行的处理客户端的请求,一个个响应,如果前一个请求慢,那就会阻塞后面请求的响应

HTTP/1.1 管道 (pipelining) 解决了队头阻塞问题?

没有。
6wezq

  • 如果没有管道 (上图左侧),浏览器必须等待发送第二个资源请求,直到第一个请求的响应被完全接收,这会为每个请求增加一个 RTT 延迟
  • 有了管道 (上图中间),浏览器不必等待任何响应数据,就可以发送新的请求,这样就可以节省一些 RTTs。但服务器必须按照接收请求的顺序发送对这些管道化请求的响应。管道解决了请求的队头阻塞,而不是响应的队头阻塞。可悲的是,响应队头阻塞是导致 Web 性能问题最多的原因。更糟糕的是,大多数浏览器实际上并没有在现实中使用 HTTP/1.1 管道,因为这会使队头阻塞在多个并行 TCP 连接的设置中变得更加不可预测。

HTTTP2 如何解决 HTTP 队头阻塞

HTTP1.1 并没有真正从 HTTP 本身层面解决该问题,只是增加了 TCP 连接,分摊风险而已。多条 TCP 连接会竞争有限的带宽,让真正优先级高的请求不能优先处理。而 HTTP2 从 HTTP 协议本身解决了队头阻塞问题。
HTTP2 引入了帧、消息和数据流等概念,每个请求/响应被称为消息,每个消息都被拆分成若干个帧进行传输,每个帧都分配一个序号。每个帧在传输是属于一个数据流,而一个连接上可以存在多个流,各个帧在流和连接上独立传输,到达之后在组装成消息,这样就避免了请求/响应阻塞。
把 TCP 协议的部分特性摞到了应用层,把原来的 Headers+Body 的报文消息打散为数个小片的二进制帧 (Frame)。用HEADERS 帧存放头部字段,DATA 帧存放请求体数据。分帧之后,服务器看到的不再是一个个完整的 HTTP 请求报文,而是一堆乱序的二进制帧。这些二进制帧不存在先后关系,因此也就不会排队等待,也就没有了 HTTP 的队头阻塞问题。

HTTP 队头阻塞和 TCP 队头阻塞

  • HTTP 协议的队头阻塞是在应用层,而 TCP 协议的队头阻塞是在传输层
  • TCP 的队头阻塞是在数据包层面,单位是数据包,前一个报文没有收到就不会将后面收到的报文上传给 HTTP,而 HTTP 的队头阻塞是在 HTTP 请求 - 响应层面,前一个请求没处理完,后面的请求就要阻塞。

HTTP2 的 TCP 队头阻塞 (TCP 丢包重传)

HTTTP2 还是基于 TCP 协议的,还是存在 TCP 队头阻塞问题。TCP 中的队头阻塞的产生是由 TCP 自身的实现机制决定的,无法避免。想要在应用程序当中避免 TCP 队头阻塞带来的影响,只有舍弃 TCP 协议。
HTTP2 主要的问题在于,多个 HTTP 请求在复用一个 TCP 连接,下层的 TCP 协议是不知道有多少个 HTTP 请求的,所以一旦发生了TCP 丢包现象,就会触发 TCP 的重传机制,这样在一个 TCP 连接中的所有 HTTP 请求都必须等待这个丢了的包被重传回来(即如果一个 TCP 包丢失,所有后续的包都需要等待它的重传,先到的数据包不会传递给 HTTP,直到丢失的包重传成功,即使它们包含来自不同流的无关联数据)。

假设包 3 先到达,服务器端必须等待包 2 到达后,才将其和包 3 发送给浏览器。

HTTTP3(基于 QUIC) 真正解决 TCP 队头阻塞

由于 TCP 本身的限制,难以对其进行改变。
所以 HTTP3 选择的替代方法是实现一个全新的传输层协议 QUIC,它运行在不可靠的 UDP 协议之上,但它包括 TCP 的所有特性(可靠性、拥塞控制、流量控制和排序等),且集成了 TLS,不允许未加密的连接。HTTP3 运行在 QUIC 协议之上。
QUIC 的流帧 (Stream Frames) 分别跟踪每个流的字节范围,QUIC 协议知道有自己独立的流。
当服务器端知道包 3 比包 2 早到达,QUIC 查看流 1 的字节范围,发现这个流帧完全遵循流 id 1 的第一个流帧,它可以立即将这些数据提供给浏览器进行处理,然而对于流 id 2,QUIC 确实看到了一个缺口(它还没有接收到字节 0-299,这些字节在丢失的 QUIC 数据包 2 中),它将保存该流帧,直到 QUIC 数据包 2 的重传到达。

GET 请求和 POST 请求的区别?

GET 和 POST 本质上都是 TCP 连接,并无差别,但是由于 HTTP 的规定和浏览器/服务器的限制,导致它们在应用过程中体现出一些不同

  • 缓存的角度,GET 请求会被浏览器主动缓存下来,留下历史记录,而 POST 默认不会。
  • 编码的角度,GET 只能进行 URL 编码,只能接收 ASCII 字符,而 POST 没有限制,支持多种编码方式。
  • 参数的角度,GET 一般放在 URL 中,因此不安全,POST 放在请求体中,更适合传输敏感信息。
  • 幂等性的角度,GET 是幂等的,而 POST 不是。(幂等表示执行相同的操作,结果也是相同的)
  • GET 请求参数会被完整的记录在浏览历史中,而 POST 的参数不会被保留
  • TCP的角度,GET 请求会把请求报文一次性发出去,而 POST 会分为两个 TCP 数据包,首先发 header 部分,如果服务器响应 100(continue), 然后发 body 部分。(火狐浏览器除外,它的 POST 请求只发一个 TCP 包)

HTTP1.0、HTTP1.1、HTTP2 和 HTTP3 协议的介绍?

HTTP1.0

  • 1996 年,HTTP1.0 使用
  • TCP 连接不复用,每发起一个网络请求都要重新建立连接。但由于 TCP 的三次握手和四次挥手机制,都会经历这样一个慢启动过程。所以 HTTP1.0 的性能很差

HTTP1.0 实现中,如果需要从服务端获取大量资源,会开启 N 条 TCP 短链接,并行的获取信息。

HTTP1.1

  • 1999 年,HTTP1.1 开始使用
  • 持久连接 keep-alive:HTTP1.1 支持持久连接和请求流水线 (Pipelining) 处理。在一个 TCP 连接上可以传送多个 HTTP 请求和响应,减少了建立和关闭连接的消耗和延迟,HTTP1.1 默认开启 keep-alive。
  • HTTP1.1 队头阻塞:后面的请求必须等待前面的请求完成才能进行,当所有请求都集中在一条连接时,在网络拥塞时就会出现队头阻塞

SPDY

  • 多路复用 TCP 通道,降低 HTTP 的高延时
  • 允许请求设置优先级
  • 头部压缩
  • 基于 SSL 的安全传输
  • 还是使用文本格式的协议

HTTP2.0(基于 SPDY 修改)

ltngb

  • TLS1.2
  • 二进制分帧:请求和响应采用二进制格式,所有的传输信息分割为更小的帧,有 Headers 帧和 Data 帧
  • TCP 多路复用:单个 TCP 连接,在一条连接上,可以发送多个请求和响应
  • 头部压缩,采用 HPACK 压缩算法,查表
  • 服务器推送,服务端可以推送资源给客户端
  • TCP 队头阻塞:HTTP/2 采用多路复用,多个资源可以共用一个连接。 但它解决的只是应用层的复用,在出现数据包丢失时,由于 TCP 重传机制,后面的资源需要等待前面的传输完毕才能继续。这就是队头阻塞现象 (Head-of-line blocking)

HTTP3.0(基于 QUIC)

  • 2018 年,基于 QUIC 的 HTTP3 协议
  • QUIC=HTTP2.0+TLS1.3+UDP
    • 基于 UDP,没有了 TCP 的三次握手和四次挥手的过程,连接更快
    • 连接迁移不需要重新连接,用 Connection ID 替换了 TCP 四元组
    • QUIC 抽象出了一个 stream(流)的概念,多个流,可以复用一条连接,那么滑动窗口这些概念就不用作用在连接上了,而是作用在 stream 上。由于 UDP 只管发送不管成功与否的特性,这些数据包的传输就能够并发执行。协议的 server 端,会解析并缓存这些数据包,进行组装和整理等。由于抽象出了 stream 的概念,就使得某个数据包传输失败,只会影响个 stream 的准确性,而不是整个连接的准确性
本文由作者按照 CC BY 4.0 进行授权