网络通信 | HTTP(S)那些事儿

大家好，这里是 Bo2SS～真快，毕业过去一年了，公司又注入了新鲜的血液。部门里有一个大前端新人培训，自己斗胆报了名，做一个 HTTP 相关知识的分享。其实之前自己没有系统地学习过 HTTP，所以提前了 2 个月准备这次分享。上周分享完后，据问卷反馈全员五星🌟好评，就在这里记录一下吧～

前言

如题，今天要聊的是网络通信中 HTTP 和 HTTPS 的那些事儿。

Q：为什么要分享这个主题？

🫡 生活中常见。HTTP 在整个互联网是非常常见的，比如我们看剧、刷短视频、面向 Google 编程，都会用到它。作为研发，我们有义务深入了解一下。

🤔 工作中常见。在我们工作中其实经常会遇到相关问题，比如在前后端接口联调的时候，如果遇到了预期之外的情况，我们首先要去关注的，就是 HTTP 请求里的一些信息。我们应该熟悉它的结构和一些规范。

📖 思想可借鉴。HTTP 它发展了 30 多年，3 个大版本，它的很多设计思路是值得我们在开发中去借鉴的。

Q：参考了哪些资料？

这次分享前我做了很多准备工作，主要参考的有：

极客时间上的《透视 HTTP 协议》课程。这个系列我可能也就听了十吧遍吧，如果大家想深入了解更多的细节，可以去看看。作者还提供了可以实战学习的仓库chronolaw/http_study，通过它你可以很方便地搭建一个 Web 服务器，然后通过浏览器去访问里面的资源来理解 HTTP。

小林 Coding。这是我关注比较久的一个公众号，里面有很多关于计算机基础的图解系列文章，现在也有了小林 Coding 网站版。

Bo2SS。这是我自己的公众号，就在这里啦👋。之前写过 2 篇 HTTPS 相关的文章：

信息安全 | 互联网时代，如何建立信任？

信息安全 | （加餐）互联网时代，如何建立信任？

此外，文中也引用了一些第三方的图片，就没有一一列举来源了，如有不当之处，欢迎联系。

Q：这次分享的目标？

🔍 快速定位 HTTP 问题。刚刚说到了我们在前后端联调的时候，可能会经常遇到结果不符合预期的情况，我们应该首先能够通过状态码去快速定位，这个问题是后端的还是前端的。

🥣 熟悉 HTTP 报文里的常见头字段。熟悉常见的头字段后，我们不仅可以掌握 HTTP 的基本功能，还可以学到很多 HTTP 的设计思想。这个报文在哪里可以看到呢？各端一般都会有抓包工具来查看。

🔐了解基本的加密知识。互联网时代，用户隐私、业务保密都是非常重要的。

🏁终极目标：看完这篇文章，你将拥有自行深入学习 HTTP 的能力了，比如通过 WireShark、Chrome、Telnet 工具，甚至可以去看 RFC 文档，里面几乎包含了所有网络相关的重要资料。

➕一些资料：各工具的使用指南（WireShark、Chrome、Telnet），RFC 文档汇总。

Q：这次要分享哪些内容？

也就是这次分享的大纲：

简单来说，今天要聊的就是HTTP 和 HTTPS 是什么，然后它们怎么发展的。

好，下面进入正题：

什么是 HTTP？

HTTP 是什么，又不是什么？

HTTP 的全称是 Hypertext Transfer Protocol，也就是超文本、传输、协议。我们从后往前解释～

协议。什么是协议，我们可以联想我们的租房协议、三方协议，其实都是一样的含义，协议的 “协”，代表有两个以上的参与方，协议的 “议” 呢，代表约定和规范，约定你可以做什么、不能做什么。

传输。然后是传输，我们可以联想快递，专门在两点之间传输，其关键有两点。第一点是双向性，我们可以寄快递，也可以收快递；第二点是传输过程可以有中转，比如我们寄快递，会经过快递小哥、快递公司、物流仓库等，最后才会到达收件方，同时这些中间者也都遵守协议。

超文本。最后关于超文本，顾名思义就是超越了普通文本的文本。这里我想问大家一个问题，超文本除了文字、图片、音频、视频格式，还有一个最关键的格式，是什么？对了，就是超链接。超链接能够让我们从一个 “超文本” 跳跃到另一个 “超文本”，让我们的文本从以前的线性结构，变成了非线性的网状结构。

一句话总结就是：“HTTP 是一个在计算机世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范”。

这张图是我们基本的 HTTP 通信过程中的参与者们。从这张图，我们可以捋清 HTTP 它不是什么，从而对 HTTP 有一个更清晰的认识。

HTTP 不是实体，比如左边的 Web 浏览器（发送方），右边的 Web 服务器（接收方）。

HTTP 不是互联网，HTTP 传输的超文本资源是互联网资源中的一部分。

HTTP 不是一门编程语言，但是 HTTP 支持各种编程语言来实现。

HTTP 不是HTML，HTTP 可以传输 HTML，HTML 是超文本的常见格式。

HTTP 不是一个孤立的协议。通常在 HTTP 的下方，会有一些底层协议支持，比如 TCP、IP、DNS 等等；在 HTTP 的上方，也有一些依赖于 HTTP 的协议，比如 WebSocket、HTTPDNS 等等。这些协议相互交织，构成了一个协议网，而 HTTP 则处于中心地位。

HTTP 世界全览

我们再对 HTTP 世界的全貌进行一下浏览，主要分为应用相关和理论相关。有了对 HTTP 通信链路更宏观的认识，我们在定位问题的时候，能够更清楚问题可能是由哪个环节导致的。

HTTP 相关应用

我们从右往左看：

Internet - WWW：Internet 就是互联网，里面存储着各种信息资源。WWW 是互联网的一个子集，简称万维网，它基于 HTTP，所以存储的都是超文本资源，这些资源在互联网中的占比大约在 90%。

Web Browser：Web 浏览器，是 HTTP 通信过程中的请求方，并可以显示请求到的资源。

Web Server：Web 服务器，是 HTTP 通信过程中的响应方，由它来管理网络资源。一般会将它分为硬件和软件，硬件指物理服务器、云服务器之类的机器，软件指 Nginx、Apache 之类的应用程序。

CDN：Content Delivery Network，也就是内容分发网络。前面说到 HTTP 的传输过程可以有中转，这里 CDN 的定位就是中转方，起到网络代理的作用。它可以缓存服务器的资源，加快网络响应，还可以提供负载均衡、安全防护等能力。

Crawler：就是爬虫。和 Web 浏览器类似，它也可以理解为是一种用户代理，一般是给各大搜索引擎自动抓取数据、存入数据库用的。

Others： 其它还有 HTML、Web 服务、WAF。Web 服务可以理解为运行在 Web 服务器上的具体服务或者服务开发规范。WAF 的全称是网络应用防火墙，它其实也是一种代理。

HTTP 相关理论

同样从右往左看，右边的 HTTP/1.1、HTTPS、HTTP/2、HTTP/3 就是今天我们要聊的主要协议，左边：

TCP/IP：它代表的其实是一个协议栈，里面包含了很多网络通信协议。

这里我们把基于 TCP/IP 的 HTTP 通信过程与快递运输又做一个类比：

1）超文本 => MAC：左图中左边这一列，要传输的超文本从应用层一直到链接层，每经过一层都会被加上对应的头，比如 HTTP 头、TCP 头、IP 头、MAC 头，这就像快递的打包过程；

2）MAC => 超文本：左图中右边这一列，被传输的数据每经过一层则都会被去除一个头，这就像快递的拆包过程。

URI - URL：URI（I - Identifier）是统一资源标识符，它又分为 URL 和 URN 两种形式，但因为后者在互联网世界并不常用，所以 URI 一般指的就是 URL。

1）URL（L - Locator）：我们浏览器上方的地址就是 URL。

它的基本组成如上图，我们可以先关注红框部分：

scheme：最左边的 scheme 代表协议，如 http、https、ftp 等等。注意这里协议后面紧跟的://符号是固定的、必须的。

host：中间 host 是主机名，也叫域名，待会讲 DNS 的时候再细说。

path：最后边的 path 代表资源路径。

Q：这里有一个问题，图中示例 URL 的域名是www.creatorseo.com/吗？

答案是否定的，最后面的斜杠/属于 path，它代表的是所访问主机的根目录，因为早期互联网上的计算机大多是 UNIX 系统，所以这里的路径格式是采用的是 UNIX 上的文件路径风格。

下面还有一张图，这张图是 URL 的完整格式。

比上图还多了三个组成：

user@：我们可以在 URL 里就填上用户密码信息，但因为安全原因已经不推荐使用它了。

?query：这个部分可以附加一些对资源的额外要求，以?开始，由多个键值对k=v组成，每个键值对用&连接。

#fragment：它代表一个片段标识，我们可以理解为资源内的一个锚，它是给客户端使用的，不会发送给服务器。平时我们在看一些博客时（如阅读原文跳转到我的博客网页），点击悬浮目录中的某个标题做跳转，就是用的这个部分。

💡 这里还有两个小提醒～一个是 host 后面还可以通过:port指定端口。另一个就是一般聊到 URL 时，还会聊到escape 转义和encode 编码两个概念，因为如果没有它们，服务器可能就无法正确地处理 URL。试着想一想如果 path 里也有?符号，服务器该怎么解析出 query 的起始位置呢？

escape - 转义：针对特殊字符，我们一般会做转义，直接将其转换成 ASCII 码的十六进制后再加一个%前缀，比如SPACE对应%20，?对应%3F；

encode - 编码：针对汉字等其它语言，我们一般会先进行 UTF-8 编码，再转义。如果你不信，可以试试把包含中文的 URL 复制粘贴到微信中（如阅读原文跳转到我的博客网页）。

2）URN（N - Name）：下面再回到 URI 的第二种形式 URN，它是通过命名空间加具体标识符的形式来标记资源，如urn:<NAMESPACE-IDENTIFIER>:<NAMESPACE-SPECIFIC-STRING>。它在我们上网时不常用，但如果你去买书，可能会发现每本书的条形码位置有一串字符，如ISBN xxx-x-xx-xxxx，这其实就是 URN 的一种用法。

DNS：全称 Domain Name System，即域名系统，它是用来做域名解析的应用层协议，也就是将域名转换为 IP 地址。

我们先来看看域名的结构，还是这张图，这里红框部分就是域名，它是一个有层次的结构，用.分隔，越靠右边，层级越高。如从右到左，分别为顶级域名、二级域名、三级域名等等。

我们再来看看 DNS 的类型和 DNS 解析域名的步骤，如下图：

1）DNS 类型。DNS 分为根 DNS、顶级 DNS、权威 DNS 和非权威 DNS。根 DNS 共有 13 组，遍布全球，它可以根据请求的顶级域名将 DNS 解析指定给下面对应的顶级 DNS。顶级 DNS 又根据二级域名指定权威 DNS，直到解析出域名对应的 IP。而一些大公司还会自建 DNS，又叫非权威 DNS，它们的分布更广，比较知名的有 Google 的 8.8.8.8，Microsoft 的 4.2.2.1，还有 CloudFlare 的 1.1.1.1 等等。

2）DNS 解析域名步骤。实际的解析过程分为 4 步：系统首先会找 DNS 缓存，可能是浏览器里的，也可能是系统里的；如果找不到，再去查看 hosts 文件，里面有我们自定义的域名 - IP 对应规则，Mac 下的 hosts 文件路径为/etc/hosts；如果匹配不到，再去问非权威 DNS，一般默认是走我们网络运营商指定的；如果还是没解析出来，就要走根 DNS 的解析流程喽～

💡 这里还有一些域名解析相关的常用命令（dig、host、nslookup），如果你感兴趣，可以去终端试一试～

Copy
1. DNS addressing process: dig www.baidu.com +trace @8.8.8.8
2. domain name <=> IP: host www.baidu.com
3. domain => IP: nslookup www.baidu.com

如果你知道用 WireShark，还可以通过filter: port 53过滤出 DNS 解析相关的抓包。

Proxy：就是代理。代理一般分为正向代理和反向代理，正向代理靠近客户端，反向代理靠近服务端。刚刚我们提到的 CDN 属于反向代理，而我们访问外网用的 VPN 就属于正向代理。

HTTP 报文

铺垫了这么多（实际上也是值得的），终于到了 HTTP 最重要的部分！

所谓 HTTP，超文本传输协议，其中最重要的部分其实是最后的协议，里面约定了 HTTP 报文的格式和用法。

基本格式

我们先来看看 HTTP 报文的基本格式，它可以简单分为头部和身体两部分：

1）头部：一般可以包含起始行部分，也就是头部由 Start line 和 Header 组成。下图展示了一次请求中，请求头和返回头的结构：

请求头

Start line 由请求方法、URI、HTTP 版本、空格间隔符以及最后的换行符组成。

Header 由一个个的key:value键值对和最后的换行符组成，注意这里:前不能有多余的空格，不信你用telnet命令试试（Mac 上可以使用brew install telnet来安装telnet，并且推荐搭配极客时间提供的实战仓库chronolaw/http_study来用）。

返回头

Start line 由HTTP 版本、状态码、状态码对应解释、空格间隔符以及最后的换行符组成。

Header 结构和请求头一样。

2）身体：一般根据业务来约定 body 的具体内容，它是可有可无的。

下面我们再来看看请求行中的请求方法和状态码具体有哪些～

请求方法

HTTP/1.1 里规定了八种请求方法，这里把它们分成了常用和不常用两类，另外还有一类是扩展的请求方法，注意这些方法都必须是大写的形式。

这里主要介绍下常用的请求方法：

GET 和 HEAD，用来获取服务器资源。两者的区别在于 HEAD 只会获取到头部信息，GET 会获取到完整的头部和身体信息。所以如果你只是想确认某个资源存不存在或者只需要头部信息，可以用 HEAD 请求，从而减少传输量。

POST 和 PUT，用来发送资源给服务器。两者的区别在于前者是在服务器创建资源，类似数据库的 CREATE 操作，后者是修改服务器的资源，类似 UPDATE 操作。两者比较类似，实际应用中 PUT 使用较少。

💡 说到请求方法，一般还会提到安全和幂等两个概念：

安全：指的是对服务器资源不会有实质的修改，所以上面提到的 GET 和 HEAD 是安全的。

幂等：指的是相同的操作执行多次后，结果是否相同，所以上面提到的 GET、HEAD 和 PUT 都是幂等的。

返回状态码（5 类）

这次到了我们的第一个目标：🔍 通过状态码快速定位 HTTP 问题。

状态码一般分为5 大类：

1xx：提示信息类。一般是一次请求的中间状态，比较少见。

2xx：成功类。这说明请求是符合预期的，也是我们最愿意看到的。

3xx：重定向类。资源发生了变动，需要客户端向另一个域名重发请求。

4xx：客户端错误。看到这个就要想想请求报文是不是填错了。

5xx：服务端错误。看到这个就要找服务端的同学确认问题原因了。

常见的具体状态码可参考下图：

301：永久重定向，可以把请求的 URL 改一下了。

302：临时重定向。

304：服务器资源没有变，所以重定向到了本地缓存。

401：未认证错误，一般与鉴权、登陆相关。

403：访问被拒绝，可能访问到了敏感信息。

404：资源没找到，可能资源路径写错了，也可能是没有权限访问（错误码是服务端自定义设置的，404 一般用于表示资源没找到，但也可以延伸其用途，隐蔽具体原因）。

405：请求方法不被允许。

502：网关或代理返回的错误码，一般是访问网关或代理背后的服务器出错。

503：服务器暂时不可用，请稍后重试。

💡 对于 400、500，它们是比较笼统的错误码，有时候是作为兜底错误码返回的，说明发生了未知的错误；有时候是因为服务端不想暴露过多的细节返回的。总之服务端在尽可能遵守公共认知的情况下，是可以自定义状态码的。

常见头字段（8 种）

很快，我们又到了本文的第二个目标：🥣 熟悉 HTTP 报文里的常见头字段。

首先，我们把头字段分为 Request、Response、Universal 三大类，Universal 类里又包含 Entity 子类。Request 类头字段是请求方用的，Response 类头字段是响应方用的，Universal 类头字段是请求方和响应方都可以用的，Entity 类头字段一般是用来描述 body 属性的。

上图列出了常见的头字段们，看着眼花缭乱，别急，我们分功能来解释。补充：填充色相同的头字段们一般是搭配着使用的或者相关的。

下面，我们将头字段们按照功能主要分为 8 种来解释。

1）Body：body 属性相关，可以是描述请求报文中的，也可以是描述响应报文中的。

提到 body 的类型，我们首先需要了解什么是 MIME（Multipurpose Internet Mail Extensions，多用途互联网邮件扩展）类型，它是从电子邮件系统中诞生的，现在也被用来描述 body 的类型。这里有MIME 类型汇总，你点开链接看一下就会觉得它们很眼熟，比如application/json、text/html、text/javascript…… 它的前半部分是一个大的类别，后半部分是具体的格式。

Accept表示的是请求方可以接受的 body 类型，可能不止一个。

Content-Type表示的是实际传输的 body 类型。

为了减少 body 的大小，我们一般会对它进行压缩，常见的压缩格式有 gzip、deflate 和 br，它们对 text 的压缩效果很好。

Accept-Encoding表示的是请求方可以支持的压缩格式，可能不止一个。

Content-Encoding表示的是传输的 body 实际采用的压缩格式。

因为上面的压缩方式一般只对文本有较好的压缩率，对于压缩效果不好的图片、音视频等多媒体格式，还有一种方式来解决大文件的问题，那就是分块传输。

Transfer-Encoding: chunked：这表示数据是被分块传输的。

对于视频类型的 body，比如我们在 b 站上看视频，这个视频不可能是一次就请求下来的，我们可以对视频进行分段请求。

Accept-Ranges: bytes：我们一般会通过 HEAD 请求先问问服务端是否支持范围请求，如果支持通过字节范围请求，服务端就会返回这个。

Range: bytes=x-y：在服务端支持的情况下，请求方就可以明确要请求第 x~y 字节的内容。

Content-Range: bytes x-y/length：这表示服务端返回的 body 是第 x~y 字节的内容，内容总长度为 length。

在国际化方面，我们还可以设置对语言的要求。

Accept-Language表示的是请求方可以理解的语言，可能不止一个。

Content-Language表示的是传输的 body 实际的语言。

这里还要举一个具体的例子：Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7，有两个细节我们要注意：

在 HTTP 规范里，,的优先级大于;，这和我们一般的编程语言语法相反，所以上面的en;q=0.9是一对。

上面的q是什么？它其实代表一个权重，默认是 1，响应方会尽可能使用权重最大的语言返回内容。

2）Connection：长连接相关。

在 HTTP/1.1 之前，客户端每次和服务端通信都需要重新建立连接，如果频繁通信，就会不断地重复建立和关闭 TCP 连接，如下图左边所示，即短连接：

所以要是可以让一次 TCP 连接保持久一点，每次连接两端多通信几次就好了，也就是图中右边所示，即长连接。这不，HTTP/1.1 就支持了。

Connection: keep-alive：即表示使用长连接，在 HTTP/1.1 中默认开启。

Connection: close：主动关闭长连接，一般是由客户端发出的。

对于服务端，它也可以设置长连接的断开时机，它们是在 Web 服务器中配置的。比如在 Nginx 中，keepalive_timeout代表长连接的超时时间，如果长时间没有数据收发就主动断开连接； keepalive_requests代表长连接过程中最多接收请求多少次。

因为长连接的方式，客户端也可以同时发起多个请求，而不必等第一个请求的结果回来再发第二个请求，这也就是管道通信。

不过不管短连接还是长连接，都还会有一个队头阻塞（HoL blocking）问题，这是因为 HTTP 的 “请求 - 应答” 模型规定报文必须是 “一发一收” 导致的，可以参考下图：

无论如何，接收方都必须先处理完红线请求后，才能处理后面发起的请求，即使后面的请求先到了，也就是 “先发必须先处理”。

为了缓解这个问题，有一个办法，就是并发连接，也就是对一个域名发起多个长连接，每个长连接之间互不干扰。但是长连接的保持是需要消耗服务器资源的，而且也可能被恶意攻击，所以规定一般长连接的上限是 6～8 个。如果还不够用，还有一个取巧的方式，就是域名分片，同一台服务器有多个域名对应，那么上限也就翻倍了。

3）Redirection：重定向相关。

在聊状态码时，我们提到过 301（永久）、302（临时），不知道你还记不记得它们的含义。如果返回这种状态码，那么在响应头里一定还会标识重定向的位置。

Location就是重定向的位置，一般有绝对路径和相对路径两种形式，绝对路径对应的就是 URL 的基本格式，相对路径则没有 scheme 和 host，默认使用原请求的 URL 里的。

和重定向相关的还有 3 种状态码：303 类似 302，但请求方法只能是 GET；307、308 分别类似 302、301（这里是反的……），但它们都不允许重定向后的请求有任何变动。

4）Cookie：解决 HTTP 无状态特点带来的问题。

首先我们要清楚无状态指的是客户端还是服务端？没错，是服务端，也就是服务端不知道收到的这次请求和上次请求有什么关联，那这样会让服务端处理一些特殊场景时方案变得复杂，比如购物。

所以这里 Cookie 就是来解决这个问题的。简单来说，就是服务端给客户端贴了一个小纸条，标识某个客户端的身份，这个客户端在每次请求时带上这个小纸条，就可以证明自己的身份了。

Set-Cookie: a=xxx，Set-Cookie: b=yyy：这是服务端返回的，一个 Cookie 本质上就是一个键值对，并且每个 Cookie 是分开的。

Cookie: a=xxx; b=yyy：这是客户端在发送请求时带上的，也就是之前服务端返回的 Cookie 们，它们是合在一起的。

注意，客户端在收到这些 Cookie 后会将它们保存到客户端里，我们去看看 Chrome 浏览器就可以发现它们。

咦？Cookie 除了 Name 和 Value，怎么还有这么多属性？其实，服务端返回的 Cookie 一般长这样：Set-Cookie: a=xxx; Domain=xx; Path=xx; Max-Age=xx; Expires=xx; HttpOnly; Secure; SameSite=xx...。

Domain、Path：只有在客户端请求的 URL 匹配上它们时，这个 Cookie 才会被带上。

Max-Age、Expires：代表 Cookie 的失效时间，后面 Cache 也有类似的属性，要注意的是 Max-Age 的优先级大于 Expires。

HttpOnly：其为真时，代表这个 Cookie 只能通过 HTTP (S) 协议传输，禁止其他方式访问，比如在 JS 里就不再可以用 document.cookie 获取它了，以防脚本攻击。

Secure：其为真时，代表这个 Cookie 只有在发起安全的 HTTPS 请求时，才会被带上。

SameSite=xxx：设置 SameSite=Strict 可以严格限定该 Cookie 不能跨站发送；SameSite=Lax 则略宽松一点，允许在 GET/HEAD 等安全的请求里使用该 Cookie。

5）Cache：缓存相关。

缓存真的无处不在，HTTP 请求里也不例外。这里提到的缓存是存放在客户端的，目的就是尽可能地减少网络请求或着返回数据的大小，从而提升网络传输效率。

Cache-Control

服务端可以返回的属性有：max-age=10/no-store/no-cache/must-revalidate。

max-age 的单位是秒，从返回那一刻就开始计算；

no-store 代表客户端不允许缓存；

no-cache 代表客户端使用缓存前必须先来服务端验证；

must-revalidate 代表缓存失效后必须验证。

客户端可以发送的属性有：max-age=0；no-cache。

一般地，cmd + R 刷新页面会带上 max-age=0，意思是只要生存了 0 秒的数据，也就是不走本地缓存了，而是向服务器要一个最新生成的报文；cmd + shift + R 强制刷新页面会带上 no-cache，和前者基本一致，看服务端如何处理。

那什么时候缓存会生效呢？一般是在浏览器前进、后退或者重定向时，客户端发起的请求就不会带上上面的两个属性了。

此外，为了增加缓存控制的灵活性，这里还有一些条件字段～

服务端返回的有：

Last-Modified代表文件的最后修改时间。

ETag全称是 Entity Tag，代表资源的唯一标识，它是为了解决修改时间无法准确区分文件变化的问题。比如一个文件在一秒内修改了很多次，而修改时间的最小单位是秒；又或者一个文件修改了时间属性，但内容没有变化。Etag 还分为强 Etag、弱 Etag：

前者不变的条件是资源在字节级别不变。

后者不变的条件是资源在语义上不变即可，比如多了几个空格之类的。另外，弱 Etag 的值会在前面加一个W/标记。

客户端请求时对应就用：

If-Modified-Since里放的就是上次请求服务端返回的 Last-Modified，如果服务端资源没有比这个时间更新的话，服务端就会返回 304，表示客户端用缓存就行。

If-None-Match里放的就是上次请求服务端返回的 ETag 了，如果服务端资源的 Etag 没变，服务端也是返回 304。

6）Proxy：代理相关。

代理是具有双重身份的，因为在客户端眼里，它是服务端，而在服务端眼里，它又是客户端。

前面也提到了代理一般分为正向代理和反向代理，反向代理一般被用来做负载均衡（合理分配任务，决定由后面的哪台服务器来响应请求）、安全防护、加密卸载（在内网里通信就不加密了，减少加解密成本）、内容缓存（暂存服务器响应，这个下面会说，也就是代理缓存）等等。

在上面有代理服务器的场景中，涉及到的头字段是：

Via：代理服务器会在发送请求时，把自己的主机名加端口信息追加到该字段的末尾。

但是服务器一般需要知道客户端的真实 IP 信息，方便做访问控制、用户画像、统计分析等，所以在 HTTP 标准外还约定了下面的头字段：

X-Forwarded-For：类似 Via 的追加方式，但追加的内容是请求方的 IP 地址。

X-Real-IP：只记录客户端的 IP 地址，它更简洁一点。

不过上面的方式其实有一个很大的缺点：损耗性能！因为每次代理服务器需要解析出 HTTP 报文头，再修改报文数据；而且在有些情况下，报文是不允许甚至是不能（加密）被修改的。所以后来就又出现了一个专门的代理协议，这也是在标准外约定的。

基于这个协议，代理服务器只需要在 HTTP 报文前再加一行文本即可。比如：

Copy
PROXY TCP4 1.1.1.1 2.2.2.2 55555 80\r\n
GET / HTTP/1.1\r\n
Host: www.xxx.com\r\n
\r\n

开头是PROXY五个大写字母；

然后是客户端的 IP 地址类型，如TCP4或者TCP6；

再后面是请求方、应答方的地址，以及请求方、应答方的端口号；

最后用一个回车换行结束。

7）Proxy Cache：代理缓存相关。

客户端可以缓存，中间商代理服务器当然也可以缓存。但因为代理的双重身份性，所以Cache-Control针对代理缓存还增加了一些定制化的属性～

从服务端到代理服务器：

private代表数据只能在客户端保存，不能缓存在代理上与别人共享，比如用户的私人数据。

public代表数据完全开放，谁都可以缓存。

s-maxage代表缓存在代理服务器上的生存时间。

no-transform代表禁止代理服务器对数据做一些转换操作，因为有的代理会提前对数据做一些格式转换，方便后面的请求处理。

从客户端到代理服务器：

max-stale代表接受缓存过期一段时间。

min-fresh则与上面相反，代表缓存必须还有一段时间的保质期。

only-if-cached代表客户端只接受代理缓存。如果代理上没有符合条件的缓存，客户端也不要代理再去请求服务端了。

8）Others

我们再看看这张常见头字段图，你是不是已经清楚每一个头字段的含义和用法了呢～

等等，上面还漏了一些头字段的说明，我把它们统一放在这里补充一下：

Host代表要请求的主机名。它在 HTTP/1.1 里是必须出现的，用来给服务器区分具体选择哪个主机来处理请求的（如果计算机上托管了多个虚拟主机就有这个作用，否则服务器一般也不会去处理）。另外，在一般的网络框架里，它会帮我们从 URL 里解析出一个默认的 Host 值兜底，所以可能你没有手动填也没有问题，因为框架默认帮我们补充了。

User-Agent即用户代理，它是用来描述请求方的身份的，服务器可以根据它来返回合适的页面布局或者数据。不过由于历史原因，它的用法已经有些混乱了，比如每个浏览器都自称是 “Mozilla Chrome Safari” 之类的。

Date代表报文创建的时间，一般出现在响应头里。

Server展示的是提供 Web 服务的软件名称和版本号，但这样暴露了服务器的部分信息，可能存在安全隐患，所以有的时候返回里没有这个字段，或者仅仅是一个模糊的信息。

Content-Length代表报文里 body 的长度。如果没有这个字段，那么一般会有另一个字段Transfer-Encoding: chunked，前面我们说到过它。

至此，我们已经讲完了什么是 HTTP 了，可以试着用 Chrome 开发者工具或者 WireShark 抓包加深理解。

什么是 HTTPS？

HTTPS 比 HTTP 多了一个 S，这个 S 代表的是 SSL/TLS 协议。

现在来到了我们的第三个目标：🔐了解基本的加密知识。

这一节因为之前写过相关文章，所以我尽量减少篇幅，你可以点击下面的链接参考：

信息安全 | 互联网时代，如何建立信任？：三大常见密码学算法，数字签名，数字证书。

信息安全 | （加餐）互联网时代，如何建立信任？：SSL/TLS，SSH，iOS 签名，OpenSSL、WireShark 实践。

要补充的一个内容是：基于ECDHE的 TLS 主流握手方式 VS. 基于RSA的 TLS 传统握手方式。

两者的关键区别在于通信密钥生成过程中，第三个随机数Pre-Master的生成方式：

前者：两端先随机生成公私钥，同时公钥（加签名）作为参数传给对方，然后两端基于双方的参数，使用 ECDHE 算法生成Pre-Master；

后者：客户端直接生成随机数Pre-Master，然后用服务器证书的公钥加密后发给服务器。

因为前者的公私钥是随机生成的，即使某次私钥泄漏了或者被破解了，也只影响一次通信过程；而后者的公私钥是固定的，只要私钥泄漏或者被破解，那之前所有的通信记录密文都会被破解，因为耐心的黑客一直在长期收集报文，等的就是这一天（据说斯诺登的棱镜门事件就是利用了这一点）。

也就是说，前者 “一次一密”，具备前向安全；而后者存在 “今日截获，明日破解” 的隐患，不具备前向安全。

更多的细节可以参看《透视 HTTP 协议》里TLS1.2 连接过程解析这一课，或者自己用 WireShark 抓包试一试～

两者从抓包上的区别来看，主要是：

前者比后者多了一个 “Server Key Exchange” 消息。

前者客户端可以不等连接完全建立就提前进行加密通信，也就是客户端不用等服务器发回 “Finished” 确认握手完毕，这个叫 “TLS False Start”。

HTTP 的发展

我们通过下面表格梳理一下 HTTP 的发展过程，今天我们对 HTTP 发展有一个整体的认知就好～

Time | Version | Main change | Note
1989 | 3 key technologies | HTML, URI, HTTP | Paper from Tim Berners-Lee.

1991

HTTP/0.9

1. Request way: GET.
2. Data: HTML.

No RFC.

1996

HTTP/1.0

1. +Request way: HEAD, POST.
2. +Data: img, audio.
3. +Other: HTTP Head, status code, protocol version.

RFC-1945 (1996).
Not a formal standard.

1999

HTTP/1.1

1. +Request way: PUT, DELETE.
2. +Cache-control.
3. +Keep-Alive.
4. +Pipeline transmission(Content-Length), chunked transmission.
5. +Host head (Required).

+Google, Sina, Sohu, Netease, Tencent.
RFC-2616 (1999).
+Facebook, Twitter, Taobao, JD.
Divided to RFC-7230~7235 (2014).
RFC-9112 (2022).

2015

HTTP/2

1. Transmission data format: text → binary data.
2. +Concurrent requests (use stream, abandon pipeline transmission).
3. +Header Compression.
4. +Allow the server to push.
5. +Combined with TLS 1.2+.

Based on SPDY in Chrome browser (2009).
RFC-7540 (2015).
RFC-9113 (2022).

2022

HTTP/3

1. Transport layer protocol: TCP → QUIC (based on UDP, including TLS 1.3, IP → connection ID).
2. Header Compression: HPACK→QPACK

Based on QUIC in Chrome browser(2012).
RFC-9114 (2022).

从 HTTP/1.0 开始，HTTP 已经被写入 RFC 文档（RFC 文档汇总）。

HTTP/1.1 是 HTTP 第一个正式标准，大多数的功能我们在常见头字段那一节介绍了。在这个阶段早期，Google、Sina、Sohu、Netease、Tencent 等公司被创立了，后期 Facebook、Twitter、Taobao、JD 等公司慢慢也出来了。

HTTP/1.1 在各方面还比较完善了，但在性能和安全上还存在很大优化空间。所以 HTTP/2、HTTP/3 都主要是针对 HTTP 的性能方面做优化。

HTTP/2基于 Chrome 的 SPDY 协议，它是 Chrome 推动的。主要的变化有：

传输数据格式从文本转成了二进制，大大方便了计算机的解析。

基于虚拟流的概念，实现了多路复用能力，同时替代了 HTTP/1.1 里的管道功能。

利用 HPACK 算法进行头部压缩，在之前都只针对 body 做压缩。

允许服务端新建 “流” 主动推送消息。比如在浏览器刚请求 HTML 的时候就提前把可能会用到的 JS、CSS 文件发给客户端。

在安全方面，其实也做了一些强化，加密版本的 HTTP/2 规定其下层的通信协议必须在 TLS1.2 以上（因为之前的版本有很多漏洞），需要支持前向安全和 SNI（Server Name Indication，它是 TLS 的一个扩展协议，在该协议下，在握手过程开始时通过客户端告诉它正在连接的服务器的主机名称），并把几百个弱密码套件给列入 “黑名单” 了。

PS：相比 HTTP/1.1 中的并发连接方式，虚拟流的概念更优美地解决了 HTTP 的队头阻塞问题。

HTTP/3基于 Chrome 的 QUIC 协议，它也是 Chrome 推动的。

先看看 QUIC

基于 UDP 实现了可靠传输，引入了类似 HTTP/2 的流概念。

内含了 TLS1.3，加快了建连速度。

连接使用 “不透明” 的连接 ID 来标记两端，而不再通过 IP 地址和端口绑定，从而支持用户无感的连接迁移。

回到 HTTP/3

它最大的改变就是把下层的传输层协议从 TCP 换成了 QUIC，完全解决了 TCP 的队头阻塞问题（注意，是 TCP 的，不是 HTTP 的），在弱网环境下表现更好。因为 QUIC 本身就已经支持了加密、流和多路复用等能力，所以 HTTP/3 的工作减轻了很多。

头部压缩算法从 HPACK 升级为 QPACK。

2022 年 6 月 6 日，HTTP/3 被正式写入 RFC 文档，同时，HTTP/1.1 和 HTTP/2 也更新了 RFC 文档。

PS：TCP 为了保证可靠传输，有个特别的 “丢包重传” 机制，即丢失的包必须要等待重新传输确认，其他的包即使已经收到了，也只能放在缓冲区（kernel）里，上层的应用（user）拿不出来，可参考下图：红色方块请求是阻塞 TCP 的关键。

（其实这里有一点疑惑的是，合着 HTTP/3 之前解决的都只是 kernel 到 user 的阻塞问题？具体地说只是经过 TCP 之后的阻塞问题，在这个阶段的阻塞问题有哪些呢🤔？欢迎大佬们答疑解惑～）

HTTPS 的发展

这部分聊聊 TLS1.2 到 TLS1.3 的发展，在此之前的版本因为各种安全问题都已经被废弃了，我们可以从信息安全 | （加餐）互联网时代，如何建立信任？这篇文章里了解到。

对于 TLS1.3 来说，它的主要优化目标有 3 个：

兼容 TLS1.2。为了保证老设备能够更轻松地升级协议，TLS1.3 保持原有的记录格式不变，利用扩展协议在原有记录末尾增加一些 “扩展字段” 来增加新的功能，老版本的 TLS 不认识它们可以直接忽略，从而实现了 “后向兼容”。

更安全。TLS1.3 基于安全考虑对支持的密码套件进行了瘦身，最终只剩下 5 个密码套件。前面提到的基于 RSA 的 TLS 传统握手方式就被废除了。

更高性能。HTTPS 建立连接的过程除了 TCP 握手，还有 TLS 握手，在 TLS1.2 中 TLS 握手需要花费 2-RTT，而这个时间在 TLS1.3 中被优化到了 1-RTT。怎么做的呢？

答案就在上一点中，因为密码套件只有这么多，TLS1.3 可以索性在 ClientHello 消息中带上所有支持的密码套件相关参数，服务端选定其中一种就可以直接生成通信密钥进行加密通信了！而客户端也省去了 TLS1.2 中要等服务端确认密码套件后再发参数的过程。

除了标准的 1-RTT 握手，TLS1.3 在之前建立过连接从而缓存了服务端密码套件参数的情况下，还可以达到 0-RTT 握手，但这也存在前向不安全和重放攻击的问题，所以需要使用者去权衡。

下面我再放上《透析 HTTP 协议》里的通信过程对比图，你可以再看看它们的差异。

尾声（好消息）

好啦，今天就聊到这里啦，回到我们的目标，看看你还能想到具体的知识吗？

🔍 快速定位 HTTP 问题。

🥣 熟悉 HTTP 报文里的常见头字段。

🔐了解基本的加密知识。

当然，别忘了我们的终极目标🏁：如果你对 HTTP 感兴趣，试着通过 WireShark、Chrome、Telnet 工具以及 RFC 文档，去自行深入学习 HTTP 吧～

这里是 Bo2SS，咱们下次再见！

插播一条好消息，经过一年多的佛系经营，在 2022 年 8 月 14 日 10 时 45 分，Bo2SS 的粉丝基数悄悄突破 500 啦🎉！大家快来帮忙想想怎么庆祝（薅）一下 Bo2SS？欢迎投票，或在下方留言！

投票：

A. 发发红包，沾沾运气

B. 赠送书本，汲取知识

C. 建交流群，促进友谊

D. 别玩了，继续出文章

E. 都可以，不要白不要

F. 都不行，我去留言啦