1.HTTP报文结构
对于 TCP 而言,在传输的时候分为两个部分:TCP头和数据部分。
而 HTTP 类似,也是header + body的结构,具体而言:
起始行 + 头部 + 空行 + 实体
由于 http 请求报文和响应报文是有一定区别,因此我们分开介绍。
1.1 起始行
对于请求报文来说,起始行类似下面这样:
GET /home HTTP/1.1
也就是方法 + 路径 + http版本。
对于响应报文来说,起始行一般张这个样:
HTTP/1.1 200 OK
响应报文的起始行也叫做状态行。由http版本、状态码和原因三部分组成。
值得注意的是,在起始行中,每两个部分之间用空格隔开,最后一个部分后面应该接一个换行,严格遵循ABNF语法规范。
1.2 头部


不管是请求头还是响应头,其中的字段是相当多的,而且牵扯到http非常多的特性,这里就不一一列举的,重点看看这些头部字段的格式:
- 字段名不区分大小写
- 字段名不允许出现空格,不可以出现下划线
_ - 字段名后面必须紧接着:
1.3 空行
很重要,用来区分开头部和实体。
问: 如果说在头部中间故意加一个空行会怎么样?
那么空行后的内容全部被视为实体。1.4 实体
就是具体的数据了,也就是body部分。请求报文对应请求体, 响应报文对应响应体。2.HTTP的请求方法
2.1 HTTP 0.9
GET2.3 HTTP 1.0
GET POST HEAD2.4 HTTP 1.1
- GET: 通常用来获取资源
- HEAD: 获取资源的元信息
- POST: 提交数据,即上传数据
- PUT: 修改数据
- DELETE: 删除资源(几乎用不到)
- CONNECT: 建立连接隧道,用于代理服务器
- OPTIONS: 列出可对资源实行的请求方法,用来跨域请求
- TRACE: 追踪请求-响应的传输路径
2.5 GET和POST有什么区别
首先最直观的是语义上的区别。
而后又有这样一些具体的差别:
- 从缓存的角度,GET 请求会被浏览器主动缓存下来,留下历史记录,而 POST 默认不会。
- 从编码的角度,GET 只能进行 URL 编码,只能接收 ASCII 字符,而 POST 没有限制。
- 从参数的角度,GET 一般放在 URL 中,因此不安全,POST 放在请求体中,更适合传输敏感信息。GET传递的数据大小会有限制,因为URL的长度有限制,POST数据的数据大小没有限制。
- 从幂等性的角度,
GET是幂等的,而POST不是。(幂等表示执行相同的操作,结果也是相同的)一般 Get 请求用于对服务器资源不会产生影响的场景,比如说请求一个网页。而 Post 不是一个幂等的请求,一般用于对服务器资源会产生影响的情景。比如注册用户这一类的操作。 - 从TCP的角度,GET 请求会把请求报文一次性发出去,而 POST 会分为两个 TCP 数据包,首先发 header 部分,如果服务器响应 100(continue), 然后发 body 部分。(火狐浏览器除外,它的 POST 请求只发一个 TCP 包)
2.6 http 请求方法 options 方法有什么用?
OPTIONS 请求与 HEAD 类似,一般也是用于客户端查看服务器的性能。这个方法会请求服务器返回该资源所支持的所有 HTTP 请求方法,该方法会用’*’来代替资源名称,向服务器发送 OPTIONS 请求,可以测试服务器功能是否正常。JS 的 XMLHttpRequest对象进行 CORS 跨域资源共享时,对于复杂请求,就是使用 OPTIONS 方法发送嗅探请求,以判断是否有对指定资源的访问权限。3.如何理解URI
URI, 全称为(Uniform Resource Identifier), 也就是统一资源标识符,它的作用很简单,就是区分互联网上不同的资源。
但是,它并不是我们常说的网址, 网址指的是URL, 实际上URI包含了URN和URL两个部分,由于 URL 过于普及,就默认将 URI 视为 URL 了3.1 URI的结构
URI 真正最完整的结构是这样的。

可能你会有疑问,好像跟平时见到的不太一样啊!先别急,我们来一一拆解。
scheme 表示协议名,比如http, https, file等等。后面必须和://连在一起。
user:passwd@ 表示登录主机时的用户信息,不过很不安全,不推荐使用,也不常用。
host:port表示主机名和端口。
path表示请求路径,标记资源所在位置。
query表示查询参数,为key=val这种形式,多个键值对之间用&隔开。
fragment表示 URI 所定位的资源内的一个锚点,浏览器可以根据这个锚点跳转到对应的位置。
举个例子:
https://www.baidu.com/s?wd=HTTP&rsv_spt=1
这个 URI 中,https即scheme部分,www.baidu.com为host:port部分(注意,http 和 https 的默认端口分别为80、443),/s为path部分,而wd=HTTP&rsv_spt=1就是query部分。
3.2 URI编码
URI 只能使用ASCII, ASCII 之外的字符是不支持显示的,而且还有一部分符号是界定符,如果不加以处理就会导致解析出错。
因此,URI 引入了编码机制,将所有非 ASCII 码字符和界定符转为十六进制字节值,然后在前面加个%
4.HTTP状态码
RFC 规定 HTTP 的状态码为三位数,被分为五类:
- 1xx: 表示目前是协议处理的中间状态,还需要后续操作。
- 2xx: 表示成功状态。
- 3xx: 重定向状态,资源位置发生变动,需要重新请求。
- 4xx: 请求报文有误。
- 5xx: 服务器端发生错误。
4.1 1XX
101 Switching Protocols。在HTTP升级为WebSocket的时候,如果服务器同意变更,就会发送状态码 101。
4.2 2XX
200 OK是见得最多的成功状态码。通常在响应体中放有数据。
204 No Content含义与 200 相同,但响应头后没有 body 数据。
206 Partial Content顾名思义,表示部分内容,它的使用场景为 HTTP 分块下载和断电续传,当然也会带上相应的响应头字段Content-Range
4.3 3XX
301 Moved Permanently即永久重定向,对应着
302 Found,即临时重定向。
比如你的网站从 HTTP 升级到了 HTTPS 了,以前的站点再也不用了,应当返回301,这个时候浏览器默认会做缓存优化,在第二次访问的时候自动访问重定向的那个地址。
而如果只是暂时不可用,那么直接返回302即可,和301不同的是,浏览器并不会做缓存优化。
303 see other,表示资源存在着另一个 URL,应使用 GET 方法获取资源。
304 Not Modified: 当协商缓存命中时会返回这个状态码。详见浏览器缓存
307 temporary redirect,临时重定向,和302含义相同,不会改变method
4.4 4XX
400 bad request,请求报文存在语法错误。
401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息。
403 forbidden,表示对请求资源的访问被服务器拒绝。
404 not found,表示在服务器上没有找到请求的资源。
405 Method Not Allowed,服务器禁止使用该方法,客户端可以通过options方法来查看服务器允许的访问方法,
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请求头的字段内容太大。
4.5 5XX
500 Internal Server Error: 表示服务器端在执行请求时发生了错误,也有可能是 Web应用存在的 bug 或某些临时的故障。
501 Not Implemented: 表示客户端请求的功能还不支持。
502 Bad Gateway: 作为代理或网关使用的服务器从请求响应链的下一条链路上收到了一条伪响应。
503 Service Unavailable: 表示服务器当前很忙,暂时无法响应服务。
5.HTTP的特点
5.1 灵活可扩展
主要体现在两个方面。一个是语义上的自由,只规定了基本格式,比如空格分隔单词,换行分隔字段,其他的各个部分都没有严格的语法限制。另一个是传输形式的多样性,不仅仅可以传输文本,还能传输图片、视频等任意数据,非常方便。
5.2 可靠传输
可靠传输。HTTP 基于 TCP/IP,因此把这一特性继承了下来。这属于 TCP 的特性,不具体介绍了。
5.3 请求-应答模式
请求-应答。也就是一发一收、有来有回, 当然这个请求方和应答方不单单指客户端和服务器之间,如果某台服务器作为代理来连接后端的服务端,那么这台服务器也会扮演请求方的角色。
5.4 无状态
这里的状态是指通信过程的上下文信息,而每次 http 请求都是独立、无关的,默认不需要保留状态信息。这是为了更快地处理大量事务,确保协议的可伸缩性,而特意把 HTTP 协议设计成如此简单的。
6.HTTP的缺点
6.1 无状态
所谓的优点和缺点还是要分场景来看的,对于 HTTP 而言,最具争议的地方在于它的无状态。
在需要长连接的场景中,需要保存大量的上下文信息,以免传输大量重复的信息,那么这时候无状态就是 http 的缺点了。
但与此同时,另外一些应用仅仅只是为了获取一些数据,不需要保存连接上下文信息,无状态反而减少了网络开销,成为了 http 的优点
6.2 明文传输
即协议里的报文(主要指的是头部)不使用二进制数据,而是文本形式。
这当然对于调试提供了便利,但同时也让 HTTP 的报文信息暴露给了外界,给攻击者也提供了便利。WIFI陷阱就是利用 HTTP 明文传输的缺点,诱导你连上热点,然后疯狂抓你所有的流量,从而拿到你的敏感信息。按TCP/IP 协议族的工作机制,通信内容在所有的通信线路上都有可能遭到窥视。即使已经过加密处理的通信,也会被窥视到通信内容,这点和未加密的通信是相同的。
6.3 不验证通信方身份
不验证通信方的身份,因此有可能遭遇伪装
任何人都可发起请求
HTTP 协议的实现本身非常简单,不论是谁发送过来的请求都会返回响应,因此不确认通信方,会存在以下各种隐患。
无法确定请求发送至目标的 Web 服务器是否是按真实意图返回响应的那台服务器。有可能是已伪装的 Web 服务器。
无法确定响应返回到的客户端是否是按真实意图接收响应的那个客户端。有可能是已伪装的客户端。
无法确定正在通信的对方是否具备访问权限。因为某些Web 服务器上保存着重要的信息,只想发给特定用户通信的权限。
无法判定请求是来自何方、出自谁手。
即使是无意义的请求也会照单全收。无法阻止海量请求下的 DoS 攻击(Denial of Service,拒绝服务攻击)。
6.4 无法验证报文完整性
无法证明报文的完整性,所以有可能已遭篡改
接收到的内容可能有误
由于 HTTP 协议无法证明通信的报文完整性,因此,在请求或响应送出之后直到对方接收之前的这段时间内,即使请求或响应的内容遭到篡改,也没有办法获悉。没有任何办法确认,发出的请求 / 响应和接收到的请求 / 响应是前后相同的。
中间人攻击
请求或响应在传输途中,遭攻击者拦截并篡改内容的攻击
6.5 队头堵塞问题
当 http 开启长连接时,共用一个 TCP 连接,同一时刻只能处理一个请求,那么当前请求耗时过长的情况下,其它的请求只能处于阻塞状态,也就是著名的队头阻塞问题。
7.定长和不定长的数据,HTTP是如何传输的
7.1 定长包体
对于定长包体而言,发送端在传输的时候一般会带上 Content-Length, 来指明包体的长度。
如果把这个长度设置的小一些,多出的响应体会被截掉,如果长度大一些会导致页面传输失败。可以看到Content-Length对于 http 传输过程起到了十分关键的作用,如果设置不当可以直接导致传输失败。
7.2 不定长包体
上述是针对于定长包体,那么对于不定长包体而言是如何传输的呢?
这里就必须介绍另外一个 http 头部字段了:
Transfer-Encoding: chunked
表示分块传输数据,设置这个字段后会自动产生两个效果:
- Content-Length 字段会被忽略
-
8.HTTP如何处理大文件的传输
对于几百 M 甚至上 G 的大文件来说,如果要一口气全部传输过来显然是不现实的,会有大量的等待时间,严重影响用户体验。因此,HTTP 针对这一场景,采取了
范围请求的解决方案,允许客户端仅仅请求一个资源的一部分。8.1 如何支持
当然,前提是服务器要支持范围请求,要支持这个功能,就必须加上这样一个响应头:
Accept-Ranges: none
8.2 Range字段拆解
而对于客户端而言,它需要指定请求哪一部分,通过
Range这个请求头字段确定,格式为bytes=x-y。接下来就来讨论一下这个 Range 的书写格式: 0-499表示从开始到第 499 个字节。
- 500- 表示从第 500 字节到文件终点。
- -100表示文件的最后100个字节。
服务器收到请求之后,首先验证范围是否合法,如果越界了那么返回416错误码,否则读取相应片段,返回206状态码。
同时,服务器需要添加Content-Range字段,这个字段的格式根据请求头中Range字段的不同而有所差异。
具体来说,请求单段数据和请求多段数据,响应头是不一样的。
举个例子:
// 单段数据Range: bytes=0-9// 多段数据Range: bytes=0-9, 30-39
8.3 单段数据
对于单段数据的请求,返回的响应如下:
HTTP/1.1 206 Partial ContentContent-Length: 10Accept-Ranges: bytesContent-Range: bytes 0-9/100i am xxxxx
值得注意的是Content-Range字段,0-9表示请求的返回,100表示资源的总大小,很好理解。
8.4 多段数据
接下来我们看看多段请求的情况。得到的响应会是下面这个形式:
HTTP/1.1 206 Partial ContentContent-Type: multipart/byteranges; boundary=00000010101Content-Length: 189Connection: keep-aliveAccept-Ranges: bytes--00000010101Content-Type: text/plainContent-Range: bytes 0-9/96i am xxxxx--00000010101Content-Type: text/plainContent-Range: bytes 20-29/96eex jspy e--00000010101--
这个时候出现了一个非常关键的字段Content-Type: multipart/byteranges;boundary=00000010101,它代表了信息量是这样的:
- 请求一定是多段数据请求
- 响应体中的分隔符是 00000010101
因此,在响应体中各段数据之间会由这里指定的分隔符分开,而且在最后的分隔末尾添上--表示结束。
以上就是 http 针对大文件传输所采用的手段。
9.HTTP中如何处理表单数据的提交
在 http 中,有两种主要的表单提交的方式,体现在两种不同的Content-Type取值:
- application/x-www-form-urlencoded
- multipart/form-data
由于表单提交一般是POST请求,很少考虑GET,因此这里我们将默认提交的数据放在请求体中。
9.1 application/x-www-form-urlencoded
对于application/x-www-form-urlencoded格式的表单内容,有以下特点:
- 其中的数据会被编码成以
&分隔的键值对 - 字符以URL编码方式编码。
如:
// 转换过程: {a: 1, b: 2} -> a=1&b=2 -> 如下(最终形式)"a%3D1%26b%3D2"
9.2 multipart/form-data
对于multipart/form-data而言:
- 请求头中的
Content-Type字段会包含boundary,且boundary的值有浏览器默认指定。例:Content-Type: multipart/form-data;boundary=----WebkitFormBoundaryRRJKeWfHPGrS4LKe。 - 数据会分为多个部分,每两个部分之间通过分隔符来分隔,每部分表述均有 HTTP 头部描述子包体,如
Content-Type,在最后的分隔符会加上--表示结束。
相应的请求体是下面这样
Content-Disposition: form-data;name="data1";Content-Type: text/plaindata1----WebkitFormBoundaryRRJKeWfHPGrS4LKeContent-Disposition: form-data;name="data2";Content-Type: text/plaindata2----WebkitFormBoundaryRRJKeWfHPGrS4LKe--
值得一提的是,multipart/form-data 格式最大的特点在于:每一个表单元素都是独立的资源表述。另外,你可能在写业务的过程中,并没有注意到其中还有boundary的存在,如果你打开抓包工具,确实可以看到不同的表单元素被拆分开了,之所以在平时感觉不到,是因为浏览器和 HTTP 给你封装了这一系列操作。
而且,在实际的场景中,对于图片等文件的上传,基本采用multipart/form-data而不用application/x-www-form-urlencoded,因为没有必要做 URL 编码,带来巨大耗时的同时也占用了更多的空间。
10.如何解决HTTP的队头堵塞问题
10.1 队头堵塞
从前面的小节可以知道,HTTP 传输是基于请求-应答的模式进行的,报文必须是一发一收,但值得注意的是,里面的任务被放在一个任务队列中串行执行,一旦队首的请求处理太慢,就会阻塞后面请求的处理。这就是著名的HTTP队头阻塞问题。
10.2 解决
并发连接
对于一个域名允许分配多个长连接,那么相当于增加了任务队列,不至于一个队伍的任务阻塞其它所有任务。在RFC2616规定过客户端最多并发 2 个连接,不过事实上在现在的浏览器标准中,这个上限要多很多,Chrome 中是 6 个。
但其实,即使是提高了并发连接,还是不能满足人们对性能的需求
域名分片
一个域名不是可以并发 6 个长连接吗?那我就多分几个域名。
比如 content1.sanyuan.com 、content2.sanyuan.com。
这样一个sanyuan.com域名下可以分出非常多的二级域名,而它们都指向同样的一台服务器,能够并发的长连接数更多了,事实上也更好地解决了队头阻塞的问题。
11.Cookie
11.1 简介
前面说到了 HTTP 是一个无状态的协议,每次 http 请求都是独立、无关的,默认不需要保留状态信息。但有时候需要保存一些状态,怎么办呢?
HTTP 为此引入了 Cookie。Cookie 本质上就是浏览器里面存储的一个很小的文本文件,内部以键值对的方式来存储(在chrome开发者面板的Application这一栏可以看到)。向同一个域名下发送请求,都会携带相同的 Cookie,服务器拿到 Cookie 进行解析,便能拿到客户端的状态。而服务端可以通过响应头中的Set-Cookie字段来对客户端写入Cookie
// 请求头Cookie: a=xxx;b=xxx// 响应头Set-Cookie: a=xxxset-Cookie: b=xxx
11.2 Cookie的属性
设置过期时间
Cookie 的有效期可以通过Expires和Max-Age两个属性来设置。
- Expires即
过期时间 - Max-Age用的是一段时间间隔,单位是秒,从浏览器收到报文开始计算。
若 Cookie 过期,则这个 Cookie 会被删除,并不会发送给服务端。
作用域
关于作用域也有两个属性: Domain和path, 给 Cookie 绑定了域名和路径,在发送请求之前,发现域名或者路径和这两个属性不匹配,那么就不会带上 Cookie。值得注意的是,对于路径来说,/表示域名下的任意路径都允许使用 Cookie。
安全相关
如果 cookie 字段带上HttpOnly,那么说明只能通过 HTTP 协议传输,不能通过 JS 访问,这也是预防 XSS 攻击的重要手段。
相应的,对于 CSRF 攻击的预防,也有SameSite属性。SameSite可以设置为三个值,Strict、Lax和None。
a. 在Strict模式下,浏览器完全禁止第三方请求携带Cookie。比如请求sanyuan.com网站只能在sanyuan.com域名当中请求才能携带 Cookie,在其他网站请求都不能。
b. 在Lax模式,就宽松一点了,但是只能在 get 方法提交表单况或者a 标签发送 get 请求的情况下可以携带 Cookie,其他情况均不能。
c. 在None模式下,也就是默认模式,请求会自动携带上 Cookie。
11.3 Cookie缺陷
- 容量缺陷。Cookie 的体积上限只有
4KB,只能用来存储少量的信息。 - 性能缺陷。Cookie 紧跟域名,不管域名下面的某一个地址需不需要这个 Cookie ,请求都会携带上完整的 Cookie,这样随着请求数的增多,其实会造成巨大的性能浪费的,因为请求携带了很多不必要的内容。但可以通过
Domain和Path指定作用域来解决。 安全缺陷。由于 Cookie 以纯文本的形式在浏览器和服务器中传递,很容易被非法用户截获,然后进行一系列的篡改,在 Cookie 的有效期内重新发送给服务器,这是相当危险的。另外,在
HttpOnly为 false 的情况下,Cookie 信息能直接通过 JS 脚本来读取。
12.HTTP代理
我们知道在 HTTP 是基于
请求-响应模型的协议,一般由客户端发请求,服务器来进行响应。
当然,也有特殊情况,就是代理服务器的情况。引入代理之后,作为代理的服务器相当于一个中间人的角色,对于客户端而言,表现为服务器进行响应;而对于源服务器,表现为客户端发起请求,具有双重身份。
那代理服务器到底是用来做什么的呢?12.1 功能
负载均衡。客户端的请求只会先到达代理服务器,后面到底有多少源服务器,IP 都是多少,客户端是不知道的。因此,这个代理服务器可以拿到这个请求之后,可以通过特定的算法分发给不同的源服务器,让各台源服务器的负载尽量平均。当然,这样的算法有很多,包括随机算法、轮询、一致性hash、LRU
(最近最少使用)等等,不过这些算法并不是本文的重点,大家有兴趣自己可以研究一下。- 保障安全。利用心跳机制监控后台的服务器,一旦发现故障机就将其踢出集群。并且对于上下行的数据进行过滤,对非法 IP 限流,这些都是代理服务器的工作。
缓存代理。将内容缓存到代理服务器,使得客户端可以直接从代理服务器获得而不用到源服务器那里。
12.2 相关头部字段
Via
代理服务器需要标明自己的身份,在 HTTP 传输中留下自己的痕迹,怎么办呢?
通过Via字段来记录。举个例子,现在中间有两台代理服务器,在客户端发送请求后会经历这样一个过程:客户端 -> 代理1 -> 代理2 -> 源服务器
在源服务器收到请求后,会在
请求头拿到这个字段:Via: proxy_server1, proxy_server2
而源服务器响应时,最终在客户端会拿到这样的
响应头:Via: proxy_server2, proxy_server1
可以看到,
Via中代理的顺序即为在 HTTP 传输中报文传达的顺序。
X-Forwarded-For
字面意思就是为谁转发, 它记录的是请求方的IP地址(注意,和Via区分开,X-Forwarded-For记录的是请求方这一个IP)。
X-Real-IP
是一种获取用户真实 IP 的字段,不管中间经过多少代理,这个字段始终记录最初的客户端的IP。
相应的,还有X-Forwarded-Host和X-Forwarded-Proto,分别记录客户端(注意哦,不包括代理)的域名和协议名。12.3 X-Forwarded-For产生的问题
前面可以看到,
X-Forwarded-For这个字段记录的是请求方的 IP,这意味着每经过一个不同的代理,这个字段的名字都要变,从客户端到代理1,这个字段是客户端的 IP,从代理1到代理2,这个字段就变为了代理1的 IP。
但是这会产生两个问题:意味着代理必须解析 HTTP 请求头,然后修改,比直接转发数据性能下降。
- 在 HTTPS 通信加密的过程中,原始报文是不允许修改的。
由此产生了代理协议,一般使用明文版本,只需要在 HTTP 请求行上面加上这样格式的文本即可:
// PROXY + TCP4/TCP6 + 请求方地址 + 接收方地址 + 请求端口 + 接收端口PROXY TCP4 0.0.0.1 0.0.0.2 1111 2222GET / HTTP/1.1...
13.HTTP缓存代理
13.1 作用
对于源服务器来说,它也是有缓存的,比如Redis, Memcache,但对于 HTTP 缓存来说,如果每次客户端缓存失效都要到源服务器获取,那给源服务器的压力是很大的。
由此引入了缓存代理的机制。让代理服务器接管一部分的服务端HTTP缓存,客户端缓存过期后就近到代理缓存中获取,代理缓存过期了才请求源服务器,这样流量巨大的时候能明显降低源服务器的压力。
那缓存代理究竟是如何做到的呢?
总的来说,缓存代理的控制分为两部分,一部分是源服务器端的控制,一部分是客户端的控制。
13.2 源服务器的缓存控制
private 和 public
在源服务器的响应头中,会加上Cache-Control这个字段进行缓存控制字段,那么它的值当中可以加入private或者public表示是否允许代理服务器缓存,前者禁止,后者为允许。
比如对于一些非常私密的数据,如果缓存到代理服务器,别人直接访问代理就可以拿到这些数据,是非常危险的,因此对于这些数据一般是不会允许代理服务器进行缓存的,将响应头部的Cache-Control设为private,而不是public。
proxy-revalidatemust-revalidate的意思是客户端缓存过期就去源服务器获取,而proxy-revalidate则表示代理服务器的缓存过期后到源服务器获取。
s-maxages是share的意思,限定了缓存在代理服务器中可以存放多久,和限制客户端缓存时间的max-age并不冲突。
讲了这几个字段,我们不妨来举个小例子,源服务器在响应头中加入这样一个字段:
Cache-Control: public, max-age=1000, s-maxage=2000
相当于源服务器说: 我这个响应是允许代理服务器缓存的,客户端缓存过期了到代理中拿,并且在客户端的缓存时间为 1000 秒,在代理服务器中的缓存时间为 2000 s。
13.3客户端的缓存控制
ax-stale 和 min-fresh
在客户端的请求头中,可以加入这两个字段,来对代理服务器上的缓存进行宽容和限制操作。比如:
max-stale: 5
表示客户端到代理服务器上拿缓存的时候,即使代理缓存过期了也不要紧,只要过期时间在5秒之内,还是可以从代理中获取的。
又比如:
min-fresh: 5
表示代理缓存需要一定的新鲜度,不要等到缓存刚好到期再拿,一定要在到期前 5 秒之前的时间拿,否则拿不到。
only-if-cached
这个字段加上后表示客户端只会接受代理缓存,而不会接受源服务器的响应。如果代理缓存无效,则直接返回504(Gateway Timeout)。
14.HTTP0.9/HTTP1.0/HTTP1.1/HTTP2/HTTP3的区别
14.1 HTTP0.9
1991年,原型版本,功能简陋,只有一个命令GET,只支持纯文本内容,该版本已过时。
14.2 HTTP1.0
- 任何格式的内容都可以发送,这使得互联网不仅可以传输文字,还能传输图像、视频、二进制等文件。
- 除了GET命令,还引入了POST命令和HEAD命令。
- http请求和回应的格式改变,除了数据部分,每次通信都必须包括头信息(HTTP header),用来描述一些元数据。
- 只使用 header 中的 If-Modified-Since 和 Expires 作为缓存失效的标准。
- 不支持断点续传,也就是说,每次都会传送全部的页面和数据。
通常每台计算机只能绑定一个 IP,所以请求消息中的 URL 并没有传递主机名(hostname)
14.3 HTTP1.1
http1.1是目前最为主流的http协议版本,从1999年发布至今,仍是主流的http协议版本。
引入了持久连接( persistent connection),即TCP连接默认不关闭,可以被多个请求复用,不用声明Connection: keep-alive。长连接的连接时长可以通过请求头中的
keep-alive来设置,http1.1 默认使用持久连接- 引入了管道机制( pipelining),即在同一个TCP连接里,客户端可以同时发送多个 请求,进一步改进了HTTP协议的效率。
- HTTP 1.1 中新增加了 E-tag,If-Unmodified-Since, If-Match, If-None-Match 等缓存控制标头来控制缓存失效。
- 支持断点续传,通过使用请求头中的
Range来实现。带宽优化以及网络连接的使用:在HTTP1.0中,存在一些浪费带宽的现象,例如客户端只需要某个对象的一部分,而服务器却将整个对象送过来了,它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。 - 使用了虚拟网络,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。HOST头处理,在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名。HTTP1.1的请求消息和响应消息 都支持HOST头域,且请求消息如果没有HOST头域会报告一个错误(400 Bad Request)
- 新增方法:PUT、 PATCH、 OPTIONS、 DELETE。
错误通知的管理:在HTTP1.1中新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上某个资源被永久性的删除。
14.4 SPDY:HTTP1.X的优化
降低延迟:针对HTTP高延迟的问题,SPDY优雅的采取了多路复用。多路复用通过多个请求stream共享一个tcp连接的方式,解决了浏览器阻塞的问题,降低了延迟同时提高了带宽的利用率。
- 请求优先级: 多路复用带来一个新的问题就是,在连接共享的基础上有可能导致关键请求被阻塞。SPDY允许给每个request设置优先级,这样重要的请求就会优先得到响应。比如浏览器加载首页,首页的html内容应该优先展示,之后才是各种静态资源文件、脚本文件加载,这样可以保证用户第一时间看到网页内容。
- header压缩:前面提到HTTP1.X的header很多时候都是重复多余的。选择合适的压缩算法可以减少包的大小和数量。
- 基于HTTPS的加密协议进行传输:大大提高了传输数据的可靠性。
- 服务端推送:采用了SPDY的网页,例如我的网页有一个style.css的请求,在客户端收到style.css数据的同时,服务端会将style.js的文件推送给客户端,当客户端再次尝试获取style.js时就可以直接从缓存中得到,不用再发请求了SPDY构成图:

SPDY位于HTTP之下,TCP和SSL之上,这样可以轻松兼容老版本的HTTP协议(将HTTP1.X的内容封装成一种新的frame格式),同时可以使用已有的SSL功能。
14.5 HTTP2.0
二进制分帧这是一次彻底的二进制协议,头信息和数据体都是二进制,并且统称为”帧”:头信息帧和数据帧。头部压缩HTTP 1.1版本会出现 「User-Agent、Cookie、Accept、Server、Range」 等字段可能会占用几百甚至几千字节,而 Body 却经常只有几十字节,所以导致头部偏重。HTTP 2.0 使用HPACK算法进行压缩。多路复用复用TCP连接,在一个连接里,客户端和浏览器都可以同时发送多个请求或回应,且不用按顺序一一对应,这样子解决了队头阻塞的问题。服务器推送允许服务器未经请求,主动向客户端发送资源,即服务器推送。请求优先级可以设置数据帧的优先级,让服务端先处理重要资源,优化用户体验14.6 SPDY和HTTP2.0的区别
HTTP2.0支持明文HTTP传输、而SPDY强制使用HTTPS
HTTP2.0消息头的压缩算法采用 HPACK http://http2.github.io/http2-spec/compression.html,而非 SPDY 采用的 DEFLATE http://zh.wikipedia.org/wiki/DEFLATE
14.7 HTTP3
虽然 HTTP/2 解决了很多之前旧版本的问题,但是它还是存在一个巨大的问题,虽然这个问题并不是它本身造成的,而是底层支撑的 TCP 协议的问题。
因为 HTTP/2 使用了多路复用,一般来说同一域名下只需要使用一个 TCP 连接。当这个连接中出现了丢包的情况,那就会导致 HTTP/2 的表现情况反倒不如 HTTP/1 了。
因为在出现丢包的情况下,整个 TCP 都要开始等待重传,也就导致了后面的所有数据都被阻塞了。但是对于 HTTP/1 来说,可以开启多个 TCP 连接,出现这种情况反到只会影响其中一个连接,剩余的 TCP 连接还可以正常传输数据。
那么可能就会有人考虑到去修改 TCP 协议,其实这已经是一件不可能完成的任务了。因为 TCP 存在的时间实在太长,已经充斥在各种设备中,并且这个协议是由操作系统实现的,更新起来不大现实。
基于这个原因,Google 就更起炉灶搞了一个基于 UDP 协议的 QUIC 协议,并且使用在了 HTTP/3 上,当然 HTTP/3 之前名为 HTTP-over-QUIC,从这个名字中我们也可以发现,HTTP/3 最大的改造就是使用了 QUIC,接下来我们就来学习关于这个协议的内容。QUIC
之前我们学习过 UDP 协议的内容,知道这个协议虽然效率很高,但是并不是那么的可靠。QUIC 虽然基于 UDP,但是在原本的基础上新增了很多功能,比如多路复用、0-RTT、使用 TLS1.3 加密、流量控制、有序交付、重传等等功能。这里我们就挑选几个重要的功能学习下这个协议的内容。
多路复用
虽然 HTTP/2 支持了多路复用,但是 TCP 协议终究是没有这个功能的。QUIC 原生就实现了这个功能,并且传输的单个数据流可以保证有序交付且不会影响其他的数据流,这样的技术就解决了之前 TCP 存在的问题。
并且 QUIC 在移动端的表现也会比 TCP 好。因为 TCP 是基于 IP 和端口去识别连接的,这种方式在多变的移动端网络环境下是很脆弱的。但是 QUIC 是通过 ID 的方式去识别一个连接,不管你网络环境如何变化,只要 ID 不变,就能迅速重连上。
0-RTT
通过使用类似 TCP 快速打开的技术,缓存当前会话的上下文,在下次恢复会话的时候,只需要将之前的缓存传递给服务端验证通过就可以进行传输了。
纠错机制
假如说这次我要发送三个包,那么协议会算出这三个包的异或值并单独发出一个校验包,也就是总共发出了四个包。
当出现其中的非校验包丢包的情况时,可以通过另外三个包计算出丢失的数据包的内容。
当然这种技术只能使用在丢失一个包的情况下,如果出现丢失多个包就不能使用纠错机制了,只能使用重传的方式了。15.HTTP2中改进详解
由于 HTTPS 在安全方面已经做的非常好了,HTTP 改进的关注点放在了性能方面。对于 HTTP/2 而言,它对于性能的提升主要在于两点:
头部压缩
- 多路复用
当然还有一些颠覆性的功能实现:
- 设置请求优先级
- 服务器推送
这些重大的提升本质上也是为了解决 HTTP 本身的问题而产生的。接下来我们来看看 HTTP/2 解决了哪些问题,以及解决方式具体是如何的。
15.1 头部压缩
在 HTTP/1.1 及之前的时代,请求体一般会有响应的压缩编码过程,通过Content-Encoding头部字段来指定,但你有没有想过头部字段本身的压缩呢?当请求字段非常复杂的时候,尤其对于 GET 请求,请求报文几乎全是请求头,这个时候还是存在非常大的优化空间的。HTTP/2 针对头部字段,也采用了对应的压缩算法——HPACK,对请求头进行压缩。
HPACK 算法是专门为 HTTP/2 服务的,它主要的亮点有两个:
- 首先是在服务器和客户端之间建立哈希表,将用到的字段存放在这张表中,那么在传输的时候对于之前出现过的值,只需要把索引(比如0,1,2,…)传给对方即可,对方拿到索引查表就行了。这种传索引的方式,可以说让请求头字段得到极大程度的精简和复用。

小贴士 HTTP/2 当中废除了起始行的概念,将起始行中的请求方法、URI、状态码转换成了头字段,不过这些字段都有一个”:”前缀,用来和其它请求头区分开。
- 其次是对于整数和字符串进行哈夫曼编码,哈夫曼编码的原理就是先将所有出现的字符建立一张索引表,然后让出现次数多的字符对应的索引尽可能短,传输的时候也是传输这样的索引序列,可以达到非常高的压缩率。
15.2 多路复用
用并发连接和域名分片的方式来解决队头堵塞并没有真正从 HTTP 本身的层面解决问题,只是增加了 TCP 连接,分摊风险而已。而且这么做也有弊端,多条 TCP 连接会竞争有限的带宽,让真正优先级高的请求不能优先处理。
而 HTTP/2 便从 HTTP 协议本身解决了队头阻塞问题。注意,这里并不是指的TCP队头阻塞,而是HTTP队头阻塞,两者并不是一回事。TCP 的队头阻塞是在数据包层面,单位是数据包,前一个报文没有收到便不会将后面收到的报文上传给 HTTP,而HTTP 的队头阻塞是在HTTP 请求-响应层面,前一个请求没处理完,后面的请求就要阻塞住。两者所在的层次不一样。
二进制分帧
首先,HTTP/2 认为明文传输对机器而言太麻烦了,不方便计算机的解析,因为对于文本而言会有多义性的字符,比如回车换行到底是内容还是分隔符,在内部需要用到状态机去识别,效率比较低。于是 HTTP/2 干脆把报文全部换成二进制格式,全部传输01串,方便了机器的解析。
原来Headers + Body的报文格式如今被拆分成了一个个二进制的帧,用Headers帧存放头部字段,Data帧存放请求体数据。分帧之后,服务器看到的不再是一个个完整的 HTTP 请求报文,而是一堆乱序的二进制帧。这些二进制帧不存在先后关系,因此也就不会排队等待,也就没有了 HTTP 的队头阻塞问题。
通信双方都可以给对方发送二进制帧,这种二进制帧的双向传输的序列,也叫做流(Stream)。HTTP/2 用流来在一个 TCP 连接上来进行多个数据帧的通信,这就是多路复用的概念。
可能你会有一个疑问,既然是乱序首发,那最后如何来处理这些乱序的数据帧呢?
首先要声明的是,所谓的乱序,指的是不同 ID 的 Stream 是乱序的,但同一个 Stream ID 的帧一定是按顺序传输的。二进制帧到达后对方会将 Stream ID 相同的二进制帧组装成完整的请求报文和响应报文。当然,在二进制帧当中还有其他的一些字段,实现了优先级和流量控制等功能。
15.3 二进制分帧
帧结构
HTTP/2 中传输的帧结构如下图所示:
每个帧分为帧头和帧体。先是三个字节的帧长度,这个长度表示的是帧体的长度。
然后是帧类型,大概可以分为数据帧和控制帧两种。数据帧用来存放 HTTP 报文,控制帧用来管理流的传输。
接下来的一个字节是帧标志,里面一共有 8 个标志位,常用的有 END_HEADERS表示头数据结束,END_STREAM表示单方向数据发送结束。
后 4 个字节是Stream ID, 也就是流标识符,有了它,接收方就能从乱序的二进制帧中选择出 ID 相同的帧,按顺序组装成请求/响应报文。
流的状态变化
从前面可以知道,在 HTTP/2 中,所谓的流,其实就是二进制帧的双向传输的序列。那么在 HTTP/2 请求和响应的过程中,流的状态是如何变化的呢?
HTTP/2 其实也是借鉴了 TCP 状态变化的思想,根据帧的标志位来实现具体的状态改变。这里我们以一个普通的请求-响应过程为例来说明
最开始两者都是空闲状态,当客户端发送Headers帧后,开始分配Stream ID, 此时客户端的流打开, 服务端接收之后服务端的流也打开,两端的流都打开之后,就可以互相传递数据帧和控制帧了。
当客户端要关闭时,向服务端发送END_STREAM帧,进入半关闭状态, 这个时候客户端只能接收数据,而不能发送数据。
服务端收到这个END_STREAM帧后也进入半关闭状态,不过此时服务端的情况是只能发送数据,而不能接收数据。随后服务端也向客户端发送END_STREAM帧,表示数据发送完毕,双方进入关闭状态。
如果下次要开启新的流,流 ID 需要自增,直到上限为止,到达上限后开一个新的 TCP 连接重头开始计数。由于流 ID 字段长度为 4 个字节,最高位又被保留,因此范围是 0 ~ 2的 31 次方,大约 21 亿个。
流的特性
刚刚谈到了流的状态变化过程,这里顺便就来总结一下流传输的特性:
- 并发性。一个 HTTP/2 连接上可以同时发多个帧,这一点和 HTTP/1 不同。这也是实现多路复用的基础。
- 自增性。流 ID 是不可重用的,而是会按顺序递增,达到上限之后又新开 TCP 连接从头开始。
- 双向性。客户端和服务端都可以创建流,互不干扰,双方都可以作为
发送方或者接收方。 - 可设置优先级。可以设置数据帧的优先级,让服务端先处理重要资源,优化用户体验。
16.HTTPS
16.1 概念
HTTPS并非是应用层的一种新协议。只是HTTP通信接口部分用SSL(Secure Scket Layer)和TLS(Transport Layer Security)协议代替而已。HTTP直接和TCP通信。当使用SSL时,则演变成和SSL通信,再用SSL和TCP通信。所谓HTTPS,其实就是身披SSL协议这层外壳的HTTP。

HTTPS协议的主要功能基本都依赖于TLS/SSL协议,TLS/SSL的功能实现主要依赖于三类基本算法: 非对称加密、对称加密、散列函数
- 非对称加密实现身份认证和秘钥协商
- 对称加密算法采用协商的秘钥对数据加密
- 基于散列函数验证信息的完整性
16.2 对称加密、非对称加密、CA和证书
了解HTTPS工作原理之前,需要先有关于对称加密、非对称加密、CA和证书的知识。1.对称加密
这种方式加密和解密同用一个秘钥。加密和解密都会用到秘钥。没有秘钥就无法对密码解密,反过来说,任何人只要持有秘钥就能解密了。
对称加密有个明显的缺陷就是秘钥本身如何进行保密和安全运输呢?只要第三方窃取到秘钥,则可以轻易破解密文。解决办法就是使用非对称加密。
非对称加密
不同于对称加密,非对称加密有一对秘钥,一个是保密的,称为私钥,一个是公开的,称为公钥。私钥加密的数据只有公钥才能解开,这个特性非常重要,因为不需要传输私钥了,而公钥是公开的,可以随便传播、常见的算法实现包括RSA、DSA等。
非对称加密的缺陷就是计算速度远慢于对称加密。
CA和证书
CA是Certificate Authority的缩写,也叫“证书授权中心”。它是负责管理和签发证书的第三方机构。可以通过以下方式来查看证书的内容:
16.3 HTTP和HTTPS的区别
如何防止窃听
HTTP的一个缺陷就是明文传输,数据包被别人捕获之后就可以获取其中的信息。但经HTTPS传输的数据是经过加密的,而解密有的秘钥是经过双方协商的一次性秘钥,只有通信双方持有。所以其他人即使抓取到了HTTPS数据包,页无法看到其中的内容,从而起到防止窃听的作用。
如何防止伪装
非对称加密方式存在的问题:就是无法证明公开秘钥本身就是货真价实的公开秘钥。HTTPS通过数字证书认证的方式防止伪装。
如何识别内容已被篡改
网络传输过程中需要经过很多中间节点,虽然数据无法被解密,但可能被篡改。HTTPS通过校验数字签名来识别内容是否被篡改。
所谓数字签名,就是对报文进行散列算出的摘要。即:明文—》散列运算—》摘要—》私钥加密—》数字签名。
服务器在发送报文之前做了三件事:
- 用哈希算法对报文提取定长摘要
- 用私钥对摘要进行加密,作为数字签名
- 将数字签名附加到报文末尾发送给客户端
客户端接收到报文后:
- 用公钥对服务器的数字签名进行解密
- 用同样的算法重新计算出报文的数字签名
- 比较解密后的签名与自己计算的签名是否一致,如果不一致,说明数据被篡改过。
同样,客户端发送数据时,通过公钥加密报文摘要,服务器用私钥解密,用同样的方法校验数据的完整性。
16.4 有了HTTPS,为什么有些网站还是HTTP
上面的内容解释了HTTPS是怎么解决HTTP缺点的,既然HTTPS是安全可靠的,为什么不是所用的Web网站都使用HTTPS呢?原因:
- 因为需要握手协商秘钥等信息,页面加载时间比使用HTTP要长。
- 加密和解密会消耗更多的CPU和内存资源
- 购买HTTPS证书也是一项开销
所以,仅仅在包含跟人信息等敏感信息时,才会使用HTTPS,尤其是企业提供的对外服务。如果是内网通信时,HTTP会是更好的选择。
17.TLS1.2 握手的过程
之前谈到了 HTTP 是明文传输的协议,传输保文对外完全透明,非常不安全,那如何进一步保证安全性呢?
由此产生了 HTTPS,其实它并不是一个新的协议,而是在 HTTP 下面增加了一层 SSL/TLS 协议,简单的讲,HTTPS = HTTP + SSL/TLS。
那什么是 SSL/TLS 呢?
SSL 即安全套接层(Secure Sockets Layer),在 OSI 七层模型中处于会话层(第 5 层)。之前 SSL 出过三个大版本,当它发展到第三个大版本的时候才被标准化,成为 TLS(传输层安全,Transport Layer Security),并被当做 TLS1.0 的版本,准确地说,TLS1.0 = SSL3.1。
现在主流的版本是 TLS/1.2, 之前的 TLS1.0、TLS1.1 都被认为是不安全的,在不久的将来会被完全淘汰。因此我们接下来主要讨论的是 TLS1.2, 当然在 2018 年推出了更加优秀的 TLS1.3,大大优化了 TLS 握手过程,这个我们放在下一节再去说。
TLS 握手的过程比较复杂,写文章之前我查阅了大量的资料,发现对 TLS 初学者非常不友好,也有很多知识点说的含糊不清,可以说这个整理的过程是相当痛苦了。希望我下面的拆解能够帮你理解得更顺畅些吧
17.1 传统RSA握手
先来说说传统的 TLS 握手,也是大家在网上经常看到的。我之前也写过这样的文章,(传统RSA版本)HTTPS为什么让数据传输更安全,其中也介绍到了对称加密和非对称加密的概念,建议大家去读一读,不再赘述。之所以称它为 RSA 版本,是因为它在加解密pre_random的时候采用的是 RSA 算法。
17.2 TLS1.2握手过程
现在我们来讲讲主流的 TLS 1.2 版本所采用的方式。
刚开始你可能会比较懵,先别着急,过一遍下面的流程再来看会豁然开朗。
step 1: Client Hello**
首先,浏览器发送 client_random、TLS版本、加密套件列表。
client_random 是什么?用来最终 secret 的一个参数。
加密套件列表是什么?我举个例子,加密套件列表一般张这样:
TLS_ECDHE_WITH_AES_128_GCM_SHA256
意思是TLS握手过程中,使用ECDHE算法生成pre_random(这个数后面会介绍),128位的AES算法进行对称加密,在对称加密的过程中使用主流的GCM分组模式,因为对称加密中很重要的一个问题就是如何分组。最后一个是哈希摘要算法,采用SHA256算法。
其中值得解释一下的是这个哈希摘要算法,试想一个这样的场景,服务端现在给客户端发消息来了,客户端并不知道此时的消息到底是服务端发的,还是中间人伪造的消息呢?现在引入这个哈希摘要算法,将服务端的证书信息通过这个算法生成一个摘要(可以理解为比较短的字符串),用来标识这个服务端的身份,用私钥加密后把加密后的标识和自己的公钥传给客户端。客户端拿到这个公钥来解密,生成另外一份摘要。两个摘要进行对比,如果相同则能确认服务端的身份。这也就是所谓数字签名的原理。其中除了哈希算法,最重要的过程是私钥加密,公钥解密。
step 2: Server Hello
可以看到服务器一口气给客户端回复了非常多的内容。server_random也是最后生成secret的一个参数, 同时确认 TLS 版本、需要使用的加密套件和自己的证书,这都不难理解。那剩下的server_params是干嘛的呢?
我们先埋个伏笔,现在你只需要知道,server_random到达了客户端。
step 3: Client 验证证书,生成secret
客户端验证服务端传来的证书和签名是否通过,如果验证通过,则传递client_params这个参数给服务器。
接着客户端通过ECDHE算法计算出pre_random,其中传入两个参数:server_params和client_params。现在你应该清楚这个两个参数的作用了吧,由于ECDHE基于椭圆曲线离散对数,这两个参数也称作椭圆曲线的公钥。
客户端现在拥有了client_random、server_random和pre_random,接下来将这三个数通过一个伪随机数函数来计算出最终的secret。
验证证书过程:
首先,会读取证书中的明文内容。CA 进行数字证书的签名时会保存一个 Hash 函数,来这个函数来计算明文内容得到信息A,然后用公钥解密明文内容得到信息B,两份信息做比对,一致则表示认证合法。
当然有时候对于浏览器而言,它不知道哪些 CA 是值得信任的,因此会继续查找 CA 的上级 CA,以同样的信息比对方式验证上级 CA 的合法性。一般根级的 CA 会内置在操作系统当中,当然如果向上找没有找到根级的 CA,那么将被视为不合法。
a.服务方 S 向第三方机构CA提交公钥、组织信息、个人信息(域名)等信息并申请认证;
b.CA 通过线上、线下等多种手段验证申请者提供信息的真实性,如组织是否存在、企业是否合法,是否拥有域名的所有权等;
c.如信息审核通过,CA 会向申请者签发认证文件-证书。
证书包含以下信息:申请者公钥、申请者的组织信息和个人信息、签发机构 CA 的信息、有效时间、证书序列号等信息的明文,同时包含一个签名;
签名的产生算法:首先,使用散列函数计算公开的明文信息的信息摘要,然后,采用 CA 的私钥对信息摘要进行加密,密文即签名;
d.客户端 C 向服务器 S 发出请求时,S 返回证书文件;
e.客户端 C 读取证书中的相关的明文信息,采用相同的散列函数计算得到信息摘要,然后,利用对应 CA 的公钥解密签名数据,对比证书的信息摘要,如果一致,则可以确认证书的合法性,即公钥合法;
f.客户端然后验证证书相关的域名信息、有效时间等信息;
g.客户端会内置信任 CA 的证书信息(包含公钥),如果CA不被信任,则找不到对应 CA 的证书,证书也会被判定非法。
在这个过程注意几点:
1.申请证书不需要提供私钥,确保私钥永远只能服务器掌握;
2.证书的合法性仍然依赖于非对称加密算法,证书主要是增加了服务器信息以及签名;
3.内置 CA 对应的证书称为根证书,颁发者和使用者相同,自己为自己签名,即自签名证书;
step4: Server 生成 secret
刚刚客户端不是传了client_params过来了吗?
现在服务端开始用ECDHE算法生成pre_random,接着用和客户端同样的伪随机数函数生成最后的secret。
为什么传输时用对称加密
通过以上步骤可知,在 TLS 握手阶段,两端使用非对称加密的方式来通信,但是因为非对称加密损耗的性能比对称加密大,大约是对称加密的2-3倍,所以在正式传输数据时,两端使用对称加密的方式通信。
17.3 注意事项
TLS的过程基本上讲完了,但还有两点需要注意。
第一、实际上 TLS 握手是一个双向认证的过程,从 step1 中可以看到,客户端有能力验证服务器的身份,那服务器能不能验证客户端的身份呢?
当然是可以的。具体来说,在 step3中,客户端传送client_params,实际上给服务器传一个验证消息,让服务器将相同的验证流程(哈希摘要 + 私钥加密 + 公钥解密)走一遍,确认客户端的身份。
第二、当客户端生成secret后,会给服务端发送一个收尾的消息,告诉服务器之后的都用对称加密,对称加密的算法就用第一次约定的。服务器生成完secret也会向客户端发送一个收尾的消息,告诉客户端以后就直接用对称加密来通信。
这个收尾的消息包括两部分,一部分是Change Cipher Spec,意味着后面加密传输了,另一个是Finished消息,这个消息是对之前所有发送的数据做的摘要,对摘要进行加密,让对方验证一下。
当双方都验证通过之后,握手才正式结束。后面的 HTTP 正式开始传输加密报文。
17.4 RSA 和 ECDHE 握手过程的区别
- ECDHE 握手,也就是主流的 TLS1.2 握手中,使用
ECDHE实现pre_random的加密解密,没有用到 RSA。 使用 ECDHE 还有一个特点,就是客户端发送完收尾消息后可以提前
抢跑,直接发送 HTTP 报文,节省了一个 RTT,不必等到收尾消息到达服务器,然后等服务器返回收尾消息给自己,直接开始发请求。这也叫TLS False Start。17.5 证书的验证过程
首先我们使用一种 Hash 算法来对我们的公钥和其他信息进行加密生成 一个信息摘要,然后让有公信力的认证中心(简称 CA )用它的私钥对消息摘要加密,形成签名。最后将原始的信息和签名合 在一起,称为数字证书。当接收方收到数字证书的时候,先根据原始信息使用同样的 Hash 算法生成一个摘要,然后使用公证 处的公钥来对数字证书中的摘要进行解密,最后将解密的摘要和我们生成的摘要进行对比,就能发现我们得到的信息是否被更改 了。这个方法最要的是认证中心的可靠性,一般浏览器里会内置一些顶层的认证中心的证书,相当于我们自动信任了他们,只有 这样我们才能保证数据的安全。
服务器将公钥证书发送给客户端 客户端验证公钥证书 从而确保公钥的合法性客户端取出提前内置在手机内部的认证机构的公钥
- 用认证机构的公钥去解密公钥证书里的数字签名 从而得到数字指纹
- 客户端对公钥证书的服务器公钥进行 数字摘要算法 从而生成数字指纹
- 对比客户端自己生成的数字指纹(第3步)和解密得到的数字指纹(第2步)是否一致 如果一致则公钥证书验证通过 就可以进行接下来的握手步骤了
17.6 TLS/SSL 中为什么一定要用三个随机数,来生成”会话密钥”?
客户端和服务器都需要生成随机数,以此来保证每次生成的秘钥都不相同。使用三个随机数,是因为 SSL 的协议默认不信任每个主机都能产生完全随机的数,如果只使用一个伪随机的数来生成秘钥,就很容易被破解。通过使用三个随机数的方式,增加了自由度,一个伪随机可能被破解,但是三个伪随机就很接近于随机了,因此可以使用这种方法来保持生成秘钥的随机性和安全性。17.7 SSL 连接断开后如何恢复?
一共有两种方法来恢复断开的 SSL 连接,一种是使用 session ID,一种是 session ticket。
使用 session ID 的方式,每一次的会话都有一个编号,当对话中断后,下一次重新连接时,只要客户端给出这个编号,服务器如果有这个编号的记录,那么双方就可以继续使用以前的秘钥,而不用重新生成一把。目前所有的浏览器都支持这一种方法。但是这种方法有一个缺点是,session ID 只能够存在一台服务器上,如果我们的请求通过负载平衡被转移到了其他的服务器上,那么就无法恢复对话。
另一种方式是 session ticket 的方式,session ticket 是服务器在上一次对话中发送给客户的,这个 ticket 是加密的
,只有服务器能够解密,里面包含了本次会话的信息,比如对话秘钥和加密方法等。这样不管我们的请求是否转移到其他的服务器上,当服务器将 ticket 解密以后,就能够获取上次对话的信息,就不用重新生成对话秘钥了。18.TLS 1.3 做了哪些改进
TLS 1.2 虽然存在了 10 多年,经历了无数的考验,但历史的车轮总是不断向前的,为了获得更强的安全、更优秀的性能,在2018年就推出了 TLS1.3,对于TLS1.2做了一系列的改进,主要分为这几个部分:强化安全、提高性能。18.1 强化安全
在 TLS1.3 中废除了非常多的加密算法,最后只保留五个加密套件:
- TLS_AES_128_GCM_SHA256
- TLS_AES_256_GCM_SHA384
- TLS_CHACHA20_POLY1305_SHA256
- TLS_AES_128_GCM_SHA256
- TLS_AES_128_GCM_8_SHA256
可以看到,最后剩下的对称加密算法只有 AES 和 CHACHA20,之前主流的也会这两种。分组模式也只剩下 GCM 和 POLY1305, 哈希摘要算法只剩下了 SHA256 和 SHA384 了。
那你可能会问了, 之前RSA这么重要的非对称加密算法怎么不在了?
我觉得有两方面的原因:
第一、2015年发现了FREAK攻击,即已经有人发现了 RSA 的漏洞,能够进行破解了。
第二、一旦私钥泄露,那么中间人可以通过私钥计算出之前所有报文的secret,破解之前所有的密文。
为什么?回到 RSA 握手的过程中,客户端拿到服务器的证书后,提取出服务器的公钥,然后生成pre_random并用公钥加密传给服务器,服务器通过私钥解密,从而拿到真实的pre_random。当中间人拿到了服务器私钥,并且截获之前所有报文的时候,那么就能拿到pre_random、server_random和client_random并根据对应的随机数函数生成secret,也就是拿到了 TLS 最终的会话密钥,每一个历史报文都能通过这样的方式进行破解。
但ECDHE在每次握手时都会生成临时的密钥对,即使私钥被破解,之前的历史消息并不会收到影响。这种一次破解并不影响历史信息的性质也叫前向安全性。RSA 算法不具备前向安全性,而 ECDHE 具备,因此在 TLS1.3 中彻底取代了RSA。
18.2 提升性能
握手改进
流程如下: 
大体的方式和 TLS1.2 差不多,不过和 TLS 1.2 相比少了一个 RTT, 服务端不必等待对方验证证书之后才拿到client_params,而是直接在第一次握手的时候就能够拿到, 拿到之后立即计算secret,节省了之前不必要的等待时间。同时,这也意味着在第一次握手的时候客户端需要传送更多的信息,一口气给传完。
这种 TLS 1.3 握手方式也被叫做1-RTT握手。但其实这种1-RTT的握手方式还是有一些优化的空间的,接下来我们来一一介绍这些优化方式。
会话复用
会话复用有两种方式: Session ID和Session Ticket。
先说说最早出现的Seesion ID,具体做法是客户端和服务器首次连接后各自保存会话的 ID,并存储会话密钥,当再次连接时,客户端发送ID过来,服务器查找这个 ID 是否存在,如果找到了就直接复用之前的会话状态,会话密钥不用重新生成,直接用原来的那份。
但这种方式也存在一个弊端,就是当客户端数量庞大的时候,对服务端的存储压力非常大。
因而出现了第二种方式——Session Ticket。它的思路就是: 服务端的压力大,那就把压力分摊给客户端呗。具体来说,双方连接成功后,服务器加密会话信息,用Session Ticket消息发给客户端,让客户端保存下来。下次重连的时候,就把这个 Ticket 进行解密,验证它过没过期,如果没过期那就直接恢复之前的会话状态。
这种方式虽然减小了服务端的存储压力,但与带来了安全问题,即每次用一个固定的密钥来解密 Ticket 数据,一旦黑客拿到这个密钥,之前所有的历史记录也被破解了。因此为了尽量避免这样的问题,密钥需要定期进行更换。
总的来说,这些会话复用的技术在保证1-RTT的同时,也节省了生成会话密钥这些算法所消耗的时间,是一笔可观的性能提升。
PSK
刚刚说的都是1-RTT情况下的优化,那能不能优化到0-RTT呢?
答案是可以的。做法其实也很简单,在发送Session Ticket的同时带上应用数据,不用等到服务端确认,这种方式被称为Pre-Shared Key,即 PSK。
这种方式虽然方便,但也带来了安全问题。中间人截获PSK的数据,不断向服务器重复发,类似于 TCP 第一次握手携带数据,增加了服务器被攻击的风险。
19.HTTP实现断点续传
HTTP协议是如何实现断点续传的?现在大部分网络下载都会有断点续传的功能,尤其是在下载容量较大的文件的时候,断点续传功能可谓是必不可少的。那么断点续传功能是怎么实现的呢?
其实实现的原理很简单,就是在Http的请求上和一般的下载有所不同而已。
打个比方说,浏览器请求服务器上的一个文件时,发出的请求如下:假设服务器域名为wwww.codetc.com,文件名为down.zip。
GET /down.zip HTTP/1.1Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-excel, application/msword, application/vnd.ms-powerpoint, */*Accept-Language: zh-cnAccept-Encoding: gzip, deflateUser-Agent: Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)Connection: Keep-Alive
服务器收到请求后,会按要求寻找请求的文件,提取文件的信息,然后返回给浏览器,返回信息如下:
200Content-Length=106786028Accept-Ranges=bytesDate=Mon, 30 Apr 2001 12:56:11 GMTETag=W/"02ca57e173c11:95b"Content-Type=application/octet-streamServer=Microsoft-IIS/5.0Last-Modified=Mon, 30 Apr 2001 12:56:11 GMT
通过返回的Http协议状态码(200)我们可以知道浏览器请求的文件,文件大小为106786028字节。
如果是断点续传,我们就不是要重新下载整个文件,而是指定下载点,也就是说从该文件的多少字节的地方开始下载。所以在客户端浏览器传给Web服务器的时候要多加一条信息(从哪里开始下载)。下面使用自己编的一个“浏览器”来传递信息给Web服务器,要求从2000070字节开始下载,请求如下:
GET /down.zip HTTP/1.0User-Agent: NetFoxRANGE: bytes=2000070-Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
仔细看一下就会发现多了一行RANGE: bytes=2000070-这一行的意思就是告诉服务器下载down.zip这个文件是从2000070字节开始传,前面的字节不用传了。服务器收到这个请求以后,返回的信息如下:
206Content-Length=106786028Content-Range=bytes 2000070-106786027/106786028Date=Mon, 30 Apr 2001 12:55:20 GMTETag=W/"02ca57e173c11:95b"Content-Type=application/octet-streamServer=Microsoft-IIS/5.0Last-Modified=Mon, 30 Apr 2001 12:55:20 GMT
这个返回信息包和前面服务器返回的信息比较一下,就会发现多了一行:Content-Range=bytes 2000070-106786027/106786028返回的代码也改为206了,而不再是200了。
其实断点续传的原理也就这样,知道了以上原理,就可以进行断点续传的编程了。

