透视HTTP协议学习笔记

294 阅读1小时+

课程:极客时间《透视HTTP协议》

1. 破冰篇

1.1 HTTP 的前世今生

  1. HTTP 协议始于三十年前蒂姆·伯纳斯 - 李的一篇论文;
  2. HTTP/0.9 是个简单的文本协议,只能获取文本资源;
  3. HTTP/1.0 确立了大部分现在使用的技术,但它不是正式标准;
  4. HTTP/1.1 是目前互联网上使用最广泛的协议,功能也非常完善;
  5. HTTP/2 基于 Google 的 SPDY 协议,注重性能改善,但还未普及;
  6. HTTP/3 基于 Google 的 QUIC 协议,是将来的发展方向。

课后问题

  1. 你认为推动 HTTP 发展的原动力是什么?

    用户需求推动技术发展
    从历史的进程来看,就是互联网的用户推动协议的发展的。刚刚开始只有文本,都只是文字;后来有了超文本,不仅仅是文字;后来嫌弃速度慢,有了持久连接,缓存机制;后来为了安全,有了加密通信。一切都是以用户的需求为导向的,用户的需要越来越高,协议就越来越高级,越来越完善。

  2. 你是怎么理解 HTTP(超文本传输协议)的?
    HTTP 是一种网络协议,也是一种约定,是交互双方所遵守的表达格式

1.2 HTTP 是什么?HTTP 又不是什么?

  1. HTTP 是一个在计算机世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范。
  2. HTTP 专门用来在两点之间传输数据,不能用于广播、寻址或路由。
  3. HTTP 传输的是文字、图片、音频、视频等超文本数据。
  4. HTTP 是构建互联网的重要基础技术,它没有实体,依赖许多其他的技术来实现,但同时许多技术也都依赖于它。

课后问题

  1. 有一种流行的说法:HTTP 是用于从互联网服务器传输超文本到本地浏览器的协议,你认为这种说法对吗?对在哪里,又错在哪里?

    我觉得是错误的,不仅仅是这样,还可以从服务器传输到服务器,浏览器传输到服务器。

注意:两个浏览器不能通信。服务器可以当客户端,但浏览器只是客户端。

课外小贴士

  • 超文本

    该词经常引起误解,让人以为 HTTP 只能传输文本文件,个人觉得叫「超媒体传输协议」更加恰当

  • 本文对「协议」的解释比较通俗,严格来说协议应该包括语法、语义、同步规则和错误处理

  • 我们通常使用浏览器访问的实际上是万维网(WWW),它是互联网(Internet)的一部分,但现在几乎很少有人能区分两者

1.3 HTTP 世界全览(上)

image.png 本节介绍了与 HTTP 有关系的各种应用技术,在这里简单小结一下要点:

  1. 互联网上绝大部分资源都使用 HTTP 协议传输;
  2. 浏览器是 HTTP 协议里的请求方,即用户代理 User Agent;
  3. 服务器是 HTTP 协议里的应答方,常用的有 Apache 和 Nginx;
  4. CDN 位于浏览器和服务器之间,主要起到缓存加速的作用;

CDN:内容分发网络,它应用了 HTTP 协议里的缓存和代理技术,代替源站响应客户端的请求。

  1. 爬虫是另一类 User Agent,是自动访问网络资源的程序。

课后问题

  1. 你觉得 CDN 在对待浏览器和爬虫时会有差异吗?为什么?

    笔者觉得没有什么差异,除非能分辨是爬虫

  2. 你怎么理解 WebService 与 Web Server 这两个非常相似的词?

    笔者认为:WebService 是使用 HTTP 协议传输 XML 或 SOAP 消息的一种技术架构或则说是一种框架技术,而 Web Server 则泛指能响应 http 请求的服务端

    他们则一致认为:WebService 是应用技术,WebServer 是服务器

1.4 HTTP 世界全览(下)

本节介绍了与 HTTP 相关的各种协议,在这里简单小结一下今天的内容。

  1. TCP/IP 是网络世界最常用的协议,HTTP 通常运行在 TCP/IP 提供的可靠传输基础上;
  2. DNS 域名是 IP 地址的等价替代,需要用域名解析实现到 IP 地址的映射(比喻:打电话);
  3. URI 是用来标记互联网上资源的一个名字,由 协议名 + 主机名 + 路径 构成,俗称 URL;
  4. HTTPS 相当于 HTTP+SSL/TLS+TCP/IP ,为 HTTP 套了一个安全的外壳;
  5. 代理是 HTTP 传输过程中的中转站,可以实现缓存加速、负载均衡等功能。

课后问题

  1. DNS 与 URI 有什么关系?

    URL 中的主机名部分是需要转换到 IP 协议中需要的 IP 地址

  2. 在讲 代理 时我特意没有举例说明,你能够用引入一个「小强」的角色,通过打电话来比喻一下吗?

1.5 常说的四层和七层到底是什么?五层、六层哪去了?

TCP/IP 网络分层模型

image.png

  • MAC 层的传输单位是帧(frame)
  • IP 层的传输单位是包(packet)
  • TCP 层的传输单位是段(segment)
  • HTTP 的传输单位则是消息或报文(message)

但这些名词并没有什么本质的区分,可以统称为数据包。

OSI 网络分层模型(开放式系统互联通信参考模型)

image.png

两个分层模型的映射关系

image.png

小结

本节介绍了 HTTP 所在的网络分层模型,它是工作中常用的交流语言,在这里简单小结一下:

  1. TCP/IP 分为四层,核心是二层的 IP 和三层的 TCP,HTTP 在第四层;
  2. OSI 分为七层,基本对应 TCP/IP,TCP 在第四层,HTTP 在第七层;
  3. OSI 可以映射到 TCP/IP,但这期间一、五、六层消失了;
  4. 日常交流的时候我们通常使用 OSI 模型,用四层、七层等术语;
  5. HTTP 利用 TCP/IP 协议栈逐层打包再拆包,实现了数据传输,但下面的细节并不可见。

有一个辨别四层和七层比较好的(但不是绝对的)小窍门,两个凡是

  • 凡是由操作系统负责处理的就是四层或四层以下,
  • 否则,凡是需要由应用程序(也就是你自己写代码)负责处理的就是七层。

课后问题

  1. 你能用自己的话解释一下二层转发、三层路由吗?

    • 二层转发:设备工作在链路层,帧在经过交换机设备时,检查帧的头部信息,拿到目标 mac 地址,进行本地转发和广播
    • 三层路由:设备工作在 ip 层,报文经过有路由功能的设备时,设备分析报文中的头部信息,拿到 ip 地址,根据网段范围,进行本地转发或选择下一个网关
  2. 你认为上一讲中的 DNS 协议位于哪一层呢?

    网络请求的第一步是域名解析,所以工作在应用层

  3. 你认为 CDN 工作在那一层呢?

    应用层

  • MAC 地址(Media Access Control Address),也称为区域网地址,可以唯一地标识一个网卡,也就同时标识了此网卡所属的设备
  • 在 TCP/IP 协议栈之外,还是有一些协议位于 OSI 5 层 和 6 层的,例如 UNIX 域套接字就可以认为在 5 层

1.6 域名里有哪些门道

DNS 架构 image.png

  1. 域名使用字符串来代替 IP 地址,方便用户记忆,本质上一个名字空间系统;
  2. DNS 就像是我们现实世界里的电话本、查号台,统管着互联网世界里的所有网站,是一个超级大管家;
  3. DNS 是一个树状的分布式查询系统,但为了提高查询效率,外围有多级的缓存;
  4. 使用 DNS 可以实现基于域名的负载均衡,既可以在内网,也可以在外网。

课后问题

  1. 在浏览器地址栏里随便输入一个不存在的域名,比如就叫 www. 不存在 .com ,试着解释一下它的 DNS 解析过程。

    浏览器缓存 -> 操作系统缓存 -> 操作系统 host 文件 -> dns 服务器

    而 dns 服务器查找顺序为:非权威 dns 服务器 -> 根域名服务器 -> 顶级域名服务器 -> 二级域名服务器

  2. 如果因为某些原因,DNS 失效或者出错了,会出现什么后果?

    失效:无法访问到该地址,域名屏蔽

    出错:解析到了别人的地址,域名污染

2. 基础篇

2.1 键入网址再按下回车,后面究竟发生了什么?

场景1:使用 IP 地址访问 Web 服务器

简要叙述一下这次最简单的浏览器 HTTP 请求过程:

  1. 浏览器从地址栏的输入中获得服务器的 IP 地址和端口号;
  2. 浏览器用 TCP 的三次握手与服务器建立连接;
  3. 浏览器向服务器发送拼好的报文;
  4. 服务器收到报文后处理请求,同样拼好报文再发给浏览器;
  5. 浏览器解析报文,渲染输出页面。

作者给这个过程画了一个交互图,可以对照着看一下。不过要提醒的是,图里 TCP 关闭连接的 四次挥手 在抓包里不会出现,这是因为 HTTP/1.1 长连接特性,默认不会立即关闭连接。 image.png

场景2:使用域名访问 Web 服务器

刚才我们是在浏览器地址栏里直接输入 IP 地址,但绝大多数情况下,我们是不知道服务器 IP 地址的,使用的是域名,浏览器看到了网址里的 http://www.chrono.com/,发现它不是数字形式的 IP 地址,那就肯定是域名了,于是就会发起域名解析动作,通过访问一系列的域名解析服务器,试图把这个域名翻译成 TCP/IP 协议里的 IP 地址。

在域名解析的过程中会有多级的缓存,浏览器首先看一下自己的缓存里有没有,如果没有就向操作系统的缓存要,还没有就检查本机域名解析文件 hosts,也就是上一讲中我们修改的 C:\WINDOWS\system32\drivers\etc\hosts,找到映射之后,于是浏览器就知道了域名对应的 IP 地址,就可以愉快地建立 TCP 连接发送 HTTP 请求了。

作者把这个过程也画出了一张图,但省略了 TCP/IP 协议的交互部分,里面的浏览器多出了一个访问 hosts 文件的动作,也就是本机的 DNS 解析。

image.png

跟场景1相比,增加了DNS域名解析动作。

DNS 解析过程:
浏览器缓存 -> 操作系统缓存 -> 操作系统 host 文件 -> dns 服务器
而 dns 服务器查找顺序为:非权威 dns 服务器 -> 根域名服务器 -> 顶级域名服务器 -> 二级域名服务器

课后问题

  1. 你能试着解释一下在浏览器里点击页面链接后发生了哪些事情吗?

    笔者认为:点击页面链接后发生的事情和浏览器里面输入的主要流程是一样的

  2. 这一节课里讲的都是正常的请求处理流程,如果是一个不存在的域名,那么浏览器的工作流程会是怎么样的呢?

    笔者认为:首先去找域名对应的 ip,如果找不到则浏览器里面显示无法访问了

  • 除了 80 端口,HTTP 协议还经常使用 8000 和 8080

  • 因为 Chrome 浏览器会缓存之前访问过的网站,所以当你再次访问 127.0.0.1 的时候它可能会直接从本地缓存而不是服务器获取数据,这样就无法用 Wireshark 捕获网络流量,解决办法是在 Chrome 的开发者工具或则设置里面清除相关的缓存

  • 现代浏览器通常都会自动且秘密的发送 favicon.ico 请求

  • 输入一个地址按下回车,浏览器把页面请求发送出去,服务器响应后返回 html,浏览器在接受到 html 后就会立即发生四次挥手吗?还是说会延迟一会,遇到 link、img 等这些带外链的标签后继续去发送请求(省去 dns 解析和 ip 寻址?),最终确定 html 中没有外链请求了才会断开链接呢?

    现在的 http 都是长连接,不会立即断开连接,尽量复用,因为握手和挥手的成本太高了。

    另外,浏览器解析和渲染的策略是浏览器决定的,一般是边解析就边发起请求加载了

2.2 HTTP 报文是什么样子的?

  1. HTTP 报文结构就像是「大头儿子」,由「起始行 + 头部 + 空行 + 实体」组成,简单地说就是「header+body」;
  2. HTTP 报文可以没有 body,但必须要有 header,而且 header 后也必须要有空行,形象地说就是大头必须要带着脖子;
  3. 请求头由「请求行 + 头部字段」构成,响应头由「状态行 + 头部字段」构成;
  4. 请求行有三部分:请求方法,请求目标和版本号,如GET / HTTP/1.1

在这个请求行里,GET 是请求方法,/ 是请求目标,HTTP/1.1 是版本号,把这三部分连起来,意思就是「服务器你好,我想获取网站根目录下的默认文件,我用的协议版本号是 1.1,请不要用 1.0 或者 2.0 回复我。」;

  1. 状态行也有三部分:版本号,状态码和原因字符串,如 HTTP/1.1 200 OK

意思就是:「浏览器你好,我已经处理完了你的请求,这个报文使用的协议版本号是 1.1,状态码是 200,一切 OK。」

  1. 头部字段是 key-value 的形式,用 : 分隔,不区分大小写,顺序任意,除了规定的标准头,也可以任意添加自定义字段,实现功能扩展;
  2. HTTP/1.1 里唯一要求必须提供的头字段是 Host,它必须出现在请求头里,标记虚拟主机名。

课下作业

  1. 如果拼 HTTP 报文的时候,在头字段后多加了一个 CRLF,导致出现了一个空行,会发生什么?

    笔者认为:会将空行之后的部分都当成 body

  2. 讲头字段时说 : 后的空格可以有多个,那为什么绝大多数情况下都只使用一个空格呢?

    节省资源

课外小贴士

  • 在 Nginx 里,默认的请求头大小不能超过 8K,但是可以用指令 large_client_hearder_buffers 修改
  • 在 HTTP 报文里用来分割请求方法、URI 等部分的不一定必须是空格,制表符(tab) 也是允许的
  • 早期曾经允许在头部用 前导空格 实现字段跨行,但现在这种方式已经被 RFC7230 废弃,字段只能放在一行里
  • 默认情况下 Nginx 是不允许头字段里使用 _ 的,配置指令 underscores_in_header on 可以接触限制,但不推荐
  • 与 Server 类似的一个响应头字段是 X-Powered-By ,它是非标准字段,表示服务器使用的编程语言,例如 X-Powered-By: PHP/700011
  • host 字段,是给 Web 服务器使用的,因为 http 基于 TCP/IP 协议,IP 已经帮你找到了具体的服务器

2.3 应该如何理解请求方法

  1. 请求方法是客户端发出的、要求服务器执行的、对资源的一种操作;
  2. 请求方法是对服务器的指示,真正应如何处理由服务器来决定;
  3. 最常用的请求方法是 GET 和 POST,分别是获取数据和发送数据;
  4. HEAD 方法是轻量级的 GET,用来获取资源的元信息;
  5. PUT 基本上是 POST 的同义词,多用于更新数据;
  6. 安全与幂等是描述请求方法的两个重要属性,具有理论指导意义,可以帮助我们设计系统。

按照 RFC 里的语义,POST 是「新增或提交数据」,多次提交数据会创建多个资源,所以不是幂等的;而 PUT 是「替换或更新数据」,多次更新一个资源,资源还是会第一次更新的状态,所以是幂等的。 可以对比一下 SQL 来加深理解:把 POST 理解成 INSERT,把 PUT 理解成 UPDATE,多次 INSERT 会添加多条记录,而多次 UPDATE 只操作一条记录,而且效果相同。

课下作业

  1. 你能把 GET/POST 等请求方法 对应到数据库的「增删改查」操作吗?请求头应该如何设计呢?

    笔者认为:

    • GET:查询,使用 query 参数
    • POST:新增,使用 body 来承载数据
    • PUT:更新,使用 body 来承载数据
    • DELETE:删除,使用 query 参数
  2. 你觉得 TRACE/OPTIONS/CONNECT 方法能够用 GET 或 POST 间接实现吗?

    按照协议规定,头我们自己自己增加,只要服务器能理解并执行对应的动作,就可以

课外小贴士

  • Nginx 默认不支持 OPTIONS 方法,但可以使用配置指令、自定义模块或 Lua 脚本实现
  • 超文本咖啡壶控制协议 HTCPCP 还有一个后续,HTCPCP-TEA(RFC7168),它用来控制茶壶

拓展阅读

  • 幂等:意思是多次执行相同的操作,结果也都是相同的,即多次幂后结果相等,多次返回的数据有可能被别人修改过的,所以响应的结果不同,这如何理解?

    幂等是指客户端操作对服务器的状态没有产生改变,虽然报文内容变了,但服务器还是

2.4 你能写出正确的网址吗

URI的格式

image.png

  1. URI 是用来唯一标记服务器上资源的一个字符串,通常也称为 URL;
  2. URI 通常由 scheme、host:port、path 和 query 四个部分组成,有的可以省略;
  3. scheme 叫方案名或者协议名,表示资源应该使用哪种协议来访问;
  4. host:port 表示资源所在的主机名和端口号;
  5. path 标记资源所在的位置;
  6. query 表示对资源附加的额外要求;
  7. 在 URI 里对 @&/ 等特殊字符和汉字必须要做编码,否则服务器收到 HTTP 报文后会无法正确处理。

URI 转义的规则有点「简单粗暴」,直接把非 ASCII 码或特殊字符转换成十六进制字节值 ,然后前面再加上一个 % 。

例如,空格被转义成 %20? 被转义成 %3F 。而中文、日文等则通常使用 UTF-8 编码后再转义,例如 银河 会被转义 %E9%93%B6%E6%B2%B3 。 不过我们在浏览器的地址栏里通常是不会看到这些转义后的 「乱码」的,这实际上是浏览器一种友好表现,隐藏了 URI 编码后的「丑陋一面」,不信你可以试试下面的这个 URI。

http://www.chrono.com:8080/11-1? 夸父逐日

image.png

先在 Chrome 的地址栏里输入这个 query 里含有中文的 URI,然后点击地址栏,把它再拷贝到其他的编辑器里,它就会「现出原形」:

http://www.chrono.com:8080/11-1?%E5%A4%B8%E7%88%B6%E9%80%90%E6%97%A5

file:///D:/http_study/www/这三个斜杠里的 前两个属于 URI 特殊分隔符 :// ,然后后面的 /D:/http_study/www/ 是路径,而中间的主机名被 省略 了。这实际上是 file 类型 URI 的「特例」,它允许省略主机名,默认是本机 localhost。比如下面这个完整的地址,你在浏览器中访问一下,就能显示你本地 D 盘的文件了 file://localhost/D:/。但对于 HTTP 或 HTTPS 这样的网络通信协议,主机名是绝对不能省略的。如果省略,会导致浏览器无法找到服务器。

课后问题

  1. HTTP 协议允许在在请求行里使用完整的 URI,但为什么浏览器没有这么做呢?

    笔者认为:节省带宽资源,协议里起始行和 host 已经包含了

课外小贴士

  • 可以直接把文件或目录从资源管理器「拖入」浏览器窗口,地址栏就会显示出对应的 URl
  • 查询参数 query 也可以不适用 key=value 的形式,只是单纯的使用 key,这样 value 就是空字符串
  • 如果查询参数 query 太长,也可以使用 POST 方法,放在 body 里发送给服务器
  • URL 还有「绝对 URL」和「相对 URL」之分,多用在 HTML 页面里标记引用其他资源,而在 HTTP 请求行里则不会出现
  • 需要注意 URI 编码转义与 HTML 里的编码转义是不同的,URI 转义使用的是 %,而 HTML 转义使用的是 &# ,不要混淆

拓展阅读

  • DNS 域名解析会优先解析到最近的 IP,如何实现最近的?

    判断远近很复杂,也是 cdn 的核心技术之一,术语叫 GSLB。简单来说,就是看 ip 地址,然后有一个对照表,就知道在哪里了。

  • URN 代表什么的?

    统一资源名,现在用的很少。

2.5 响应状态码该怎么用

  1. 状态码在响应报文里表示了服务器对请求的处理结果;
  2. 状态码后的原因短语是简单的文字描述,可以自定义;
  3. 状态码是十进制的三位数,分为五类,从 100 到 599;
  4. 2×× 类状态码表示成功,常用的有 200、204、206;
  5. 3×× 类状态码表示重定向,常用的有 301、302、304;
  6. 4×× 类状态码表示客户端错误,常用的有 400、403、404;
  7. 5×× 类状态码表示服务器错误,常用的有 500、501、502、503。
200OK,表示一切正常
204No Content,它的含义与 `200 OK` 基本相同,但响应头后没有 body 数据
206Partial Content,与200一样,也是服务器成功处理了请求,但body里的数据不是资源的全部,而是一部分

301Moved Permanently,永久重定向,
302Moved Temporarily,临时重定向,意思是请求的资源还在,但需要暂时用另一个 URI 来访问。
304Not Modified,缓存重定向

400Bad Request,一个通用的错误码,表示请求报文有错误,但没有明确指出哪里有错误
403Forbidden,表示服务器禁止访问资源,原因可能多种多样,例如信息敏感、法律禁止等
404Not Found,资源在本服务器上未找到,所以无法提供给客户端

500Internal Server Error,一个通用的错误码,服务器究竟发生了什么错误我们是不知道的
501Not Implemented,表示客户端请求的功能还不支持,和即将开业,敬请期待的意思差不多
502Bad Gateway,表示服务器自身工作正常,访问后端服务器时发生了错误,但具体的错误原因也是不知道的
503Service Unavailable,表示服务器当前很忙,暂时无法响应服务,可能过几秒钟后就可以继续提供服务了

课下作业

  1. 你在开发 HTTP 客户端,收到了一个非标准的状态码,比如 4××、5××,应当如何应对呢?

    笔者认为:弹框出错误信息

  2. 你在开发 HTTP 服务器,处理请求时发现报文里缺了一个必需的 query 参数,应该如何告知客户端错误原因呢?

    笔者认为:返回 400 状态,并把原因放在 body 中返回

2.6 HTTP 有哪些特点

  1. HTTP 是灵活可扩展的,可以任意添加头字段实现任意功能;
  2. HTTP 是可靠传输协议,基于 TCP/IP 协议“尽量”保证数据的送达;
  3. HTTP 是应用层协议,比 FTP、SSH 等更通用功能更多,能够传输任意数据;
  4. HTTP 使用了请求 - 应答模式,客户端主动发起请求,服务器被动回复请求;
  5. HTTP 本质上是无状态的,每个请求都是互相独立、毫无关联的,协议不要求客户端或服务器记录请求相关的信息。

课下作业

  1. 就如同开头我讲的那样,你能说一下今天列出的这些 HTTP 的特点中哪些是优点,哪些是缺点吗?

    笔者认为:以上特点在既定的场景中就是优点也可能是缺点,比如无状态,需要额外的来打补丁实现

  2. 不同的应用场合有不同的侧重方面,你觉得哪个特点对你来说是最重要的呢?

    笔者认为:灵活可扩展是最重要的,不然也不会屹立三十年不倒了

课外小贴士

  • 如果要 100% 保证数据收发成功就不能使用 HTTP 或则 TCP 协议了,而是要用各种消息中间件(MQ),如 RabbitMQ、ZeroMQ、Kafka 等
  • 以前 HTTP 协议还有一个 无连接 的特点,指的是协议不保持连接状态,每次请求应答后都会关闭连接,这就和 UDP 几乎一模一样了。但这样很影响性能,在 HTTP/1.1 里就改成了总是默认启用 keepalive 长连接机制,所以现在的 HTTP 已经不再是 无连接 的了
  • 注意 HTTP 的 无状态 特点与响应头里的 状态码 是完全不相关的两个概念,状态码表示的是此次报文处理的结果,并不会导致服务器内部状态变化

2.7 HTTP 有哪些优点?又有哪些缺点?

  1. HTTP 最大的优点是简单、灵活和易于扩展;
  2. HTTP 拥有成熟的软硬件环境,应用的非常广泛,是互联网的基础设施;
  3. HTTP 是无状态的,可以轻松实现集群化,扩展性能,但有时也需要用 Cookie 技术来实现“有状态”;
  4. HTTP 是明文传输,数据完全肉眼可见,能够方便地研究分析,但也容易被窃听;
  5. HTTP 是不安全的,无法验证通信双方的身份,也不能判断报文是否被窜改;
  6. HTTP 的性能不算差,但不完全适应现在的互联网,还有很大的提升空间。

虽然 HTTP 免不了这样那样的缺点,但你也不要怕,别忘了它有一个最重要的灵活可扩展的优点,所有的缺点都可以在这个基础上想办法解决,接下来的进阶篇和安全篇就会讲到。

课外小贴士

  • 简洁至上,也是 Apple 公司前领导人乔布斯所崇尚的设计理念
  • 与 DNT 类似的还有 P3P(Platform for Privacy Preferences Project)字段,用来控制网站对用户的隐私访问,同样也失败了
  • 处于安全原因,绝大多数网站都封禁了 80/8080 以外的端口号,只允许 HTTP 协议穿透,这也是造成 HTTP 流行的客观原因之一
  • HTTP/1.1 以文本格式传输 header,有严重的数据冗余,也影响了它的性能

3. 进阶篇

3.1 海纳百川:HTTP 的实体数据

image.png

  1. 数据类型表示实体数据的内容是什么,使用的是 MIME type,相关的头字段是 Accept 和 Content-Type;
  2. 数据编码表示实体数据的压缩方式,相关的头字段是 Accept-Encoding 和 Content-Encoding;
  3. 语言类型表示实体数据的自然语言,相关的头字段是 Accept-Language 和 Content-Language;
  4. 字符集表示实体数据的编码方式,相关的头字段是 Accept-Charset 和 Content-Type;
  5. 客户端需要在请求头里使用 Accept 等头字段与服务器进行内容协商,要求服务器返回最合适的数据;
  6. Accept 等头字段可以用 , 顺序列出多个可能的选项,还可以用 ;q= 参数来精确指定权重。(q权重的最大值是 1,最小值是 0.01,默认值是 1,如果值是 0 就表示拒绝)

TIP

  • Accept、Accept-xx 是告诉 服务端 ,我能解释什么

    这里也不一定是对的,就像后面讲解的 Accept-Ranges: bytes 分块请求,就是服务器端响应给客户端的

  • Content-xxx:告诉 对方 ,我给你的是什么,也就是说,可以在请求和响应中使用

课下作业

  1. 试着解释一下这个请求头 Accept-Encoding: gzip, deflate;q=1.0, *;q=0.5, br;q=0,再模拟一下服务器的响应头。

    笔者认为:优先使用 gzip 压缩,其次 deflate,然后是其他的压缩方式,br 方式我不能识别

    响应头:Content-Encodeing: gzip, deflate;

  2. 假设你要使用 POST 方法向服务器提交一些 JSON 格式的数据,里面包含有中文,请求头应该是什么样子的呢?

    注意题意,问的是提交,不是接受服务端的结果

    Content-Length: 92
    Content-Type: application/json; charset=utf-8
    

    charset 可以省略,大部分的应用服务器都默认是 utf8

  3. 试着用快递发货收货比喻一下 MIME、Encoding 等概念。

    比如你的商品类别(MIME)是文件,快递人员可以给你发航空,但是得用包裹把你的文件包装起来,也就是文件封(Encoding)

3.2 把大象装进冰箱:HTTP 传输大文件的方法

  1. 压缩 HTML 等文本文件是传输大文件最基本的方法;
  2. 分块传输可以流式收发数据,节约内存和带宽,使用响应头字段 Transfer-Encoding: chunked 来表示,分块的格式是 16 进制长度头 + 数据块;
  3. 范围请求可以只获取部分数据,即 分块请求,实现视频拖拽或者断点续传,使用请求头字段 Range 和响应头字段 Content-Range ,响应状态码必须是 206;
  4. 也可以一次请求多个范围,这时候响应报文的数据类型是 multipart/byteranges ,body 里的多个部分会用 boundary 字符串分隔。

要注意这四种方法不是互斥的,而是可以混合起来使用,例如压缩后再分块传输,或者分段后再分块。

课下作业

  1. 分块传输数据的时候,如果数据里含有回车换行(\r\n)是否会影响分块的处理呢?

    由于分块响应时,报文结构给出了当前块的数据长度,所以不影响

  2. 如果对一个被 gzip 的文件执行范围请求,比如 Range: bytes=10-19 ,那么这个范围是应用于原文件还是压缩后的文件呢?

    记住一个规则:range 是针对原文件的

课外小贴士

  • gzip 的压缩率通常能够超过 60%,而 br 算法是专门为 HTML 设计的,压缩效率和性能比 gzip 还要好,能够再提高 20% 的压缩密度
  • Nginx 的 gzip on 指令很智能,只会压缩文本数据,不会压缩图片、音频、视频
  • Transfer-Encoding 字段最常见的值是 chunked,但也可以用 gzip、deflate 等,表示传输时使用了压缩编码。注意这与 Content-Encoding 不同,Transfer-Encoding 在传输后会被自动解码还原出原始数据,而 Content-Encoding 则必须由应用自行解码
  • 分块传输在末尾还允许有 「拖尾数据」,由响应字段 Trailer 指定
  • 与 Range 有关的还有一个 If-Range ,即条件范围请求,将在后续章节讲解

拓展阅读

  • http 交给 tcp 进行传输的时候本来就会分块,那 http 分块的意义是什么呢?
    在 http 层是看不到 tcp 的,它不知道下层协议是否会分块,下层是否分块对它来说没有意义,不关心。
    在 http 里一个报文必须是完整交付,在处理大文件的时候就很不方便,所以就要分块,在 http 层面方便处理。
    chunked 主要是在 http 的层次来解决问题。

  • 客户端上传也可以使用 chunked、gzip,但不能用 range,注意这些字段的类型,只要是 实体字段,那就在请求响应里都可以用。

  • 分块传输、分段传输,用的一个 tcp 连接吗?
    一个 http 请求必定是在一个 tcp 连接里收发的,虽然是分块,但也是用一个 tcp。
    在范围请求的时候,可以使用多线程建立多个 tcp 连接获取,最后拼接起来原文件。

  • 区分一个字段是什么类型?比如请求字段、响应字段还是通用字段
    rfc 里有说明,其次多看看 http 抓包就能熟悉,不需要刻意去记

  • Transfer-Encoding: chunked 表示分段传输,改成 Transfer-Encoding: gzip 以后会自动解压,分段传输的语意还在么

    看字段的值,没有 chunked,那就不是分块,只是压缩。
    Transfer-Encoding: chunked,gzip 这样的多种组合理论上是可行的,但一般用的比较少。

3.3 排队也要讲效率:HTTP 的连接管理

  1. 早期的 HTTP 协议使用短连接,收到响应后就立即关闭连接,效率很低;
  2. HTTP/1.1 默认启用长连接,在一个连接上收发多个请求响应,提高了传输效率;
  3. 服务器会发送 Connection: keep-alive 字段表示启用了长连接;
  4. 报文头里如果有 Connection: close 就意味着长连接即将关闭;
  5. 过多的长连接会占用服务器资源,所以服务器会用一些策略有选择地关闭长连接;
  6. 队头阻塞问题会导致性能下降,可以用 并发连接 和 域名分片 技术缓解。

举个例子:

1. 短连接
假设你的公司买了一台打卡机,放在前台,因为这台机器比较贵,所以专门做了一个保护罩盖着它,公司要求每次上下班打卡时都要先打开盖子,打卡后再盖上盖子。可是偏偏这个盖子非常牢固,打开关闭要费很大力气,打卡可能只要 1 秒钟,而开关盖子却需要四五秒钟,大部分时间都浪费在了毫无意义的开关盖子操作上了 。

可想而知,平常还好说,一到上下班的点在打卡机前就会排起长队,每个人都要重复“开盖 - 打卡 - 关盖”的三个步骤,你说着急不着急。

在这个比喻里,打卡机就相当于服务器,盖子的开关就是 TCP 的连接与关闭,而每个打卡的人就是 HTTP 请求,很显然,短连接的缺点严重制约了服务器的服务能力,导致它无法处理更多的请求。

2. 长连接
公司也觉得这种反复「开盖 - 打卡 - 关盖」的操作太反人类了,于是颁布了新规定,早上打开盖子后就不用关上了,可以自由打卡,到下班后再关上盖子。

这样打卡的效率(即服务能力)就大幅度提升了,原来一次打卡需要五六秒钟,现在只要一秒就可以了,上下班时排长队的景象一去不返。

3. 队头阻塞
上班的时间点上,大家都在排队打卡,可这个时候偏偏最前面的那个人遇到了打卡机故障,怎么也不能打卡成功,急得满头大汗。等找人把打卡机修好,后面排队的所有人全迟到了。

4. 并发连接 也就是同时对一个域名发起多个长连接,用数量来解决质量的问题
公司可以再多买几台打卡机放在前台,这样大家可以不用挤在一个队伍里,分散打卡,一个队伍偶尔阻塞也不要紧,可以改换到其他不阻塞的队伍。

5. 域名分片 还是用数量来解决质量的思路
公司发展的太快了,员工越来越多,上下班打卡成了迫在眉睫的大问题。前台空间有限,放不下更多的打卡机了,怎么办?那就多开几个打卡的地方,每个楼层、办公区的入口也放上三四台打卡机,把人进一步分流,不要都往前台挤。

课下作业

  1. 在开发基于 HTTP 协议的客户端时应该如何选择使用的连接模式呢?短连接还是长连接?

    根据使用场景来规划:

    1. 密集的请求:使用长链接
    2. 不密集的请求:使用短连接
  2. 应当如何降低长连接对服务器的负面影响呢?

    超时时间不要设置得太长或太短,应该根据服务器性能设置连接数和长连接超时时间,保证服务器 TCP 资源使用处于正常范围。

课外小贴士

  • 因为 TCP 协议还有慢启动、拥塞窗口等特性,通常新建立的「冷链接」会比打开了一段时间的「热连接」要慢一些,所以长链接比短连接还多了这一层优势
  • 在长链接中的一个重要问题是如何正确区分多个报文的开始和结束,所以最好总使用 Content-Length 头明确响应实体的长度,正确标记报文结束。如果是流式传输,body 长度不能立即确定,就必须用分块传输编码。
  • 利用 HTTP 的长连接特性对服务器发起大量请求,导致服务器最终耗尽资源,拒绝服务,这就是常说的 DDos 攻击
  • HTTP 的连接管理还有第三种方式 pipeline(管道或流水线),它在长连接的基础上又进了一步,可以批量发送请求批量接收响应,但因为存在一些问题,Chrome、Firefox 等浏览器都没有实现它,已经被事实上废弃了
  • Connection 字段还有一个取值: Connection: Upgrade 配合状态码 101 表示协议升级,例如从 HTTP 切换到 WebSocket

拓展阅读

  • 一个连接究竟是什么?
    http 里的连接通常就是 tcp 连接,也就是调用 socket api 打开的一个套接字,可以理解成一个流式文件的句柄,可读可写,但数据都是在网络上。
    想要理解清楚应该去看一下 tcp/ip 相关的资料。

  • 一个长链接,同一时间只能发送一个请求是么?
    是的,http 是 半双工,只能一来一回收发数据,这就是队头阻塞的根源。

  • 为什么 tcp 握手 1 个 rtt,挥手 2 个 rtt ?
    一个来回就是 1 rtt,三次回收准确来说是 1.5 个 rtt,四次挥手是两个来回,所以是 2 rtt。

  • 如果不写 Connection: Keep-Alive HTTP/1.1 默认则是 Keep-Alive

3.4 四通八达:HTTP 的重定向和跳转

  1. 重定向是服务器发起的跳转,要求客户端改用新的 URI 重新发送请求,通常会自动进行,用户是无感知的;
  2. 301/302 是最常用的重定向状态码,分别是 永久重定向 和 临时重定向 ;
  3. 响应头字段 Location 指示了要跳转的 URI,可以用绝对或相对的形式;
  4. 重定向可以把一个 URI 指向另一个 URI,也可以把多个 URI 指向同一个 URI,用途很多;
  5. 使用重定向时需要当心性能损耗,还要避免出现循环跳转。

问题1 性能损耗:重定向的机制决定了一个跳转会有两次请求 - 应答,比正常的访问多了一次。 问题2 循环跳转:如果重定向的策略设置欠考虑,可能会出现 A=>B=>C=>A 的无限循环,不停地在这个链路里转圈圈,后果可想而知。所以 HTTP 协议特别规定,浏览器必须具有检测 循环跳转 的能力,在发现这种情况时应当停止发送请求并给出错误提示。

课下作业

  1. 301 和 302 非常相似,试着结合 响应状态码该怎么使用中的 3xx ,用自己的理解描述一下两者的异同点。
    相同点:

    • 都需要配合 Location 字段跳转
    • 都是跳转到另外一个地址

    不同点:

    • 语义不同, 301 永久性,302 临时的

拓展阅读

  • 网页的 入链接 和 出链接 也是标记网页重要性的关键指标,最著名的就是 Google 发明的 PageRank
  • 300 Multiple Choices 也是一个特殊的重定向状态码,它会返回一个有多个链接选项的页面,由用户自行选择要跳转的链接,用得较少
  • 重定向报文里还可以用 Refresh 字段,实现延时重定向,例如 Refresh: 5; url=xxx 告诉浏览器 5 秒后再跳转
  • 与跳转有关的还有一个 Referer 和 Referrer-Policy (注意前者是拼写错误的单词,但是已经将错就错),表示浏览器跳转的来源(引用地址),可用于统计分析和防盗链
  • 301/302 重定向是由浏览器执行的,对于服务器来说可以称为 外部重定向,相应的也就有服务器的 内部重定向,直接在服务器内部跳转 URI,因为不会发出 HTTP 请求,所以没有额外的性能损失

3.5 让我知道你是谁:HTTP 的 Cookie 机制

image.png Cookie 是由浏览器负责存储的,而不是操作系统。所以,它是浏览器绑定的,只能在本浏览器内生效。 如果你换个浏览器或者换台电脑,新的浏览器里没有服务器对应的 Cookie,就好像是脱掉了贴着纸条的衣服,健忘的服务器也就认不出来了,只能再走一遍 Set-Cookie 流程。

Set-Cookie: favorite=hamburger; Max-Age=10; Expires=Sat, 06-Mar-21 09:14:22 GMT; Domain=www.chrono.com; Path=/; HttpOnly; SameSite=Strict
  • Expires 和 Max-Age
    Expires 俗称 过期时间,用的是 绝对时间点 ,可以理解为 截止日期(deadline)。
    Max-Age 用的是相对时间,单位是秒,浏览器用收到报文的时间点再加上 Max-Age,就可以得到失效的绝对时间。
    Expires 和 Max-Age 可以同时出现,两者的失效时间可以一致,也可以不一致,但浏览器会优先采用 Max-Age 计算失效期 。

  • Domain 和 Path
    Domain 和 Path 指定了 Cookie 所属的域名和路径,通常 Path 就用一个 / 或者直接省略,表示域名下的任意路径都允许使用 Cookie,让服务器自己去挑。

  • HttpOnly
    属性 HttpOnly 会告诉浏览器,此 Cookie 只能通过浏览器 HTTP 协议传输,禁止其他方式访问 ,浏览器的 JS 引擎就会禁用 document.cookie 等一切相关的 API,脚本攻击也就无从谈起了。

  • SameSite
    可以防范 跨站请求伪造(XSRF)攻击 :设置成
    SameSite=Strict :可以严格限定 Cookie 不能随着跳转链接跨站发送
    SameSite=Lax:则略宽松一点,允许 GET/HEAD 等安全方法,但禁止 POST 跨站发送。
    还有一个 None,不限制

  • Secure
    表示这个 Cookie 仅能用 HTTPS 协议加密传输 ,明文的 HTTP 协议会禁止发送。但 Cookie 本身不是加密的,浏览器里还是以明文的形式存在。

虽然现在已经出现了多种 Local Web Storage 技术,能够比 Cookie 存储更多的数据,但 Cookie 仍然是最通用、兼容性最强的客户端数据存储手段。

简单小结一下:

  1. Cookie 是服务器委托浏览器存储的一些数据,让服务器有了记忆能力;
  2. 响应报文使用 Set-Cookie 字段发送 key=value 形式的 Cookie 值;
  3. 请求报文里用 Cookie 字段发送多个 Cookie 值;
  4. 为了保护 Cookie,还要给它设置有效期、作用域等属性,常用的有 Max-Age、Expires、Domain、HttpOnly 等;
  5. Cookie 最基本的用途是身份识别,实现有状态的会话事务。

还要提醒你一点,因为 Cookie 并不属于 HTTP 标准(RFC6265,而不是 RFC2616/7230),所以语法上与其他字段不太一致,使用的分隔符是 ; ,与 Accept 等字段的 , 不同,小心不要弄错了。

课下作业

  1. 如果 Cookie 的 Max-Age 属性设置为 0,会有什么效果呢?
    立即失效
    rfc 里有说明,如果 max-age <=0 ,统一按 0 算,立即过期。

  2. Cookie 的好处已经很清楚了,你觉得它有什么缺点呢?
    不安全,容易被拦截、有数量和大小限制,传输数据变大、某些客户端不支持 cookie

拓展阅读

  • Cookie 这个词来源于计算机编程里的术语 「Magic Cookie」,意思是不透明的数据,并不是「小甜甜」的含义

  • 早期 Cookie 直接就是磁盘上的一些小文本文件,现在基本上都是以数据库记录的形式存放的(通常使用的是 Sqlite)。浏览器对 Cookie 的数量和大小也都有限制,不允许无限存储,一般总大小不能超过 4K

  • 如果不指定 Expires 或 Max-Age 属性,那么 Cookie 仅在浏览器允许时有效,一旦浏览器关闭就会失效,这被称为 会话 Cookie(session cookie)  或内存 Cookie(in-memory cookie) ,在 Chrome 里过期时间会显示为 session 或 N/A

  • 历史上还有 Set-Cookie2 和 Cookie2 这样的字段,但是现在不再使用了

  • 广告追踪

    网站的页面里会嵌入很多广告代码,里面就会访问广告商,在浏览器里存储广告商的 cookie
    你换到其他网站,上面也有这个广告商的广告代码,因为都是一个广告商网站 ,自然就能够读取之前设置的cookie ,也就获得了你的信息。

3.6 生鲜速递:HTTP 的缓存控制

服务器的缓存控制

image.png

Cache-Control 取值:

  • max-age=30: 这个页面只能缓存 30 秒,之后就算是过期,不能用
  • no_store :不允许缓存 ,用于某些变化非常频繁的数据,例如秒杀页面;
  • no_cache :它的字面含义容易与 no_store 搞混,实际的意思并不是不允许缓存,而是 可以缓存 ,但在使用之前必须要去服务器验证是否过期,是否有最新的版本;
  • must-revalidate :又是一个和 no_cache 相似的词,它的意思是 如果缓存不过期就可以继续使用 ,但过期了如果还想用就必须去服务器验证 。

客户端的缓存控制

其实不止服务器可以发 Cache-Control 头,浏览器也可以发 Cache-Control ,也就是说请求 - 应答的双方都可以用这个字段进行缓存控制,互相协商缓存的使用策略。

当你点 「刷新」按钮的时候,浏览器会在请求头里加一个 Cache-Control: max-age=0 。因为 max-age 是 生存时间 ,max-age=0 的意思就是「我要一个最最新鲜的西瓜」,而本地缓存里的数据至少保存了几秒钟,所以浏览器就不会使用缓存,而是向服务器发请求。服务器看到 max-age=0,也就会用一个最新生成的报文回应浏览器。

Ctrl+F5 的「强制刷新」又是什么样的呢?

它其实是发了一个 Cache-Control: no-cache ,含义和 max-age=0 基本一样,就看后台的服务器怎么理解,通常两者的效果是相同的。

条件请求

image.png

Last-modified :文件的最后修改时间。
ETag :是实体标签(Entity Tag)  的缩写,是资源的一个唯一标识
另外补充,

  • 200 (from memory cache):这个表示没有向服务器发起请求,使用的是浏览器本地的缓存数据
  • 304 Not Modified:这个是表示向服务器发起了请求,但是服务器响应该文件没有变化,没有传回数据内容,使用浏览器的缓存

小结

  1. 缓存是优化系统性能的重要手段,HTTP 传输的每一个环节中都可以有缓存;
  2. 服务器使用 Cache-Control 设置缓存策略,常用的是 max-age ,表示资源的有效期;
  3. 浏览器收到数据就会存入缓存,如果没过期就可以直接使用,过期就要去服务器验证是否仍然可用;
  4. 验证资源是否失效需要使用「条件请求」,常用的是 if-Modified-Since 和 If-None-Match ,收到 304 就可以复用缓存里的资源 ;
  5. 验证资源是否被修改的条件有两个:Last-modified 和 ETag ,需要服务器预先在响应报文里设置,搭配条件请求使用;
  6. 浏览器也可以发送 Cache-Control 字段,使用 max-age=0 或 no_cache 刷新数据。

HTTP 缓存看上去很复杂,但基本原理说白了就是一句话:没有消息就是好消息,没有请求的请求,才是最快的请求。

课下作业

  1. Cache 和 Cookie 都是服务器发给客户端并存储的数据,你能比较一下两者的异同吗?

相同点:都会保存数据在浏览器端

不同点:

携带数据到服务端:

  • Cookie 存储的数据,在路径匹配的情况下,匹配的请求都会携带所有的 cookie 到服务端
  • Cache 针对不同的请求,进行缓存,只有访问和资源匹配的链接,才会触发缓存相关的检测和重用

数据的获取:

  • Cookie 可以通过脚本获取(无 HttpOnly),也可以在浏览器中看到有哪些 cookie
  • Cache 无法查看到相关列表,只能通过对应访问触发

用途不同:

  • Cookie:用于身份识别
  • Cache:用于节省带宽和加快响应速度

有效时间的计算:

  • Cookie:max-age 是从浏览器拿到响应报文时开始计算
  • Cache:max-age 是从响应报文的生成时间(Date 字段)开始计算的
  1. 即使有 Last-modified 和 ETag ,强制刷新(Ctrl+F5)也能够从服务器获取最新数据(返回 200 而不是 304),请你在实验环境里试一下,观察请求头和响应头,解释原因。

    强制刷新会增加 Cache-Control: no-cache 请求头来告知服务器,我需要最新的数据,但是请求头时不会携带 ETag,那么服务器端其实没有对比 ETag 的数据,就按正常的数据返回了

    image-20210309095056568

拓展阅读

  • 较早版本的 Chrome(66 之前) 可以使用 URL chrome://cache 检查本地缓存,但因为存在安全隐患,现在已经不能使用了

  • no-cache 属性可以理解为 max-age=0,must-revalidate
    如果缓存不过期就可以继续使用 ,但过期了如果还想用就必须去服务器验证 。设置为 0 ,则表示过期,需要去服务器验证

  • 除了 Cache-Control,服务器也可以用 Expires 字段来标记资源的有效期,它的形式和 Cookie 的差不多,同样属于 过时 的属性,优先级低于 Cache-Control 。还有一个历史遗留字段 Pragma: no-cache ,它相当于 Cache-Control: no-cache,除非为了兼容 HTTP/1.0 否则不建议使用

  • 如果响应报文里提供了 Last-modified,但是没有 Cache-Control 或 Expires ,浏览器会使用 启发(Heuristic)算法 计算一个缓存时间,在 RFC 里的建议是 (Date - Last-modified) * 10%

  • 每个 Web 服务器对 ETag 的计算方式都不一样,只要保证数据变化后值不一样就好,但复制的计算会增加服务器的负担。 Nginx 的算法是 修改时间 + 长度,实际上和 Last-modified 基本等价

  • 强 etag 和 etag 的的区别?
    只是计算 etag 的方式不一样,流程是一样的

  • no-cache 每次使用前都需要去浏览器问一下有没有过期,这不也是一次请求吗?那不和没有缓存一个意思吗
    笔者前面特别强调过,304 和 200 (from memory cache) 的含义。

  • 如果响应头里什么缓存字段都没有,客户端对缓存是采取什么策略呢?
    按照规范是无法缓存

  • 在 pwa 应用中,使用浏览器的「刷新」功能,从表现上看 max-age 并未设置为 0 ,这个笔者也不清楚是怎么回事

3.7 良心中间商:HTTP 的代理服务

  1. HTTP 代理就是客户端和服务器通信链路中的一个中间环节,为两端提供 代理服务 ;
  2. 代理处于中间层,为 HTTP 处理增加了更多的灵活性,可以实现负载均衡、安全防护、数据过滤等功能;
  3. 代理服务器需要使用字段 Via 标记自己的身份,多个代理会形成一个列表;
  4. 如果想要知道客户端的真实 IP 地址,可以使用字段 X-Forwarded-For 和 X-Real-IP ;
  5. 专门的 代理协议 可以在不改动原始报文的情况下传递客户端的真实 IP。

课下作业

  1. 你觉得代理有什么缺点?实际应用时如何避免?

    代理的缺点是增加链路长度,会增加响应耗时,应尽量减少在代理商所做的的一些与业务无关的复杂耗时操作。

  2. 你知道多少反向代理中使用的负载均衡算法?它们有什么优缺点?

    • 随机
    • 轮询
    • 哈希
    • 最近最少使用
    • 链接最少

拓展阅读

  • 现实生活中也有很多代理,例如房产代理、留学代理、保险代理、诉讼代理、可以对比理解下

  • 知名的代理软件有 HAProxy、Squid、Varnish 等,而 Nginx 虽然是 Web 服务器,但也可以作为代理服务器,而且功能毫不逊色

  • Via 是 HTTP 协议里规定的标准头字段,但有的服务器返回的响应报文里会使用 X-Via 含义是相同的

  • 因为 HTTP 是明文传输,请求头是很容易被篡改,所以 X-Forwarded-For 也不是完全可信

  • RFC7239定义了字段 Forwarded,它可以代替 X-Forwarded-ForX-Forwarded-Host 等字段,但应用得不多

  • 如何检测匿名代理?

    如果代理比较善良,修改了字段 X-Forwarded-For 和 X-Real-IP,我们还能看到,如果它不携带这些字段,我们也没有办法,因为它就是一个真实的客户端

3.8 冷链周转:HTTP 的缓存代理

  1. 计算机领域里最常用的性能优化手段是时空转换,也就是时间换空间或者空间换时间,HTTP 缓存属于后者;
  2. 缓存代理是增加了缓存功能的代理服务,缓存源服务器的数据,分发给下游的客户端;
  3. Cache-Control 字段也可以控制缓存代理,常用的有 privates-maxage 、no-transform 等,同样必须配合 Last-modifiedETag 等字段才能使用;
  4. 缓存代理有时候也会带来负面影响,缓存不良数据,需要及时刷新或删除。

拓展阅读

  • 常用的缓存代理软件有 Squid、 Varnish、ATS( Apache Traffic Server) 等,而 Nginx 不仅是 Web 服务器、代理服务器,也是一个出色的缓存代理服务器,堪称全能。
  • 有的缓存代理在 Cache Hit 的时候会在响应报文里加一个 Age 头字段,表示报文的生存时间,即已经在缓存里存了多久,通常它会小于 Cache- Control 里的 max-age 值,如果大于就意味着数据是陈旧的( stale)。
  • 判断缓存是否命中 (Hit) 类似于查询 hash 表,使用的 key 通常就是 URI ,在 Nginx 里可以用指令 proxy_cache_ key 自定义。
  • Nginx 对 vary 的处理实际上是做了 MD5,把 vary 头摘要后写入缓存,请求时不仅比较 URI,也比较摘要。

4. 安全篇

4.1 TLS 又是什么?

  1. 因为 HTTP 是明文传输,所以不安全,容易被黑客窃听或窜改;
  2. 通信安全必须同时具备机密性、完整性,身份认证和不可否认这四个特性;
  3. HTTPS 的语法、语义仍然是 HTTP,只是下层的协议在 TCP/IP 基础上又包装了 SSL/TLS;
  4. SSL/TLS 是信息安全领域中的权威标准,采用多种先进的加密技术保证通信安全;
  5. OpenSSL 是著名的开源密码学工具包,是 SSL/TLS 的具体实现。

课下作业

  1. 你能说出 HTTPS 与 HTTP 有哪些区别吗?
    HTTPS 相对于 HTTP 具有机密性,完整性,身份认证和不可否认的特性
    HTTPS 是HTTP over SSL/TLS,HTTP 是 HTTP over TCP/IP

  2. 你知道有哪些方法能够实现机密性、完整性等安全特性呢?
    实现机密性可以采用加密手段,接口签名实现完整性,数字签名用于身份认证

拓展阅读

  • 一个有趣的事实,当前所有 TLS 的 RFC 文档末尾数字都是 46(2246、4346、5246、8846)
  • 除了 HTTP,SSL/TLS 也可以承载其他的应用协议,例如 FTP=>FTPLDAP= LDAPS 等
  • OpenSSL 前身 SSLeay 的名字来源于其作者之的 EriC A. Young
  • 关于 OpenSSL 有一个著名的「心脏出血( Heart Bleed)」漏洞,出现在 1.0.1 版里
  • OpenSSL 里的密码套件定义与 TLS 略有不同,TLS 里的形式是 TLS_ECDHE_RSA_WITH_AES256_GCM_SHA384,加了前缀 TLS,并用 WTH 分开了握手和通信的算法
  • 另一个比较著名的开源密码库是 NSS( Network Security Services),由 Mozilla 开发

4.2 固若金汤的根本: 对称加密与非对称加密

  1. 加密算法的核心思想是「把一个小秘密(密钥)转化为一个大秘密(密文消息)」,守住了小秘密,也就守住了大秘密;
  2. 对称加密只使用一个密钥,运算速度快,密钥必须保密,无法做到安全的密钥交换,常用的有 AES 和 ChaCha20;
  3. 非对称加密使用两个密钥:公钥和私钥,公钥可以任意分发而私钥保密,解决了密钥交换问题但速度慢,常用的有 RSA 和 ECC;
  4. 把对称加密和非对称加密结合起来就得到了「又好又快」的混合加密,也就是 TLS 里使用的加密方式。

课下作业

  1. 加密算法中「密钥」的名字很形象,你能试着用现实中的锁和钥匙来比喻一下吗?

    加密算法是公开的,好比锁的制造方法是公开的,任何人都可以研究,但是想要开一个锁,只能用某把特定的钥匙,用其他的钥匙是打不开锁的,即想要解密特定的密文,只能用特定的密钥,用其他的密钥是无法解密的

  2. 在混合加密中用到了公钥加密,因为只能由私钥解密。那么反过来,私钥加密后任何人都可以用公钥解密,这有什么用呢?

    解决了身份认证(不可抵赖),私钥少数人知道,比如只有网站,公钥所有访问网站的人都知道,你用公钥加密的密文,只有私钥才能解密,相反的,你至少知道你是和网站对话,并不是和代理或则黑客

拓展阅读

  • 严格来说对称加密算法还可以分为 块加密算法( block cipher)  和 流加密算法( stream cipher) ,DES、AES 等属于块加密,而 RC4、ChaCha20 属于流加密。

  • ECC 虽然定义了公钥和私钥,但不能直接实现密钥交换和身份认证,需要搭配 DH、DSA 等算法,形成专门的 ECDHE、 ECDSA。RSA 比较特殊,本身即支持密钥交换也支持身份认证。

    • 加密交换:不对称,两边所需要的密匙不一致

    • 身份认证:公钥能解密私钥加密的密文,私钥只有持有人有,所以这个是身份认证

      比如你是一个大型网站,你响应给用户的请求,它能解开说明一定是你用私钥加密的

  • 比特币、以太坊等区块链技术里也用到了 ECC,它们选择的曲线是 secp256k1。

  • 由于密码学界普遍不信任 NST 和 NSA,怀疑 secp 系列曲线有潜在的弱点,所以研究出了 x25519 ,它的名字来源于曲线方程里的参数 2^255-19 。另有一个更高强度的曲线 ×448,参数是 2^448-2^224-1 。

  • 在 Linux上可以使用 OpenSSL 的命令行工具来测试算法的加解密速度,例如 openssl speed aes 、openssl speed rsa2048 等。

  • TLS1.2 要求必须实现 TLS_RSA_WITH_AES128_CBC_SHA,TLS1.3 要求必须实现 TLS_AES_128_GCM_SHA256,并且因为前向安全的原因废除了 DH 和 RSA 密钥交换算法。

  • 加密的分组模式到底是什么?

    拿 ECB 来举例子,假设使用 aes128,密钥长度是16 字节,那么就把明文按 16 字节分组,然后每个分组用密钥加密,最后能得到 n 组加密后的密文

4.3 固若金汤的根本: 数字签名与证书

  1. 摘要算法用来实现完整性,能够为数据生成独一无二的指纹,常用的算法是 SHA-2;
  2. 数字签名是私钥对摘要的加密,可以由公钥解密后验证,实现身份认证和不可否认;
  3. 公钥的分发需要使用数字证书,必须由 CA 的信任链来验证,否则就是不可信的;
  4. 作为信任链的源头 CA 有时也会不可信,解决办法有 CRL、OCSP,还有终止信任。

课下作业

  1. 为什么公钥能够建立信任链,用对称加密算法里的对称密钥行不行呢?
    非对称加密需要 公开公钥,让客户端解密。对称加密如果公开了密钥,就达不到加密效果了

  2. 假设有一个三级的证书体系(Root CA=> 一级 CA=> 二级 CA),你能详细解释一下证书信任链的验证过程吗?

    1. 客户端发现当前网站的证书是 二级 CA,在可信任的签发机构中找不到
    2. 就会拿二级 CA 的数字证书的签发机构去做检查,发现它是一级 CA,也不在可信的签发机构中
    3. 再找一级 CA 的数字证书的签发机构,发现受信任的 ROOT CA ,完成验证

    如果最后都没有找到可验证的数字证书,则验证失败

拓展阅读

  • 摘要算法除了用于 TLS 安全通信,还有很多其他的用途,比如:散列表、数据校验、大文件比较等。

  • 虽然 SHA-2 很安全,但出于未雨绸缪的考虑,又出现了 SHA-3,它也有 6 种算法,名字与 SHA-2 差不多,比如SHA3-224、SHA3-256,目前还未纳入 TLS。

  • 账号+密码 也能够实现简单的的身份认证,但在安全通信未建立前使用很容易就会被窃取,所以在 TLS 里不能用

  • Let's Encrypt 是著名的免费 CA,它只颁发 DV 证书,而且出于安全目的有效期只有 90 天,但可以用 Certbot 工具自动续订

  • 证书的格式遵循 ×5093 标准,有两种编码方式:

    • 一种是二进制的 DER
    • 另一种是 ASCII 码的 PEM,实验环境使用的是 PEM
  • 「操作系统和浏览器都内置了各大 CA 的根证书,上网的时候只要服务器发过来它的证书,就可以验证证书里的签名,顺着证书链(Certificate Chain)一层层地验,直到找到根证书」,服务器只返回了他的证书(假如返回的是二级证书),浏览器内置的是根证书(根公钥)使用根公钥只能解密根机构签名的证书,无法解密二级证书,使用一级证书(公钥)才能解密二级证,那么浏览器是怎么自下向上层层解析到根证书?

    服务器返回的是证书链,然后浏览器就可以使用信任的根证书(根公钥)解析证书链的根证书得到一级证书的公钥+摘要验签,然后拿一级证书的公钥解密一级证书拿到二级证书的公钥和摘要验签,再然后拿二级证书的公钥解密二级证书得到服务器的公钥和摘要验签,验证过程就结束了

    上述猜想是对的,服务器会在握手的时候返回整个证书链 ,但通常为了节约数据量,不会包含最终的根证书,因为根证书通常已经在浏览器或者操作系统里内置了

  • 重放和篡改的问题没有提,黑客是解不开秘文,但是可以重复发送,需要时间戳和随机数再合起来做一个不可逆的签名,服务端收到重复的就丢弃

    这个就是 nonce 参数

  • MD5(Message-Digest 5)、SHA-1(Secure Hash Algorithm 1),两个摘要算法,能够生成 16 字节和 20 字节长度的数字摘要,为什么实验环境中实际 MD5 算法生成的 32 字节长度的呢?(英文中一个字母占一个字节) sha1 算法生成的 是 40个字节长度的?还有 sha-2 的算法,生成的长度都是扩大了 2 倍

    md5、sha1 的摘要是二进制数据的 16 字节、20字节,不能直接看,所以做了 hex 编码,也就是一个字节变成了两个字符,所以扩大了两倍。

  • 如果有中间人,截获了证书,将证书替换成了自己申请的证书,这里假设中间人申请的证书和网站申请的证书是同一家的,确保用的都是相同的第三方公钥,那么这里是不是就会泄密了呢?

    证书体系中的中间人攻击是可行的,需要预先在客户端信任中间人的根证书,这样中间人就可以使用这个根证书来「伪造」证书,冒充原网站,像 fiddler 就是这么做的。

    简单修改证书是不行的,因为证书被 ca 签名,能够防窜改。而中间人没有 ca 的私钥,所以也无法伪造。

总结

TIP

下面总结有一个点,笔者需要强调下,下面过程中,先不要去想浏览器和客户端是如何交互的,先搞明白几个关键的技术他们是为了解决什么问题
在下一章节中会讲解 TLS 的握手协议流程,这里讲解了是如何使用下面这些知识点来达成 https 加密效果的

  • 对称加密(速度快):解决机密性,让没有密匙的人无法看到是什么内容
    但是对称加密使用的密匙是同一个,如何让密匙到达用户手中?直接传递?这时候只能用明文,否则浏览器无法解密,这就是 密匙交换 问题

  • 非对称加密(速度慢):解决密匙交换问题
    私钥持有人自己保存,公钥任何人可用。

    将 对称加密中的密钥 使用私钥加密后,发送给对方,对方再用公钥解密后,得到了后续用于 对称加密 的密钥。

    这个就是 TLS 的混合加密

    1. 用随机数产生 对称算法的会话密钥,使用会话级密匙来作为 对称加密/解密中的密匙
    2. 这样双方就可以使用这个密钥来进行内容的加密/解密了
  • 在上面,可以看到如果黑客 伪造公钥,那么你就和黑客在通话了。黑客也可以窃听收集足够多的密文,尝试重组修改后发送给网站。这就是缺乏了 完整性 和 身份认证

  • 摘要算法:
    把一个大空间映射到小空间,由于对输入具有 单向性 和 雪崩效应,可以用来做数据的完整性校验
    但是它不具备机密性,在混合加密系统里用 会话密钥加密消息和摘要,这个术语叫做 哈希消息认证码(HMAC)

  • 数字签名

    通信的两个端点(endpoint)  也就是你怎么证明是你?服务器怎么证明是服务器?

    非对称加密里的 私钥 ,使用私钥再加上摘要算法,就能够实现 数字签名 ,同时实现 身份认证 和 不可否认

    但又因为非对称加密效率太低,所以私钥只加密原文的摘要

    这里的私钥是你自己需要有一个 私钥 ,服务器也需要有一个 私钥,你们互相交换公钥,除非你们的私钥被泄密,否则身份认证和不可否认就能保证

  • 数字证书 和 CA

    公钥的信任 问题。因为谁都可以发布公钥,如何保证公钥不是伪造的?也就是说如何判定这个公钥是否是某宝的公钥呢。

    找一个 公认的可信第三方 ,让它作为「信任的起点,递归的终点」,构建起公钥的信任链。这就是 CA(Certificate Authority,证书认证机构) ,使用 CA 的私钥对你的 公钥进行签名(包含序列号、用途、颁发者、有效时间等等和你的公钥打包再签名),形成 数字证书(Certificate)

    那么 CA 怎么证明自己呢?这还是信任链的问题。小一点的 CA 可以让大 CA 签名认证 ,但链条的最后,也就是 Root CA ,就只能自己证明自己了。

    也就是说,我的公钥是 CA 的私钥签名的,那么我需要拿到该 CA 的公钥进行解密,解密成功才能证明没有被伪造,那么最后还是信任链的问题,最终解决办法就是 Root CA,这就叫 自签名证书(Self-Signed Certificate)或者 根证书(Root Certificate),有了这个证书体系,操作系统和浏览器都内置了各大 CA 的根证书

    也就是说,如果你的公钥不是 CA 颁发的,那么想要浏览器认为是安全的,就必须将它安装到系统的根证书存储区里。

4.4 信任始于握手: TLS 1.2 连接过程解析

TLS 的握手过程

下面的这张图简要地描述了 TLS 的握手过程,其中每一个「框」都是一个记录,多个记录组合成一个 TCP 包发送。所以,最多经过两次消息往返(4 个消息)就可以完成握手,然后就可以在安全的通信环境里发送 HTTP 报文,实现 HTTPS 协议。

image.png

ECDHE 握手过程

刚才你看到的是握手过程的简要图,我又画了一个详细图,对应 Wireshark 的抓包,下面我就用这个图来仔细剖析 TLS 的握手过程。

image.png

RSA 握手过程

整个握手过程可真是够复杂的,但你可能会问了,好像这个过程和其他地方看到的不一样呢?

刚才说的其实是如今主流的 TLS 握手过程,这与传统的握手有两点不同。

第一个,使用 ECDHE 实现密钥交换,而不是 RSA,所以会在服务器端发出 Server Key Exchange 消息。

第二个,因为使用了 ECDHE,客户端可以不用等到服务器发回 Finished 确认握手完毕,立即就发出 HTTP 报文,省去了一个消息往返的时间浪费。这个叫 TLS False Start ,意思就是抢跑,和 TCP Fast Open 有点像,都是不等连接完全建立就提前发应用数据,提高传输的效率。

下图实现了传统的 RSA 密钥交换,没有 False Start

image.png 大体的流程没有变,只是 Pre-Master 不再需要用算法生成,而是客户端直接生成随机数,然后用服务器的公钥加密,通过 Client Key Exchange 消息发给服务器。服务器再用私钥解密,这样双方也实现了共享三个随机数,就可以生成主密钥。

这个相对简单一点,下面是流程:

  1. 客户端连上服务端

  2. 服务端发送 CA 证书给客户端

    发送的是一个 CA 链,不包含 ROOT 证书

  3. 客户端验证该证书的可靠性

    解析 CA 链,用链条上的 CA 进行验证,一层层地验证,直到找到根证书,就能够确定证书是可信的

  4. 客户端从 CA 证书中取出公钥

  5. 客户端生成一个随机密钥 k,并用这个公钥加密得到 k*

  6. 客户端把 k* 发送给服务端

  7. 服务端收到 k* 后用自己的私钥解密得到 k

  8. 此时双方都得到了密钥 k,协商完成

  9. 后续使用密钥 k* 加密信息

攻击方式:

  • 防偷窥(嗅探)

    攻击者虽然可以监视网络流量并拿到公钥,但是 无法 通过公钥推算出私钥(这点由 RSA 算法保证)

    攻击者虽然可以监视网络流量并拿到 k*,但是攻击者没有私钥,无法解密 k*  ,因此也就无法得到 k

  • 如何防范篡改(假冒身份)

    如果攻击者在第 2 步篡改数据,伪造了证书,那么客户端在第 3 步会发现(这点由证书体系保证)

    如果攻击者在第 6 步篡改数据,伪造了 k* ,那么服务端收到假的 k* 之后,解密会失败(这点由 RSA 算法保证)。服务端就知道被攻击了。

双向认证

到这里 TLS 握手就基本讲完了。

不过上面说的是 单向认证 握手过程,只认证了服务器的身份,而没有认证客户端的身份。这是因为通常单向认证通过后已经建立了安全通信,用账号、密码等简单的手段就能够确认用户的真实身份。

但为了防止账号、密码被盗,有的时候(比如网上银行)还会使用 U 盾给用户颁发客户端证书,实现 双向认证 ,这样会更加安全。

双向认证的流程也没有太多变化,只是在 Server Hello Done 之后,Client Key Exchange 之前,客户端要发送 Client Certificate 消息,服务器收到后也把证书链走一遍,验证客户端的身份。

小结

今天我们学习了 HTTPS/TLS 的握手,内容比较多、比较难,不过记住下面四点就可以。

  1. HTTPS 协议会先与服务器执行 TCP 握手,然后执行 TLS 握手,才能建立安全连接;
  2. 握手的目标是安全地交换对称密钥,需要三个随机数,第三个随机数 Pre-Master 必须加密传输,绝对不能让黑客破解;
  3. Hello 消息交换随机数,Key Exchange 消息交换 Pre-Master ;
  4. Change Cipher Spec 之前传输的都是明文,之后都是对称密钥加密的密文。

拓展阅读

  • TLS 中记录协议原本定义有压缩方式,但后来发现存在安全漏洞(CRME 攻击),所以现在这个字段总是 NULL,即不压缩。
  • 在 TLS1.2 里,客户端和随机数的长度都是 28 字节,前面的四个字节是 UNX 时间戳,但并没有实际意义。
  • Chrome 开发者工具的 Security 面板里可以看到 Https 握手时选择的版本号、密码套件和椭圆曲线,例如 ECDHE_RSA With X25519,and AES_256_GCM
  • ECDHE 即 「短暂-椭圆曲线-迪菲-赫尔曼」(ephemeral Elliptic Curve Diffe-Hellman) 算法,使用椭圆曲线增强了DH 算法的安全性和性能,公钥和私钥都是临时生成的。
  • 在 Wireshark 抓包里你还会看见 Session iDExtension 等字段,涉及会话复用和扩展协议,后面会讲到。

4.5 更好更快的握手: TLS 1.3 特性解析

  1. 为了兼容 1.1、1.2 等“老”协议,TLS1.3 会“伪装”成 TLS1.2,新特性在“扩展”里实现;
  2. 1.1、1.2 在实践中发现了很多安全隐患,所以 TLS1.3 大幅度删减了加密算法,只保留了 ECDHE、AES、ChaCha20、SHA-2 等极少数算法,强化了安全;
  3. TLS1.3 也简化了握手过程,完全握手只需要一个消息往返,提升了性能。

课下作业

  1. TLS1.3 里的密码套件没有指定密钥交换算法和签名算法,那么在握手的时候会不会有问题呢?

    TLS1.3 精简了加密算法,通过 support_groups、key_share、signature_algorithms 这些参数就能判断出密钥交换算法和签名算法,不用在 cipher suite 中协商了

  2. 结合上一讲的 RSA 握手过程,解释一下为什么 RSA 密钥交换不具有 前向安全

    RSA 握手时,client key exchage 会使用 RSA 公钥加密 pre master 后传给服务端,一旦私钥被破解,那么之前的信息都会被破译,根本原因还是在于 RSA 的这一对公钥私钥并不是临时的

  3. TLS1.3 的握手过程与 TLS1.2 的 False Start 有什么异同?

    相同点:都在未收到 Finished 确认消息时就已经向对方发送加密信息了,不同点:TLS1.3 将 change cipher spec 合并到了 hello 中

拓展阅读

  • 对 TLS12 已知的攻击有 BEAST、 BREACH、CRME、 FREAK、 LUCKY13、 POODLE、ROBOT 等
  • 虽然 TLS1.3 到今天刚满一岁,但由于有之前多个草案的实践,各大浏览器和服务器基本都 已经实现了支持,整个互联网也正在快速向 TLS1.3 迁移。
  • 关于前向安全最著名的案例就是斯诺登于 2013 年爆出的棱镜计划
  • 在 TLS1.3 的 RFC 文档里已经删除了 Change Cipher Spec 子协议,但用 Wireshark 抓包 却还能看到,这里以抓包为准
  • TLS1.3 还提供了降级保护机制,如果 「中间人」恶意降级到 1.2,服务器的随机数最后 8 个字节会被设置为 44 4F 57 4E 47 52 44 01,即 DOWNGRD01,支持 TLS1.3 的客户端就可以检查发现被降级,然后发出警报终止连接

4.6 连接太慢该怎么办 HTTPS 的优化

  1. 可以有多种硬件和软件手段减少网络耗时和计算耗时,让 HTTPS 变得和 HTTP 一样快,最可行的是软件优化;
  2. 应当尽量使用 ECDHE 椭圆曲线密码套件,节约带宽和计算量,还能实现 False Start ;
  3. 服务器端应当开启 OCSP Stapling 功能,避免客户端访问 CA 去验证证书;
  4. 会话复用的效果类似 Cache,前提是客户端必须之前成功建立连接,后面就可以用 Session IDSession Ticket 等凭据跳过密钥交换、证书验证等步骤,直接开始加密通信。

课下作业

  1. 你能比较一下 Session IDSession TicketPSK 这三种会话复用手段的异同吗?

    • Session ID

      类似网站开发中用来验证用户的 cookie,服务器会保存 Session ID对应的主密钥,需要用到服务器的存储空间

    • Session Ticket

      类似网站开发中的 JWT(JSON Web Token),JWT 的做法是服务器将必要的信息(主密钥和过期时间)加上密钥进行 HMAC 加密,然后将生成的密文和原文相连得到 JWT 字符串,交给客户端。当客户端发送 JWT 给服务端后,服务器会取出其中的原文和自己的密钥进行 HMAC 运算,如果得到的结果和 JWT 中的密文一样,就说明是服务端颁发的 JWT,服务器就会认为 JWT 存储 的主密钥和有效时间是有效的。另外,JWT 中不应该存放用户的敏感信息,明文部分任何人可见

    • PSK

      psk 实际上是 Session Ticket 的强化版,本身也是缓存,但它简化了 Session Ticket 的协商过程,省掉了一次 RTT

拓展阅读

  • 使用 SSL加速卡 的一个案例是阿里的 Tengine,它基于 Intel QAT 加速卡,定制了 Nginx 和 OpenSSL

  • 因为 OCSP 会增加额外的网络连接成本,所以 Chrome 等浏览器的策略是只对 EV 证书使用 OCSP 检查有效性,普通网站使用 DV、OV 证书省略了这个操作,就会略微快一点

  • 在 Nginx 里可以用指令 ssl_stapling on 开启 OCSP Stapling,而在 OpenResty 里更 可以编写 Lua 代码灵活定制

  • Session ID 和 Session Ticket 这两种会话复用技术在 TLS1.3 中均已经被废除,只能使用 PSK 实现会话复用

  • 常见的对信息安全系统的攻击手段有 重放攻击( Replay attack)和 中间人攻击 (Man-nthe- middle attack),还有一种叫 社会工程学 ( Social engineering attack),它不属于计算机科学或密码学,而是利用了人性的弱点

  • 预共享密钥的 0-RTT 不是真的 0-RTT 吧

    当然是 0-rtt,不过是指在建立 tcp 连接后的 0-rtt,也就是 tcp 握手之后立即发送应用数据,不需要再次 tls 握手

4.7 我应该迁移到 HTTPS 吗?

  1. 从 HTTP 迁移到 HTTPS 是大势所趋,能做就应该尽早做;
  2. 升级 HTTPS 首先要申请数字证书,可以选择免费好用的 Let’s Encrypt;
  3. 配置 HTTPS 时需要注意选择恰当的 TLS 版本和密码套件,强化安全;
  4. 原有的 HTTP 站点可以保留作为过渡,使用 301 重定向到 HTTPS。

拓展阅读

  • 也有少数知名网站仍然坚持使用 HTTP,例如 nginx. org、 apache.org

  • SN 使用明文表示域名,也就提前暴露了一部分 Https 的信息,有安全隐患,容易被中间人 发起拒绝攻击,被认为是 TLS 盔甲上最后的一个缝隙,目前正在起草 ESN 规范

  • HSTS 无法防止黑客对第一次访问的攻击,所以 Chrome 等浏览器还内置了一个 HSTS preload 的列表(chrome://net-internals/#hsts ),只要域名在这个列表里,无论何时都会强制使用 Https 访问

  • HPKP ( Http Public Key Pinning) 是另一种 Https 安全技术,指示客户端固定网站使用的公钥,防止中间人攻击,但因为接受程度过低,现在已经被放弃

  • 如果要支持老的 WindowsXP 和 IE6,可以选择开启 SSLv3 和 RSA、RC4、SHA

  • 之前在实验环境访问 HTTP 协议时可以看到请求头里有 Upgrade- nsecure- Requests:1,它就是 CSP 的一种,表示浏览器支持升级到 Https 协议

  • 文中提到的虚拟主机,跟正向代理,反向代理,有什么区别?

    虚拟主机与代理没有关系,是 http 服务器里的概念,在一个 ip 地址上存在多个域名(即主机),所以叫「虚拟主机」

    因为不能用 ip 地址区分,所以就要用 host 字段,区分不同的主机(域名、网站)。

5. 飞翔篇

5.1 时代之风:HTTP/2 特性概览

下面的这张图对比了 HTTP/1、HTTPS 和 HTTP/2 的协议栈,你可以清晰地看到,HTTP/2 是建立在 HPackStreamTLS1.2 基础之上的,比 HTTP/1、HTTPS 复杂了一些。

image.png

  1. HTTP 协议取消了小版本号,所以 HTTP/2 的正式名字不是 2.0;
  2. HTTP/2 在语义上兼容 HTTP/1,保留了请求方法、URI 等传统概念;
  3. HTTP/2 使用 HPACK 算法压缩头部信息,消除冗余数据节约带宽;
  4. HTTP/2 的消息不再是 Header+Body 的形式,而是分散为多个二进制帧;
  5. HTTP/2 使用虚拟的流传输消息,解决了困扰多年的队头阻塞问题,同时实现了多路复用,提高连接的利用率;
  6. HTTP/2 也增强了安全性,要求至少是 TLS1.2,而且禁用了很多不安全的密码套件。

课下作业

  1. 你觉得明文形式的 HTTP/2(h2c)有什么好处,应该如何使用呢?

    感觉没有太大好处吧,header 和 body 都序列化成 byte 了

  2. 你觉得应该怎样理解 HTTP/2 里的流,为什么它是虚拟的?

header 和 body 都用 Frame 封装投递,同一个消息使用逻辑 id 来区分,按照 id 聚合出一个消息,那么就可以乱序发送

  1. 你能对比一下 HTTP/2 与 HTTP/1、HTTPS 的相同点和不同点吗?

    在语义上是相同的,报文格式发生了变化、请求头也可以被压缩、服务器还可以主动推送

拓展阅读

  • 在早期还有一个 HTTP-NG (HttpNext Generation) 项目,最终失败了
  • HTTP/2 的前身 SPDY 在压缩头部时使用了 gzip,但发现会受到 CRME 攻击,所以开发了专用的压缩算法 HPACK
  • HTTP/2 里的流可以实现 HTTP/1 里的管道(pipeline)功能,而且综合性能更好,所以「管道」在 HTTP/2 里就被废弃了
  • 如果你写过 Linux 程序,用过 epol,就应该知道 epo 也是一种多路复用,不过它是 I/O Multiplexing
  • HTTP/2 要求必须实现的密码套件是 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 ,比 TLS12 默认的 TLS_RSA_WITH_AES_128_CBC_SHA 的安全强度高了很多
  • 实验环境的 www.metroid.net 启用了 RSA 和 ECC 双证书,在浏览器里可以看到实际连接时用的会是 ECC 书。另外,这个域名还用到了重定向跳转技术,使用 301 跳转,把 80/443 端口的请求重定向到 HTTP/2 的 8443

5.2 时代之风:HTTP/2 内核剖析

  1. HTTP/2 必须先发送一个连接前言字符串,然后才能建立正式连接;
  2. HTTP/2 废除了起始行,统一使用头字段,在两端维护字段 Key-Value 的索引表,使用 HPACK 算法压缩头部;
  3. HTTP/2 把报文切分为多种类型的二进制帧,报头里最重要的字段是流标识符,标记帧属于哪个流;
  4. 流是 HTTP/2 虚拟的概念,是帧的双向传输序列,相当于 HTTP/1 里的一次请求 - 应答;
  5. 在一个 HTTP/2 连接上可以并发多个流,也就是多个“请求 - 响应”报文,这就是多路复用。

课下作业

  1. HTTP/2 的动态表维护、流状态转换很复杂,你认为 HTTP/2 还是「无状态」的吗?
    还是无状态,流状态只是表示流是否建立,单次请求响应的状态。并非会话级的状态保持

  2. HTTP/2 的帧最大可以达到 16M,你觉得大帧好还是小帧好?
    小帧好,少量多次,万一拥堵重复的的少。假设大帧好,只要分流不用分帧了。

  3. 结合这两讲,谈谈 HTTP/2 是如何解决队头阻塞问题的。
    每一个请求响应都是一个流,流和流之间可以并行,流内的帧还是有序串行

拓展阅读

  • 你一定很好奇 HTTP/2 连接前言的来历吧,其实把里面的字符串连起来就是 PRSM,也就是 2013 年斯诺登爆岀的棱镜计划。

  • 在 HTTP/1 里头字段是不区分大小写的,这在实践中造成了一些混乱,写法很随意,所以 HTTP/2 做出了明确的规定,要求所有的头字段必须全小写,大写会认为是格式错误。

  • HPACK 的编码规则比较复杂,使用了一些特殊的标志位,所以在 Wireshark 抓包里不会直接看到字段的索引号,需要按照规则解码。

  • HEADERS 帧后还可以接特殊的「CONTINUATION」帧,发送特别大的头,最后一个「CONTNUATION」需要设置标志位 END HEADERS 表示头结束。

  • 服务器端发起推送流需要使用 PUSH_PROMSE 帧,状态转换与客户端流基本类似,只是方向不同。

  • 在 RST_STREAM 和 GOAWAY 帧里可以携带 32 位的错误代码,表示终止流的原因,它是真正的「错误」,与状态码的含义是不同的。

  • 服务端是不是要为每一个客户端都单独维护一份索引表?连接的客户端多了的话内存不就 OOM 了

    是的,不过动态表也有淘汰机制,服务器可以自己定制策略,不会过度占用内存。

5.3 未来之路:HTTP/3 展望

HTTP/3 综合了我们之前讲的所有技术(HTTP/1、SSL/TLS、HTTP/2),包含知识点很多,比如队头阻塞、0-RTT 握手、虚拟的流、多路复用,算得上是集大成之作,需要多下些功夫好好体会。

  1. HTTP/3 基于 QUIC 协议,完全解决了队头阻塞问题,弱网环境下的表现会优于 HTTP/2;
  2. QUIC 是一个新的传输层协议,建立在 UDP 之上,实现了可靠传输;
  3. QUIC 内含了 TLS1.3,只能加密通信,支持 0-RTT 快速建连;
  4. QUIC 的连接使用不透明的连接 ID,不绑定在 IP 地址 + 端口上,支持连接迁移;
  5. QUIC 的流与 HTTP/2 的流很相似,但分为双向流和单向流;
  6. HTTP/3 没有指定默认端口号,需要用 HTTP/2 的扩展帧 Alt-Svc 来发现。

课下作业

  1. IP 协议要比 UDP 协议省去 8 个字节的成本,也更通用,QUIC 为什么不构建在 IP 协议之上呢?

    传输层 TCP 和 UDP 就够了,在多加会提高复杂度,基于 UDP 向前兼容会好一些

  2. 说一说你理解的 QUIC、HTTP/3 的好处。

    在传输层解决了队首阻塞,基于 UDP 协议,在网络拥堵的情况下,提高传输效率,原生封装 TLS,实现安全加密、连接迁移、多路复用

  3. 对比一下 HTTP/3 和 HTTP/2 各自的流、帧,有什么相同点和不同点。

    http3 在传输层基于 UDP 真正解决了队头阻塞。http2 只是部分解决。

拓展阅读

  • 根据当前的标准草案,QUC 已经不再是 Quick UDP Internet Connections (快速 UDP 互联网连接)的缩写了,QUC 就是 QUIC 。
  • QUC 早期还有一个前向纠错( Forward Error Correction) 的特性,通过发送 xor 冗余数据来实现数据校验和恢复,但目前此特性已经被搁置,也许会在以后的版本里出现。
  • QUC 虽然是个传输层协议,但它并不由操作系统内核实现,而是运行在用户空间,所以能够不受操作系统的限制,快速迭代演化,有点像 Inte 的 DPDK
  • QUC 里的包分为长包和短包两类,长包的第一个字节高位是 1,格式比较完整,而短包只有目标连接 ID
  • QUC 和 HTTP/3 的变长编码使用第一个字节的高两位决定整数的长度,最多是 8 个字节( 64 位),所以最大值是 2^62
  • HTTP/3 的帧不再需要 ENDHEADERS 标志位和 CONTINUATION 帧,因为帧的长度足够大(2^62),无论是多大的头都可以用一个帧传输。

5.4 我应该迁移到 HTTP/2 吗?

  1. HTTP/2 完全兼容 HTTP/1,是更安全的 HTTP、更快的 HTTPS,头部压缩、多路复用等技术可以充分利用带宽,降低延迟,从而大幅度提高上网体验;
  2. TCP 协议存在队头阻塞,所以 HTTP/2 在弱网或者移动网络下的性能表现会不如 HTTP/1;
  3. 迁移到 HTTP/2 肯定会有性能提升,但高流量网站效果会更显著;
  4. 如果已经升级到了 HTTPS,那么再升级到 HTTP/2 会很简单;
  5. TLS 协议提供 ALPN 扩展,让客户端和服务器协商使用的应用层协议,发现 HTTP/2 服务。

课下作业

  1. 结合自己的实际情况,分析一下是否应该迁移到 HTTP/2,有没有难点?
    分情况吧,在 HTTPS 的基础上,迁移很简单。

  2. 精灵图(Spriting)、资源内联(inlining)、域名分片(Sharding)这些手段为什么会对 HTTP/2 的性能优化造成反效果呢?
    因为 HTTP/2 中使用小颗粒化的资源,优化了缓存,而使用精灵图就相当于传输大文件,但是大文件会延迟客户端的处理执行,并且缓存失效的开销很昂贵,很少数量的数据更新就会使整个精灵图失效,需要重新下载(http1 中使用精灵图是为了减少请求)

    HTTP1 中使用内联资源也是为了减少请求,内联资源没有办法独立缓存,破坏了 HTTP/2 的多路复用和优先级策略;

    域名分片在 HTTP1 中是为了突破浏览器每个域名下同时连接数,但是这在 HTTP/2 中使用多路复用解决了这个问题,如果使用域名分片反而会限制 HTTP2 的自由发挥

拓展阅读

  • Nginx 也支持明文形式的 HTP/2(即 h2c ),在配置 listen 指令时不添加 ss 参数即可,但无法使用 Chrome 等浏览器直接测试,因为浏览器只支持 h2
  • HTTP/2 的优先级只使用一个字节,优先级最低是 0,最高是 255,一些过时的书刊和网上资料中把 HTTP/2 的优先级写成了 2^31,是非常错误的。
  • ALPN 的前身是 Google 的 NPN(Next Protocol Negotiation),它与 ALPN 的协商过程刚好相反,服务器提供支持的协议列表,由客户端决定最终使用的协议
  • 明文的 HTTP/2( h2c ) 不使用 TLS,也就无法使用 ALPN 进行协议协商,所以需要使用头字段 Connection: Upgrade 升级到 HTTP/2,服务器返回状态码 101 切换协议
  • 目前国内已经有不少大网站迁移到了 HTTP/2,比如 www.qg.com、www.tmal.com,你可以用 Chrome 的开发者工具检查它们的 Protocol

6. 探索篇

6.1 Nginx:高性能的 Web 服务器

  1. Nginx 是一个高性能的 Web 服务器,它非常的轻量级,消耗的 CPU、内存很少;
  2. Nginx 采用 master/workers 进程池架构,不使用多线程,消除了进程、线程切换的成本;
  3. Nginx 基于 epoll 实现了 I/O 多路复用 ,不会阻塞,所以性能很高;
  4. Nginx 使用了职责链模式,多个模块分工合作,自由组合,以流水线的方式处理 HTTP 请求。

课下作业

  1. 你是怎么理解进程、线程上下文切换时的成本的,为什么 Nginx 要尽量避免?

    一个线程的时间片没用完,系统调用就被系统调度切换出去,浪费了剩余的时间片,nginx 通过 epoll 和注册回调,和非阻塞 io 自己在用户态主动切换上下文,充分利用了系统分配给进程或者线程的时间片,所以对系统资源利用很充分。

拓展阅读

  • 也有不少的人把 Nginx 读成 NGks,这就错得太多了。
  • Nginx 自 1.7.11 开始引入了「多线程」,但只是作为辅助手段,卸载阳塞的磁盘 I/O 操作,主要的 HTTP 请求处理使用的还是单线程里的 epoll
  • 如何让Web服务器能够高效地处理 10K 以上的并发请求( Concurrent 10K ),这就是著名的 C10K 问题,当然它早已经被 epo/kqueue 等解决了,现在的新问题是 C10M
  • Nginx 的 PRECONTENT 阶段在 1.13.3 之前叫 TRY FILES,仅供 Nginx 内部使用,用户不可介入
  • 正文里的流水线图没有画出 filter 模块所在的位置,它其实是在 CONTENT 阶段的末尾,专门过滤响应数据

6.2 OpenResty:更灵活的 Web 服务器

  1. Nginx 依赖于磁盘上的静态配置文件,修改后必须重启才能生效,缺乏灵活性;
  2. OpenResty 基于 Nginx,打包了很多有用的模块和库,是一个高性能的 Web 开发平台;
  3. OpenResty 的工作语言是 Lua,它小巧灵活,执行效率高,支持“代码热加载”;
  4. OpenResty 的核心编程范式是 同步非阻塞 ,使用协程,不需要异步回调函数;
  5. OpenResty 也使用阶段式处理的工作模式,但因为在阶段里执行的都是 Lua 代码,所以非常灵活,配合 Redis 等外部数据库能够实现各种动态配置。

6.3 WAF:保护我们的网络服务

“网络应用防火墙”,也就是 WAF,使用它可以加固 Web 服务。

  1. Web 服务通常都运行在公网上,容易受到 DDoS、代码注入等各种黑客攻击,影响正常的服务,所以必须要采取措施加以保护;
  2. WAF 是一种 HTTP 入侵检测和防御系统,工作在七层,为 Web 服务提供全面的防护;
  3. ModSecurity 是一个开源的、生产级的 WAF 产品,核心组成部分是 规则引擎 和 规则集 ,两者的关系有点像杀毒引擎和病毒特征库;
  4. WAF 实质上是模式匹配与数据过滤,所以会消耗 CPU,增加一些计算成本,降低服务能力,使用时需要在安全与性能之间找到一个平衡点。

6.4 CDN:加速我们的网络服务

CDN 发展到现在已经有二十来年的历史了,早期的 CDN 功能比较简单,只能加速静态资源。随着这些年 Web 2.0、HTTPS、视频、直播等新技术、新业务的崛起,它也在不断进步,增加了很多的新功能,比如 SSL 加速、内容优化(数据压缩、图片格式转换、视频转码)、资源防盗链、WAF 安全防护等等。

现在,再说 CDN 是搬运工已经不太准确了,它更像是一个无微不至的网站保姆,让网站只安心生产优质的内容,其他的杂事都由它去代劳。

  1. 由于客观地理距离的存在,直连网站访问速度会很慢,所以就出现了 CDN;
  2. CDN 构建了全国、全球级别的专网,让用户就近访问专网里的边缘节点,降低了传输延迟,实现了网站加速;
  3. GSLB 是 CDN 的“大脑”,使用 DNS 负载均衡技术,智能调度边缘节点提供服务;
  4. 缓存系统是 CDN 的“心脏”,使用 HTTP 缓存代理技术,缓存命中就返回给用户,否则就要回源。

课下作业

  1. 网站也可以自建同城、异地多处机房,构建集群来提高服务能力,为什么非要选择 CDN 呢?

    自建成本太高,一般的公司玩不起

  2. 对于无法缓存的动态资源,你觉得 CDN 也能有加速效果吗?

    cdn 一般有专用的高速网络直连源站,或者是动态路径优化,所以动态资源回源要比通过公网速度快很多。

拓展阅读

  • 关于静态资源和动态资源,更准确的说法是只要 Cache-Control 允许缓存,就是静态资 源,否则就是动态资源、
  • 目前应用最广泛的 DNS 软件是开源的 BIND9(Berkeley Internet Name Domain),OpenResty 则使用 stream_ lua 实现了纯 Lua 的 DNS 服务
  • CDN 里除了核心的负载均衡和缓存系统,还有其他的辅助系统,比如管理、监控、日志、统计 计费等。
  • ATS 源自雅虎,后来被捐献给了 Apache 基金会,它使用 C++ 开发,性能好,但内部结构复杂,定制不太容易。
  • CDN 大厂 CloudFlare 的系统就都是由 Nginx/OpenResty 驱动的,而 OpenResty 公司的主要商业产品 OpenRestyEdge 也是 CDN
  • 当前的 CDN 也有了云化的趋势,很多商都把 CDN 作为一项标配服务

6.5 WebSocket:沙盒里的 TCP

浏览器是一个沙盒环境,有很多的限制,不允许建立 TCP 连接收发数据,而有了 WebSocket,我们就可以在浏览器里与服务器直接建立 TCP 连接,获得更多的自由。

不过自由也是有代价的,WebSocket 虽然是在应用层,但使用方式却与 TCP Socket 差不多,过于原始,用户必须自己管理连接、缓存、状态,开发上比 HTTP 复杂的多,所以是否要在项目中引入 WebSocket 必须慎重考虑。

  1. HTTP 的请求 - 应答模式不适合开发实时通信应用,效率低,难以实现动态页面,所以出现了 WebSocket;
  2. WebSocket 是一个全双工的通信协议,相当于对 TCP 做了一层薄薄的包装,让它运行在浏览器环境里;
  3. WebSocket 使用兼容 HTTP 的 URI 来发现服务,但定义了新的协议名 ws 和 wss ,端口号也沿用了 80 和 443;
  4. WebSocket 使用二进制帧,结构比较简单,特殊的地方是有个“掩码”操作,客户端发数据必须掩码,服务器则不用;
  5. WebSocket 利用 HTTP 协议实现连接握手,发送 GET 请求要求协议升级,握手过程中有个非常简单的认证机制,目的是防止误连接。

课下作业

  1. WebSocket 与 HTTP/2 有很多相似点,比如都可以从 HTTP/1 升级,都采用二进制帧结构,你能比较一下这两个协议吗?
    WebSocket 和 HTTP/2 都是用来弥补HTTP 协议的一些缺陷和不足,
    WebSocket 主要解决双向通信问题、全双工问题;
    HTTP/2 主要解决传输效率问题,两者在二进制帧数据的格式上也不太一样,HTTP/2 有多路复用、优先级和流的概念。

  2. 试着自己解释一下 WebSocket 里的”Web“和”Socket“的含义。
    笔者认为:
    Web 指的是 HTTP,Socket 是套接字调用,那么这两个连起来又是什么意思呢? 所谓望文生义,大概你也能猜出来,WebSocket 就是运行在 Web,也就是 HTTP 上的 Socket 通信规范,提供与 TCP Socket 类似的功能,使用它就可以像 TCP Socket 一样调用下层协议栈,任意地收发数据。

  3. 结合自己的实际工作,你觉得 WebSocket 适合用在哪些场景里?
    WebSocket 适合实时通信交互的场景。

7. 总结

7.1 HTTP性能优化上篇

image.png

可以从HTTP服务器、HTTP客户端HTTP传输链路这三个方面进行优化。

HTTP 服务器

衡量服务器性能的主要指标有三个:吞吐量(requests per second)、并发数(concurrency)和 响应时间(time per request)。

吞吐量就是我们常说的 RPS,每秒的请求次数,也有叫 TPS、QPS,它是服务器最基本的性能指标,RPS 越高就说明服务器的性能越好。

并发数反映的是服务器的负载能力,也就是服务器能够同时支持的客户端数量,当然也是越多越好,能够服务更多的用户。

响应时间反映的是服务器的处理能力,也就是快慢程度,响应时间越短,单位时间内服务器就能够给越多的用户提供服务,提高吞吐量和并发数。 除了上面的三个基本性能指标,服务器还要考虑 CPU、内存、硬盘和网卡等系统资源的占用程度,利用率过高或者过低都可能有问题。

在 HTTP 多年的发展过程中,已经出现了很多成熟的工具来测量这些服务器的性能指标,开源的、商业的、命令行的、图形化的都有。

HTTP 客户端

客户端的基本性能指标是延迟,影响因素有地理距离、带宽、DNS 查询、TCP 握手等。

下面是一个直观的瀑布图(Waterfall Chart),清晰地列出了页面中所有资源加载的先后顺序和时间消耗。

img

Chrome 等浏览器自带的开发者工具也可以很好地观察客户端延迟指标,面板左边有每个 URI 具体消耗的时间,面板的右边也是类似的瀑布图。

点击某个 URI,在 Timing 页里会显示出一个小型的“瀑布图”,是这个资源消耗时间的详细分解,延迟的原因都列的清清楚楚,比如下面的这张图:

img

图里面的这些指标都是什么含义呢?我给你解释一下:

  • Queued:因为有队头阻塞,浏览器对每个域名最多开 6 个并发连接(HTTP/1.1),当页面里链接很多的时候就必须排队等待(Queued、Queueing),这里它就等待了 1.62 秒,然后才被浏览器正式处理;
  • stalled:浏览器要预先分配资源,调度连接,花费了 11.56 毫秒(Stalled);
  • DNS Lookup:连接前必须要解析域名,这里因为有本地缓存,所以只消耗了 0.41 毫秒(DNS Lookup);
  • Initial connection、SSL:与网站服务器建立连接的成本很高,总共花费了 270.87 毫秒,其中有 134.89 毫秒用于 TLS 握手,那么 TCP 握手的时间就是 135.98 毫秒(Initial connection、SSL);
  • Request sent:实际发送数据非常快,只用了 0.11 毫秒(Request sent);
  • Waiting:之后就是等待服务器的响应,专有名词叫 TTFB(Time To First Byte),也就是“首字节响应时间”,里面包括了服务器的处理时间和网络传输时间,花了 124.2 毫秒;
  • Content Dowload:接收数据也是非常快的,用了 3.58 毫秒(Content Dowload)。

从这张图你可以看到,一次 HTTP 请求 - 响应 的过程中延迟的时间是非常惊人的,总时间 415.04 毫秒里占了差不多 99%。

所以,客户端 HTTP 性能优化的关键就是:降低延迟。

HTTP 传输链路

image.png 第一公里:是指网站的出口 ,也就是服务器接入互联网的传输线路,它的带宽直接决定了网站对外的服务能力,也就是吞吐量等指标。显然,优化性能应该在这“第一公里”加大投入,尽量购买大带宽,接入更多的运营商网络。

中间一公里:就是由许多小网络组成的实际的互联网 ,其实它远不止一公里,而是非常非常庞大和复杂的网络,地理距离、网络互通都严重影响了传输速度。好在这里面有一个 HTTP 的好帮手——CDN,它可以帮助网站跨越千山万水,让这段距离看起来真的就好像只有一公里。

最后一公里:是用户访问互联网的入口 ,对于固网用户就是光纤、网线,对于移动用户就是 WiFi、基站。以前它是客户端性能的主要瓶颈,延迟大带宽小,但随着近几年 4G 和高速宽带的普及,最后一公里的情况已经好了很多,不再是制约性能的主要因素了。

除了这三公里,我个人认为还有一个第零公里, 就是网站内部的 Web 服务系统。它其实也是一个小型的网络(当然也可能会非常大),中间的数据处理、传输会导致延迟,增加服务器的响应时间,也是一个不可忽视的优化点。

在上面整个互联网传输链路中,末端的最后一公里我们是无法控制的,所以我们只能在第零公里、第一公里和中间一公里这几个部分下功夫,增加带宽降低延迟,优化传输速度。

小结

  1. 性能优化是一个复杂的概念,在 HTTP 里可以分解为服务器性能优化、客户端性能优化和传输链路优化;
  2. 服务器有三个主要的性能指标:吞吐量、并发数和响应时间,此外还需要考虑资源利用率;
  3. 客户端的基本性能指标是延迟,影响因素有地理距离、带宽、DNS 查询、TCP 握手等;
  4. 从服务器到客户端的传输链路可以分为三个部分,我们能够优化的是前两个部分,也就是第一公里和中间一公里;
  5. 有很多工具可以测量这些指标,服务器端有 ab、top、sar 等,客户端可以使用测试网站,浏览器的开发者工具。

拓展阅读

  • HTTP 性能优化是 Web 性能优化的一部分,后者涉及的范围更广,除了 HTTP 协议,还包含 HTML、CSS、 JavaScript 等方面的优化。例如为了优化页面渲染顺序,CSS 应该放在 HTML 顶部,而 JavaScript 应该放在 HTML 的底部
  • 更高级的服务器性能测试工具有 Load Runner、JMeter 等,很多云服务商也会提供专业的测试平台。
  • 在 Chrome 开发者工具的瀑布图里可以看到有两条蓝色和红色的竖线。蓝线表示的是 DOM Ready,也就是说浏览器已经解析完 HTML 文档的 DOM 结构;红线表示的是 Load Complete,即已经下载完页面包含的所有资源(JS、CSS、图片等)
  • 还记得几年前的 光进铜退 吗?以前都是用电话线里的铜上网,用的是 ADSL,网速只有 10M 左右,现在都变成了光纤入户,网速通常都是 100M 起步。

7.2 HTTP性能优化下篇

小结

  1. 花钱购买硬件、软件或者服务可以直接提升网站的服务能力,其中最有价值的是 CDN;
  2. 不花钱也可以优化 HTTP,三个关键词是开源、节流和缓存;
  3. 后端应该选用高性能的 Web 服务器,开启长连接,提升 TCP 的传输效率(即开源);
  4. 前端应该启用 gzip、br 压缩,减小文本、图片的体积,尽量少传不必要的头字段(即节流);
  5. 缓存是无论何时都不能忘记的性能优化利器,应该总使用 Etag 或 Last-modified 字段标记资源;
  6. 升级到 HTTP/2 能够直接获得许多方面的性能提升,但要留意一些 HTTP/1 的反模式。

开源:是指抓源头,开发网站服务器自身的潜力,在现有条件不变的情况下尽量挖掘出更多的服务能力。

首先,我们应该选用高性能的 Web 服务器,最佳选择当然就是 Nginx/OpenResty 了,尽量不要选择基于 Java、Python、Ruby 的其他服务器,它们用来做后面的业务逻辑服务器更好。利用 Nginx 强大的反向代理能力实现动静分离,动态页面交给 Tomcat、Django、Rails,图片、样式表等静态资源交给 Nginx。

另外,对于 HTTP 协议一定要 启用长连接 。TCP 和 SSL 建立新连接的成本是非常高的,有可能会占到客户端总延迟的一半以上。长连接虽然不能优化连接握手,但可以把成本“均摊”到多次请求里,这样只有第一次请求会有延迟,之后的请求就不会有连接延迟,总体的延迟也就降低了。

节流是指减少客户端和服务器之间收发的数据量,在有限的带宽里传输更多的内容。 节流最基本的做法就是使用 HTTP 协议内置的“数据压缩”编码,不仅可以选择标准的 gzip,还可以积极尝试新的压缩算法 br,它有更好的压缩效果。

拓展阅读

  • 关于 TCP 的性能优化也是个很大的话题,相关研究有很多,常用的优化手段有增大初始拥塞窗口、启用窗口缩放、慢启动重启等。
  • Nginx 默认不支持 br 压缩算法,需要安裝一个第三方模块 ngx_broth。
  • 文本、图片的优化可以使用 Google 开发的一个工具: PageSpeed,它最初是 Apache 的个模块,后来也推出了 Nginx 版本(ngx_ pagespeed)。
  • 在 HTML 里可以使用一些特殊的指令,例如 dns- prefetch、 preconnect 等,来预先执行 DNS 解析、TCP 连接,減少客户端的等待时间。此外,还可以使用一些 JS 黑魔法,用 JavaScript 来动态下载页面内容,而不是完全使用 HTTP 协议。

参考文章:
极客时间的《透视HTTP协议》
zq99299.github.io/note-book2/… (Github仓库: github.com/zq99299/rep…

推荐阅读资料:
xiaolincoding.com/ (其Github仓库:github.com/xiaolincode…

补充:

1. TCP 建立连接时,为什么是三次握手?

TCP 建立连接时,通过三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。

不使用「两次握手」和「四次握手」的原因:

  • 「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
  • 「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。

2. 为什么 TCP 挥手需要四次呢?

服务器收到客户端的 FIN 报文时,内核会马上回一个 ACK 应答报文,但是服务端应用程序可能还有数据要发送,所以并不能马上发送 FIN 报文,而是将发送 FIN 报文的控制权交给服务端应用程序

  • 如果服务端应用程序有数据要发送的话,就发完数据后,才调用关闭连接的函数;
  • 如果服务端应用程序没有数据要发送的话,可以直接调用关闭连接的函数,

从上面过程可知,是否要发送第三次挥手的控制权不在内核,而是在被动关闭方(上图的服务端)的应用程序,因为应用程序可能还有数据要发送,由应用程序决定什么时候调用关闭连接的函数,当调用了关闭连接的函数,内核就会发送 FIN 报文了,所以服务端的 ACK 和 FIN 一般都会分开发送。

3. 什么情况会出现三次挥手?

当被动关闭方在 TCP 挥手过程中,如果「没有数据要发送」,同时「没有开启 TCP_QUICKACK(默认情况就是没有开启,没有开启 TCP_QUICKACK,等于就是在使用 TCP 延迟确认机制)」,那么第二和第三次挥手就会合并传输,这样就出现了三次挥手。