# HTTP 协议
HTTP 是一个在计算机世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范。
在互联网世界里,HTTP 通常跑在 TCP/IP 协议栈之上,依靠 IP 协议实现寻址和路由、TCP 协议实现可靠数据传输、DNS 协议实现域名查找、SSL/TLS 协议实现安全通信。此外,还有一些协议依赖于 HTTP,例如 WebSocket、HTTPDNS 等。这些协议相互交织,构成了一个协议网,而 HTTP 则处于中心地位。
# http 请求构成
- http: 请求行 + 头部信息 + 空白行 + body。
- tcp: tcp 头(至少 20 字节)+实际传输的数据(通常 1460 字节)。
- 空白行的意义:按照 http 协议,空白行就是为了分隔 header 和 body,因为 http 是纯文本的协议。
# http 常用头字段
HTTP 协议规定了非常多的头部字段,实现各种各样的功能,但基本上可以分为四大类:
- 通用字段:在请求头和响应头里都可以出现;
- 请求字段:仅能出现在请求头里,进一步说明请求信息或者额外的附加条件;
- 响应字段:仅能出现在响应头里,补充说明响应报文的信息;
- 实体字段:它实际上属于通用字段,但专门描述 body 的额外信息。
# Methods
在 HTTP 协议里,所谓的安全
是指请求方法不会破坏
服务器上的资源,即不会对服务器上的资源造成实质的修改。
HEAD 方法可以看做是 GET 方法的一个简化版
或者轻量版
。因为它的响应头与 GET 完全相同,但服务器不会返回请求的实体数据,只会传回响应头,也就是资源的元信息
。所以可以用在很多并不真正需要资源的场合,避免传输 body 数据的浪费。
方法 Method | 幂等 Idempotent | 安全 Safe | 可缓存 Cacheable |
---|---|---|---|
HEAD | Y | Y | Y |
OPTIONS | Y | Y | N |
GET | Y | Y | Y |
PUT | Y | N | N |
DELETE | Y | N | N |
TRACE | Y | Y | N |
POST | N | N | Only if freshness information is included 包含新鲜度信息时 |
PATCH | N | N | N |
CONNECT | N | N | N |
# 幂等
- Q: 什么是幂等性?
- A: 简单来说就是一个操作无论执行多少次,都会得到相同的结果,即 f(x)=f(f(x))。
- GET 和 HEAD 方法,它们是
只读
操作,很显然,GET 和 HEAD 既是安全的也是幂等的。 - DELETE 可以多次删除同一个资源,效果都是
资源不存在
,所以也是幂等的。 - POST 是
新增或提交数据
,多次提交数据会创建多个资源,所以不是幂等的。 - PUT 是
替换或更新数据
,多次更新一个资源,资源还是第一次更新的状态,所以是幂等的。
# OPTIONS 方法
跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。在现在前端最常用的 CORS 跨域中,浏览器都是用 OPTIONS 方法发预检请求的。
# PATCH 和 PUT 方法的区别
- PATCH 方法是新引入的,是对 PUT 方法的补充,用来对已知资源进行局部更新;
- PATCH 请求中的实体是一组将要应用到实体的更改,而不是像 PUT 请求那样是要替换旧资源的实体;
- PATCH 只传要更新的字段到指定资源去,表示该请求是一个局部更新,后端仅更新接收到的字段;
- PUT 虽然也是更新资源,但要求前端提供的一定是一个完整的资源对象;
- PUT 是幂等的,而 PATCH 不一定是幂等的;
- PUT 更新整个资源,即整个集合,所以 PUT 是幂等的;
- PATCH 请求中的实体保存的是修改资源的指令,该指令指导服务器来对资源做出修改,所以不是幂等的,也就是,通过使用 PATCH,新资源可能被创造,或者现有资源被修改。
- POST 方法一定不是幂等的;多次执行同样的操作,每一次都会创建一个相同的新资源。
# 杂记
- 使用 HttpOnly 标记的 Cookie 只能使用在 HTTP 请求过程中,所以无法通过 JavaScript 来读取这段 Cookie。
- HTTP 本质是无状态的,使用 Cookies 可以创建有状态的会话。
- 实际上,URL 不存在参数上限的问题,HTTP 协议规范没有对 URL 长度进行限制。这个限制是特定的浏览器及服务器、操作系统对它的限制。同理,POST 是没有大小限制的,HTTP 协议规范也没有进行大小限制。
- 还会包括「代理」的因素在里面,可能 url 太长还没到服务,就已经被代理拒绝掉了。
- 一般浏览器对 URL 长度的最大限制从 2083 个字符(IE)到 19 万个字符(Opera)不等,Chrome-8182 个字符,Apache (Server)-8192 个字符,Firefox-65536 个字符,等等。
- 一般,URL 如果包含汉字,会进行转换 encodeURIComponent,如果浏览器的编码为 UTF8 的话,一个汉字最终编码后的字符长度为 9 个字符。因此如果使用的 GET 方法,最大长度等于 URL 最大长度减去实际路径中的字符数。
# GET VS POST
- 多数浏览器对于 POST 采用两阶段发送数据的,先发送请求头,再发送请求体,即使参数再少再短,也会被分成两个步骤来发送(相对于 GET),也就是第一步发送 header 数据,第二步再发送 body 部分。HTTP 是应用层的协议,而在传输层有些情况 TCP 会出现两次连结的过程,HTTP 协议本身不保存状态信息,一次请求一次响应。对于 TCP 而言,通信次数越多反而靠性越低,能在一次连结中传输完需要的消息是最可靠的,尽量使用 GET 请求来减少网络耗时。如果通信时间增加,这段时间客户端与服务器端一直保持连接状态,在服务器侧负载可能会增加,可靠性会下降。
- GET 请求能够被 cache,GET 请求能够被保存在浏览器的浏览历史里面(密码等重要数据 GET 提交,别人查看历史记录,就可以直接看到这些私密数据)POST 不进行缓存。
- GET 参数是带在 URL 后面,传统 IE 中 URL 的最大可用长度为 2048 字符,其他浏览器对 URL 长度限制实现上有所不同。POST 请求无长度限制(目前理论上是这样的)。
- GET 提交的数据大小,不同浏览器的限制不同,一般在 2k-8K 之间,POST 提交数据比较大,大小靠服务器的设定值限制,而且某些数据只能用 POST 方法「携带」,比如 file。
- 安全性、幂等性、可缓存性。
# HTTP/2 和 HTTP/1 的区别
- 相对于 HTTP1.0,HTTP1.1 的优化:
- 缓存处理:多了 Entity tag,If-Unmodified-Since, If-Match, If-None-Match 等缓存信息(HTTTP1.0 If-Modified-Since,Expires)
- 带宽优化及网络连接的使用
- 错误通知的管理
- Host 头处理
长连接: HTTP1.1 中默认开启 Connection: keep-alive,一定程度上弥补了 HTTP1.0 每次请求都要创建连接的缺点。单个 TCP 连接在同一时刻只能处理一个请求。一般不使用 Pipelining(一个支持持久连接的客户端可以在一个连接中发送多个请求,收到请求的服务器必须按照请求收到的顺序发送响应),因为有缺点:代理服务器不能正确处理 HTTP Pipelining、Head-of-line Blocking 连接头阻塞(首个请求耗时过长,阻塞其他请求)。Chrome 最多允许同一个 Host 可建立 6 个 TCP 连接。
相对于 HTTP1.1,HTTP2 的优化:
- HTTP2 支持二进制传送(实现方便且健壮),HTTP1.x 是字符串传送
- HTTP2 支持多路复用 Multiplexing,一个 TCP 可以并发多个 HTTP 请求
- HTTP2 采用 HPACK 压缩算法压缩头部,减小了传输的体积
- HTTP2 支持服务端推送
# HTTP 状态码
code | status | intro |
---|---|---|
信息响应 (100–199) | ||
100 | 接受,继续请求 | Continue |
101 | 更换协议 | Switching Protocols |
成功响应 (200–299) | ||
200 | 成功,并返回数据 | OK |
201 | 已创建 | Created |
202 | 已接受 | Accepted |
203 | 成功,但未授权 | Non-Authoritative Information |
204 | 成功,无内容 | No Content |
205 | 成功,重置内容 | Reset Content |
206 | 成功,部分内容 | Partial Content |
重定向消息 (300–399) | ||
300 | 请求拥有不只一个响应 | Multiple Choice |
301 | 永久移动,重定向 | Moved Permanently |
302 | 临时移动,可使用原有 URI | Found |
303 | 指示客户端通过一个 GET 请求在另一个 URI 中获取所请求的资源 | See Other |
304 | 资源未修改,可使用协商缓存 | Not Modified |
客户端错误响应 (400–499) | ||
400 | 客户端错误,如请求语法错误 | Bad Request |
401 | 要求身份认证,客户端必须对自身进行身份验证 | Unauthorized |
403 | 拒绝请求,客户端没有访问内容的权限 | Forbidden |
404 | 资源不存在 | Not Found |
405 | 目标资源不支持该方法 | Method Not Allowed |
408 | 请求超时 | Request Timeout |
409 | 请求与服务器的当前状态冲突 | Conflict |
服务端错误响应 (500–599) | ||
500 | 服务器错误 | Internal Server Error |
501 | 服务器不支持请求方法 | Not Implemented |
502 | 网关错误的响应 | Bad Gateway |
503 | 服务不可用,服务器没有准备好处理请求 | Service Temporarily Unavailable |
504 | 网关响应超时 | Gateway Timeout |
505 | 服务器不支持请求中使用的 HTTP 版本 | HTTP Version Not Supported |
# HTTP 缓存
强制缓存优先于协商缓存进行,若强制缓存(Expires
和Cache-Control
)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since
和Etag / If-None-Match
),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,重新获取请求结果,再存入浏览器缓存中;生效则返回 304,继续使用缓存。
# 1. 强缓存
对于强制缓存来说,响应 header 中会有两个字段来标明失效规则( Expires
、Cache-Control
)。
Expires
的值为服务端返回的到期时间,即下一次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据;Expires
是 HTTP 1.0 的东西,现在浏览器均默认使用 HTTP 1.1,所以它的作用基本忽略。此外到期时间是由服务端生成的,但是客户端时间可能跟服务端时间有误差,这就会导致缓存命中的误差。所以 HTTP 1.1 的版本,使用Cache-Control
替代。Cache-Control
常见的取值有private
、public
、no-cache
、max-age
,no-store
,默认为private
。
private
: 是用户私有
的,不能放在代理上与别人共享-不能被代理服务缓存,但客户端可以缓存,防止信息泄漏;为默认值。public
: 客户端和代理服务器都可缓存;表示该资源可以被任何节点缓存;max-age=xxx
: 缓存的内容将在自「创建」 xxx 秒后失效;在代理服务器中s-maxage
优先级高于max-age
,同时出现时max-age
会被覆盖。起始时间是从浏览器获取并缓存该资源的时间开始算起。no-cache
: 需要使用对比缓存来验证缓存数据;可以缓存,但在使用之前必须要去服务器(协商缓存)验证是否过期,是否有最新的版本;每一次发起请求都不会再去询问浏览器的缓存情况,而是直接向服务端去确认该资源是否过期(即走协商缓存的路线);no-store
: 所有内容都不会缓存,强制缓存、对比缓存都不会触发;连服务端的缓存确认也绕开了,只允许你直接向服务端发送请求、并下载完整的响应。用于某些变化非常频繁的数据,例如秒杀页面;must-revalidate
: 告诉浏览器、缓存服务器,本地副本过期前,可以使用本地副本;本地副本一旦过期,必须去源服务器进行有效性校验。proxy-revalidate
: 只要求代理的缓存过期后必须验证,客户端不必回源,只验证到代理这个环节就行了。s-maxage
: 缓存的生存时间,只限定在代理上能够存多久,而客户端仍然使用max-age
。s-maxage
与max-age
不同之处在于,其只适用于公共缓存服务器,比如资源从源服务器发出后又被中间的代理服务器接收并缓存。当使用s-maxage
指令后,公共缓存服务器将直接忽略Expires
和max-age
指令的值。当同时设置了 private 指令后s-maxage
指令将被忽略。no-transform
: 代理专用的属性,代理有时候会对缓存下来的数据做一些优化,比如把图片生成 png、webp 等几种格式,方便今后的请求处理,而no-transform
就会禁止这样做,不许偷偷摸摸搞小动作
。Ctrl+F5
的强制刷新
:其实是发了一个Cache-Control: no-cache
,含义和max-age=0
基本一样,就看后台的服务器怎么理解,通常两者的效果是相同的。
- Pragma,HTTP/1.0 中规定的通用首部;用来向后兼容只支持 HTTP/1.0 协议的缓存服务器,那时候 HTTP/1.1 协议中的
Cache-Control
还没有出来。 只有一个值no-cache
,与Cache-Control: no-cache
效果一致。 - 硬性重新加载(Ctrl + F5)时,并没有清空缓存,而是禁用缓存--所有资源的请求首部都被加上了
cache-control: no-cache
和pragma: no-cache
,但是对于资源异步加载命中缓存不受硬性重新加载控制,还是可能会走缓存。 - 还有一种资源比异步资源更加“顽固”,几乎永远都是 from memory cache,不管是首次加载还是清空缓存都不奏效,它便是 base64 图片。Base64 格式的图片被塞进 memory cache 可以视作浏览器为节省渲染开销的“自保行为”。
- 浏览器内存缓存生效的前提下,JS 资源的执行加载时间会影响其是否被内存缓存。是否缓存可能和资源的渲染时机有关,在渲染机制还没有介入前的资源加载不会被内存缓存。
- 假设三种缓存类型都存在的时候,浏览器的缓存优先级从高到低:Service Worker > 强缓存(Cache-Control>Expires) > 协商缓存(ETag >Last-Modified)
# 2. 协商缓存
浏览器第一次请求数据时,服务器会将缓存标识与数据一起返回给客户端。再次请求数据时,客户端将备份的缓存标识发送给服务器,服务器根据缓存标识进行判断,判断成功后,返回 304 状态码,通知客户端可以使用缓存数据。缓存标识在请求 header 和响应 header 间进行传递,一共分为两种。
Last-Modified(response header) / If-Modified-Since(request header)
:http1.0;精确到秒;1.编辑了文件但没改变内容;2.修改文件的速度过快时;
ETag(response header) / If-None-Match(request header)
:http1.1;
- ETag 在服务器响应请求时,告诉浏览器当前资源在服务器的唯一标识,生成规则由服务器决定。
- ETag 优先级高于 Last-Modified,这里优先级指服务端优先级,客户端两者并存的情况下,都会在 request header 中带上,但是 Nginx 会优先匹配 Etag。
- ETag 还有
强弱
之分。强 ETag 要求资源在字节级别必须完全相符,弱ETag
在值前有个W/
标记,只要求资源在语义上没有变化,但内部可能会有部分发生了改变(例如 HTML 里的标签顺序调整,或者多了几个空格)。
# 3. 例外:Last-Modified 命中强缓存
出现这种情况,一般是 Nginx 配置有问题,可以通过增加add_header Cache-Control no-cache;
来解决。
在我们的认知里,通常 Last-Modified
是和协商缓存相关,但一些情况下也会触发启发式(heuristic)缓存。
如果服务器总是提供强缓存所需字段( Expires
、 Cache-Control
),浏览器可以通过判断是否使用本地缓存文件,来实现更好的加载性能。但由于服务器不是总返回强缓存的 response 字段,此时浏览器会根据其他的 response header 字段来计算 Cache-Control
的 max-age
值(通常是 Last-Modified
字段)。HTTP/1.1 规范没有给出特定的实现算法,使得不同浏览器内核的浏览器对此表现不尽相同。
通常推荐的计算方法是 过期时间 <
时间间隔 _
系数。时间间隔指的是 response 的返回时间与最后更新时间的间隔,而这个系数的典型值是 10%,计算公式为:max-age = ( date - last-modified ) * 0.1
,绝大多数的客户端,包括浏览器和各类 app 都是采用的这一推荐算法。
# HTTP 缓存别再乱用了!推荐一个缓存设置的最佳姿势!
资源的缓存通常是有多级的,一些缓存专门用于单个用户,一些缓存专用于多个用户。有些是由服务器控制的,有些是由用户控制的,有些则由中介层控制。缓存的种类:
- 浏览器缓存:一般并专用于单个用户,在浏览器客户端中实现。它们通过避免多次获取相同的响应来提高性能。
- 本地代理:可能是用户自己安装的,也可能是由某个中介层管理的:比如公司的网络层或者网络提供商。本地代理通常会为多个用户缓存单个响应,这就构成了一种
公共
缓存。 - 源服务器缓存/CDN。由服务器控制,源服务器缓存的目标是通过为多个用户缓存相同的响应来减少源服务器的负载。CDN 的目标是相似的,但它分布在全球各个地区,然后通过分配给最近的一组用户来达到减少延迟的目的。
为了防止中介缓存,建议设置:
Cache-Control: private
;
- 禁用 Public Cache,减少了攻击者跨界访问到公共内存的可能性。
- 建议设置适当的二级缓存 key:如果我们请求的响应是跟请求的 Cookie 相关的,建议设置:
Vary: Cookie;
- 默认情况下,我们浏览器的缓存使用 URL 和 请求方法来做缓存 key 的。这意味着,如果一个网站需要登录,不同用户的请求由于它们的请求 URL 和方法相同,数据会被缓存到一块内存里。这显然是有点问题,我们可以通过设置
Vary: Cookie
来避免这个问题。当用户身份信息发生变化的时候,缓存的内存也会发生变化。
# 200 状态码和 304 状态码何时出现
在没有设置
Cache-Control
的情况下,设置Last-Modified
和ETag
缓存,会出现200(from cache)
和 304 交替出现的情况。设置
Cache-Control
的情况下,过期刷新会出现 304(如果有更新内容,则是 200),之后再过期之前刷新都是200(from cache)
。如果要确保要向服务端确认,可以将Cache-Control
的max-age
设置为 0。
# 透视 HTTP 协议
拷贝项目(需要Git)
1. git clone https://github.com/chronolaw/http_study
安装OpenResty (推荐使用Homebrew)
1. brew tap openresty/brew
2. brew install openresty
运行项目
1. cd http_study/www/
2. openresty -p `pwd` -c conf/nginx.conf
停止项目
1. openresty -s quit -p `pwd` -c conf/nginx.conf
2
3
4
5
6
7
8
9
10
11
12
13
# HTTP 版本对比
相同点:
- 都基于 TCP/IP 协议,都是应用层协议,可靠传输;
- 都是无状态的;
- 都有队头阻塞的缺陷,TCP 导致的或者 http 和 tcp 共同导致的;
- 都是不安全的,需要配合 SSL/TLS 协议;
- 都可以灵活扩展, 使用了请求 - 应答模式;
简单对比:
- http/1.0:最基础的 http 协议,支持基本的 get、post 方法
- http/1.1:目前广泛应用,增加缓存策略
cache-control|E-tag
等;支持长连接Connection:keep-alive
,一次 TCP 连接可以多次请求;支持断点续传,状态码 206;支持新的方法 put、delete 等,可用于 restful api;还支持以管道方式同时发送多个请求,以便降低线路负载,提高传输速度。 - http/2:二进制分帧,不再明文传输;头部压缩算法(HPACK),可压缩 header,减小体积;多路复用,一次 tcp 连接中可以多个 http 并行请求,修复队头阻塞问题,允许设置设定请求优先级;支持服务端推送;
# HTTP 特点
- HTTP 是灵活可扩展的,可以任意添加头字段实现任意功能;如传输的实体数据可缓存可压缩、可分段获取数据、支持身份认证、支持国际化语言等
- HTTP 是可靠传输协议,基于 TCP/IP 协议
尽量
保证数据的送达; - HTTP 是应用层协议,比 FTP、SSH 等更通用、功能更多,能够传输任意数据;
- HTTP 使用了请求 - 应答模式,客户端主动发起请求,服务器被动回复请求;
- HTTP 本质上是无状态的,明文传输,每个请求都是互相独立、毫无关联的,协议不要求客户端或服务器记录请求相关的信息。
明文
意思就是协议里的报文(准确地说是 header 部分)不使用二进制数据,而是用简单可阅读的文本形式。
优缺点对比:
- HTTP 最大的优点是简单、灵活和易于扩展;
- HTTP 拥有成熟的软硬件环境,应用的非常广泛,是互联网的基础设施;
- HTTP 是无状态的,可以轻松实现集群化,扩展性能,但有时也需要用 Cookie 技术来实现
有状态
; - HTTP 是明文传输,数据完全肉眼可见,能够方便地研究分析,但也容易被窃听;
- HTTP 是不安全的,无法验证通信双方的身份,也不能判断报文是否被篡改;
- HTTP 的性能不算差,但不完全适应现在的互联网,有
队头阻塞
等问题,还有很大的提升空间。
# websocket 和 http 的区别
- 浏览器有原生 api:
new WebSocket()
/fetch
/new XMLHttpRequest()
; - websocket 协议名是:ws 或者 wss,可双端发起请求、接收信息,先通过 http 协议建立连接,然后升级到 websocket 协议,
status code = 101
,底层都是 tcp 协议;101 Switching Protocols
:它的意思是客户端使用 Upgrade 头字段,要求在 HTTP 协议的基础上改成其他的协议继续通信,比如 WebSocket。而如果服务器也同意变更协议,就会发送状态码 101,但这之后的数据传输就不会再使用 HTTP 了。 - websocket 没有跨域限制;
- 通过 send 和 onmessage 通讯,http 通过 request 和 response 通信;
- http 长轮询:用来模拟服务端推送的。客户端发起请求,服务端阻塞等待,不会立即返回响应而是等到有数据之后才返回响应,而客户端收到响应后,又会立即再发送一个请求到服务端。如此往复。长轮询需要处理 timeout 机制,即 timeout 之后重新发起请求。
# UDP 和 http
对比一下 UDP 协议,它是无连接也无状态的,顺序发包乱序收包,数据包发出去后就不管了,收到后也不会顺序整理,也就是所谓的不可靠传输。
而 HTTP 是有连接无状态,顺序发包顺序收包,按照收发的顺序管理报文,可靠传输(因为是基于 TCP)。
# 中间人攻击
- https 加密过程,
- 中间人攻击,
# URI 转译
- URI 是用来唯一标记服务器上资源的一个字符串,通常也称为 URL;URI>URL;
- URI 通常由 scheme、host:port、path 和 query 四个部分组成,有的可以省略;
- URI 引入了编码机制,对于 ASCII 码以外的字符集和特殊字符做一个特殊的操作,把它们转换成与 URI 语义不冲突的形式。URI 转义的规则有点
简单粗暴
,直接把非 ASCII 码或特殊字符转换成十六进制字节值,然后前面再加上一个%
。对中文、日文等则通常使用 UTF-8 编码后再转义。 - 前端常用的转译方法 encodeURI(url)和 encodeURIComponent(urlComponent)有何不同?
- encodeURI()主要用于整个 URI,而 encodeURIComponent()主要用于对 URI 中的某一段进行编码。
- encodeURI()不会对本身属于 URI 的特殊字符进行编码,例如冒号、正斜杠、问号和井号;而 encodeURIComponent()则会对它发现的任何非标准字符进行编码。
- encodeURI 的原理:把字符(unicode)编码成 utf-8,utf-8 是用 1-4 个字节表示的,所以每个字节转换成 16 进制并在前面用百分号(%)连接,最后把每个字节转换的结果连接起来。
# MIME type
「多用途互联网邮件扩展」(Multipurpose Internet Mail Extensions),简称为 MIME。HTTP 协议使用它可以检查传输的文件类型。MIME 把数据分成了八大类,每个大类下再细分出多个子类,形式是type/subtype
的字符串。简单列举一下在 HTTP 里经常遇到的几个类别:
- 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,即不透明的二进制数据。
# Encoding type
HTTP 在传输时为了节约带宽,有时候还会压缩数据,为了不要让浏览器继续猜
,还需要有一个Encoding type
,告诉数据是用的什么编码格式,这样对方才能正确解压缩,还原出原始的数据。就少了很多,常用的只有下面三种:
- gzip:GNU zip 压缩格式,也是互联网上最流行的压缩格式;
- deflate:zlib(deflate)压缩格式,流行程度仅次于 gzip;
- br:一种专门为 HTTP 优化的新压缩算法(Brotli);
有了 MIME type 和 Encoding type,无论是浏览器还是服务器就都可以轻松识别出 body 的类型,也就能够正确处理数据了。
如果请求报文里没有 Accept-Encoding 字段,就表示客户端不支持压缩数据;如果响应报文里没有 Content-Encoding 字段,就表示响应数据没有被压缩。
Unicode 和 UTF-8,把世界上所有的语言都容纳在一种编码方案里,遵循 UTF-8 字符编码方式的 Unicode 字符集也成为了互联网上的标准字符集。
# 传输大文件
- 这种
化整为零
的思路在 HTTP 协议里就是chunked
分块传输编码,在响应报文里用头字段Transfer-Encoding: chunked
来表示,意思是报文里的 body 部分不是一次性发过来的,而是分成了许多的块(chunk)逐个发送。 Transfer-Encoding: chunked
和Content-Length
这两个字段是互斥的,也就是说响应报文里这两个字段不能同时出现,一个响应报文的传输要么是长度已知,要么是长度未知(chunked)。- 当拖动进度条快进几分钟时,这实际上是想获取一个大文件其中的片段数据,而分块传输并没有这个能力。HTTP 协议为了满足这样的需求,提出了
范围请求
(range requests)的概念,允许客户端在请求头里使用专用字段来表示只获取文件的一部分,相当于是客户端的化整为零
。 - 范围请求不是 Web 服务器必备的功能,可以实现也可以不实现,所以服务器必须在响应头里使用字段
Accept-Ranges: bytes
明确告知客户端:我是支持范围请求的
。 - 如果不支持的话该怎么办呢?服务器可以发送
Accept-Ranges: none
,或者干脆不发送Accept-Ranges
字段,这样客户端就认为服务器没有实现范围请求功能,只能老老实实地收发整块文件了。 - 看视频时可以根据时间点计算出文件的 Range,不用下载整个文件,直接精确获取片段所在的数据内容。不仅看视频的拖拽进度需要范围请求,常用的下载工具里的多段下载、断点续传也是基于它实现的,要点是:
- 先发个 HEAD,看服务器是否支持范围请求,同时获取文件的大小;
- 开 N 个线程,每个线程使用 Range 字段划分出各自负责下载的片段,发请求传输数据;
- 下载意外中断也不怕,不必重头再来一遍,只要根据上次的下载记录,用 Range 请求剩下的那一部分就可以了。
- 分段的 range 和分块的 chunk 是两个完全无关的概念。chunk 是传输时分成小块逐个发送,接收到全部 chunk 之后会拼成完整的,而 range 是取大文件中间的一部分,收到之后不需要再拼接就可直接用,响应状态码必须是 206。
- 范围请求一次也可以获取多个片段。
# TCP 长链接
- TCP 连接长时间不关闭,服务器必须在内存里保存它的状态,这就占用了服务器的资源。如果有大量的空闲长连接只连不发,就会很快耗尽服务器的资源,导致服务器无法为真正有需要的用户提供服务。
- 在客户端,可以在请求头里加上
Connection: close
字段,告诉服务器:这次通信后就关闭连接
。服务器看到这个字段,就知道客户端要主动关闭连接,于是在响应报文里也加上这个字段,发送之后就调用 Socket API 关闭 TCP 连接。 - 关闭策略:
- 使用
keepalive_timeout
指令,设置长连接的超时时间,如果在一段时间内连接上没有任何数据收发就主动断开连接,避免空闲连接占用系统资源。 - 使用
keepalive_requests
指令,设置长连接上可发送的最大请求次数。比如设置成 1000,那么当 Nginx 在这个连接上处理了 1000 个请求后,也会主动断开连接。
- 使用
- 队头阻塞:参考「TCP 协议」中的「拥塞控制」。
队头阻塞
问题会导致性能下降,可以用并发连接
和域名分片
技术缓解。
# HTTP 代理服务器
- HTTP 代理就是客户端和服务器通信链路中的一个中间环节,为两端提供
代理服务
; - 代理处于中间层,为 HTTP 处理增加了更多的灵活性,可以实现负载均衡、安全防护、数据过滤等功能;
- 代理服务器需要使用字段
Via
标记自己的身份,多个代理会形成一个列表; - 如果想要知道客户端的真实 IP 地址,可以使用字段
X-Forwarded-For
和X-Real-IP
; - 专门的
代理协议
可以在不改动原始报文的情况下传递客户端的真实 IP。 - 反向代理中使用的负载均衡算法:1.随机;2.轮询;3.一致性 hash;4.最近最少使用;5.链接最少;6.ip_hash;7.最少连接数;8.最快连接数;
# HTTP 缓存代理
HTTP 的服务器缓存功能主要由代理服务器来实现(即缓存代理)。在 HTTP 的缓存体系中,缓存代理的身份十分特殊,它既是客户端,又是服务器
,同时也既不是客户端,又不是服务器
。
源服务器在设置完Cache-Control
后必须要为报文加上Last-modified
或ETag
字段。否则,客户端和代理后面就无法使用条件请求来验证缓存是否有效,也就不会有 304 缓存重定向。
- 客户端的缓存控制:Cache-Control 中:
max-stale
的意思是如果代理上的缓存过期了也可以接受,但不能过期太多,超过 x 秒也会不要。min-fresh
的意思是缓存必须有效,而且必须在 x 秒后依然有效。- 有的时候客户端还会发出一个特别的
only-if-cached
属性,表示只接受代理缓存的数据,不接受源服务器的响应。如果代理上没有缓存或者缓存过期,就应该给客户端返回一个 504(Gateway Timeout)。
Vary
字段,是内容协商的结果,相当于报文的一个版本标记。同一个请求,经过内容协商后可能会有不同的字符集、编码、浏览器等版本。比如,Vary: Accept-Encoding
/Vary: User-Agent
,缓存代理必须要存储这些不同的版本。Purge
,也就是缓存清理
,它对于代理也是非常重要的功能:- 过期的数据应该及时淘汰,避免占用空间;
- 源站的资源有更新,需要删除旧版本,主动换成最新版(即刷新);
- 有时候会缓存了一些本不该存储的信息,例如网络谣言或者危险链接,必须尽快把它们删除。
- 清理缓存的方法有很多,比较常用的一种做法是使用自定义请求方法
PURGE
,发给代理服务器,要求删除 URI 对应的缓存数据。
# HTTPS
HTTPS 在 HTTP 下层的传输协议 TCP/IP 上又加了一层了 SSL/TLS,由HTTP over TCP/IP
变成了HTTP over SSL/TLS
,让 HTTP 运行在了安全的 SSL/TLS 协议上,收发报文不再使用 Socket API,而是调用专门的安全接口。
TLS(传输层安全,Transport Layer Security)由记录协议、握手协议、警告协议、变更密码规范协议、扩展协议等几个子协议组成,综合使用了对称加密、非对称加密、身份认证等许多密码学前沿技术。简单来说,SSL 就是通信双方通过非对称加密协商出一个用于对称加密的密钥。
对称加密只使用一个密钥,运算速度快,密钥必须保密,无法做到安全的密钥交换,常用的有 AES 和 ChaCha20;
非对称加密使用两个密钥:公钥和私钥,公钥可以任意分发而私钥保密,解决了密钥交换问题但速度慢,常用的有 RSA 和 ECC;公钥和私钥有个特别的“单向”性,虽然都可以用来加密解密,但公钥加密后只能用私钥解密,反过来,私钥加密后也只能用公钥解密。
ECDHE-RSA-AES256-GCM-SHA384
:握手时使用 ECDHE 算法进行密钥交换,用 RSA 非对称加密算法进行签名和身份认证(身份认证和不可否认),握手后的通信使用 AES 对称算法(机密性),密钥长度 256 位,分组模式是 GCM,摘要算法 SHA384 用于消息认证和产生随机数(完整性)。OpenSSL,它是一个著名的开源密码学程序库和工具包,几乎支持所有公开的加密算法和协议,已经成为了事实上的标准,许多应用软件都会使用它作为底层库来实现 TLS 功能,包括常用的 Web 服务器 Apache、Nginx 等。
通信安全必须同时具备机密性、完整性、身份认证和不可否认这四个特性;
非对称加密为什么慢,非对称加密除了慢外还有什么缺点:
- 非对称加密基于大数运算,比如大素数或者椭圆曲线,是复杂的数学难题,所以消耗计算量,运算速度慢。
- 除了慢,可能还有一个缺点就是需要更多的位数,相同强度的对称密钥要比非对称密钥短。对称密钥一般都 128 位、256 位,而 rsa 一般要 2048 位,不过椭圆曲线(ECC244)的会短一点。
TLS 里使用的混合加密方式:
- 在通信刚开始的时候使用非对称算法,比如 RSA、ECDHE,首先解决密钥交换的问题。
- 然后用随机数产生对称算法使用的“会话密钥”(session key),再用公钥加密。因为会话密钥很短,通常只有 16 字节或 32 字节,所以慢一点也无所谓。
- 对方拿到密文后用私钥解密,取出会话密钥。这样,双方就实现了对称密钥的安全交换,后续就不再使用非对称加密,全都使用对称加密。
- 即用非对称加密,加密对称加密的私钥。对称加密的私钥又是会话级的随机数=一次会话一个私钥。就算别人 baoli 破解也只是破解了一个会话。
- 私钥加密用公钥解是为了做身份认证(数字签名),不可抵赖,因为默认私钥只有持有人知道
数字签名的原理其实很简单,就是把非对称加密的公钥私钥的用法反过来,之前是公钥加密、私钥解密,现在是私钥加密、公钥解密。因为默认私钥只有持有人知道。
CA(Certificate Authority,证书认证机构)来解决公钥的信任链问题,由它来给各个公钥签名,用自身的信誉来保证公钥无法伪造,是可信的。
CA 对公钥的签名认证也是有格式的,不是简单地把公钥绑定在持有者身份上就完事了,还要包含序列号、用途、颁发者、有效时间等等,把这些打成一个包再签名,完整地证明公钥关联的各种信息,形成“数字证书”(Certificate)。
小一点的 CA 可以让大 CA 签名认证,但链条的最后,也就是 Root CA,就只能自己证明自己了,这个就叫“自签名证书”(Self-Signed Certificate)或者“根证书”(Root Certificate)。你必须相信,否则整个证书信任链就走不下去了。
有了这个证书体系,操作系统和浏览器都内置了各大 CA 的根证书,上网的时候只要服务器发过来它的证书,就可以验证证书里的签名,顺着证书链(Certificate Chain)一层层地验证,直到找到根证书,就能够确定证书是可信的,从而里面的公钥也是可信的。
- 摘要算法用来实现完整性,能够为数据生成独一无二的“指纹”,常用的算法是 SHA-2;
- 数字签名是私钥对摘要的加密,可以由公钥解密后验证,实现身份认证和不可否认;
- 公钥的分发需要使用数字证书,必须由 CA 的信任链来验证,否则就是不可信的;
- 作为信任链的源头 CA 有时也会不可信,解决办法有 CRL、OCSP,还有终止信任。
# https 速度优化
# 硬件优化
- 更快的 CPU,最好还内建 AES 优化,这样即可以加速握手,也可以加速传输。
- SSL 加速卡,加解密时调用它的 API,让专门的硬件来做非对称加解密,分担 CPU 的计算压力。(升级慢、支持算法有限,不能灵活定制解决方案)
- SSL 加速服务器,用专门的服务器集群来彻底“卸载”TLS 握手时的加密解密计算,性能自然要比单纯的“加速卡”要强大的多。
# 软件优化
- 软件升级:把现在正在使用的软件尽量升级到最新版本。
# 协议优化
- 尽量采用 TLS1.3,它大幅度简化了握手的过程,完全握手只要 1-RTT,而且更加安全。
- 如果暂时不能升级到 1.3,只能用 1.2,那么握手时使用的密钥交换协议应当尽量选用椭圆曲线的 ECDHE 算法。它不仅运算速度快,安全性高,还支持“False Start”,能够把握手的消息往返由 2-RTT 减少到 1-RTT,达到与 TLS1.3 类似的效果。
- 椭圆曲线也要选择高性能的曲线,最好是 x25519,次优选择是 P-256。对称加密算法方面,也可以选用“AES_128_GCM”,它能比“AES_256_GCM”略快一点点。
# 证书优化
- 证书传输
- 服务器的证书可以选择椭圆曲线(ECDSA)证书而不是 RSA 证书,因为 224 位的 ECC 相当于 2048 位的 RSA,所以椭圆曲线证书的“个头”要比 RSA 小很多,即能够节约带宽也能减少客户端的运算量,可谓“一举两得”。
- 证书验证
- OCSP 装订:它可以让服务器预先访问 CA 获取 OCSP 响应,然后在握手时随着证书一起发给客户端,免去了客户端连接 CA 服务器查询的时间。
PS:
- CRL(Certificate revocation list,证书吊销列表)由 CA 定期发布,里面是所有被撤销信任的证书序号,查询这个列表就可以知道证书是否有效。
- 现在 CRL 基本上不用了,取而代之的是 OCSP(在线证书状态协议,Online Certificate Status Protocol),向 CA 发送查询请求,让 CA 返回证书的有效状态。
- OCSP 也要多出一次网络请求的消耗,而且还依赖于 CA 服务器,如果 CA 服务器很忙,那响应延迟也是等不起的。
# 会话复用
HTTPS 建立连接的过程:先是 TCP 三次握手,然后是 TLS 一次握手。TLS 握手的重点是算出主密钥“Master Secret”,而主密钥每次连接都要重新计算,未免有点太浪费。
会话复用:主密钥缓存一下“重用”。可以免去一次 TLS 握手和计算的成本。
- 会话复用分两种,第一种叫“Session ID”,就是客户端和服务器首次连接后各自保存一个会话的 ID 号,内存里存储主密钥和其他相关的信息。当客户端再次连接时发一个 ID 过来,服务器就在内存里找,找到就直接用主密钥恢复会话状态,跳过证书验证和密钥交换,只用一个消息往返就可以建立安全通信。
- 第二种“Session Ticket”方案。会话票证。类似 HTTP 的 Cookie,存储的责任由服务器转移到了客户端,服务器加密会话信息,用“New Session Ticket”消息发给客户端,让客户端保存。重连的时候,客户端使用扩展“session_ticket”发送“Ticket”而不是“Session ID”,服务器解密后验证有效期,就可以恢复会话,开始加密通信。不过“Session Ticket”方案需要使用一个固定的密钥文件(ticket_key)来加密 Ticket,为了防止密钥被破解,保证“前向安全”,密钥文件需要定期轮换,比如设置为一小时或者一天。
# 预共享密钥
“False Start”“Session ID”“Session Ticket”等方式只能实现 1-RTT,而 TLS1.3 更进一步实现了“0-RTT”,原理和“Session Ticket”差不多,但在发送 Ticket 的同时会带上应用数据(Early Data),免去了 1.2 里的服务器确认步骤,这种方式叫“Pre-shared Key”,简称为“PSK”。
但“PSK”也不是完美的,它为了追求效率而牺牲了一点安全性,容易受到“重放攻击”(Replay attack)的威胁。黑客可以截获“PSK”的数据,像复读机那样反复向服务器发送。
解决的办法是只允许安全的 GET/HEAD 方法,在消息里加入时间戳、“nonce”验证,或者“一次性票证”限制重放。
# HTTP/2
- HTTP 协议不再使用小版本号,只使用大版本号。与 HTTPS 不同,HTTP/2 没有在 URI 里引入新的协议名,仍然用“http”表示明文协议,用“https”表示加密协议。
- HTTP/2 在“语义”上兼容 HTTP/1,保留了请求方法、URI 等传统概念;
- HTTP/2 并没有使用传统的压缩算法,而是开发了专门的“HPACK”算法,在客户端和服务器两端建立“字典”,用索引号表示重复的字符串,还釆用哈夫曼编码来压缩整数和字符串,可以达到 50%~90% 的高压缩率。
- HTTP/2 报文不再使用肉眼可见的 ASCII 码,而是向下层的 TCP/IP 协议“靠拢”,全面采用二进制格式。
- HTTP/2 使用二进制帧存放数据(数据分帧),通过「流」(stream,是虚拟的,实际上并不存在,二进制帧的双向传输序列,同一个消息往返的帧会分配一个唯一的流 ID)可以实现同时发送多个“碎片化”的消息,这就是常说的“多路复用”( Multiplexing)——多个往返通信都复用一个连接来处理。多个请求 / 响应之间没有了顺序关系,不需要排队等待,也就不会再出现“队头阻塞”问题,降低了延迟,大幅度提高了连接的利用率。HTTP/2 的帧最大可以达到 16M。
- 在 HTTP/2 连接上,虽然帧是乱序收发的,但只要它们都拥有相同的流 ID,就都属于一个流,而且在这个流里帧不是无序的,而是有着严格的先后顺序。流 ID 不能重用,只能顺序递增,客户端发起的 ID 是奇数,服务器端发起的 ID 是偶数;在流上发送“RST_STREAM”帧可以随时终止流,取消接收或发送。
- 流 ID 用完了该怎么办呢?这个时候可以再发一个控制帧“GOAWAY”,真正关闭 TCP 连接。
- 为了更好地利用连接,加大吞吐量,HTTP/2 还添加了一些控制帧来管理虚拟的“流”,实现了优先级和流量控制。第 0 号流比较特殊,不能关闭,也不能发送数据帧,只能发送控制帧,用于流量控制。
- 最开始的时候流都是“空闲”(idle)状态,也就是“不存在”,可以理解成是待分配的“号段资源”。
- 当客户端发送 HEADERS 帧后,有了流 ID,流就进入了“打开”状态,两端都可以收发数据,然后客户端发送一个带“END_STREAM”标志位的帧,流就进入了“半关闭”状态。
- 这个“半关闭”状态很重要,意味着客户端的请求数据已经发送完了,需要接受响应数据,而服务器端也知道请求数据接收完毕,之后就要内部处理,再发送响应数据。
- 响应数据发完了之后,也要带上“END_STREAM”标志位,表示数据发送完毕,这样流两端就都进入了“关闭”状态,流就结束了。
- 由于流 ID 不能重用,所以流的生命周期就是 HTTP/1 里的一次完整的“请求 - 应答”,流关闭就是一次通信结束。
- 下一次再发请求就要开一个新流(而不是新连接),流 ID 不断增加,直到到达上限,发送“GOAWAY”帧开一个新的 TCP 连接,流 ID 就又可以重头计数。流标识符的上限是 2^31,大约是 21 亿。
- HTTP/2 还在一定程度上改变了传统的“请求 - 应答”工作模式,服务器不再是完全被动地响应请求,也可以新建“流”主动向客户端发送消息。比如,在浏览器刚请求 HTML 的时候就提前把可能会用到的 JS、CSS 文件发给客户端,减少等待的延迟,这被称为“服务器推送”(Server Push,也叫 Cache Push)。
- HTTP/2 协议定义了两个字符串标识符:“h2”表示加密的 HTTP/2,“h2c”表示明文的 HTTP/2,多出的那个字母“c”的意思是“clear text”。
- 加密版本的 HTTP/2 在安全方面做了强化,要求下层的通信协议必须是 TLS1.2 以上,还要支持前向安全和 SNI,并且把几百个弱密码套件列入了“黑名单”,比如 DES、RC4、CBC、SHA-1 都不能在 HTTP/2 里使用,相当于底层用的是“TLS1.25”。
- HTTP/2 虽然使用“帧”“流”“多路复用”,没有了“队头阻塞”,但这些手段都是在应用层里,而在下层,也就是 TCP 协议里,还是会发生“队头阻塞”。
- http2 的多路复用就是为了解决两个性能问题:
- 串行的文件传输。当请求 a 文件时,b 文件只能等待。
- 连接数过多。假设 Apache 设置了最大并发数为 300,因为浏览器限制,浏览器发起的最大请求数为 6,也就是服务器能承载的最高并发为 50,当第 51 个人访问时,就需要等待前面某个请求处理完成。
- 在 HTTP/2 中,有两个非常重要的概念,分别是帧(frame)和流(stream)。 帧代表着最小的数据单位,每个帧会标识出该帧属于哪个流,流也就是多个帧组成的数据流。 多路复用,就是在一个 TCP 连接中可以存在多条流。换句话说,也就是可以发送多个请求,对端可以通过帧中的标识知道属于哪个请求。通过这个技术,可以避免 HTTP 旧版本中的队头阻塞问题,极大的提高传输性能。
# HTTP/1 为什么不能实现多路复用
HTTP/1.1 不是二进制传输,而是通过文本进行传输。由于没有流的概念,在使用并行传输(多路复用)传递数据时,接收端在接收到响应后,并不能区分多个响应分别对应的请求,所以无法将多个响应的结果重新进行组装,也就实现不了多路复用。
# HTTP/3 -- QUIC
- HTTP/3 基于 QUIC 协议,完全解决了“队头阻塞”问题,弱网环境下的表现会优于 HTTP/2;
- QUIC 是一个新的传输层协议,建立在 UDP 之上,实现了可靠传输;
- QUIC 内含了 TLS1.3,只能加密通信,支持 0-RTT 快速建连;
- QUIC 的连接使用“不透明”的连接 ID,不绑定在“IP 地址 + 端口”上,支持“连接迁移”;
- QUIC 的流与 HTTP/2 的流很相似,但分为双向流和单向流;
- HTTP/3 没有指定默认端口号,需要用 HTTP/2 的扩展帧“Alt-Svc”来发现。
- QUIC、HTTP/3 的好处:
- 彻底解决队头阻塞,
- 用户态定义流量控制、拥塞避免等算法,
- 优化慢启动、弱网、重建连接等问题。
# 网络应用防火墙(Web Application Firewall)-- WAF
WAF 是一种“防火墙”,它工作在七层,看到的不仅是 IP 地址和端口号,还能看到整个 HTTP 报文,所以就能够对报文内容做更深入细致的审核,使用更复杂的条件、规则来过滤数据。WAF 就是一种“HTTP 入侵检测和防御系统”。
WAF 领域里的最顶级产品:ModSecurity,它可以说是 WAF 界“事实上的标准”。
ModSecurity 有两个核心组件。第一个是“规则引擎”,它实现了自定义的“SecRule”语言,有自己特定的语法。但“SecRule”主要基于正则表达式,还是不够灵活,所以后来也引入了 Lua,实现了脚本化配置。
ModSecurity 的第二个核心组件就是它的“规则集”。有了规则集,就可以在 Nginx 配置文件里加载,然后启动规则引擎。
ModSecurity 还有强大的审计日志(Audit Log)功能,记录任何可疑的数据,供事后离线分析。
WAF 实质上是模式匹配与数据过滤,所以会消耗 CPU,增加一些计算成本,降低服务能力,使用时需要在安全与性能之间找到一个“平衡点”。
# CDN
“内容”“分发”和“网络”。是专门为解决“长距离”上网络访问速度慢而诞生的一种网络应用服务。
CDN 的最核心原则是“就近访问”。
CDN 发展到现在已经有二十来年的历史了,早期的 CDN 功能比较简单,只能加速静态资源。随着这些年 Web 2.0、HTTPS、视频、直播等新技术、新业务的崛起,它也在不断进步,增加了很多的新功能,比如 SSL 加速、内容优化(数据压缩、图片格式转换、视频转码)、资源防盗链、WAF 安全防护等等。
- CDN 构建了全国、全球级别的专网,让用户就近访问专网里的边缘节点,降低了传输延迟,实现了网站加速;
- CDN 是具体怎么运行的:它有两个关键组成部分:全局负载均衡和缓存系统,对应的是 DNS 和缓存代理技术。
- GSLB 是 CDN 的“大脑”,使用 DNS 负载均衡技术,智能调度边缘节点提供服务;
- 看用户的 IP 地址,查表得知地理位置,找相对最近的边缘节点;
- 看用户所在的运营商网络,找相同网络的边缘节点;
- 检查边缘节点的负载情况,找负载较轻的节点;
- 其他,比如节点的“健康状况”、服务能力、带宽、响应时间等。
- 缓存系统是 CDN 的“心脏”,使用 HTTP 缓存代理技术,缓存命中就返回给用户,否则就要回源。
- 两个衡量 CDN 服务质量的指标:“命中率”和“回源率”。命中率就是命中次数与所有访问次数之比,回源率是回源次数与所有访问次数之比。
- 最基本的方式就是在存储系统上下功夫,硬件用高速 CPU、大内存、万兆网卡,再搭配 TB 级别的硬盘和快速的 SSD。软件方面则不断“求新求变”,各种新的存储软件都会拿来尝试,比如 Memcache、Redis、Ceph,尽可能地高效利用存储,存下更多的内容。
- 缓存系统也可以划分出层次,分成一级缓存节点和二级缓存节点。一级缓存配置高一些,直连源站,二级缓存配置低一些,直连用户。回源的时候二级缓存只找一级缓存,一级缓存没有才回源站,这样最终“扇入度”就缩小了,可以有效地减少真正的回源。
- 使用高性能的缓存服务。
# WebSocket
- WebSocket 是一种基于 TCP 的轻量级网络通信协议,在地位上是与 HTTP“平级”的。
- WebSocket 与 HTTP/2 一样,都是为了解决 HTTP 某方面的缺陷而诞生的。HTTP/2 针对的是“队头阻塞”,而 WebSocket 针对的是“请求 - 应答”通信模式。
- WebSocket 是一个真正“全双工”的通信协议,与 TCP 一样,客户端和服务器都可以随时向对方发送数据。
- WebSocket 采用了二进制帧结构,语法、语义与 HTTP 完全不兼容。二进制帧结构比较简单,特殊的地方是有个“掩码”操作,客户端发数据必须掩码,服务器则不用;
- 服务发现方面,WebSocket 没有使用 TCP 的“IP 地址 + 端口号”,而是延用了 HTTP 的 URI 格式,但开头的协议名不是“http”,引入的是两个新的名字:“ws”和“wss”,分别表示明文和加密的 WebSocket 协议。
- WebSocket 的默认端口也选择了 80 和 443,因为现在互联网上的防火墙屏蔽了绝大多数的端口,只对 HTTP 的 80、443 端口“放行”,所以 WebSocket 就可以“伪装”成 HTTP 协议,比较容易地“穿透”防火墙,与服务器建立连接。
- WebSocket 的帧头就四个部分:“结束标志位 + 操作码 + 帧长度 + 掩码”,只是使用了变长编码的“小花招”,不像 HTTP/2 定长报文头那么简单明了。
- 和 TCP、TLS 一样,WebSocket 也要有一个握手过程,然后才能正式收发数据。利用了 HTTP 本身的“协议升级”特性,“伪装”成 HTTP,这样就能绕过浏览器沙盒、网络防火墙等等限制,这也是 WebSocket 与 HTTP 的另一个重要关联点。
- WebSocket 的握手是一个标准的 HTTP GET 请求,但要带上两个协议升级的专用头字段:
- “Connection: Upgrade”,表示要求协议“升级”;
- “Upgrade: websocket”,表示要“升级”成 WebSocket 协议。
- 为了防止普通的 HTTP 消息被“意外”识别成 WebSocket,握手消息还增加了两个额外的认证用头字段(所谓的“挑战”,Challenge):
- Sec-WebSocket-Key:一个 Base64 编码的 16 字节随机数,作为简单的认证密钥;
- Sec-WebSocket-Version:协议的版本号,当前必须是 13。
- 服务器收到 HTTP 请求报文,看到上面的四个字段,就知道这不是一个普通的 GET 请求,而是 WebSocket 的升级请求,于是就不走普通的 HTTP 处理流程,而是构造一个特殊的“101 Switching Protocols”响应报文,通知客户端,接下来就不用 HTTP 了,全改用 WebSocket 协议通信。
# HTTP 性能优化
- 性能优化是一个复杂的概念,在 HTTP 里可以分解为服务器性能优化、客户端性能优化和传输链路优化;
- 服务器有三个主要的性能指标:吞吐量、并发数和响应时间,此外还需要考虑资源利用率;
- 客户端的基本性能指标是延迟,影响因素有地理距离、带宽、DNS 查询、TCP 握手等;
- 从服务器到客户端的传输链路可以分为三个部分,我们能够优化的是前两个部分,也就是“第一公里”和“中间一公里”;
- 有很多工具可以测量这些指标,服务器端有 ab、top、sar 等,客户端可以使用测试网站,浏览器的开发者工具。
- 花钱购买硬件、软件或者服务可以直接提升网站的服务能力,其中最有价值的是 CDN;
- 不花钱也可以优化 HTTP,三个关键词是“开源”“节流”和“缓存”;
- 后端应该选用高性能的 Web 服务器,开启长连接,提升 TCP 的传输效率;
- 前端应该启用 gzip、br 压缩,减小文本、图片的体积,尽量少传不必要的头字段;
- 缓存是无论何时都不能忘记的性能优化利器,应该总使用 Etag 或 Last-modified 字段标记资源;
- 升级到 HTTP/2 能够直接获得许多方面的性能提升,但要留意一些 HTTP/1 的“反模式”。
- 对于 HTTP/2 来说,一个域名使用一个 TCP 连接才能够获得最佳性能,如果开多个域名,就会浪费带宽和服务器资源,也会降低 HTTP/2 的效率,所以“域名收缩”在 HTTP/2 里是必须要做的。
- “资源合并”在 HTTP/1 里减少了多次请求的成本,但在 HTTP/2 里因为有头部压缩和多路复用,传输小文件的成本很低,所以合并就失去了意义。
- 而且“资源合并”还有一个缺点,就是降低了缓存的可用性,只要一个小文件更新,整个缓存就完全失效,必须重新下载。
# HTTP 四次挥手的原因
- 第一个 rtt
- 主动关闭方发送 FIN 到被动关闭方;进入 FIN_WAIT;
- 被动关闭方收到 FIN 并回复 ACK 给主动关闭方;进入 CLOSE_WAIT;
- 主动关闭方收到 ACK 之后,不再发送新的请求了,进入 FIN_WAIT2;
- 第二个 rtt
- 被动关闭方检查之后发现已经不需要继续再发送数据(之前的请求都已响应完毕),发送 FIN 给主动关闭方;进入 LAST_ACK;
- 主动关闭方收到 FIN 之后,发送 ACK 给被动关闭方,再等待一段时间(2MSL)即进入关闭状态,此时主动关闭方 CLOSED;
- 被动关闭方收到 ACK 之后,也进入关闭状态,此时被动关闭方也 CLOSED;
主要有两个原因,一个是为了让被动关闭方能够按照正常步骤进入 CLOSED 状态,二是为了防止已经失效的请求连接报文出现在下次连接中。
TCP 是全双工信道,是可以双向传输/收发数据的,因此每个方向都必须单独进行关闭,每个方向都需要一个请求和一个确认。因为在第二次握手结束后,服务端可能还有数据传输,所以没有办法把第二次确认和第三次合并。即使没有最后一个包,也需要先回复断开连接的请求,然后再发送关闭请求。
# 主动方为什么会等待 2MSL
客户端在发送完第四次的确认报文段后会等待 2MSL 才真正关闭连接,MSL 是指数据包在网络中最大的生存时间。目的是确保服务端收到了这个确认报文段。一来一去,一共是 2MSL。所以客户端在发送完第四次握手数据包后,等待 2MSL 是一种兜底机制,如果在 2MSL 内没有收到其他报文段,客户端则认为服务端已经成功接受到第四次挥手,连接正式关闭。
# 三次握手的原因
三次握手是为了保证客户端存活,防止服务端在收到失效的超时请求造成资源浪费。
三次握手之所以是三次是保证 client 和 server 均让对方知道自己的接收和发送能力没问题而保证的最小次数。
- 第一次 client => server 只能 server 判断出 client 具备发送能力。
- 第二次 server => client client 就可以判断出 server 具备发送和接受能力。此时 client 还需让 server 知道自己接收能力没问题于是就有了第三次。
- 第三次 client => server 双方均保证了自己的接收和发送能力没有问题。
其中,为了保证后续的握手是为了应答上一个握手,每次握手都会带一个标识 seq,后续的 ACK 都会对这个 seq 进行加一来进行确认。
# HTTPS 握手过程
- 客户端使用 https 的 url 访问 web 服务器,要求与服务器建立 ssl 连接
- web 服务器收到客户端请求后, 会将网站的证书(包含公钥)传送一份给客户端
- 客户端收到网站证书后会检查证书的颁发机构以及过期时间, 如果没有问题就随机产生一个秘钥
- 客户端利用公钥将会话秘钥加密, 并传送给服务端, 服务端利用自己的私钥解密出会话秘钥
- 之后服务器与客户端使用秘钥加密传输
# HTTPS 握手过程中,客户端如何验证证书的合法性
- 首先什么是 HTTP 协议? http 协议是超文本传输协议,位于应用层,通过请求/响应的方式在客户端和服务器之间进行通信;但是缺少安全性,http 协议信息传输是通过明文的方式传输,不做任何加密,相当于在网络上裸奔,容易被中间人恶意篡改,这种行为叫做中间人攻击;
- 加密通信:
- 为了安全性,双方可以使用对称加密的方式 key 进行信息交流,但是这种方式对称加密秘钥也会被拦截,也不够安全,进而还是存在被中间人攻击风险;
- 于是人们又想出来另外一种方式,使用非对称加密的方式,使用公钥/私钥加解密;通信方 A 发起通信并携带自己的公钥,接收方 B 通过公钥来加密对称秘钥,然后发送给发起方 A,A 通过私钥解密,双发接下来通过对称秘钥来进行加密通信;但是这种方式还是会存在一种安全性:中间人虽然不知道发起方 A 的私钥,但是可以做到偷天换日,将拦截发起方的公钥 key,并将自己生成的一对公/私钥的公钥发送给 B,接收方 B 并不知道公钥已经被偷偷换过;按照之前的流程,B 通过公钥加密自己生成的对称加密秘钥 key2,发送给 A;这次通信再次被中间人拦截,尽管后面的通信,两者还是用 key2 通信,但是中间人已经掌握了 Key2,可以进行轻松的加解密,还是存在被中间人攻击风险;
- 解决困境:权威的证书颁发机构 CA 来解决:
- 制作证书:作为服务端的 A,首先把自己的公钥 key1 发给证书颁发机构,向证书颁发机构进行申请证书;证书颁发机构有一套自己的公私钥,CA 通过自己的私钥来加密 key1,并且通过服务端网址等信息生成一个证书签名,证书签名同样使用机构的私钥进行加密;制作完成后,机构将证书发给 A;
- 校验证书真伪:当 B 向服务端 A 发起请求通信的时候,A 不再直接返回自己的公钥,而是返回一个证书;说明:各大浏览器和操作系统已经维护了所有的权威证书机构的名称和公钥。B 只需要知道是哪个权威机构发的证书,使用对应的机构公钥,就可以解密出证书签名;接下来,B 使用同样的规则,生成自己的证书签名,如果两个签名是一致的,说明证书是有效的; 签名验证成功后,B 就可以再次利用机构的公钥,解密出 A 的公钥 key1;接下来的操作,就是和之前一样的流程了;
- 中间人是否会拦截发送假证书到 B 呢? 因为证书的签名是由服务器端网址等信息生成的,并且通过第三方机构的私钥加密中间人无法篡改;所以最关键的问题是证书签名的真伪;
- 校验证书真伪:
- (1)首先浏览器读取证书中的证书所有者、有效期等信息进行校验,校验证书的网站域名是否与证书颁发的域名一致,校验证书是否在有效期内
- (2)浏览器开始查找操作系统中已内置的受信任的证书发布机构 CA,与服务器发来的证书中的颁发者 CA 比对,用于校验证书是否为合法机构颁发
- (3)如果找不到,浏览器就会报错,说明服务器发来的证书是不可信任的。
- (4)如果找到,那么浏览器就会从操作系统中取出颁发者 CA 的公钥(多数浏览器开发商发布 版本时,会事先在内部植入常用认证机关的公开密钥),然后对服务器发来的证书里面的签名进行解密
- (5)浏览器使用相同的 hash 算法计算出服务器发来的证书的 hash 值,将这个计算的 hash 值与证书中签名做对比
- (6)对比结果一致,则证明服务器发来的证书合法,没有被冒充
# HTTP 长连接和 TCP 长连接有什么区别
其实是在问 HTTP 的 Keep-Alive 和 TCP 的 Keepalive 有什么区别。这两个完全是两样不同东西,实现的层面也不同:
- HTTP 的 Keep-Alive,是由应用层(用户态) 实现的,称为 HTTP 长连接;
- TCP 的 Keepalive,是由 TCP 层(内核态) 实现的,称为 TCP 保活机制;
# HTTP 长连接
- HTTP 协议采用的是「请求-应答」的模式,也就是客户端发起了请求,服务端才会返回响应,一来一回。由于 HTTP 是基于 TCP 传输协议实现的,客户端与服务端要进行 HTTP 通信前,需要先建立 TCP 连接,然后客户端发送 HTTP 请求,服务端收到后就返回响应,至此「请求-应答」的模式就完成了,随后就会释放 TCP 连接。如果每次请求都要经历这样的过程:建立 TCP -> 请求资源 -> 响应资源 -> 释放连接,那么此方式就是 HTTP 短连接。
- 在第一个 HTTP 请求完后,先不断开 TCP 连接,让后续的 HTTP 请求继续使用此连接,HTTP 的 Keep-Alive 就是实现了这个功能,可以使用同一个 TCP 连接来发送和接收多个 HTTP 请求/应答,避免了连接建立和释放的开销,这个方法称为 HTTP 长连接。
- HTTP 长连接的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。
- 使用 HTTP 的 Keep-Alive 功能:
- 在 HTTP 1.0 中默认是关闭的,如果浏览器要开启 Keep-Alive,它必须在请求的包头中添加:
Connection: Keep-Alive
,然后当服务器收到请求,作出回应的时候,它也添加一个头在响应中:Connection: Keep-Alive
。这样做,连接就不会中断,而是保持连接。当客户端发送另一个请求时,它会使用同一个连接。这一直继续到客户端或服务器端提出断开连接。 - 从 HTTP 1.1 开始, 就默认是开启了 Keep-Alive,如果要关闭 Keep-Alive,需要在 HTTP 请求的包头里添加:
Connection: close
。现在大多数浏览器都默认是使用 HTTP/1.1,所以 Keep-Alive 都是默认打开的。一旦客户端和服务端达成协议,那么长连接就建立好了。 - 为了避免资源浪费的情况,web 服务软件一般都会提供 keepalive_timeout 参数,用来指定 HTTP 长连接的超时时间。比如设置了 HTTP 长连接的超时时间是 60 秒,web 服务软件就会启动一个定时器,如果客户端在完后一个 HTTP 请求后,在 60 秒内都没有再发起新的请求,定时器的时间一到,就会触发回调函数来释放该连接。
- 在 HTTP 1.0 中默认是关闭的,如果浏览器要开启 Keep-Alive,它必须在请求的包头中添加:
- HTTP 长连接不仅仅减少了 TCP 连接资源的开销,而且这给 HTTP 流水线技术提供了可实现的基础。所谓的 HTTP 流水线,是客户端可以先一次性发送多个请求,而在发送过程中不需先等待服务器的回应,可以减少整体的响应时间。当然服务器还是需要按照顺序响应,所以会导致「队头阻塞」的问题。
# TCP 长连接
TCP 的 Keepalive 这东西其实就是 TCP 的保活机制。
如果两端的 TCP 连接一直没有数据交互,达到了触发 TCP 保活机制的条件,那么内核里的 TCP 协议栈就会发送探测报文。
- 如果对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。
- 如果对端主机崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。
所以,TCP 保活机制可以在双方没有数据交互的情况,通过探测报文,来确定对方的 TCP 连接是否存活,这个工作是在内核完成的。
注意,应用程序若想使用 TCP 保活机制需要通过 socket 接口设置 SO_KEEPALIVE
选项才能够生效,如果没有设置,那么就无法使用 TCP 保活机制。
# 总结
- HTTP 的 Keep-Alive 也叫 HTTP 长连接,该功能是由「应用程序」实现的,可以使得用同一个 TCP 连接来发送和接收多个 HTTP 请求/应答,减少了 HTTP 短连接带来的多次 TCP 连接建立和释放的开销。
- TCP 的 Keepalive 也叫 TCP 保活机制,该功能是由「内核」实现的,当客户端和服务端长达一定时间没有进行数据交互时,内核为了确保该连接是否还有效,就会发送探测报文,来检测对方是否还在线,然后来决定是否要关闭该连接。