http杂谈

如题,这篇文章是一些HTTP零散的知识点,主要是一些报文中常用的响应头字段和一些面试题。

1. 请求方法

有哪些请求方法,作用分别是什么?

  • GET:请求指定页面信息,并返回对应实体。
  • HEAD:类似于GET请求,只不过返回的响应中没有具体的内容,用于获取报文头部。
  • POST:向指定资源提交数据进行处理请求(例如表单提交或者上传文件)。数据包含在请求体中。POST请 求可能会导致新的资源建立或已有资源修改(不是幂等)。
  • PUT:从客户端向服务器传送的数据代替指定的文档的内容。
  • DELETE:请求服务器删除指定的页面。
  • OPTIONS:允许客户端查看服务器的性能,列出可对资源实行的请求方法,用来跨域请求。
  • TRACRE:追踪请求-响应的传输路径,主要用于测试和判断。

GET和POST有什么区别?

首先最直观的是语义上的区别。接下来是一些细节上的区别:

  • 缓存的角度,GET请求会被浏览器主动缓存下来,留下历史记录,能存入书签,而POST请求默认不会
  • 编码的角度,GET请求只能进行URI编码,只能接收ASCll字符,而POST没有限制。
  • 参数的角度,GET一般放在URI中,在历史记录中也能看到,因此不安全,POST放在请求体中,适合传输敏感信息。
  • 幂等的角度,GET是幂等的,而POST不是。
  • TCP的角度,GET方式传输的数据量非常小,一般限制在2KB左右,但是执行效率却比POST还好,会一次性把包发出去。而POST方式传递的数据量相对较大,它是等待服务器来读取数据,不过也有字节限制(实际上IIS4中最大为80KB,IIS5中最大为100KB),这是为了避免对服务器用大量数据进行恶意攻击。而 POST 会分为两个 TCP 数据包,首先发 header 部分,如果服务器响应 100(continue), 然后发 body 部分。(火狐浏览器除外,它的 POST 请求只发一个 TCP 包)

2. URI

URI结构

URI真正的结构是这样:

http杂谈 流程图

解释下字段的含义:

scheme 表示协议名,比如http, https, file等等。后面必须和://连在一起。

user:passwd@ 表示登录主机时的用户信息,不过很不安全,不推荐使用,也不常用。

host:port表示主机名和端口。

path表示请求路径,标记资源所在位置。

query表示查询参数,为key=val这种形式,多个键值对之间用&隔开。

fragment表示 URI 所定位的资源内的一个锚点,浏览器可以根据这个锚点跳转到对应的位置。

URI编码

URI不支持显示ASCll之外的字符,而且还有一部分是界定符,如果不加以限制会导致解析出错。

因此URI引入了编码机制,将所有非ASCll字符和界定符转为十六进制字节值,然后在前面加上一个%,如空格被转换为%20

3. HTTP状态码

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

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

1xx

100 Continue:POST请求TCP发送header部分,服务器发送100,客户端再发送body部分。

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:临时重定向,表示请求的资源临时搬到了其他位置。

比如你的网站从 HTTP 升级到了 HTTPS 了,以前的站点再也不用了,应当返回301,这个时候浏览器默认会做缓存优化,在第二次访问的时候自动访问重定向的那个地址。

而如果只是暂时不可用,那么直接返回302即可,和301不同的是,浏览器并不会做缓存优化

303 See Other:临时重定向,应使用GET定向获取资源。303功能与302一样,区别只是303明确 客户端应该使用GET访问

304 Not Modified:表示客户端发送附带条件的请求(GET方法请求报文中的IF…)时,条件不满足。返回304时,不包含任何响应主体。虽然304被划分在3XX,但和重定向没有关系

307 Temporary Redirect:临时重定向,和302有着相同含义。POST不会变成GET

4xx

400 Bad Request:客户端请求有错,服务器不知道哪里错了。

401 Unauthorized:请求未被授权。这个状态代码必须和WWW-Authenticate报头域一起使用。

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 太大。

415 Unsupported media type:不支持的媒体类型。

416 Range Not Satisfiable:请求的范围无效。

429 Too Many Request:客户端发送的请求过多。

431 Request Header Fields Too Large:请求头的字段内容太大。

5xx

500 Internal Server Error:仅仅告诉你服务器出错了,出了啥错咱也不知道。

501 Not Implemented:表示客户端请求的功能还不支持。

502 Bad Gateway:服务器自身是正常的,但访问的时候出错了,啥错误咱也不知道。

503 Service Unavailable:表示服务器当前很忙,暂时无法响应服务,一段时间后可能恢复正常

4. HTTP特点和缺点?

HTTP特点

  1. 灵活可扩展,主要体现在两个方面。一个是语义上的自由,只规定了基本格式,比如空格分隔单词,换行分隔字段,其他的各个部分都没有严格的语法限制。另一个是传输形式的多样性,不仅仅可以传输文本,还能传输图片、视频等任意数据,非常方便。
  2. 可靠传输。HTTP 基于 TCP/IP,因此把这一特性继承了下来。这属于 TCP 的特性,不具体介绍了。
  3. 基于请求/响应模型。也就是一发一收有来有回, 当然这个请求方和应答方不单单指客户端和服务器之间,如果某台服务器作为代理来连接后端的服务端,那么这台服务器也会扮演请求方的角色。
  4. 无状态。这里的状态是指通信过程的上下文信息,而每次 http 请求都是独立、无关的,默认不需要保留状态信息。

HTTP缺点

无状态

所谓的优点和缺点还是要分场景来看的,对于 HTTP 而言,最具争议的地方在于它的无状态

在需要长连接的场景中,需要保存大量的上下文信息,以免传输大量重复的信息,那么这时候无状态就是 http 的缺点了。

但与此同时,另外一些应用仅仅只是为了获取一些数据,不需要保存连接上下文信息,无状态反而减少了网络开销,成为了 http 的优点。

明文传输

即协议里的报文(对于HTTP1.1及之前,主要指的是头部,数据体部分可以是二进制或者文本)不使用二进制数据,而是文本形式。

这当然对于调试提供了便利,但同时也让 HTTP 的报文信息暴露给了外界,给攻击者也提供了便利。WIFI陷阱就是利用 HTTP 明文传输的缺点,诱导你连上热点,然后疯狂抓你所有的流量,从而拿到你的敏感信息。

队头阻塞问题

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

5. 对Accept字段的理解

数据格式

HTTP持非常多的数据格式,那么这么多格式的数据一起到达客户端,客户端怎么知道它的格式呢?

开始之前需要先介绍一个标准——MIME(Multipurpose Internet Mail Extensions, 多用途互联网邮件扩展)。它首先用在电子邮件系统中,让邮件可以发任意类型的数据,这对于 HTTP 来说也是通用的。

因此,HTTP 从MIME type取了一部分来标记报文 body 部分的数据类型,这些类型体现在Content-Type这个字段,当然这是针对于发送端而言,接收端想要收到特定类型的数据,也可以用Accept字段。

具体而言,这两个字段的取值可以分为下面几类:

1
2
3
4
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字段上。这个字段的取值有下面几种:

  • gzip: 当今最流行的压缩格式
  • deflate: 另外一种著名的压缩格式
  • br: 一种专门为 HTTP 发明的压缩算法
1
2
3
4
// 发送端
Content-Encoding: gzip
// 接收端
Accept-Encoding: gzip

支持语言

对于发送方而言,还有一个Content-Language字段,在需要实现国际化的方案当中,可以用来指定支持的语言,在接受方对应的字段为Accept-Language。如:

1
2
3
4
// 发送端
Content-Language: zh-CN, zh, en
// 接收端
Accept-Language: zh-CN, zh, en

字符集

最后是一个比较特殊的字段, 在接收端对应为Accept-Charset,指定可以接受的字符集,而在发送端并没有对应的Content-Charset, 而是直接放在了Content-Type中,以charset属性指定。如:

1
2
3
4
// 发送端
Content-Type: text/html; charset=utf-8
// 接收端
Accept-Charset: charset=utf-8

http杂谈 流程图

6. 定长和不定长的数据的传输

定长包体

对于定长包体而言,发送端在传输的时候一般会带上 Content-Length, 来指明包体的长度。

我们用一个nodejs服务器来模拟一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const http = require('http');

const server = http.createServer();

server.on('request', (req, res) => {
if(req.url === '/') {
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Length', 10);
res.write("helloworld");
}
})

server.listen(8081, () => {
console.log("成功启动");
})

启动后访问: localhost:8081。浏览器中显示如下:

1
helloworld

这是长度正确的情况,那不正确的情况是如何处理的呢?我们试着把这个长度设置的小一些:

1
res.setHeader('Content-Length', 8);

重启服务,再次访问,现在浏览器中内容如下:

1
hellowor

那后面的ld哪里去了呢?实际上在 http 的响应体中直接被截去了。然后我们试着将这个长度设置得大一些:

1
res.setHeader('Content-Length', 12);

此时浏览器显示如下:

http杂谈 流程图

直接无法显示了。可以看到Content-Length对于 http 传输过程起到了十分关键的作用,如果设置不当可以直接导致传输失败。

不定长包体

这里就必须介绍另外一个 http 头部字段了:

1
Transfer-Encoding: chunked

表示分块传输数据,设置这个字段后会自动产生两个效果:

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

我们依然以一个实际的例子来模拟分块传输,nodejs 程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const http = require('http');

const server = http.createServer();

server.on('request', (req, res) => {
if(req.url === '/') {
res.setHeader('Content-Type', 'text/html; charset=utf8');
res.setHeader('Content-Length', 10);
res.setHeader('Transfer-Encoding', 'chunked');
res.write("<p>来啦</p>");
setTimeout(() => {
res.write("第一次传输<br/>");
}, 1000);
setTimeout(() => {
res.write("第二次传输");
res.end()
}, 2000);
}
})

server.listen(8009, () => {
console.log("成功启动");
})

访问效果如下:

http杂谈 流程图

抓取响应报文:

http杂谈 流程图

响应体结构大概是这样(最后是留有有一个空行的):

1
2
3
4
5
6
chunk长度(16进制的数)
第一个chunk的内容
chunk长度(16进制的数)
第二个chunk的内容
......
0

7. 大文件的传输

对于几百 M 甚至上 G 的大文件来说,如果要一口气全部传输过来显然是不现实的,会有大量的等待时间,严重影响用户体验。因此,HTTP 针对这一场景,采取了范围请求的解决方案,允许客户端仅仅请求一个资源的一部分。

当然,前提是服务器要支持范围请求,要支持这个功能,就必须加上这样一个响应头:Accept-Ranges: none,用来告知客户端这边是支持范围请求的。

Range字段拆解

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

  • 0-499表示从开始到第 499 个字节。
  • 500- 表示从第 500 字节到文件终点。
  • -100表示文件的最后100个字节。

服务器收到请求之后,首先验证范围是否合法,如果越界了那么返回416错误码,否则读取相应片段,返回206状态码。

同时,服务器需要添加Content-Range字段,这个字段的格式根据请求头中Range字段的不同而有所差异。请求单段数据和请求多段数据,响应头是不一样的。

1
2
3
4
// 单段数据
Range: bytes=0-9
// 多段数据
Range: bytes=0-9, 30-39

单段数据

返回响应通常如下:

1
2
3
4
5
6
HTTP/1.1 206 Partial Content
Content-Length: 10
Accept-Ranges: bytes
Content-Range: bytes 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--

这个时候出现了一个非常关键的字段Content-Type:multipart/byteranges;boundary=00000010101,它代表:

  • 请求一定是多段数据请求
  • 响应体中的分隔符是 00000010101,最后的分隔末尾添上--表示结束。

8. 表单数据的提交

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

  • application/x-www-form-urlencoded
  • multipart/form-data

由于表单提交一般是POST请求,很少考虑GET,因此这里我们将默认提交的数据放在请求体中。

application/x-www-form-urlencoded

它有以下特点:

  • 其中的数据会被编码成以&分隔的键值对
  • 字符以URI编码方式编码

如:

1
2
// 转换过程: {a: 1, b: 2} -> 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--

multipart/form-data 格式最大的特点在于:每一个表单元素都是独立的资源表述

在写业务的过程中,并没有注意到其中还有boundary的存在,如果你打开抓包工具,确实可以看到不同的表单元素被拆分开了,之所以在平时感觉不到,是以为浏览器和 HTTP 给你封装了这一系列操作。

在实际的场景中,对于图片等文件的上传,基本采用multipart/form-data而不用application/x-www-form-urlencoded,因为没有必要做 URL 编码,带来巨大耗时的同时也占用了更多的空间。

9. 持久化连接与管道化

在HTTP1.0中,默认的是短连接,没有正式规定 Connection:Keep-alive 操作;HTTP/1.1所有连接都是Keep-alive的,也就是默认都是持续连接的。在事务处理结束之后仍然保持在打开状态的TCP连接称之为持久连接

HTTP/1.1允许在持久连接上可选的使用请求管道,是相对于keep-alive连接的又一性能优化。在响应到达之前,可以将多条请求放入队列,当第一条请求通过网络流向服务器时,第二条和第三条请求也可以开始发送了。在高时延网络条件下,这样做可以降低网络的环回时间,提高性能。

管道连接注意点:

  • 如果HTTP客户端无法确认连接是持久的,就不应该使用管道
  • 必须按照与请求相同的顺序回送HTTP响应。HTTP 管线化仍旧有阻塞的问题,若上一响应迟迟不回, 后面的响应都会被阻塞到。
  • HTTP客户端必须做好连接会在任意时刻关闭的准备,还要准备好重发所有未完成管道化的请求。
  • 出错的时候,管道连接会阻碍客户端了解服务器执行的是一些列管道化请求中的哪一些。由于无法安全地重试POST这样的非幂请求,所以出错时,就存在某些方法永远不会被执行的风险。

http杂谈 流程图

10. 解决队头阻塞问题

并发连接

对于一个域名允许分配多个长连接,那么相当于增加了任务队列,不至于一个队伍的任务阻塞其它所有任务。在RFC2616规定过客户端最多并发 2 个连接,不过事实上在现在的浏览器标准中,这个上限要多很多,Chrome 中是 6 个。

但其实,即使是提高了并发连接,还是不能满足人们对性能的需求。

域名分片

一个域名不是可以并发 6 个长连接吗?那我就多分几个域名。比如 content1.baidu.com 、content2.baidu.com。

这样一个baidu.com域名下可以分出非常多的二级域名,而它们都指向同样的一台服务器,能够并发的长连接数更多了,事实上也更好地解决了队头阻塞的问题。

11. HTTP 代理

引入代理之后,作为代理的服务器相当于一个中间人的角色,对于客户端而言,表现为服务器进行响应;而对于源服务器,表现为客户端发起请求,具有双重身份

那代理服务器到底是用来做什么的呢?

功能

  1. 负载均衡。客户端的请求只会先到达代理服务器,后面到底有多少源服务器,IP 都是多少,客户端是不知道的。因此,这个代理服务器可以拿到这个请求之后,可以通过特定的算法分发给不同的源服务器,让各台源服务器的负载尽量平均。当然这样的算法有很多,包括随机算法、轮询、一致性Hash、LRU等。
  2. 保障安全。利用心跳机制监控后台的服务器,一旦发现故障机就将其踢出集群。并且对于上下行的数据进行过滤,对非法 IP 限流,这些都是代理服务器的工作。
  3. 缓存代理。将内容缓存到代理服务器,使得客户端可以直接从代理服务器获得资源。

相关头部字段

Via

代理服务器需要标明自己的身份,在 HTTP 传输中留下自己的痕迹,通过Via字段来记录。比如,现在中间有两台代理服务器,在客户端发送请求后会经历这样一个过程:

1
客户端 -> 代理1 -> 代理2 -> 源服务器

在源服务器收到请求后,会在请求头拿到这个字段:

1
Via: proxy_server1, proxy_server2

而源服务器响应时,最终在客户端会拿到这样的响应头:

1
Via: proxy_server2, proxy_server1

X-Forwarded-For

字面意思就是为谁转发, 它记录的是请求方的IP地址(注意,和Via区分开,X-Forwarded-For记录的是请求方这一个IP)。

X-Forwarded-For这个字段记录的是请求方的 IP,这意味着每经过一个不同的代理,这个字段的名字都要变,从客户端代理1,这个字段是客户端的 IP,从代理1代理2,这个字段就变为了代理1的 IP。

但是这会产生两个问题:

  1. 意味着代理必须解析 HTTP 请求头,然后修改,比直接转发数据性能下降。
  2. 在 HTTPS 通信加密的过程中,原始报文是不允许修改的。

由此产生了代理协议,一般使用明文版本,只需要在 HTTP 请求行上面加上这样格式的文本即可:

1
2
3
4
// PROXY + TCP4/TCP6 + 请求方地址 + 接收方地址 + 请求端口 + 接收端口
PROXY TCP4 0.0.0.1 0.0.0.2 1111 2222
GET / HTTP/1.1
...

X-Real-IP

是一种获取用户真实 IP 的字段,不管中间经过多少代理,这个字段始终记录最初的客户端的IP。

相应的,还有X-Forwarded-Host和X-Forwarded-Proto,分别记录客户端(注意哦,不包括代理)的域名和协议名。

12. HTTP各版本的差别

HTTP/0.9

已过时。只接受GET一种请求方法,没有在通讯中指定版本号,且不支持请求头。由于该版本不支持POST方法,因此客户端无法向服务器传递太多信息

HTTP/1.0

第一个在通讯中指定版本号的HTTP协议版本,至今仍被广泛采用,特别是在代理服务器中。

  1. 默认不支持长连接,需要设置keep-alive参数指定
  2. 强缓存expired、协商缓存last-modified\if-modified-since 有一定的缺陷

HTTP/1.1

持久连接被默认采用,并能很好地配合代理服务器工作。还支持以管道方式在同时发送多个请求,以便降低线路负载,提高传输速度。

  1. 默认长连接(keep-alive),http请求可以复用TCP连接,但是同一时间只能对应一个http请求(http请求在一个TCP中是串行的)
  2. 增加了强缓存cache-control协商缓存etag\if-none-match是对http/1缓存的优化。
  3. 带宽优化以及网络连接的使用,HTTP1.0中,存在一些浪费的现象,例如客户端只是需要某个对象一部分,而服务器却将整个对象送过来了,并且不支持断点续传的功能,HTTP1.1则在请求头中引入了range头域,它允许只请求资源的某一个部分,即返回206 这样开发者方便了开发者自由选择以便充分利用带宽和链接。
  4. 错误通知的管理,在HTTP1.1中新增了24个错误状态码,例如409表示请求的资源和当前状态发生冲突;410表示服务器上的某一个资源被永久的删除。
  5. HOST头处理,在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 BadRequest)

HTTP/2.0

这部分内容过多,留到下一节单独来说。

13.HTTP/2

由于 HTTPS 在安全方面已经做的非常好了,因此HTTP 改进的关注点放在了性能方面。对于 HTTP/2 而言,它对于性能的提升主要在于两点:

  • 头部压缩
  • 多路复用

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

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

这些重大的提升本质上也是为了解决 HTTP 本身的问题而产生的。

头部压缩

当请求字段非常复杂的时候,尤其对于 GET 请求,请求报文几乎全是请求头,这个时候还是存在非常大的优化空间的。HTTP/2 针对头部字段,也采用了对应的压缩算法——HPACK,对请求头进行压缩。

它主要的亮点有两个:

  1. 首先是在服务器和客户端之间建立哈希表,将用到的字段存放在这张表中,那么在传输的时候对于之前出现过的值,只需要把索引(比如0,1,2,…)传给对方即可,对方拿到索引查表就行了。这种传索引的方式,可以说让请求头字段得到极大程度的精简和复用。

HTTP2中废除了起始行的概念,将起始行中的请求方法、URL、状态码转换为了头部字段。不过这样的头部字段都有一个:前缀,用于和其他的请求头分开。

http杂谈 流程图

  1. 其次是对整数和字符串进行哈夫曼编码,哈夫曼编码的原理就是先将所有出现的字符建立一张索引表,然后让出现次数多的字符对应的索引尽可能短,传输的时候也是传输这样的索引序列,可以达到非常高的压缩率。

HTTP2一方面使用gzip或者compressbr压缩后再发送,另一方面,客户端和服务器共同维护一张头信息表,所有字段都会存入这个表格,只要发过同样的字段,下次就不需要重新发送了,只需要发索引号,这样就能提高传输速度了。具体实现是这样的:

  • 客户端和服务器根据RFC 7541的附录A维护了维护一份共同的静态字典,其中包含了常见头部名及常见头部名称与值的组合的代码。客户端和服务端根据先入先出的原则,维护一份可动态添加内容的共同动态字典。
  • 客户端和服务端根据 RFC 7541 的附录B,支持基于该静态哈夫曼码表的哈夫曼编码。

多路复用

HTTP 队头阻塞

我们之前讨论了 HTTP 队头阻塞的问题,其根本原因在于HTTP 基于请求-响应的模型,在同一个 TCP 长连接中,前面的请求没有得到响应,后面的请求就会被阻塞。

后面我们又讨论到用并发连接域名分片的方式来解决这个问题,但这并没有真正从 HTTP 本身的层面解决问题,只是增加了 TCP 连接,分摊风险而已。而且这么做也有弊端,多条 TCP 连接会竞争有限的带宽,让真正优先级高的请求不能优先处理

HTTP/2 便从 HTTP 协议本身解决了队头阻塞问题。注意,这里并不是指的TCP队头阻塞,而是HTTP队头阻塞,两者并不是一回事。TCP 的队头阻塞是在数据包层面,单位是数据包,前一个报文没有收到便不会将后面收到的报文上传给 HTTP,而HTTP 的队头阻塞是在 HTTP 请求-响应层面,前一个请求没处理完,后面的请求就要阻塞住。两者所在的层次不一样。

那HTTP/2是如何解决所谓的队头阻塞呢?

二进制分帧

首先,HTTP/2 认为明文传输对机器而言太麻烦了,不方便计算机的解析,因为对于文本而言会有多义性的字符,比如回车换行到底是内容还是分隔符,在内部需要用到状态机去识别,效率比较低。于是 HTTP/2 干脆把报文全部换成二进制格式,全部传输01串,方便了机器的解析。接下来我们先了解几个概念:

  • 帧(Frame):HTTP/2通信的最小单位,每个帧包含帧首部,至少也会标识出当前帧所属的流。
  • 消息(Message):由一个或多个帧组合而成,例如请求和响应。
  • 连接(Connection):与 HTTP/1 相同,都是指对应的 TCP 连接。
  • 流(Stream):已建立的连接上的双向字节流。

有别于HTTP/1.1在连接中的明文请求,HTTP/2与SPDY一样,将一个TCP连接分为若干个(Stream),每个流中可以传输若干消息(Message),每个消息由若干最小的二进制帧(Frame)组成。这也是HTTP/1.1与HTTP/2最大的区别所在。

http杂谈 流程图

HTTP/2通信双方都可以给对方发送二进制帧,这种二进制帧的双向传输的序列,就是,每个请求都是一个数据流,由自己独一无二的数据流ID标识。HTTP/2用流来表示一个TCP连接上的多个数据帧的通信,这就是多路复用。

HTTP/2中,数据流以消息的形式发送,而消息由一个或多个帧组成,帧可以在数据流上乱序发送,所谓的乱序,指的是不同数据流ID的帧是乱序的,对于一个数据流而言,它的帧一定是按照顺序传输的。二进制帧到达后,对方会根据流ID把二进制帧组装成为完整的请求报文或响应报文。这个举措在SPDY中的实践表明,相比HTTP/1.1,新页面加载可以加快11.81% 到 47.7%。

二进制分帧是HTTP/2的基石,其他优化都是在这一基础上来实现的。在二进制帧中还有一些其他字段,实现了优先级和流量控制等功能。这部分内容我们在帧的结构中会介绍。

弄懂上面的内容,HTTP/2是如何解决队头阻塞的就很好理解了:

原来Headers + Body的报文格式如今被拆分成了一个个二进制的帧,用Headers帧存放头部字段,Data帧存放请求体数据。分帧之后,服务器看到的不再是一个个完整的 HTTP 请求报文,而是一堆乱序的二进制帧,这些二进制帧不存在先后关系。二进制帧到达后,对方会根据流ID把二进制帧组装成为完整的请求报文或响应报文。因此也就不会排队等待,也就没有了 HTTP 的队头阻塞问题。

http杂谈 流程图

服务器推送

网站为了使请求数减少,通常采用对页面上的图片、脚本进行极简化处理。但是,这一举措十分不方便,也不高效,依然需要诸多HTTP链接来加载页面和页面资源。

HTTP/2引入了服务器推送,即服务端向客户端发送比客户端请求更多的数据。这允许服务器直接提供浏览器渲染页面所需资源,而无须浏览器在收到、解析页面后再提起一轮请求,节约了加载时间。

因此服务器已经不再是完全被动地接收-响应请求,它也能新建 stream 来给客户端发送消息,当 TCP 连接建立之后,比如浏览器请求一个 HTML 文件,服务器就可以在返回 HTML 的基础上,将 HTML 中引用到的其他资源文件(CSS、JavaScript、图片等)一起返回给客户端,减少客户端的加载时间,提高页面的渲染速度。

http杂谈 流程图

14. HTTP/2二进制帧

帧结构

06-10.png

每个帧分为帧头帧体。先是3个字节的帧长度,这个长度表示的是帧体的长度。

然后是帧类型,大概可以分为数据帧控制帧两种。数据帧用来存放 HTTP 报文,控制帧用来管理流的传输。

接下来的一个字节是帧标志,里面一共有 8 个标志位,常用的有END_HEADERS表示头数据结束,END_STREAM表示单方向数据发送结束。

后 4 个字节是Stream ID, 也就是流标识符,有了它,接收方就能从乱序的二进制帧中选择出 ID 相同的帧,按顺序组装成请求/响应报文。流的状态变化。

HTTP/2 中,所谓的流,其实就是二进制帧的双向传输的序列。那么在 HTTP/2 请求和响应的过程中,流的状态是如何变化的呢?

流的状态

HTTP/2 其实也是借鉴了 TCP 状态变化的思想,根据帧的标志位来实现具体的状态改变。这里我们以一个普通的请求-响应过程为例来说明:

http杂谈 流程图

  1. 最开始两者都是空闲状态,当客户端发送Headers帧后,开始分配Stream ID, 此时客户端的流打开。服务端接收Header帧之后流也打开,两端的流都打开之后,就可以互相传递数据帧和控制帧了。
  2. 当客户端要关闭时,向服务端发送END_STREAM帧,进入半关闭状态, 这个时候客户端只能接收数据,而不能发送数据。
  3. 服务端收到这个END_STREAM帧后也进入半关闭状态,不过此时服务端的情况是只能发送数据,而不能接收数据。随后服务端也向客户端发送END_STREAM帧,表示数据发送完毕,双方进入关闭状态。

如果下次要开启新的流,流 ID 需要自增,直到上限为止,到达上限后开一个新的 TCP 连接重头开始计数。由于流 ID 字段长度为 4 个字节,最高位又被保留,因此范围是 0 ~ 2的 31 次方,大约 21 亿个。

流的特性

  1. 并发性。一个 HTTP/2 连接上可以同时发多个帧,这一点和 HTTP/1 不同。这也是实现多路复用的基础。
  2. 自增性。流 ID 是不可重用的,而是会按顺序递增,达到上限之后又新开 TCP 连接从头开始。
  3. 双向性。客户端和服务端都可以创建流,互不干扰,双方都可以作为发送方或者接收方。
  4. 可设置优先级。可以设置数据帧的优先级,让服务端先处理重要资源,优化用户体验。

14. 状态码301和302的区别

服务器给浏览器返回一个状态码未301或302的重定向响应后,客户端会自动跳转到Location字段指明的URL,需要的信息都在HTTP头部,响应体一般是空的。

而对于SEO来说,如果一个页面有两个地址,就像http://www.yy.comhttp://yy.com,搜素引擎会认为他们是两个网站,结果会造成每个搜索链接都减少而降低网站排名。如果使用了301永久重定向,搜索引擎就会把这两个访问地址归到同一个网站排名下。针对SEO,301优于302

但是使用不同的地址会造成缓存友好型变差,当一个页面有好几个名字的时候,它可能被缓存好几次。

301

表示旧的地址的资源已经被永久的移除了,搜索引擎在抓取新内容的时候也将旧的网站替换为重定向之后的网站。

使用场景

  1. 之前的网站因为某些原因需要移除掉,然后要到新的地址访问,是永久性的。
  2. 域名到期不想续费或者发现了更适合网站的域名,想换个域名。
  3. 空间服务器不稳定,换空间的时候。
  4. 在搜索引擎的搜索结果中出现了不带www的域名,而带www的域名没有被收录,这个时候可以用重定向告诉搜索引擎目标域名是哪一个。

302

表示旧的地址资源还在,这个重定向只是暂时地从旧网址跳转到新网址,搜索引擎会抓取新的内容而保存旧的网址

使用场景:当一个网站或者网页24-48小时内临时移动到一个新的位置。

替代方案

  1. 如果路径指向同一台服务器,可以使用Alias或者mod_rewrite
  2. 如果是域名的变化,可以创建一条CNAME的DNS记录,结合Alias或者mod_rewrite指令。

15. 接口如何防刷

  1. 网关控制流量洪峰,对在一个时间段内出现流量异常,可以拒绝请求
  2. IP 请求个数限制。对请求来源的 IP 请求个数做限制
  3. HTTP请求头信息校验;例如 hostUser-AgentReferer
  4. 对用户唯一身份uid进行限制和校验,例如基本的长度,组合方式,甚至有效性进行判断。或者uid具有一定的时效性
  5. 前后端协议采用二进制方式进行交互或者协议采用签名机制
  6. 人机验证,验证码,短信验证码,滑动图片形式,12306形式

16. 为什么HTTP1.1不能实现多路复用

HTTP/1.1 不是二进制传输,而是通过文本进行传输。 由于没有流的概念,在使用并行传输(多路复用)传递数据时,接收端在接收到响应后,并不能区分多个响应分别对应的请求,所以无法将多个响应的结果重新进行组装,也就实现不了多路复用。