This commit is contained in:
louzefeng
2024-07-09 18:38:56 +00:00
parent 8bafaef34d
commit bf99793fd0
6071 changed files with 1017944 additions and 0 deletions

View File

@@ -0,0 +1,130 @@
<audio id="audio" title="08 | 键入网址再按下回车,后面究竟发生了什么?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4f/c4/4f223253ac4c86e4072043ab1e6134c4.mp3"></audio>
经过上一讲的学习你是否已经在自己的电脑上搭建好了“最小化”的HTTP实验环境呢
我相信你的答案一定是“Yes”那么让我们立刻开始“螺蛳壳里做道场”在这个实验环境里看一下HTTP协议工作的全过程。
## 使用IP地址访问Web服务器
首先我们运行www目录下的“start”批处理程序启动本机的OpenResty服务器启动后可以用“list”批处理确认服务是否正常运行。
然后我们打开Wireshark选择“HTTP TCP port(80)”过滤器再鼠标双击“Npcap loopback Adapter”开始抓取本机127.0.0.1地址上的网络数据。
第三步在Chrome浏览器的地址栏里输入“[http://127.0.0.1/](http://127.0.0.1/)”再按下回车键等欢迎页面显示出来后Wireshark里就会有捕获的数据包如下图所示。
<img src="https://static001.geekbang.org/resource/image/86/b0/86e3c635e9a9ab0abd523c01fc181cb0.png" alt="">
如果你还没有搭好实验环境或者捕获与本文里的不一致也没关系。我把这次捕获的数据存成了pcap包文件名是“08-1”放到了GitHub上你可以下载到本地后再用Wireshark打开完全精确“重放”刚才的HTTP传输过程。
## 抓包分析
在Wireshark里你可以看到这次一共抓到了11个包这里用了滤包功能滤掉了3个包原本是14个包耗时0.65秒,下面我们就来一起分析一下"键入网址按下回车"后数据传输的全过程。
通过前面“破冰篇”的讲解你应该知道HTTP协议是运行在TCP/IP基础上的依靠TCP/IP协议来实现数据的可靠传输。所以浏览器要用HTTP协议收发数据首先要做的就是建立TCP连接。
因为我们在地址栏里直接输入了IP地址“127.0.0.1”而Web服务器的默认端口是80所以浏览器就要依照TCP协议的规范使用“三次握手”建立与Web服务器的连接。
对应到Wireshark里就是最开始的三个抓包浏览器使用的端口是52085服务器使用的端口是80经过SYN、SYN/ACK、ACK的三个包之后浏览器与服务器的TCP连接就建立起来了。
有了可靠的TCP连接通道后HTTP协议就可以开始工作了。于是浏览器按照HTTP协议规定的格式通过TCP发送了一个“GET / HTTP/1.1”请求报文也就是Wireshark里的第四个包。至于包的内容具体是什么现在先不用管我们下一讲再说。
随后Web服务器回复了第五个包在TCP协议层面确认“刚才的报文我已经收到了”不过这个TCP包HTTP协议是看不见的。
Web服务器收到报文后在内部就要处理这个请求。同样也是依据HTTP协议的规定解析报文看看浏览器发送这个请求想要干什么。
它一看原来是要求获取根目录下的默认文件好吧那我就从磁盘上把那个文件全读出来再拼成符合HTTP格式的报文发回去吧。这就是Wireshark里的第六个包“HTTP/1.1 200 OK”底层走的还是TCP协议。
同样的浏览器也要给服务器回复一个TCP的ACK确认“你的响应报文收到了多谢”即第七个包。
这时浏览器就收到了响应数据但里面是什么呢所以也要解析报文。一看服务器给我的是个HTML文件那我就调用排版引擎、JavaScript引擎等等处理一下然后在浏览器窗口里展现出了欢迎页面。
这之后还有两个来回共四个包重复了相同的步骤。这是浏览器自动请求了作为网站图标的“favicon.ico”文件与我们输入的网址无关。但因为我们的实验环境没有这个文件所以服务器在硬盘上找不到返回了一个“404 Not Found”。
至此,“键入网址再按下回车”的全过程就结束了。
我为这个过程画了一个交互图你可以对照着看一下。不过要提醒你图里TCP关闭连接的“四次挥手”在抓包里没有出现这是因为HTTP/1.1长连接特性,默认不会立即关闭连接。
<img src="https://static001.geekbang.org/resource/image/8a/19/8a5bddd3d8046daf7032c7d60a3d1a19.png" alt="">
再简要叙述一下这次最简单的浏览器HTTP请求过程
1. 浏览器从地址栏的输入中获得服务器的IP地址和端口号
1. 浏览器用TCP的三次握手与服务器建立连接
1. 浏览器向服务器发送拼好的报文;
1. 服务器收到报文后处理请求,同样拼好报文再发给浏览器;
1. 浏览器解析报文,渲染输出页面。
## 使用域名访问Web服务器
刚才我们是在浏览器地址栏里直接输入IP地址但绝大多数情况下我们是不知道服务器IP地址的使用的是域名那么改用域名后这个过程会有什么不同吗
还是实际动手试一下吧,把地址栏的输入改成“[http://www.chrono.com](http://www.chrono.com)”重复Wireshark抓包过程你会发现好像没有什么不同浏览器上同样显示出了欢迎界面抓到的包也同样是11个先是三次握手然后是两次HTTP传输。
这里就出现了一个问题浏览器是如何从网址里知道“www.chrono.com”的IP地址就是“127.0.0.1”的呢?
还记得我们之前讲过的DNS知识吗浏览器看到了网址里的“www.chrono.com”发现它不是数字形式的IP地址那就肯定是域名了于是就会发起域名解析动作通过访问一系列的域名解析服务器试图把这个域名翻译成TCP/IP协议里的IP地址。
不过因为域名解析的全过程实在是太复杂了,如果每一个域名都要大费周折地去网上查一下,那我们上网肯定会慢得受不了。
所以在域名解析的过程中会有多级的缓存浏览器首先看一下自己的缓存里有没有如果没有就向操作系统的缓存要还没有就检查本机域名解析文件hosts也就是上一讲中我们修改的“C:\WINDOWS\system32\drivers\etc\hosts”。
刚好里面有一行映射关系“127.0.0.1 www.chrono.com”于是浏览器就知道了域名对应的IP地址就可以愉快地建立TCP连接发送HTTP请求了。
我把这个过程也画出了一张图但省略了TCP/IP协议的交互部分里面的浏览器多出了一个访问hosts文件的动作也就是本机的DNS解析。
<img src="https://static001.geekbang.org/resource/image/57/1b/5717c967b8d46e5ba438e1d8ed605a1b.png" alt="">
## 真实的网络世界
通过上面两个在“最小化”环境里的实验你是否已经对HTTP协议的工作流程有了基本的认识呢
第一个实验是最简单的场景只有两个角色浏览器和服务器浏览器可以直接用IP地址找到服务器两者直接建立TCP连接后发送HTTP报文通信。
第二个实验在浏览器和服务器之外增加了一个DNS的角色浏览器不知道服务器的IP地址所以必须要借助DNS的域名解析功能得到服务器的IP地址然后才能与服务器通信。
真实的互联网世界要比这两个场景要复杂的多,我利用下面的这张图来做一个详细的说明。
<img src="https://static001.geekbang.org/resource/image/df/6d/df4696154fc8837e33117d8d6ab1776d.png" alt="">
如果你用的是电脑台式机那么你可能会使用带水晶头的双绞线连上网口由交换机接入固定网络。如果你用的是手机、平板电脑那么你可能会通过蜂窝网络、WiFi由电信基站、无线热点接入移动网络。
接入网络的同时网络运行商会给你的设备分配一个IP地址这个地址可能是静态分配的也可能是动态分配的。静态IP就始终不变而动态IP可能你下次上网就变了。
假设你要访问的是Apple网站显然你是不知道它的真实IP地址的在浏览器里只能使用域名“www.apple.com”访问那么接下来要做的必然是域名解析。这就要用DNS协议开始从操作系统、本地DNS、根DNS、顶级DNS、权威DNS的层层解析当然这中间有缓存可能不会费太多时间就能拿到结果。
别忘了互联网上还有另外一个重要的角色CDN它也会在DNS的解析过程中“插上一脚”。DNS解析可能会给出CDN服务器的IP地址这样你拿到的就会是CDN服务器而不是目标网站的实际地址。
因为CDN会缓存网站的大部分资源比如图片、CSS样式表所以有的HTTP请求就不需要再发到AppleCDN就可以直接响应你的请求把数据发给你。
由PHP、Java等后台服务动态生成的页面属于“动态资源”CDN无法缓存只能从目标网站获取。于是你发出的HTTP请求就要开始在互联网上的“漫长跋涉”经过无数的路由器、网关、代理最后到达目的地。
目标网站的服务器对外表现的是一个IP地址但为了能够扛住高并发在内部也是一套复杂的架构。通常在入口是负载均衡设备例如四层的LVS或者七层的Nginx在后面是许多的服务器构成一个更强更稳定的集群。
负载均衡设备会先访问系统里的缓存服务器通常有memory级缓存Redis和disk级缓存Varnish它们的作用与CDN类似不过是工作在内部网络里把最频繁访问的数据缓存几秒钟或几分钟减轻后端应用服务器的压力。
如果缓存服务器里也没有那么负载均衡设备就要把请求转发给应用服务器了。这里就是各种开发框架大显神通的地方了例如Java的Tomcat/Netty/JettyPython的Django还有PHP、Node.js、Golang等等。它们又会再访问后面的MySQL、PostgreSQL、MongoDB等数据库服务实现用户登录、商品查询、购物下单、扣款支付等业务操作然后把执行的结果返回给负载均衡设备同时也可能给缓存服务器里也放一份。
应用服务器的输出到了负载均衡设备这里请求的处理就算是完成了就要按照原路再走回去还是要经过许多的路由器、网关、代理。如果这个资源允许缓存那么经过CDN的时候它也会做缓存这样下次同样的请求就不会到达源站了。
最后网站的响应数据回到了你的设备它可能是HTML、JSON、图片或者其他格式的数据需要由浏览器解析处理才能显示出来如果数据里面还有超链接指向别的资源那么就又要重走一遍整个流程直到所有的资源都下载完。
## 小结
今天我们在本机的环境里做了两个简单的实验学习了HTTP协议请求-应答的全过程,在这里做一个小结。
1. HTTP协议基于底层的TCP/IP协议所以必须要用IP地址建立连接
1. 如果不知道IP地址就要用DNS协议去解析得到IP地址否则就会连接失败
1. 建立TCP连接后会顺序收发数据请求方和应答方都必须依据HTTP规范构建和解析报文
1. 为了减少响应时间,整个过程中的每一个环节都会有缓存,能够实现“短路”操作;
1. 虽然现实中的HTTP传输过程非常复杂但理论上仍然可以简化成实验里的“两点”模型。
## 课下作业
1. 你能试着解释一下在浏览器里点击页面链接后发生了哪些事情吗?
1. 这一节课里讲的都是正常的请求处理流程,如果是一个不存在的域名,那么浏览器的工作流程会是怎么样的呢?
欢迎你把自己的答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/8e/56/8ef903c86d3ef548a9536bd4345f0156.png" alt="unpreview">

View File

@@ -0,0 +1,217 @@
<audio id="audio" title="09 | HTTP报文是什么样子的" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fd/04/fd314ef48924d547230a89a7115b9204.mp3"></audio>
在上一讲里我们在本机的最小化环境了做了两个HTTP协议的实验使用Wireshark抓包弄清楚了HTTP协议基本工作流程也就是“请求-应答”“一发一收”的模式。
可以看到HTTP的工作模式是非常简单的由于TCP/IP协议负责底层的具体传输工作HTTP协议基本上不用在这方面操心太多。单从这一点上来看所谓的“超文本传输协议”其实并不怎么管“传输”的事情有点“名不副实”。
那么HTTP协议的核心部分是什么呢
答案就是它传输的报文内容。
HTTP协议在规范文档里详细定义了报文的格式规定了组成部分解析规则还有处理策略所以可以在TCP/IP层之上实现更灵活丰富的功能例如连接控制缓存管理、数据编码、内容协商等等。
## 报文结构
你也许对TCP/UDP的报文格式有所了解拿TCP报文来举例它在实际要传输的数据之前附加了一个20字节的头部数据存储TCP协议必须的额外信息例如发送方的端口号、接收方的端口号、包序号、标志位等等。
有了这个附加的TCP头数据包才能够正确传输到了目的地后把头部去掉就可以拿到真正的数据。
<img src="https://static001.geekbang.org/resource/image/17/95/174bb72bad50127ac84427a72327f095.png" alt="">
HTTP协议也是与TCP/UDP类似同样也需要在实际传输的数据前附加一些头数据不过与TCP/UDP不同的是它是一个“**纯文本**”的协议所以头数据都是ASCII码的文本可以很容易地用肉眼阅读不用借助程序解析也能够看懂。
HTTP协议的请求报文和响应报文的结构基本相同由三大部分组成
1. 起始行start line描述请求或响应的基本信息
1. 头部字段集合header使用key-value形式更详细地说明报文
1. 消息正文entity实际传输的数据它不一定是纯文本可以是图片、视频等二进制数据。
这其中前两部分起始行和头部字段经常又合称为“**请求头**”或“**响应头**”,消息正文又称为“**实体**”,但与“**header**”对应,很多时候就直接称为“**body**”。
HTTP协议规定报文必须有header但可以没有body而且在header之后必须要有一个“空行”也就是“CRLF”十六进制的“0D0A”。
所以一个完整的HTTP报文就像是下图的这个样子注意在header和body之间有一个“空行”。
<img src="https://static001.geekbang.org/resource/image/62/3c/62e061618977565c22c2cf09930e1d3c.png" alt="">
说到这里我不由得想起了一部老动画片《大头儿子和小头爸爸》你看HTTP的报文结构像不像里面的“大头儿子”
报文里的header就是“大头儿子”的“大头”空行就是他的“脖子”而后面的body部分就是他的身体了。
看一下我们之前用Wireshark抓的包吧。
<img src="https://static001.geekbang.org/resource/image/b1/df/b191c8760c8ad33acd9bb005b251a2df.png" alt="unpreview">
在这个浏览器发出的请求报文里第一行“GET / HTTP/1.1”就是请求行而后面的“Host”“Connection”等等都属于header报文的最后是一个空白行结束没有body。
在很多时候特别是浏览器发送GET请求的时候都是这样HTTP报文经常是只有header而没body相当于只发了一个超级“大头”过来你可以想象的出来每时每刻网络上都会有数不清的“大头儿子”在跑来跑去。
不过这个“大头”也不能太大虽然HTTP协议对header的大小没有做限制但各个Web服务器都不允许过大的请求头因为头部太大可能会占用大量的服务器资源影响运行效率。
## 请求行
了解了HTTP报文的基本结构后我们来看看请求报文里的起始行也就是**请求行**request line它简要地描述了**客户端想要如何操作服务器端的资源**。
请求行由三部分构成:
1. 请求方法是一个动词如GET/POST表示对资源的操作
1. 请求目标通常是一个URI标记了请求方法要操作的资源
1. 版本号表示报文使用的HTTP协议版本。
这三个部分通常使用空格space来分隔最后要用CRLF换行表示结束。
<img src="https://static001.geekbang.org/resource/image/36/b9/36108959084392065f36dff3e12967b9.png" alt="">
还是用Wireshark抓包的数据来举例
```
GET / HTTP/1.1
```
在这个请求行里“GET”是请求方法“/”是请求目标“HTTP/1.1”是版本号把这三部分连起来意思就是“服务器你好我想获取网站根目录下的默认文件我用的协议版本号是1.1请不要用1.0或者2.0回复我。”
别看请求行就一行,貌似很简单,其实这里面的“讲究”是非常多的,尤其是前面的请求方法和请求目标,组合起来变化多端,后面我还会详细介绍。
## 状态行
看完了请求行,我们再看响应报文里的起始行,在这里它不叫“响应行”,而是叫“**状态行**”status line意思是**服务器响应的状态**。
比起请求行来说,状态行要简单一些,同样也是由三部分构成:
1. 版本号表示报文使用的HTTP协议版本
1. 状态码一个三位数用代码的形式表示处理的结果比如200是成功500是服务器错误
1. 原因:作为数字状态码补充,是更详细的解释文字,帮助人理解原因。
<img src="https://static001.geekbang.org/resource/image/a1/00/a1477b903cd4d5a69686683c0dbc3300.png" alt="">
看一下上一讲里Wireshark抓包里的响应报文状态行是
```
HTTP/1.1 200 OK
```
意思就是“浏览器你好我已经处理完了你的请求这个报文使用的协议版本号是1.1状态码是200一切OK。”
而另一个“GET /favicon.ico HTTP/1.1”的响应报文状态行是:
```
HTTP/1.1 404 Not Found
```
翻译成人话就是“抱歉啊浏览器刚才你的请求收到了但我没找到你要的资源错误代码是404接下来的事情你就看着办吧。”
## 头部字段
请求行或状态行再加上头部字段集合就构成了HTTP报文里完整的请求头或响应头我画了两个示意图你可以看一下。
<img src="https://static001.geekbang.org/resource/image/1f/ea/1fe4c1121c50abcf571cebd677a8bdea.png" alt="">
<img src="https://static001.geekbang.org/resource/image/cb/75/cb0d1d2c56400fe9c9988ee32842b175.png" alt="">
请求头和响应头的结构是基本一样的,唯一的区别是起始行,所以我把请求头和响应头里的字段放在一起介绍。
头部字段是key-value的形式key和value之间用“:”分隔最后用CRLF换行表示字段结束。比如在“Host: 127.0.0.1”这一行里key就是“Host”value就是“127.0.0.1”。
HTTP头字段非常灵活不仅可以使用标准里的Host、Connection等已有头也可以任意添加自定义头这就给HTTP协议带来了无限的扩展可能。
不过使用头字段需要注意下面几点:
1. 字段名不区分大小写例如“Host”也可以写成“host”但首字母大写的可读性更好
1. 字段名里不允许出现空格,可以使用连字符“-”但不能使用下划线“_”。例如“test-name”是合法的字段名而“test name”“test_name”是不正确的字段名
1. 字段名后面必须紧接着“:”,不能有空格,而“:”后的字段值前可以有多个空格;
1. 字段的顺序是没有意义的,可以任意排列不影响语义;
1. 字段原则上不能重复除非这个字段本身的语义允许例如Set-Cookie。
我在实验环境里用Lua编写了一个小服务程序URI是“/09-1”效果是输出所有的请求头。
你可以在实验环境里用Telnet连接OpenResty服务器试一下手动发送HTTP请求头试验各种正确和错误的情况。
先启动OpenResty服务器然后用组合键“Win+R”运行telnet输入命令“open www.chrono.com 80”就连上了Web服务器。
<img src="https://static001.geekbang.org/resource/image/34/7b/34fb2b5899bdb87a3899dd133c0c457b.png" alt="">
连接上之后按组合键“CTRL+]”然后按回车键就进入了编辑模式。在这个界面里你可以直接用鼠标右键粘贴文本敲两下回车后就会发送数据也就是模拟了一次HTTP请求。
下面是两个最简单的HTTP请求第一个在“:”后有多个空格,第二个在“:”前有空格。
```
GET /09-1 HTTP/1.1
Host: www.chrono.com
GET /09-1 HTTP/1.1
Host : www.chrono.com
```
第一个可以正确获取服务器的响应报文而第二个得到的会是一个“400 Bad Request”表示请求报文格式有误服务器无法正确处理
```
HTTP/1.1 400 Bad Request
Server: openresty/1.15.8.1
Connection: close
```
## 常用头字段
HTTP协议规定了非常多的头部字段实现各种各样的功能但基本上可以分为四大类
1. 通用字段:在请求头和响应头里都可以出现;
1. 请求字段:仅能出现在请求头里,进一步说明请求信息或者额外的附加条件;
1. 响应字段:仅能出现在响应头里,补充说明响应报文的信息;
1. 实体字段它实际上属于通用字段但专门描述body的额外信息。
对HTTP报文的解析和处理实际上主要就是对头字段的处理理解了头字段也就理解了HTTP报文。
后续的课程中我将会以应用领域为切入点介绍连接管理、缓存控制等头字段今天先讲几个最基本的头看完了它们你就应该能够读懂大多数HTTP报文了。
首先要说的是**Host**字段它属于请求字段只能出现在请求头里它同时也是唯一一个HTTP/1.1规范里要求**必须出现**的字段也就是说如果请求头里没有Host那这就是一个错误的报文。
Host字段告诉服务器这个请求应该由哪个主机来处理当一台计算机上托管了多个虚拟主机的时候服务器端就需要用Host字段来选择有点像是一个简单的“路由重定向”。
例如我们的试验环境在127.0.0.1上有三个虚拟主机“www.chrono.com”“www.metroid.net”和“origin.io”。那么当使用域名的方式访问时就必须要用Host字段来区分这三个IP相同但域名不同的网站否则服务器就会找不到合适的虚拟主机无法处理。
**User-Agent**是请求字段只出现在请求头里。它使用一个字符串来描述发起HTTP请求的客户端服务器可以依据它来返回最合适此浏览器显示的页面。
但由于历史的原因User-Agent非常混乱每个浏览器都自称是“Mozilla”“Chrome”“Safari”企图使用这个字段来互相“伪装”导致User-Agent变得越来越长最终变得毫无意义。
不过有的比较“诚实”的爬虫会在User-Agent里用“spider”标明自己是爬虫所以可以利用这个字段实现简单的反爬虫策略。
**Date**字段是一个通用字段但通常出现在响应头里表示HTTP报文创建的时间客户端可以使用这个时间再搭配其他字段决定缓存策略。
**Server**字段是响应字段只能出现在响应头里。它告诉客户端当前正在提供Web服务的软件名称和版本号例如在我们的实验环境里它就是“Server: openresty/1.15.8.1”即使用的是OpenResty 1.15.8.1。
Server字段也不是必须要出现的因为这会把服务器的一部分信息暴露给外界如果这个版本恰好存在bug那么黑客就有可能利用bug攻陷服务器。所以有的网站响应头里要么没有这个字段要么就给出一个完全无关的描述信息。
比如GitHub它的Server字段里就看不出是使用了Apache还是Nginx只是显示为“GitHub.com”。
<img src="https://static001.geekbang.org/resource/image/f1/1c/f1970aaecad58fb18938e262ea7f311c.png" alt="">
实体字段里要说的一个是**Content-Length**它表示报文里body的长度也就是请求头或响应头空行后面数据的长度。服务器看到这个字段就知道了后续有多少数据可以直接接收。如果没有这个字段那么body就是不定长的需要使用chunked方式分段传输。
## 小结
今天我们学习了HTTP的报文结构下面做一个简单小结。
1. HTTP报文结构就像是“大头儿子”由“起始行+头部+空行+实体”组成简单地说就是“header+body”
1. HTTP报文可以没有body但必须要有header而且header后也必须要有空行形象地说就是“大头”必须要带着“脖子”
1. 请求头由“请求行+头部字段”构成,响应头由“状态行+头部字段”构成;
1. 请求行有三部分:请求方法,请求目标和版本号;
1. 状态行也有三部分:版本号,状态码和原因字符串;
1. 头部字段是key-value的形式用“:”分隔,不区分大小写,顺序任意,除了规定的标准头,也可以任意添加自定义字段,实现功能扩展;
1. HTTP/1.1里唯一要求必须提供的头字段是Host它必须出现在请求头里标记虚拟主机名。
## 课下作业
1. 如果拼HTTP报文的时候在头字段后多加了一个CRLF导致出现了一个空行会发生什么
1. 讲头字段时说“:”后的空格可以有多个,那为什么绝大多数情况下都只使用一个空格呢?
欢迎你把自己的答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/1a/26/1aa9cb1a1d637e10340451d81e87fc26.png" alt="unpreview">

View File

@@ -0,0 +1,161 @@
<audio id="audio" title="10 | 应该如何理解请求方法?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/07/ea/0784c7505e2fbce9f21b6ac7454b9aea.mp3"></audio>
上一讲我介绍了HTTP的报文结构它是由header+body构成请求头里有请求方法和请求目标响应头里有状态码和原因短语今天要说的就是请求头里的请求方法。
## 标准请求方法
HTTP协议里为什么要有“请求方法”这个东西呢
这就要从HTTP协议设计时的定位说起了。还记得吗蒂姆·伯纳斯-李最初设想的是要用HTTP协议构建一个超链接文档系统使用URI来定位这些文档也就是资源。那么该怎么在协议里操作这些资源呢
很显然需要有某种“动作的指示”告诉操作这些资源的方式。所以就这么出现了“请求方法”。它的实际含义就是客户端发出了一个“动作指令”要求服务器端对URI定位的资源执行这个动作。
目前HTTP/1.1规定了八种方法,单词**都必须是大写的形式**,我先简单地列把它们列出来,后面再详细讲解。
1. GET获取资源可以理解为读取或者下载数据
1. HEAD获取资源的元信息
1. POST向资源提交数据相当于写入或上传数据
1. PUT类似POST
1. DELETE删除资源
1. CONNECT建立特殊的连接隧道
1. OPTIONS列出可对资源实行的方法
1. TRACE追踪请求-响应的传输路径。
<img src="https://static001.geekbang.org/resource/image/3c/6d/3cdc8ac71b80929f4a94dfeb9ffe4b6d.jpg" alt="">
看看这些方法,是不是有点像对文件或数据库的“增删改查”操作,只不过这些动作操作的目标不是本地资源,而是远程服务器上的资源,所以只能由客户端“请求”或者“指示”服务器来完成。
既然请求方法是一个“指示”那么客户端自然就没有决定权服务器掌控着所有资源也就有绝对的决策权力。它收到HTTP请求报文后看到里面的请求方法可以执行也可以拒绝或者改变动作的含义毕竟HTTP是一个“协议”两边都要“商量着来”。
比如你发起了一个GET请求想获取“/orders”这个文件但这个文件保密级别比较高不是谁都能看的服务器就可以有如下的几种响应方式
1. 假装这个文件不存在直接返回一个404 Not found报文
1. 稍微友好一点明确告诉你有这个文件但不允许访问返回一个403 Forbidden
1. 再宽松一些返回405 Method Not Allowed然后用Allow头告诉你可以用HEAD方法获取文件的元信息。
## GET/HEAD
虽然HTTP/1.1里规定了八种请求方法,但只有前四个是比较常用的,所以我们先来看一下这四个方法。
**GET**方法应该是HTTP协议里最知名的请求方法了也应该是用的最多的自0.9版出现并一直被保留至今,是名副其实的“元老”。
它的含义是请求**从服务器获取资源**这个资源既可以是静态的文本、页面、图片、视频也可以是由PHP、Java动态生成的页面或者其他格式的数据。
GET方法虽然基本动作比较简单但搭配URI和其他头字段就能实现对资源更精细的操作。
例如在URI后使用“#”就可以在获取页面后直接定位到某个标签所在的位置使用If-Modified-Since字段就变成了“有条件的请求”仅当资源被修改时才会执行获取动作使用Range字段就是“范围请求”只获取资源的一部分数据。
**HEAD**方法与GET方法类似也是请求从服务器获取资源服务器的处理机制也是一样的但服务器不会返回请求的实体数据只会传回响应头也就是资源的“元信息”。
HEAD方法可以看做是GET方法的一个“简化版”或者“轻量版”。因为它的响应头与GET完全相同所以可以用在很多并不真正需要资源的场合避免传输body数据的浪费。
比如想要检查一个文件是否存在只要发个HEAD请求就可以了没有必要用GET把整个文件都取下来。再比如要检查文件是否有最新版本同样也应该用HEAD服务器会在响应头里把文件的修改时间传回来。
你可以在实验环境里试一下这两个方法运行Telnet分别向URI“/10-1”发送GET和HEAD请求观察一下响应头是否一致。
```
GET /10-1 HTTP/1.1
Host: www.chrono.com
HEAD /10-1 HTTP/1.1
Host: www.chrono.com
```
## POST/PUT
接下来要说的是**POST**和**PUT**方法,这两个方法也很像。
GET和HEAD方法是从服务器获取数据而POST和PUT方法则是相反操作向URI指定的资源提交数据数据就放在报文的body里。
POST也是一个经常用到的请求方法使用频率应该是仅次于GET应用的场景也非常多只要向服务器发送数据用的大多数都是POST。
比如你上论坛灌水敲了一堆字后点击“发帖”按钮浏览器就执行了一次POST请求把你的文字放进报文的body里然后拼好POST请求头通过TCP协议发给服务器。
又比如你上购物网站看到了一件心仪的商品点击“加入购物车”这时也会有POST请求浏览器会把商品ID发给服务器服务器再把ID写入你的购物车相关的数据库记录。
PUT的作用与POST类似也可以向服务器提交数据但与POST存在微妙的不同通常POST表示的是“新建”“create”的含义而PUT则是“修改”“update”的含义。
在实际应用中PUT用到的比较少。而且因为它与POST的语义、功能太过近似有的服务器甚至就直接禁止使用PUT方法只用POST方法上传数据。
实验环境的“/10-2”模拟了POST和PUT方法的处理过程你仍然可以用Telnet发送测试请求看看运行的效果。注意在发送请求时头字段“Content-Length”一定要写对是空行后body的长度
```
POST /10-2 HTTP/1.1
Host: www.chrono.com
Content-Length: 17
POST DATA IS HERE
PUT /10-2 HTTP/1.1
Host: www.chrono.com
Content-Length: 16
PUT DATA IS HERE
```
## 其他方法
讲完了GET/HEAD/POST/PUT还剩下四个标准请求方法它们属于比较“冷僻”的方法应用的不是很多。
**DELETE**方法指示服务器删除资源因为这个动作危险性太大所以通常服务器不会执行真正的删除操作而是对资源做一个删除标记。当然更多的时候服务器就直接不处理DELETE请求。
**CONNECT**是一个比较特殊的方法要求服务器为客户端和另一台远程服务器建立一条特殊的连接隧道这时Web服务器在中间充当了代理的角色。
**OPTIONS**方法要求服务器列出可对资源实行的操作方法在响应头的Allow字段里返回。它的功能很有限用处也不大有的服务器例如Nginx干脆就没有实现对它的支持。
**TRACE**方法多用于对HTTP链路的测试或诊断可以显示出请求-响应的传输路径。它的本意是好的但存在漏洞会泄漏网站的信息所以Web服务器通常也是禁止使用。
## 扩展方法
虽然HTTP/1.1里规定了八种请求方法但它并没有限制我们只能用这八种方法这也体现了HTTP协议良好的扩展性我们可以任意添加请求动作只要请求方和响应方都能理解就行。
例如著名的愚人节玩笑RFC2324它定义了协议HTCPCP即“超文本咖啡壶控制协议”为HTTP协议增加了用来煮咖啡的BREW方法要求添牛奶的WHEN方法。
此外还有一些得到了实际应用的请求方法WebDAV例如MKCOL、COPY、MOVE、LOCK、UNLOCK、PATCH等。如果有合适的场景你也可以把它们应用到自己的系统里比如用LOCK方法锁定资源暂时不允许修改或者使用PATCH方法给资源打个小补丁部分更新数据。但因为这些方法是非标准的所以需要为客户端和服务器编写额外的代码才能添加支持。
当然了你也完全可以根据实际需求自己发明新的方法比如“PULL”拉取某些资源到本地“PURGE”清理某个目录下的所有缓存数据。
## 安全与幂等
关于请求方法还有两个面试时有可能会问到、比较重要的概念:**安全**与**幂等**。
在HTTP协议里所谓的“**安全**”是指请求方法不会“破坏”服务器上的资源,即不会对服务器上的资源造成实质的修改。
按照这个定义只有GET和HEAD方法是“安全”的因为它们是“只读”操作只要服务器不故意曲解请求方法的处理方式无论GET和HEAD操作多少次服务器上的数据都是“安全的”。
而POST/PUT/DELETE操作会修改服务器上的资源增加或删除数据所以是“不安全”的。
所谓的“**幂等**”实际上是一个数学用语被借用到了HTTP协议里意思是多次执行相同的操作结果也都是相同的即多次“幂”后结果“相等”。
很显然GET和HEAD既是安全的也是幂等的DELETE可以多次删除同一个资源效果都是“资源不存在”所以也是幂等的。
POST和PUT的幂等性质就略费解一点。
按照RFC里的语义POST是“新增或提交数据”多次提交数据会创建多个资源所以不是幂等的而PUT是“替换或更新数据”多次更新一个资源资源还是会第一次更新的状态所以是幂等的。
我对你的建议是你可以对比一下SQL来加深理解把POST理解成INSERT把PUT理解成UPDATE这样就很清楚了。多次INSERT会添加多条记录而多次UPDATE只操作一条记录而且效果相同。
## 小结
今天我们学习了HTTP报文里请求方法相关的知识简单小结一下。
1. 请求方法是客户端发出的、要求服务器执行的、对资源的一种操作;
1. 请求方法是对服务器的“指示”,真正应如何处理由服务器来决定;
1. 最常用的请求方法是GET和POST分别是获取数据和发送数据
1. HEAD方法是轻量级的GET用来获取资源的元信息
1. PUT基本上是POST的同义词多用于更新数据
1. “安全”与“幂等”是描述请求方法的两个重要属性,具有理论指导意义,可以帮助我们设计系统。
## 课下作业
1. 你能把GET/POST等请求方法对应到数据库的“增删改查”操作吗请求头应该如何设计呢
1. 你觉得TRACE/OPTIONS/CONNECT方法能够用GET或POST间接实现吗
欢迎你把自己的答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,欢迎你把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/60/81/60ee384d93d46cd6632be0606ae21681.png" alt="unpreview">

View File

@@ -0,0 +1,194 @@
<audio id="audio" title="11 | 你能写出正确的网址吗?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/de/df/de20194c5948ebde8408520e53b4ccdf.mp3"></audio>
上一讲里我们一起学习了HTTP协议里的请求方法其中最常用的一个是GET它用来从服务器上某个资源获取数据另一个是POST向某个资源提交数据。
那么,应该用什么来标记服务器上的资源呢?怎么区分“这个”资源和“那个”资源呢?
经过前几讲的学习你一定已经知道了用的是URI也就是**统一资源标识符****U**niform **R**esource **I**dentifier。因为它经常出现在浏览器的地址栏里所以俗称为“网络地址”简称“网址”。
严格地说URI不完全等同于网址它包含有URL和URN两个部分在HTTP世界里用的网址实际上是URL——**统一资源定位符****U**niform **R**esource **L**ocator。但因为URL实在是太普及了所以常常把这两者简单地视为相等。
不仅我们生活中的上网要用到URI平常的开发、测试、运维的工作中也少不了它。
如果你在客户端做iOS、 Android或者某某小程序开发免不了要连接远程服务就会调用底层API用URI访问服务。
如果你使用Java、PHP做后台Web开发也会调用getPath()、parse_url() 等函数来处理URI解析里面的各个要素。
在测试、运维配置Apache、Nginx等Web服务器的时候也必须正确理解URI分离静态资源与动态资源或者设置规则实现网页的重定向跳转。
总之一句话URI非常重要要搞懂HTTP甚至网络应用就必须搞懂URI。
## URI的格式
不知道你平常上网的时候有没有关注过地址栏里的那一长串字符,有的比较简短,有的则一行都显示不下,有的意思大概能看明白,而有的则带着各种怪字符,有如“天书”。
其实只要你弄清楚了URI的格式就能够轻易地“破解”这些难懂的“天书”了。
URI本质上是一个字符串这个字符串的作用是**唯一地标记资源的位置或者名字**。
这里我要提醒你注意它不仅能够标记万维网的资源也可以标记其他的如邮件系统、本地文件系统等任意资源。而“资源”既可以是存在磁盘上的静态文本、页面数据也可以是由Java、PHP提供的动态服务。
下面的这张图显示了URI最常用的形式由scheme、host:port、path和query四个部分组成但有的部分可以视情况省略。
<img src="https://static001.geekbang.org/resource/image/46/2a/46581d7e1058558d8e12c1bf37d30d2a.png" alt="">
## URI的基本组成
URI第一个组成部分叫**scheme**,翻译成中文叫“**方案名**”或者“**协议名**”,表示**资源应该使用哪种协议**来访问。
最常见的当然就是“http”了表示使用HTTP协议。另外还有“https”表示使用经过加密、安全的HTTPS协议。此外还有其他不是很常见的scheme例如ftp、ldap、file、news等。
浏览器或者你的应用程序看到URI里的scheme就知道下一步该怎么走了会调用相应的HTTP或者HTTPS下层API。显然如果一个URI没有提供scheme即使后面的地址再完善也是无法处理的。
在scheme之后必须是**三个特定的字符**“**://**”它把scheme和后面的部分分离开。
实话实说,这个设计非常的怪异,我最早上网的时候看见地址栏里的“://”就觉得很别扭直到现在也还是没有太适应。URI的创造者蒂姆·伯纳斯-李也曾经私下承认“://”并非必要,当初有些“过于草率”了。
不过这个设计已经有了三十年的历史,不管我们愿意不愿意,只能接受。
在“://”之后,是被称为“**authority**”的部分,表示**资源所在的主机名**通常的形式是“host:port”即主机名加端口号。
主机名可以是IP地址或者域名的形式必须要有否则浏览器就会找不到服务器。但端口号有时可以省略浏览器等客户端会依据scheme使用默认的端口号例如HTTP的默认端口号是80HTTPS的默认端口号是443。
有了协议名和主机地址、端口号,再加上后面**标记资源所在位置**的**path**,浏览器就可以连接服务器访问资源了。
URI里path采用了类似文件系统“目录”“路径”的表示方式因为早期互联网上的计算机多是UNIX系统所以采用了UNIX的“/”风格。其实也比较好理解它与scheme后面的“://”是一致的。
这里我也要再次提醒你注意URI的path部分必须以“/”开始,也就是必须包含“/”,不要把“/”误认为属于前面authority。
说了这么多“理论”,来看几个实例。
```
http://nginx.org
http://www.chrono.com:8080/11-1
https://tools.ietf.org/html/rfc7230
file:///D:/http_study/www/
```
第一个URI算是最简单的了协议名是“http”主机名是“nginx.org”端口号省略所以是默认的80而路径部分也被省略了默认就是一个“/”,表示根目录。
第二个URI是在实验环境里这次课程的专用URI主机名是“www.chrono.com”端口号是8080后面的路径是“/11-1”。
第三个是HTTP协议标准文档RFC7230的URI主机名是“tools.ietf.org”路径是“/html/rfc7230”。
最后一个URI要注意了它的协议名不是“http”而是“file”表示这是本地文件而后面居然有三个斜杠这是怎么回事
如果你刚才仔细听了scheme的介绍就能明白这三个斜杠里的前两个属于URI特殊分隔符“://”,然后后面的“/D:/http_study/www/”是路径而中间的主机名被“省略”了。这实际上是file类型URI的“特例”它允许省略主机名默认是本机localhost。
但对于HTTP或HTTPS这样的网络通信协议主机名是绝对不能省略的。原因之前也说了会导致浏览器无法找到服务器。
我们可以在实验环境里用Chrome浏览器再仔细观察一下HTTP报文里的URI。
运行Chrome用F12打开开发者工具然后在地址栏里输入“[http://www.chrono.com/11-1](http://www.chrono.com/11-1)”,得到的结果如下图。
<img src="https://static001.geekbang.org/resource/image/20/9f/20ac5ee55b8ee30527492c8abb60ff9f.png" alt="">
在开发者工具里依次选“Network”“Doc”就可以找到请求的URI。然后在Headers页里看Request Headers用“view source”就可以看到浏览器发的原始请求头了。
发现了什么特别的没有?
在HTTP报文里的URI“/11-1”与浏览器里输入的“[http://www.chrono.com/11-1](http://www.chrono.com/11-1)”有很大的不同,协议名和主机名都不见了,只剩下了后面的部分。
这是因为协议名和主机名已经分别出现在了请求行的版本号和请求头的Host字段里没有必要再重复。当然在请求行里使用完整的URI也是可以的你可以在课后自己试一下。
通过这个小实验我们还得到了一个结论客户端和服务器看到的URI是不一样的。客户端看到的必须是完整的URI使用特定的协议去连接特定的主机而服务器看到的只是报文请求行里被删除了协议名和主机名的URI。
如果你配置过Nginx你就应该明白了Nginx作为一个Web服务器它的location、rewrite等指令操作的URI其实指的是真正URI里的path和后续的部分。
## URI的查询参数
使用“协议名+主机名+路径”的方式,已经可以精确定位网络上的任何资源了。但这还不够,很多时候我们还想在操作资源的时候附加一些额外的修饰参数。
举几个例子获取商品图片但想要一个32×32的缩略图版本获取商品列表但要按某种规则做分页和排序跳转页面但想要标记跳转前的原始页面。
仅用“协议名+主机名+路径”的方式是无法适应这些场景的所以URI后面还有一个“**query**”部分它在path之后用一个“?”开始,但不包含“?”,表示对资源附加的额外要求。这是个很形象的符号,比“://”要好的多,很明显地表示了“查询”的含义。
查询参数query有一套自己的格式是多个“**key=value**”的字符串这些KV值用字符“**&amp;**”连接,浏览器和服务器都可以按照这个格式把长串的查询参数解析成可理解的字典或关联数组形式。
你可以在实验环境里用Chrome试试下面这个加了query参数的URI
```
http://www.chrono.com:8080/11-1?uid=1234&amp;name=mario&amp;referer=xxx
```
Chrome的开发者工具也能解码出query里的KV对省得我们“人肉”分解。
<img src="https://static001.geekbang.org/resource/image/e4/f3/e42073080968e8e0c58d9a9126ab82f3.png" alt="">
还可以再拿一个实际的URI来看一下这个URI是某电商网站的一个商品查询URI比较复杂但相信现在的你能够毫不费力地区分出里面的协议名、主机名、路径和查询参数。
```
https://search.jd.com/Search?keyword=openresty&amp;enc=utf-8&amp;qrst=1&amp;rt=1&amp;stop=1&amp;vt=2&amp;wq=openresty&amp;psort=3&amp;click=0
```
你也可以把这个URI输入到Chrome的地址栏里再用开发者工具仔细检查它的组成部分。
## URI的完整格式
讲完了query参数URI就算完整了HTTP协议里用到的URI绝大多数都是这种形式。
不过必须要说的是URI还有一个“真正”的完整形态如下图所示。
<img src="https://static001.geekbang.org/resource/image/ff/38/ff41d020c7a27d1e8191057f0e658b38.png" alt="">
这个“真正”形态比基本形态多了两部分。
第一个多出的部分是协议名之后、主机名之前的**身份信息**“user:passwd@”表示登录主机时的用户名和密码但现在已经不推荐使用这种形式了RFC7230因为它把敏感信息以明文形式暴露出来存在严重的安全隐患。
第二个多出的部分是查询参数后的**片段标识符**“#fragment它是URI所定位的资源内部的一个“锚点”或者说是“标签”浏览器可以在获取资源后直接跳转到它指示的位置。
但片段标识符仅能由浏览器这样的客户端使用,服务器是看不到的。也就是说,浏览器永远不会把带“#fragment”的URI发送给服务器服务器也永远不会用这种方式去处理资源的片段。
## URI的编码
刚才我们看到了在URI里只能使用ASCII码但如果要在URI里使用英语以外的汉语、日语等其他语言该怎么办呢
还有某些特殊的URI会在path、query里出现“@&amp;?"等起界定符作用的字符会导致URI解析错误这时又该怎么办呢
所以URI引入了编码机制对于ASCII码以外的字符集和特殊字符做一个特殊的操作把它们转换成与URI语义不冲突的形式。这在RFC规范里称为“escape”和“unescape”俗称“转义”。
URI转义的规则有点“简单粗暴”直接把非ASCII码或特殊字符转换成十六进制字节值然后前面再加上一个“%”。
例如,空格被转义成“%20”“?”被转义成“%3F”。而中文、日文等则通常使用UTF-8编码后再转义例如“银河”会被转义成“%E9%93%B6%E6%B2%B3”。
有了这个编码规则后URI就更加完美了可以支持任意的字符集用任何语言来标记资源。
不过我们在浏览器的地址栏里通常是不会看到这些转义后的“乱码”的这实际上是浏览器一种“友好”表现隐藏了URI编码后的“丑陋一面”不信你可以试试下面的这个URI。
```
http://www.chrono.com:8080/11-1?夸父逐日
```
先在Chrome的地址栏里输入这个query里含有中文的URI然后点击地址栏把它再拷贝到其他的编辑器里它就会“现出原形”
```
http://www.chrono.com:8080/11-1?%E5%A4%B8%E7%88%B6%E9%80%90%E6%97%A5
```
## 小结
今天我们学习了网址也就是URI的知识在这里小结一下今天的内容。
1. URI是用来唯一标记服务器上资源的一个字符串通常也称为URL
1. URI通常由scheme、host:port、path和query四个部分组成有的可以省略
1. scheme叫“方案名”或者“协议名”表示资源应该使用哪种协议来访问
1. “host:port”表示资源所在的主机名和端口号
1. path标记资源所在的位置
1. query表示对资源附加的额外要求
1. 在URI里对“@&amp;/”等特殊字符和汉字必须要做编码否则服务器收到HTTP报文后会无法正确处理。
## 课下作业
1. HTTP协议允许在在请求行里使用完整的URI但为什么浏览器没有这么做呢
1. URI的查询参数和头字段很相似都是key-value形式都可以任意自定义那么它们在使用时该如何区别呢具体分析可以在“答疑篇”[第41讲](https://time.geekbang.org/column/article/146833)中的URI查询参数和头字段部分查看
欢迎你把自己的答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,欢迎你把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/26/cb/26e06ff1fee9a7cd0b9c1a99bf9d32cb.png" alt="">

View File

@@ -0,0 +1,141 @@
<audio id="audio" title="12 | 响应状态码该怎么用?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/48/77/48e25c659bc0c244092903ce7b3f7e77.mp3"></audio>
前两讲中我们学习了HTTP报文里请求行的组成部分包括请求方法和URI。有了请求行加上后面的头字段就形成了请求头可以通过TCP/IP协议发送给服务器。
服务器收到请求报文,解析后需要进行处理,具体的业务逻辑多种多样,但最后必定是拼出一个响应报文发回客户端。
响应报文由响应头加响应体数据组成,响应头又由状态行和头字段构成。
我们先来复习一下状态行的结构,有三部分:
<img src="https://static001.geekbang.org/resource/image/a1/00/a1477b903cd4d5a69686683c0dbc3300.png" alt="">
开头的Version部分是HTTP协议的版本号通常是HTTP/1.1,用处不是很大。
后面的Reason部分是原因短语是状态码的简短文字描述例如“OK”“Not Found”等等也可以自定义。但它只是为了兼容早期的文本客户端而存在提供的信息很有限目前的大多数客户端都会忽略它。
所以,状态行里有用的就只剩下中间的**状态码**Status Code了。它是一个十进制数字以代码的形式表示服务器对请求的处理结果就像我们通常编写程序时函数返回的错误码一样。
不过你要注意它的名字是“状态码”而不是“错误码”。也就是说它的含义不仅是错误更重要的意义在于表达HTTP数据处理的“状态”客户端可以依据代码适时转换处理状态例如继续发送请求、切换协议重定向跳转等有那么点TCP状态转换的意思。
## 状态码
目前RFC标准里规定的状态码是三位数所以取值范围就是从000到999。但如果把代码简单地从000开始顺序编下去就显得有点太“low”不灵活、不利于扩展所以状态码也被设计成有一定的格式。
RFC标准把状态码分成了五类用数字的第一位表示分类而0~99不用这样状态码的实际可用范围就大大缩小了由000~999变成了100~599。
这五类的具体含义是:
- 1××提示信息表示目前是协议处理的中间状态还需要后续的操作
- 2××成功报文已经收到并被正确处理
- 3××重定向资源位置发生变动需要客户端重新发送请求
- 4××客户端错误请求报文有误服务器无法处理
- 5××服务器错误服务器在处理请求时内部发生了错误。
在HTTP协议中正确地理解并应用这些状态码不是客户端或服务器单方的责任而是双方共同的责任。
客户端作为请求的发起方,获取响应报文后,需要通过状态码知道请求是否被正确处理,是否要再次发送请求,如果出错了原因又是什么。这样才能进行下一步的动作,要么发送新请求,要么改正错误重发请求。
服务器端作为请求的接收方也应该很好地运用状态码。在处理请求时选择最恰当的状态码回复客户端告知客户端处理的结果指示客户端下一步应该如何行动。特别是在出错的时候尽量不要简单地返400、500这样意思含糊不清的状态码。
目前RFC标准里总共有41个状态码但状态码的定义是开放的允许自行扩展。所以Apache、Nginx等Web服务器都定义了一些专有的状态码。如果你自己开发Web应用也完全可以在不冲突的前提下定义新的代码。
在我们的实验环境里也可以对这些状态码做测试验证访问URI“**/12-1**”用查询参数“code=xxx”来检查这些状态码的效果服务器不仅会在状态行里显示状态码还会在响应头里用自定义的“Expect-Code”字段输出这个代码。
例如在Chrome里访问“[http://www.chrono.com/12-1?code=405](http://www.chrono.com/12-1?code=405)”的结果如下图。
<img src="https://static001.geekbang.org/resource/image/07/d7/07e7a40241a09683c5420e7b311227d7.png" alt="">
接下来我就挑一些实际开发中比较有价值的状态码逐个详细介绍。
## 1××
1××类状态码属于提示信息是协议处理的中间状态实际能够用到的时候很少。
我们偶尔能够见到的是“**101 Switching Protocols**”。它的意思是客户端使用Upgrade头字段要求在HTTP协议的基础上改成其他的协议继续通信比如WebSocket。而如果服务器也同意变更协议就会发送状态码101但这之后的数据传输就不会再使用HTTP了。
## 2××
2××类状态码表示服务器收到并成功处理了客户端的请求这也是客户端最愿意看到的状态码。
“**200 OK**”是最常见的成功状态码表示一切正常服务器如客户端所期望的那样返回了处理结果如果是非HEAD请求通常在响应头后都会有body数据。
“**204 No Content**”是另一个很常见的成功状态码它的含义与“200 OK”基本相同但响应头后没有body数据。所以对于Web服务器来说正确地区分200和204是很必要的。
“**206 Partial Content**”是HTTP分块下载或断点续传的基础在客户端发送“范围请求”、要求获取资源的部分数据时出现它与200一样也是服务器成功处理了请求但body里的数据不是资源的全部而是其中的一部分。
状态码206通常还会伴随着头字段“**Content-Range**”表示响应报文里body数据的具体范围供客户端确认例如“Content-Range: bytes 0-99/2000”意思是此次获取的是总计2000个字节的前100个字节。
## 3××
3××类状态码表示客户端请求的资源发生了变动客户端必须用新的URI重新发送请求获取资源也就是通常所说的“重定向”包括著名的301、302跳转。
“**301 Moved Permanently**”俗称“永久重定向”含义是此次请求的资源已经不存在了需要改用新的URI再次访问。
与它类似的是“**302 Found**”,曾经的描述短语是“**Moved Temporarily**”俗称“临时重定向”意思是请求的资源还在但需要暂时用另一个URI来访问。
301和302都会在响应头里使用字段**Location**指明后续要跳转的URI最终的效果很相似浏览器都会重定向到新的URI。两者的根本区别在于语义一个是“永久”一个是“临时”所以在场景、用法上差距很大。
比如你的网站升级到了HTTPS原来的HTTP不打算用了这就是“永久”的所以要配置301跳转把所有的HTTP流量都切换到HTTPS。
再比如今天夜里网站后台要系统维护服务暂时不可用这就属于“临时”的可以配置成302跳转把流量临时切换到一个静态通知页面浏览器看到这个302就知道这只是暂时的情况不会做缓存优化第二天还会访问原来的地址。
“**304 Not Modified**” 是一个比较有意思的状态码它用于If-Modified-Since等条件请求表示资源未修改用于缓存控制。它不具有通常的跳转含义但可以理解成“重定向已到缓存的文件”即“缓存重定向”
301、302和304分别涉及了HTTP协议里重要的“重定向跳转”和“缓存控制”在之后的课程中我还会细讲。
## 4××
4××类状态码表示客户端发送的请求报文有误服务器无法处理它就是真正的“错误码”含义了。
“**400 Bad Request**”是一个通用的错误码表示请求报文有错误但具体是数据格式错误、缺少请求头还是URI超长它没有明确说只是一个笼统的错误客户端看到400只会是“一头雾水”“不知所措”。所以在开发Web应用时应当尽量避免给客户端返回400而是要用其他更有明确含义的状态码。
“**403 Forbidden**”实际上不是客户端的请求出错而是表示服务器禁止访问资源。原因可能多种多样例如信息敏感、法律禁止等如果服务器友好一点可以在body里详细说明拒绝请求的原因不过现实中通常都是直接给一个“闭门羹”。
“**404 Not Found**”可能是我们最常看见也是最不愿意看到的一个状态码它的原意是资源在本服务器上未找到所以无法提供给客户端。但现在已经被“用滥了”只要服务器“不高兴”就可以给出个404而我们也无从得知后面到底是真的未找到还是有什么别的原因某种程度上它比403还要令人讨厌。
4××里剩下的一些代码较明确地说明了错误的原因都很好理解开发中常用的有
- 405 Method Not Allowed不允许使用某些方法操作资源例如不允许POST只能GET
- 406 Not Acceptable资源无法满足客户端请求的条件例如请求中文但只有英文
- 408 Request Timeout请求超时服务器等待了过长的时间
- 409 Conflict多个请求发生了冲突可以理解为多线程并发时的竞态
- 413 Request Entity Too Large请求报文里的body太大
- 414 Request-URI Too Long请求行里的URI太大
- 429 Too Many Requests客户端发送了太多的请求通常是由于服务器的限连策略
- 431 Request Header Fields Too Large请求头某个字段或总体太大
## 5××
5××类状态码表示客户端请求报文正确但服务器在处理时内部发生了错误无法返回应有的响应数据是服务器端的“错误码”。
“**500 Internal Server Error**”与400类似也是一个通用的错误码服务器究竟发生了什么错误我们是不知道的。不过对于服务器来说这应该算是好事通常不应该把服务器内部的详细信息例如出错的函数调用栈告诉外界。虽然不利于调试但能够防止黑客的窥探或者分析。
“**501 Not Implemented**”表示客户端请求的功能还不支持这个错误码比500要“温和”一些和“即将开业敬请期待”的意思差不多不过具体什么时候“开业”就不好说了。
“**502 Bad Gateway**”通常是服务器作为网关或者代理时返回的错误码,表示服务器自身工作正常,访问后端服务器时发生了错误,但具体的错误原因也是不知道的。
“**503 Service Unavailable**”表示服务器当前很忙暂时无法响应服务我们上网时有时候遇到的“网络服务正忙请稍后重试”的提示信息就是状态码503。
503是一个“临时”的状态很可能过几秒钟后服务器就不那么忙了可以继续提供服务所以503响应报文里通常还会有一个“**Retry-After**”字段,指示客户端可以在多久以后再次尝试发送请求。
## 小结
1. 状态码在响应报文里表示了服务器对请求的处理结果;
1. 状态码后的原因短语是简单的文字描述,可以自定义;
1. 状态码是十进制的三位数分为五类从100到599
1. 2××类状态码表示成功常用的有200、204、206
1. 3××类状态码表示重定向常用的有301、302、304
1. 4××类状态码表示客户端错误常用的有400、403、404
1. 5××类状态码表示服务器错误常用的有500、501、502、503。
## 课下作业
1. 你在开发HTTP客户端收到了一个非标准的状态码比如4××、5××应当如何应对呢
1. 你在开发HTTP服务器处理请求时发现报文里缺了一个必需的query参数应该如何告知客户端错误原因呢
欢迎你把自己的答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,欢迎你把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/11/ad/11d330fe6de5b9fe34464a6994162dad.png" alt="">
<img src="https://static001.geekbang.org/resource/image/56/63/56d766fc04654a31536f554b8bde7b63.jpg" alt="unpreview">

View File

@@ -0,0 +1,104 @@
<audio id="audio" title="13 | HTTP有哪些特点" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d6/d6/d6554abef86d388199d15c4bc259e0d6.mp3"></audio>
通过“基础篇”前几讲的学习你应该已经知道了HTTP协议的基本知识了解它的报文结构请求头、响应头以及内部的请求方法、URI和状态码等细节。
你会不会有种疑惑“HTTP协议好像也挺简单的啊凭什么它就能统治互联网这么多年呢
所以接下来的这两讲我会跟你聊聊HTTP协议的特点、优点和缺点。既要看到它好的一面也要正视它不好的一面只有全方位、多角度了解HTTP才能实现“扬长避短”更好地利用HTTP。
今天这节课主要说的是HTTP协议的特点但不会讲它们的好坏这些特点即有可能是优点也有可能是缺点你可以边听边思考。
<img src="https://static001.geekbang.org/resource/image/78/4a/7808b195c921e0685958c20509855d4a.png" alt="">
## 灵活可扩展
首先, HTTP协议是一个“灵活可扩展”的传输协议。
HTTP协议最初诞生的时候就比较简单本着开放的精神只规定了报文的基本格式比如用空格分隔单词用换行分隔字段“header+body”等报文里的各个组成部分都没有做严格的语法语义限制可以由开发者任意定制。
所以HTTP协议就随着互联网的发展一同成长起来了。在这个过程中HTTP协议逐渐增加了请求方法、版本号、状态码、头字段等特性。而body也不再限于文本形式的TXT或HTML而是能够传输图片、音频视频等任意数据这些都是源于它的“灵活可扩展”的特点。
而那些RFC文档实际上也可以理解为是对已有扩展的“承认和标准化”实现了“从实践中来到实践中去”的良性循环。
也正是因为这个特点HTTP才能在三十年的历史长河中“屹立不倒”从最初的低速实验网络发展到现在的遍布全球的高速互联网始终保持着旺盛的生命力。
## 可靠传输
第二个特点, HTTP协议是一个“可靠”的传输协议。
这个特点显而易见因为HTTP协议是基于TCP/IP的而TCP本身是一个“可靠”的传输协议所以HTTP自然也就继承了这个特性能够在请求方和应答方之间“可靠”地传输数据。
它的具体做法与TCP/UDP差不多都是对实际传输的数据entity做了一层包装加上一个头然后调用Socket API通过TCP/IP协议栈发送或者接收。
不过我们必须正确地理解“可靠”的含义HTTP并不能100%保证数据一定能够发送到另一端,在网络繁忙、连接质量差等恶劣的环境下,也有可能收发失败。“可靠”只是向使用者提供了一个“承诺”,会在下层用多种手段“尽量”保证数据的完整送达。
当然如果遇到光纤被意外挖断这样的极端情况即使是神仙也不能发送成功。所以“可靠”传输是指在网络基本正常的情况下数据收发必定成功借用运维里的术语大概就是“3个9”或者“4个9”的程度吧。
## 应用层协议
第三个特点HTTP协议是一个应用层的协议。
这个特点也是不言自明的,但却很重要。
在TCP/IP诞生后的几十年里虽然出现了许多的应用层协议但它们都仅关注很小的应用领域局限在很少的应用场景。例如FTP只能传输文件、SMTP只能发送邮件、SSH只能远程登录等在通用的数据传输方面“完全不能打”。
所以HTTP凭借着可携带任意头字段和实体数据的报文结构以及连接控制、缓存代理等方便易用的特性一出现就“技压群雄”迅速成为了应用层里的“明星”协议。只要不太苛求性能HTTP几乎可以传递一切东西满足各种需求称得上是一个“万能”的协议。
套用一个网上流行的段子HTTP完全可以用开玩笑的口吻说“不要误会我不是针对FTP我是说在座的应用层各位都是垃圾。”
## 请求-应答
第四个特点HTTP协议使用的是请求-应答通信模式。
这个请求-应答模式是HTTP协议最根本的通信模型通俗来讲就是“一发一收”“有来有去”就像是写代码时的函数调用只要填好请求头里的字段“调用”后就会收到答复。
请求-应答模式也明确了HTTP协议里通信双方的定位永远是请求方先发起连接和请求是主动的而应答方只有在收到请求后才能答复是被动的如果没有请求时不会有任何动作。
当然,请求方和应答方的角色也不是绝对的,在浏览器-服务器的场景里,通常服务器都是应答方,但如果将它用作代理连接后端服务器,那么它就可能同时扮演请求方和应答方的角色。
HTTP的请求-应答模式也恰好契合了传统的C/SClient/Server系统架构请求方作为客户端、应答方作为服务器。所以随着互联网的发展就出现了B/SBrowser/Server架构用轻量级的浏览器代替笨重的客户端应用实现零维护的“瘦”客户端而服务器则摈弃私有通信协议转而使用HTTP协议。
此外,请求-应答模式也完全符合RPCRemote Procedure Call的工作模式可以把HTTP请求处理封装成远程函数调用导致了WebService、RESTful和gRPC等的出现。
## 无状态
第五个特点HTTP协议是无状态的。
这个所谓的“状态”应该怎么理解呢?
“状态”其实就是客户端或者服务器里保存的一些数据或者标志,记录了通信过程中的一些变化信息。
你一定知道TCP协议是有状态的一开始处于CLOSED状态连接成功后是ESTABLISHED状态断开连接后是FIN-WAIT状态最后又是CLOSED状态。
这些“状态”就需要TCP在内部用一些数据结构去维护可以简单地想象成是个标志量标记当前所处的状态例如0是CLOSED2是ESTABLISHED等等。
再来看HTTP那么对比一下TCP就看出来了在整个协议里没有规定任何的“状态”客户端和服务器永远是处在一种“**无知**”的状态。建立连接前两者互不知情,每次收发的报文也都是互相独立的,没有任何的联系。收发报文也不会对客户端或服务器产生任何影响,连接后也不会要求保存任何信息。
“无状态”形象地来说就是“没有记忆能力”。比如浏览器发了一个请求说“我是小明请给我A文件。”服务器收到报文后就会检查一下权限看小明确实可以访问A文件于是把文件发回给浏览器。接着浏览器还想要B文件但服务器不会记录刚才的请求状态不知道第二个请求和第一个请求是同一个浏览器发来的所以浏览器必须还得重复一次自己的身份才行“我是刚才的小明请再给我B文件。”
我们可以再对比一下UDP协议不过它是无连接也无状态的顺序发包乱序收包数据包发出去后就不管了收到后也不会顺序整理。而HTTP是有连接无状态顺序发包顺序收包按照收发的顺序管理报文。
但不要忘了HTTP是“灵活可扩展”的虽然标准里没有规定“状态”但完全能够在协议的框架里给它“打个补丁”增加这个特性。
## 其他特点
除了以上的五大特点其实HTTP协议还可以列出非常多的特点例如传输的实体数据可缓存可压缩、可分段获取数据、支持身份认证、支持国际化语言等。但这些并不能算是HTTP的基本特点因为这都是由第一个“灵活可扩展”的特点所衍生出来的。
## 小结
1. HTTP是灵活可扩展的可以任意添加头字段实现任意功能
1. HTTP是可靠传输协议基于TCP/IP协议“尽量”保证数据的送达
1. HTTP是应用层协议比FTP、SSH等更通用功能更多能够传输任意数据
1. HTTP使用了请求-应答模式,客户端主动发起请求,服务器被动回复请求;
1. HTTP本质上是无状态的每个请求都是互相独立、毫无关联的协议不要求客户端或服务器记录请求相关的信息。
## 课下作业
1. 就如同开头我讲的那样你能说一下今天列出的这些HTTP的特点中哪些是优点哪些是缺点吗
1. 不同的应用场合有不同的侧重方面,你觉得哪个特点对你来说是最重要的呢?
欢迎你把自己的答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,欢迎你把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/a2/7d/a233c19f92c566614e4e0facbaeab27d.png" alt="unpreview">
<img src="https://static001.geekbang.org/resource/image/56/63/56d766fc04654a31536f554b8bde7b63.jpg" alt="unpreview">

View File

@@ -0,0 +1,124 @@
<audio id="audio" title="14 | HTTP有哪些优点又有哪些缺点" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c0/8d/c03e1b29c56f439415690ec5a20c138d.mp3"></audio>
上一讲我介绍了HTTP的五个基本特点这一讲要说的则是它的优点和缺点。其实这些也应该算是HTTP的特点但这一讲会更侧重于评价它们的优劣和好坏。
上一讲我也留了两道课下作业,不知道你有没有认真思考过,今天可以一起来看看你的答案与我的观点想法是否相符,共同探讨。
不过在正式开讲之前我还要提醒你一下今天的讨论范围仅限于HTTP/1.1所说的优点和缺点也仅针对HTTP/1.1。实际上专栏后续要讲的HTTPS和HTTP/2都是对HTTP/1.1优点的发挥和缺点的完善。
## 简单、灵活、易于扩展
首先HTTP最重要也是最突出的优点是“**简单、灵活、易于扩展**”。
初次接触HTTP的人都会认为HTTP协议是很“**简单**”的基本的报文格式就是“header+body”头部信息也是简单的文本格式用的也都是常见的英文单词即使不去看RFC文档只靠猜也能猜出个“八九不离十”。
可不要小看了“简单”这个优点它不仅降低了学习和使用的门槛能够让更多的人研究和开发HTTP应用而且我在[第1讲](https://time.geekbang.org/column/article/97837)时就说过,“简单”蕴含了进化和扩展的可能性,所谓“少即是多”,“把简单的系统变复杂”,要比“把复杂的系统变简单”容易得多**。**
所以在“简单”这个最基本的设计理念之下HTTP协议又多出了“**灵活和易于扩展**”的优点。
“灵活和易于扩展”实际上是一体的它们互为表里、相互促进因为“灵活”所以才会“易于扩展”而“易于扩展”又反过来让HTTP更加灵活拥有更强的表现能力。
HTTP协议里的请求方法、URI、状态码、原因短语、头字段等每一个核心组成要素都没有被“写死”允许开发者任意定制、扩充或解释给予了浏览器和服务器最大程度的信任和自由也正好符合了互联网“自由与平等”的精神——缺什么功能自己加个字段或者错误码什么的补上就是了。
“请勿跟踪”所使用的头字段 DNTDo Not Track就是一个很好的例子。它最早由Mozilla提出用来保护用户隐私防止网站监测追踪用户的偏好。不过可惜的是DNT从推出至今有差不多七八年的历史但很多网站仍然选择“无视”DNT。虽然DNT基本失败了但这也正说明HTTP协议是“灵活自由的”不会受单方面势力的压制。
“灵活、易于扩展”的特性还表现在HTTP对“可靠传输”的定义上它不限制具体的下层协议不仅可以使用TCP、UNIX Domain Socket还可以使用SSL/TLS甚至是基于UDP的QUIC下层可以随意变化而上层的语义则始终保持稳定。
## 应用广泛、环境成熟
HTTP协议的另一大优点是“**应用广泛**”,软硬件环境都非常成熟。
随着互联网特别是移动互联网的普及HTTP的触角已经延伸到了世界的每一个角落从简单的Web页面到复杂的JSON、XML数据从台式机上的浏览器到手机上的各种APP从看新闻、泡论坛到购物、理财、“吃鸡”你很难找到一个没有使用HTTP的地方。
不仅在应用领域在开发领域HTTP协议也得到了广泛的支持。它并不限定某种编程语言或者操作系统所以天然具有“**跨语言、跨平台**”的优越性。而且因为本身的简单特性很容易实现所以几乎所有的编程语言都有HTTP调用库和外围的开发测试工具这一点我觉得就不用再举例了吧你可能比我更熟悉。
HTTP广泛应用的背后还有许多硬件基础设施支持各个互联网公司和传统行业公司都不遗余力地“触网”购买服务器开办网站建设数据中心、CDN和高速光纤持续地优化上网体验让HTTP运行的越来越顺畅。
“应用广泛”的这个优点也就决定了无论是创业者还是求职者无论是做网站服务器还是写应用客户端HTTP协议都是必须要掌握的基本技能。
## 无状态
看过了两个优点我们再来看看一把“双刃剑”也就是上一讲中说到的“无状态”它对于HTTP来说既是优点也是缺点。
“无状态”有什么好处呢?
因为服务器没有“记忆能力”所以就不需要额外的资源来记录状态信息不仅实现上会简单一些而且还能减轻服务器的负担能够把更多的CPU和内存用来对外提供服务。
而且,“无状态”也表示服务器都是相同的,没有“状态”的差异,所以可以很容易地组成集群,让负载均衡把请求转发到任意一台服务器,不会因为状态不一致导致处理出错,使用“堆机器”的“笨办法”轻松实现高并发高可用。
那么,“无状态”又有什么坏处呢?
既然服务器没有“记忆能力”,它就无法支持需要连续多个步骤的“事务”操作。例如电商购物,首先要登录,然后添加购物车,再下单、结算、支付,这一系列操作都需要知道用户的身份才行,但“无状态”服务器是不知道这些请求是相互关联的,每次都得问一遍身份信息,不仅麻烦,而且还增加了不必要的数据传输量。
所以HTTP协议最好是既“无状态”又“有状态”不过还真有“鱼和熊掌”两者兼得这样的好事这就是“小甜饼”Cookie技术第19讲
## 明文
HTTP协议里还有一把优缺点一体的“双刃剑”就是**明文传输**。
“明文”意思就是协议里的报文准确地说是header部分不使用二进制数据而是用简单可阅读的文本形式。
对比TCP、UDP这样的二进制协议它的优点显而易见不需要借助任何外部工具用浏览器、Wireshark或者tcpdump抓包后直接用肉眼就可以很容易地查看或者修改为我们的开发调试工作带来极大的便利。
当然明文的缺点也是一样显而易见HTTP报文的所有信息都会暴露在“光天化日之下”在漫长的传输链路的每一个环节上都毫无隐私可言不怀好意的人只要侵入了这个链路里的某个设备简单地“旁路”一下流量就可以实现对通信的窥视。
你有没有听说过“免费WiFi陷阱”之类的新闻呢
黑客就是利用了HTTP明文传输的缺点在公共场所架设一个WiFi热点开始“钓鱼”诱骗网民上网。一旦你连上了这个WiFi热点所有的流量都会被截获保存里面如果有银行卡号、网站密码等敏感信息的话那就危险了黑客拿到了这些数据就可以冒充你为所欲为。
## 不安全
与“明文”缺点相关但不完全等同的另一个缺点是“不安全”。
安全有很多的方面明文只是“机密”方面的一个缺点在“身份认证”和“完整性校验”这两方面HTTP也是欠缺的。
“身份认证”简单来说就是“**怎么证明你就是你**”。在现实生活中比较好办,你可以拿出身份证、驾照或者护照,上面有照片和权威机构的盖章,能够证明你的身份。
但在虚拟的网络世界里这却是个麻烦事。HTTP没有提供有效的手段来确认通信双方的真实身份。虽然协议里有一个基本的认证机制但因为刚才所说的明文传输缺点这个机制几乎可以说是“纸糊的”非常容易被攻破。如果仅使用HTTP协议很可能你会连到一个页面一模一样但却是个假冒的网站然后再被“钓”走各种私人信息。
HTTP协议也不支持“完整性校验”数据在传输过程中容易被窜改而无法验证真伪。
比如你收到了一条银行用HTTP发来的消息“小明向你转账一百元”你无法知道小明是否真的就只转了一百元也许他转了一千元或者五十元但被黑客窜改成了一百元真实情况到底是什么样子HTTP协议没有办法给你答案。
虽然银行可以用MD5、SHA1等算法给报文加上数字摘要但还是因为“明文”这个致命缺点黑客可以连同摘要一同修改最终还是判断不出报文是否被窜改。
为了解决HTTP不安全的缺点所以就出现了HTTPS这个我们以后再说。
## 性能
最后我们来谈谈HTTP的性能可以用六个字来概括“**不算差,不够好**”。
HTTP协议基于TCP/IP并且使用了“请求-应答”的通信模式,所以性能的关键就在这两点上。
必须要说的是TCP的性能是不差的否则也不会纵横互联网江湖四十余载了而且它已经被研究的很透集成在操作系统内核里经过了细致的优化足以应付大多数的场景。
只可惜如今的江湖已经不是从前的江湖现在互联网的特点是移动和高并发不能保证稳定的连接质量所以在TCP层面上HTTP协议有时候就会表现的不够好。
而“请求-应答”模式则加剧了HTTP的性能问题这就是著名的“队头阻塞”Head-of-line blocking当顺序发送的请求序列中的一个请求因为某种原因被阻塞时在后面排队的所有请求也一并被阻塞会导致客户端迟迟收不到数据。
为了解决这个问题就诞生出了一个专门的研究课题“Web性能优化”HTTP官方标准里就有“缓存”一章RFC7234非官方的“花招”就更多了例如切图、数据内嵌与合并域名分片、JavaScript“黑科技”等等。
不过现在已经有了终极解决方案HTTP/2和HTTP/3后面我也会展开来讲。
## 小结
1. HTTP最大的优点是简单、灵活和易于扩展
1. HTTP拥有成熟的软硬件环境应用的非常广泛是互联网的基础设施
1. HTTP是无状态的可以轻松实现集群化扩展性能但有时也需要用Cookie技术来实现“有状态”
1. HTTP是明文传输数据完全肉眼可见能够方便地研究分析但也容易被窃听
1. HTTP是不安全的无法验证通信双方的身份也不能判断报文是否被窜改
1. HTTP的性能不算差但不完全适应现在的互联网还有很大的提升空间。
虽然HTTP免不了这样那样的缺点但你也不要怕别忘了它有一个最重要的“灵活可扩展”的优点所有的缺点都可以在这个基础上想办法解决接下来的“进阶篇”和“安全篇”就会讲到。
## 课下作业
1. 你最喜欢的HTTP优点是哪个最不喜欢的缺点又是哪个为什么
1. 你能够再进一步扩展或补充论述今天提到这些优点或缺点吗?
1. 你能试着针对这些缺点提出一些自己的解决方案吗?
欢迎你把自己的答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,欢迎你把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/75/ad/7573b0a37ed275bbf6c94eb20875b1ad.png" alt="unpreview">

View File

@@ -0,0 +1,138 @@
<audio id="audio" title="23 | HTTPS是什么SSL/TLS又是什么" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/60/27/606366f47954111645ec28ffab6f4127.mp3"></audio>
从今天开始我们开始进入全新的“安全篇”聊聊与安全相关的HTTPS、SSL、TLS。
在[第14讲](https://time.geekbang.org/column/article/103746)中我曾经谈到过HTTP的一些缺点其中的“无状态”在加入Cookie后得到了解决而另两个缺点——“明文”和“不安全”仅凭HTTP自身是无力解决的需要引入新的HTTPS协议。
## 为什么要有HTTPS
简单的回答是“**因为HTTP不安全**”。
由于HTTP天生“明文”的特点整个传输过程完全透明任何人都能够在链路中截获、修改或者伪造请求/响应报文,数据不具有可信性。
比如前几讲中说过的“代理服务”。它作为HTTP通信的中间人在数据上下行的时候可以添加或删除部分头字段也可以使用黑白名单过滤body里的关键字甚至直接发送虚假的请求、响应而浏览器和源服务器都没有办法判断报文的真伪。
这对于网络购物、网上银行、证券交易等需要高度信任的应用场景来说是非常致命的。如果没有基本的安全保护,使用互联网进行各种电子商务、电子政务就根本无从谈起。
对于安全性要求不那么高的新闻、视频、搜索等网站来说,由于互联网上的恶意用户、恶意代理越来越多,也很容易遭到“流量劫持”的攻击,在页面里强行嵌入广告,或者分流用户,导致各种利益损失。
对于你我这样的普通网民来说HTTP不安全的隐患就更大了上网的记录会被轻易截获网站是否真实也无法验证黑客可以伪装成银行网站盗取真实姓名、密码、银行卡等敏感信息威胁人身安全和财产安全。
总的来说,今天的互联网已经不再是早期的“田园牧歌”时代,而是进入了“黑暗森林”状态。上网的时候必须步步为营、处处小心,否则就会被不知道埋伏在哪里的黑客所“猎杀”。
## 什么是安全?
既然HTTP“不安全”那什么样的通信过程才是安全的呢
通常认为,如果通信过程具备了四个特性,就可以认为是“安全”的,这四个特性是:机密性、完整性,身份认证和不可否认。
**机密性**Secrecy/Confidentiality是指对数据的“保密”只能由可信的人访问对其他人是不可见的“秘密”简单来说就是不能让不相关的人看到不该看的东西。
比如小明和小红私下聊天但“隔墙有耳”被小强在旁边的房间里全偷听到了这就是没有机密性。我们之前一直用的Wireshark 实际上也是利用了HTTP的这个特点捕获了传输过程中的所有数据。
**完整性**Integrity也叫一致性是指数据在传输过程中没有被篡改不多也不少“完完整整”地保持着原状。
机密性虽然可以让数据成为“秘密”,但不能防止黑客对数据的修改,黑客可以替换数据,调整数据的顺序,或者增加、删除部分数据,破坏通信过程。
比如,小明给小红写了张纸条:“明天公园见”。小强把“公园”划掉,模仿小明的笔迹把这句话改成了“明天广场见”。小红收到后无法验证完整性,信以为真,第二天的约会就告吹了。
**身份认证**Authentication是指确认对方的真实身份也就是“证明你真的是你”保证消息只能发送给可信的人。
如果通信时另一方是假冒的网站,那么数据再保密也没有用,黑客完全可以使用冒充的身份“套”出各种信息,加密和没加密一样。
比如,小明给小红写了封情书:“我喜欢你”,但不留心发给了小强。小强将错就错,假冒小红回复了一个“白日做梦”,小明不知道这其实是小强的话,误以为是小红的,后果可想而知。
第四个特性是**不可否认**Non-repudiation/Undeniable也叫不可抵赖意思是不能否认已经发生过的行为不能“说话不算数”“耍赖皮”。
使用前三个特性,可以解决安全通信的大部分问题,但如果缺了不可否认,那通信的事务真实性就得不到保证,有可能出现“老赖”。
比如,小明借了小红一千元,没写借条,第二天矢口否认,小红也确实拿不出借钱的证据,只能认倒霉。另一种情况是小明借钱后还了小红,但没写收条,小红于是不承认小明还钱的事,说根本没还,要小明再掏出一千元。
所以,只有同时具备了机密性、完整性、身份认证、不可否认这四个特性,通信双方的利益才能有保障,才能算得上是真正的安全。
## 什么是HTTPS
说到这里终于轮到今天的主角HTTPS出场了它为HTTP增加了刚才所说的四大安全特性。
HTTPS其实是一个“非常简单”的协议RFC文档很小只有短短的7页里面规定了**新的协议名“https”默认端口号443**,至于其他的什么请求-应答模式、报文结构、请求方法、URI、头字段、连接管理等等都完全沿用HTTP没有任何新的东西。
也就是说除了协议名“http”和端口号80这两点不同HTTPS协议在语法、语义上和HTTP完全一样优缺点也“照单全收”当然要除去“明文”和“不安全”
不信你可以用URI“[https://www.chrono.com](https://www.chrono.com)”访问之前08至21讲的所有示例看看它的响应报文是否与HTTP一样。
```
https://www.chrono.com
https://www.chrono.com/11-1
https://www.chrono.com/15-1?name=a.json
https://www.chrono.com/16-1
```
<img src="https://static001.geekbang.org/resource/image/40/b0/40fbb989a9fd2217320ab287e80e1fb0.png" alt="">
你肯定已经注意到了在用HTTPS访问实验环境时Chrome会有不安全提示必须点击“高级-继续前往”才能顺利显示页面。而且如果用Wireshark抓包也会发现与HTTP不一样不再是简单可见的明文多了“Client Hello”“Server Hello”等新的数据包。
这就是HTTPS与HTTP最大的区别它能够鉴别危险的网站并且尽最大可能保证你的上网安全防御黑客对信息的窃听、篡改或者“钓鱼”、伪造。
你可能要问了既然没有新东西HTTPS凭什么就能做到机密性、完整性这些安全特性呢
秘密就在于HTTPS名字里的“S”它把HTTP下层的传输协议由TCP/IP换成了SSL/TLS由“**HTTP over TCP/IP**”变成了“**HTTP over SSL/TLS**”让HTTP运行在了安全的SSL/TLS协议上可参考第4讲和第5讲收发报文不再使用Socket API而是调用专门的安全接口。
<img src="https://static001.geekbang.org/resource/image/50/a3/50d57e18813e18270747806d5d73f0a3.png" alt="">
所以说HTTPS本身并没有什么“惊世骇俗”的本事全是靠着后面的SSL/TLS“撑腰”。只要学会了SSL/TLSHTTPS自然就“手到擒来”。
## SSL/TLS
现在我们就来看看SSL/TLS它到底是个什么来历。
SSL即安全套接层Secure Sockets Layer在OSI模型中处于第5层会话层由网景公司于1994年发明有v2和v3两个版本而v1因为有严重的缺陷从未公开过。
SSL发展到v3时已经证明了它自身是一个非常好的安全通信协议于是互联网工程组IETF在1999年把它改名为TLS传输层安全Transport Layer Security正式标准化版本号从1.0重新算起所以TLS1.0实际上就是SSLv3.1。
到今天TLS已经发展出了三个版本分别是2006年的1.1、2008年的1.2和去年2018的1.3,每个新版本都紧跟密码学的发展和互联网的现状,持续强化安全和性能,已经成为了信息安全领域中的权威标准。
目前应用的最广泛的TLS是1.2而之前的协议TLS1.1/1.0、SSLv3/v2都已经被认为是不安全的各大浏览器即将在2020年左右停止支持所以接下来的讲解都针对的是TLS1.2。
TLS由记录协议、握手协议、警告协议、变更密码规范协议、扩展协议等几个子协议组成综合使用了对称加密、非对称加密、身份认证等许多密码学前沿技术。
浏览器和服务器在使用TLS建立连接时需要选择一组恰当的加密算法来实现安全通信这些算法的组合被称为“密码套件”cipher suite也叫加密套件
你可以访问实验环境的URI“/23-1”对TLS和密码套件有个感性的认识。
<img src="https://static001.geekbang.org/resource/image/5e/24/5ead57e03f127ea8f244d715186adb24.png" alt="">
你可以看到实验环境使用的TLS是1.2客户端和服务器都支持非常多的密码套件而最后协商选定的是“ECDHE-RSA-AES256-GCM-SHA384”。
这么长的名字看着有点晕吧不用怕其实TLS的密码套件命名非常规范格式很固定。基本的形式是“密钥交换算法+签名算法+对称加密算法+摘要算法”,比如刚才的密码套件的意思就是:
“握手时使用ECDHE算法进行密钥交换用RSA签名和身份认证握手后的通信使用AES对称算法密钥长度256位分组模式是GCM摘要算法SHA384用于消息认证和产生随机数。”
## OpenSSL
说到TLS就不能不谈到OpenSSL它是一个著名的开源密码学程序库和工具包几乎支持所有公开的加密算法和协议已经成为了事实上的标准许多应用软件都会使用它作为底层库来实现TLS功能包括常用的Web服务器Apache、Nginx等。
OpenSSL是从另一个开源库SSLeay发展出来的曾经考虑命名为“OpenTLS”但当时1998年TLS还未正式确立而SSL早已广为人知所以最终使用了“OpenSSL”的名字。
OpenSSL目前有三个主要的分支1.0.2和1.1.0都将在今年2019年底不再维护最新的长期支持版本是1.1.1我们的实验环境使用的OpenSSL是“1.1.0j”。
由于OpenSSL是开源的所以它还有一些代码分支比如Google的BoringSSL、OpenBSD的LibreSSL这些分支在OpenSSL的基础上删除了一些老旧代码也增加了一些新特性虽然背后有“大金主”但离取代OpenSSL还差得很远。
## 小结
1. 因为HTTP是明文传输所以不安全容易被黑客窃听或篡改
1. 通信安全必须同时具备机密性、完整性、身份认证和不可否认这四个特性;
1. HTTPS的语法、语义仍然是HTTP但把下层的协议由TCP/IP换成了SSL/TLS
1. SSL/TLS是信息安全领域中的权威标准采用多种先进的加密技术保证通信安全
1. OpenSSL是著名的开源密码学工具包是SSL/TLS的具体实现。
## 课下作业
1. 你能说出HTTPS与HTTP有哪些区别吗
1. 你知道有哪些方法能够实现机密性、完整性等安全特性呢?
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/05/4a/052e28eaa90a37f21ae4052135750a4a.png" alt="unpreview">

View File

@@ -0,0 +1,146 @@
<audio id="audio" title="24 | 固若金汤的根本(上):对称加密与非对称加密" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/67/ca/67d40f007e6e960a7835e04c986212ca.mp3"></audio>
在上一讲中我们初步学习了HTTPS知道HTTPS的安全性是由TLS来保证的。
你一定很好奇它是怎么为HTTP增加了机密性、完整性身份认证和不可否认等特性的呢
先说说机密性。它是信息安全的基础缺乏机密性TLS就会成为“无水之源”“无根之木”。
实现机密性最常用的手段是“**加密**”encrypt就是把消息用某种方式转换成谁也看不懂的乱码只有掌握特殊“钥匙”的人才能再转换出原始文本。
这里的“钥匙”就叫做“**密钥**”key加密前的消息叫“**明文**”plain text/clear text加密后的乱码叫“**密文**”cipher text使用密钥还原明文的过程叫“**解密**”decrypt是加密的反操作加密解密的操作过程就是“**加密算法**”。
所有的加密算法都是公开的,任何人都可以去分析研究,而算法使用的“密钥”则必须保密。那么,这个关键的“密钥”又是什么呢?
由于HTTPS、TLS都运行在计算机上所以“密钥”就是一长串的数字但约定俗成的度量单位是“位”bit而不是“字节”byte。比如说密钥长度是128就是16字节的二进制串密钥长度1024就是128字节的二进制串。
按照密钥的使用方式,加密可以分为两大类:**对称加密和非对称加密**。
## 对称加密
“对称加密”很好理解,就是指加密和解密时使用的密钥都是同一个,是“对称”的。只要保证了密钥的安全,那整个通信过程就可以说具有了机密性。
举个例子,你想要登录某网站,只要事先和它约定好使用一个对称密钥,通信过程中传输的全是用密钥加密后的密文,只有你和网站才能解密。黑客即使能够窃听,看到的也只是乱码,因为没有密钥无法解出明文,所以就实现了机密性。
<img src="https://static001.geekbang.org/resource/image/8f/49/8feab67c25a534f8c72077680927ab49.png" alt="">
TLS里有非常多的对称加密算法可供选择比如RC4、DES、3DES、AES、ChaCha20等但前三种算法都被认为是不安全的通常都禁止使用目前常用的只有AES和ChaCha20。
AES的意思是“高级加密标准”Advanced Encryption Standard密钥长度可以是128、192或256。它是DES算法的替代者安全强度很高性能也很好而且有的硬件还会做特殊优化所以非常流行是应用最广泛的对称加密算法。
ChaCha20是Google设计的另一种加密算法密钥长度固定为256位纯软件运行性能要超过AES曾经在移动客户端上比较流行但ARMv8之后也加入了AES硬件优化所以现在不再具有明显的优势但仍然算得上是一个不错的算法。
## 加密分组模式
对称算法还有一个“**分组模式**”的概念,它可以让算法用固定长度的密钥加密任意长度的明文,把小秘密(即密钥)转化为大秘密(即密文)。
最早有ECB、CBC、CFB、OFB等几种分组模式但都陆续被发现有安全漏洞所以现在基本都不怎么用了。最新的分组模式被称为AEADAuthenticated Encryption with Associated Data在加密的同时增加了认证的功能常用的是GCM、CCM和Poly1305。
把上面这些组合起来就可以得到TLS密码套件中定义的对称加密算法。
比如AES128-GCM意思是密钥长度为128位的AES算法使用的分组模式是GCMChaCha20-Poly1305的意思是ChaCha20算法使用的分组模式是Poly1305。
你可以用实验环境的URI“/24-1”来测试OpenSSL里的AES128-CBC在URI后用参数“key”“plain”输入密钥和明文服务器会在响应报文里输出加密解密的结果。
```
https://www.chrono.com/24-1?key=123456
algo = aes_128_cbc
plain = hello openssl
enc = 93a024a94083bc39fb2c2b9f5ce27c09
dec = hello openssl
```
## 非对称加密
对称加密看上去好像完美地实现了机密性,但其中有一个很大的问题:如何把密钥安全地传递给对方,术语叫“**密钥交换**”。
因为在对称加密算法中只要持有密钥就可以解密。如果你和网站约定的密钥在传递途中被黑客窃取,那他就可以在之后随意解密收发的数据,通信过程也就没有机密性可言了。
这个问题该怎么解决呢?
你或许会说:“把密钥再加密一下发过去就好了”,但传输“加密密钥的密钥”又成了新问题。这就像是“鸡生蛋、蛋生鸡”,可以无限递归下去。只用对称加密算法,是绝对无法解决密钥交换的问题的。
所以,就出现了非对称加密(也叫公钥加密算法)。
它有两个密钥,一个叫“**公钥**”public key一个叫“**私钥**”private key。两个密钥是不同的“不对称”公钥可以公开给任何人使用而私钥必须严格保密。
公钥和私钥有个特别的“**单向**”性,虽然都可以用来加密解密,但公钥加密后只能用私钥解密,反过来,私钥加密后也只能用公钥解密。
非对称加密可以解决“密钥交换”的问题。网站秘密保管私钥,在网上任意分发公钥,你想要登录网站只要用公钥加密就行了,密文只能由私钥持有者才能解密。而黑客因为没有私钥,所以就无法破解密文。
<img src="https://static001.geekbang.org/resource/image/89/17/89344c2e493600b486d5349a84318417.png" alt="">
非对称加密算法的设计要比对称算法难得多在TLS里只有很少的几种比如DH、DSA、RSA、ECC等。
RSA可能是其中最著名的一个几乎可以说是非对称加密的代名词它的安全性基于“**整数分解**”的数学难题,使用两个超大素数的乘积作为生成密钥的材料,想要从公钥推算出私钥是非常困难的。
10年前RSA密钥的推荐长度是1024但随着计算机运算能力的提高现在1024已经不安全普遍认为至少要2048位。
ECCElliptic Curve Cryptography是非对称加密里的“后起之秀”它基于“**椭圆曲线离散对数**”的数学难题使用特定的曲线方程和基点生成公钥和私钥子算法ECDHE用于密钥交换ECDSA用于数字签名。
目前比较常用的两个曲线是P-256secp256r1在OpenSSL称为prime256v1和x25519。P-256是NIST美国国家标准技术研究所和NSA美国国家安全局推荐使用的曲线而x25519被认为是最安全、最快速的曲线。
ECC名字里的“椭圆”经常会引起误解其实它的曲线并不是椭圆形只是因为方程很类似计算椭圆周长的公式实际的形状更像抛物线比如下面的图就展示了两个简单的椭圆曲线。
<img src="https://static001.geekbang.org/resource/image/b4/ba/b452ceb3cbfc5c644a3053f2054b1aba.jpg" alt="">两个简单的椭圆曲线y^2=x^3+7y^2=x^3-x
比起RSAECC在安全强度和性能上都有明显的优势。160位的ECC相当于1024位的RSA而224位的ECC则相当于2048位的RSA。因为密钥短所以相应的计算量、消耗的内存和带宽也就少加密解密的性能就上去了对于现在的移动互联网非常有吸引力。
实验环境的URI“/24-2”演示了RSA1024你在课后可以动手试一下。
## 混合加密
看到这里,你是不是认为可以抛弃对称加密,只用非对称加密来实现机密性呢?
很遗憾虽然非对称加密没有“密钥交换”的问题但因为它们都是基于复杂的数学难题运算速度很慢即使是ECC也要比AES差上好几个数量级。如果仅用非对称加密虽然保证了安全但通信速度有如乌龟、蜗牛实用性就变成了零。
实验环境的URI“/24-3”对比了AES和RSA这两种算法的性能下面列出了一次测试的结果
```
aes_128_cbc enc/dec 1000 times : 0.97ms, 13.11MB/s
rsa_1024 enc/dec 1000 times : 138.59ms, 93.80KB/s
rsa_1024/aes ratio = 143.17
rsa_2048 enc/dec 1000 times : 840.35ms, 15.47KB/s
rsa_2048/aes ratio = 868.13
```
可以看到RSA的运算速度是非常慢的2048位的加解密大约是15KB/S微秒或毫秒级而AES128则是13MB/S纳秒级差了几百倍。
那么,是不是能够把对称加密和非对称加密结合起来呢,两者互相取长补短,即能高效地加密解密,又能安全地密钥交换。
这就是现在TLS里使用的**混合加密**方式,其实说穿了也很简单:
在通信刚开始的时候使用非对称算法比如RSA、ECDHE首先解决密钥交换的问题。
然后用随机数产生对称算法使用的“**会话密钥**”session key再用公钥加密。因为会话密钥很短通常只有16字节或32字节所以慢一点也无所谓。
对方拿到密文后用私钥解密,取出会话密钥。这样,双方就实现了对称密钥的安全交换,后续就不再使用非对称加密,全都使用对称加密。
<img src="https://static001.geekbang.org/resource/image/e4/85/e41f87110aeea3e548d58cc35a478e85.png" alt="">
这样混合加密就解决了对称加密算法的密钥交换问题,而且安全和性能兼顾,完美地实现了机密性。
不过这只是“万里长征的第一步”,后面还有完整性、身份认证、不可否认等特性没有实现,所以现在的通信还不是绝对安全,我们下次再说。
## 小结
1. 加密算法的核心思想是“把一个小秘密(密钥)转化为一个大秘密(密文消息)”,守住了小秘密,也就守住了大秘密;
1. 对称加密只使用一个密钥运算速度快密钥必须保密无法做到安全的密钥交换常用的有AES和ChaCha20
1. 非对称加密使用两个密钥公钥和私钥公钥可以任意分发而私钥保密解决了密钥交换问题但速度慢常用的有RSA和ECC
1. 把对称加密和非对称加密结合起来就得到了“又好又快”的混合加密也就是TLS里使用的加密方式。
## 课下作业
1. 加密算法中“密钥”的名字很形象,你能试着用现实中的锁和钥匙来比喻一下吗?
1. 在混合加密中用到了公钥加密,因为只能由私钥解密。那么反过来,私钥加密后任何人都可以用公钥解密,这有什么用呢?
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/b4/d7/b437f2b898a2f3424bd8812d9a0dcbd7.png" alt="unpreview">

View File

@@ -0,0 +1,152 @@
<audio id="audio" title="25 | 固若金汤的根本(下):数字签名与证书" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ac/7b/acc313467c268709079037625577c67b.mp3"></audio>
上一讲中我们学习了对称加密和非对称加密,以及两者结合起来的混合加密,实现了机密性。
但仅有机密性,离安全还差的很远。
黑客虽然拿不到会话密钥,无法破解密文,但可以通过窃听收集到足够多的密文,再尝试着修改、重组后发给网站。因为没有完整性保证,服务器只能“照单全收”,然后他就可以通过服务器的响应获取进一步的线索,最终就会破解出明文。
另外,黑客也可以伪造身份发布公钥。如果你拿到了假的公钥,混合加密就完全失效了。你以为自己是在和“某宝”通信,实际上网线的另一端却是黑客,银行卡号、密码等敏感信息就在“安全”的通信过程中被窃取了。
所以,在机密性的基础上还必须加上完整性、身份认证等特性,才能实现真正的安全。
## 摘要算法
实现完整性的手段主要是**摘要算法**Digest Algorithm也就是常说的散列函数、哈希函数Hash Function
你可以把摘要算法近似地理解成一种特殊的压缩算法,它能够把任意长度的数据“压缩”成固定长度、而且独一无二的“摘要”字符串,就好像是给这段数据生成了一个数字“指纹”。
换一个角度,也可以把摘要算法理解成特殊的“单向”加密算法,它只有算法,没有密钥,加密后的数据无法解密,不能从摘要逆推出原文。
<img src="https://static001.geekbang.org/resource/image/28/d8/2865d2c77466efb7a480833bcb27f9d8.png" alt="">
摘要算法实际上是把数据从一个“大空间”映射到了“小空间”所以就存在“冲突”collision也叫碰撞的可能性就如同现实中的指纹一样可能会有两份不同的原文对应相同的摘要。好的摘要算法必须能够“抵抗冲突”让这种可能性尽量地小。
因为摘要算法对输入具有“单向性”和“雪崩效应”输入的微小不同会导致输出的剧烈变化所以也被TLS用来生成伪随机数PRFpseudo random function
你一定在日常工作中听过、或者用过MD5Message-Digest 5、SHA-1Secure Hash Algorithm 1它们就是最常用的两个摘要算法能够生成16字节和20字节长度的数字摘要。但这两个算法的安全强度比较低不够安全在TLS里已经被禁止使用了。
目前TLS推荐使用的是SHA-1的后继者SHA-2。
SHA-2实际上是一系列摘要算法的统称总共有6种常用的有SHA224、SHA256、SHA384分别能够生成28字节、32字节、48字节的摘要。
你可以用实验环境的URI“/25-1”来测试一下TLS里的各种摘要算法包括MD5、SHA-1和SHA-2。
```
https://www.chrono.com/25-1?algo=md5
https://www.chrono.com/25-1?algo=sha1
https://www.chrono.com/25-1?algo=sha256
```
## 完整性
摘要算法保证了“数字摘要”和原文是完全等价的。所以,我们只要在原文后附上它的摘要,就能够保证数据的完整性。
比如你发了条消息“转账1000元”然后再加上一个SHA-2的摘要。网站收到后也计算一下消息的摘要把这两份“指纹”做个对比如果一致就说明消息是完整可信的没有被修改。
如果黑客在中间哪怕改动了一个标点符号,摘要也会完全不同,网站计算比对就会发现消息被窜改,是不可信的。
不过摘要算法不具有机密性,如果明文传输,那么黑客可以修改消息后把摘要也一起改了,网站还是鉴别不出完整性。
所以,真正的完整性必须要建立在机密性之上,在混合加密系统里用会话密钥加密消息和摘要,这样黑客无法得知明文,也就没有办法动手脚了。
这有个术语叫哈希消息认证码HMAC
<img src="https://static001.geekbang.org/resource/image/c2/96/c2e10e9afa1393281b5633b1648f2696.png" alt="">
## 数字签名
加密算法结合摘要算法我们的通信过程可以说是比较安全了。但这里还有漏洞就是通信的两个端点endpoint
就像一开始所说的,黑客可以伪装成网站来窃取信息。而反过来,他也可以伪装成你,向网站发送支付、转账等消息,网站没有办法确认你的身份,钱可能就这么被偷走了。
现实生活中,解决身份认证的手段是签名和印章,只要在纸上写下签名或者盖个章,就能够证明这份文件确实是由本人而不是其他人发出的。
你回想一下之前的课程在TLS里有什么东西和现实中的签名、印章很像只能由本人持有而其他任何人都不会有呢只要用这个东西就能够在数字世界里证明你的身份。
没错,这个东西就是非对称加密里的“**私钥**”,使用私钥再加上摘要算法,就能够实现“**数字签名**”,同时实现“身份认证”和“不可否认”。
数字签名的原理其实很简单,就是把公钥私钥的用法反过来,之前是公钥加密、私钥解密,现在是私钥加密、公钥解密。
但又因为非对称加密效率太低,所以私钥只加密原文的摘要,这样运算量就小的多,而且得到的数字签名也很小,方便保管和传输。
签名和公钥一样完全公开,任何人都可以获取。但这个签名只有用私钥对应的公钥才能解开,拿到摘要后,再比对原文验证完整性,就可以像签署文件一样证明消息确实是你发的。
<img src="https://static001.geekbang.org/resource/image/84/d2/84a79826588ca35bf6ddcade027597d2.png" alt="">
刚才的这两个行为也有专用术语,叫做“**签名**”和“**验签**”。
只要你和网站互相交换公钥,就可以用“签名”和“验签”来确认消息的真实性,因为私钥保密,黑客不能伪造签名,就能够保证通信双方的身份。
比如,你用自己的私钥签名一个消息“我是小明”。网站收到后用你的公钥验签,确认身份没问题,于是也用它的私钥签名消息“我是某宝”。你收到后再用它的公钥验一下,也没问题,这样你和网站就都知道对方不是假冒的,后面就可以用混合加密进行安全通信了。
实验环境的URI“/25-2”演示了TLS里的数字签名它使用的是RSA1024。
## 数字证书和CA
到现在,综合使用对称加密、非对称加密和摘要算法,我们已经实现了安全的四大特性,是不是已经完美了呢?
不是的,这里还有一个“**公钥的信任**”问题。因为谁都可以发布公钥,我们还缺少防止黑客伪造公钥的手段,也就是说,怎么来判断这个公钥就是你或者某宝的公钥呢?
真是“按下葫芦又起了瓢”,安全还真是个麻烦事啊,“一环套一环”的。
我们可以用类似密钥交换的方法来解决公钥认证问题,用别的私钥来给公钥签名,显然,这又会陷入“无穷递归”。
但这次实在是“没招”了,要终结这个“死循环”,就必须引入“外力”,找一个公认的可信第三方,让它作为“信任的起点,递归的终点”,构建起公钥的信任链。
这个“第三方”就是我们常说的**CA**Certificate Authority证书认证机构。它就像网络世界里的公安局、教育部、公证中心具有极高的可信度由它来给各个公钥签名用自身的信誉来保证公钥无法伪造是可信的。
CA对公钥的签名认证也是有格式的不是简单地把公钥绑定在持有者身份上就完事了还要包含序列号、用途、颁发者、有效时间等等把这些打成一个包再签名完整地证明公钥关联的各种信息形成“**数字证书**”Certificate
知名的CA全世界就那么几家比如DigiCert、VeriSign、Entrust、Lets Encrypt等它们签发的证书分DV、OV、EV三种区别在于可信程度。
DV是最低的只是域名级别的可信背后是谁不知道。EV是最高的经过了法律和审计的严格核查可以证明网站拥有者的身份在浏览器地址栏会显示出公司的名字例如Apple、GitHub的网站
不过CA怎么证明自己呢
这还是信任链的问题。小一点的CA可以让大CA签名认证但链条的最后也就是**Root CA**,就只能自己证明自己了,这个就叫“**自签名证书**”Self-Signed Certificate或者“**根证书**”Root Certificate。你必须相信否则整个证书信任链就走不下去了。
<img src="https://static001.geekbang.org/resource/image/8f/9c/8f0813e9555ba1a40bd2170734aced9c.png" alt="">
有了这个证书体系操作系统和浏览器都内置了各大CA的根证书上网的时候只要服务器发过来它的证书就可以验证证书里的签名顺着证书链Certificate Chain一层层地验证直到找到根证书就能够确定证书是可信的从而里面的公钥也是可信的。
我们的实验环境里使用的证书是“野路子”的自签名证书在Linux上用OpenSSL命令行签发肯定是不会被浏览器所信任的所以用Chrome访问时就会显示成红色标记为不安全。但你只要把它安装进系统的根证书存储区里让它作为信任链的根就不会再有危险警告。
<img src="https://static001.geekbang.org/resource/image/a5/8f/a55051ca7ae941ae04791cdddde6658f.png" alt="">
## 证书体系的弱点
证书体系PKIPublic Key Infrastructure虽然是目前整个网络世界的安全基础设施但绝对的安全是不存在的它也有弱点还是关键的“**信任**”二字。
如果CA失误或者被欺骗签发了错误的证书虽然证书是真的可它代表的网站却是假的。
还有一种更危险的情况CA被黑客攻陷或者CA有恶意因为它即根证书是信任的源头整个信任链里的所有证书也就都不可信了。
这两种事情并不是“耸人听闻”,都曾经实际出现过。所以,需要再给证书体系打上一些补丁。
针对第一种开发出了CRL证书吊销列表Certificate revocation list和OCSP在线证书状态协议Online Certificate Status Protocol及时废止有问题的证书。
对于第二种因为涉及的证书太多就只能操作系统或者浏览器从根上“下狠手”了撤销对CA的信任列入“黑名单”这样它颁发的所有证书就都会被认为是不安全的。
## 小结
今天我们学习了数字签名和证书、CA是不是有种“盗梦空间”一层套一层的感觉你可以在课后再去各大网站结合它们“小锁头”里的信息来加深理解。
今天的内容可以简单概括为四点:
1. 摘要算法用来实现完整性能够为数据生成独一无二的“指纹”常用的算法是SHA-2
1. 数字签名是私钥对摘要的加密,可以由公钥解密后验证,实现身份认证和不可否认;
1. 公钥的分发需要使用数字证书必须由CA的信任链来验证否则就是不可信的
1. 作为信任链的源头CA有时也会不可信解决办法有CRL、OCSP还有终止信任。
## 课下作业
1. 为什么公钥能够建立信任链,用对称加密算法里的对称密钥行不行呢?
1. 假设有一个三级的证书体系Root CA=&gt;一级CA=&gt;二级CA你能详细解释一下证书信任链的验证过程吗
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/37/57/37c59439c36e75f610fe84c22009cc57.png" alt="">

View File

@@ -0,0 +1,192 @@
<audio id="audio" title="26 | 信任始于握手TLS1.2连接过程解析" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/29/e8/296bb845794dbb049778d7725589bde8.mp3"></audio>
经过前几讲的介绍,你应该已经熟悉了对称加密与非对称加密、数字签名与证书等密码学知识。
有了这些知识“打底”现在我们就可以正式开始研究HTTPS和TLS协议了。
## HTTPS建立连接
当你在浏览器地址栏里键入“**https**”开头的URI再按下回车会发生什么呢
回忆一下[第8讲](https://time.geekbang.org/column/article/100502)的内容你应该知道浏览器首先要从URI里提取出协议名和域名。因为协议名是“https”所以浏览器就知道了端口号是默认的443它再用DNS解析域名得到目标的IP地址然后就可以使用三次握手与网站建立TCP连接了。
在HTTP协议里建立连接后浏览器会立即发送请求报文。但现在是HTTPS协议它需要再用另外一个“握手”过程在TCP上建立安全连接之后才是收发HTTP报文。
这个“握手”过程与TCP有些类似是HTTPS和TLS协议里最重要、最核心的部分懂了它你就可以自豪地说自己“掌握了HTTPS”。
## TLS协议的组成
在讲TLS握手之前我先简单介绍一下TLS协议的组成。
TLS包含几个子协议你也可以理解为它是由几个不同职责的模块组成比较常用的有记录协议、警报协议、握手协议、变更密码规范协议等。
**记录协议**Record Protocol规定了TLS收发数据的基本单位记录record。它有点像是TCP里的segment所有的其他子协议都需要通过记录协议发出。但多个记录数据可以在一个TCP包里一次性发出也并不需要像TCP那样返回ACK。
**警报协议**Alert Protocol的职责是向对方发出警报信息有点像是HTTP协议里的状态码。比如protocol_version就是不支持旧版本bad_certificate就是证书有问题收到警报后另一方可以选择继续也可以立即终止连接。
**握手协议**Handshake Protocol是TLS里最复杂的子协议要比TCP的SYN/ACK复杂的多浏览器和服务器会在握手过程中协商TLS版本号、随机数、密码套件等信息然后交换证书和密钥参数最终双方协商得到会话密钥用于后续的混合加密系统。
最后一个是**变更密码规范协议**Change Cipher Spec Protocol它非常简单就是一个“通知”告诉对方后续的数据都将使用加密保护。那么反过来在它之前数据都是明文的。
下面的这张图简要地描述了TLS的握手过程其中每一个“框”都是一个记录多个记录组合成一个TCP包发送。所以最多经过两次消息往返4个消息就可以完成握手然后就可以在安全的通信环境里发送HTTP报文实现HTTPS协议。
<img src="https://static001.geekbang.org/resource/image/69/6c/69493b53f1b1d540acf886ebf021a26c.png" alt="">
## 抓包的准备工作
这次我们在实验环境里测试TLS握手的URI是“/26-1”看了上面的图你就可以知道TLS握手的前几个消息都是明文的能够在Wireshark里直接看。但只要出现了“Change Cipher Spec”后面的数据就都是密文了看到的也就会是乱码不知道究竟是什么东西。
为了更好地分析TLS握手过程你可以再对系统和Wireshark做一下设置让浏览器导出握手过程中的秘密信息这样Wireshark就可以把密文解密还原出明文。
首先你需要在Windows的设置里新增一个系统变量“**SSLKEYLOGFILE**”设置浏览器日志文件的路径比如“D:\http_study\www\temp\sslkey.log”具体的设置过程就不详细说了可以在设置里搜索“系统变量”
<img src="https://static001.geekbang.org/resource/image/70/42/70b36338611d5a249a7d2fc448f06d42.png" alt="">
然后在Wireshark里设置“Protocols-TLS”较早版本的Wireshark里是“SSL”在“(Pre)-Master-Secret log filename”里填上刚才的日志文件。
<img src="https://static001.geekbang.org/resource/image/02/e7/0274e31e74e92b61892ec11cc3cd58e7.png" alt="">
设置好之后,过滤器选择“**tcp port 443**”就可以抓到实验环境里的所有HTTPS数据了。
如果你觉得麻烦也没关系GitHub上有抓好的包和相应的日志用Wireshark直接打开就行。
## ECDHE握手过程
刚才你看到的是握手过程的简要图我又画了一个详细图对应Wireshark的抓包下面我就用这个图来仔细剖析TLS的握手过程。
<img src="https://static001.geekbang.org/resource/image/9c/1e/9caba6d4b527052bbe7168ed4013011e.png" alt="">
在TCP建立连接之后浏览器会首先发一个“**Client Hello**”消息,也就是跟服务器“打招呼”。里面有客户端的版本号、支持的密码套件,还有一个**随机数Client Random**,用于后续生成会话密钥。
```
Handshake Protocol: Client Hello
Version: TLS 1.2 (0x0303)
Random: 1cbf803321fd2623408dfe…
Cipher Suites (17 suites)
Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (0xc02f)
Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (0xc030)
```
这个的意思就是:“我这边有这些这些信息,你看看哪些是能用的,关键的随机数可得留着。”
作为“礼尚往来”服务器收到“Client Hello”后会返回一个“Server Hello”消息。把版本号对一下也给出一个**随机数Server Random**然后从客户端的列表里选一个作为本次通信使用的密码套件在这里它选择了“TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384”。
```
Handshake Protocol: Server Hello
Version: TLS 1.2 (0x0303)
Random: 0e6320f21bae50842e96…
Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (0xc030)
```
这个的意思就是“版本号对上了可以加密你的密码套件挺多我选一个最合适的吧用椭圆曲线加RSA、AES、SHA384。我也给你一个随机数你也得留着。”
然后服务器为了证明自己的身份就把证书也发给了客户端Server Certificate
接下来是一个关键的操作因为服务器选择了ECDHE算法所以它会在证书后发送“**Server Key Exchange**”消息,里面是**椭圆曲线的公钥Server Params**,用来实现密钥交换算法,再加上自己的私钥签名认证。
```
Handshake Protocol: Server Key Exchange
EC Diffie-Hellman Server Params
Curve Type: named_curve (0x03)
Named Curve: x25519 (0x001d)
Pubkey: 3b39deaf00217894e...
Signature Algorithm: rsa_pkcs1_sha512 (0x0601)
Signature: 37141adac38ea4...
```
这相当于说:“刚才我选的密码套件有点复杂,所以再给你个算法的参数,和刚才的随机数一样有用,别丢了。为了防止别人冒充,我又盖了个章。”
之后是“**Server Hello Done**”消息,服务器说:“我的信息就是这些,打招呼完毕。”
这样第一个消息往返就结束了两个TCP包结果是客户端和服务器通过明文共享了三个信息**Client Random、Server Random和Server Params**。
客户端这时也拿到了服务器的证书,那这个证书是不是真实有效的呢?
这就要用到第25讲里的知识了开始走证书链逐级验证确认证书的真实性再用证书公钥验证签名就确认了服务器的身份“刚才跟我打招呼的不是骗子可以接着往下走。”
然后,客户端按照密码套件的要求,也生成一个**椭圆曲线的公钥Client Params**,用“**Client Key Exchange**”消息发给服务器。
```
Handshake Protocol: Client Key Exchange
EC Diffie-Hellman Client Params
Pubkey: 8c674d0e08dc27b5eaa…
```
现在客户端和服务器手里都拿到了密钥交换算法的两个参数Client Params、Server Params就用ECDHE算法一阵算算出了一个新的东西叫“**Pre-Master**”,其实也是一个随机数。
至于具体的计算原理和过程,因为太复杂就不细说了,但算法可以保证即使黑客截获了之前的参数,也是绝对算不出这个随机数的。
现在客户端和服务器手里有了三个随机数:**Client Random、Server Random和Pre-Master**。用这三个作为原始材料,就可以生成用于加密会话的主密钥,叫“**Master Secret**”。而黑客因为拿不到“Pre-Master”所以也就得不到主密钥。
为什么非得这么麻烦,非要三个随机数呢?
这就必须说TLS的设计者考虑得非常周到了他们不信任客户端或服务器伪随机数的可靠性为了保证真正的“完全随机”“不可预测”把三个不可靠的随机数混合起来那么“随机”的程度就非常高了足够让黑客难以猜测。
你一定很想知道“Master Secret”究竟是怎么算出来的吧贴一下RFC里的公式
```
master_secret = PRF(pre_master_secret, &quot;master secret&quot;,
ClientHello.random + ServerHello.random)
```
这里的“PRF”就是伪随机数函数它基于密码套件里的最后一个参数比如这次的SHA384通过摘要算法来再一次强化“Master Secret”的随机性。
主密钥有48字节但它也不是最终用于通信的会话密钥还会再用PRF扩展出更多的密钥比如客户端发送用的会话密钥client_write_key、服务器发送用的会话密钥server_write_key等等避免只用一个密钥带来的安全隐患。
有了主密钥和派生的会话密钥,握手就快结束了。客户端发一个“**Change Cipher Spec**”,然后再发一个“**Finished**”消息,把之前所有发送的数据做个摘要,再加密一下,让服务器做个验证。
意思就是告诉服务器“后面都改用对称算法加密通信了啊用的就是打招呼时说的AES加密对不对还得你测一下。”
服务器也是同样的操作,发“**Change Cipher Spec**”和“**Finished**”消息双方都验证加密解密OK握手正式结束后面就收发被加密的HTTP请求和响应了。
## RSA握手过程
整个握手过程可真是够复杂的,但你可能会问了,好像这个过程和其他地方看到的不一样呢?
刚才说的其实是如今主流的TLS握手过程这与传统的握手有两点不同。
第一个使用ECDHE实现密钥交换而不是RSA所以会在服务器端发出“Server Key Exchange”消息。
第二个因为使用了ECDHE客户端可以不用等到服务器发回“Finished”确认握手完毕立即就发出HTTP报文省去了一个消息往返的时间浪费。这个叫“**TLS False Start**”意思就是“抢跑”和“TCP Fast Open”有点像都是不等连接完全建立就提前发应用数据提高传输的效率。
实验环境在440端口[https://www.chrono.com:440/26-1](https://www.chrono.com:440/26-1)实现了传统的RSA密钥交换没有“False Start”你可以课后自己抓包看一下这里我也画了个图。
<img src="https://static001.geekbang.org/resource/image/cb/d2/cb9a89055eadb452b7835ba8db7c3ad2.png" alt="">
大体的流程没有变只是“Pre-Master”不再需要用算法生成而是客户端直接生成随机数然后用服务器的公钥加密通过“**Client Key Exchange**”消息发给服务器。服务器再用私钥解密,这样双方也实现了共享三个随机数,就可以生成主密钥。
## 双向认证
到这里TLS握手就基本讲完了。
不过上面说的是“**单向认证**”握手过程,只认证了服务器的身份,而没有认证客户端的身份。这是因为通常单向认证通过后已经建立了安全通信,用账号、密码等简单的手段就能够确认用户的真实身份。
但为了防止账号、密码被盗有的时候比如网上银行还会使用U盾给用户颁发客户端证书实现“**双向认证**”,这样会更加安全。
双向认证的流程也没有太多变化,只是在“**Server Hello Done**”之后,“**Client Key Exchange**”之前,客户端要发送“**Client Certificate**”消息,服务器收到后也把证书链走一遍,验证客户端的身份。
## 小结
今天我们学习了HTTPS/TLS的握手内容比较多、比较难不过记住下面四点就可以。
1. HTTPS协议会先与服务器执行TCP握手然后执行TLS握手才能建立安全连接
1. 握手的目标是安全地交换对称密钥需要三个随机数第三个随机数“Pre-Master”必须加密传输绝对不能让黑客破解
1. “Hello”消息交换随机数“Key Exchange”消息交换“Pre-Master”
1. “Change Cipher Spec”之前传输的都是明文之后都是对称密钥加密的密文。
## 课下作业
1. 密码套件里的那些算法分别在握手过程中起了什么作用?
1. 你能完整地描述一下RSA的握手过程吗
1. 你能画出双向认证的流程图吗?
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/93/14/93d002084d9bf8283bab3e34e3f4bf14.png" alt="unpreview">

View File

@@ -0,0 +1,183 @@
<audio id="audio" title="27 | 更好更快的握手TLS1.3特性解析" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ac/aa/ac1b38c2d8b6278fffc5ef099f7edeaa.mp3"></audio>
上一讲中我讲了TLS1.2的握手过程,你是不是已经完全掌握了呢?
不过TLS1.2已经是10年前2008年的“老”协议了虽然历经考验但毕竟“岁月不饶人”在安全、性能等方面已经跟不上如今的互联网了。
于是经过四年、近30个草案的反复打磨TLS1.3终于在去年2018年“粉墨登场”再次确立了信息安全领域的新标准。
在抓包分析握手之前我们先来快速浏览一下TLS1.3的三个主要改进目标:**兼容**、**安全与性能**。
## 最大化兼容性
由于1.1、1.2等协议已经出现了很多年很多应用软件、中间代理官方称为“MiddleBox”只认老的记录协议格式更新改造很困难甚至是不可行设备僵化
在早期的试验中发现一旦变更了记录头字段里的版本号也就是由0x303TLS1.2改为0x304TLS1.3的话大量的代理服务器、网关都无法正确处理最终导致TLS握手失败。
为了保证这些被广泛部署的“老设备”能够继续使用避免新协议带来的“冲击”TLS1.3不得不做出妥协保持现有的记录格式不变通过“伪装”来实现兼容使得TLS1.3看上去“像是”TLS1.2。
那么该怎么区分1.2和1.3呢?
这要用到一个新的**扩展协议**Extension Protocol它有点“补充条款”的意思通过在记录末尾添加一系列的“扩展字段”来增加新的功能老版本的TLS不认识它可以直接忽略这就实现了“后向兼容”。
在记录头的Version字段被兼容性“固定”的情况下只要是TLS1.3协议握手的“Hello”消息后面就必须有“**supported_versions**”扩展它标记了TLS的版本号使用它就能区分新旧协议。
其实上一讲Chrome在握手时发的就是TLS1.3协议你可以看一下“Client Hello”消息后面的扩展只是因为服务器不支持1.3所以就“后向兼容”降级成了1.2。
```
Handshake Protocol: Client Hello
Version: TLS 1.2 (0x0303)
Extension: supported_versions (len=11)
Supported Version: TLS 1.3 (0x0304)
Supported Version: TLS 1.2 (0x0303)
```
TLS1.3利用扩展实现了许多重要的功能比如“supported_groups”“key_share”“signature_algorithms”“server_name”等这些等后面用到的时候再说。
## 强化安全
TLS1.2在十来年的应用中获得了许多宝贵的经验陆续发现了很多的漏洞和加密算法的弱点所以TLS1.3就在协议里修补了这些不安全因素。
比如:
- 伪随机数函数由PRF升级为HKDFHMAC-based Extract-and-Expand Key Derivation Function
- 明确禁止在记录协议里使用压缩;
- 废除了RC4、DES对称加密算法
- 废除了ECB、CBC等传统分组模式
- 废除了MD5、SHA1、SHA-224摘要算法
- 废除了RSA、DH密钥交换算法和许多命名曲线。
经过这一番“减肥瘦身”之后TLS1.3里只保留了AES、ChaCha20对称加密算法分组模式只能用AEAD的GCM、CCM和Poly1305摘要算法只能用SHA256、SHA384密钥交换算法只有ECDHE和DHE椭圆曲线也被“砍”到只剩P-256和x25519等5种。
减肥可以让人变得更轻巧灵活TLS也是这样。
算法精简后带来了一个意料之中的好处原来众多的算法、参数组合导致密码套件非常复杂难以选择而现在的TLS1.3里只有5个套件无论是客户端还是服务器都不会再犯“选择困难症”了。
<img src="https://static001.geekbang.org/resource/image/ee/65/eeeb1d30acbc0e69541ce0620346b765.jpg" alt="">
这里还要特别说一下废除RSA和DH密钥交换算法的原因。
上一讲用Wireshark抓包时你一定看到了浏览器默认会使用ECDHE而不是RSA做密钥交换这是因为它不具有“**前向安全**”Forward Secrecy
假设有这么一个很有耐心的黑客一直在长期收集混合加密系统收发的所有报文。如果加密系统使用服务器证书里的RSA做密钥交换一旦私钥泄露或被破解使用社会工程学或者巨型计算机那么黑客就能够使用私钥解密出之前所有报文的“Pre-Master”再算出会话密钥破解所有密文。
这就是所谓的“**今日截获,明日破解**”。
而ECDHE算法在每次握手时都会生成一对临时的公钥和私钥每次通信的密钥对都是不同的也就是“一次一密”即使黑客花大力气破解了这一次的会话密钥也只是这次通信被攻击之前的历史消息不会受到影响仍然是安全的。
所以现在主流的服务器和浏览器在握手阶段都已经不再使用RSA改用ECDHE而TLS1.3在协议里明确废除RSA和DH则在标准层面保证了“前向安全”。
## 提升性能
HTTPS建立连接时除了要做TCP握手还要做TLS握手在1.2中会多花两个消息往返2-RTT可能导致几十毫秒甚至上百毫秒的延迟在移动网络中延迟还会更严重。
现在因为密码套件大幅度简化也就没有必要再像以前那样走复杂的协商流程了。TLS1.3压缩了以前的“Hello”协商过程删除了“Key Exchange”消息把握手时间减少到了“1-RTT”效率提高了一倍。
那么它是怎么做的呢?
其实具体的做法还是利用了扩展。客户端在“Client Hello”消息里直接用“**supported_groups**”带上支持的曲线比如P-256、x25519用“**key_share**”带上曲线对应的客户端公钥参数,用“**signature_algorithms**”带上签名算法。
服务器收到后在这些扩展里选定一个曲线和参数再用“key_share”扩展返回服务器这边的公钥参数就实现了双方的密钥交换后面的流程就和1.2基本一样了。
我为1.3的握手过程画了一张图你可以对比1.2看看区别在哪里。
<img src="https://static001.geekbang.org/resource/image/4d/b0/4d1df4d07dbb1c2500fc4ea69ecf7ab0.png" alt="">
除了标准的“1-RTT”握手TLS1.3还引入了“0-RTT”握手用“pre_shared_key”和“early_data”扩展在TCP连接后立即就建立安全连接发送加密消息不过这需要有一些前提条件今天暂且不说。
## 握手分析
目前Nginx等Web服务器都能够很好地支持TLS1.3但要求底层的OpenSSL必须是1.1.1而我们实验环境里用的OpenSSL是1.1.0所以暂时无法直接测试TLS1.3。
不过我在Linux上用OpenSSL1.1.1编译了一个支持TLS1.3的Nginx用Wireshark抓包存到了GitHub上用它就可以分析TLS1.3的握手过程。
<img src="https://static001.geekbang.org/resource/image/7a/db/7a2bc39fdbb421cf72a01e887e9156db.png" alt="">
在TCP建立连接之后浏览器首先还是发一个“**Client Hello**”。
因为1.3的消息兼容1.2所以开头的版本号、支持的密码套件和随机数Client Random结构都是一样的不过这时的随机数是32个字节
```
Handshake Protocol: Client Hello
Version: TLS 1.2 (0x0303)
Random: cebeb6c05403654d66c2329…
Cipher Suites (18 suites)
Cipher Suite: TLS_AES_128_GCM_SHA256 (0x1301)
Cipher Suite: TLS_CHACHA20_POLY1305_SHA256 (0x1303)
Cipher Suite: TLS_AES_256_GCM_SHA384 (0x1302)
Extension: supported_versions (len=9)
Supported Version: TLS 1.3 (0x0304)
Supported Version: TLS 1.2 (0x0303)
Extension: supported_groups (len=14)
Supported Groups (6 groups)
Supported Group: x25519 (0x001d)
Supported Group: secp256r1 (0x0017)
Extension: key_share (len=107)
Key Share extension
Client Key Share Length: 105
Key Share Entry: Group: x25519
Key Share Entry: Group: secp256r1
```
注意“Client Hello”里的扩展“**supported_versions**”表示这是TLS1.3,“**supported_groups**”是支持的曲线,“**key_share**”是曲线对应的参数。
这就好像是说:
“还是照老规矩打招呼,这边有这些这些信息。但我猜你可能会升级,所以再多给你一些东西,也许后面用的上,咱们有话尽量一口气说完。”
服务器收到“Client Hello”同样返回“Server Hello”消息还是要给出一个**随机数**Server Random和选定密码套件。
```
Handshake Protocol: Server Hello
Version: TLS 1.2 (0x0303)
Random: 12d2bce6568b063d3dee2…
Cipher Suite: TLS_AES_128_GCM_SHA256 (0x1301)
Extension: supported_versions (len=2)
Supported Version: TLS 1.3 (0x0304)
Extension: key_share (len=36)
Key Share extension
Key Share Entry: Group: x25519, Key Exchange length: 32
```
表面上看和TLS1.2是一样的,重点是后面的扩展。“**supported_versions**”里确认使用的是TLS1.3,然后在“**key_share**”扩展带上曲线和对应的公钥参数。
服务器的“Hello”消息大概是这个意思
“还真让你给猜对了,虽然还是按老规矩打招呼,但咱们来个‘旧瓶装新酒’。刚才你给的我都用上了,我再给几个你缺的参数,这次加密就这么定了。”
这时只交换了两条消息,客户端和服务器就拿到了四个共享信息:**Client Random**和**Server Random**、**Client Params**和**Server Params**两边就可以各自用ECDHE算出“**Pre-Master**”再用HKDF生成主密钥“**Master Secret**”效率比TLS1.2提高了一大截。
在算出主密钥后,服务器立刻发出“**Change Cipher Spec**”消息比TLS1.2提早进入加密通信,后面的证书等就都是加密的了,减少了握手时的明文信息泄露。
这里TLS1.3还有一个安全强化措施,多了个“**Certificate Verify**”消息,用服务器的私钥把前面的曲线、套件、参数等握手数据加了签名,作用和“**Finished**”消息差不多。但由于是私钥签名,所以强化了身份认证和和防窜改。
这两个“Hello”消息之后客户端验证服务器证书再发“Finished”消息就正式完成了握手开始收发HTTP报文。
虽然我们的实验环境暂时不能抓包测试TLS1.3但互联网上很多网站都已经支持了TLS1.3,比如[Nginx](https://www.nginx.com/)、[GitHub](https://github.com/)你可以课后自己用Wireshark试试。
在Chrome的开发者工具里可以看到这些网站的TLS1.3应用情况。
<img src="https://static001.geekbang.org/resource/image/44/3c/44d8c3349ffdea5a1e4e13d222bc743c.png" alt="">
## 小结
今天我们一起学习了TLS1.3的新特性用抓包研究了它的握手过程不过TLS1.3里的内容很多,还有一些特性没有谈到,后面会继续讲。
1. 为了兼容1.1、1.2等“老”协议TLS1.3会“伪装”成TLS1.2,新特性在“扩展”里实现;
1. 1.1、1.2在实践中发现了很多安全隐患所以TLS1.3大幅度删减了加密算法只保留了ECDHE、AES、ChaCha20、SHA-2等极少数算法强化了安全
1. TLS1.3也简化了握手过程,完全握手只需要一个消息往返,提升了性能。
## 课下作业
1. TLS1.3里的密码套件没有指定密钥交换算法和签名算法,那么在握手的时候会不会有问题呢?
1. 结合上一讲的RSA握手过程解释一下为什么RSA密钥交换不具有“前向安全”。
1. TLS1.3的握手过程与TLS1.2的“False Start”有什么异同
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/ab/64/ab532f0074ddb136cd96c76c3a385164.png" alt="unpreview">

View File

@@ -0,0 +1,166 @@
<audio id="audio" title="28 | 连接太慢该怎么办HTTPS的优化" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3e/02/3e5a61bf873fe75c1dc93dfc5c08d602.mp3"></audio>
你可能或多或少听别人说过“HTTPS的连接很慢”。那么“慢”的原因是什么呢
通过前两讲的学习你可以看到HTTPS连接大致上可以划分为两个部分第一个是建立连接时的**非对称加密握手**,第二个是握手后的**对称加密报文传输**。
由于目前流行的AES、ChaCha20性能都很好还有硬件优化报文传输的性能损耗可以说是非常地小小到几乎可以忽略不计了。所以通常所说的“HTTPS连接慢”指的就是刚开始建立连接的那段时间。
在TCP建连之后正式数据传输之前HTTPS比HTTP增加了一个TLS握手的步骤这个步骤最长可以花费两个消息往返也就是2-RTT。而且在握手消息的网络耗时之外还会有其他的一些“隐形”消耗比如
- 产生用于密钥交换的临时公私钥对ECDHE
- 验证证书时访问CA获取CRL或者OCSP
- 非对称加密解密处理“Pre-Master”。
在最差的情况下也就是不做任何的优化措施HTTPS建立连接可能会比HTTP慢上几百毫秒甚至几秒这其中既有网络耗时也有计算耗时就会让人产生“打开一个HTTPS网站好慢啊”的感觉。
不过刚才说的情况早就是“过去时”了现在已经有了很多行之有效的HTTPS优化手段运用得好可以把连接的额外耗时降低到几十毫秒甚至是“零”。
我画了一张图把TLS握手过程中影响性能的部分都标记了出来对照着它就可以“有的放矢”地来优化HTTPS。
<img src="https://static001.geekbang.org/resource/image/c4/ed/c41da1f1b1bdf4dc92c46330542c5ded.png" alt="">
## 硬件优化
在计算机世界里的“优化”可以分成“硬件优化”和“软件优化”两种方式,先来看看有哪些硬件的手段。
硬件优化,说白了就是“花钱”。但花钱也是有门道的,要“有钱用在刀刃上”,不能大把的银子撒出去“只听见响”。
HTTPS连接是计算密集型而不是I/O密集型。所以如果你花大价钱去买网卡、带宽、SSD存储就是“南辕北辙”了起不到优化的效果。
那该用什么样的硬件来做优化呢?
首先,你可以选择**更快的CPU**最好还内建AES优化这样即可以加速握手也可以加速传输。
其次,你可以选择“**SSL加速卡**”加解密时调用它的API让专门的硬件来做非对称加解密分担CPU的计算压力。
不过“SSL加速卡”也有一些缺点比如升级慢、支持算法有限不能灵活定制解决方案等。
所以,就出现了第三种硬件加速方式:“**SSL加速服务器**”用专门的服务器集群来彻底“卸载”TLS握手时的加密解密计算性能自然要比单纯的“加速卡”要强大的多。
## 软件优化
不过硬件优化方式中除了CPU其他的通常可不是靠简单花钱就能买到的还要有一些开发适配工作有一定的实施难度。比如“加速服务器”中关键的一点是通信必须是“异步”的不能阻塞应用服务器否则加速就没有意义了。
所以,软件优化的方式相对来说更可行一些,性价比高,能够“少花钱,多办事”。
软件方面的优化还可以再分成两部分:一个是**软件升级**,一个是**协议优化**。
软件升级实施起来比较简单就是把现在正在使用的软件尽量升级到最新版本比如把Linux内核由2.x升级到4.x把Nginx由1.6升级到1.16把OpenSSL由1.0.1升级到1.1.0/1.1.1。
由于这些软件在更新版本的时候都会做性能优化、修复错误,只要运维能够主动配合,这种软件优化是最容易做的,也是最容易达成优化效果的。
但对于很多大中型公司来说,硬件升级或软件升级都是个棘手的问题,有成千上万台各种型号的机器遍布各个机房,逐一升级不仅需要大量人手,而且有较高的风险,可能会影响正常的线上服务。
所以,在软硬件升级都不可行的情况下,我们最常用的优化方式就是在现有的环境下挖掘协议自身的潜力。
## 协议优化
从刚才的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”略快一点点。
在Nginx里可以用“ssl_ciphers”“ssl_ecdh_curve”等指令配置服务器使用的密码套件和椭圆曲线把优先使用的放在前面例如
```
ssl_ciphers TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:EECDH+CHACHA20
ssl_ecdh_curve X25519:P-256;
```
## 证书优化
除了密钥交换,握手过程中的证书验证也是一个比较耗时的操作,服务器需要把自己的证书链全发给客户端,然后客户端接收后再逐一验证。
这里就有两个优化点,一个是**证书传输**,一个是**证书验证**。
服务器的证书可以选择椭圆曲线ECDSA证书而不是RSA证书因为224位的ECC相当于2048位的RSA所以椭圆曲线证书的“个头”要比RSA小很多即能够节约带宽也能减少客户端的运算量可谓“一举两得”。
客户端的证书验证其实是个很复杂的操作除了要公钥解密验证多个证书签名外因为证书还有可能会被撤销失效客户端有时还会再去访问CA下载CRL或者OCSP数据这又会产生DNS查询、建立连接、收发数据等一系列网络通信增加好几个RTT。
CRLCertificate revocation list证书吊销列表由CA定期发布里面是所有被撤销信任的证书序号查询这个列表就可以知道证书是否有效。
但CRL因为是“定期”发布就有“时间窗口”的安全隐患而且随着吊销证书的增多列表会越来越大一个CRL经常会上MB。想象一下每次需要预先下载几M的“无用数据”才能连接网站实用性实在是太低了。
所以现在CRL基本上不用了取而代之的是OCSP在线证书状态协议Online Certificate Status Protocol向CA发送查询请求让CA返回证书的有效状态。
但OCSP也要多出一次网络请求的消耗而且还依赖于CA服务器如果CA服务器很忙那响应延迟也是等不起的。
于是又出来了一个“补丁”叫“OCSP Stapling”OCSP装订它可以让服务器预先访问CA获取OCSP响应然后在握手时随着证书一起发给客户端免去了客户端连接CA服务器查询的时间。
## 会话复用
到这里我们已经讨论了四种HTTPS优化手段硬件优化、软件优化、协议优化、证书优化那么还有没有其他更好的方式呢
我们再回想一下HTTPS建立连接的过程先是TCP三次握手然后是TLS一次握手。这后一次握手的重点是算出主密钥“Master Secret”而主密钥每次连接都要重新计算未免有点太浪费了如果能够把“辛辛苦苦”算出来的主密钥缓存一下“重用”不就可以免去了握手和计算的成本了吗
这种做法就叫“**会话复用**”TLS session resumption和HTTP Cache一样也是提高HTTPS性能的“大杀器”被浏览器和服务器广泛应用。
会话复用分两种,第一种叫“**Session ID**”就是客户端和服务器首次连接后各自保存一个会话的ID号内存里存储主密钥和其他相关的信息。当客户端再次连接时发一个ID过来服务器就在内存里找找到就直接用主密钥恢复会话状态跳过证书验证和密钥交换只用一个消息往返就可以建立安全通信。
实验环境的端口441实现了“Session ID”的会话复用你可以访问URI<br>
“[https://www.chrono.com:441/28-1](https://www.chrono.com:441/28-1)”刷新几次用Wireshark抓包看看实际的效果。
```
Handshake Protocol: Client Hello
Version: TLS 1.2 (0x0303)
Session ID: 13564734eeec0a658830cd…
Cipher Suites Length: 34
Handshake Protocol: Server Hello
Version: TLS 1.2 (0x0303)
Session ID: 13564734eeec0a658830cd…
Cipher Suite: TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (0xc030)
```
通过抓包可以看到服务器在“ServerHello”消息后直接发送了“Change Cipher Spec”和“Finished”消息复用会话完成了握手。
<img src="https://static001.geekbang.org/resource/image/12/ac/125fe443a147ed38a97a4492045d98ac.png" alt="">
## 会话票证
“Session ID”是最早出现的会话复用技术也是应用最广的但它也有缺点服务器必须保存每一个客户端的会话数据对于拥有百万、千万级别用户的网站来说存储量就成了大问题加重了服务器的负担。
于是,又出现了第二种“**Session Ticket**”方案。
它有点类似HTTP的Cookie存储的责任由服务器转移到了客户端服务器加密会话信息用“New Session Ticket”消息发给客户端让客户端保存。
重连的时候,客户端使用扩展“**session_ticket**”发送“Ticket”而不是“Session ID”服务器解密后验证有效期就可以恢复会话开始加密通信。
这个过程也可以在实验环境里测试端口号是442URI是“[https://www.chrono.com:442/28-1](https://www.chrono.com:442/28-1)”。
不过“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”。
<img src="https://static001.geekbang.org/resource/image/11/ab/119cfd261db49550411a12b1f6d826ab.png" alt="">
但“PSK”也不是完美的它为了追求效率而牺牲了一点安全性容易受到“重放攻击”Replay attack的威胁。黑客可以截获“PSK”的数据像复读机那样反复向服务器发送。
解决的办法是只允许安全的GET/HEAD方法参见[第10讲](https://time.geekbang.org/column/article/101518)在消息里加入时间戳、“nonce”验证或者“一次性票证”限制重放。
## 小结
1. 可以有多种硬件和软件手段减少网络耗时和计算耗时让HTTPS变得和HTTP一样快最可行的是软件优化
1. 应当尽量使用ECDHE椭圆曲线密码套件节约带宽和计算量还能实现“False Start”
1. 服务器端应当开启“OCSP Stapling”功能避免客户端访问CA去验证证书
1. 会话复用的效果类似Cache前提是客户端必须之前成功建立连接后面就可以用“Session ID”“Session Ticket”等凭据跳过密钥交换、证书验证等步骤直接开始加密通信。
## 课下作业
1. 你能比较一下“Session ID”“Session Ticket”“PSK”这三种会话复用手段的异同吗
1. 你觉得哪些优化手段是你在实际工作中能用到的?应该怎样去用?
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/a2/ab/a251606fb0637c6db45b7fd6660af5ab.png" alt="unpreview">

View File

@@ -0,0 +1,187 @@
<audio id="audio" title="29 | 我应该迁移到HTTPS吗" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/00/fe/0066de60d6234eee65329be1555eb2fe.mp3"></audio>
今天是“安全篇”的最后一讲我们已经学完了HTTPS、TLS相关的大部分知识。不过或许你心里还会有一些困惑
“HTTPS这么复杂我是否应该迁移到HTTPS呢它能带来哪些好处呢具体又应该怎么实施迁移呢
这些问题不单是你,也是其他很多人,还有当初的我的真实想法,所以今天我就来跟你聊聊这方面的事情。
## 迁移的必要性
如果你做移动应用开发的话那么就一定知道Apple、Android、某信等开发平台在2017年就相继发出通知要求所有的应用必须使用HTTPS连接禁止不安全的HTTP。
在台式机上主流的浏览器Chrome、Firefox等也早就开始“强推”HTTPS把HTTP站点打上“不安全”的标签给用户以“心理压力”。
Google等搜索巨头还利用自身的“话语权”优势降低HTTP站点的排名而给HTTPS更大的权重力图让网民只访问到HTTPS网站。
这些手段都逐渐“挤压”了纯明文HTTP的生存空间“迁移到HTTPS”已经不是“要不要做”的问题而是“要怎么做”的问题了。HTTPS的大潮无法阻挡如果还是死守着HTTP那么无疑会被冲刷到互联网的角落里。
目前国内外的许多知名大站都已经实现了“全站HTTPS”打开常用的某宝、某东、某浪都可以在浏览器的地址栏里看到“小锁头”如果你正在维护的网站还没有实施HTTPS那可要抓点紧了。
## 迁移的顾虑
据我观察阻碍HTTPS实施的因素还有一些这样那样的顾虑我总结出了三个比较流行的观点“慢、贵、难”。
所谓“慢”是指惯性思维拿以前的数据来评估HTTPS的性能认为HTTPS会增加服务器的成本增加客户端的时延影响用户体验。
其实现在服务器和客户端的运算能力都已经有了很大的提升,性能方面完全没有担心的必要,而且还可以应用很多的优化解决方案(参见[第28讲](https://time.geekbang.org/column/article/111287)。根据Google等公司的评估在经过适当优化之后HTTPS的额外CPU成本小于1%额外的网络成本小于2%可以说是与无加密的HTTP相差无几。
所谓“贵”,主要是指证书申请和维护的成本太高,网站难以承担。
这也属于惯性思维在早几年的确是个问题向CA申请证书的过程不仅麻烦而且价格昂贵每年要交几千甚至几万元。
但现在就不一样了为了推广HTTPS很多云服务厂商都提供了一键申请、价格低廉的证书而且还出现了专门颁发免费证书的CA其中最著名的就是“**Lets Encrypt**”。
所谓的“难”是指HTTPS涉及的知识点太多、太复杂有一定的技术门槛不能很快上手。
这第三个顾虑比较现实HTTPS背后关联到了密码学、TLS、PKI等许多领域不是短短几周、几个月就能够精通的。但实施HTTPS也并不需要把这些完全掌握只要抓住少数几个要点就好下面我就来帮你逐个解决一些关键的“难点”。
## 申请证书
要把网站从HTTP切换到HTTPS首先要做的就是为网站申请一张证书。
大型网站出于信誉、公司形象的考虑通常会选择向传统的CA申请证书例如DigiCert、GlobalSign而中小型网站完全可以选择使用“Lets Encrypt”这样的免费证书效果也完全不输于那些收费的证书。
“**Lets Encrypt**”一直在推动证书的自动化部署为此还实现了专门的ACME协议RFC8555。有很多的客户端软件可以完成申请、验证、下载、更新的“一条龙”操作比如Certbot、acme.sh等等都可以在“Lets Encrypt”网站上找到用法很简单相关的文档也很详细几分钟就能完成申请所以我在这里就不细说了。
不过我必须提醒你几个注意事项。
第一申请证书时应当同时申请RSA和ECDSA两种证书在Nginx里配置成双证书验证这样服务器可以自动选择快速的椭圆曲线证书同时也兼容只支持RSA的客户端。
第二如果申请RSA证书私钥至少要2048位摘要算法应该选用SHA-2例如SHA256、SHA384等。
第三出于安全的考虑“Lets Encrypt”证书的有效期很短只有90天时间一到就会过期失效所以必须要定期更新。你可以在crontab里加个每周或每月任务发送更新请求不过很多ACME客户端会自动添加这样的定期任务完全不用你操心。
## 配置HTTPS
搞定了证书接下来就是配置Web服务器在443端口上开启HTTPS服务了。
这在Nginx上非常简单只要在“listen”指令后面加上参数“ssl”再配上刚才的证书文件就可以实现最基本的HTTPS。
```
listen 443 ssl;
ssl_certificate xxx_rsa.crt; #rsa2048 cert
ssl_certificate_key xxx_rsa.key; #rsa2048 private key
ssl_certificate xxx_ecc.crt; #ecdsa cert
ssl_certificate_key xxx_ecc.key; #ecdsa private ke
```
为了提高HTTPS的安全系数和性能你还可以强制Nginx只支持TLS1.2以上的协议打开“Session Ticket”会话复用
```
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_timeout 5m;
ssl_session_tickets on;
ssl_session_ticket_key ticket.key;
```
密码套件的选择方面我给你的建议是以服务器的套件优先。这样可以避免恶意客户端故意选择较弱的套件、降低安全等级然后密码套件向TLS1.3“看齐”只使用ECDHE、AES和ChaCha20支持“False Start”。
```
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-CHACHA20-POLY1305:ECDHE+AES128:!MD5:!SHA1;
```
如果你的服务器上使用了OpenSSL的分支BorringSSL那么还可以使用一个特殊的“等价密码组”Equal preference cipher groups特性它可以让服务器配置一组“等价”的密码套件在这些套件里允许客户端优先选择比如这么配置
```
ssl_ciphers
[ECDHE-ECDSA-AES128-GCM-SHA256|ECDHE-ECDSA-CHACHA20-POLY1305];
```
如果客户端硬件没有AES优化服务器就会顺着客户端的意思优先选择与AES“等价”的ChaCha20算法让客户端能够快一点。
全部配置完成后,你可以访问“[SSLLabs](https://www.ssllabs.com/)”网站,测试网站的安全程度,它会模拟多种客户端发起测试,打出一个综合的评分。
下图就是GitHub网站的评分结果
<img src="https://static001.geekbang.org/resource/image/a6/1b/a662d410dfdaa8ab44b36cbb68ab8d1b.png" alt="">
## 服务器名称指示
配置HTTPS服务时还有一个“虚拟主机”的问题需要解决。
在HTTP协议里多个域名可以同时在一个IP地址上运行这就是“虚拟主机”Web服务器会使用请求头里的Host字段参见[第9讲](https://time.geekbang.org/column/article/100513))来选择。
但在HTTPS里因为请求头只有在TLS握手之后才能发送在握手时就必须选择“虚拟主机”对应的证书TLS无法得知域名的信息就只能用IP地址来区分。所以最早的时候每个HTTPS域名必须使用独立的IP地址非常不方便。
那么怎么解决这个问题呢?
这还是得用到TLS的“扩展”给协议加个**SNI**Server Name Indication的“补充条款”。它的作用和Host字段差不多客户端会在“Client Hello”时带上域名信息这样服务器就可以根据名字而不是IP地址来选择证书。
```
Extension: server_name (len=19)
Server Name Indication extension
Server Name Type: host_name (0)
Server Name: www.chrono.com
```
Nginx很早就基于SNI特性支持了HTTPS的虚拟主机但在OpenResty里可还以编写Lua脚本利用Redis、MySQL等数据库更灵活快速地加载证书。
## 重定向跳转
现在有了HTTPS服务但原来的HTTP站点也不能马上弃用还是会有很多网民习惯在地址栏里直接敲域名或者是旧的书签、超链接默认使用HTTP协议访问。
所以我们就需要用到第18讲里的“重定向跳转”技术了把不安全的HTTP网址用301或302“重定向”到新的HTTPS网站这在Nginx里也很容易做到使用“return”或“rewrite”都可以。
```
return 301 https://$host$request_uri; #永久重定向
rewrite ^ https://$host$request_uri permanent; #永久重定向
```
但这种方式有两个问题。一个是重定向增加了网络成本,多出了一次请求;另一个是存在安全隐患,重定向的响应可能会被“中间人”窜改,实现“会话劫持”,跳转到恶意网站。
不过有一种叫“**HSTS**”HTTP严格传输安全HTTP Strict Transport Security的技术可以消除这种安全隐患。HTTPS服务器需要在发出的响应头里添加一个“**Strict-Transport-Security**”的字段,再设定一个有效期,例如:
```
Strict-Transport-Security: max-age=15768000; includeSubDomains
```
这相当于告诉浏览器我这个网站必须严格使用HTTPS协议在半年之内182.5天都不允许用HTTP你以后就自己做转换吧不要再来麻烦我了。
有了“HSTS”的指示以后浏览器再访问同样的域名的时候就会自动把URI里的“http”改成“https”直接访问安全的HTTPS网站。这样“中间人”就失去了攻击的机会而且对于客户端来说也免去了一次跳转加快了连接速度。
比如如果在实验环境的配置文件里用“add_header”指令添加“HSTS”字段
```
add_header Strict-Transport-Security max-age=15768000; #182.5days
```
那么Chrome浏览器只会在第一次连接时使用HTTP协议之后就会都走HTTPS协议。
## 小结
今天我介绍了一些HTTPS迁移的技术要点掌握了它们你就可以搭建出一个完整的HTTPS站点了。
但想要实现大型网站的“全站HTTPS”还是需要有很多的细枝末节的工作要做比如使用CSPContent Security Policy的各种指令和标签来配置安全策略使用反向代理来集中“卸载”SSL。
简单小结一下今天的内容:
1. 从HTTP迁移到HTTPS是“大势所趋”能做就应该尽早做
1. 升级HTTPS首先要申请数字证书可以选择免费好用的“Lets Encrypt”
1. 配置HTTPS时需要注意选择恰当的TLS版本和密码套件强化安全
1. 原有的HTTP站点可以保留作为过渡使用301重定向到HTTPS。
## 课下作业
1. 结合你的实际工作分析一下迁移HTTPS的难点有哪些应该如何克服
1. 参考上一讲你觉得配置HTTPS时还应该加上哪些部分
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/db/ec/dbe386f94df8f69fc0b32d2b52e3b3ec.png" alt="unpreview">
<img src="https://static001.geekbang.org/resource/image/56/63/56d766fc04654a31536f554b8bde7b63.jpg" alt="unpreview">

View File

@@ -0,0 +1,86 @@
<audio id="audio" title="开篇词To Be a HTTP Hero" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/24/7f/243a3f2cac4406a379a8030f9e2e897f.mp3"></audio>
你好我是罗剑锋Chrono一名埋头于前线辛勤“耕耘”了十余载的资深“码农”。
工作的这十多年来我开发过智能IC卡也倒腾过商用密码机做过政务项目也做过商务搜索写过网游核心引擎也写过CDN存储系统在Windows上用C/C++做客户端在AIX、Linux上用Java、PHP写后台服务……现在则是专注于“魔改”Nginx深度定制实现网络协议的分析与检测。
当极客时间的编辑联系我要我写HTTP专栏的时候我的第一反应是“HTTP协议好简单的有这个必要吗
你可能也会有同样的想法“HTTP不就是请求/响应、GET/POST、Header/Body吗网络上的资料一抓一大把有什么问题搜一下就是了。”
不瞒你说我当时就是这么想的在之前的工作中也是一直这么做的而且一直“感觉良好”觉得HTTP就是这个样子没有什么特别的地方没有什么值得讲的。
但在编辑的一再坚持下,我“勉为其难”接下了这个任务。然后做了一个小范围的“调查”,问一些周围的同事,各个领域的都有,比如产品、开发、运维、测试、前端、后端、手机端……想看看他们有什么意见。
出乎我的意料他们无一例外都对这个“HTTP专栏”有很强烈的需求想好好“补补课”系统地学习了解HTTP这其中甚至还包括有七、八年甚至更多工作经验的老手。
这不禁让我陷入了思考,为什么如此“简单”的协议却还有这么多的人想要学呢?
我想一个原因可能是HTTP协议“**太常见**”了。就像现实中的水和空气一样,如此重要却又如此普遍,普遍到我们几乎忽视了它的存在。真的很像那句俗语所说:“鱼总是最后看见水的”,但水对鱼的生存却又是至关重要。
我认真回忆了一下这些年的工作经历这才发现HTTP只是表面上显得简单而底层的运行机制、工作原理绝不简单可以说是非常地复杂。只是我们平常总是“KPI优先”网上抓到一个解决方法用过就完事了没有去深究里面的要点和细节。
下面的几个场景,都是我周围同事的实际感受,你是否也在工作中遇到过这样的困惑呢?你能把它们都解释清楚吗?
- 用Nginx搭建Web服务器照着网上的文章配好了但里面那么多的指令什么keepalive、rewrite、proxy_pass都是怎么回事为什么要这么配置
- 用Python写爬虫URI、URL“傻傻分不清”有时里面还会加一些奇怪的字符怎么处理才好
- 都说HTTP缓存很有用可以大幅度提升系统性能可它是怎么做到的又应该用在何时何地
- HTTP和HTTPS是什么关系还经常听说有SSL/TLS/SNI/OCSP/ALPN……这么多稀奇古怪的缩写头都大了实在是搞不懂。
其实这些问题也并不是什么新问题把关键字粘贴进搜索栏再点一下按钮搜索引擎马上就能找出几十万个相关的页面。但看完第一页的前几个链接后通常还是有种“懵懵懂懂”“似懂非懂”的感觉觉得说的对又不全对和自己的思路总是不够“Match”。
不过大多数情况下你可能都没有时间细想优先目标是把手头的工作“对付过去”。长此以来你对HTTP的认识也可能仅限于这样的“知其然而不知其所以然”实际情况就是HTTP天天用时时用但想认真、系统地学习一下梳理出自己的知识体系经常会发现无从下手。
我把这种HTTP学习的现状归纳为三点**正式资料“少”、网上资料“杂”、权威资料“难”**。
第一个,**正式资料“少”**。
上购书网站搜个Python、Java搜个MySQL、Node.js能出一大堆。但搜HTTP实在是少得可怜那么几本一只手的手指头就可以数得过来和语言类、数据库类、框架类图书真是形成了鲜明的对比。
现有的HTTP相关图书我都看过怎么说呢它们都有一个特点“广撒网捕小鱼”都是知识点可未免太“照本宣科”了理论有余实践不足看完了还是不知道怎么去用。
而且这些书的“岁数”都很大依据的都是20年前的RFC2616很多内容都不合时宜而新标准7230已经更新了很多关键的细节。
第二个,**网上资料“杂”**。
正式的图书少而且过时那就求助于网络社区吧。现在的博客、论坛、搜索引擎非常发达网上有很多HTTP协议相关的文章也都是网友的实践经验分享“干货”很多很能解决实际问题。
但网上文章的特点是细小、零碎通常只“钉”在一个很小的知识点上而且由于帖子长度的限制无法深入展开论述很多都是“浅尝辄止”通常都止步在“How”层次很少能说到“Why”能说透的更是寥寥无几。
网文还有一个难以避免的“毛病”,就是“良莠不齐”。同一个主题可能会有好几种不同的说法,有的还会互相矛盾、以讹传讹。这种情况是最麻烦的,你必须花大力气去鉴别真假,不小心就会被“带到沟里”。
可想而知这种“东一榔头西一棒子”的学习方式用“碎片”拼凑出来的HTTP知识体系是非常不完善的会有各种漏洞遇到问题时基本派不上用场还得再去找其他的“碎片”。
第三个,**权威资料“难”**。
图书少网文杂我们还有一个终极的学习资料那就是RFC文档。
RFC是互联网工程组IETF发布的官方文件是对HTTP最权威的定义和解释。但它也是最难懂的全英文看着费劲理解起来更是难上加难文档之间还会互相关联引用“劝退率”极高。
这三个问题就像是“三座大山”阻碍了像你这样的很多有心人去学习、了解HTTP协议。
那么怎么才能更好地学习HTTP呢
我为这个专栏定了一个基调“要有广度但更要有深度”。目标是成为含金量最高的HTTP学习资料新手可以由浅入深、系统学习老手可以温故知新、查缺补漏让你花最少的时间用最少的精力掌握最多、最全面、最系统的知识。
由于HTTP应用得非常广泛几乎涉及到所有的领域所以我会在广度上从HTTP尽量向外扩展不只讲协议本身与它相关的TCP/IP、DNS、SSL/TLS、Web Server等都会讲到而且会把它们打通串联在一起形成知识链让你知道它们之间是怎么联系、怎么运行的。
专栏文章的深度上我也是下足了功夫全部基于最新的RFC标准文档再结合我自己多年的实践体会力求讲清讲透能让你看了以后有豁然开朗的感觉。
比如分析HTTPS我会用Wireshark从建立TCP连接时就开始抓包从二进制最底层来分析里面的Record、Cipher Suite、Extension讲ECDHE、AES、SHA384再画出详细的流程图做到“一览无余”。
陆游有诗:“**纸上得来终觉浅,绝知此事要躬行**”。学习网络协议最重要的就是实践在专栏里我还会教你用Nginx搭建一个“麻雀虽小五脏俱全”的实验环境让你与HTTP零距离接触。
它有一个最大的优点自身就是一个完整的网络环境即使不联网也能够在里面收发HTTP消息。
我还精心设计了配套的测试用例最小化应用场景排除干扰因素你可以在里面任意测试HTTP的各种特性再配合Wireshark抓包就能够理论结合实践更好地掌握HTTP的知识。
每一讲的末尾,我也会留几个思考题,你可以把它当作是求职时的面试官问题,尽量认真思考后再回答,这样能够把专栏的学习由“被动地听”,转变为“主动地学”,实现“学以致用”。
当然了,你和我的“兴趣点”不可能完全一样,我在讲课时也难免“顾此失彼”“挂一漏万”,希望你积极留言,我会视情况做些调整,或者用答疑的形式补充没讲到的内容。
今年是万维网和HTTP诞生30周年也是HTTP/1.1诞生20周年套用莎翁《哈姆雷特》里的名句让我们在接下来的三个月里一起努力。
“To Be a HTTP Hero

View File

@@ -0,0 +1,128 @@
<audio id="audio" title="39 | HTTP性能优化面面观" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/8d/55/8d022162cc6c4652d1f6f73562edf155.mp3"></audio>
“透视HTTP协议”这个专栏已经陪伴了你近三个月的时间在最后的这两讲里我将把散落在前面各个章节的零散知识点整合起来做一个总结和你一起聊聊HTTP的性能优化。
由于HTTPSSSL/TLS的优化已经在[第28讲](https://time.geekbang.org/column/article/111287)里介绍的比较详细了,所以这次就暂时略过不谈,你可以课后再找机会复习。
既然要做性能优化,那么,我们就需要知道:什么是性能?它都有哪些指标,又应该如何度量,进而采取哪些手段去优化?
“性能”其实是一个复杂的概念。不同的人、不同的应用场景都会对它有不同的定义。对于HTTP来说它又是一个非常复杂的系统里面有非常多的角色所以很难用一两个简单的词就能把性能描述清楚。
还是从HTTP最基本的“请求-应答”模型来着手吧。在这个模型里有两个角色:客户端和服务器,还有中间的传输链路,考查性能就可以看这三个部分。
<img src="https://static001.geekbang.org/resource/image/3a/62/3a8ab1e3ace62d184adc2dc595d32f62.png" alt="unpreview">
## HTTP服务器
我们先来看看服务器它一般运行在Linux操作系统上用Apache、Nginx等Web服务器软件对外提供服务所以性能的含义就是它的服务能力也就是尽可能多、尽可能快地处理用户的请求。
衡量服务器性能的主要指标有三个:**吞吐量**requests per second、**并发数**concurrency和**响应时间**time per request
吞吐量就是我们常说的RPS每秒的请求次数也有叫TPS、QPS它是服务器最基本的性能指标RPS越高就说明服务器的性能越好。
并发数反映的是服务器的负载能力,也就是服务器能够同时支持的客户端数量,当然也是越多越好,能够服务更多的用户。
响应时间反映的是服务器的处理能力,也就是快慢程度,响应时间越短,单位时间内服务器就能够给越多的用户提供服务,提高吞吐量和并发数。
除了上面的三个基本性能指标服务器还要考虑CPU、内存、硬盘和网卡等系统资源的占用程度利用率过高或者过低都可能有问题。
在HTTP多年的发展过程中已经出现了很多成熟的工具来测量这些服务器的性能指标开源的、商业的、命令行的、图形化的都有。
在Linux上最常用的性能测试工具可能就是abApache Bench比如下面的命令指定了并发数100总共发送10000个请求
```
ab -c 100 -n 10000 'http://www.xxx.com'
```
系统资源监控方面Linux自带的工具也非常多常用的有uptime、top、vmstat、netstat、sar等等可能你比我还要熟悉我就列几个简单的例子吧
```
top #查看CPU和内存占用情况
vmstat 2 #每2秒检查一次系统状态
sar -n DEV 2 #看所有网卡的流量定时2秒检查
```
理解了这些性能指标,我们就知道了服务器的性能优化方向:合理利用系统资源,提高服务器的吞吐量和并发数,降低响应时间。
## HTTP客户端
看完了服务器的性能指标,我们再来看看如何度量客户端的性能。
客户端是信息的消费者,一切数据都要通过网络从服务器获取,所以它最基本的性能指标就是“**延迟**”latency
之前在讲HTTP/2的时候就简单介绍过延迟。所谓的“延迟”其实就是“等待”等待数据到达客户端时所花费的时间。但因为HTTP的传输链路很复杂所以延迟的原因也就多种多样。
首先,我们必须谨记有一个“不可逾越”的障碍——光速,因为地理距离而导致的延迟是无法克服的,访问数千公里外的网站显然会有更大的延迟。
其次第二个因素是带宽它又包括接入互联网时的电缆、WiFi、4G和运营商内部网络、运营商之间网络的各种带宽每一处都有可能成为数据传输的瓶颈降低传输速度增加延迟。
第三个因素是DNS查询如果域名在本地没有缓存就必须向DNS系统发起查询引发一连串的网络通信成本而在获取IP地址之前客户端只能等待无法访问网站。
第四个因素是TCP握手你应该对它比较熟悉了吧必须要经过SYN、SYN/ACK、ACK三个包之后才能建立连接它带来的延迟由光速和带宽共同决定。
建立TCP连接之后就是正常的数据收发了后面还有解析HTML、执行JavaScript、排版渲染等等这些也会耗费一些时间。不过它们已经不属于HTTP了所以不在今天的讨论范围之内。
之前讲HTTPS时介绍过一个专门的网站“[SSLLabs](https://www.ssllabs.com/)”而对于HTTP性能优化也有一个专门的测试网站“[WebPageTest](https://www.webpagetest.org)”。它的特点是在世界各地建立了很多的测试点,可以任意选择地理位置、机型、操作系统和浏览器发起测试,非常方便,用法也很简单。
网站测试的最终结果是一个直观的“瀑布图”Waterfall Chart清晰地列出了页面中所有资源加载的先后顺序和时间消耗比如下图就是对GitHub首页的一次测试。
<img src="https://static001.geekbang.org/resource/image/5c/f4/5cd2a91b4466ee63f48bc049ba61b9f4.png" alt="">
Chrome等浏览器自带的开发者工具也可以很好地观察客户端延迟指标面板左边有每个URI具体消耗的时间面板的右边也是类似的瀑布图。
点击某个URI在Timing页里会显示出一个小型的“瀑布图”是这个资源消耗时间的详细分解延迟的原因都列的清清楚楚比如下面的这张图
<img src="https://static001.geekbang.org/resource/image/d7/a2/d77ee484b62910b8eedce0ecddb305a2.png" alt="">
图里面的这些指标都是什么含义呢?我给你解释一下:
- 因为有“队头阻塞”浏览器对每个域名最多开6个并发连接HTTP/1.1当页面里链接很多的时候就必须排队等待Queued、Queueing这里它就等待了1.62秒,然后才被浏览器正式处理;
- 浏览器要预先分配资源调度连接花费了11.56毫秒Stalled;
- 连接前必须要解析域名这里因为有本地缓存所以只消耗了0.41毫秒DNS Lookup
- 与网站服务器建立连接的成本很高总共花费了270.87毫秒其中有134.89毫秒用于TLS握手那么TCP握手的时间就是135.98毫秒Initial connection、SSL
- 实际发送数据非常快只用了0.11毫秒Request sent
- 之后就是等待服务器的响应专有名词叫TTFBTime To First Byte也就是“首字节响应时间”里面包括了服务器的处理时间和网络传输时间花了124.2毫秒;
- 接收数据也是非常快的用了3.58毫秒Content Dowload
从这张图你可以看到一次HTTP“请求-响应”的过程中延迟的时间是非常惊人的总时间415.04毫秒里占了差不多99%。
所以客户端HTTP性能优化的关键就是降低延迟。
## HTTP传输链路
以HTTP基本的“请求-应答”模型为出发点刚才我们得到了HTTP性能优化的一些指标现在我们来把视角放大到“真实的世界”看看客户端和服务器之间的传输链路它也是影响HTTP性能的关键。
还记得[第8讲](https://time.geekbang.org/column/article/100502)里的互联网示意图吗?我把它略微改了一下,划分出了几个区域,这就是所谓的“**第一公里**”“**中间一公里**”和“**最后一公里**”在英语原文中是mile英里
<img src="https://static001.geekbang.org/resource/image/50/32/5011b2998d2a0c58c87e31000d551732.png" alt="">
“第一公里”是指网站的出口,也就是服务器接入互联网的传输线路,它的带宽直接决定了网站对外的服务能力,也就是吞吐量等指标。显然,优化性能应该在这“第一公里”加大投入,尽量购买大带宽,接入更多的运营商网络。
“中间一公里”就是由许多小网络组成的实际的互联网其实它远不止“一公里”而是非常非常庞大和复杂的网络地理距离、网络互通都严重影响了传输速度。好在这里面有一个HTTP的“好帮手”——CDN它可以帮助网站跨越“千山万水”让这段距离看起来真的就好像只有“一公里”。
“最后一公里”是用户访问互联网的入口对于固网用户就是光纤、网线对于移动用户就是WiFi、基站。以前它是客户端性能的主要瓶颈延迟大带宽小但随着近几年4G和高速宽带的普及“最后一公里”的情况已经好了很多不再是制约性能的主要因素了。
除了这“三公里”,我个人认为还有一个“第零公里”, 就是网站内部的Web服务系统。它其实也是一个小型的网络当然也可能会非常大中间的数据处理、传输会导致延迟增加服务器的响应时间也是一个不可忽视的优化点。
在上面整个互联网传输链路中,末端的“最后一公里”我们是无法控制的,所以我们只能在“第零公里”“第一公里”和“中间一公里”这几个部分下功夫,增加带宽降低延迟,优化传输速度。
## 小结
1. 性能优化是一个复杂的概念在HTTP里可以分解为服务器性能优化、客户端性能优化和传输链路优化
1. 服务器有三个主要的性能指标:吞吐量、并发数和响应时间,此外还需要考虑资源利用率;
1. 客户端的基本性能指标是延迟影响因素有地理距离、带宽、DNS查询、TCP握手等
1. 从服务器到客户端的传输链路可以分为三个部分,我们能够优化的是前两个部分,也就是“第一公里”和“中间一公里”;
1. 有很多工具可以测量这些指标服务器端有ab、top、sar等客户端可以使用测试网站浏览器的开发者工具。
## 课下作业
1. 你有HTTP性能优化的经验吗常用的有哪些方法
1. 你是怎么理解客户端的“延迟”的?应该怎样降低延迟?
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/fb/32/fbc85df2c908cb8fa6bffde6ea989732.png" alt="unpreview">
<img src="https://static001.geekbang.org/resource/image/56/63/56d766fc04654a31536f554b8bde7b63.jpg" alt="unpreview">

View File

@@ -0,0 +1,123 @@
<audio id="audio" title="40 | HTTP性能优化面面观" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0d/73/0d24df4c27787e99c654ac8f28969873.mp3"></audio>
今天我们继续上次的话题看看HTTP性能优化有哪些行之有效的手段。
上一讲里我说到了在整个HTTP系统里有三个可优化的环节分别是**服务器**、**客户端**和**传输链路**“第一公里”和“中间一公里”。但因为我们是无法完全控制客户端的所以实际上的优化工作通常是在服务器端。这里又可以细分为后端和前端后端是指网站的后台服务而前端就是HTML、CSS、图片等展现在客户端的代码和数据。
知道了大致的方向HTTP性能优化具体应该怎么做呢
总的来说,任何计算机系统的优化都可以分成这么几类:硬件软件、内部外部、花钱不花钱。
**投资购买现成的硬件**最简单的优化方式比如换上更强的CPU、更快的网卡、更大的带宽、更多的服务器效果也会“立竿见影”直接提升网站的服务能力也就实现了HTTP优化。
另外,**花钱购买外部的软件或者服务**也是一种行之有效的优化方式最“物有所值”的应该算是CDN了参见[第37讲](https://time.geekbang.org/column/article/120664)。CDN专注于网络内容交付帮助网站解决“中间一公里”的问题还有很多其他非常专业的优化功能。把网站交给CDN运营就好像是“让网站坐上了喷气飞机”能够直达用户几乎不需要费什么力气就能够达成很好的优化效果。
不过这些“花钱”的手段实在是太没有“技术含量”了,属于“懒人”(无贬义)的做法,所以我就不再细说,接下来重点就讲讲在网站内部、“不花钱”的软件优化。
我把这方面的HTTP性能优化概括为三个关键词**开源**、**节流**、**缓存**。
## 开源
这个“开源”可不是Open Source而是指抓“源头”开发网站服务器自身的潜力在现有条件不变的情况下尽量挖掘出更多的服务能力。
首先我们应该选用高性能的Web服务器最佳选择当然就是Nginx/OpenResty了尽量不要选择基于Java、Python、Ruby的其他服务器它们用来做后面的业务逻辑服务器更好。利用Nginx强大的反向代理能力实现“动静分离”动态页面交给Tomcat、Django、Rails图片、样式表等静态资源交给Nginx。
Nginx或者OpenResty自身也有很多配置参数可以用来进一步调优举几个例子比如说禁用负载均衡锁、增大连接池绑定CPU等等相关的资料有很多。
特别要说的是对于HTTP协议一定要**启用长连接**。在[第39讲](https://time.geekbang.org/column/article/126374)里你也看到了TCP和SSL建立新连接的成本是非常高的有可能会占到客户端总延迟的一半以上。长连接虽然不能优化连接握手但可以把成本“均摊”到多次请求里这样只有第一次请求会有延迟之后的请求就不会有连接延迟总体的延迟也就降低了。
另外在现代操作系统上都已经支持TCP的新特性“**TCP Fast Open**”Win10、iOS9、Linux 4.1它的效果类似TLS的“False Start”可以在初次握手的时候就传输数据也就是0-RTT所以我们应该尽可能在操作系统和Nginx里开启这个特性减少外网和内网里的握手延迟。
下面给出一个简短的Nginx配置示例启用了长连接等优化参数实现了动静分离。
```
server {
listen 80 deferred reuseport backlog=4096 fastopen=1024;
keepalive_timeout 60;
keepalive_requests 10000;
location ~* \.(png)$ {
root /var/images/png/;
}
location ~* \.(php)$ {
proxy_pass http://php_back_end;
}
}
```
## 节流
“节流”是指减少客户端和服务器之间收发的数据量,在有限的带宽里传输更多的内容。
“节流”最基本的做法就是使用HTTP协议内置的“数据压缩”编码不仅可以选择标准的gzip还可以积极尝试新的压缩算法br它有更好的压缩效果。
不过在数据压缩的时候应当注意选择适当的压缩率,不要追求最高压缩比,否则会耗费服务器的计算资源,增加响应时间,降低服务能力,反而会“得不偿失”。
gzip和br是通用的压缩算法对于HTTP协议传输的各种格式数据我们还可以有针对性地采用特殊的压缩方式。
HTML/CSS/JavaScript属于纯文本就可以采用特殊的“压缩”去掉源码里多余的空格、换行、注释等元素。这样“压缩”之后的文本虽然看起来很混乱对“人类”不友好但计算机仍然能够毫无障碍地阅读不影响浏览器上的运行效果。
图片在HTTP传输里占有非常高的比例虽然它本身已经被压缩过了不能被gzip、br处理但仍然有优化的空间。比如说去除图片里的拍摄时间、地点、机型等元数据适当降低分辨率缩小尺寸。图片的格式也很关键尽量选择高压缩率的格式有损格式应该用JPEG无损格式应该用Webp格式。
对于小文本或者小图片还有一种叫做“资源合并”Concatenation的优化方式就是把许多小资源合并成一个大资源用一个请求全下载到客户端然后客户端再用JavaScript、CSS切分后使用好处是节省了请求次数但缺点是处理比较麻烦。
刚才说的几种数据压缩针对的都是HTTP报文里的body在HTTP/1里没有办法可以压缩header但我们也可以采取一些手段来减少header的大小不必要的字段就尽量不发例如Server、X-Powered-By
网站经常会使用Cookie来记录用户的数据浏览器访问网站时每次都会带上Cookie冗余度很高。所以应当少使用Cookie减少Cookie记录的数据量总使用domain和path属性限定Cookie的作用域尽可能减少Cookie的传输。如果客户端是现代浏览器还可以使用HTML5里定义的Web Local Storage避免使用Cookie。
压缩之外,“节流”还有两个优化点,就是**域名**和**重定向**。
DNS解析域名会耗费不少的时间如果网站拥有多个域名那么域名解析获取IP地址就是一个不小的成本所以应当适当“收缩”域名限制在两三个左右减少解析完整域名所需的时间让客户端尽快从系统缓存里获取解析结果。
重定向引发的客户端延迟也很高它不仅增加了一次请求往返还有可能导致新域名的DNS解析是HTTP前端性能优化的“大忌”。除非必要应当尽量不使用重定向或者使用Web服务器的“内部重定向”。
## 缓存
在[第20讲](https://time.geekbang.org/column/article/106804)里我就说到了“缓存”它不仅是HTTP也是任何计算机系统性能优化的“法宝”把它和上面的“开源”“节流”搭配起来应用于传输链路就能够让HTTP的性能再上一个台阶。
在“第零公里”也就是网站系统内部可以使用Memcache、Redis、Varnish等专门的缓存服务把计算的中间结果和资源存储在内存或者硬盘里Web服务器首先检查缓存系统如果有数据就立即返回给客户端省去了访问后台服务的时间。
在“中间一公里”缓存更是性能优化的重要手段CDN的网络加速功能就是建立在缓存的基础之上的可以这么说如果没有缓存那就没有CDN。
利用好缓存功能的关键是理解它的工作原理(参见[第20讲](https://time.geekbang.org/column/article/106804)和[第22讲](https://time.geekbang.org/column/article/108313)为每个资源都添加ETag和Last-modified字段再用Cache-Control、Expires设置好缓存控制属性。
其中最基本的是max-age有效期标记资源可缓存的时间。对于图片、CSS等静态资源可以设置较长的时间比如一天或者一个月对于动态资源除非是实时性非常高也可以设置一个较短的时间比如1秒或者5秒。
这样一旦资源到达客户端,就会被缓存起来,在有效期内都不会再向服务器发送请求,也就是:“**没有请求的请求,才是最快的请求。**”
## HTTP/2
在“开源”“节流”和“缓存”这三大策略之外HTTP性能优化还有一个选择那就是把协议由HTTP/1升级到HTTP/2。
通过“飞翔篇”的学习你已经知道了HTTP/2的很多优点它消除了应用层的队头阻塞拥有头部压缩、二进制帧、多路复用、流量控制、服务器推送等许多新特性大幅度提升了HTTP的传输效率。
实际上这些特性也是在“开源”和“节流”这两点上做文章但因为这些都已经内置在了协议内所以只要换上HTTP/2网站就能够立刻获得显著的性能提升。
不过你要注意一些在HTTP/1里的优化手段到了HTTP/2里会有“反效果”。
对于HTTP/2来说一个域名使用一个TCP连接才能够获得最佳性能如果开多个域名就会浪费带宽和服务器资源也会降低HTTP/2的效率所以“域名收缩”在HTTP/2里是必须要做的。
“资源合并”在HTTP/1里减少了多次请求的成本但在HTTP/2里因为有头部压缩和多路复用传输小文件的成本很低所以合并就失去了意义。而且“资源合并”还有一个缺点就是降低了缓存的可用性只要一个小文件更新整个缓存就完全失效必须重新下载。
所以在现在的大带宽和CDN应用场景下应当尽量少用资源合并JavaScript、CSS图片合并数据内嵌让资源的粒度尽可能地小才能更好地发挥缓存的作用。
## 小结
1. 花钱购买硬件、软件或者服务可以直接提升网站的服务能力其中最有价值的是CDN
1. 不花钱也可以优化HTTP三个关键词是“开源”“节流”和“缓存”
1. 后端应该选用高性能的Web服务器开启长连接提升TCP的传输效率
1. 前端应该启用gzip、br压缩减小文本、图片的体积尽量少传不必要的头字段
1. 缓存是无论何时都不能忘记的性能优化利器应该总使用Etag或Last-modified字段标记资源
1. 升级到HTTP/2能够直接获得许多方面的性能提升但要留意一些HTTP/1的“反模式”。
到这里,专栏的全部课程就学完了,在这三个月的时间里你是否有了很多的收获呢?
接下来,就请在广阔的网络世界里去实践这些知识吧,祝你成功!
<img src="https://static001.geekbang.org/resource/image/7b/8a/7b2351d7175e815710de646d53d7958a.png" alt="unpreview">

View File

@@ -0,0 +1,125 @@
<audio id="audio" title="34 | Nginx高性能的Web服务器" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c2/b4/c244add2f3ad05a959d875e667c336b4.mp3"></audio>
经过前面几大模块的学习你已经完全掌握了HTTP的所有知识那么接下来请收拾一下行囊整理一下装备跟我一起去探索HTTP之外的广阔天地。
现在的互联网非常发达用户越来越多网速越来越快HTTPS的安全加密、HTTP/2的多路复用等特性都对Web服务器提出了非常高的要求。一个好的Web服务器必须要具备稳定、快速、易扩展、易维护等特性才能够让网站“立于不败之地”。
那么,在搭建网站的时候,应该选择什么样的服务器软件呢?
在开头的几讲里我也提到过Web服务器就那么几款目前市面上主流的只有两个Apache和Nginx两者合计占据了近90%的市场份额。
今天我要说的就是其中的Nginx它是Web服务器的“后起之秀”虽然比Apache小了10岁但增长速度十分迅猛已经达到了与Apache“平起平坐”的地位而在“Top Million”网站中更是超过了Apache拥有超过50%的用户([参考数据](https://w3techs.com/technologies/cross/web_server/ranking))。
<img src="https://static001.geekbang.org/resource/image/c5/0b/c5df0592cc8aef91ba961f7fab5a4a0b.png" alt="unpreview">
在这里必须要说一下Nginx的正确发音它应该读成“Engine X”但我个人感觉“X”念起来太“拗口”还是比较倾向于读做“Engine ks”这也与UNIX、Linux的发音一致。
作为一个Web服务器Nginx的功能非常完善完美支持HTTP/1、HTTPS和HTTP/2而且还在不断进步。当前的主线版本已经发展到了1.17正在进行HTTP/3的研发或许一年之后就能在Nginx上跑HTTP/3了。
Nginx也是我个人的主要研究领域我也写过相关的书按理来说今天的课程应该是“手拿把攥”但真正动笔的时候还是有些犹豫的很多要点都已经在书里写过了这次的专栏如果再重复相同的内容就不免有“骗稿费”的嫌疑应该有些“不一样的东西”。
所以我决定抛开书本换个角度结合HTTP协议来讲Nginx带你窥视一下HTTP处理的内幕看看Web服务器的工作原理。
## 进程池
你也许听说过Nginx是个“轻量级”的Web服务器那么这个所谓的“轻量级”是什么意思呢
“轻量级”是相对于“重量级”而言的。“重量级”就是指服务器进程很“重”占用很多资源当处理HTTP请求时会消耗大量的CPU和内存受到这些资源的限制很难提高性能。
而Nginx作为“轻量级”的服务器它的CPU、内存占用都非常少同样的资源配置下就能够为更多的用户提供服务其奥秘在于它独特的工作模式。
<img src="https://static001.geekbang.org/resource/image/3e/c1/3e94fbd78ed043e88c443f6416f99dc1.png" alt="">
在Nginx之前Web服务器的工作模式大多是“Per-Process”或者“Per-Thread”对每一个请求使用单独的进程或者线程处理。这就存在创建进程或线程的成本还会有进程、线程“上下文切换”的额外开销。如果请求数量很多CPU就会在多个进程、线程之间切换时“疲于奔命”平白地浪费了计算时间。
Nginx则完全不同“一反惯例”地没有使用多线程而是使用了“**进程池+单线程**”的工作模式。
Nginx在启动的时候会预先创建好固定数量的worker进程在之后的运行过程中不会再fork出新进程这就是进程池而且可以自动把进程“绑定”到独立的CPU上这样就完全消除了进程创建和切换的成本能够充分利用多核CPU的计算能力。
在进程池之上还有一个“master”进程专门用来管理进程池。它的作用有点像是supervisor一个用Python编写的进程管理工具用来监控进程自动恢复发生异常的worker保持进程池的稳定和服务能力。
不过master进程完全是Nginx自行用C语言实现的这就摆脱了外部的依赖简化了Nginx的部署和配置。
## I/O多路复用
如果你用Java、C等语言写过程序一定很熟悉“多线程”的概念使用多线程能够很容易实现并发处理。
但多线程也有一些缺点,除了刚才说到的“上下文切换”成本,还有编程模型复杂、数据竞争、同步等问题,写出正确、快速的多线程程序并不是一件容易的事情。
所以Nginx就选择了单线程的方式带来的好处就是开发简单没有互斥锁的成本减少系统消耗。
那么疑问也就产生了为什么单线程的Nginx处理能力却能够超越其他多线程的服务器呢
这要归功于Nginx利用了Linux内核里的一件“神兵利器”**I/O多路复用接口**“大名鼎鼎”的epoll。
“多路复用”这个词我们已经在之前的HTTP/2、HTTP/3里遇到过好几次如果你理解了那里的“多路复用”那么面对Nginx的epoll“多路复用”也就好办了。
Web服务器从根本上来说是“I/O密集型”而不是“CPU密集型”处理能力的关键在于网络收发而不是CPU计算这里暂时不考虑HTTPS的加解密而网络I/O会因为各式各样的原因不得不等待比如数据还没到达、对端没有响应、缓冲区满发不出去等等。
这种情形就有点像是HTTP里的“队头阻塞”。对于一般的单线程来说CPU就会“停下来”造成浪费。而多线程的解决思路有点类似“并发连接”虽然有的线程可能阻塞但由于多个线程并行总体上看阻塞的情况就不会太严重了。
Nginx里使用的epoll就好像是HTTP/2里的“多路复用”技术它把多个HTTP请求处理打散成碎片都“复用”到一个单线程里不按照先来后到的顺序处理而是只当连接上真正可读、可写的时候才处理如果可能发生阻塞就立刻切换出去处理其他的请求。
通过这种方式Nginx就完全消除了I/O阻塞把CPU利用得“满满当当”又因为网络收发并不会消耗太多CPU计算能力也不需要切换进程、线程所以整体的CPU负载是相当低的。
这里我画了一张Nginx“I/O多路复用”的示意图你可以看到它的形式与HTTP/2的流非常相似每个请求处理单独来看是分散、阻塞的但因为都复用到了一个线程里所以资源的利用率非常高。
<img src="https://static001.geekbang.org/resource/image/4c/59/4c6832cdce34133c9ed89237fb9d5059.png" alt="">
epoll还有一个特点大量的连接管理工作都是在操作系统内核里做的这就减轻了应用程序的负担所以Nginx可以为每个连接只分配很小的内存维护状态即使有几万、几十万的并发连接也只会消耗几百M内存而其他的Web服务器这个时候早就“Memory not enough”了。
## 多阶段处理
有了“进程池”和“I/O多路复用”Nginx是如何处理HTTP请求的呢
Nginx在内部也采用的是“**化整为零**”的思路把整个Web服务器分解成了多个“功能模块”就好像是乐高积木可以在配置文件里任意拼接搭建从而实现了高度的灵活性和扩展性。
Nginx的HTTP处理有四大类模块
1. handler模块直接处理HTTP请求
1. filter模块不直接处理请求而是加工过滤响应报文
1. upstream模块实现反向代理功能转发请求到其他服务器
1. balance模块实现反向代理时的负载均衡算法。
因为upstream模块和balance模块实现的是代理功能Nginx作为“中间人”运行机制比较复杂所以我今天只讲handler模块和filter模块。
不知道你有没有了解过“设计模式”这方面的知识,其中有一个非常有用的模式叫做“**职责链**”。它就好像是工厂里的流水线,原料从一头流入,线上有许多工人会进行各种加工处理,最后从另一头出来的就是完整的产品。
Nginx里的handler模块和filter模块就是按照“职责链”模式设计和组织的HTTP请求报文就是“原材料”各种模块就是工厂里的工人走完模块构成的“流水线”出来的就是处理完成的响应报文。
下面的这张图显示了Nginx的“流水线”在Nginx里的术语叫“阶段式处理”Phases一共有11个阶段每个阶段里又有许多各司其职的模块。
<img src="https://static001.geekbang.org/resource/image/41/30/41318c867fda8a536d0e3db6f9987030.png" alt="">
我简单列几个与我们的课程相关的模块吧:
- charset模块实现了字符集编码转换[第15讲](https://time.geekbang.org/column/article/104024)
- chunked模块实现了响应数据的分块传输[第16讲](https://time.geekbang.org/column/article/104456)
- range模块实现了范围请求只返回数据的一部分[第16讲](https://time.geekbang.org/column/article/104456)
- rewrite模块实现了重定向和跳转还可以使用内置变量自定义跳转的URI[第18讲](https://time.geekbang.org/column/article/105614)
- not_modified模块检查头字段“if-Modified-Since”和“If-None-Match”处理条件请求[第20讲](https://time.geekbang.org/column/article/106804)
- realip模块处理“X-Real-IP”“X-Forwarded-For”等字段获取客户端的真实IP地址[第21讲](https://time.geekbang.org/column/article/107577)
- ssl模块实现了SSL/TLS协议支持读取磁盘上的证书和私钥实现TLS握手和SNI、ALPN等扩展功能[安全篇](https://time.geekbang.org/column/article/108643)
- http_v2模块实现了完整的HTTP/2协议。[飞翔篇](https://time.geekbang.org/column/article/112036)
在这张图里你还可以看到limit_conn、limit_req、access、log等其他模块它们实现的是限流限速、访问控制、日志等功能不在HTTP协议规定之内但对于运行在现实世界的Web服务器却是必备的。
如果你有C语言基础感兴趣的话可以下载Nginx的源码在代码级别仔细看看HTTP的处理过程。
## 小结
1. Nginx是一个高性能的Web服务器它非常的轻量级消耗的CPU、内存很少
1. Nginx采用“master/workers”进程池架构不使用多线程消除了进程、线程切换的成本
1. Nginx基于epoll实现了“I/O多路复用”不会阻塞所以性能很高
1. Nginx使用了“职责链”模式多个模块分工合作自由组合以流水线的方式处理HTTP请求。
## 课下作业
1. 你是怎么理解进程、线程上下文切换时的成本的为什么Nginx要尽量避免
1. 试着自己描述一下Nginx用进程、epoll、模块流水线处理HTTP请求的过程。
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/4c/3d/4c7bceb80a8027389705e9d6ec9eb43d.png" alt="unpreview">

View File

@@ -0,0 +1,130 @@
<audio id="audio" title="35 | OpenResty更灵活的Web服务器" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7d/68/7d395f67094c2bfa140ee2d100996168.mp3"></audio>
在上一讲里我们看到了高性能的Web服务器Nginx它资源占用少处理能力高是搭建网站的首选。
虽然Nginx成为了Web服务器领域无可争议的“王者”但它也并不是没有缺点的毕竟它已经15岁了。
“一个人很难超越时代而时代却可以轻易超越所有人”Nginx当初设计时针对的应用场景已经发生了变化它的一些缺点也就暴露出来了。
Nginx的服务管理思路延续了当时的流行做法使用磁盘上的静态配置文件所以每次修改后必须重启才能生效。
这在业务频繁变动的时候是非常致命的(例如流行的微服务架构),特别是对于拥有成千上万台服务器的网站来说,仅仅增加或者删除一行配置就要分发、重启所有的机器,对运维是一个非常大的挑战,要耗费很多的时间和精力,成本很高,很不灵活,难以“随需应变”。
那么有没有这样的一个Web服务器它有Nginx的优点却没有Nginx的缺点既轻量级、高性能又灵活、可动态配置呢
这就是我今天要说的OpenResty它是一个“更好更灵活的Nginx”。
## OpenResty是什么
其实你对OpenResty并不陌生这个专栏的实验环境就是用OpenResty搭建的这么多节课程下来你应该或多或少对它有了一些印象吧。
OpenResty诞生于2009年到现在刚好满10周岁。它的创造者是当时就职于某宝的“神级”程序员**章亦春**网名叫“agentzh”。
OpenResty并不是一个全新的Web服务器而是基于Nginx它利用了Nginx模块化、可扩展的特性开发了一系列的增强模块并把它们打包整合形成了一个**“一站式”的Web开发平台**。
虽然OpenResty的核心是Nginx但它又超越了Nginx关键就在于其中的ngx_lua模块把小巧灵活的Lua语言嵌入了Nginx可以用脚本的方式操作Nginx内部的进程、多路复用、阶段式处理等各种构件。
脚本语言的好处你一定知道它不需要编译随写随执行这就免去了C语言编写模块漫长的开发周期。而且OpenResty还把Lua自身的协程与Nginx的事件机制完美结合在一起优雅地实现了许多其他语言所没有的“**同步非阻塞**”编程范式能够轻松开发出高性能的Web应用。
目前OpenResty有两个分支分别是开源、免费的“OpenResty”和闭源、商业产品的“OpenResty+”运作方式有社区支持、OpenResty基金会、OpenResty.Inc公司还有其他的一些外界赞助例如Kong、CloudFlare正在蓬勃发展。
<img src="https://static001.geekbang.org/resource/image/9f/01/9f7b79c43c476890f03c2c716a20f301.png" alt="unpreview">
顺便说一下OpenResty的官方logo是一只展翅飞翔的海鸥选择海鸥是因为“鸥”与OpenResty的发音相同。另外这个logo的形状也像是左手比出的一个“OK”姿势正好也是一个“O”。
## 动态的Lua
刚才说了OpenResty里的一个关键模块是ngx_lua它为Nginx引入了脚本语言Lua。
Lua是一个比较“小众”的语言虽然历史比较悠久但名气却没有PHP、Python、JavaScript大这主要与它的自身定位有关。
<img src="https://static001.geekbang.org/resource/image/4f/d5/4f24aa3f53969b71baaf7d9c7cf68fd5.png" alt="unpreview">
Lua的设计目标是嵌入到其他应用程序里运行为其他编程语言带来“脚本化”能力所以它的“个头”比较小功能集有限不追求“大而全”而是“小而美”大多数时间都“隐匿”在其他应用程序的后面是“无名英雄”。
你或许玩过或者听说过《魔兽世界》《愤怒的小鸟》吧它们就在内部嵌入了Lua使用Lua来调用底层接口充当“胶水语言”glue language编写游戏逻辑脚本提高开发效率。
OpenResty选择Lua作为“工作语言”也是基于同样的考虑。因为Nginx C开发实在是太麻烦了限制了Nginx的真正实力。而Lua作为“最快的脚本语言”恰好可以成为Nginx的完美搭档既可以简化开发性能上又不会有太多的损耗。
作为脚本语言Lua还有一个重要的“**代码热加载**”特性不需要重启进程就能够从磁盘、Redis或者任何其他地方加载数据随时替换内存里的代码片段。这就带来了“**动态配置**”让OpenResty能够永不停机在微秒、毫秒级别实现配置和业务逻辑的实时更新比起Nginx秒级的重启是一个极大的进步。
你可以看一下实验环境的“www/lua”目录里面存放了我写的一些测试HTTP特性的Lua脚本代码都非常简单易懂就像是普通的英语“阅读理解”这也是Lua的另一个优势易学习、易上手。
## 高效率的Lua
OpenResty能够高效运行的一大“秘技”是它的“**同步非阻塞**”编程范式如果你要开发OpenResty应用就必须时刻铭记于心。
“同步非阻塞”本质上还是一种“**多路复用**”我拿上一讲的Nginx epoll来对比解释一下。
epoll是操作系统级别的“多路复用”运行在内核空间。而OpenResty的“同步非阻塞”则是基于Lua内建的“**协程**”,是应用程序级别的“多路复用”,运行在用户空间,所以它的资源消耗要更少。
OpenResty里每一段Lua程序都由协程来调度运行。和Linux的epoll一样每当可能发生阻塞的时候“协程”就会立刻切换出去执行其他的程序。这样单个处理流程是“阻塞”的但整个OpenResty却是“非阻塞的”多个程序都“复用”在一个Lua虚拟机里运行。
<img src="https://static001.geekbang.org/resource/image/9f/c6/9fc3df52df7d6c11aa02b8013f8e9bc6.png" alt="">
下面的代码是一个简单的例子读取POST发送的body数据然后再发回客户端
```
ngx.req.read_body() -- 同步非阻塞(1)
local data = ngx.req.get_body_data()
if data then
ngx.print(&quot;body: &quot;, data) -- 同步非阻塞(2)
end
```
代码中的“ngx.req.read_body”和“ngx.print”分别是数据的收发动作只有收到数据才能发送数据所以是“同步”的。
但即使因为网络原因没收到或者发不出去OpenResty也不会在这里阻塞“干等着”而是做个“记号”把等待的这段CPU时间用来处理其他的请求等网络可读或者可写时再“回来”接着运行。
假设收发数据的等待时间是10毫秒而真正CPU处理的时间是0.1毫秒那么OpenResty就可以在这10毫秒内同时处理100个请求而不是把这100个请求阻塞排队用1000毫秒来处理。
除了“同步非阻塞”OpenResty还选用了**LuaJIT**作为Lua语言的“运行时Runtime进一步“挖潜增效”。
LuaJIT是一个高效的Lua虚拟机支持JITJust In Time技术可以把Lua代码即时编译成“本地机器码”这样就消除了脚本语言解释运行的劣势让Lua脚本跑得和原生C代码一样快。
另外LuaJIT还为Lua语言添加了一些特别的增强比如二进制位运算库bit内存优化库table还有FFIForeign Function Interface让Lua直接调用底层C函数比原生的压栈调用快很多。
## 阶段式处理
和Nginx一样OpenResty也使用“流水线”来处理HTTP请求底层的运行基础是Nginx的“阶段式处理”但它又有自己的特色。
Nginx的“流水线”是由一个个C模块组成的只能在静态文件里配置开发困难配置麻烦相对而言。而OpenResty的“流水线”则是由一个个的Lua脚本组成的不仅可以从磁盘上加载也可以从Redis、MySQL里加载而且编写、调试的过程非常方便快捷。
下面我画了一张图列出了OpenResty的阶段比起NginxOpenResty的阶段更注重对HTTP请求响应报文的加工和处理。
<img src="https://static001.geekbang.org/resource/image/36/df/3689312a970bae0e949b017ad45438df.png" alt="">
OpenResty里有几个阶段与Nginx是相同的比如rewrite、access、content、filter这些都是标准的HTTP处理。
在这几个阶段里可以用“xxx_by_lua”指令嵌入Lua代码执行重定向跳转、访问控制、产生响应、负载均衡、过滤报文等功能。因为Lua的脚本语言特性不用考虑内存分配、资源回收释放等底层的细节问题可以专注于编写非常复杂的业务逻辑比C模块的开发效率高很多即易于扩展又易于维护。
OpenResty里还有两个不同于Nginx的特殊阶段。
一个是“**init阶段**”它又分成“master init”和“worker init”在master进程和worker进程启动的时候运行。这个阶段还没有开始提供服务所以慢一点也没关系可以调用一些阻塞的接口初始化服务器比如读取磁盘、MySQL加载黑白名单或者数据模型然后放进共享内存里供运行时使用。
另一个是“**ssl阶段**”这算得上是OpenResty的一大创举可以在TLS握手时动态加载证书或者发送“OCSP Stapling”。
还记得[第29讲](https://time.geekbang.org/column/article/111940)里说的“SNI扩展”吗Nginx可以依据“服务器名称指示”来选择证书实现HTTPS虚拟主机但静态配置很不灵活要编写很多雷同的配置块。虽然后来Nginx增加了变量支持但它每次握手都要读磁盘效率很低。
而在OpenResty里就可以使用指令“ssl_certificate_by_lua”编写Lua脚本读取SNI名字后直接从共享内存或者Redis里获取证书。不仅没有读盘阻塞而且证书也是完全动态可配置的无需修改配置文件就能够轻松支持大量的HTTPS虚拟主机。
## 小结
1. Nginx依赖于磁盘上的静态配置文件修改后必须重启才能生效缺乏灵活性
1. OpenResty基于Nginx打包了很多有用的模块和库是一个高性能的Web开发平台
1. OpenResty的工作语言是Lua它小巧灵活执行效率高支持“代码热加载”
1. OpenResty的核心编程范式是“同步非阻塞”使用协程不需要异步回调函数
1. OpenResty也使用“阶段式处理”的工作模式但因为在阶段里执行的都是Lua代码所以非常灵活配合Redis等外部数据库能够实现各种动态配置。
## 课下作业
1. 谈一下这些天你对实验环境里OpenResty的感想和认识。
1. 你觉得Nginx和OpenResty的“阶段式处理”有什么好处对你的实际工作有没有启发
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/c5/9f/c5b7ac40c585c800af0fe3ab98f3449f.png" alt="unpreview">

View File

@@ -0,0 +1,182 @@
<audio id="audio" title="36 | WAF保护我们的网络服务" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f5/0f/f53e1775cb784eeb10197f3bd6fa1b0f.mp3"></audio>
在前些天的“安全篇”里我谈到了HTTPS它使用了SSL/TLS协议加密整个通信过程能够防止恶意窃听和窜改保护我们的数据安全。
但HTTPS只是网络安全中很小的一部分仅仅保证了“通信链路安全”让第三方无法得知传输的内容。在通信链路的两端也就是客户端和服务器它是无法提供保护的。
因为HTTP是一个开放的协议Web服务都运行在公网上任何人都可以访问所以天然就会成为黑客的攻击目标。
而且黑客的本领比我们想象的还要大得多。虽然不能在传输过程中做手脚,但他们还可以“假扮”成合法的用户访问系统,然后伺机搞破坏。
## Web服务遇到的威胁
黑客都有哪些手段来攻击Web服务呢我给你大概列出几种常见的方式。
第一种叫“**DDoS**”攻击distributed denial-of-service attack有时候也叫“洪水攻击”。
黑客会控制许多“僵尸”计算机向目标服务器发起大量无效请求。因为服务器无法区分正常用户和黑客只能“照单全收”这样就挤占了正常用户所应有的资源。如果黑客的攻击强度很大就会像“洪水”一样对网站的服务能力造成冲击耗尽带宽、CPU和内存导致网站完全无法提供正常服务。
“DDoS”攻击方式比较“简单粗暴”虽然很有效但不涉及HTTP协议内部的细节“技术含量”比较低不过下面要说的几种手段就不一样了。
网站后台的Web服务经常会提取出HTTP报文里的各种信息应用于业务有时会缺乏严格的检查。因为HTTP报文在语义结构上非常松散、灵活URI里的query字符串、头字段、body数据都可以任意设置这就带来了安全隐患给了黑客“**代码注入**”的可能性。
黑客可以精心编制HTTP请求报文发送给服务器服务程序如果没有做防备就会“上当受骗”执行黑客设定的代码。
“**SQL注入**”SQL injection应该算是最著名的一种“代码注入”攻击了它利用了服务器字符串拼接形成SQL语句的漏洞构造出非正常的SQL语句获取数据库内部的敏感信息。
另一种“**HTTP头注入**”攻击的方式也是类似的原理它在“Host”“User-Agent”“X-Forwarded-For”等字段里加入了恶意数据或代码服务端程序如果解析不当就会执行预设的恶意代码。
在之前的[第19讲](https://time.geekbang.org/column/article/106034)里也说过一种利用Cookie的攻击手段“**跨站脚本**”XSS攻击它属于“JS代码注入”利用JavaScript脚本获取未设防的Cookie。
## 网络应用防火墙
面对这么多的黑客攻击手段,我们应该怎么防御呢?
这就要用到“**网络应用防火墙**”Web Application Firewall简称为“**WAF**”。
你可能对传统的“防火墙”比较熟悉。传统“防火墙”工作在三层或者四层隔离了外网和内网使用预设的规则只允许某些特定IP地址和端口号的数据包通过拒绝不符合条件的数据流入或流出内网实质上是**一种网络数据过滤设备**。
WAF也是一种“防火墙”但它工作在七层看到的不仅是IP地址和端口号还能看到整个HTTP报文所以就能够对报文内容做更深入细致的审核使用更复杂的条件、规则来过滤数据。
说白了WAF就是一种“**HTTP入侵检测和防御系统**”。
<img src="https://static001.geekbang.org/resource/image/e8/a3/e8369d077454e5b92e3722e7090551a3.png" alt="">
WAF都能干什么呢
通常一款产品能够称为WAF要具备下面的一些功能
- IP黑名单和白名单拒绝黑名单上地址的访问或者只允许白名单上的用户访问
- URI黑名单和白名单与IP黑白名单类似允许或禁止对某些URI的访问
- 防护DDoS攻击对特定的IP地址限连限速
- 过滤请求报文,防御“代码注入”攻击;
- 过滤响应报文,防御敏感信息外泄;
- 审计日志,记录所有检测到的入侵操作。
听起来WAF好像很高深但如果你理解了它的工作原理其实也不难。
它就像是平时编写程序时必须要做的函数入口参数检查拿到HTTP请求、响应报文用字符串处理函数看看有没有关键字、敏感词或者用正则表达式做一下模式匹配命中了规则就执行对应的动作比如返回403/404。
如果你比较熟悉Apache、Nginx、OpenResty可以自己改改配置文件写点JS或者Lua代码就能够实现基本的WAF功能。
比如说在Nginx里实现IP地址黑名单可以利用“map”指令从变量$remote_addr获取IP地址在黑名单上就映射为值1然后在“if”指令里判断
```
map $remote_addr $blocked {
default 0;
&quot;1.2.3.4&quot; 1;
&quot;5.6.7.8&quot; 1;
}
if ($blocked) {
return 403 &quot;you are blocked.&quot;;
}
```
Nginx的配置文件只能静态加载改名单必须重启比较麻烦。如果换成OpenResty就会非常方便在access阶段进行判断IP地址列表可以使用cosocket连接外部的Redis、MySQL等数据库实现动态更新
```
local ip_addr = ngx.var.remote_addr
local rds = redis:new()
if rds:get(ip_addr) == 1 then
ngx.exit(403)
end
```
看了上面的两个例子你是不是有种“跃跃欲试”的冲动了想自己动手开发一个WAF
不过我必须要提醒你,在网络安全领域必须时刻记得“**木桶效应**”(也叫“短板效应”)。网站的整体安全不在于你加固的最强的那个方向,而是在于你可能都没有意识到的“短板”。黑客往往会“避重就轻”,只要发现了网站的一个弱点,就可以“一点突破”,其他方面的安全措施也就都成了“无用功”。
所以使用WAF最好“**不要重新发明轮子**”而是使用现有的、比较成熟的、经过实际考验的WAF产品。
## 全面的WAF解决方案
这里我就要“隆重”介绍一下WAF领域里的最顶级产品了ModSecurity它可以说是WAF界“事实上的标准”。
ModSecurity是一个开源的、生产级的WAF工具包历史很悠久比Nginx还要大几岁。它开始于一个私人项目后来被商业公司Breach Security收购现在则是由TrustWave公司的SpiderLabs团队负责维护。
ModSecurity最早是Apache的一个模块只能运行在Apache上。因为其品质出众大受欢迎后来的2.x版添加了Nginx和IIS支持但因为底层架构存在差异不够稳定。
所以这两年SpiderLabs团队就开发了全新的3.0版本移除了对Apache架构的依赖使用新的“连接器”来集成进Apache或者Nginx比2.x版更加稳定和快速误报率也更低。
ModSecurity有两个核心组件。第一个是“**规则引擎**”它实现了自定义的“SecRule”语言有自己特定的语法。但“SecRule”主要基于正则表达式还是不够灵活所以后来也引入了Lua实现了脚本化配置。
ModSecurity的规则引擎使用C++11实现可以从[GitHub](https://github.com/SpiderLabs/ModSecurity)上下载源码然后集成进Nginx。因为它比较庞大编译很费时间所以最好编译成动态模块在配置文件里用指令“load_module”加载
```
load_module modules/ngx_http_modsecurity_module.so;
```
只有引擎还不够要让引擎运转起来还需要完善的防御规则所以ModSecurity的第二个核心组件就是它的“**规则集**”。
ModSecurity源码提供一个基本的规则配置文件“**modsecurity.conf-recommended**”使用前要把它的后缀改成“conf”。
有了规则集就可以在Nginx配置文件里加载然后启动规则引擎
```
modsecurity on;
modsecurity_rules_file /path/to/modsecurity.conf;
```
“modsecurity.conf”文件默认只有检测功能不提供入侵阻断这是为了防止误杀误报把“SecRuleEngine”后面改成“On”就可以开启完全的防护
```
#SecRuleEngine DetectionOnly
SecRuleEngine On
```
基本的规则集之外ModSecurity还额外提供一个更完善的规则集为网站提供全面可靠的保护。这个规则集的全名叫“**OWASP ModSecurity 核心规则集**”Open Web Application Security Project ModSecurity Core Rule Set因为名字太长了所以有时候会简称为“核心规则集”或者“CRS”。
<img src="https://static001.geekbang.org/resource/image/ad/48/add929f8439c64f29db720d30f7de548.png" alt="">
CRS也是完全开源、免费的可以从GitHub上下载
```
git clone https://github.com/SpiderLabs/owasp-modsecurity-crs.git
```
其中有一个“**crs-setup.conf.example**”的文件它是CRS的基本配置可以用“Include”命令添加到“modsecurity.conf”里然后再添加“rules”里的各种规则。
```
Include /path/to/crs-setup.conf
Include /path/to/rules/*.conf
```
你如果有兴趣可以看一下这些配置文件里面用“SecRule”定义了很多的规则基本的形式是“SecRule 变量 运算符 动作”。不过ModSecurity的这套语法“自成一体”比较复杂要完全掌握不是一朝一夕的事情我就不详细解释了。
另外ModSecurity还有强大的审计日志Audit Log功能记录任何可疑的数据供事后离线分析。但在生产环境中会遇到大量的攻击日志会快速增长消耗磁盘空间而且写磁盘也会影响Nginx的性能所以一般建议把它关闭
```
SecAuditEngine off #RelevantOnly
SecAuditLog /var/log/modsec_audit.log
```
## 小结
今天我们一起学习了“网络应用防火墙”也就是WAF使用它可以加固Web服务。
1. Web服务通常都运行在公网上容易受到“DDoS”、“代码注入”等各种黑客攻击影响正常的服务所以必须要采取措施加以保护
1. WAF是一种“HTTP入侵检测和防御系统”工作在七层为Web服务提供全面的防护
1. ModSecurity是一个开源的、生产级的WAF产品核心组成部分是“规则引擎”和“规则集”两者的关系有点像杀毒引擎和病毒特征库
1. WAF实质上是模式匹配与数据过滤所以会消耗CPU增加一些计算成本降低服务能力使用时需要在安全与性能之间找到一个“平衡点”。
## 课下作业
1. HTTPS为什么不能防御DDoS、代码注入等攻击呢
1. 你还知道有哪些手段能够抵御网络攻击吗?
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/b9/24/b9e48b813c98bb34b4b433b7326ace24.png" alt="unpreview">

View File

@@ -0,0 +1,120 @@
<audio id="audio" title="37 | CDN加速我们的网络服务" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/44/4f/44e3c0a62c765e9df59d0447ec226a4f.mp3"></audio>
在正式开讲前我们先来看看到现在为止HTTP手头都有了哪些“武器”。
协议方面HTTPS强化通信链路安全、HTTP/2优化传输效率应用方面Nginx/OpenResty提升网站服务能力WAF抵御网站入侵攻击讲到这里你是不是感觉还少了点什么
没错在应用领域还缺一个在外部加速HTTP协议的服务这个就是我们今天要说的CDNContent Delivery Network或Content Distribution Network中文名叫“内容分发网络”。
## 为什么要有网络加速?
你可能要问了HTTP的传输速度也不算差啊而且还有更好的HTTP/2为什么还要再有一个额外的CDN来加速呢是不是有点“多此一举”呢
这里我们就必须要考虑现实中会遇到的问题了。你一定知道光速是有限的虽然每秒30万公里但这只是真空中的上限在实际的电缆、光缆中的速度会下降到原本的三分之二左右也就是20万公里/秒,这样一来,地理位置的距离导致的传输延迟就会变得比较明显了。
比如北京到广州直线距离大约是2000公里按照刚才的20万公里/秒来算的话发送一个请求单程就要10毫秒往返要20毫秒即使什么都不干这个“硬性”的时延也是躲不过的。
另外不要忘了, 互联网从逻辑上看是一张大网,但实际上是由许多小网络组成的,这其中就有小网络“互连互通”的问题,典型的就是各个电信运营商的网络,比如国内的电信、联通、移动三大家。
<img src="https://static001.geekbang.org/resource/image/41/b9/413605355db69278cb137b318b70b3b9.png" alt="">
这些小网络内部的沟通很顺畅但网络之间却只有很少的联通点。如果你在A网络而网站在C网络那么就必须“跨网”传输和成千上万的其他用户一起去“挤”连接点的“独木桥”。而带宽终究是有限的能抢到多少只能看你的运气。
还有,网络中还存在许多的路由器、网关,数据每经过一个节点,都要停顿一下,在二层、三层解析转发,这也会消耗一定的时间,带来延迟。
把这些因素再放到全球来看,地理距离、运营商网络、路由转发的影响就会成倍增加。想象一下,你在北京,访问旧金山的网站,要跨越半个地球,中间会有多少环节,会增加多少时延?
最终结果就是如果仅用现有的HTTP传输方式大多数网站都会访问速度缓慢、用户体验糟糕。
## 什么是CDN
这个时候CDN就出现了它就是专门为解决“长距离”上网络访问速度慢而诞生的一种网络应用服务。
从名字上看CDN有三个关键词“**内容**”“**分发**”和“**网络**”。
先看一下“网络”的含义。CDN的最核心原则是“**就近访问**”如果用户能够在本地几十公里的距离之内获取到数据那么时延就基本上变成0了。
所以CDN投入了大笔资金在全国、乃至全球的各个大枢纽城市都建立了机房部署了大量拥有高存储高带宽的节点构建了一个专用网络。这个网络是跨运营商、跨地域的虽然内部也划分成多个小网络但它们之间用高速专有线路连接是真正的“信息高速公路”基本上可以认为不存在网络拥堵。
有了这个高速的专用网之后CDN就要“分发”源站的“内容”了用到的就是在[第22讲](https://time.geekbang.org/column/article/108313)说过的“**缓存代理**”技术。使用“推”或者“拉”的手段,把源站的内容逐级缓存到网络的每一个节点上。
于是用户在上网的时候就不直接访问源站而是访问离他“最近的”一个CDN节点术语叫“**边缘节点**”edge node其实就是缓存了源站内容的代理服务器这样一来就省去了“长途跋涉”的时间成本实现了“网络加速”。
<img src="https://static001.geekbang.org/resource/image/46/5b/46d1dbbb545fcf3cfb53407e0afe9a5b.png" alt="">
那么CDN都能加速什么样的“内容”呢
在CDN领域里“内容”其实就是HTTP协议里的“资源”比如超文本、图片、视频、应用程序安装包等等。
资源按照是否可缓存又分为“**静态资源**”和“**动态资源**”。所谓的“静态资源”是指数据内容“静态不变”,任何时候来访问都是一样的,比如图片、音频。所谓的“动态资源”是指数据内容是“动态变化”的,也就是由后台服务计算生成的,每次访问都不一样,比如商品的库存、微博的粉丝数等。
很显然只有静态资源才能够被缓存加速、就近访问而动态资源只能由源站实时生成即使缓存了也没有意义。不过如果动态资源指定了“Cache-Control”允许缓存短暂的时间那它在这段时间里也就变成了“静态资源”可以被CDN缓存加速。
套用一句广告词来形容CDN吧我觉得非常恰当“**我们不生产内容,我们只是内容的搬运工。**”
CDN正是把“数据传输”这件看似简单的事情“做大做强”“做专做精”就像专门的快递公司一样在互联网世界里实现了它的价值。
## CDN的负载均衡
我们再来看看CDN是具体怎么运行的它有两个关键组成部分**全局负载均衡**和**缓存系统**对应的是DNS[第6讲](https://time.geekbang.org/column/article/99665))和缓存代理([第21讲](https://time.geekbang.org/column/article/107577)、[第22讲](https://time.geekbang.org/column/article/108313))技术。
全局负载均衡Global Sever Load Balance一般简称为GSLB它是CDN的“大脑”主要的职责是当用户接入网络的时候在CDN专网中挑选出一个“最佳”节点提供服务解决的是用户如何找到“最近的”边缘节点对整个CDN网络进行“负载均衡”。
<img src="https://static001.geekbang.org/resource/image/6c/ca/6c39e76d58d9f17872c83ae72908faca.png" alt="">
GSLB最常见的实现方式是“**DNS负载均衡**”,这个在[第6讲](https://time.geekbang.org/column/article/99665)里也说过不过GSLB的方式要略微复杂一些。
原来没有CDN的时候权威DNS返回的是网站自己服务器的实际IP地址浏览器收到DNS解析结果后直连网站。
但加入CDN后就不一样了权威DNS返回的不是IP地址而是一个CNAME( Canonical Name )别名记录指向的就是CDN的GSLB。它有点像是HTTP/2里“Alt-Svc”的意思告诉外面“我这里暂时没法给你真正的地址你去另外一个地方再查查看吧。”
因为没拿到IP地址于是本地DNS就会向GSLB再发起请求这样就进入了CDN的全局负载均衡系统开始“智能调度”主要的依据有这么几个
1. 看用户的IP地址查表得知地理位置找相对最近的边缘节点
1. 看用户所在的运营商网络,找相同网络的边缘节点;
1. 检查边缘节点的负载情况,找负载较轻的节点;
1. 其他,比如节点的“健康状况”、服务能力、带宽、响应时间等。
GSLB把这些因素综合起来用一个复杂的算法最后找出一台“最合适”的边缘节点把这个节点的IP地址返回给用户用户就可以“就近”访问CDN的缓存代理了。
## CDN的缓存代理
缓存系统是CDN的另一个关键组成部分相当于CDN的“心脏”。如果缓存系统的服务能力不够不能很好地满足用户的需求那GSLB调度算法再优秀也没有用。
但互联网上的资源是无穷无尽的不管CDN厂商有多大的实力也不可能把所有资源都缓存起来。所以缓存系统只能有选择地缓存那些最常用的那些资源。
这里就有两个CDN的关键概念“**命中**”和“**回源**”。
“命中”就是指用户访问的资源恰好在缓存系统里,可以直接返回给用户;“回源”则正相反,缓存里没有,必须用代理的方式回源站取。
相应地也就有了两个衡量CDN服务质量的指标“**命中率**”和“**回源率**”。命中率就是命中次数与所有访问次数之比回源率是回源次数与所有访问次数之比。显然好的CDN应该是命中率越高越好回源率越低越好。现在的商业CDN命中率都在90%以上相当于把源站的服务能力放大了10倍以上。
怎么样才能尽可能地提高命中率、降低回源率呢?
首先最基本的方式就是在存储系统上下功夫硬件用高速CPU、大内存、万兆网卡再搭配TB级别的硬盘和快速的SSD。软件方面则不断“求新求变”各种新的存储软件都会拿来尝试比如Memcache、Redis、Ceph尽可能地高效利用存储存下更多的内容。
其次,缓存系统也可以划分出层次,分成一级缓存节点和二级缓存节点。一级缓存配置高一些,直连源站,二级缓存配置低一些,直连用户。回源的时候二级缓存只找一级缓存,一级缓存没有才回源站,这样最终“扇入度”就缩小了,可以有效地减少真正的回源。
第三个就是使用高性能的缓存服务据我所知目前国内的CDN厂商内部都是基于开源软件定制的。最常用的是专门的缓存代理软件Squid、Varnish还有新兴的ATSApache Traffic Server而Nginx和OpenResty作为Web服务器领域的“多面手”凭借着强大的反向代理能力和模块化、易于扩展的优点也在CDN里占据了不少的份额。
## 小结
CDN发展到现在已经有二十来年的历史了早期的CDN功能比较简单只能加速静态资源。随着这些年Web 2.0、HTTPS、视频、直播等新技术、新业务的崛起它也在不断进步增加了很多的新功能比如SSL加速、内容优化数据压缩、图片格式转换、视频转码、资源防盗链、WAF安全防护等等。
现在再说CDN是“搬运工”已经不太准确了它更像是一个“无微不至”的“网站保姆”让网站只安心生产优质的内容其他的“杂事”都由它去代劳。
1. 由于客观地理距离的存在直连网站访问速度会很慢所以就出现了CDN
1. CDN构建了全国、全球级别的专网让用户就近访问专网里的边缘节点降低了传输延迟实现了网站加速
1. GSLB是CDN的“大脑”使用DNS负载均衡技术智能调度边缘节点提供服务
1. 缓存系统是CDN的“心脏”使用HTTP缓存代理技术缓存命中就返回给用户否则就要回源。
## 课下作业
1. 网站也可以自建同城、异地多处机房构建集群来提高服务能力为什么非要选择CDN呢
1. 对于无法缓存的动态资源你觉得CDN也能有加速效果吗
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/bc/51/bc1805a7c49977c7838b29602f3bba51.png" alt="unpreview">

View File

@@ -0,0 +1,151 @@
<audio id="audio" title="38 | WebSocket沙盒里的TCP" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/5e/e2/5e9e27f590f3fd65f21975e334447ee2.mp3"></audio>
在之前讲TCP/IP协议栈的时候我说过有“TCP Socket”它实际上是一种功能接口通过这些接口就可以使用TCP/IP协议栈在传输层收发数据。
那么你知道还有一种东西叫“WebSocket”吗
单从名字上看“Web”指的是HTTP“Socket”是套接字调用那么这两个连起来又是什么意思呢
所谓“望文生义”大概你也能猜出来“WebSocket”就是运行在“Web”也就是HTTP上的Socket通信规范提供与“TCP Socket”类似的功能使用它就可以像“TCP Socket”一样调用下层协议栈任意地收发数据。
<img src="https://static001.geekbang.org/resource/image/ee/28/ee6685c7d3c673b95e46d582828eee28.png" alt="">
更准确地说“WebSocket”是一种基于TCP的轻量级网络通信协议在地位上是与HTTP“平级”的。
## 为什么要有WebSocket
不过已经有了被广泛应用的HTTP协议为什么要再出一个WebSocket呢它有哪些好处呢
其实WebSocket与HTTP/2一样都是为了解决HTTP某方面的缺陷而诞生的。HTTP/2针对的是“队头阻塞”而WebSocket针对的是“请求-应答”通信模式。
那么,“请求-应答”有什么不好的地方呢?
“请求-应答”是一种“**半双工**”的通信模式,虽然可以双向收发数据,但同一时刻只能一个方向上有动作,传输效率低。更关键的一点,它是一种“被动”通信模式,服务器只能“被动”响应客户端的请求,无法主动向客户端发送数据。
虽然后来的HTTP/2、HTTP/3新增了Stream、Server Push等特性但“请求-应答”依然是主要的工作方式。这就导致HTTP难以应用在动态页面、即时消息、网络游戏等要求“**实时通信**”的领域。
在WebSocket出现之前在浏览器环境里用JavaScript开发实时Web应用很麻烦。因为浏览器是一个“受限的沙盒”不能用TCP只有HTTP协议可用所以就出现了很多“变通”的技术“**轮询**”polling就是比较常用的的一种。
简单地说轮询就是不停地向服务器发送HTTP请求问有没有数据有数据的话服务器就用响应报文回应。如果轮询的频率比较高那么就可以近似地实现“实时通信”的效果。
但轮询的缺点也很明显反复发送无效查询请求耗费了大量的带宽和CPU资源非常不经济。
所以为了克服HTTP“请求-应答”模式的缺点WebSocket就“应运而生”了。它原来是HTML5的一部分后来“自立门户”形成了一个单独的标准RFC文档编号是6455。
## WebSocket的特点
WebSocket是一个真正“**全双工**”的通信协议与TCP一样客户端和服务器都可以随时向对方发送数据而不用像HTTP“你拍一我拍一”那么“客套”。于是服务器就可以变得更加“主动”了。一旦后台有新的数据就可以立即“推送”给客户端不需要客户端轮询“实时通信”的效率也就提高了。
WebSocket采用了二进制帧结构语法、语义与HTTP完全不兼容但因为它的主要运行环境是浏览器为了便于推广和应用就不得不“搭便车”在使用习惯上尽量向HTTP靠拢这就是它名字里“Web”的含义。
服务发现方面WebSocket没有使用TCP的“IP地址+端口号”而是延用了HTTP的URI格式但开头的协议名不是“http”引入的是两个新的名字“**ws**”和“**wss**”分别表示明文和加密的WebSocket协议。
WebSocket的默认端口也选择了80和443因为现在互联网上的防火墙屏蔽了绝大多数的端口只对HTTP的80、443端口“放行”所以WebSocket就可以“伪装”成HTTP协议比较容易地“穿透”防火墙与服务器建立连接。具体是怎么“伪装”的我稍后再讲。
下面我举几个WebSocket服务的例子你看看是不是和HTTP几乎一模一样
```
ws://www.chrono.com
ws://www.chrono.com:8080/srv
wss://www.chrono.com:445/im?user_id=xxx
```
要注意的一点是WebSocket的名字容易让人产生误解虽然大多数情况下我们会在浏览器里调用API来使用WebSocket但它不是一个“调用接口的集合”而是一个通信协议所以我觉得把它理解成“**TCP over Web**”会更恰当一些。
## WebSocket的帧结构
刚才说了WebSocket用的也是二进制帧有之前HTTP/2、HTTP/3的经验相信你这次也能很快掌握WebSocket的报文结构。
不过WebSocket和HTTP/2的关注点不同WebSocket更**侧重于“实时通信”**而HTTP/2更侧重于提高传输效率所以两者的帧结构也有很大的区别。
WebSocket虽然有“帧”但却没有像HTTP/2那样定义“流”也就不存在“多路复用”“优先级”等复杂的特性而它自身就是“全双工”的也就不需要“服务器推送”。所以综合起来WebSocket的帧学习起来会简单一些。
下图就是WebSocket的帧结构定义长度不固定最少2个字节最多14字节看着好像很复杂实际非常简单。
<img src="https://static001.geekbang.org/resource/image/29/c4/29d33e972dda5a27aa4773eea896a8c4.png" alt="">
开头的两个字节是必须的,也是最关键的。
第一个字节的第一位“**FIN**”是消息结束的标志位相当于HTTP/2里的“END_STREAM”表示数据发送完毕。一个消息可以拆成多个帧接收方看到“FIN”后就可以把前面的帧拼起来组成完整的消息。
“FIN”后面的三个位是保留位目前没有任何意义但必须是0。
第一个字节的后4位很重要叫**“Opcode**”操作码其实就是帧类型比如1表示帧内容是纯文本2表示帧内容是二进制数据8是关闭连接9和10分别是连接保活的PING和PONG。
第二个字节第一位是掩码标志位“**MASK**”表示帧内容是否使用异或操作xor做简单的加密。目前的WebSocket标准规定客户端发送数据必须使用掩码而服务器发送则必须不使用掩码。
第二个字节后7位是“**Payload len**”表示帧内容的长度。它是另一种变长编码最少7位最多是7+64位也就是额外增加8个字节所以一个WebSocket帧最大是2^64。
长度字段后面是“**Masking-key**”掩码密钥它是由上面的标志位“MASK”决定的如果使用掩码就是4个字节的随机数否则就不存在。
这么分析下来其实WebSocket的帧头就四个部分“**结束标志位+操作码+帧长度+掩码**”只是使用了变长编码的“小花招”不像HTTP/2定长报文头那么简单明了。
我们的实验环境利用OpenResty的“lua-resty-websocket”库实现了一个简单的WebSocket通信你可以访问URI“/38-1”它会连接后端的WebSocket服务“ws://127.0.0.1/38-0”用Wireshark抓包就可以看到WebSocket的整个通信过程。
下面的截图是其中的一个文本帧因为它是客户端发出的所以需要掩码报文头就在两个字节之外多了四个字节的“Masking-key”总共是6个字节。
<img src="https://static001.geekbang.org/resource/image/c9/94/c91ee4815097f5f9059ab798bb841594.png" alt="">
而报文内容经过掩码不是直接可见的明文但掩码的安全强度几乎是零用“Masking-key”简单地异或一下就可以转换出明文。
## WebSocket的握手
和TCP、TLS一样WebSocket也要有一个握手过程然后才能正式收发数据。
这里它还是搭上了HTTP的“便车”利用了HTTP本身的“协议升级”特性“伪装”成HTTP这样就能绕过浏览器沙盒、网络防火墙等等限制这也是WebSocket与HTTP的另一个重要关联点。
WebSocket的握手是一个标准的HTTP GET请求但要带上两个协议升级的专用头字段
- “Connection: Upgrade”表示要求协议“升级”
- “Upgrade: websocket”表示要“升级”成WebSocket协议。
另外为了防止普通的HTTP消息被“意外”识别成WebSocket握手消息还增加了两个额外的认证用头字段所谓的“挑战”Challenge
- Sec-WebSocket-Key一个Base64编码的16字节随机数作为简单的认证密钥
- Sec-WebSocket-Version协议的版本号当前必须是13。
<img src="https://static001.geekbang.org/resource/image/8f/97/8f007bb0e403b6cc28493565f709c997.png" alt="">
服务器收到HTTP请求报文看到上面的四个字段就知道这不是一个普通的GET请求而是WebSocket的升级请求于是就不走普通的HTTP处理流程而是构造一个特殊的“101 Switching Protocols”响应报文通知客户端接下来就不用HTTP了全改用WebSocket协议通信。有点像TLS的“Change Cipher Spec”
WebSocket的握手响应报文也是有特殊格式的要用字段“Sec-WebSocket-Accept”验证客户端请求报文同样也是为了防止误连接。
具体的做法是把请求头里“Sec-WebSocket-Key”的值加上一个专用的UUID “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”再计算SHA-1摘要。
```
encode_base64(
sha1(
Sec-WebSocket-Key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' ))
```
客户端收到响应报文,就可以用同样的算法,比对值是否相等,如果相等,就说明返回的报文确实是刚才握手时连接的服务器,认证成功。
握手完成后续传输的数据就不再是HTTP报文而是WebSocket格式的二进制帧了。
<img src="https://static001.geekbang.org/resource/image/84/03/84e9fa337f2b4c2c9f14760feb41c903.png" alt="">
## 小结
浏览器是一个“沙盒”环境有很多的限制不允许建立TCP连接收发数据而有了WebSocket我们就可以在浏览器里与服务器直接建立“TCP连接”获得更多的自由。
不过自由也是有代价的WebSocket虽然是在应用层但使用方式却与“TCP Socket”差不多过于“原始”用户必须自己管理连接、缓存、状态开发上比HTTP复杂的多所以是否要在项目中引入WebSocket必须慎重考虑。
1. HTTP的“请求-应答”模式不适合开发“实时通信”应用效率低难以实现动态页面所以出现了WebSocket
1. WebSocket是一个“全双工”的通信协议相当于对TCP做了一层“薄薄的包装”让它运行在浏览器环境里
1. WebSocket使用兼容HTTP的URI来发现服务但定义了新的协议名“ws”和“wss”端口号也沿用了80和443
1. WebSocket使用二进制帧结构比较简单特殊的地方是有个“掩码”操作客户端发数据必须掩码服务器则不用
1. WebSocket利用HTTP协议实现连接握手发送GET请求要求“协议升级”握手过程中有个非常简单的认证机制目的是防止误连接。
## 课下作业
1. WebSocket与HTTP/2有很多相似点比如都可以从HTTP/1升级都采用二进制帧结构你能比较一下这两个协议吗
1. 试着自己解释一下WebSocket里的”Web“和”Socket“的含义。
1. 结合自己的实际工作你觉得WebSocket适合用在哪些场景里
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/4b/5b/4b81de6b5c57db92ed7808344482ef5b.png" alt="unpreview">

View File

@@ -0,0 +1,175 @@
<audio id="audio" title="44 | 先睹为快HTTP/3实验版本长什么样子" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/75/4a/756326afb9b24943b205e78feae33b4a.mp3"></audio>
你好我是Chrono。
不知不觉《透视HTTP协议》这个专栏马上就要两周岁了。前几天我看了一下专栏的相关信息订阅数刚好破万非常感谢包括你在内的所有朋友们的关心、支持、鼓励和鞭策。
在专栏的结束语里我曾经说过希望HTTP/3发布之时能够再相会。而如今虽然它还没有发布但也为时不远了。
所以今天呢我就来和你聊聊HTTP/3的一些事就当是“尝尝鲜”吧。
## HTTP/3的现状
从2019到2021的这两年间大家对HTTP协议的关注重点差不多全都是放在HTTP/3标准的制订上。
最初专栏开始的时候HTTP/3草案还是第20版而现在则已经是第34版了发展的速度可以说是非常快的里面的内容也变动得非常多。很有可能最多再过一年甚至是今年内我们就可以看到正式的标准。
在标准文档的制订过程中,互联网业届也没有闲着,也在积极地为此做准备,以草案为基础做各种实验性质的开发。
这其中比较引人瞩目的要数CDN大厂Cloudflare还有Web Server领头羊Nginx而另一个Web Server Apache好像没什么动静了。
Cloudflare公司用Rust语言编写了一个QUIC支持库名字叫“quiche”然后在上面加了一层薄薄的封装由此能够以一个C模块的形式加入进Nginx框架为Nginx提供了HTTP/3的功能。可以参考这篇文章[HTTP/3过去现在还有未来](https://blog.cloudflare.com/zh-cn/http3-the-past-present-and-future-zh-cn/)
不过Cloudflare的这个QUIC支持库属于“民间行为”没有得到Nginx的认可。Nginx的官方HTTP/3模块其实一直在“秘密”开发中在去年的6月份这个模块终于正式公布了名字就叫“http_v3_module”。可以参考这篇文章[Introducing a Technology Preview of NGINX Support for QUIC and HTTP/3](https://www.nginx.com/blog/introducing-technology-preview-nginx-support-for-quic-http-3/)
目前http_v3_module已经度过了Alpha阶段处于Beta状态但支持的草案版本是29而不是最新的34。
这当然也有情可原。相比于HTTP/2HTTP/3的变化太大了Nginx团队的精力还是集中在核心功能实现上选择一个稳定的版本更有利于开发而且29后面的多个版本标准其实差异非常小仅文字编辑变更
Nginx也为测试HTTP/3专门搭建了一个网站[quic.nginx.org](https://quic.nginx.org/)任何人都可以上去测试验证HTTP/3协议。
所以接下来我们就用它来看看HTTP/3到底长什么样。
## 初识HTTP/3
在体验之前得先说一下浏览器这是测试QUIC和HTTP/3的关键最好使用最新版本的Chrome或者Firefox这里我用的是Chrome88。
打开浏览器窗口输入测试网站的URI[https://quic.nginx.org/](https://quic.nginx.org/)如果“运气好”刷新几次就能够在网页上看到大大的QUIC标志了。
<img src="https://static001.geekbang.org/resource/image/82/e0/827d261f49f6a20eb227f851dec667e0.png" alt="">
不过你很可能“运气”没有这么好在网页上看到的QUIC标志是灰色的。这意味着暂时没有应用QUIC和HTTP/3这就需要对Chrome做一点设置开启QUIC的支持。
首先要在地址栏输入“chrome://flags”打开设置页面然后搜索“QUIC”找到启用QUIC的选项把它改为“Enabled”具体可以参考下面的图片。
<img src="https://static001.geekbang.org/resource/image/67/65/674ff32bf05b5859fb6985e29f8c1e65.png" alt="">
接下来我们要在命令行里启动Chrome浏览器在命令行里传递“enable-quic”“quic-version”等参数来要求Chrome使用特定的草案版本。
下面的示例就是我在macOS上运行Chrome的命令行。你也可以参考Nginx网站上的README文档在Windows或者Linux上用类似的形式运行Chrome的命令行
```
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
--enable-quic --quic-version=h3-29 \
--origin-to-force-quic-on=quic.nginx.org:443
```
如果这样操作之后网页上仍然是显示灰色标志也不要紧你还可以用“F12”打开Chrome的开发者工具面板查看protocol一栏。
应该可以看到大部分资源显示的还是“h2”表示使用的是HTTP/2协议但有一小部分资源显示的是“h3-29”这就表示它是使用HTTP/3协议传输的而后面的“29”后缀意思是基于第29版草案也就是说启用了QUIC+HTTP/3协议。
<img src="https://static001.geekbang.org/resource/image/c3/e6/c3a532736412a4457ee81a280fc76be6.png" alt="">
## Wireshark抓包分析
好了大概看了HTTP/3是什么样有了感性认识我们就可以进一步来抓包分析。
网络抓包工具Wireshark你一定已经比较熟悉了这里同样要用最新的不然可能识别不了QUIC和HTTP/3的数据包比如我用的就是3.4.3。
QUIC的底层是UDP所以在抓包的时候过滤器要设置成“udp port 443”然后启动就可以了。这次我抓的包也放到了GitHub的[Wireshark目录](https://github.com/chronolaw/http_study/tree/master/wireshark)文件名是“44-1.pcapng”。
<img src="https://static001.geekbang.org/resource/image/6d/d4/6d217ee87e1f777d432059f81fc2f5d4.png" alt="">
因为HTTP/3内置了TLS加密可参考之前的[第32讲](https://time.geekbang.org/column/article/115564)所以用Wireshark抓包后看到的数据大部分都是乱码想要解密看到真实数据就必须设置SSLKEYLOG参考[第26讲](https://time.geekbang.org/column/article/110354))。
不过非常遗憾不知道是什么原因虽然我导出了SSLKEYLOG但在Wireshark里还是无法解密HTTP/3的数据显示解密错误。但同样的设置和操作步骤抓包解密HTTPS和HTTP/2却是正常的估计可能是目前Wireshark自身对HTTP/3的支持还不太完善吧。
所以今天我也就只能带你一起来看QUIC的握手阶段了。这个其实与TLS1.3非常接近只不过是内嵌在了QUIC协议里如果你学过了“安全篇”“飞翔篇”的课程看QUIC应该是不会费什么力气。
首先我们来看Header数据
```
[Packet Length: 1350]
1... .... = Header Form: Long Header (1)
.1.. .... = Fixed Bit: True
..00 .... = Packet Type: Initial (0)
.... 00.. = Reserved: 0
.... ..00 = Packet Number Length: 1 bytes (0)
Version: draft-29 (0xff00001d)
Destination Connection ID Length: 20
Destination Connection ID: 3ae893fa047246e55f963ea14fc5ecac3774f61e
Source Connection ID Length: 0
```
QUIC包头的第一个字节是标志位可以看到最开始建立连接会发一个长包Long Header包类型是初始化Initial
标志位字节后面是4字节的版本号因为目前还是草案所以显示的是“draft-29”。再后面是QUIC的特性之一“连接ID”长度为20字节的十六进制字符串。
这里我要特别提醒你注意,因为标准版本的演变,这个格式已经与当初[第32讲](https://time.geekbang.org/column/article/115564)的内容draft-20完全不一样了在分析查看的时候一定要使用[对应的RFC文档](https://tools.ietf.org/html/draft-ietf-quic-transport-28#section-17.2)。
往下再看是QUIC的CRYPTO帧用来传输握手消息帧类型是0x06
```
TLSv1.3 Record Layer: Handshake Protocol: Client Hello
Frame Type: CRYPTO (0x0000000000000006)
Offset: 0
Length: 309
Crypto Data
Handshake Protocol: Client Hello
```
CRYPTO帧里的数据就是QUIC内置的TLS “Client Hello”了我把里面的一些重要信息摘了出来
```
Handshake Protocol: Client Hello
Handshake Type: Client Hello (1)
Version: TLS 1.2 (0x0303)
Random: b4613d...
Cipher Suites (3 suites)
Cipher Suite: TLS_AES_128_GCM_SHA256 (0x1301)
Cipher Suite: TLS_AES_256_GCM_SHA384 (0x1302)
Cipher Suite: TLS_CHACHA20_POLY1305_SHA256 (0x1303)
Extension: server_name (len=19)
Type: server_name (0)
Server Name Indication extension
Server Name: quic.nginx.org
Extension: application_layer_protocol_negotiation (len=8)
Type: application_layer_protocol_negotiation (16)
ALPN Protocol
ALPN Next Protocol: h3-29
Extension: key_share (len=38)
Key Share extension
Extension: supported_versions (len=3)
Type: supported_versions (43)
Supported Version: TLS 1.3 (0x0304)
```
你看这个就是标准的TLS1.3数据伪装成了TLS1.2支持AES128、AES256、CHACHA20三个密码套件SNI是“quic.nginx.org”ALPN是“h3-29”。
浏览器发送完Initial消息之后服务器回复Handshake用一个RTT就完成了握手包的格式基本一样用了一个CRYPTO帧和ACK帧我就不细分析了可参考[相应的RFC](https://tools.ietf.org/html/draft-ietf-quic-transport-28#section-17.2)只贴一下里面的“Server Hello”信息
```
Handshake Protocol: Server Hello
Handshake Type: Server Hello (2)
Version: TLS 1.2 (0x0303)
Random: d6aede...
Cipher Suite: TLS_AES_128_GCM_SHA256 (0x1301)
Extension: key_share (len=36)
Key Share extension
Extension: supported_versions (len=2)
Type: supported_versions (43)
Supported Version: TLS 1.3 (0x0304)
```
这里服务器选择了“TLS_AES_128_GCM_SHA256”然后带回了随机数和key_share完成了握手阶段的密钥交换。
## 小结
好了QUIC和HTTP/3的“抢鲜体验”就到这里吧我简单小结一下今天的要点
1. HTTP/3的最新草案版本是34很快就会发布正式版本。
1. Nginx提供了对HTTP/3的实验性质支持目前是Beta状态只能用于测试。
1. 最新版本的Chrome和Firefox都支持QUIC和HTTP/3但可能需要一些设置工作才能启用。
1. 访问专门的测试网站“quic.nginx.org”可以检查浏览器是否支持QUIC和HTTP/3。
1. 抓包分析QUIC和HTTP/3需要用最新的Wireshark过滤器用UDP还要导出SSLKEYLOG才能解密。
希望你看完这一讲后自己实际动手操作一下访问网站再抓包如果能正确解密HTTP/3数据就把资料发出来和我们分享下。
如果你觉得有所收获,也欢迎把这一讲的内容分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/1b/4f/1b4266dcedc5785f3023f47083e4894f.jpg" alt="">

View File

@@ -0,0 +1,147 @@
<audio id="audio" title="01 | 时势与英雄HTTP的前世今生" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/94/33/945bf44de1d08d72f9ce94d290401733.mp3"></audio>
HTTP协议在我们的生活中随处可见打开手机或者电脑只要你上网不论是用iPhone、Android、Windows还是Mac不论是用浏览器还是App不论是看新闻、短视频还是听音乐、玩游戏后面总会有HTTP在默默为你服务。
据NetCraft公司统计目前全球至少有16亿个网站、2亿多个独立域名而这个庞大网络世界的底层运转机制就是HTTP。
那么在享受如此便捷舒适的网络生活时你有没有想过HTTP协议是怎么来的它最开始是什么样子的又是如何一步一步发展到今天几乎“统治”了整个互联网世界的呢
常言道:“**时势造英雄,英雄亦造时势**”。
今天我就和你来聊一聊HTTP的发展历程看看它的成长轨迹看看历史上有哪些事件推动了它的前进它又促进了哪些技术的产生一起来见证“英雄之旅”。
在这个过程中你也能够顺便了解一下HTTP的“历史局限性”明白HTTP为什么会设计成现在这个样子。
## 史前时期
20世纪60年代美国国防部高等研究计划署ARPA建立了ARPA网它有四个分布在各地的节点被认为是如今互联网的“始祖”。
然后在70年代基于对ARPA网的实践和思考研究人员发明出了著名的TCP/IP协议。由于具有良好的分层结构和稳定的性能TCP/IP协议迅速战胜其他竞争对手流行起来并在80年代中期进入了UNIX系统内核促使更多的计算机接入了互联网。
## 创世纪
<img src="https://static001.geekbang.org/resource/image/a2/9a/a2960d0e44ef6a8fedd4e9bb836e049a.jpg" alt="unpreview">
1989年任职于欧洲核子研究中心CERN的蒂姆·伯纳斯-李Tim Berners-Lee发表了一篇论文提出了在互联网上构建超链接文档系统的构想。这篇论文中他确立了三项关键技术。
1. URI即统一资源标识符作为互联网上资源的唯一身份
1. HTML即超文本标记语言描述超文本文档
1. HTTP即超文本传输协议用来传输超文本。
这三项技术在如今的我们看来已经是稀松平常但在当时却是了不得的大发明。基于它们就可以把超文本系统完美地运行在互联网上让各地的人们能够自由地共享信息蒂姆把这个系统称为“万维网”World Wide Web也就是我们现在所熟知的Web。
所以在这一年我们的英雄“HTTP”诞生了从此开始了它伟大的征途。
## HTTP/0.9
20世纪90年代初期的互联网世界非常简陋计算机处理能力低存储容量小网速很慢还是一片“信息荒漠”。网络上绝大多数的资源都是纯文本很多通信协议也都使用纯文本所以HTTP的设计也不可避免地受到了时代的限制。
这一时期的HTTP被定义为0.9版,结构比较简单,为了便于服务器和客户端处理,它也采用了纯文本格式。蒂姆·伯纳斯-李最初设想的系统里的文档都是只读的所以只允许用“GET”动作从服务器上获取HTML文档并且在响应请求之后立即关闭连接功能非常有限。
HTTP/0.9虽然很简单但它作为一个“原型”充分验证了Web服务的可行性而“简单”也正是它的优点蕴含了进化和扩展的可能性因为
“把简单的系统变复杂”,要比“把复杂的系统变简单”容易得多。
## HTTP/1.0
1993年NCSA美国国家超级计算应用中心开发出了Mosaic是第一个可以图文混排的浏览器随后又在1995年开发出了服务器软件Apache简化了HTTP服务器的搭建工作。
同一时期计算机多媒体技术也有了新的发展1992年发明了JPEG图像格式1995年发明了MP3音乐格式。
这些新软件新技术一经推出立刻就吸引了广大网民的热情更的多的人开始使用互联网研究HTTP并提出改进意见甚至实验性地往协议里添加各种特性从用户需求的角度促进了HTTP的发展。
于是在这些已有实践的基础上经过一系列的草案HTTP/1.0版本在1996年正式发布。它在多方面增强了0.9版形式上已经和我们现在的HTTP差别不大了例如
1. 增加了HEAD、POST等新方法
1. 增加了响应状态码,标记可能的错误原因;
1. 引入了协议版本号概念;
1. 引入了HTTP Header头部的概念让HTTP处理请求和响应更加灵活
1. 传输的数据不再仅限于文本。
但HTTP/1.0并不是一个“标准”,只是记录已有实践和模式的一份参考文档,不具有实际的约束力,相当于一个“备忘录”。
所以HTTP/1.0的发布对于当时正在蓬勃发展的互联网来说并没有太大的实际意义,各方势力仍然按照自己的意图继续在市场上奋力拼杀。
## HTTP/1.1
1995年网景的Netscape Navigator和微软的Internet Explorer开始了著名的“浏览器大战”都希望在互联网上占据主导地位。
<img src="https://static001.geekbang.org/resource/image/94/42/9470d41cab80f36438ebb06a71672242.png" alt="unpreview">
这场战争的结果你一定早就知道了最终微软的IE取得了决定性的胜利而网景则“败走麦城”但后来却凭借Mozilla Firefox又扳回一局
“浏览器大战”的是非成败我们放在一边暂且不管不可否认的是它再一次极大地推动了Web的发展HTTP/1.0也在这个过程中经受了实践检验。于是在“浏览器大战”结束之后的1999年HTTP/1.1发布了RFC文档编号为2616正式确立了延续十余年的传奇。
从版本号我们就可以看到HTTP/1.1是对HTTP/1.0的小幅度修正。但一个重要的区别是它是一个“正式的标准”而不是一份可有可无的“参考文档”。这意味着今后互联网上所有的浏览器、服务器、网关、代理等等只要用到HTTP协议就必须严格遵守这个标准相当于是互联网世界的一个“立法”。
不过说HTTP/1.1是“小幅度修正”也不太确切它还是有很多实质性进步的。毕竟经过了多年的实战检验比起0.9/1.0少了“学术气”更加“接地气”同时表述也更加严谨。HTTP/1.1主要的变更点有:
1. 增加了PUT、DELETE等新的方法
1. 增加了缓存管理和控制;
1. 明确了连接管理,允许持久连接;
1. 允许响应数据分块chunked利于传输大文件
1. 强制要求Host头让互联网主机托管成为可能。
HTTP/1.1的推出可谓是“众望所归”互联网在它的“保驾护航”下迈开了大步由此走上了“康庄大道”开启了后续的“Web 1.0”“Web 2.0”时代。现在许多的知名网站都是在这个时间点左右创立的例如Google、新浪、搜狐、网易、腾讯等。
不过由于HTTP/1.1太过庞大和复杂所以在2014年又做了一次修订原来的一个大文档被拆分成了六份较小的文档编号为7230-7235优化了一些细节但此外没有任何实质性的改动。
## HTTP/2
HTTP/1.1发布之后整个互联网世界呈现出了爆发式的增长度过了十多年的“快乐时光”更涌现出了Facebook、Twitter、淘宝、京东等互联网新贵。
这期间也出现了一些对HTTP不满的意见主要就是连接慢无法跟上迅猛发展的互联网但HTTP/1.1标准一直“岿然不动”无奈之下人们只好发明各式各样的“小花招”来缓解这些问题比如以前常见的切图、JS合并等网页优化手段。
终于有一天搜索巨头Google忍不住了决定“揭竿而起”就像马云说的“如果银行不改变我们就改变银行”。那么它是怎么“造反”的呢
Google首先开发了自己的浏览器Chrome然后推出了新的SPDY协议并在Chrome里应用于自家的服务器如同十多年前的网景与微软一样从实际的用户方来“倒逼”HTTP协议的变革这也开启了第二次的“浏览器大战”。
历史再次重演不过这次的胜利者是GoogleChrome目前的全球的占有率超过了60%。“挟用户以号令天下”Google借此顺势把SPDY推上了标准的宝座互联网标准化组织以SPDY为基础开始制定新版本的HTTP协议最终在2015年发布了HTTP/2RFC编号7540。
HTTP/2的制定充分考虑了现今互联网的现状宽带、移动、不安全在高度兼容HTTP/1.1的同时在性能改善方面做了很大努力,主要的特点有:
1. 二进制协议,不再是纯文本;
1. 可发起多个请求废弃了1.1里的管道;
1. 使用专用算法压缩头部,减少数据传输量;
1. 允许服务器主动向客户端推送数据;
1. 增强了安全性,“事实上”要求加密通信。
虽然HTTP/2到今天已经四岁也衍生出了gRPC等新协议但由于HTTP/1.1实在是太过经典和强势目前它的普及率还比较低大多数网站使用的仍然还是20年前的HTTP/1.1。
## HTTP/3
看到这里你可能会问了“HTTP/2这么好是不是就已经完美了呢
答案是否定的这一次还是Google而且它要“革自己的命”。
在HTTP/2还处于草案之时Google又发明了一个新的协议叫做QUIC而且还是相同的“套路”继续在Chrome和自家服务器里试验着“玩”依托它的庞大用户量和数据量持续地推动QUIC协议成为互联网上的“既成事实”。
“功夫不负有心人”当然也是因为QUIC确实自身素质过硬。
在去年也就是2018年互联网标准化组织IETF提议将“HTTP over QUIC”更名为“HTTP/3”并获得批准HTTP/3正式进入了标准化制订阶段也许两三年后就会正式发布到时候我们很可能会跳过HTTP/2直接进入HTTP/3。
## 小结
今天我和你一起跨越了三十年的历史长河回顾了HTTP协议的整个发展过程在这里简单小结一下今天的内容
1. HTTP协议始于三十年前蒂姆·伯纳斯-李的一篇论文;
1. HTTP/0.9是个简单的文本协议,只能获取文本资源;
1. HTTP/1.0确立了大部分现在使用的技术,但它不是正式标准;
1. HTTP/1.1是目前互联网上使用最广泛的协议,功能也非常完善;
1. HTTP/2基于Google的SPDY协议注重性能改善但还未普及
1. HTTP/3基于Google的QUIC协议是将来的发展方向。
希望通过今天的介绍你能够对HTTP有一个初步但清晰的印象知道了“来龙”才能更好地知道“去脉”。
## 课下作业
1. 你认为推动HTTP发展的原动力是什么
1. 你是怎么理解HTTP超文本传输协议
欢迎你把自己的答案写在留言区,与我和其他同学一起讨论。暂时回答不出来也不要紧,你可以带着这些问题在后续的课程里寻找答案。
如果你觉得有所收获,欢迎你把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/20/00/2016d4e0ab9698c3d2cd560f0fa89a00.png" alt="unpreview">

View File

@@ -0,0 +1,141 @@
<audio id="audio" title="02 | HTTP是什么HTTP又不是什么" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/bd/46/bd2f893babbff670fb54341068f21a46.mp3"></audio>
首先我来问出这个问题“你觉得HTTP是什么呢
你可能会不假思索、脱口而出“HTTP就是超文本传输协议也就是**H**yper**T**ext **T**ransfer **P**rotocol。”
回答非常正确我必须由衷地恭喜你能给出这个答案就表明你具有至少50%HTTP相关的知识储备应该算得上是“半个专家”了。
不过让我们换个对话场景,假设不是我,而是由一位面试官问出刚才的问题呢?
<img src="https://static001.geekbang.org/resource/image/b4/bf/b4de4be0f7dfd4185464bb5a1d6df0bf.png" alt="unpreview">
显然,这个答案有点过于简单了,不能让他满意,他肯定会再追问你一些问题:
- 你是怎么理解HTTP字面上的“超文本”和“传输协议”的
- 能否谈一下你对HTTP的认识越多越好。
- HTTP有什么特点有什么优点和缺点
- HTTP下层都有哪些协议是如何工作的
- ……
几乎所有面试时问到的HTTP相关问题都可以从这个最简单的“HTTP是什么”引出来。
所以,今天的话题就从这里开始,深度地解答一下“**HTTP是什么**”,以及延伸出来的第二个问题“**HTTP不是什么**”
## HTTP是什么
咱们中国有个成语“人如其名”,意思是一个人的性格和特点是与他的名字相符的。
先看一下HTTP的名字“**超文本传输协议**”,它可以拆成三个部分,分别是:“**超文本**”“**传输**”和“**协议**”。我们从后往前来逐个解析理解了这三个词我们也就明白了什么是HTTP。
<img src="https://static001.geekbang.org/resource/image/d6/20/d697ba915bcca40a11b8a25571516720.jpg" alt="unpreview">
首先HTTP是一个**协议**。不过,协议又是什么呢?
其实“协议”并不仅限于计算机世界,现实生活中也随处可见。例如,你在刚毕业时会签一个“三方协议”,找房子时会签一个“租房协议”,公司入职时还可能会签一个“保密协议”,工作中使用的各种软件也都带着各自的“许可协议”。
刚才说的这几个都是“协议”本质上与HTTP是相同的那么“协议”有什么特点呢
第一点,协议必须要有两个或多个参与者,也就是“协”。
如果**只有**你一个人,那你自然可以想干什么就干什么,想怎么玩就怎么玩,不会干涉其他人,其他人也不会干涉你,也就不需要所谓的“协议”。但是,一旦有了两个以上的参与者出现,为了保证最基本的顺畅交流,协议就自然而然地出现了。
例如,为了保证你顺利就业,“三方协议”里的参与者有三个:你、公司和学校;为了保证你顺利入住,“租房协议”里的参与者有两个:你和房东。
第二点,协议是对参与者的一种行为约定和规范,也就是“议”。
协议意味着有多个参与者为了达成某个共同的目的而站在了一起,除了要无疑义地沟通交流之外,还必须明确地规定各方的“责、权、利”,约定该做什么不该做什么,先做什么后做什么,做错了怎么办,有没有补救措施等等。例如,“租房协议”里就约定了,租期多少个月,每月租金多少,押金是多少,水电费谁来付,违约应如何处理等等。
到这里你应该能够明白HTTP的第一层含义了。
HTTP是一个用在计算机世界里的协议。它使用计算机能够理解的语言确立了一种计算机之间交流通信的规范以及相关的各种控制和错误处理方式。
接下来我们看HTTP字面里的第二部分“**传输**”。
计算机和网络世界里有数不清的各种角色CPU、内存、总线、磁盘、操作系统、浏览器、网关、服务器……这些角色之间相互通信也必然会有各式各样、五花八门的协议用处也各不相同例如广播协议、寻址协议、路由协议、隧道协议、选举协议等等。
HTTP是一个“**传输协议**”所谓的“传输”Transfer其实很好理解就是把一堆东西从A点搬到B点或者从B点搬到A点即“A&lt;===&gt;B”。
别小看了这个简单的动作,它也至少包含了两项重要的信息。
第一点HTTP协议是一个“**双向协议**”。
也就是说有两个最基本的参与者A和B从A开始到B结束数据在A和B之间双向而不是单向流动。通常我们把先发起传输动作的A叫做**请求方**把后接到传输的B叫做**应答方**或者**响应方**。拿我们最常见的上网冲浪来举例子浏览器就是请求方A网易、新浪这些网站就是应答方B。双方约定用HTTP协议来通信于是浏览器把一些数据发送给网站网站再把一些数据发回给浏览器最后展现在屏幕上你就可以看到各种有意思的新闻、视频了。
第二点数据虽然是在A和B之间传输但并没有限制只有A和B这两个角色允许中间有“中转”或者“接力”。
这样传输方式就从“A&lt;===&gt;B”变成了“A&lt;=&gt;X&lt;=&gt;Y&lt;=&gt;Z&lt;=&gt;B”A到B的传输过程中可以存在任意多个“中间人”而这些中间人也都遵从HTTP协议只要不打扰基本的数据传输就可以添加任意的额外功能例如安全认证、数据压缩、编码转换等等优化整个传输过程。
说到这里你差不多应该能够明白HTTP的第二层含义了。
HTTP是一个在计算机世界里专门用来在两点之间传输数据的约定和规范。
讲完了“协议”和“传输”现在我们终于到HTTP字面里的第三部分“**超文本**”。
既然HTTP是一个“传输协议”那么它传输的“超文本”到底是什么呢我还是用两点来进一步解释。
所谓“**文本**”Text就表示HTTP传输的不是TCP/UDP这些底层协议里被切分的杂乱无章的二进制包datagram而是完整的、有意义的数据可以被浏览器、服务器这样的上层应用程序处理。
在互联网早期“文本”只是简单的字符文字但发展到现在“文本”的涵义已经被大大地扩展了图片、音频、视频、甚至是压缩包在HTTP眼里都可以算做是“文本”。
所谓“**超文本**”,就是“超越了普通文本的文本”,它是文字、图片、音频和视频等的混合体,最关键的是含有“超链接”,能够从一个“超文本”跳跃到另一个“超文本”,形成复杂的非线性、网状的结构关系。
对于“超文本”我们最熟悉的就应该是HTML了它本身只是纯文字文件但内部用很多标签定义了对图片、音频、视频等的链接再经过浏览器的解释呈现在我们面前的就是一个含有多种视听信息的页面。
OK经过了对HTTP里这三个名词的详细解释下次当你再面对面试官时就可以给出比“超文本传输协议”这七个字更准确更有技术含量的答案“HTTP是一个在计算机世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范”。
## HTTP不是什么
现在你对“**HTTP是什么**”应该有了比较清晰的认识,紧接着的问题就是“**HTTP不是什么**”等价的问题是“HTTP不能干什么”。想想看你能回答出来吗
因为HTTP是一个协议是一种计算机间通信的规范所以它**不存在“单独的实体”**。它不是浏览器、手机APP那样的应用程序也不是Windows、Linux那样的操作系统更不是Apache、Nginx、Tomcat那样的Web服务器。
但HTTP又与应用程序、操作系统、Web服务器密切相关在它们之间的通信过程中存在而且是一种“动态的存在”是发生在网络连接、传输超文本数据时的一个“动态过程”。
**HTTP不是互联网**
互联网Internet是遍布于全球的许多网络互相连接而形成的一个巨大的国际网络在它上面存放着各式各样的资源也对应着各式各样的协议例如超文本资源使用HTTP普通文件使用FTP电子邮件使用SMTP和POP3等。
但毫无疑问HTTP是构建互联网的一块重要拼图而且是占比最大的那一块。
**HTTP不是编程语言**
编程语言是人与计算机沟通交流所使用的语言而HTTP是计算机与计算机沟通交流的语言我们无法使用HTTP来编程但可以反过来用编程语言去实现HTTP告诉计算机如何用HTTP来与外界通信。
很多流行的编程语言都支持编写HTTP相关的服务或应用例如使用Java在Tomcat里编写Web服务使用PHP在后端实现页面模板渲染使用JavaScript在前端实现动态页面更新你是否也会其中的一两种呢
**HTTP不是HTML**这个可能要特别强调一下千万不要把HTTP与HTML混为一谈虽然这两者经常是同时出现。
HTML是超文本的载体是一种标记语言使用各种标签描述文字、图片、超链接等资源并且可以嵌入CSS、JavaScript等技术实现复杂的动态效果。单论次数在互联网上HTTP传输最多的可能就是HTML但要是论数据量HTML可能要往后排了图片、音频、视频这些类型的资源显然更大。
**HTTP不是一个孤立的协议**
俗话说“一个好汉三个帮”HTTP也是如此。
在互联网世界里HTTP通常跑在TCP/IP协议栈之上依靠IP协议实现寻址和路由、TCP协议实现可靠数据传输、DNS协议实现域名查找、SSL/TLS协议实现安全通信。此外还有一些协议依赖于HTTP例如WebSocket、HTTPDNS等。这些协议相互交织构成了一个协议网而HTTP则处于中心地位。
## 小结
1. HTTP是一个用在计算机世界里的协议它确立了一种计算机之间交流通信的规范以及相关的各种控制和错误处理方式。
1. HTTP专门用来在两点之间传输数据不能用于广播、寻址或路由。
1. HTTP传输的是文字、图片、音频、视频等超文本数据。
1. HTTP是构建互联网的重要基础技术它没有实体依赖许多其他的技术来实现但同时许多技术也都依赖于它。
把这些综合起来使用递归缩写方式模仿PHP我们可以把HTTP定义为“**与HTTP协议相关的所有应用层技术的总和**”。
这里我画了一个思维导图,也可以算是这个专栏系列文章的“知识地图”。
<img src="https://static001.geekbang.org/resource/image/27/cc/2781919e73f5d258ff1dc371af632acc.png" alt="">
你可以对照这张图,看一下哪些部分是自己熟悉的,哪些部分是陌生的,又有哪些部分是想要进一步了解的,下一讲我会详细讲解这张图。
## 课下作业
1. 有一种流行的说法“HTTP是用于从互联网服务器传输超文本到本地浏览器的协议”你认为这种说法对吗对在哪里又错在哪里
1. 你能再说出几个“HTTP不是什么”吗
欢迎你通过留言分享答案,与我和其他同学一起讨论。如果你觉得有所收获,欢迎你把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/eb/3a/ebcadbddbc7e0c146dbe7617a844e83a.png" alt="unpreview">

View File

@@ -0,0 +1,146 @@
<audio id="audio" title="03 | HTTP世界全览与HTTP相关的各种概念" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a5/98/a5050b7253387e72a48472ad47886198.mp3"></audio>
在上一讲的末尾我画了一张图里面是与HTTP关联的各种技术和知识点也可以说是这个专栏的总索引不知道你有没有认真看过呢
那张图左边的部分是与HTTP有关系的各种协议比较偏向于理论而右边的部分是与HTTP有关系的各种应用技术偏向于实际应用。
我希望借助这张图帮你澄清与HTTP相关的各种概念和角色让你在实际工作中清楚它们在链路中的位置和作用知道发起一个HTTP请求会有哪些角色参与会如何影响请求的处理做到“手中有粮心中不慌”。
因为那张图比较大所以我会把左右两部分拆开来分别讲今天先讲右边的部分也就是与HTTP相关的各种应用着重介绍互联网、浏览器、Web服务器等常见且重要的概念。
<img src="https://static001.geekbang.org/resource/image/51/64/5102fc33d04b59b36971a5e487779864.png" alt="">
为了方便你查看,我又把这部分重新画了一下,比那张大图小了一些,更容易地阅读,你可以点击查看。
暖场词就到这里,让我们正式开始吧。
## 网络世界
你一定已经习惯了现在的网络生活,甚至可能会下意识地认为网络世界就应该是这个样子的:“一张平坦而且一望无际的巨大网络,每一台电脑就是网络上的一个节点,均匀地点缀在这张网上”。
这样的理解既对,又不对。从抽象的、虚拟的层面来看,网络世界确实是这样的,我们可以从一个节点毫无障碍地访问到另一个节点。
但现实世界的网络却远比这个抽象的模型要复杂得多。实际的互联网是由许许多多个规模略小的网络连接而成的,这些“小网络”可能是只有几百台电脑的局域网,可能是有几万、几十万台电脑的广域网,可能是用电缆、光纤构成的固定网络,也可能是用基站、热点构成的移动网络……
互联网世界更像是由数不清的大小岛屿组成的“千岛之国”。
互联网的正式名称是Internet里面存储着无穷无尽的信息资源我们通常所说的“上网”实际上访问的只是互联网的一个子集“万维网”World Wide Web它基于HTTP协议传输HTML等超文本资源能力也就被限制在HTTP协议之内。
互联网上还有许多万维网之外的资源例如常用的电子邮件、BT和Magnet点对点下载、FTP文件下载、SSH安全登录、各种即时通信服务等等它们需要用各自的专有协议来访问。
不过由于HTTP协议非常灵活、易于扩展而且“超文本”的表述能力很强所以很多其他原本不属于HTTP的资源也可以“包装”成HTTP来访问这就是我们为什么能够总看到各种“网页应用”——例如“微信网页版”“邮箱网页版”——的原因。
综合起来看现在的互联网90%以上的部分都被万维网也就是HTTP所覆盖所以把互联网约等于万维网或HTTP应该也不算大错。
## 浏览器
上网就要用到浏览器常见的浏览器有Google的Chrome、Mozilla的Firefox、Apple的Safari、Microsoft的IE和Edge还有小众的Opera以及国内的各种“换壳”的“极速”“安全”浏览器。
<img src="https://static001.geekbang.org/resource/image/61/8b/613fffb6defee1735431dc5f89085d8b.png" alt="unpreview">
那么你想过没有,所谓的“浏览器”到底是个什么东西呢?
浏览器的正式名字叫“**Web Browser**”顾名思义就是检索、查看互联网上网页资源的应用程序名字里的Web实际上指的就是“World Wide Web”也就是万维网。
浏览器本质上是一个HTTP协议中的**请求方**使用HTTP协议获取网络上的各种资源。当然为了让我们更好地检索查看网页它还集成了很多额外的功能。
例如HTML排版引擎用来展示页面JavaScript引擎用来实现动态化效果甚至还有开发者工具用来调试网页以及五花八门的各种插件和扩展。
在HTTP协议里浏览器的角色被称为“User Agent”即“用户代理”意思是作为访问者的“代理”来发起HTTP请求。不过在不引起混淆的情况下我们通常都简单地称之为“客户端”。
## Web服务器
刚才说的浏览器是HTTP里的请求方那么在协议另一端的**应答方**(响应方)又是什么呢?
这个你一定也很熟悉,答案就是**服务器****Web Server**。
Web服务器是一个很大也很重要的概念它是HTTP协议里响应请求的主体通常也把控着绝大多数的网络资源在网络世界里处于强势地位。
当我们谈到“Web服务器”时有两个层面的含义硬件和软件。
**硬件**含义就是物理形式或“云”形式的机器,在大多数情况下它可能不是一台服务器,而是利用反向代理、负载均衡等技术组成的庞大集群。但从外界看来,它仍然表现为一台机器,但这个形象是“虚拟的”。
**软件**含义的Web服务器可能我们更为关心它就是提供Web服务的应用程序通常会运行在硬件含义的服务器上。它利用强大的硬件能力响应海量的客户端HTTP请求处理磁盘上的网页、图片等静态文件或者把请求转发给后面的Tomcat、Node.js等业务应用返回动态的信息。
比起层出不穷的各种Web浏览器Web服务器就要少很多了一只手的手指头就可以数得过来。
Apache是老牌的服务器到今天已经快25年了功能相当完善相关的资料很多学习门槛低是许多创业者建站的入门产品。
Nginx是Web服务器里的后起之秀特点是高性能、高稳定且易于扩展。自2004年推出后就不断蚕食Apache的市场份额在高流量的网站里更是不二之选。
此外还有Windows上的IIS、Java的Jetty/Tomcat等因为性能不是很高所以在互联网上应用得较少。
## CDN
浏览器和服务器是HTTP协议的两个端点那么在这两者之间还有别的什么东西吗
当然有了。浏览器通常不会直接连到服务器中间会经过“重重关卡”其中的一个重要角色就叫做CDN。
**CDN**全称是“Content Delivery Network”翻译过来就是“内容分发网络”。它应用了HTTP协议里的缓存和代理技术代替源站响应客户端的请求。
CDN有什么好处呢
简单来说它可以缓存源站的数据让浏览器的请求不用“千里迢迢”地到达源站服务器直接在“半路”就可以获取响应。如果CDN的调度算法很优秀更可以找到离用户最近的节点大幅度缩短响应时间。
打个比方,就好像唐僧西天取经,刚出长安城,就看到阿难与迦叶把佛祖的真经递过来了,是不是很省事?
CDN也是现在互联网中的一项重要基础设施除了基本的网络加速外还提供负载均衡、安全防护、边缘计算、跨运营商网络等功能能够成倍地“放大”源站服务器的服务能力很多云服务商都把CDN作为产品的一部分我也会在后面用一讲的篇幅来专门讲解CDN。
## 爬虫
前面说到过浏览器,它是一种用户代理,代替我们访问互联网。
但HTTP协议并没有规定用户代理后面必须是“真正的人类”它也完全可以是“机器人”这些“机器人”的正式名称就叫做“**爬虫**”Crawler实际上是一种可以自动访问Web资源的应用程序。
“爬虫”这个名字非常形象,它们就像是一只只不知疲倦的、辛勤的蚂蚁,在无边无际的网络上爬来爬去,不停地在网站间奔走,搜集抓取各种信息。
据估计互联网上至少有50%的流量都是由爬虫产生的,某些特定领域的比例还会更高,也就是说,如果你的网站今天的访问量是十万,那么里面至少有五六万是爬虫机器人,而不是真实的用户。
爬虫是怎么来的呢?
绝大多数是由各大搜索引擎“放”出来的,抓取网页存入庞大的数据库,再建立关键字索引,这样我们才能够在搜索引擎中快速地搜索到互联网角落里的页面。
爬虫也有不好的一面它会过度消耗网络资源占用服务器和带宽影响网站对真实数据的分析甚至导致敏感信息泄漏。所以又出现了“反爬虫”技术通过各种手段来限制爬虫。其中一项就是“君子协定”robots.txt约定哪些该爬哪些不该爬。
无论是“爬虫”还是“反爬虫”用到的基本技术都是两个一个是HTTP另一个就是HTML。
## HTML/WebService/WAF
到现在我已经说完了图中右边的五大部分而左边的HTML、WebService、WAF等由于与HTTP技术上实质关联不太大所以就简略地介绍一下不再过多展开。
**HTML**是HTTP协议传输的主要内容之一它描述了超文本页面用各种“标签”定义文字、图片等资源和排版布局最终由浏览器“渲染”出可视化页面。
HTML目前有两个主要的标准HTML4和HTML5。广义上的HTML通常是指HTML、JavaScript、CSS等前端技术的组合能够实现比传统静态页面更丰富的动态页面。
接下来是**Web** **Service**它的名字与Web Server很像但却是一个完全不同的东西。
Web Service是一种由W3C定义的应用服务开发规范使用client-server主从架构通常使用WSDL定义服务接口使用HTTP协议传输XML或SOAP消息也就是说它是**一个基于WebHTTP的服务架构技术**,既可以运行在内网,也可以在适当保护后运行在外网。
因为采用了HTTP协议传输数据所以在Web Service架构里服务器和客户端可以采用不同的操作系统或编程语言开发。例如服务器端用Linux+Java客户端用Windows+C#,具有跨平台跨语言的优点。
**WAF**是近几年比较“火”的一个词意思是“网络应用防火墙”。与硬件“防火墙”类似它是应用层面的“防火墙”专门检测HTTP流量是防护Web应用的安全技术。
WAF通常位于Web服务器之前可以阻止如SQL注入、跨站脚本等攻击目前应用较多的一个开源项目是ModSecurity它能够完全集成进Apache或Nginx。
## 小结
今天我详细介绍了与HTTP有关系的各种应用技术在这里简单小结一下要点。
1. 互联网上绝大部分资源都使用HTTP协议传输
1. 浏览器是HTTP协议里的请求方即User Agent
1. 服务器是HTTP协议里的应答方常用的有Apache和Nginx
1. CDN位于浏览器和服务器之间主要起到缓存加速的作用
1. 爬虫是另一类User Agent是自动访问网络资源的程序。
希望通过今天的讲解,你能够更好地理解这些概念,也利于后续的课程学习。
## 课下作业
1. 你觉得CDN在对待浏览器和爬虫时会有差异吗为什么
1. 你怎么理解WebService与Web Server这两个非常相似的词
欢迎你通过留言分享答案,与我和其他同学一起讨论。如果你觉得有所收获,欢迎你把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/9a/5a/9ae4483ad53e403464869f227678cf5a.png" alt="unpreview">

View File

@@ -0,0 +1,148 @@
<audio id="audio" title="04 | HTTP世界全览与HTTP相关的各种协议" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/99/51/995cf58219a4e99589c7d58ffa66a451.mp3"></audio>
在上一讲中我介绍了与HTTP相关的浏览器、服务器、CDN、网络爬虫等应用技术。
今天要讲的则是比较偏向于理论的各种HTTP相关协议重点是TCP/IP、DNS、URI、HTTPS等希望能够帮你理清楚它们与HTTP的关系。
同样的,我还是画了一张详细的思维导图,你可以点击后仔细查看。
<img src="https://static001.geekbang.org/resource/image/1e/81/1e7533f765d2ede0abfab73cf6b57781.png" alt="">
## TCP/IP
TCP/IP协议是目前网络世界“事实上”的标准通信协议即使你没有用过也一定听说过因为它太著名了。
TCP/IP协议实际上是一系列网络通信协议的统称其中最核心的两个协议是**TCP**和**IP**其他的还有UDP、ICMP、ARP等等共同构成了一个复杂但有层次的协议栈。
这个协议栈有四层最上层是“应用层”最下层是“链接层”TCP和IP则在中间**TCP属于“传输层”IP属于“网际层”**。协议的层级关系模型非常重要,我会在下一讲中再专门讲解,这里先暂时放一放。
**IP协议**是“**I**nternet **P**rotocol”的缩写主要目的是解决寻址和路由问题以及如何在两点间传送数据包。IP协议使用“**IP地址**”的概念来定位互联网上的每一台计算机。可以对比一下现实中的电话系统你拿着的手机相当于互联网上的计算机而要打电话就必须接入电话网由通信公司给你分配一个号码这个号码就相当于IP地址。
现在我们使用的IP协议大多数是v4版地址是四个用“.”分隔的数字例如“192.168.0.1”总共有2^32大约42亿个可以分配的地址。看上去好像很多但互联网的快速发展让地址的分配管理很快就“捉襟见肘”。所以就又出现了v6版使用8组“:”分隔的数字作为地址容量扩大了很多有2^128个在未来的几十年里应该是足够用了。
**TCP协议**是“**T**ransmission **C**ontrol **P**rotocol”的缩写意思是“传输控制协议”它位于IP协议之上基于IP协议提供可靠的、字节流形式的通信是HTTP协议得以实现的基础。
“可靠”是指保证数据不丢失“字节流”是指保证数据完整所以在TCP协议的两端可以如同操作文件一样访问传输的数据就像是读写在一个密闭的管道里“流动”的字节。
在[第2讲](https://time.geekbang.org/column/article/98128)时我曾经说过HTTP是一个"传输协议"但它不关心寻址、路由、数据完整性等传输细节而要求这些工作都由下层来处理。因为互联网上最流行的是TCP/IP协议而它刚好满足HTTP的要求所以互联网上的HTTP协议就运行在了TCP/IP上HTTP也就可以更准确地称为“**HTTP over TCP/IP**”。
## DNS
在TCP/IP协议中使用IP地址来标识计算机数字形式的地址对于计算机来说是方便了但对于人类来说却既难以记忆又难以输入。
于是“**域名系统**”(**D**omain **N**ame **S**ystem出现了用有意义的名字来作为IP地址的等价替代。设想一下你是愿意记“95.211.80.227”这样枯燥的数字还是“nginx.org”这样的词组呢
在DNS中“域名”Domain Name又称为“主机名”Host为了更好地标记不同国家或组织的主机让名字更好记所以被设计成了一个有层次的结构。
域名用“.”分隔成多个单词级别从左到右逐级升高最右边的被称为“顶级域名”。对于顶级域名可能你随口就能说出几个例如表示商业公司的“com”、表示教育机构的“edu”表示国家的“cn”“uk”等买火车票时的域名还记得吗是“www.12306.cn”。
<img src="https://static001.geekbang.org/resource/image/36/b3/36b6a41da6e9abc2fc28ee9a305f48b3.jpg" alt="unpreview">
但想要使用TCP/IP协议来通信仍然要使用IP地址所以需要把域名做一个转换“映射”到它的真实IP这就是所谓的“**域名解析**”。
继续用刚才的打电话做个比喻你想要打电话给小明但不知道电话号码就得在手机里的号码簿里一项一项地找直到找到小明那一条记录然后才能查到号码。这里的“小明”就相当于域名而“电话号码”就相当于IP地址这个查找的过程就是域名解析。
域名解析的实际操作要比刚才的例子复杂很多因为互联网上的电脑实在是太多了。目前全世界有13组根DNS服务器下面再有许多的顶级DNS、权威DNS和更小的本地DNS逐层递归地实现域名查询。
HTTP协议中并没有明确要求必须使用DNS但实际上为了方便访问互联网上的Web服务器通常都会使用DNS来定位或标记主机名间接地把DNS与HTTP绑在了一起。
## URI/URL
有了TCP/IP和DNS是不是我们就可以任意访问网络上的资源了呢
还不行DNS和IP地址只是标记了互联网上的主机但主机上有那么多文本、图片、页面到底要找哪一个呢就像小明管理了一大堆文档你怎么告诉他是哪个呢
所以就出现了URI**U**niform **R**esource **I**dentifier中文名称是 **统一资源标识符**,使用它就能够唯一地标记互联网上资源。
URI另一个更常用的表现形式是URL**U**niform **R**esource **L**ocator **统一资源定位符**也就是我们俗称的“网址”它实际上是URI的一个子集不过因为这两者几乎是相同的差异不大所以通常不会做严格的区分。
我就拿Nginx网站来举例看一下URI是什么样子的。
```
http://nginx.org/en/download.html
```
你可以看到URI主要有三个基本的部分构成
1. 协议名即访问该资源应当使用的协议在这里是“http”
1. 主机名即互联网上主机的标记可以是域名或IP地址在这里是“nginx.org”
1. 路径:即资源在主机上的位置,使用“/”分隔多级目录,在这里是“/en/download.html”。
还是用打电话来做比喻你通过电话簿找到了小明让他把昨天做好的宣传文案快递过来。那么这个过程中你就完成了一次URI资源访问“小明”就是“主机名”“昨天做好的宣传文案”就是“路径”而“快递”就是你要访问这个资源的“协议名”。
## HTTPS
在TCP/IP、DNS和URI的“加持”之下HTTP协议终于可以自由地穿梭在互联网世界里顺利地访问任意的网页了真的是“好生快活”。
但且慢,互联网上不仅有“美女”,还有很多的“野兽”。
假设你打电话找小明要一份广告创意,很不幸,电话被商业间谍给窃听了,他立刻动用种种手段偷窃了你的快递,就在你还在等包裹的时候,他抢先发布了这份广告,给你的公司造成了无形或有形的损失。
有没有什么办法能够防止这种情况的发生呢?确实有。你可以使用“加密”的方法,比如这样打电话:
>
<p>你:“喂,小明啊,接下来我们改用火星文通话吧。”<br>
小明:“好啊好啊,就用火星文吧。”<br>
你:“巴拉巴拉巴拉巴拉……”<br>
小明:“巴拉巴拉巴拉巴拉……”</p>
如果你和小明说的火星文只有你们两个才懂,那么即使窃听到了这段谈话,他也不会知道你们到底在说什么,也就无从破坏你们的通话过程。
HTTPS就相当于这个比喻中的“火星文”它的全称是“**HTTP over SSL/TLS**”也就是运行在SSL/TLS协议上的HTTP。
注意它的名字这里是SSL/TLS而不是TCP/IP它是一个负责加密通信的安全协议建立在TCP/IP之上所以也是个可靠的传输协议可以被用作HTTP的下层。
因为HTTPS相当于“HTTP+SSL/TLS+TCP/IP”其中的“HTTP”和“TCP/IP”我们都已经明白了只要再了解一下SSL/TLSHTTPS也就能够轻松掌握。
SSL的全称是“**S**ecure **S**ocket **L**ayer”由网景公司发明当发展到3.0时被标准化改名为TLS即“**T**ransport **L**ayer **S**ecurity”但由于历史的原因还是有很多人称之为SSL/TLS或者直接简称为SSL。
SSL使用了许多密码学最先进的研究成果综合了对称加密、非对称加密、摘要算法、数字签名、数字证书等技术能够在不安全的环境中为通信的双方创建出一个秘密的、安全的传输通道为HTTP套上一副坚固的盔甲。
你可以在今后上网时留心看一下浏览器地址栏如果有一个小锁头标志那就表明网站启用了安全的HTTPS协议而URI里的协议名也从“http”变成了“https”。
## 代理
代理Proxy是HTTP协议中请求方和应答方中间的一个环节作为“中转站”既可以转发客户端的请求也可以转发服务器的应答。
代理有很多的种类,常见的有:
1. 匿名代理:完全“隐匿”了被代理的机器,外界看到的只是代理服务器;
1. 透明代理:顾名思义,它在传输过程中是“透明开放”的,外界既知道代理,也知道客户端;
1. 正向代理:靠近客户端,代表客户端向服务器发送请求;
1. 反向代理:靠近服务器端,代表服务器响应客户端的请求;
上一讲提到的CDN实际上就是一种代理它代替源站服务器响应客户端的请求通常扮演着透明代理和反向代理的角色。
由于代理在传输过程中插入了一个“中间层”,所以可以在这个环节做很多有意思的事情,比如:
1. 负载均衡:把访问请求均匀分散到多台机器,实现访问集群化;
1. 内容缓存:暂存上下行的数据,减轻后端的压力;
1. 安全防护隐匿IP,使用WAF等工具抵御网络攻击保护被代理的机器
1. 数据处理:提供压缩、加密等额外的功能。
关于HTTP的代理还有一个特殊的“代理协议”proxy protocol它由知名的代理软件HAProxy制订但并不是RFC标准我也会在之后的课程里专门讲解。
## 小结
这次我介绍了与HTTP相关的各种协议在这里简单小结一下今天的内容。
1. TCP/IP是网络世界最常用的协议HTTP通常运行在TCP/IP提供的可靠传输基础上
1. DNS域名是IP地址的等价替代需要用域名解析实现到IP地址的映射
1. URI是用来标记互联网上资源的一个名字由“协议名+主机名+路径”构成俗称URL
1. HTTPS相当于“HTTP+SSL/TLS+TCP/IP”为HTTP套了一个安全的外壳
1. 代理是HTTP传输过程中的“中转站”可以实现缓存加速、负载均衡等功能。
经过这两讲的学习相信你应该对HTTP有了一个比较全面的了解虽然还不是很深入但已经为后续的学习扫清了障碍。
## 课下作业
1. DNS与URI有什么关系
1. 在讲**代理**时我特意没有举例说明,你能够用引入一个“小强”的角色,通过打电话来比喻一下吗?
欢迎你通过留言分享答案,与我和其他同学一起讨论。如果你觉得有所收获,欢迎你把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/4e/56/4eab55dc3600071330e088b40cae4856.png" alt="unpreview">

View File

@@ -0,0 +1,147 @@
<audio id="audio" title="05 | 常说的“四层”和“七层”到底是什么?“五层”“六层”哪去了?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/26/de/26833f97618434f8a57449e97f4da3de.mp3"></audio>
在上一讲中我简单提到了TCP/IP协议它是HTTP协议的下层协议负责具体的数据传输工作。并且还特别说了TCP/IP协议是一个“**有层次的协议栈**”。
在工作中你一定经常听别人谈起什么“四层负载均衡”“七层负载均衡”,什么“二层转发”“三层路由”,那么你真正理解这些层次的含义吗?
网络分层的知识教科书上都有,但很多都是“泛泛而谈”,只有“学术价值”,于是就容易和实际应用“脱节”,造成的后果就是“似懂非懂”,真正用的时候往往会“一头雾水”。
所以今天我就从HTTP应用的角度帮你把这些模糊的概念弄清楚。
## TCP/IP网络分层模型
还是先从TCP/IP协议开始讲起一是因为它非常经典二是因为它是目前事实上的网络通信标准研究它的实用价值最大。
TCP/IP当初的设计者真的是非常聪明创造性地提出了“**分层**”的概念,把复杂的网络通信划分出多个层次,再给每一个层次分配不同的职责,层次内只专心做自己的事情就好,用“分而治之”的思想把一个“大麻烦”拆分成了数个“小麻烦”,从而解决了网络通信的难题。
你应该对TCP/IP的协议栈有所了解吧这里我再贴一下层次图。
<img src="https://static001.geekbang.org/resource/image/2b/03/2b8fee82b58cc8da88c74a33f2146703.png" alt="">
TCP/IP协议总共有四层就像搭积木一样每一层需要下层的支撑同时又支撑着上层任何一层被抽掉都可能会导致整个协议栈坍塌。
我们来仔细地看一下这个精巧的积木架构,注意它的层次顺序是“**从下往上**”数的,所以第一层就是最下面的一层。
第一层叫“**链接层**”link layer负责在以太网、WiFi这样的底层网络上发送原始数据包工作在网卡这个层次使用MAC地址来标记网络上的设备所以有时候也叫MAC层。
第二层叫“**网际层**”或者“**网络互连层**”internet layerIP协议就处在这一层。因为IP协议定义了“IP地址”的概念所以就可以在“链接层”的基础上用IP地址取代MAC地址把许许多多的局域网、广域网连接成一个虚拟的巨大网络在这个网络里找设备时只要把IP地址再“翻译”成MAC地址就可以了。
第三层叫“**传输层**”transport layer这个层次协议的职责是保证数据在IP地址标记的两点之间“可靠”地传输是TCP协议工作的层次另外还有它的一个“小伙伴”UDP。
TCP是一个有状态的协议需要先与对方建立连接然后才能发送数据而且保证数据不丢失不重复。而UDP则比较简单它无状态不用事先建立连接就可以任意发送数据但不保证数据一定会发到对方。两个协议的另一个重要区别在于数据的形式。TCP的数据是连续的“字节流”有先后顺序而UDP则是分散的小数据包是顺序发乱序收。
关于TCP和UDP可以展开讨论的话题还有很多比如最经典的“三次握手”和“四次挥手”一时半会很难说完好在与HTTP的关系不是太大以后遇到了再详细讲解。
协议栈的第四层叫“**应用层**”application layer由于下面的三层把基础打得非常好所以在这一层就“百花齐放”了有各种面向具体应用的协议。例如Telnet、SSH、FTP、SMTP等等当然还有我们的HTTP。
MAC层的传输单位是帧frameIP层的传输单位是包packetTCP层的传输单位是段segmentHTTP的传输单位则是消息或报文message。但这些名词并没有什么本质的区分可以统称为数据包。
## OSI网络分层模型
看完TCP/IP协议栈你可能要问了“它只有四层那常说的七层怎么没见到呢
别着急,这就是今天要说的第二个网络分层模型:**OSI**,全称是“**开放式系统互联通信参考模型**”Open System Interconnection Reference Model
TCP/IP发明于1970年代当时除了它还有很多其他的网络协议整个网络世界比较混乱。
这个时候国际标准组织ISO注意到了这种现象感觉“野路子”太多就想要来个“大一统”。于是设计出了一个新的网络分层模型想用这个新框架来统一既存的各种网络协议。
OSI模型分成了七层部分层次与TCP/IP很像从下到上分别是
<img src="https://static001.geekbang.org/resource/image/3a/dc/3abcf1462621ff86758a8d9571c07cdc.png" alt="">
1. 第一层:物理层,网络的物理形式,例如电缆、光纤、网卡、集线器等等;
1. 第二层数据链路层它基本相当于TCP/IP的链接层
1. 第三层网络层相当于TCP/IP里的网际层
1. 第四层传输层相当于TCP/IP里的传输层
1. 第五层:会话层,维护网络中的连接状态,即保持会话和同步;
1. 第六层:表示层,把数据转换为合适、可理解的语法和语义;
1. 第七层:应用层,面向具体的应用传输数据。
至此,我们常说的“四层”“七层”就出现了。
不过国际标准组织心里也很清楚TCP/IP等协议已经在许多网络上实际运行再推翻重来是不可能的。所以OSI分层模型在发布的时候就明确地表明是一个“参考”不是强制标准意思就是说“你们以后该干什么还干什么我不管但面子上还是要按照我说的来”。
但OSI模型也是有优点的。对比一下就可以看出TCP/IP是一个纯软件的栈没有网络应有的最根基的电缆、网卡等物理设备的位置。而OSI则补足了这个缺失在理论层面上描述网络更加完整。
还有一个重要的形式上的优点OSI为每一层标记了明确了编号最底层是一层最上层是七层而TCP/IP的层次从来只有名字而没有编号。显然在交流的时候说“七层”要比“应用层”更简单快捷特别是英文对比一下“Layer seven”与“application layer”。
综合以上几点在OSI模型之后“四层”“七层”这样的说法就逐渐流行开了。不过在实际工作中你一定要注意这种说法只是“理论上”的层次并不是与现实完全对应。
## 两个分层模型的映射关系
现在我们有了两个网络分层模型TCP/IP和OSI新的问题又出现了一个是四层模型一个是七层模型这两者应该如何互相映射或者说互相解释呢
好在OSI在设计之初就参考了TCP/IP等多个协议可以比较容易但不是很精确地实现对应关系。
<img src="https://static001.geekbang.org/resource/image/9d/94/9d9b3c9274465c94e223676b6d434194.png" alt="">
1. 第一层物理层TCP/IP里无对应
1. 第二层数据链路层对应TCP/IP的链接层
1. 第三层网络层对应TCP/IP的网际层
1. 第四层传输层对应TCP/IP的传输层
1. 第五、六、七层统一对应到TCP/IP的应用层。
所以你看,这就是“理想与现实”之间的矛盾。理想很美好,有七层,但现实很残酷,只有四层,“多余”的五层、六层就这样“消失”了。
但这也有一定的实际原因。
OSI的分层模型在四层以上分的太细而TCP/IP实际应用时的会话管理、编码转换、压缩等和具体应用经常联系的很紧密很难分开。例如HTTP协议就同时包含了连接管理和数据格式定义。
到这里,你应该能够明白一开始那些“某某层”的概念了。
所谓的“四层负载均衡”就是指工作在传输层上基于TCP/IP协议的特性例如IP地址、端口号等实现对后端服务器的负载均衡。
所谓的“七层负载均衡”就是指工作在应用层上看到的是HTTP协议解析HTTP报文里的URI、主机名、资源类型等数据再用适当的策略转发给后端服务器。
## TCP/IP协议栈的工作方式
TCP/IP协议栈是如何工作的呢
你可以把HTTP利用TCP/IP协议栈传输数据想象成一个发快递的过程。
假设你想把一件毛绒玩具送给朋友但你要先拿个塑料袋套一下这件玩具就相当于HTTP协议里要传输的内容比如HTML然后HTTP协议为它加一个HTTP专用附加数据。
你把玩具交给快递小哥为了保护货物他又加了层包装再贴了个标签相当于在TCP层给数据再次打包加上了TCP头。
接着快递小哥下楼把包裹放进了三轮车里运到集散点然后再装进更大的卡车里相当于在IP层、MAC层对TCP数据包加上了IP头、MAC头。
之后经过漫长的运输包裹到达目的地要卸货再放进另一位快递员的三轮车就是在IP层、MAC层传输后拆包。
快递员到了你朋友的家门口撕掉标签去除了TCP层的头你朋友再拆掉塑料袋包装也就是HTTP头最后就拿到了玩具也就是真正的HTML页面。
这个比喻里省略了很多TCP/IP协议里的细节比如建连、路由、数据切分与重组、错误检查等但核心的数据传输过程是差不多的。
HTTP协议的传输过程就是这样通过协议栈逐层向下每一层都添加本层的专有数据层层打包然后通过下层发送出去。
接收数据则是相反的操作,从下往上穿过协议栈,逐层拆包,每层去掉本层的专有头,上层就会拿到自己的数据。
但下层的传输过程对于上层是完全“透明”的上层也不需要关心下层的具体实现细节所以就HTTP层次来看它不管下层是不是TCP/IP协议看到的只是一个可靠的传输链路只要把数据加上自己的头对方就能原样收到。
我为这个过程画了一张图,你可以对照着加深理解。
<img src="https://static001.geekbang.org/resource/image/70/6f/70bc19acacf2245fa841349f15cb7a6f.png" alt="">
## 小结
这次我们学习了HTTP所在的网络分层模型它是工作中常用的交流语言在这里简单小结一下今天的内容。
1. TCP/IP分为四层核心是二层的IP和三层的TCPHTTP在第四层
1. OSI分为七层基本对应TCP/IPTCP在第四层HTTP在第七层
1. OSI可以映射到TCP/IP但这期间一、五、六层消失了
1. 日常交流的时候我们通常使用OSI模型用四层、七层等术语
1. HTTP利用TCP/IP协议栈逐层打包再拆包实现了数据传输但下面的细节并不可见。
有一个辨别四层和七层比较好的(但不是绝对的)小窍门,“**两个凡是**”:凡是由操作系统负责处理的就是四层或四层以下,否则,凡是需要由应用程序(也就是你自己写代码)负责处理的就是七层。
## 课下作业
1. 你能用自己的话解释一下“二层转发”“三层路由”吗?
1. 你认为上一讲中的DNS协议位于哪一层呢
1. 你认为CDN工作在那一层呢
欢迎你把自己的答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/30/92/3010b80124bca2f3a91a726713e9ac92.png" alt="unpreview">

View File

@@ -0,0 +1,136 @@
<audio id="audio" title="06 | 域名里有哪些门道?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/81/3e/81cf8701700965c6fea30f406497bc3e.mp3"></audio>
在上一讲里我们学习了HTTP协议使用的TCP/IP协议栈知道了HTTP协议是运行在TCP/IP上的。
IP协议的职责是“网际互连”它在MAC层之上使用IP地址把MAC编号转换成了四位数字这就对物理网卡的MAC地址做了一层抽象发展出了许多的“新玩法”。
例如分为A、B、C、D、E五种类型公有地址和私有地址掩码分割子网等。只要每个小网络在IP地址这个概念上达成一致不管它在MAC层有多大的差异都可以接入TCP/IP协议栈最终汇合进整个互联网。
但接入互联网的计算机越来越多IP地址的缺点也就暴露出来了最主要的是它“对人不友好”虽然比MAC的16进制数要好一点但还是难于记忆和输入。
怎么解决这个问题呢?
那就“以其人之道还治其人之身”在IP地址之上再来一次抽象把数字形式的IP地址转换成更有意义更好记的名字在字符串的层面上再增加“新玩法”。于是DNS域名系统就这么出现了。
## 域名的形式
在第4讲曾经说过域名是一个有层次的结构是一串用“.”分隔的多个单词,最右边的被称为“顶级域名”,然后是“二级域名”,层级关系向左依次降低。
最左边的是主机名通常用来表明主机的用途比如“www”表示提供万维网服务、“mail”表示提供邮件服务不过这也不是绝对的名字的关键是要让我们容易记忆。
看一下极客时间的域名“time.geekbang.org”这里的“org”就是顶级域名“geekbang”是二级域名“time”则是主机名。使用这个域名DNS就会把它转换成相应的IP地址你就可以访问极客时间的网站了。
域名不仅能够代替IP地址还有许多其他的用途。
在Apache、Nginx这样的Web服务器里域名可以用来标识虚拟主机决定由哪个虚拟主机来对外提供服务比如在Nginx里就会使用“server_name”指令
```
server {
listen 80; #监听80端口
server_name time.geekbang.org; #主机名是time.geekbang.org
...
}
```
域名本质上还是个名字空间系统,使用多级域名就可以划分出不同的国家、地区、组织、公司、部门,每个域名都是独一无二的,可以作为一种身份的标识。
举个例子吧假设A公司里有个小明B公司里有个小强于是他们就可以分别说是“小明.A公司”“小强.B公司”即使B公司里也有个小明也不怕可以标记为“小明.B公司”很好地解决了重名问题。
因为这个特性域名也被扩展到了其他应用领域比如Java的包机制就采用域名作为命名空间只是它使用了反序。如果极客时间要开发Java应用那么它的包名可能就是“org.geekbang.time”。
而XML里使用URI作为名字空间也是间接使用了域名。
## 域名的解析
就像IP地址必须转换成MAC地址才能访问主机一样域名也必须要转换成IP地址这个过程就是“**域名解析**”。
目前全世界有几亿个站点有几十亿网民而每天网络上发生的HTTP流量更是天文数字。这些请求绝大多数都是基于域名来访问网站的所以DNS就成了互联网的重要基础设施必须要保证域名解析稳定可靠、快速高效。
DNS的核心系统是一个三层的树状、分布式服务基本对应域名的结构
1. 根域名服务器Root DNS Server管理顶级域名服务器返回“com”“net”“cn”等顶级域名服务器的IP地址
1. 顶级域名服务器Top-level DNS Server管理各自域名下的权威域名服务器比如com顶级域名服务器可以返回apple.com域名服务器的IP地址
1. 权威域名服务器Authoritative DNS Server管理自己域名下主机的IP地址比如apple.com权威域名服务器可以返回www.apple.com的IP地址。
<img src="https://static001.geekbang.org/resource/image/6b/f2/6b020454987543efdd1cf6ddec784bf2.png" alt="">
在这里根域名服务器是关键它必须是众所周知的否则下面的各级服务器就无从谈起了。目前全世界共有13组根域名服务器又有数百台的镜像保证一定能够被访问到。
有了这个系统以后任何一个域名都可以在这个树形结构里从顶至下进行查询就好像是把域名从右到左顺序走了一遍最终就获得了域名对应的IP地址。
例如你要访问“www.apple.com”就要进行下面的三次查询
1. 访问根域名服务器它会告诉你“com”顶级域名服务器的地址
1. 访问“com”顶级域名服务器它再告诉你“apple.com”域名服务器的地址
1. 最后访问“apple.com”域名服务器就得到了“www.apple.com”的地址。
虽然核心的DNS系统遍布全球服务能力很强也很稳定但如果全世界的网民都往这个系统里挤即使不挤瘫痪了访问速度也会很慢。
所以在核心DNS系统之外还有两种手段用来减轻域名解析的压力并且能够更快地获取结果基本思路就是“**缓存**”。
首先许多大公司、网络运行商都会建立自己的DNS服务器作为用户DNS查询的代理代替用户访问核心DNS系统。这些“野生”服务器被称为“非权威域名服务器”可以缓存之前的查询结果如果已经有了记录就无需再向根服务器发起查询直接返回对应的IP地址。
这些DNS服务器的数量要比核心系统的服务器多很多而且大多部署在离用户很近的地方。比较知名的DNS有Google的“8.8.8.8”Microsoft的“4.2.2.1”还有CloudFlare的“1.1.1.1”等等。
其次操作系统里也会对DNS解析结果做缓存如果你之前访问过“www.apple.com”那么下一次在浏览器里再输入这个网址的时候就不会再跑到DNS那里去问了直接在操作系统里就可以拿到IP地址。
另外操作系统里还有一个特殊的“主机映射”文件通常是一个可编辑的文本在Linux里是“/etc/hosts”在Windows里是“C:\WINDOWS\system32\drivers\etc\hosts”如果操作系统在缓存里找不到DNS记录就会找这个文件。
有了上面的“野生”DNS服务器、操作系统缓存和hosts文件后很多域名解析的工作就都不用“跋山涉水”了直接在本地或本机就能解决不仅方便了用户也减轻了各级DNS服务器的压力效率就大大提升了。
下面的这张图比较完整地表示了现在的DNS架构。
<img src="https://static001.geekbang.org/resource/image/e5/ac/e51df3245609880641043af65bba94ac.png" alt="">
在Nginx里有这么一条配置指令“resolver”它就是用来配置DNS服务器的如果没有它那么Nginx就无法查询域名对应的IP也就无法反向代理到外部的网站。
```
resolver 8.8.8.8 valid=30s; #指定Google的DNS缓存30秒
```
## 域名的“新玩法”
有了域名又有了可以稳定工作的解析系统于是我们就可以实现比IP地址更多的“新玩法”了。
第一种也是最简单的“重定向”。因为域名代替了IP地址所以可以让对外服务的域名不变而主机的IP地址任意变动。当主机有情况需要下线、迁移时可以更改DNS记录让域名指向其他的机器。
比如你有一台“buy.tv”的服务器要临时停机维护那你就可以通知DNS服务器“我这个buy.tv域名的地址变了啊原先是1.2.3.4现在是5.6.7.8麻烦你改一下。”DNS于是就修改内部的IP地址映射关系之后再有访问buy.tv的请求就不走1.2.3.4这台主机改由5.6.7.8来处理,这样就可以保证业务服务不中断。
第二种因为域名是一个名字空间所以可以使用bind9等开源软件搭建一个在内部使用的DNS作为名字服务器。这样我们开发的各种内部服务就都用域名来标记比如数据库服务都用域名“mysql.inner.app”商品服务都用“goods.inner.app”发起网络通信时也就不必再使用写死的IP地址了可以直接用域名而且这种方式也兼具了第一种“玩法”的优势。
第三种“玩法”包含了前两种,也就是基于域名实现的负载均衡。
这种“玩法”也有两种方式,两种方式可以混用。
第一种方式因为域名解析可以返回多个IP地址所以一个域名可以对应多台主机客户端收到多个IP地址后就可以自己使用轮询算法依次向服务器发起请求实现负载均衡。
第二种方式域名解析可以配置内部的策略返回离客户端最近的主机或者返回当前服务质量最好的主机这样在DNS端把请求分发到不同的服务器实现负载均衡。
前面我们说的都是可信的DNS如果有一些不怀好意的DNS那么它也可以在域名这方面“做手脚”弄一些比较“恶意”的“玩法”举两个例子
- “域名屏蔽”对域名直接不解析返回错误让你无法拿到IP地址也就无法访问网站
- “域名劫持”也叫“域名污染”你要访问A网站但DNS给了你B网站。
好在互联网上还是好人多而且DNS又是互联网的基础设施这些“恶意DNS”并不多见你上网的时候不需要太过担心。
## 小结
这次我们学习了与HTTP协议有重要关系的域名和DNS在这里简单小结一下今天的内容
1. 域名使用字符串来代替IP地址方便用户记忆本质上一个名字空间系统
1. DNS就像是我们现实世界里的电话本、查号台统管着互联网世界里的所有网站是一个“超级大管家”
1. DNS是一个树状的分布式查询系统但为了提高查询效率外围有多级的缓存
1. 使用DNS可以实现基于域名的负载均衡既可以在内网也可以在外网。
## 课下作业
1. 在浏览器地址栏里随便输入一个不存在的域名比如就叫“www.不存在.com”试着解释一下它的DNS解析过程。
1. 如果因为某些原因DNS失效或者出错了会出现什么后果
欢迎你把自己的答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/78/36/7838e0e705864ddfeacc79e0aeb8f236.png" alt="unpreview">

View File

@@ -0,0 +1,155 @@
<audio id="audio" title="07 | 自己动手搭建HTTP实验环境" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/5a/80/5a81d350ff30475e95ccc4261b90b580.mp3"></audio>
这一讲是“破冰篇”的最后一讲我会先简单地回顾一下之前的内容然后在Windows系统上实际操作用几个应用软件搭建出一个“最小化”的HTTP实验环境方便后续的“基础篇”“进阶篇”“安全篇”的学习。
## “破冰篇”回顾
HTTP协议诞生于30年前设计之初的目的是用来传输纯文本数据。但由于形式灵活搭配URI、HTML等技术能够把互联网上的资源都联系起来构成一个复杂的超文本系统让人们自由地获取信息所以得到了迅猛发展。
HTTP有多个版本目前应用的最广泛的是HTTP/1.1它几乎可以说是整个互联网的基石。但HTTP/1.1的性能难以满足如今的高流量网站于是又出现了HTTP/2和HTTP/3。不过这两个新版本的协议还没有完全推广开。在可预见的将来HTTP/1.1还会继续存在下去。
HTTP翻译成中文是“超文本传输协议”是一个应用层的协议通常基于TCP/IP能够在网络的任意两点之间传输文字、图片、音频、视频等数据。
HTTP协议中的两个端点称为**请求方**和**应答方**。请求方通常就是Web浏览器也叫user agent应答方是Web服务器存储着网络上的大部分静态或动态的资源。
在浏览器和服务器之间还有一些“中间人”的角色如CDN、网关、代理等它们也同样遵守HTTP协议可以帮助用户更快速、更安全地获取资源。
HTTP协议不是一个孤立的协议需要下层很多其他协议的配合。最基本的是TCP/IP实现寻址、路由和可靠的数据传输还有DNS协议实现对互联网上主机的定位查找。
对HTTP更准确的称呼是“**HTTP over TCP/IP**”,而另一个“**HTTP over SSL/TLS**”就是增加了安全功能的HTTPS。
## 软件介绍
常言道“实践出真知”又有俗语“光说不练是假把式”。要研究HTTP协议最好有一个实际可操作、可验证的环境通过实际的数据、现象来学习肯定要比单纯的“动嘴皮子”效果要好的多。
现成的环境当然有只要能用浏览器上网就会有HTTP协议就可以进行实验。但现实的网络环境又太复杂了有很多无关的干扰因素这些“噪音”会“淹没”真正有用的信息。
所以,我给你的建议是:搭建一个“**最小化**”的环境在这个环境里仅有HTTP协议的两个端点请求方和应答方去除一切多余的环节从而可以抓住重点快速掌握HTTP的本质。
<img src="https://static001.geekbang.org/resource/image/85/0b/85cadf90dc96cf413afaf8668689ef0b.png" alt="">
简单说一下这个“最小化”环境用到的应用软件:
- Wireshark
- Chrome/Firefox
- Telnet
- OpenResty
**Wireshark**是著名的网络抓包工具能够截获在TCP/IP协议栈中传输的所有流量并按协议类型、地址、端口等任意过滤功能非常强大是学习网络协议的必备工具。
它就像是网络世界里的一台“高速摄像机”,把只在一瞬间发生的网络传输过程如实地“拍摄”下来,事后再“慢速回放”,让我们能够静下心来仔细地分析那一瞬到底发生了什么。
**Chrome**是Google开发的浏览器是目前的主流浏览器之一。它不仅上网方便也是一个很好的调试器对HTTP/1.1、HTTPS、HTTP/2、QUIC等的协议都支持得非常好用F12打开“开发者工具”还可以非常详细地观测HTTP传输全过程的各种数据。
如果你更习惯使用**Firefox**那也没问题其实它和Chrome功能上都差不太多选择自己喜欢的就好。
与Wireshark不同Chrome和Firefox属于“事后诸葛亮”不能观测HTTP传输的过程只能看到结果。
**Telnet**是一个经典的虚拟终端基于TCP协议远程登录主机我们可以使用它来模拟浏览器的行为连接服务器后手动发送HTTP请求把浏览器的干扰也彻底排除能够从最原始的层面去研究HTTP协议。
**OpenResty**你可能比较陌生它是基于Nginx的一个“强化包”里面除了Nginx还有一大堆有用的功能模块不仅支持HTTP/HTTPS还特别集成了脚本语言Lua简化Nginx二次开发方便快速地搭建动态网关更能够当成应用容器来编写业务逻辑。
选择OpenResty而不直接用Nginx的原因是它相当于Nginx的“超集”功能更丰富安装部署更方便。我也会用Lua编写一些服务端脚本实现简单的Web服务器响应逻辑方便实验。
## 安装过程
这个“最小化”环境的安装过程也比较简单,大约只需要你半个小时不到的时间就能搭建完成。
我在GitHub上为本专栏开了一个项目[http_study](https://github.com/chronolaw/http_study.git)可以直接用“git clone”下载或者去Release页面下载打好的[压缩包](https://github.com/chronolaw/http_study/releases)。
我使用的操作环境是Windows 10如果你用的是Mac或者Linux可以用VirtualBox等虚拟机软件安装一个Windows虚拟机再在里面操作或者可以到“答疑篇”的[Linux/Mac实验环境搭建](https://time.geekbang.org/column/article/146833)中查看搭建方法)。
首先你要获取**最新**的http_study项目源码假设clone或解压的目录是“D:\http_study”操作完成后大概是下图这个样子。
<img src="https://static001.geekbang.org/resource/image/86/ee/862511b8ef87f78218631d832927bcee.png" alt="">
Chrome和WireShark的安装比较简单一路按“下一步”就可以了。版本方面使用最新的就好我的版本可能不是最新的Chrome是73WireShark是3.0.0。
Windows 10自带Telnet不需要安装但默认是不启用的需要你稍微设置一下。
打开Windows的设置窗口搜索“Telnet”就会找到“启用或关闭Windows功能”在这个窗口里找到“Telnet客户端”打上对钩就可以了可以参考截图。
<img src="https://static001.geekbang.org/resource/image/1a/47/1af035861c4fd33cb42005eaa1f5f247.png" alt="">
接下来我们要安装OpenResty去它的[官网](http://openresty.org)点击左边栏的“Download”进入下载页面下载适合你系统的版本这里我下载的是64位的1.15.8.1包的名字是“openresty-1.15.8.1-win64.zip”
<img src="https://static001.geekbang.org/resource/image/ee/0a/ee7016fecd79919de550677af32f740a.png" alt="">
然后要注意你必须把OpenResty的压缩包解压到刚才的“D:\http_study”目录里并改名为“openresty”。
<img src="https://static001.geekbang.org/resource/image/5a/b5/5acb89c96041f91bbc747b7e909fd4b5.png" alt="">
安装工作马上就要完成了为了能够让浏览器能够使用DNS域名访问我们的实验环境还要改一下本机的hosts文件位置在“C:\WINDOWS\system32\drivers\etc”在里面添加三行本机IP地址到测试域名的映射你也可以参考GitHub项目里的hosts文件这就相当于在一台物理实机上“托管”了三个虚拟主机。
```
127.0.0.1 www.chrono.com
127.0.0.1 www.metroid.net
127.0.0.1 origin.io
```
注意修改hosts文件需要管理员权限直接用记事本编辑是不行的可以切换管理员身份或者改用其他高级编辑器比如Notepad++,而且改之前最好做个备份。
到这里我们的安装工作就完成了之后你就可以用Wireshark、Chrome、Telnet在这个环境里随意“折腾”弄坏了也不要紧只要把目录删除再来一遍操作就能复原。
## 测试验证
实验环境搭建完了,但还需要把它运行起来,做一个简单的测试验证,看是否运转正常。
首先我们要启动Web服务器也就是OpenResty。
在http_study的“www”目录下有四个批处理文件分别是
<img src="https://static001.geekbang.org/resource/image/e5/da/e5d35bb94c46bfaaf8ce5c143b2bb2da.png" alt="">
- start启动OpenResty服务器
- stop停止OpenResty服务器
- reload重启OpenResty服务器
- list列出已经启动的OpenResty服务器进程。
使用鼠标双击“start”批处理文件就会启动OpenResty服务器在后台运行这个过程可能会有Windows防火墙的警告选择“允许”即可。
运行后鼠标双击“list”可以查看OpenResty是否已经正常启动应该会有两个nginx.exe的后台进程大概是下图的样子。
<img src="https://static001.geekbang.org/resource/image/db/1d/dba34b8a38e98bef92289315db29ee1d.png" alt="">
有了Web服务器后接下来我们要运行Wireshark开始抓包。
因为我们的实验环境运行在本机的127.0.0.1上也就是loopback“环回”地址。所以在Wireshark里要选择“Npcap loopback Adapter”过滤器选择“HTTP TCP port(80)”即只抓取HTTP相关的数据包。鼠标双击开始界面里的“Npcap loopback Adapter”即可开始抓取本机上的网络数据。
<img src="https://static001.geekbang.org/resource/image/12/c4/128d8a5ed9cdd666dbfa4e17fd39afc4.png" alt="">
然后我们打开Chrome在地址栏输入“`http://localhost`访问刚才启动的OpenResty服务器就会看到一个简单的欢迎界面如下图所示。
<img src="https://static001.geekbang.org/resource/image/d7/88/d7f12d4d480d7100cd9804d2b16b8a88.png" alt="">
这时再回头去看Wireshark应该会显示已经抓到了一些数据就可以用鼠标点击工具栏里的“停止捕获”按钮告诉Wireshark“到此为止”不再继续抓包。
<img src="https://static001.geekbang.org/resource/image/f7/79/f7d05a3939d81742f18d2da7a1883179.png" alt="">
至于这些数据是什么,表示什么含义,我会在下一讲再详细介绍。
如果你能够在自己的电脑上走到这一步就说明“最小化”的实验环境已经搭建成功了不要忘了实验结束后运行批处理“stop”停止OpenResty服务器。
## 小结
这次我们学习了如何在自己的电脑上搭建HTTP实验环境在这里简单小结一下今天的内容。
1. 现实的网络环境太复杂有很多干扰因素搭建“最小化”的环境可以快速抓住重点掌握HTTP的本质
1. 我们选择Wireshark作为抓包工具捕获在TCP/IP协议栈中传输的所有流量
1. 我们选择Chrome或Firefox浏览器作为HTTP协议中的user agent
1. 我们选择OpenResty作为Web服务器它是一个Nginx的“强化包”功能非常丰富
1. Telnet是一个命令行工具可用来登录主机模拟浏览器操作
1. 在GitHub上可以下载到本专栏的专用项目源码只要把OpenResty解压到里面即可完成实验环境的搭建。
## 课下作业
1.按照今天所学的在你自己的电脑上搭建出这个HTTP实验环境并测试验证。
2.由于篇幅所限我无法详细介绍Wireshark你有时间可以再上网搜索Wireshark相关的资料了解更多的用法。
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/03/dd/03727c2a64cbc628ec18cf39a6a526dd.png" alt="unpreview">
<img src="https://static001.geekbang.org/resource/image/56/63/56d766fc04654a31536f554b8bde7b63.jpg" alt="unpreview">

View File

@@ -0,0 +1,183 @@
<audio id="audio" title="41 | Linux/Mac实验环境搭建与URI查询参数" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/53/09/53387d0bb500b74eea2e2b8ca622d009.mp3"></audio>
你好我是Chrono。
先要说一声“抱歉”。由于工作比较紧张、项目实施频繁出差导致原本预定的“答疑篇”迟迟没有进展这次趁着“十一”长假总算赶出了两期集中回答几个同学们问得比较多的问题Linux/Mac实验环境搭建[第7讲](https://time.geekbang.org/column/article/100124)URI查询参数[第11讲](https://time.geekbang.org/column/article/102008)还有DHE/ECDHE算法的原理[第26讲](https://time.geekbang.org/column/article/110354)),后续有时间可能还会再陆续补充完善。
很高兴在时隔一个多月后与你再次见面,废话不多说了,让我们开始吧。
## Linux上搭建实验环境
我们先来看一下如何在Linux上搭建课程的实验环境。
首先需要安装OpenResty但它在Linux上提供的不是zip压缩包而是各种Linux发行版的预编译包支持常见的Ubuntu、Debian、CentOS等等而且[官网](http://openresty.org/cn/linux-packages.html)上有非常详细安装步骤。
以Ubuntu为例只要“按部就班”地执行下面的几条命令就可以了非常轻松
```
# 安装导入GPG公钥所需的依赖包
sudo apt-get -y install --no-install-recommends wget gnupg ca-certificates
# 导入GPG密钥
wget -O - https://openresty.org/package/pubkey.gpg | sudo apt-key add -
# 安装add-apt-repository命令
sudo apt-get -y install --no-install-recommends software-properties-common
# 添加官方仓库:
sudo add-apt-repository -y &quot;deb http://openresty.org/package/ubuntu $(lsb_release -sc) main&quot;
# 更新APT索引
sudo apt-get update
# 安装 OpenResty
sudo apt-get -y install openresty
```
全部完成后OpenResty会安装到“/usr/local/openresty”目录里可以用它自带的命令行工具“resty”来验证是否安装成功
```
$resty -v
resty 0.23
nginx version: openresty/1.15.8.2
built with OpenSSL 1.1.0k 28 May 2019
```
有了OpenResty就可以从GitHub上获取http_study项目的源码了用“git clone”是最简单快捷的方法
```
git clone https://github.com/chronolaw/http_study
```
在Git仓库的“www”目录我为Linux环境补充了一个Shell脚本“run.sh”作用和Windows下的start.bat、stop.bat差不多可以简单地启停实验环境后面可以接命令行参数start/stop/reload/list
```
cd http_study/www/ #脚本必须在www目录下运行才能找到nginx.conf
./run.sh start #启动实验环境
./run.sh list #列出实验环境的Nginx进程
./run.sh reload #重启实验环境
./run.sh stop #停止实验环境
```
启动OpenResty之后就可以用浏览器或者curl来验证课程里的各个测试URI但之前不要忘记修改“/etc/hosts”添加域名解析例如
```
curl -v &quot;http://127.0.0.1/&quot;
curl -v &quot;http://www.chrono.com/09-1&quot;
curl -k &quot;https://www.chrono.com/24-1?key=1234&quot;
curl -v &quot;http://www.chrono.com/41-1&quot;
```
## Mac上搭建实验环境
看完了Linux我们再来看一下Mac。
这里我用的是两个环境Mac mini 和 MacBook Air不过都是好几年前的“老古董”了系统是10.13 High Sierra和10.14 Mojave更早的版本没有测试但应该也都可以
首先要保证Mac里有第三方包管理工具homebrew可以用下面的命令安装
```
#先安装Mac的homebrew
/usr/bin/ruby -e &quot;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)&quot;
```
然后要用homebrew安装OpenResty但它在Mac上的安装过程和Linux不同不是预编译包而是要下载许多相关的源码如OpenSSL然后再用clang本地编译大概要花上五六分钟的时间整体上比较慢要有点耐心。
```
#使用homebrew安装OpenResty
brew install openresty/brew/openresty
```
安装完OpenResty后续的操作就和Linux一样了“git clone”项目源码
```
git clone https://github.com/chronolaw/http_study
```
然后进“http_study/www”目录用脚本“run.sh”启停实验环境用Safari或者curl测试。
## Linux/Mac下的抓包
Linux和Mac里都有图形界面版本的Wireshark抓包的用法与Windows完全一样简单易用。
所以,今天我主要介绍命令行形式的抓包。
命令行抓包最基本的方式就是著名的tcpdump不过我用得不是很多所以就尽可能地“藏拙”了。
简单的抓包使用“-i lo”指定抓取本地环回地址“port”指定端口号“-w”指定抓包的存放位置抓包结束时用“Ctrl+C”中断
```
sudo tcpdump -i lo -w a.pcap
sudo tcpdump -i lo port 443 -w a.pcap
```
抓出的包也可以用tcpdump直接查看用“-r”指定包的名字
```
tcpdump -r a.pcap
tcpdump -r 08-1.pcapng -A
```
不过在命令行界面下可以用一个更好的工具——tshark它是Wireshark的命令行版本用法和tcpdump差不多但更易读功能也更丰富一些。
```
tshark -r 08-1.pcapng
tshark -r 08-1.pcapng -V
tshark -r 08-1.pcapng -O tcp|less
tshark -r 08-1.pcapng -O http|less
```
tshark也支持使用keylogfile解密查看HTTPS的抓包需要用“-o”参数指定log文件例如
```
tshark -r 26-1.pcapng -O http -o ssl.keylog_file:26-1.log|less
```
tcpdump、tshark和Linux里的许多工具一样参数繁多、功能强大你可以课后再找些资料仔细研究这里就不做过多地介绍了。
## URI的查询参数和头字段
在[第11讲](https://time.geekbang.org/column/article/102008)里我留了一个课下作业:
“URI的查询参数和头字段很相似都是key-value形式都可以任意自定义那么它们在使用时该如何区别呢
从课程后的留言反馈来看有的同学没理解这个问题的本意误以为问题问的是这两者在表现上应该如何区分比如查询参数是跟在“”后面头字段是请求头里的KV对。
这主要是怪我没有说清楚。这个问题实际上想问的是查询参数和头字段两者的形式很相近query是key-value头字段也是key-value它们有什么区别在发送请求时应该如何正确地使用它们。
换个说法就是:应该在什么场景下恰当地自定义查询参数或者头字段来附加额外信息。
当然了因为HTTP协议非常灵活这个问题也不会有唯一的、标准的答案我只能说说我自己的理解。
因为查询参数是与URI关联在一起的所以它针对的就是资源URI是长期、稳定的。而头字段是与一次HTTP请求关联的针对的是本次请求报文所以是短期、临时的。简单来说就是两者的作用域和时效性是不同的。
从这一点出发,我们就可以知道在哪些场合下使用查询参数和头字段更加合适。
比如要获取一个JS文件而它会有多个版本这个“版本”就是资源的一种属性应该用查询参数来描述。而如果要压缩传输、或者控制缓存的时间这些操作并不是资源本身固有的特性所以用头字段来描述更好。
除了查询参数和头字段还可以用其他的方式来向URI发送附加信息最常用的一种方式就是POST一个JSON结构里面能够存放比key-value复杂得多的数据也许你早就在实际工作中这么做了。
在这种情况下就可以完全不使用查询参数和头字段服务器从JSON里获取所有必需的数据让URI和请求头保持干净、整洁^_^)。
今天的答疑就先到这里我们下期再见到时候再讲ECDHE算法。
<img src="https://static001.geekbang.org/resource/image/c1/f9/c17f3027ba3cfb45e391107a8cf04cf9.png" alt="unpreview">

View File

@@ -0,0 +1,93 @@
<audio id="audio" title="42 | DHE/ECDHE算法的原理" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/66/7b/669bffe5b009bca02f827d434fec157b.mp3"></audio>
你好我是Chrono。
在[第26讲](https://time.geekbang.org/column/article/110354)里我介绍了TLS 1.2的握手过程在Client Hello和Server Hello里用到了ECDHE算法做密钥交换参数完全公开但却能够防止黑客攻击算出只有通信双方才能知道的秘密Pre-Master。
这是TLS握手的关键步骤也让很多同学不太理解“为什么数据都是不保密的但中间人却无法破解呢
解答这个问题必须要涉及密码学,我原本觉得有点太深了,不想展开细讲,但后来发现大家都对这个很关心,有点“打破砂锅问到底”的精神。所以,这次我就试着从底层来解释一下。不过你要有点心理准备,这不是那么好懂的。
先从ECDHE算法的名字说起。ECDHE就是“短暂-椭圆曲线-迪菲-赫尔曼”算法ephemeral Elliptic Curve DiffieHellman里面的关键字是“短暂”“椭圆曲线”和“迪菲-赫尔曼”,我先来讲“迪菲-赫尔曼”也就是DH算法。
## 离散对数
DH算法是一种非对称加密算法只能用于密钥交换它的数学基础是“**离散对数**”Discrete logarithm
那么,什么是离散对数呢?
上中学的时候我们都学过初等代数,知道指数和对数,指数就是幂运算,对数是指数的逆运算,是已知底数和真数(幂结果),反推出指数。
例如如果以10作为底数那么指数运算是y=10^x对数运算是y=logx100的对数是210^2=100log100=22的对数是0.301log2≈0.301)。
对数运算的域是实数,取值是连续的,而“离散对数”顾名思义,取值是不连续的,数值都是整数,但运算具有与实数对数相似的性质。
离散对数里的一个核心操作是模运算也就是取余数mod在C、Java、Lua等语言里的操作符是“%”)。
假设有模数17底数5那么“5的3次方再对17取余数得6”5 ^ 3 % 17 = 6就是在离散整数域上的一次指数运算5 ^ 3 (mod 17) = 6。反过来以5为底17为模数6的离散对数就是3Ind(5, 6) = 3 ( mod 17))。
这里的175是离散对数的公共参数6是真数3是对数。知道了对数就可以用幂运算很容易地得到真数但反过来知道真数却很难推断出对数于是就形成了一个“**单向函数**”。
在这个例子里选择的模数17很小使用穷举法从1到17暴力破解也能够计算得到6的离散对数是3。
但如果我们选择的是一个非常非常大的数比如说是有1024位的超大素数那么暴力破解的成本就非常高了几乎没有什么有效的方法能够快速计算出离散对数这就是DH算法的数学基础。
## DH算法
知道了离散对数我们来看DH算法假设Alice和Bob约定使用DH算法来交换密钥。
基于离散对数Alice和Bob需要首先确定模数和底数作为算法的参数这两个参数是公开的用P和G来代称简单起见我们还是用17和5P=17G=5
然后Alice和Bob各自选择一个随机整数作为**私钥**必须在1和P-2之间严格保密。比如Alice选择a=10Bob选择b=5。
有了DH的私钥Alice和Bob再计算幂作为**公钥**也就是A = (G ^ a % P) = 9B = (G ^ b % P) = 14这里的A和B完全可以公开因为根据离散对数的原理从真数反向计算对数a和b是非常困难的。
交换DH公钥之后Alice手里有五个数P=17G=5a=10A=9B=14然后执行一个运算(B ^ a % P)= 8。
因为离散对数的幂运算有交换律B ^ a = (G ^ b ) ^ a = (G ^ a) ^ b = A ^ b所以Bob计算A ^ b % P也会得到同样的结果8这个就是Alice和Bob之间的共享秘密可以作为会话密钥使用也就是TLS里的Pre-Master。
<img src="https://static001.geekbang.org/resource/image/4f/ef/4fd1b613d46334827b53a1f31fa4b3ef.png" alt="">
那么黑客在这个密钥交换的通信过程中能否实现攻击呢?
整个通信过程中Alice和Bob公开了4个信息P、G、A、B其中P、G是算法的参数A和B是公钥而a、b是各自秘密保管的私钥无法获取所以黑客只能从已知的P、G、A、B下手计算9或14的离散对数。
由离散对数的性质就可以知道如果P非常大那么他很难在短时间里破解出私钥a、b所以Alice和Bob的通信是安全的但在本例中数字小计算难度很低
实验环境的URI“/42-1”演示了这个简单DH密钥交换过程可以用浏览器直接访问命令行下也可以用“resty www/lua/42-1.lua”直接运行。
## DHE算法
DH算法有两种实现形式一种是已经被废弃的DH算法也叫static DH算法另一种是现在常用的DHE算法有时候也叫EDH
static DH算法里有一方的私钥是静态的通常是服务器方固定即a不变。而另一方也就是客户端随机选择私钥即b采用随机数。
于是DH交换密钥时就只有客户端的公钥会变而服务器公钥不变在长期通信时就增加了被破解的风险使得拥有海量计算资源的攻击者获得了足够的时间最终能够暴力破解出服务器私钥然后计算得到所有的共享秘密Pre-Master不具有“前向安全”。
而DHE算法的关键在于“E”表示的临时性上ephemeral每次交换密钥时双方的私钥都是随机选择、临时生成的用完就扔掉下次通信不会再使用相当于“一次一密”。
所以,即使攻击者破解了某一次的私钥,其他通信过程的私钥仍然是安全的,不会被解密,实现了“前向安全”。
## ECDHE算法
现在如果你理解了DHE那么理解ECDHE也就不那么困难了。
ECDHE算法就是把DHE算法里整数域的离散对数替换成了椭圆曲线上的离散对数。
<img src="https://static001.geekbang.org/resource/image/b4/ba/b452ceb3cbfc5c644a3053f2054b1aba.jpg" alt="">
原来DHE算法里的是任意整数而ECDHE则是把连续的椭圆曲线给“离散化”成整数用椭圆曲线上的“**倍运算**”替换了DHE里的幂运算。
在ECDHE里算法的公开参数是椭圆曲线C、基点G和模数P私钥是倍数x公钥是倍点xG已知倍点xG要想计算出离散对数x是非常困难的。
在通信时Alice和Bob各自随机选择两个数字a和b作为私钥计算A=aG、B=bG作为公钥然后互相交换用与DHE相同的算法计算得到aB=abG=Ab就是共享秘密Pre-Master。
因为椭圆曲线离散对数的计算难度比普通的离散对数更大所以ECDHE的安全性比DHE还要高更能够抵御黑客的攻击。
## 思考题
最后留一个思考题吧为什么DH算法只能用于密钥交换不能用于数字签名如果你理解了DH算法的原理应该不难回答出来。
<img src="https://static001.geekbang.org/resource/image/07/af/0773f7b9a098a64cdbe1bf2a666f87af.png" alt="">

View File

@@ -0,0 +1,227 @@
<audio id="audio" title="43 | 如何进行Docker实验环境搭建" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/38/c7/3855a80ef278a46a10d97d775e2e18c7.mp3"></audio>
你好我是Chrono。
《透视HTTP协议》这个专栏正式完结已经一年多了感谢你的支持与鼓励。
这一年的时间下来我发现专栏“实验环境的搭建”确实是一个比较严重的问题虽然我已经尽量把Windows、macOS、Linux里的搭建步骤写清楚了但因为具体的系统环境千差万别总会有各式各样奇怪的问题出现比如端口冲突、目录权限等等。
所以为了彻底解决这个麻烦我特意制作了一个Docker镜像里面是完整可用的HTTP实验环境下面我就来详细说一下该怎么用。
## 安装Docker环境
因为我不知道你对Docker是否了解所以第一步我还是先来简单介绍一下它。
Docker是一种虚拟化技术基于Linux的容器机制Linux Containers简称LXC你可以把它近似地理解成是一个“轻量级的虚拟机”只消耗较少的资源就能实现对进程的隔离保护。
使用Docker可以把应用程序和它相关的各种依赖如底层库、组件等“打包”在一起这就是Docker镜像Docker image。Docker镜像可以让应用程序不再顾虑环境的差异在任意的系统中以容器的形式运行当然必须要基于Docker环境极大地增强了应用部署的灵活性和适应性。
Docker是跨平台的支持Windows、macOS、Linux等操作系统在Windows、macOS上只要下载一个安装包然后简单点几下鼠标就可以完成安装。
<img src="https://static001.geekbang.org/resource/image/24/b2/2490f6a2a514710e0b683d6cdb4614b2.png" alt="">
下面我以Ubuntu为例说一下在Linux上的安装方法。
你可以在Linux上用apt-get或者yum安装Docker不过更好的方式是使用Docker官方提供的脚本自动完成全套的安装步骤。
```
curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
```
因为Docker是国外网站直接从官网安装速度可能比较慢。所以你还可以选择国内的镜像网站来加快速度像这里我就使用“mirror”选项指定了“某某云”。
Docker是C/S架构安装之后还需要再执行一条命令启动它的服务。
```
sudo service docker start #Ubuntu启动docker服务
```
此外操作Docker必须要有sudo权限你可以用“usermod”命令把当前的用户加入“Docker”组里。如果图省事也可以用sudo命令直接切换成root用户来操作。
```
sudo usermod -aG docker ${USER} #当前用户加入Docker组
sudo su - #或者直接用root用户
```
这些做完后,你需要执行命令“**docker version**”“**docker info**”来检查是否安装成功。比如下面这张图显示的就是我使用的Docker环境版本是“18.06.3-ce”。
<img src="https://static001.geekbang.org/resource/image/d3/91/d3ba248bf564yy289a197dab8635c991.png" alt="">
## 获取Docker镜像
如果你已经安装好了Docker运行环境现在就可以从Docker Hub上获取课程相应的Docker镜像文件了用的是“**docker pull**”命令。
```
docker pull chronolaw/http_study
```
这个镜像里打包了操作系统Ubuntu 18.04和最新的Openresty 1.17.8.2还有项目的全部示例代码。为了方便你学习我还在里面加入了Vim、Git、Telnet、Curl、Tcpdump等实用工具。
由于镜像里的东西多所以体积比较大下载需要一些时间你要有点耐心。镜像下载完成之后你可以用“Docker images”来查看结果列出目前本地的所有镜像文件。
<img src="https://static001.geekbang.org/resource/image/07/22/0779b1016002f4bdafc6a785c991ba22.png" alt="">
从图中你可以看到这个镜像的名字是“chronolaw/http_study”大小是645MB。
## 启动Docker容器
有了镜像文件,你就可以用“**docker run**”命令,从镜像启动一个容器了。
这里面就是我们完整的HTTP实验环境不需要再操心这样、那样的问题了做到了真正的“开箱即用”。
```
docker run -it --rm chronolaw/http_study
```
对于上面这条命令,我还要稍微解释一下:“-it”参数表示开启一个交互式的Shell默认使用的是bashrm”参数表示容器是“用完即扔”不保存容器实例一旦退出Shell就会自动删除容器但不会删除镜像免去了管理容器的麻烦。
“docker run”之后你就会像虚拟机一样进入容器的运行环境这里就是Ubuntu 18.04身份也自动变成了root用户大概是下面这样的。
```
docker run -it --rm chronolaw/http_study
root@8932f62c972:/#
```
项目的源码我放在了root用户目录下你可以直接进入“**http_study/www**”目录,然后执行“**run.sh**”启动OpenResty服务可参考[第41讲](https://time.geekbang.org/column/article/146833))。
```
cd ~/http_study/www
./run.sh start
```
不过因为Docker自身的限制镜像里的hosts文件不能直接添加“[www.chrono.com](http://www.chrono.com)”等实验域名的解析。如果你想要在URI里使用域名就必须在容器启动后手动修改hosts文件用Vim或者cat都可以。
```
vim /etc/hosts #手动编辑hosts文件
cat ~/http_study/hosts &gt;&gt; /etc/hosts #cat追加到hosts末尾
```
另一种方式是在“docker run”的时候用“**add-host**”参数,手动指定域名/IP的映射关系。
```
docker run -it --rm --add-host=www.chrono.com:127.0.0.1 chronolaw/http_study
```
保险起见我建议你还是用第一种方式比较好。也就是启动容器后用“cat”命令把实验域名的解析追加到hosts文件里然后再启动OpenResty服务。
```
docker run -it --rm chronolaw/http_study
cat ~/http_study/hosts &gt;&gt; /etc/hosts
cd ~/http_study/www
./run.sh start
```
## 在Docker容器里做实验
把上面的工作都做完之后,我们的实验环境就算是完美地运行起来了,现在你就可以在里面任意验证各节课里的示例了,我来举几个例子。
不过在开始之前我要提醒你一点因为这个Docker镜像是基于Linux的没有图形界面所以只能用命令行比如telnet、curl来访问HTTP服务。当然你也可以查一下资料让容器对外暴露80等端口比如使用参数“net=host”在外部用浏览器来访问这里我就不细说了。
先来看最简单的,[第7讲](https://time.geekbang.org/column/article/100124)里的测试实验环境用curl来访问localhost会输出一个文本形式的HTML文件内容。
```
curl http://localhost #访问本机的HTTP服务
```
<img src="https://static001.geekbang.org/resource/image/30/1a/30671d607d6cb74076c467bab1b95b1a.png" alt="">
然后我们来看[第9讲](https://time.geekbang.org/column/article/100513)用telnet来访问HTTP服务输入“**telnet 127.0.0.1 80**”回车就进入了telnet界面。
Linux下的telnet操作要比Windows的容易一些你可以直接把HTTP请求报文复制粘贴进去再按两下回车就行了结束telnet可以用“Ctrl+C”。
```
GET /09-1 HTTP/1.1
Host: www.chrono.com
```
<img src="https://static001.geekbang.org/resource/image/8d/fa/8d40c02c57b13835c8dd92bda97fa3fa.png" alt="">
实验环境里测试HTTPS和HTTP/2也是毫无问题的只要你按之前说的正确修改了hosts域名解析就可以用curl来访问但要加上“**-k**”参数来忽略证书验证。
```
curl https://www.chrono.com/23-1 -vk
curl https://www.metroid.net:8443/30-1 -vk
```
<img src="https://static001.geekbang.org/resource/image/yy/94/yyff754d5fbe34cd2dfdb002beb00094.png" alt="">
<img src="https://static001.geekbang.org/resource/image/69/51/69163cc988f8b95cf906cd4dbcyy3151.png" alt="">
这里要注意一点因为Docker镜像里的Openresty 1.17.8.2内置了OpenSSL1.1.1g默认使用的是TLS1.3所以如果你想要测试TLS1.2的话,需要使用参数“**tlsv1.2**”。
```
curl https://www.chrono.com/30-1 -k --tlsv1.2
```
<img src="https://static001.geekbang.org/resource/image/b6/aa/b62a2278b73a4f264a14a04f0058cbaa.png" alt="">
## 在Docker容器里抓包
到这里课程中的大部分示例都可以运行了。最后我再示范一下在Docker容器里tcpdump抓包的用法。
首先,你要指定抓取的协议、地址和端口号,再用“-w”指定存储位置启动tcpdump后按“Ctrl+Z”让它在后台运行。比如为了测试TLS1.3就可以用下面的命令行抓取HTTPS的443端口存放到“/tmp”目录。
```
tcpdump tcp port 443 -i lo -w /tmp/a.pcap
```
然后我们执行任意的telnet或者curl命令完成HTTP请求之后输入“fg”恢复tcpdump再按“Ctrl+C”这样抓包就结束了。
对于HTTPS需要导出密钥的情形你必须在curl请求的同时指定环境变量“SSLKEYLOGFILE”不然抓包获取的数据无法解密你就只能看到乱码了。
```
SSLKEYLOGFILE=/tmp/a.log curl https://www.chrono.com/11-1 -k
```
我把完整的抓包过程截了个图,你可以参考一下。
<img src="https://static001.geekbang.org/resource/image/40/40/40f3c47814a174a4d135316b7cfdcf40.png" alt="">
抓包生成的文件在容器关闭后就会消失,所以还要用“**docker cp**”命令及时从容器里拷出来指定容器的ID看提示符或者用“docker ps -a”查看也可以从GitHub仓库里获取43-1.pcap/43-1.log
```
docker cp xxx:/tmp/a.pcap . #需要指定容器的ID
docker cp xxx:/tmp/a.log . #需要指定容器的ID
```
现在有了pcap文件和log文件我们就可以用Wireshark来看网络数据细致地分析HTTP/HTTPS通信过程了HTTPS还需要设置一下Wireshark见[第26讲](https://time.geekbang.org/column/article/110354))。
<img src="https://static001.geekbang.org/resource/image/1a/bf/1ab18685ca765e8050b58ee76abd3cbf.png" alt="">
在这个包里你可以清楚地看到通信时使用的是TLS1.3协议服务器选择的密码套件是TLS_AES_256_GCM_SHA384。
掌握了tcpdump的用法之后你也可以再参考[第27讲](https://time.geekbang.org/column/article/110718)改改Nginx配置文件自己抓包仔细研究TLS1.3协议的“supported_versions”“key_share”“server_name”等各个扩展协议。
## 小结
今天讲了Docker实验环境的搭建我再小结一下要点。
1. Docker是一种非常流行的虚拟化技术可以近似地把它理解成是一个“轻量级的虚拟机”
1. 可以用“docker pull”命令从Docker Hub上获取课程相应的Docker镜像文件
1. 可以用“docker run”命令从镜像启动一个容器里面是完整的HTTP实验环境支持TLS1.3
1. 可以在Docker容器里任意验证各节课里的示例但需要使用命令行形式的telnet或者curl
1. 抓包需要使用tcpdump指定抓取的协议、地址和端口号
1. 对于HTTPS需要指定环境变量“SSLKEYLOGFILE”导出密钥再发送curl请求。
很高兴时隔一年后再次与你见面今天就到这里吧期待下次HTTP/3发布时的相会。
<img src="https://static001.geekbang.org/resource/image/26/e3/26bbe56074b40fd5c259f396ddcfd6e3.png" alt="">

View File

@@ -0,0 +1,71 @@
<audio id="audio" title="结束语 | 做兴趣使然的Hero" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0f/d3/0f2ae4c72a93ab9c85abc5ce032128d3.mp3"></audio>
从今年年初与极客时间编辑的初次接触开始到这个月底专栏的正式结束经过了差不多7个月的时间。这段历程有痛苦也有欢乐有迷惘也有清朗有困惑也有顿悟有挫折也有奋进各种感受五味杂陈一言难尽。
无论如何首先要感谢的就是你——感谢你对我和这个专栏的支持Many thanks to you。
写文章都讲究“首尾呼应”,所以在这篇“结束语”里,我就顺着“开篇词”,和你聊些轻松的话题,唠唠家常、说说心里话。
## 我是怎么写专栏的
咱们都是吃计算机这碗饭的应该知道通信协议这个东西很不好学更是很难讲因为它真的是太“虚”了。不像编程语言、算法、数据结构、开发框架、操作系统那样有实实在在的代码协议只是一个文本规范是一个动态的过程而不是在计算机系统里真实存在的东西你不能用GDB直接去调试内存里也看不见。
所以,讲协议的书本、课程实在是少之又少。
落到HTTP协议就如同我在专栏一开始时所说的它“既简单又不简单”而且历史悠久涉及的范围很广关联的技能点很多、很杂。当我接下写专栏的任务时甚至有点“懵”的感觉千头万绪不知从何谈起。
好在我一直有写学习笔记的习惯最早是用“原始”的word文档近几年改用云端笔记工具随时记录、整理散乱的知识碎片。
既然暂时“无从下手”,那就先读文章、记笔记好了。
于是,我开始大量地粗读、精读现有资料,在阅读的过程中慢慢搜集思维中闪现的“火花”,即使是一两个零星的词汇也不放过。一个多月的辛苦整理过后,这才逐渐理清了脉络,有了模糊的写作思路,全程有点“垃圾堆里筛金子”的感觉。
虽然我有写书的经验,但写专栏则是完全不同的体验,在正式动笔写作的时候(严格来说应该是“敲键盘码字”),我才意识到,它与传统的技术类书籍有很大的不同。
书籍的阅读场景通常会比较安静、放松,读者会有比较长的思考时间,可以翻来覆去地看,再时不时拿起铅笔画个重点、做个记号,一段时间的阅读下来可以关注很多的知识点,然后再慢慢思索,总结串联。
而专栏的阅读场景则更可能是在地铁、公交车里,周围人挤人、人挨人,拿着手机,或看或听,还要时刻当心别坐过站。在这种情况下,读者很难有足够的思考时间和精力,更希望能够高效率、便捷地在短短几分钟的碎片时间里吸取知识,如果有太多的知识点就难以接受,一两个略有深度的点会更好。
所以这次的专栏写作我就改换了风格开始“口语化写作”不再像写书那样斟词酌句为一两句话的用词反复思量、咬文嚼字而是完全“放飞自我”定下每篇文章的主旨、要点后就笔随心动把头脑里的思绪完全“dump”出来。
这样虽然在一定程度上降低了文字的信息密度,却会让文章形式更流畅、更易理解,做为补充,文章的末尾我再用小结的形式集中强化一下要点,实现了“浅入深出”。
现在看来效果似乎还算不错,不知道你以为如何呢?
## 兴趣使然的HERO
虽然风格定了,但专栏写作过程中的困难程度还是我当初没有预计到的,写书是一个“慢功夫”,可以慢慢思考,有想法了就写一点,没有灵感可能十天半个月都动不了笔。
而写专栏却有“硬性”的时间限制和编辑确定了写作大纲后就开始了“奴隶”一样的日子每周固定要交两、三篇每篇三四千字相当于毎天要产出至少一千的有效文字这简直成了“夺命连环call”同时还有构思、画图、编码、试验、核查等其他工作压力非常大真是一次“触及灵魂”之旅。
记得有一句名言“兴趣是最好的老师”支撑着我把这个专栏按时交付下去的最大动力可能就是对学习计算机知识的兴趣和探索欲了。每当get到一个以前没有注意的知识点每当成功领会了协议背后的设计意图我的心底都会产生由衷的喜悦前面钻研过程中的苦恼和烦躁也就瞬间“烟消云散”了。
所以,只要发自内心地对一件事情产生兴趣和喜爱,那么即使有再多的困难,也会想办法去克服、去解决。
说到这里我联想到了《一拳超人》里的主角埼玉他可以算得上是典型的“兴趣使然的Hero”纯粹是因为自己的“兴趣”而走上了“打怪升级”的道路不图名不图利不在意排名也不在意奖励。单纯而快乐的生活也许正是我们很多人想要追求的目标。
当然除了兴趣更重要的是恒心、毅力和坚持。埼玉之所以成为“无敌的存在”就是因为他每天坚持做100个俯卧撑、100个仰卧起坐、100个下蹲天天如此从不间断。
我在这几个月的专栏写作过程中,遇到的困难和烦恼是以前写书的好几倍,经常是坐在电脑前,脑子里有很多乱麻一样想法,却无法“落地”转化成合适的词语,有时候会就这么干坐上一两个小时,焦灼的心情可想而知。
幸运的是最终我在“兴趣”这个原动力的支撑下坚持到了最后另外还有了一个意外的收获。每天夜里码字没有思路的时候我会走出家门在小区里慢跑两三圈呼吸新鲜空气顺便“放空”大脑。到专栏结束的这个时间点居然减掉了差不多8斤的体重。
所以你看,“兴趣”给我带来的好处还真是不少呢。
## 相濡以沫,不如相忘于江湖
“透视HTTP”这个专栏马上就要结束了但HTTP协议的学习还远没有结束。
这有点像是调查兵团历经磨难和牺牲,终于看到了大海,但在海的另一头,还会更多更大的挑战等待着他们(看过《进击的巨人》的朋友一定能领会这种情景吧)。
在这篇“结束语”的留言区里,希望大家都能“冒个泡”,看看当时定下的“小目标”有没有达成,一起分享一下在这个专栏中的收获和心路历程,还有将来的打算。也欢迎你访问[专栏的GitHub主页](https://github.com/chronolaw/http_study)提issue和PR把HTTP的学习、实践继续下去。
我还为你准备了一份[结课问卷](https://jinshuju.net/f/tRGu0l),你花两三分钟就可以填完。希望你能在问卷里说出你的学习经历、感受和意见,毕竟专栏结课后的优化离不开你的反馈。
最后的最后,我要说的是:
感谢陪伴,一路有你,祝愿我们都能够保持初心,做兴趣使然的英雄。
期待HTTP/3发布之时的再会 See you next mission
<img src="https://static001.geekbang.org/resource/image/65/53/65575a4f4fd668fb0cda13dd7b4b8053.png" alt="unpreview">

View File

@@ -0,0 +1,10 @@
你好我是Chrono。
《透视HTTP协议》已经完结一段时间了。在这段时间里我依然收到了很多用户的留言很感谢你一直以来的认真学习和支持
为了帮助你检验自己的学习效果,我特别给你准备了一套结课测试题。这套测试题共有 20 道题目包括4道单选题和16道多选题满分 100 分,系统自动评分。
点击下面按钮,马上开始测试吧!
[<img src="https://static001.geekbang.org/resource/image/28/a4/28d1be62669b4f3cc01c36466bf811a4.png" alt="">](http://time.geekbang.org/quiz/intro?act_id=118&amp;exam_id=254)

View File

@@ -0,0 +1,198 @@
<audio id="audio" title="15 | 海纳百川HTTP的实体数据" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f4/8d/f48b1f2b88e4de06c4551576992bf78d.mp3"></audio>
你好我是Chrono。
今天我要与你分享的话题是“海纳百川HTTP的实体数据”。
这一讲是“进阶篇”的第一讲从今天开始我会用连续的8讲的篇幅来详细解析HTTP协议里的各种头字段包括定义、功能、使用方式、注意事项等等。学完了这些课程你就可以完全掌握HTTP协议。
在前面的“基础篇”里我们了解了HTTP报文的结构知道一个HTTP报文是由“header+body”组成的。但那时我们主要研究的是header没有涉及到body。所以“进阶篇”的第一讲就从HTTP的body谈起。
## 数据类型与编码
在TCP/IP协议栈里传输数据基本上都是“header+body”的格式。但TCP、UDP因为是传输层的协议它们不会关心body数据是什么只要把数据发送到对方就算是完成了任务。
而HTTP协议则不同它是应用层的协议数据到达之后工作只能说是完成了一半还必须要告诉上层应用这是什么数据才行否则上层应用就会“不知所措”。
你可以设想一下假如HTTP没有告知数据类型的功能服务器把“一大坨”数据发给了浏览器浏览器看到的是一个“黑盒子”这时候该怎么办呢
当然它可以“猜”。因为很多数据都是有固定格式的所以通过检查数据的前几个字节也许就能知道这是个GIF图片、或者是个MP3音乐文件但这种方式无疑十分低效而且有很大几率会检查不出来文件类型。
幸运的是早在HTTP协议诞生之前就已经有了针对这种问题的解决方案不过它是用在电子邮件系统里的让电子邮件可以发送ASCII码以外的任意数据方案的名字叫做“**多用途互联网邮件扩展**”Multipurpose Internet Mail Extensions简称为MIME。
MIME是一个很大的标准规范但HTTP只“顺手牵羊”取了其中的一部分用来标记body的数据类型这就是我们平常总能听到的“**MIME type**”。
MIME把数据分成了八大类每个大类下再细分出多个子类形式是“type/subtype”的字符串巧得很刚好也符合了HTTP明文的特点所以能够很容易地纳入HTTP头字段里。
这里简单列举一下在HTTP里经常遇到的几个类别
1. text即文本格式的可读数据我们最熟悉的应该就是text/html了表示超文本文档此外还有纯文本text/plain、样式表text/css等。
1. image即图像文件有image/gif、image/jpeg、image/png等。
1. audio/video音频和视频数据例如audio/mpeg、video/mp4等。
1. application数据格式不固定可能是文本也可能是二进制必须由上层应用程序来解释。常见的有application/jsonapplication/javascript、application/pdf等另外如果实在是不知道数据是什么类型像刚才说的“黑盒”就会是application/octet-stream即不透明的二进制数据。
但仅有MIME type还不够因为HTTP在传输时为了节约带宽有时候还会压缩数据为了不要让浏览器继续“猜”还需要有一个“Encoding type”告诉数据是用的什么编码格式这样对方才能正确解压缩还原出原始的数据。
比起MIME type来说Encoding type就少了很多常用的只有下面三种
1. gzipGNU zip压缩格式也是互联网上最流行的压缩格式
1. deflatezlibdeflate压缩格式流行程度仅次于gzip
1. br一种专门为HTTP优化的新压缩算法Brotli
## 数据类型使用的头字段
有了MIME type和Encoding type无论是浏览器还是服务器就都可以轻松识别出body的类型也就能够正确处理数据了。
HTTP协议为此定义了两个Accept请求头字段和两个Content实体头字段用于客户端和服务器进行“**内容协商**”。也就是说客户端用Accept头告诉服务器希望接收什么样的数据而服务器用Content头告诉客户端实际发送了什么样的数据。
<img src="https://static001.geekbang.org/resource/image/51/b9/5191bce1329efa157a6cc37ab9e789b9.png" alt="">
**Accept**字段标记的是客户端可理解的MIME type可以用“,”做分隔符列出多个类型,让服务器有更多的选择余地,例如下面的这个头:
```
Accept: text/html,application/xml,image/webp,image/png
```
这就是告诉服务器“我能够看懂HTML、XML的文本还有webp和png的图片请给我这四类格式的数据”。
相应的,服务器会在响应报文里用头字段**Content-Type**告诉实体数据的真实类型:
```
Content-Type: text/html
Content-Type: image/png
```
这样浏览器看到报文里的类型是“text/html”就知道是HTML文件会调用排版引擎渲染出页面看到“image/png”就知道是一个PNG文件就会在页面上显示出图像。
**Accept-Encoding**字段标记的是客户端支持的压缩格式例如上面说的gzip、deflate等同样也可以用“,”列出多个,服务器可以选择其中一种来压缩数据,实际使用的压缩格式放在响应头字段**Content-Encoding**里。
```
Accept-Encoding: gzip, deflate, br
Content-Encoding: gzip
```
不过这两个字段是可以省略的如果请求报文里没有Accept-Encoding字段就表示客户端不支持压缩数据如果响应报文里没有Content-Encoding字段就表示响应数据没有被压缩。
## 语言类型与编码
MIME type和Encoding type解决了计算机理解body数据的问题但互联网遍布全球不同国家不同地区的人使用了很多不同的语言虽然都是text/html但如何让浏览器显示出每个人都可理解可阅读的语言文字呢
这实际上就是“国际化”的问题。HTTP采用了与数据类型相似的解决方案又引入了两个概念语言类型与字符集。
所谓的“**语言类型**”就是人类使用的自然语言例如英语、汉语、日语等而这些自然语言可能还有下属的地区性方言所以在需要明确区分的时候也要使用“type-subtype”的形式不过这里的格式与数据类型不同**分隔符不是“/”,而是“-”**。
举几个例子en表示任意的英语en-US表示美式英语en-GB表示英式英语而zh-CN就表示我们最常使用的汉语。
关于自然语言的计算机处理还有一个更麻烦的东西叫做“字符集”。
在计算机发展的早期各个国家和地区的人们“各自为政”发明了许多字符编码方式来处理文字比如英语世界用的ASCII、汉语世界用的GBK、BIG5日语世界用的Shift_JIS等。同样的一段文字用一种编码显示正常换另一种编码后可能就会变得一团糟。
所以后来就出现了Unicode和UTF-8把世界上所有的语言都容纳在一种编码方案里遵循UTF-8字符编码方式的Unicode字符集也成为了互联网上的标准字符集。
## 语言类型使用的头字段
同样的HTTP协议也使用Accept请求头字段和Content实体头字段用于客户端和服务器就语言与编码进行“**内容协商**”。
**Accept-Language**字段标记了客户端可理解的自然语言,也允许用“,”做分隔符列出多个类型,例如:
```
Accept-Language: zh-CN, zh, en
```
这个请求头会告诉服务器“最好给我zh-CN的汉语文字如果没有就用其他的汉语方言如果还没有就给英文”。
相应的,服务器应该在响应报文里用头字段**Content-Language**告诉客户端实体数据使用的实际语言类型:
```
Content-Language: zh-CN
```
字符集在HTTP里使用的请求头字段是**Accept-Charset**但响应头里却没有对应的Content-Charset而是在**Content-Type**字段的数据类型后面用“charset=xxx”来表示这点需要特别注意。
例如浏览器请求GBK或UTF-8的字符集然后服务器返回的是UTF-8编码就是下面这样
```
Accept-Charset: gbk, utf-8
Content-Type: text/html; charset=utf-8
```
不过现在的浏览器都支持多种字符集通常不会发送Accept-Charset而服务器也不会发送Content-Language因为使用的语言完全可以由字符集推断出来所以在请求头里一般只会有Accept-Language字段响应头里只会有Content-Type字段。
<img src="https://static001.geekbang.org/resource/image/0e/10/0e9bcd6922fa8908bdba79d98ae5fa10.png" alt="">
## 内容协商的质量值
在HTTP协议里用Accept、Accept-Encoding、Accept-Language等请求头字段进行内容协商的时候还可以用一种特殊的“q”参数表示权重来设定优先级这里的“q”是“quality factor”的意思。
权重的最大值是1最小值是0.01默认值是1如果值是0就表示拒绝。具体的形式是在数据类型或语言代码后面加一个“;”然后是“q=value”。
这里要提醒的是“;”的用法,在大多数编程语言里“;”的断句语气要强于“,”而在HTTP的内容协商里却恰好反了过来“;”的意义是小于“,”的。
例如下面的Accept字段
```
Accept: text/html,application/xml;q=0.9,*/*;q=0.8
```
它表示浏览器最希望使用的是HTML文件权重是1其次是XML文件权重是0.9最后是任意数据类型权重是0.8。服务器收到请求头后就会计算权重再根据自己的实际情况优先输出HTML或者XML。
## 内容协商的结果
内容协商的过程是不透明的每个Web服务器使用的算法都不一样。但有的时候服务器会在响应头里多加一个**Vary**字段,记录服务器在内容协商时参考的请求头字段,给出一点信息,例如:
```
Vary: Accept-Encoding,User-Agent,Accept
```
这个Vary字段表示服务器依据了Accept-Encoding、User-Agent和Accept这三个头字段然后决定了发回的响应报文。
Vary字段可以认为是响应报文的一个特殊的“版本标记”。每当Accept等请求头变化时Vary也会随着响应报文一起变化。也就是说同一个URI可能会有多个不同的“版本”主要用在传输链路中间的代理服务器实现缓存服务这个之后讲“HTTP缓存”时还会再提到。
## 动手实验
上面讲完了理论部分接下来就是实际动手操作了。可以用我们的实验环境在www目录下有一个mime目录里面预先存放了几个文件可以用URI“/15-1?name=file”的形式访问例如
```
http://www.chrono.com/15-1?name=a.json
http://www.chrono.com/15-1?name=a.xml
```
在Chrome里打开开发者工具就能够看到Accept和Content头
<img src="https://static001.geekbang.org/resource/image/b4/9e/b4d1671872876a60ad717821f0fb819e.png" alt="">
你也可以把任意的文件拷贝到mime目录下比如压缩包、MP3、图片、视频等再用Chrome访问观察更多的MIME type。
有了这些经验后你还可以离开实验环境直接访问各大门户网站看看真实网络世界里的HTTP报文是什么样子的。
## 小结
今天我们学习了HTTP里的数据类型和语言类型在这里为今天的内容做个小结。
<img src="https://static001.geekbang.org/resource/image/b2/58/b2118315a977969ddfcc7ab9d26cb358.png" alt="">
1. 数据类型表示实体数据的内容是什么使用的是MIME type相关的头字段是Accept和Content-Type
1. 数据编码表示实体数据的压缩方式相关的头字段是Accept-Encoding和Content-Encoding
1. 语言类型表示实体数据的自然语言相关的头字段是Accept-Language和Content-Language
1. 字符集表示实体数据的编码方式相关的头字段是Accept-Charset和Content-Type
1. 客户端需要在请求头里使用Accept等头字段与服务器进行“内容协商”要求服务器返回最合适的数据
1. Accept等头字段可以用“,”顺序列出多个可能的选项,还可以用“;q=”参数来精确指定权重。
## 课下作业
1. 试着解释一下这个请求头“Accept-Encoding: gzip, deflate;q=1.0, *;q=0.5, br;q=0”再模拟一下服务器的响应头。
1. 假设你要使用POST方法向服务器提交一些JSON格式的数据里面包含有中文请求头应该是什么样子的呢
1. 试着用快递发货收货比喻一下MIME、Encoding等概念。
欢迎你把自己的答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,欢迎你把文章分享给你的朋友。

View File

@@ -0,0 +1,199 @@
<audio id="audio" title="16 | 把大象装进冰箱HTTP传输大文件的方法" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ba/6a/ba4551c47d8a72e53add314d06f3b46a.mp3"></audio>
上次我们谈到了HTTP报文里的body知道了HTTP可以传输很多种类的数据不仅是文本也能传输图片、音频和视频。
早期互联网上传输的基本上都是只有几K大小的文本和小图片现在的情况则大有不同。网页里包含的信息实在是太多了随随便便一个主页HTML就有可能上百K高质量的图片都以M论更不要说那些电影、电视剧了几G、几十G都有可能。
相比之下100M的光纤固网或者4G移动网络在这些大文件的压力下都变成了“小水管”无论是上传还是下载都会把网络传输链路挤的“满满当当”。
所以,如何在有限的带宽下高效快捷地传输这些大文件就成了一个重要的课题。这就好比是已经打开了冰箱门(建立连接),该怎么把大象(文件)塞进去再关上门(完成传输)呢?
今天我们就一起看看HTTP协议里有哪些手段能解决这个问题。
## 数据压缩
还记得上一讲中说到的“数据类型与编码”吗?如果你还有印象的话,肯定能够想到一个最基本的解决方案,那就是“**数据压缩**”,把大象变成小猪佩奇,再放进冰箱。
通常浏览器在发送请求时都会带着“**Accept-Encoding**”头字段里面是浏览器支持的压缩格式列表例如gzip、deflate、br等这样服务器就可以从中选择一种压缩算法放进“**Content-Encoding**”响应头里,再把原数据压缩后发给浏览器。
如果压缩率能有50%也就是说100K的数据能够压缩成50K的大小那么就相当于在带宽不变的情况下网速提升了一倍加速的效果是非常明显的。
不过这个解决方法也有个缺点gzip等压缩算法通常只对文本文件有较好的压缩率而图片、音频视频等多媒体数据本身就已经是高度压缩的再用gzip处理也不会变小甚至还有可能会增大一点所以它就失效了。
不过数据压缩在处理文本的时候效果还是很好的所以各大网站的服务器都会使用这个手段作为“保底”。例如在Nginx里就会使用“gzip on”指令启用对“text/html”的压缩。
## 分块传输
在数据压缩之外,还能有什么办法来解决大文件的问题呢?
压缩是把大文件整体变小,我们可以反过来思考,如果大文件整体不能变小,那就把它“拆开”,分解成多个小块,把这些小块分批发给浏览器,浏览器收到后再组装复原。
这样浏览器和服务器都不用在内存里保存文件的全部,每次只收发一小部分,网络也不会被大文件长时间占用,内存、带宽等资源也就节省下来了。
这种“**化整为零**”的思路在HTTP协议里就是“**chunked**”分块传输编码,在响应报文里用头字段“**Transfer-Encoding: chunked**”来表示意思是报文里的body部分不是一次性发过来的而是分成了许多的块chunk逐个发送。
这就好比是用魔法把大象变成“乐高积木”,拆散了逐个装进冰箱,到达目的地后再施法拼起来“满血复活”。
分块传输也可以用于“流式数据”例如由数据库动态生成的表单页面这种情况下body数据的长度是未知的无法在头字段“**Content-Length**”里给出确切的长度所以也只能用chunked方式分块发送。
“Transfer-Encoding: chunked”和“Content-Length”这两个字段是**互斥的**也就是说响应报文里这两个字段不能同时出现一个响应报文的传输要么是长度已知要么是长度未知chunked这一点你一定要记住。
下面我们来看一下分块传输的编码规则,其实也很简单,同样采用了明文的方式,很类似响应头。
1. 每个分块包含两个部分,长度头和数据块;
1. 长度头是以CRLF回车换行即\r\n结尾的一行明文用16进制数字表示长度
1. 数据块紧跟在长度头后最后也用CRLF结尾但数据不包含CRLF
1. 最后用一个长度为0的块表示结束即“0\r\n\r\n”。
听起来好像有点难懂,看一下图就好理解了:
<img src="https://static001.geekbang.org/resource/image/25/10/25e7b09cf8cb4eaebba42b4598192410.png" alt="">
实验环境里的URI“/16-1”简单地模拟了分块传输可以用Chrome访问这个地址看一下效果
<img src="https://static001.geekbang.org/resource/image/e1/db/e183bf93f4759b74c8ee974acbcaf9db.png" alt="">
不过浏览器在收到分块传输的数据后会自动按照规则去掉分块编码重新组装出内容所以想要看到服务器发出的原始报文形态就得用Telnet手工发送请求或者用Wireshark抓包
```
GET /16-1 HTTP/1.1
Host: www.chrono.com
```
因为Telnet只是收到响应报文就完事了不会解析分块数据所以可以很清楚地看到响应报文里的chunked数据格式先是一行16进制长度然后是数据然后再是16进制长度和数据如此重复最后是0长度分块结束。
<img src="https://static001.geekbang.org/resource/image/66/02/66a6d229058c7072ab5b28ef518da302.png" alt="">
## 范围请求
有了分块传输编码服务器就可以轻松地收发大文件了但对于上G的超大文件还有一些问题需要考虑。
比如,你在看当下正热播的某穿越剧,想跳过片头,直接看正片,或者有段剧情很无聊,想拖动进度条快进几分钟,这实际上是想获取一个大文件其中的片段数据,而分块传输并没有这个能力。
HTTP协议为了满足这样的需求提出了“**范围请求**”range requests的概念允许客户端在请求头里使用专用字段来表示只获取文件的一部分相当于是**客户端的“化整为零”**。
范围请求不是Web服务器必备的功能可以实现也可以不实现所以服务器必须在响应头里使用字段“**Accept-Ranges: bytes**”明确告知客户端:“我是支持范围请求的”。
如果不支持的话该怎么办呢服务器可以发送“Accept-Ranges: none”或者干脆不发送“Accept-Ranges”字段这样客户端就认为服务器没有实现范围请求功能只能老老实实地收发整块文件了。
请求头**Range**是HTTP范围请求的专用字段格式是“**bytes=x-y**”其中的x和y是以字节为单位的数据范围。
要注意x、y表示的是“偏移量”范围必须从0计数例如前10个字节表示为“0-9”第二个10字节表示为“10-19”而“0-10”实际上是前11个字节。
Range的格式也很灵活起点x和终点y可以省略能够很方便地表示正数或者倒数的范围。假设文件是100个字节那么
- “0-”表示从文档起点到文档终点相当于“0-99”即整个文件
- “10-”是从第10个字节开始到文档末尾相当于“10-99”
- “-1”是文档的最后一个字节相当于“99-99”
- “-10”是从文档末尾倒数10个字节相当于“90-99”。
服务器收到Range字段后需要做四件事。
第一它必须检查范围是否合法比如文件只有100个字节但请求“200-300”这就是范围越界了。服务器就会返回状态码**416**,意思是“你的范围请求有误,我无法处理,请再检查一下”。
第二如果范围正确服务器就可以根据Range头计算偏移量读取文件的片段了返回状态码“**206 Partial Content**”和200的意思差不多但表示body只是原数据的一部分。
第三,服务器要添加一个响应头字段**Content-Range**,告诉片段的实际偏移量和资源的总大小,格式是“**bytes x-y/length**”与Range头区别在没有“=”范围后多了总长度。例如对于“0-10”的范围请求值就是“bytes 0-10/100”。
最后剩下的就是发送数据了直接把片段用TCP发给客户端一个范围请求就算是处理完了。
你可以用实验环境的URI“/16-2”来测试范围请求它处理的对象是“/mime/a.txt”。不过我们不能用Chrome浏览器因为它没有编辑HTTP请求头的功能这点上不如Firefox方便所以还是要用Telnet。
例如下面的这个请求使用Range字段获取了文件的前32个字节
```
GET /16-2 HTTP/1.1
Host: www.chrono.com
Range: bytes=0-31
```
返回的数据是(去掉了几个无关字段):
```
HTTP/1.1 206 Partial Content
Content-Length: 32
Accept-Ranges: bytes
Content-Range: bytes 0-31/96
// this is a plain text json doc
```
有了范围请求之后HTTP处理大文件就更加轻松了看视频时可以根据时间点计算出文件的Range不用下载整个文件直接精确获取片段所在的数据内容。
不仅看视频的拖拽进度需要范围请求,常用的下载工具里的多段下载、断点续传也是基于它实现的,要点是:
- 先发个HEAD看服务器是否支持范围请求同时获取文件的大小
- 开N个线程每个线程使用Range字段划分出各自负责下载的片段发请求传输数据
- 下载意外中断也不怕不必重头再来一遍只要根据上次的下载记录用Range请求剩下的那一部分就可以了。
## 多段数据
刚才说的范围请求一次只获取一个片段其实它还支持在Range头里使用多个“x-y”一次性获取多个片段数据。
这种情况需要使用一种特殊的MIME类型“**multipart/byteranges**”表示报文的body是由多段字节序列组成的并且还要用一个参数“**boundary=xxx**”给出段之间的分隔标记。
多段数据的格式与分块传输也比较类似但它需要用分隔标记boundary来区分不同的片段可以通过图来对比一下。
<img src="https://static001.geekbang.org/resource/image/ff/37/fffa3a65e367c496428f3c0c4dac8a37.png" alt="">
每一个分段必须以“- -boundary”开始前面加两个“-”之后要用“Content-Type”和“Content-Range”标记这段数据的类型和所在范围然后就像普通的响应头一样以回车换行结束再加上分段数据最后用一个“- -boundary- -”(前后各有两个“-”)表示所有的分段结束。
例如我们在实验环境里用Telnet发出有两个范围的请求
```
GET /16-2 HTTP/1.1
Host: www.chrono.com
Range: bytes=0-9, 20-29
```
得到的就会是下面这样:
```
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=00000000001
Content-Length: 189
Connection: keep-alive
Accept-Ranges: bytes
--00000000001
Content-Type: text/plain
Content-Range: bytes 0-9/96
// this is
--00000000001
Content-Type: text/plain
Content-Range: bytes 20-29/96
ext json d
--00000000001--
```
报文里的“- -00000000001”就是多段的分隔符使用它客户端就可以很容易地区分出多段Range 数据。
## 小结
今天我们学习了HTTP传输大文件相关的知识在这里做一下简单小结
1. 压缩HTML等文本文件是传输大文件最基本的方法
1. 分块传输可以流式收发数据节约内存和带宽使用响应头字段“Transfer-Encoding: chunked”来表示分块的格式是16进制长度头+数据块;
1. 范围请求可以只获取部分数据即“分块请求”实现视频拖拽或者断点续传使用请求头字段“Range”和响应头字段“Content-Range”响应状态码必须是206
1. 也可以一次请求多个范围这时候响应报文的数据类型是“multipart/byteranges”body里的多个部分会用boundary字符串分隔。
要注意这四种方法不是互斥的而是可以混合起来使用例如压缩后再分块传输或者分段后再分块实验环境的URI“/16-3”就模拟了后一种的情形你可以自己用Telnet试一下。
## 课下作业
1. 分块传输数据的时候,如果数据里含有回车换行(\r\n是否会影响分块的处理呢
1. 如果对一个被gzip的文件执行范围请求比如“Range: bytes=10-19”那么这个范围是应用于原文件还是压缩后的文件呢
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/ab/71/ab951899844cef3d1e029ba094c2eb71.png" alt="unpreview">

View File

@@ -0,0 +1,138 @@
<audio id="audio" title="17 | 排队也要讲效率HTTP的连接管理" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/6d/31/6d6ee2a60f872bdddf2616d64d4ae531.mp3"></audio>
在[第14讲](https://time.geekbang.org/column/article/103746)里我曾经提到过HTTP的性能问题用了六个字来概括“**不算差,不够好**”。同时我也谈到了“队头阻塞”但由于时间的限制没有展开来细讲这次就来好好地看看HTTP在连接这方面的表现。
HTTP的连接管理也算得上是个“老生常谈”的话题了你一定曾经听说过“短连接”“长连接”之类的名词今天让我们一起来把它们弄清楚。
## 短连接
HTTP协议最初0.9/1.0)是个非常简单的协议,通信过程也采用了简单的“请求-应答”方式。
它底层的数据传输基于TCP/IP每次发送请求前需要先与服务器建立连接收到响应报文后会立即关闭连接。
因为客户端与服务器的整个连接过程很短暂,不会与服务器保持长时间的连接状态,所以就被称为“**短连接**”short-lived connections。早期的HTTP协议也被称为是“**无连接**”的协议。
短连接的缺点相当严重因为在TCP协议里建立连接和关闭连接都是非常“昂贵”的操作。TCP建立连接要有“三次握手”发送3个数据包需要1个RTT关闭连接是“四次挥手”4个数据包需要2个RTT。
而HTTP的一次简单“请求-响应”通常只需要4个包如果不算服务器内部的处理时间最多是2个RTT。这么算下来浪费的时间就是“3÷5=60%”,有三分之二的时间被浪费掉了,传输效率低得惊人。
<img src="https://static001.geekbang.org/resource/image/54/0c/54315ed9ac37fbc6547258040f00a80c.png" alt="">
单纯地从理论上讲TCP协议你可能还不太好理解我就拿打卡考勤机来做个形象的比喻吧。
假设你的公司买了一台打卡机,放在前台,因为这台机器比较贵,所以专门做了一个保护罩盖着它,公司要求每次上下班打卡时都要先打开盖子,打卡后再盖上盖子。
可是偏偏这个盖子非常牢固打开关闭要费很大力气打卡可能只要1秒钟而开关盖子却需要四五秒钟大部分时间都浪费在了毫无意义的开关盖子操作上了。
可想而知,平常还好说,一到上下班的点在打卡机前就会排起长队,每个人都要重复“开盖-打卡-关盖”的三个步骤,你说着急不着急。
在这个比喻里打卡机就相当于服务器盖子的开关就是TCP的连接与关闭而每个打卡的人就是HTTP请求很显然短连接的缺点严重制约了服务器的服务能力导致它无法处理更多的请求。
## 长连接
针对短连接暴露出的缺点HTTP协议就提出了“**长连接**”的通信方式也叫“持久连接”persistent connections、“连接保活”keep alive、“连接复用”connection reuse
其实解决办法也很简单,用的就是“**成本均摊**”的思路既然TCP的连接和关闭非常耗时间那么就把这个时间成本由原来的一个“请求-应答”均摊到多个“请求-应答”上。
这样虽然不能改善TCP的连接效率但基于“**分母效应**”,每个“请求-应答”的无效时间就会降低不少,整体传输效率也就提高了。
这里我画了一个短连接与长连接的对比示意图。
<img src="https://static001.geekbang.org/resource/image/57/b4/57b3d80234a1f1b8c538a376aa01d3b4.png" alt="">
在短连接里发送了三次HTTP“请求-应答”每次都会浪费60%的RTT时间。而在长连接的情况下同样发送三次请求因为只在第一次时建立连接在最后一次时关闭连接所以浪费率就是“3÷9≈33%”,降低了差不多一半的时间损耗。显然,如果在这个长连接上发送的请求越多,分母就越大,利用率也就越高。
继续用刚才的打卡机的比喻,公司也觉得这种反复“开盖-打卡-关盖”的操作太“反人类”了,于是颁布了新规定,早上打开盖子后就不用关上了,可以自由打卡,到下班后再关上盖子。
这样打卡的效率(即服务能力)就大幅度提升了,原来一次打卡需要五六秒钟,现在只要一秒就可以了,上下班时排长队的景象一去不返,大家都开心。
## 连接相关的头字段
由于长连接对性能的改善效果非常显著所以在HTTP/1.1中的连接都会默认启用长连接。不需要用什么特殊的头字段指定只要向服务器发送了第一次请求后续的请求都会重复利用第一次打开的TCP连接也就是长连接在这个连接上收发数据。
当然,我们也可以在请求头里明确地要求使用长连接机制,使用的字段是**Connection**,值是“**keep-alive**”。
不过不管客户端是否显式要求长连接,如果服务器支持长连接,它总会在响应报文里放一个“**Connection: keep-alive**”字段告诉客户端“我是支持长连接的接下来就用这个TCP一直收发数据吧”。
你可以在实验环境里访问URI“/17-1”用Chrome看一下服务器返回的响应头
<img src="https://static001.geekbang.org/resource/image/27/c6/27f13aacad9704368ce383b764c46bc6.png" alt="">
不过长连接也有一些小缺点,问题就出在它的“长”字上。
因为TCP连接长时间不关闭服务器必须在内存里保存它的状态这就占用了服务器的资源。如果有大量的空闲长连接只连不发就会很快耗尽服务器的资源导致服务器无法为真正有需要的用户提供服务。
所以,长连接也需要在恰当的时间关闭,不能永远保持与服务器的连接,这在客户端或者服务器都可以做到。
在客户端,可以在请求头里加上“**Connection: close**”字段告诉服务器“这次通信后就关闭连接”。服务器看到这个字段就知道客户端要主动关闭连接于是在响应报文里也加上这个字段发送之后就调用Socket API关闭TCP连接。
服务器端通常不会主动关闭连接但也可以使用一些策略。拿Nginx来举例它有两种方式
1. 使用“keepalive_timeout”指令设置长连接的超时时间如果在一段时间内连接上没有任何数据收发就主动断开连接避免空闲连接占用系统资源。
1. 使用“keepalive_requests”指令设置长连接上可发送的最大请求次数。比如设置成1000那么当Nginx在这个连接上处理了1000个请求后也会主动断开连接。
另外客户端和服务器都可以在报文里附加通用头字段“Keep-Alive: timeout=value”限定长连接的超时时间。但这个字段的约束力并不强通信的双方可能并不会遵守所以不太常见。
我们的实验环境配置了“keepalive_timeout 60”和“keepalive_requests 5”意思是空闲连接最多60秒最多发送5个请求。所以如果连续刷新五次页面就能看到响应头里的“Connection: close”了。
把这个过程用Wireshark抓一下包就能够更清晰地看到整个长连接中的握手、收发数据与挥手过程在课后你可以再实际操作看看。
<img src="https://static001.geekbang.org/resource/image/ec/45/ecfb04b7a97f3591efedc428800a4845.png" alt="">
## 队头阻塞
看完了短连接和长连接接下来就要说到著名的“队头阻塞”Head-of-line blocking也叫“队首阻塞”了。
“队头阻塞”与短连接和长连接无关而是由HTTP基本的“请求-应答”模型所导致的。
因为HTTP规定报文必须是“一发一收”这就形成了一个先进先出的“串行”队列。队列里的请求没有轻重缓急的优先级只有入队的先后顺序排在最前面的请求被最优先处理。
如果队首的请求因为处理的太慢耽误了时间,那么队列里后面的所有请求也不得不跟着一起等待,结果就是其他的请求承担了不应有的时间成本。
<img src="https://static001.geekbang.org/resource/image/6a/72/6a6d30a89fb085d5f1773a887aaf5572.png" alt="">
还是用打卡机做个比喻。
上班的时间点上,大家都在排队打卡,可这个时候偏偏最前面的那个人遇到了打卡机故障,怎么也不能打卡成功,急得满头大汗。等找人把打卡机修好,后面排队的所有人全迟到了。
## 性能优化
因为“请求-应答”模型不能变所以“队头阻塞”问题在HTTP/1.1里无法解决,只能缓解,有什么办法呢?
公司里可以再多买几台打卡机放在前台,这样大家可以不用挤在一个队伍里,分散打卡,一个队伍偶尔阻塞也不要紧,可以改换到其他不阻塞的队伍。
这在HTTP里就是“**并发连接**”concurrent connections也就是同时对一个域名发起多个长连接用数量来解决质量的问题。
但这种方式也存在缺陷。如果每个客户端都想自己快,建立很多个连接,用户数×并发数就会是个天文数字。服务器的资源根本就扛不住,或者被服务器认为是恶意攻击,反而会造成“拒绝服务”。
所以HTTP协议建议客户端使用并发但不能“滥用”并发。RFC2616里明确限制每个客户端最多并发2个连接。不过实践证明这个数字实在是太小了众多浏览器都“无视”标准把这个上限提高到了6~8。后来修订的RFC7230也就“顺水推舟”取消了这个“2”的限制。
但“并发连接”所压榨出的性能也跟不上高速发展的互联网无止境的需求,还有什么别的办法吗?
公司发展的太快了,员工越来越多,上下班打卡成了迫在眉睫的大问题。前台空间有限,放不下更多的打卡机了,怎么办?那就多开几个打卡的地方,每个楼层、办公区的入口也放上三四台打卡机,把人进一步分流,不要都往前台挤。
这个就是“**域名分片**”domain sharding技术还是用数量来解决质量的思路。
HTTP协议和浏览器不是限制并发连接数量吗那我就多开几个域名比如shard1.chrono.com、shard2.chrono.com而这些域名都指向同一台服务器www.chrono.com这样实际长连接的数量就又上去了真是“美滋滋”。不过实在是有点“上有政策下有对策”的味道。
## 小结
这一讲中我们学习了HTTP协议里的短连接和长连接简单小结一下今天的内容
1. 早期的HTTP协议使用短连接收到响应后就立即关闭连接效率很低
1. HTTP/1.1默认启用长连接,在一个连接上收发多个请求响应,提高了传输效率;
1. 服务器会发送“Connection: keep-alive”字段表示启用了长连接
1. 报文头里如果有“Connection: close”就意味着长连接即将关闭
1. 过多的长连接会占用服务器资源,所以服务器会用一些策略有选择地关闭长连接;
1. “队头阻塞”问题会导致性能下降,可以用“并发连接”和“域名分片”技术缓解。
## 课下作业
1. 在开发基于HTTP协议的客户端时应该如何选择使用的连接模式呢短连接还是长连接
1. 应当如何降低长连接对服务器的负面影响呢?
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/f9/72/f93afe4b663d681b8ce63c947f478072.png" alt="unpreview">

View File

@@ -0,0 +1,169 @@
<audio id="audio" title="18 | 四通八达HTTP的重定向和跳转" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ce/f0/ce5c9c5647eb34a1af654866d8533bf0.mp3"></audio>
在专栏[第1讲](https://time.geekbang.org/column/article/97837)时我曾经说过,为了实现在互联网上构建超链接文档系统的设想,蒂姆·伯纳斯-李发明了万维网使用HTTP协议传输“超文本”让全世界的人都能够自由地共享信息。
“超文本”里含有“超链接”,可以从一个“超文本”跳跃到另一个“超文本”,对线性结构的传统文档是一个根本性的变革。
能够使用“超链接”在网络上任意地跳转也是万维网的一个关键特性。它把分散在世界各地的文档连接在一起,形成了复杂的网状结构,用户可以在查看时随意点击链接、转换页面。再加上浏览器又提供了“前进”“后退”“书签”等辅助功能,让用户在文档间跳转时更加方便,有了更多的主动性和交互性。
那么点击页面“链接”时的跳转是怎样的呢具体一点比如在Nginx的主页上点了一下“download”链接会发生什么呢
结合之前的课程稍微思考一下你就能得到答案浏览器首先要解析链接文字里的URI。
```
http://nginx.org/en/download.html
```
再用这个URI发起一个新的HTTP请求获取响应报文后就会切换显示内容渲染出新URI指向的页面。
这样的跳转动作是由浏览器的使用者主动发起的,可以称为“**主动跳转**”,但还有一类跳转是由服务器来发起的,浏览器使用者无法控制,相对地就可以称为“**被动跳转**”这在HTTP协议里有个专门的名词叫做“**重定向**”Redirection
## 重定向的过程
其实之前我们就已经见过重定向了,在[第12讲](https://time.geekbang.org/column/article/102483)里3××状态码时就说过301是“永久重定向”302是“临时重定向”浏览器收到这两个状态码就会跳转到新的URI。
那么,它们是怎么做到的呢?难道仅仅用这两个代码就能够实现跳转页面吗?
先在实验环境里看一下重定向的过程吧用Chrome访问URI “/18-1”它会使用302立即跳转到“/index.html”。
<img src="https://static001.geekbang.org/resource/image/ad/2d/ad5eb7546ee7ef62a9987120934b592d.png" alt="">
从这个实验可以看到这一次“重定向”实际上发送了两次HTTP请求第一个请求返回了302然后第二个请求就被重定向到了“/index.html”。但如果不用开发者工具的话你是完全看不到这个跳转过程的也就是说重定向是“用户无感知”的。
我们再来看看第一个请求返回的响应报文:
<img src="https://static001.geekbang.org/resource/image/a4/ac/a4276db758bf90f63fd5a5c2357af4ac.png" alt="">
这里出现了一个新的头字段“Location: /index.html”它就是301/302重定向跳转的秘密所在。
“**Location**”字段属于响应字段必须出现在响应报文里。但只有配合301/302状态码才有意义它**标记了服务器要求重定向的URI**这里就是要求浏览器跳转到“index.html”。
浏览器收到301/302报文会检查响应头里有没有“Location”。如果有就从字段值里提取出URI发出新的HTTP请求相当于自动替我们点击了这个链接。
在“Location”里的URI既可以使用绝对URI也可以使用相对URI。所谓“绝对URI”就是完整形式的URI包括scheme、host:port、path等。所谓“相对URI”就是省略了scheme和host:port只有path和query部分是不完整的但可以从请求上下文里计算得到。
例如刚才的实验例子里的“Location: /index.html”用的就是相对URI。它没有说明访问URI的协议和主机但因为是由“[http://www.chrono.com/18-1](http://www.chrono.com/18-1)”重定向返回的响应报文所以浏览器就可以拼出完整的URI
```
http://www.chrono.com/index.html
```
实验环境的URI“/18-1”还支持使用query参数“dst=xxx”指明重定向的URI你可以用这种形式再多试几次重定向看看浏览器是如何工作的。
```
http://www.chrono.com/18-1?dst=/15-1?name=a.json
http://www.chrono.com/18-1?dst=/17-1
```
注意在重定向时如果只是在站内跳转你可以放心地使用相对URI。但如果要跳转到站外就必须用绝对URI。
例如如果想跳转到Nginx官网就必须在“nginx.org”前把“http://”都写出来否则浏览器会按照相对URI去理解得到的就会是一个不存在的URI“[http://www.chrono.com/nginx.org”](http://www.chrono.com/nginx.org%E2%80%9D)
```
http://www.chrono.com/18-1?dst=nginx.org #错误
http://www.chrono.com/18-1?dst=http://nginx.org #正确
```
<img src="https://static001.geekbang.org/resource/image/00/aa/006059602ee75b176a80429f49ffc9aa.png" alt="">
那么如果301/302跳转时没有Location字段会怎么样呢
这个你也可以自己试一下使用第12讲里的URI“/12-1”查询参数用“code=302”
```
http://www.chrono.com/12-1?code=302
```
## 重定向状态码
刚才我把重定向的过程基本讲完了,现在来说一下重定向用到的状态码。
最常见的重定向状态码就是301和302另外还有几个不太常见的例如303、307、308等。它们最终的效果都差不多让浏览器跳转到新的URI但语义上有一些细微的差别使用的时候要特别注意。
**301**俗称“永久重定向”Moved Permanently意思是原URI已经“永久”性地不存在了今后的所有请求都必须改用新的URI。
浏览器看到301就知道原来的URI“过时”了就会做适当的优化。比如历史记录、更新书签下次可能就会直接用新的URI访问省去了再次跳转的成本。搜索引擎的爬虫看到301也会更新索引库不再使用老的URI。
**302**俗称“临时重定向”“Moved Temporarily”意思是原URI处于“临时维护”状态新的URI是起“顶包”作用的“临时工”。
浏览器或者爬虫看到302会认为原来的URI仍然有效但暂时不可用所以只会执行简单的跳转页面不记录新的URI也不会有其他的多余动作下次访问还是用原URI。
301/302是最常用的重定向状态码在3××里剩下的几个还有
- 303 See Other类似302但要求重定向后的请求改为GET方法访问一个结果页面避免POST/PUT重复操作
- 307 Temporary Redirect类似302但重定向后请求里的方法和实体不允许变动含义比302更明确
- 308 Permanent Redirect类似307不允许重定向后的请求变动但它是301“永久重定向”的含义。
不过这三个状态码的接受程度较低,有的浏览器和服务器可能不支持,开发时应当慎重,测试确认浏览器的实际效果后才能使用。
## 重定向的应用场景
理解了重定向的工作原理和状态码的含义,我们就可以**在服务器端拥有主动权**,控制浏览器的行为,不过要怎么利用重定向才好呢?
使用重定向跳转,核心是要理解“**重定向**”和“**永久/临时**”这两个关键词。
先来看什么时候需要重定向。
一个最常见的原因就是“**资源不可用**”需要用另一个新的URI来代替。
至于不可用的原因那就很多了。例如域名变更、服务器变更、网站改版、系统维护这些都会导致原URI指向的资源无法访问为了避免出现404就需要用重定向跳转到新的URI继续为网民提供服务。
另一个原因就是“**避免重复**”让多个网址都跳转到一个URI增加访问入口的同时还不会增加额外的工作量。
例如有的网站都会申请多个名称类似的域名然后把它们再重定向到主站上。比如你可以访问一下“qq.com”“github.com ”“bing.com”记得事先清理缓存看看它是如何重定向的。
决定要实行重定向后接下来要考虑的就是“永久”和“临时”的问题了也就是选择301还是302。
301的含义是“**永久**”的。
如果域名、服务器、网站架构发生了大幅度的改变比如启用了新域名、服务器切换到了新机房、网站目录层次重构这些都算是“永久性”的改变。原来的URI已经不能用了必须用301“永久重定向”通知浏览器和搜索引擎更新到新地址这也是搜索引擎优化SEO要考虑的因素之一。
302的含义是“**临时**”的。
原来的URI在将来的某个时间点还会恢复正常常见的应用场景就是系统维护把网站重定向到一个通知页面告诉用户过一会儿再来访问。另一种用法就是“服务降级”比如在双十一促销的时候把订单查询、领积分等不重要的功能入口暂时关闭保证核心服务能够正常运行。
## 重定向的相关问题
重定向的用途很多,掌握了重定向,就能够在架设网站时获得更多的灵活性,不过在使用时还需要注意两个问题。
第一个问题是“**性能损耗**”。很明显,重定向的机制决定了一个跳转会有两次请求-应答,比正常的访问多了一次。
虽然301/302报文很小但大量的跳转对服务器的影响也是不可忽视的。站内重定向还好说可以长连接复用站外重定向就要开两个连接如果网络连接质量差那成本可就高多了会严重影响用户的体验。
所以重定向应当适度使用,决不能滥用。
第二个问题是“**循环跳转**”。如果重定向的策略设置欠考虑可能会出现“A=&gt;B=&gt;C=&gt;A”的无限循环不停地在这个链路里转圈圈后果可想而知。
所以HTTP协议特别规定浏览器必须具有检测“循环跳转”的能力在发现这种情况时应当停止发送请求并给出错误提示。
实验环境的URI“/18-2”就模拟了这样的一个“循环跳转”它跳转到“/18-1”并用参数“dst=/18-2”再跳回自己实现了两个URI的无限循环。
使用Chrome访问这个地址会得到“该网页无法正常运作”的结果
<img src="https://static001.geekbang.org/resource/image/4b/2f/4b91aeea08d90f173c62493934e5f52f.png" alt="">
## 小结
今天我们学习了HTTP里的重定向和跳转简单小结一下这次的内容
1. 重定向是服务器发起的跳转要求客户端改用新的URI重新发送请求通常会自动进行用户是无感知的
1. 301/302是最常用的重定向状态码分别是“永久重定向”和“临时重定向”
1. 响应头字段Location指示了要跳转的URI可以用绝对或相对的形式
1. 重定向可以把一个URI指向另一个URI也可以把多个URI指向同一个URI用途很多
1. 使用重定向时需要当心性能损耗,还要避免出现循环跳转。
## 课下作业
1. 301和302非常相似试着结合第12讲用自己的理解再描述一下两者的异同点。
1. 你能结合自己的实际情况,再列出几个应当使用重定向的场景吗?
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/9b/05/9b873d25e33f86bb2818fc8b50fbff05.png" alt="unpreview">

View File

@@ -0,0 +1,140 @@
<audio id="audio" title="19 | 让我知道你是谁HTTP的Cookie机制" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/02/e4/02aa096c0f4c679d4037d358dba0d2e4.mp3"></audio>
在之前的[第13讲](https://time.geekbang.org/column/article/103270)、[第14讲](https://time.geekbang.org/column/article/103746)中我曾经说过HTTP是“无状态”的这既是优点也是缺点。优点是服务器没有状态差异可以很容易地组成集群而缺点就是无法支持需要记录状态的事务操作。
好在HTTP协议是可扩展的后来发明的Cookie技术给HTTP增加了“记忆能力”。
## 什么是Cookie
不知道你有没有看过克里斯托弗·诺兰导演的一部经典电影《记忆碎片》Memento里面的主角患有短期失忆症记不住最近发生的事情。
<video poster="https://static001.geekbang.org/resource/image/81/25/816df396bae0b37101543f967ff82125.jpeg" preload="none" controls=""><source src="https://media001.geekbang.org/customerTrans/fe4a99b62946f2c31c2095c167b26f9c/11137c6c-16d14222dfe-0000-0000-01d-dbacd.mp4" type="video/mp4"><source src="https://media001.geekbang.org/fccbc2d505ee4d33a5b4c61ddf2d79bd/a20b79548de949b1a9c5adc39d46334f-78b40e38c736ae3917956dc5ead50a1e-sd.m3u8" type="application/x-mpegURL"><source src="https://media001.geekbang.org/fccbc2d505ee4d33a5b4c61ddf2d79bd/a20b79548de949b1a9c5adc39d46334f-689660f8bb4929b7b37d995a528eeae1-hd.m3u8" type="application/x-mpegURL"></video>
比如,电影里有个场景,某人刚跟主角说完话,大闹了一通,过了几分钟再回来,主角却是一脸茫然,完全不记得这个人是谁,刚才又做了什么,只能任人摆布。
这种情况就很像HTTP里“无状态”的Web服务器只不过服务器的“失忆症”比他还要严重连一分钟的记忆也保存不了请求处理完立刻就忘得一干二净。即使这个请求会让服务器发生500的严重错误下次来也会依旧“热情招待”。
如果Web服务器只是用来管理静态文件还好说对方是谁并不重要把文件从磁盘读出来发走就可以了。但随着HTTP应用领域的不断扩大对“记忆能力”的需求也越来越强烈。比如网上论坛、电商购物都需要“看客下菜”只有记住用户的身份才能执行发帖子、下订单等一系列会话事务。
那该怎么样让原本无“记忆能力”的服务器拥有“记忆能力”呢?
看看电影里的主角是怎么做的吧。他通过纹身、贴纸条、立拍得等手段,在外界留下了各种记录,一旦失忆,只要看到这些提示信息,就能够在头脑中快速重建起之前的记忆,从而把因失忆而耽误的事情继续做下去。
HTTP的Cookie机制也是一样的道理既然服务器记不住那就在外部想办法记住。相当于是服务器给每个客户端都贴上一张小纸条上面写了一些只有服务器才能理解的数据需要的时候客户端把这些信息发给服务器服务器看到Cookie就能够认出对方是谁了。
## Cookie的工作过程
那么Cookie这张小纸条是怎么传递的呢
这要用到两个字段:响应头字段**Set-Cookie**和请求头字段**Cookie**。
当用户通过浏览器第一次访问服务器的时候,服务器肯定是不知道他的身份的。所以,就要创建一个独特的身份标识数据,格式是“**key=value**”然后放进Set-Cookie字段里随着响应报文一同发给浏览器。
浏览器收到响应报文看到里面有Set-Cookie知道这是服务器给的身份标识于是就保存起来下次再请求的时候就自动把这个值放进Cookie字段里发给服务器。
因为第二次请求里面有了Cookie字段服务器就知道这个用户不是新人之前来过就可以拿出Cookie里的值识别出用户的身份然后提供个性化的服务。
不过因为服务器的“记忆能力”实在是太差一张小纸条经常不够用。所以服务器有时会在响应头里添加多个Set-Cookie存储多个“key=value”。但浏览器这边发送时不需要用多个Cookie字段只要在一行里用“;”隔开就行。
我画了一张图来描述这个过程,你看过就能理解了。
<img src="https://static001.geekbang.org/resource/image/9f/a4/9f6cca61802d65d063e24aa9ca7c38a4.png" alt="">
从这张图中我们也能够看到Cookie是由浏览器负责存储的而不是操作系统。所以它是“浏览器绑定”的只能在本浏览器内生效。
如果你换个浏览器或者换台电脑新的浏览器里没有服务器对应的Cookie就好像是脱掉了贴着纸条的衣服“健忘”的服务器也就认不出来了只能再走一遍Set-Cookie流程。
在实验环境里你可以用Chrome访问URI“/19-1”实地看一下Cookie工作过程。
首次访问时服务器会设置两个Cookie。
<img src="https://static001.geekbang.org/resource/image/97/87/974063541e5f9b43893db45ac4ce3687.png" alt="">
然后刷新这个页面浏览器就会在请求头里自动送出Cookie服务器就能认出你了。
<img src="https://static001.geekbang.org/resource/image/da/9f/da9b39d88ddd717a6e3feb6637dc3f9f.png" alt="">
如果换成Firefox等其他浏览器因为Cookie是存在Chrome里的所以服务器就又“蒙圈”了不知道你是谁就会给Firefox再贴上小纸条。
## Cookie的属性
说到这里你应该知道了Cookie就是服务器委托浏览器存储在客户端里的一些数据而这些数据通常都会记录用户的关键识别信息。所以就需要在“key=value”外再用一些手段来保护防止外泄或窃取这些手段就是Cookie的属性。
下面这个截图是实验环境“/19-2”的响应头我来对着这个实际案例讲一下都有哪些常见的Cookie属性。
<img src="https://static001.geekbang.org/resource/image/9d/5d/9dbb8b490714360475911ca04134df5d.png" alt="">
首先,我们应该**设置Cookie的生存周期**也就是它的有效期让它只能在一段时间内可用就像是食品的“保鲜期”一旦超过这个期限浏览器就认为是Cookie失效在存储里删除也不会发送给服务器。
Cookie的有效期可以使用Expires和Max-Age两个属性来设置。
“**Expires**”俗称“过期时间”用的是绝对时间点可以理解为“截止日期”deadline。“**Max-Age**”用的是相对时间单位是秒浏览器用收到报文的时间点再加上Max-Age就可以得到失效的绝对时间。
Expires和Max-Age可以同时出现两者的失效时间可以一致也可以不一致但浏览器会优先采用Max-Age计算失效期。
比如在这个例子里Expires标记的过期时间是“GMT 2019年6月7号8点19分”而Max-Age则只有10秒如果现在是6月6号零点那么Cookie的实际有效期就是“6月6号零点过10秒”。
其次,我们需要**设置Cookie的作用域**让浏览器仅发送给特定的服务器和URI避免被其他网站盗用。
作用域的设置比较简单,“**Domain**”和“**Path**”指定了Cookie所属的域名和路径浏览器在发送Cookie前会从URI中提取出host和path部分对比Cookie的属性。如果不满足条件就不会在请求头里发送Cookie。
使用这两个属性可以为不同的域名和路径分别设置各自的Cookie比如“/19-1”用一个Cookie“/19-2”再用另外一个Cookie两者互不干扰。不过现实中为了省事通常Path就用一个“/”或者直接省略表示域名下的任意路径都允许使用Cookie让服务器自己去挑。
最后要考虑的就是**Cookie的安全性**了,尽量不要让服务器以外的人看到。
写过前端的同学一定知道在JS脚本里可以用document.cookie来读写Cookie数据这就带来了安全隐患有可能会导致“跨站脚本”XSS攻击窃取数据。
属性“**HttpOnly**”会告诉浏览器此Cookie只能通过浏览器HTTP协议传输禁止其他方式访问浏览器的JS引擎就会禁用document.cookie等一切相关的API脚本攻击也就无从谈起了。
另一个属性“**SameSite**”可以防范“跨站请求伪造”XSRF攻击设置成“SameSite=Strict”可以严格限定Cookie不能随着跳转链接跨站发送而“SameSite=Lax”则略宽松一点允许GET/HEAD等安全方法但禁止POST跨站发送。
还有一个属性叫“**Secure**”表示这个Cookie仅能用HTTPS协议加密传输明文的HTTP协议会禁止发送。但Cookie本身不是加密的浏览器里还是以明文的形式存在。
Chrome开发者工具是查看Cookie的有力工具在“Network-Cookies”里可以看到单个页面Cookie的各种属性另一个“Application”面板里则能够方便地看到全站的所有Cookie。
<img src="https://static001.geekbang.org/resource/image/a8/9d/a8accc7e1836fa348c2fbd29f494069d.png" alt="">
<img src="https://static001.geekbang.org/resource/image/37/6e/37fbfef0490a20179c0ee274dccf5e6e.png" alt="">
## Cookie的应用
现在回到我们最开始的话题有了Cookie服务器就有了“记忆能力”能够保存“状态”那么应该如何使用Cookie呢
Cookie最基本的一个用途就是**身份识别**,保存用户的登录信息,实现会话事务。
比如你用账号和密码登录某电商登录成功后网站服务器就会发给浏览器一个Cookie内容大概是“name=yourid”这样就成功地把身份标签贴在了你身上。
之后你在网站里随便访问哪件商品的页面浏览器都会自动把身份Cookie发给服务器所以服务器总会知道你的身份一方面免去了重复登录的麻烦另一方面也能够自动记录你的浏览记录和购物下单在后台数据库或者也用Cookie实现了“状态保持”。
Cookie的另一个常见用途是**广告跟踪**。
你上网的时候肯定看过很多的广告图片这些图片背后都是广告商网站例如Google它会“偷偷地”给你贴上Cookie小纸条这样你上其他的网站别的广告就能用Cookie读出你的身份然后做行为分析再推给你广告。
这种Cookie不是由访问的主站存储的所以又叫“第三方Cookie”third-party cookie。如果广告商势力很大广告到处都是那么就比较“恐怖”了无论你走到哪里它都会通过Cookie认出你来实现广告“精准打击”。
为了防止滥用Cookie搜集用户隐私互联网组织相继提出了DNTDo Not Track和P3PPlatform for Privacy Preferences Project但实际作用不大。
## 小结
今天我们学习了HTTP里的Cookie知识。虽然现在已经出现了多种Local Web Storage技术能够比Cookie存储更多的数据但Cookie仍然是最通用、兼容性最强的客户端数据存储手段。
简单小结一下今天的内容:
1. Cookie是服务器委托浏览器存储的一些数据让服务器有了“记忆能力”
1. 响应报文使用Set-Cookie字段发送“key=value”形式的Cookie值
1. 请求报文里用Cookie字段发送多个Cookie值
1. 为了保护Cookie还要给它设置有效期、作用域等属性常用的有Max-Age、Expires、Domain、HttpOnly等
1. Cookie最基本的用途是身份识别实现有状态的会话事务。
还要提醒你一点因为Cookie并不属于HTTP标准RFC6265而不是RFC2616/7230所以语法上与其他字段不太一致使用的分隔符是“;”与Accept等字段的“,”不同,小心不要弄错了。
## 课下作业
1. 如果Cookie的Max-Age属性设置为0会有什么效果呢
1. Cookie的好处已经很清楚了你觉得它有什么缺点呢
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/f0/97/f03db082760cfa8920b266ce44f52597.png" alt="unpreview">
<img src="https://static001.geekbang.org/resource/image/56/63/56d766fc04654a31536f554b8bde7b63.jpg" alt="unpreview">

View File

@@ -0,0 +1,162 @@
<audio id="audio" title="20 | 生鲜速递HTTP的缓存控制" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/2f/6e/2f82dcbcb3aa772e80b5b2c57f200b6e.mp3"></audio>
缓存Cache是计算机领域里的一个重要概念是优化系统性能的利器。
由于链路漫长网络时延不可控浏览器使用HTTP获取资源的成本较高。所以非常有必要把“来之不易”的数据缓存起来下次再请求的时候尽可能地复用。这样就可以避免多次请求-应答的通信成本,节约网络带宽,也可以加快响应速度。
试想一下如果有几十K甚至几十M的数据不是从网络而是从本地磁盘获取那将是多么大的一笔节省免去多少等待的时间。
实际上HTTP传输的每一个环节基本上都会有缓存非常复杂。
基于“请求-应答”模式的特点,可以大致分为客户端缓存和服务器端缓存,因为服务器端缓存经常与代理服务“混搭”在一起,所以今天我先讲客户端——也就是浏览器的缓存。
## 服务器的缓存控制
为了更好地说明缓存的运行机制,下面我用“生鲜速递”作为比喻,看看缓存是如何工作的。
夏天到了天气很热。你想吃西瓜消暑于是打开冰箱但很不巧冰箱是空的。不过没事现在物流很发达给生鲜超市打个电话不一会儿就给你送来一个8斤的沙瓤大西瓜上面还贴着标签“保鲜期5天”。好了你把它放进冰箱想吃的时候随时拿出来。
在这个场景里“生鲜超市”就是Web服务器“你”就是浏览器“冰箱”就是浏览器内部的缓存。整个流程翻译成HTTP就是
1. 浏览器发现缓存无数据,于是发送请求,向服务器获取资源;
1. 服务器响应请求,返回资源,同时标记资源的有效期;
1. 浏览器缓存资源,等待下次重用。
<img src="https://static001.geekbang.org/resource/image/a1/5b/a1968821f214df4a3ae16c9b30f99a5b.png" alt="">
你可以访问实验环境的URI “/20-1”看看具体的请求-应答过程。
<img src="https://static001.geekbang.org/resource/image/df/d8/dfd2d20670443a782443fc3193ae1cd8.png" alt="">
服务器标记资源有效期使用的头字段是“**Cache-Control**”,里面的值“**max-age=30**”就是资源的有效时间相当于告诉浏览器“这个页面只能缓存30秒之后就算是过期不能用。”
你可能要问了,让浏览器直接缓存数据就好了,为什么要加个有效期呢?
这是因为网络上的数据随时都在变化不能保证它稍后的一段时间还是原来的样子。就像生鲜超市给你快递的西瓜只有5天的保鲜期过了这个期限最好还是别吃不然可能会闹肚子。
“Cache-Control”字段里的“max-age”和上一讲里Cookie有点像都是标记资源的有效期。
但我必须提醒你注意这里的max-age是“**生存时间**”又叫“新鲜度”“缓存寿命”类似TTLTime-To-Live时间的计算起点是响应报文的创建时刻即Date字段也就是离开服务器的时刻而不是客户端收到报文的时刻也就是说包含了在链路传输过程中所有节点所停留的时间。
比如服务器设定“max-age=5”但因为网络质量很糟糕等浏览器收到响应报文已经过去了4秒那么这个资源在客户端就最多能够再存1秒钟之后就会失效。
“max-age”是HTTP缓存控制最常用的属性此外在响应报文里还可以用其他的属性来更精确地指示浏览器应该如何使用缓存
- no-store**不允许缓存**,用于某些变化非常频繁的数据,例如秒杀页面;
- no-cache它的字面含义容易与no-store搞混实际的意思并不是不允许缓存而是**可以缓存**,但在使用之前必须要去服务器验证是否过期,是否有最新的版本;
- must-revalidate又是一个和no-cache相似的词它的意思是如果缓存不过期就可以继续使用但过期了如果还想用就必须去服务器验证。
听的有点糊涂吧。没关系,我拿生鲜速递来举例说明一下:
- no-store买来的西瓜不允许放进冰箱要么立刻吃要么立刻扔掉
- no-cache可以放进冰箱但吃之前必须问超市有没有更新鲜的有就吃超市里的
- must-revalidate可以放进冰箱保鲜期内可以吃过期了就要问超市让不让吃。
你看这超市管的还真多啊西瓜到了家里怎么吃还得听他。不过没办法在HTTP协议里服务器就是这样的“霸气”。
我把服务器的缓存控制策略画了一个流程图对照着它你就可以在今后的后台开发里明确“Cache-Control”的用法了。
<img src="https://static001.geekbang.org/resource/image/1b/99/1b4f48bc0d8fb9a08b45d1f0deac8a99.png" alt="">
## 客户端的缓存控制
现在冰箱里已经有了“缓存”的西瓜,是不是就可以直接开吃了呢?
你可以在Chrome里点几次“刷新”按钮估计你会失望页面上的ID一直在变根本不是缓存的结果明明说缓存30秒怎么就不起作用呢
其实不止服务器可以发“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”基本一样就看后台的服务器怎么理解通常两者的效果是相同的。
<img src="https://static001.geekbang.org/resource/image/2f/49/2fc3fa639f44b98d7c19d25604c65249.png" alt="">
那么,浏览器的缓存究竟什么时候才能生效呢?
别着急试着点一下浏览器的“前进”“后退”按钮再看开发者工具你就会惊喜地发现“from disk cache”的字样意思是没有发送网络请求而是读取的磁盘上的缓存。
另外,如果用[第18讲](https://time.geekbang.org/column/article/105614)里的重定向跳转功能,也可以发现浏览器使用了缓存:
```
http://www.chrono.com/18-1?dst=20-1
```
<img src="https://static001.geekbang.org/resource/image/f2/06/f2a12669e997ea6dc0f2228bcaf65a06.png" alt="">
这几个操作与刷新有什么区别呢?
其实也很简单在“前进”“后退”“跳转”这些重定向动作中浏览器不会“夹带私货”只用最基本的请求头没有“Cache-Control”所以就会检查缓存直接利用之前的资源不再进行网络通信。
这个过程你也可以用Wireshark抓包看看是否真的没有向服务器发请求。
## 条件请求
浏览器用“Cache-Control”做缓存控制只能是刷新数据不能很好地利用缓存数据又因为缓存会失效使用前还必须要去服务器验证是否是最新版。
那么该怎么做呢?
浏览器可以用两个连续的请求组成“验证动作”先是一个HEAD获取资源的修改时间等元信息然后与缓存数据比较如果没有改动就使用缓存节省网络流量否则就再发一个GET请求获取最新的版本。
但这样的两个请求网络成本太高了所以HTTP协议就定义了一系列“**If**”开头的“**条件请求**”字段,专门用来检查验证资源是否过期,把两个请求才能完成的工作合并在一个请求里做。而且,验证的责任也交给服务器,浏览器只需“坐享其成”。
条件请求一共有5个头字段我们最常用的是“**if-Modified-Since**”和“**If-None-Match**”这两个。需要第一次的响应报文预先提供“**Last-modified**”和“**ETag**”,然后第二次请求时就可以带上缓存里的原值,验证资源是否是最新的。
如果资源没有变,服务器就回应一个“**304 Not Modified**”,表示缓存依然有效,浏览器就可以更新一下有效期,然后放心大胆地使用缓存了。
<img src="https://static001.geekbang.org/resource/image/b2/37/b239d0804be630ce182e24ea9e4ab237.png" alt="">
“Last-modified”很好理解就是文件的最后修改时间。ETag是什么呢
ETag是“实体标签”Entity Tag的缩写**是资源的一个唯一标识**,主要是用来解决修改时间无法准确区分文件变化的问题。
比如,一个文件在一秒内修改了多次,但因为修改时间是秒级,所以这一秒内的新版本无法区分。
再比如,一个文件定期更新,但有时会是同样的内容,实际上没有变化,用修改时间就会误以为发生了变化,传送给浏览器就会浪费带宽。
使用ETag就可以精确地识别资源的变动情况让浏览器能够更有效地利用缓存。
ETag还有“强”“弱”之分。
强ETag要求资源在字节级别必须完全相符弱ETag在值前有个“W/”标记只要求资源在语义上没有变化但内部可能会有部分发生了改变例如HTML里的标签顺序调整或者多了几个空格
还是拿生鲜速递做比喻最容易理解:
你打电话给超市“我这个西瓜是3天前买的还有最新的吗”。超市看了一下库存“没有啊我这里都是3天前的。”于是你就知道了再让超市送货也没用还是吃冰箱里的西瓜吧。这就是“**if-Modified-Since**”和“**Last-modified**”。
但你还是想要最新的就又打电话“有不是沙瓤的西瓜吗超市告诉你都是沙瓤的Match于是你还是只能吃冰箱里的沙瓤西瓜。这就是“**If-None-Match**”和“**弱ETag**”。
第三次打电话你说“有不是8斤的沙瓤西瓜吗这回超市给了你满意的答复“有个10斤的沙瓤西瓜”。于是你就扔掉了冰箱里的存货让超市重新送了一个新的大西瓜。这就是“**If-None-Match**”和“**强ETag**”。
再来看看实验环境的URI “/20-2”。它为资源增加了ETag字段刷新页面时浏览器就会同时发送缓存控制头“max-age=0”和条件请求头“If-None-Match”如果缓存有效服务器就会返回304
<img src="https://static001.geekbang.org/resource/image/30/f9/30965c97bb7433eabe10008fefaeb5f9.png" alt="">
条件请求里其他的三个头字段是“If-Unmodified-Since”“If-Match”和“If-Range”其实只要你掌握了“if-Modified-Since”和“If-None-Match”可以轻易地“举一反三”。
## 小结
今天我们学习了HTTP的缓存控制和条件请求用好它们可以减少响应时间、节约网络流量一起小结一下今天的内容吧
1. 缓存是优化系统性能的重要手段HTTP传输的每一个环节中都可以有缓存
1. 服务器使用“Cache-Control”设置缓存策略常用的是“max-age”表示资源的有效期
1. 浏览器收到数据就会存入缓存,如果没过期就可以直接使用,过期就要去服务器验证是否仍然可用;
1. 验证资源是否失效需要使用“条件请求”常用的是“if-Modified-Since”和“If-None-Match”收到304就可以复用缓存里的资源
1. 验证资源是否被修改的条件有两个“Last-modified”和“ETag”需要服务器预先在响应报文里设置搭配条件请求使用
1. 浏览器也可以发送“Cache-Control”字段使用“max-age=0”或“no_cache”刷新数据。
HTTP缓存看上去很复杂但基本原理说白了就是一句话“没有消息就是好消息”“没有请求的请求才是最快的请求。”
## 课下作业
1. Cache 和Cookie都是服务器发给客户端并存储的数据你能比较一下两者的异同吗
1. 即使有“Last-modified”和“ETag”强制刷新Ctrl+F5也能够从服务器获取最新数据返回200而不是304请你在实验环境里试一下观察请求头和响应头解释原因。
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/13/4b/1348aa2c81bd5d65ace3aa068b21044b.png" alt="unpreview">

View File

@@ -0,0 +1,146 @@
<audio id="audio" title="21 | 良心中间商HTTP的代理服务" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e6/a2/e6f7d038c706b21af02e2bdb5e8774a2.mp3"></audio>
在前面讲HTTP协议的时候我们严格遵循了HTTP的“请求-应答”模型,协议中只有两个互相通信的角色,分别是“请求方”浏览器(客户端)和“应答方”服务器。
今天我们要在这个模型里引入一个新的角色那就是HTTP代理。
引入HTTP代理后原来简单的双方通信就变复杂了一些加入了一个或者多个中间人但整体上来看还是一个有顺序关系的链条而且链条里相邻的两个角色仍然是简单的一对一通信不会出现越级的情况。
<img src="https://static001.geekbang.org/resource/image/28/f9/28237ef93ce0ddca076d2dc19c16fdf9.png" alt="">
链条的起点还是客户端也就是浏览器中间的角色被称为代理服务器proxy server链条的终点被称为源服务器origin server意思是数据的“源头”“起源”。
## 代理服务
“代理”这个词听起来好像很神秘,有点“高大上”的感觉。
但其实HTTP协议里对它并没有什么特别的描述它就是在客户端和服务器原本的通信链路中插入的一个中间环节也是一台服务器但提供的是“代理服务”。
所谓的“代理服务”就是指服务本身不生产内容,而是处于中间位置转发上下游的请求和响应,具有双重身份:面向下游的用户时,表现为服务器,代表源服务器响应客户端的请求;而面向上游的源服务器时,又表现为客户端,代表客户端发送请求。
还是拿上一讲的“生鲜超市”来打个比方。
之前你都是从超市里买东西现在楼底下新开了一家24小时便利店由超市直接供货于是你就可以在便利店里买到原本必须去超市才能买到的商品。
这样超市就不直接和你打交道了,成了“源服务器”,便利店就成了超市的“代理服务器”。
在[第4讲](https://time.geekbang.org/column/article/98934)中,我曾经说过,代理有很多的种类,例如匿名代理、透明代理、正向代理和反向代理。
今天我主要讲的是实际工作中最常见的反向代理,它在传输链路中更靠近源服务器,为源服务器提供代理服务。
## 代理的作用
为什么要有代理呢?换句话说,代理能干什么、带来什么好处呢?
你也许听过这样一句至理名言“计算机科学领域里的任何问题都可以通过引入一个中间层来解决”在这句话后面还可以再加上一句“如果一个中间层解决不了问题那就再加一个中间层”。TCP/IP协议栈是这样而代理也是这样。
由于代理处在HTTP通信过程的中间位置相应地就对上屏蔽了真实客户端对下屏蔽了真实服务器简单的说就是“**欺上瞒下**”。在这个中间层的“小天地”里就可以做很多的事情为HTTP协议增加更多的灵活性实现客户端和服务器的“双赢”。
代理最基本的一个功能是**负载均衡**。因为在面向客户端时屏蔽了源服务器客户端看到的只是代理服务器源服务器究竟有多少台、是哪些IP地址都不知道。于是代理服务器就可以掌握请求分发的“大权”决定由后面的哪台服务器来响应请求。
<img src="https://static001.geekbang.org/resource/image/8c/7c/8c1fe47a7ca4b52702a6a14956033f7c.png" alt="">
代理中常用的负载均衡算法你应该也有所耳闻吧,比如轮询、一致性哈希等等,这些算法的目标都是尽量把外部的流量合理地分散到多台源服务器,提高系统的整体资源利用率和性能。
在负载均衡的同时,代理服务还可以执行更多的功能,比如:
- **健康检查**:使用“心跳”等机制监控后端服务器,发现有故障就及时“踢出”集群,保证服务高可用;
- **安全防护**保护被代理的后端服务器限制IP地址或流量抵御网络攻击和过载
- **加密卸载**对外网使用SSL/TLS加密通信认证而在安全的内网不加密消除加解密成本
- **数据过滤**:拦截上下行的数据,任意指定策略修改请求或者响应;
- **内容缓存**:暂存、复用服务器响应,这个与[第20讲](https://time.geekbang.org/column/article/106804)密切相关,我们稍后再说。
接着拿刚才的便利店来举例说明。
因为便利店和超市之间是专车配送,所以有了便利店,以后你买东西就更省事了,打电话给便利店让它去帮你取货,不用关心超市是否停业休息、是否人满为患,而且总能买到最新鲜的。
便利店同时也方便了超市,不用额外加大店面就可以增加客源和销量,货物集中装卸也节省了物流成本,由于便利店直接面对客户,所以也可以把恶意骚扰电话挡在外面。
## 代理相关头字段
代理的好处很多,但因为它“欺上瞒下”的特点,隐藏了真实客户端和服务器,如果双方想要获得这些“丢失”的原始信息,该怎么办呢?
首先,代理服务器需要用字段“**Via**”标明代理的身份。
Via是一个通用字段请求头或响应头里都可以出现。每当报文经过一个代理节点代理服务器就会把自身的信息追加到字段的末尾就像是经手人盖了一个章。
如果通信链路中有很多中间代理就会在Via里形成一个链表这样就可以知道报文究竟走过了多少个环节才到达了目的地。
例如下图中有两个代理proxy1和proxy2客户端发送请求会经过这两个代理依次添加就是“Via: proxy1, proxy2”等到服务器返回响应报文的时候就要反过来走头字段就是“Via: proxy2, proxy1”。
<img src="https://static001.geekbang.org/resource/image/52/d7/52a3bd760584972011f6be1a5258e2d7.png" alt="">
Via字段只解决了客户端和源服务器判断是否存在代理的问题还不能知道对方的真实信息。
但服务器的IP地址应该是保密的关系到企业的内网安全所以一般不会让客户端知道。不过反过来通常服务器需要知道客户端的真实IP地址方便做访问控制、用户画像、统计分析。
可惜的是HTTP标准里并没有为此定义头字段但已经出现了很多“事实上的标准”最常用的两个头字段是“**X-Forwarded-For**”和“**X-Real-IP**”。
“X-Forwarded-For”的字面意思是“为谁而转发”形式上和“Via”差不多也是每经过一个代理节点就会在字段里追加一个信息。但“Via”追加的是代理主机名或者域名而“X-Forwarded-For”追加的是请求方的IP地址。所以在字段里最左边的IP地址就是客户端的地址。
“X-Real-IP”是另一种获取客户端真实IP的手段它的作用很简单就是记录客户端IP地址没有中间的代理信息相当于是“X-Forwarded-For”的简化版。如果客户端和源服务器之间只有一个代理那么这两个字段的值就是相同的。
我们的实验环境实现了一个反向代理,访问“[http://www.chrono.com/21-1](http://www.chrono.com/21-1)”,它会转而访问“[http://origin.io](http://origin.io)”。这里的“origin.io”就是源站它会在响应报文里输出“Via”“X-Forwarded-For”等代理头字段信息
<img src="https://static001.geekbang.org/resource/image/c5/e7/c5aa6d5f82e8cc1a35772293972446e7.png" alt="">
单从浏览器的页面上很难看出代理做了哪些工作因为代理的转发都在后台不可见所以我把这个过程用Wireshark抓了一个包
<img src="https://static001.geekbang.org/resource/image/5a/54/5a247e9e5bf66f5ac3316fddf4e2b254.png" alt="">
从抓包里就可以清晰地看出代理与客户端、源服务器的通信过程:
1. 客户端55061先用三次握手连接到代理的80端口然后发送GET请求
1. 代理不直接生产内容所以就代表客户端用55063端口连接到源服务器也是三次握手
1. 代理成功连接源服务器后发出了一个HTTP/1.0 的GET请求
1. 因为HTTP/1.0默认是短连接,所以源服务器发送响应报文后立即用四次挥手关闭连接;
1. 代理拿到响应报文后再发回给客户端,完成了一次代理服务。
在这个实验中你可以看到除了“X-Forwarded-For”和“X-Real-IP”还出现了两个字段“X-Forwarded-Host”和“X-Forwarded-Proto”它们的作用与“X-Real-IP”类似只记录客户端的信息分别是客户端请求的原始域名和原始协议名。
## 代理协议
有了“X-Forwarded-For”等头字段源服务器就可以拿到准确的客户端信息了。但对于代理服务器来说它并不是一个最佳的解决方案。
因为通过“X-Forwarded-For”操作代理信息必须要解析HTTP报文头这对于代理来说成本比较高原本只需要简单地转发消息就好而现在却必须要费力解析数据再修改数据会降低代理的转发性能。
另一个问题是“X-Forwarded-For”等头必须要修改原始报文而有些情况下是不允许甚至不可能的比如使用HTTPS通信被加密
所以就出现了一个专门的“代理协议”The PROXY protocol它由知名的代理软件HAProxy所定义也是一个“事实标准”被广泛采用注意并不是RFC
“代理协议”有v1和v2两个版本v1和HTTP差不多也是明文而v2是二进制格式。今天只介绍比较好理解的v1它在HTTP报文前增加了一行ASCII码文本相当于又多了一个头。
这一行文本其实非常简单开头必须是“PROXY”五个大写字母然后是“TCP4”或者“TCP6”表示客户端的IP地址类型再后面是请求方地址、应答方地址、请求方端口号、应答方端口号最后用一个回车换行\r\n结束。
例如下面的这个例子在GET请求行前多出了PROXY信息行客户端的真实IP地址是“1.1.1.1”端口号是55555。
```
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
```
服务器看到这样的报文只要解析第一行就可以拿到客户端地址不需要再去理会后面的HTTP数据省了很多事情。
不过代理协议并不支持“X-Forwarded-For”的链式地址形式所以拿到客户端地址后再如何处理就需要代理服务器与后端自行约定。
## 小结
1. HTTP代理就是客户端和服务器通信链路中的一个中间环节为两端提供“代理服务”
1. 代理处于中间层为HTTP处理增加了更多的灵活性可以实现负载均衡、安全防护、数据过滤等功能
1. 代理服务器需要使用字段“Via”标记自己的身份多个代理会形成一个列表
1. 如果想要知道客户端的真实IP地址可以使用字段“X-Forwarded-For”和“X-Real-IP”
1. 专门的“代理协议”可以在不改动原始报文的情况下传递客户端的真实IP。
## 课下作业
1. 你觉得代理有什么缺点?实际应用时如何避免?
1. 你知道多少反向代理中使用的负载均衡算法?它们有什么优缺点?
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/a8/9f/a8122180bd01e05613d75d34962da79f.png" alt="unpreview">
<img src="https://static001.geekbang.org/resource/image/56/63/56d766fc04654a31536f554b8bde7b63.jpg" alt="unpreview">

View File

@@ -0,0 +1,135 @@
<audio id="audio" title="22 | 冷链周转HTTP的缓存代理" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/06/97/0623d9e97995f131e46202f146be6f97.mp3"></audio>
在[第20讲](https://time.geekbang.org/column/article/106804)中我介绍了HTTP的缓存控制[第21讲](https://time.geekbang.org/column/article/107577)我介绍了HTTP的代理服务。那么把这两者结合起来就是这节课所要说的“**缓存代理**”,也就是支持缓存控制的代理服务。
之前谈到缓存时,主要讲了客户端(浏览器)上的缓存控制,它能够减少响应时间、节约带宽,提升客户端的用户体验。
但HTTP传输链路上不只是客户端有缓存服务器上的缓存也是非常有价值的可以让请求不必走完整个后续处理流程“就近”获得响应结果。
特别是对于那些“读多写少”的数据例如突发热点新闻、爆款商品的详情页一秒钟内可能有成千上万次的请求。即使仅仅缓存数秒钟也能够把巨大的访问流量挡在外面让RPSrequest per second降低好几个数量级减轻应用服务器的并发压力对性能的改善是非常显著的。
HTTP的服务器缓存功能主要由代理服务器来实现即缓存代理而源服务器系统内部虽然也经常有各种缓存如Memcache、Redis、Varnish等但与HTTP没有太多关系所以这里暂且不说。
## 缓存代理服务
我还是沿用“生鲜速递+便利店”的比喻,看看缓存代理是怎么回事。
便利店作为超市的代理,生意非常红火,顾客和超市双方都对现状非常满意。但时间一长,超市发现还有进一步提升的空间,因为每次便利店接到顾客的请求后都要专车跑一趟超市,还是挺麻烦的。
干脆这样吧,给便利店配发一个大冰柜。水果海鲜什么的都可以放在冰柜里,只要产品在保鲜期内,就允许顾客直接从冰柜提货。这样便利店就可以一次进货多次出货,省去了超市之间的运输成本。
<img src="https://static001.geekbang.org/resource/image/5e/c2/5e8d10b5758685850aeed2a473a6cdc2.png" alt="">
通过这个比喻,你可以看到:在没有缓存的时候,代理服务器每次都是直接转发客户端和服务器的报文,中间不会存储任何数据,只有最简单的中转功能。
加入了缓存后就不一样了。
代理服务收到源服务器发来的响应数据后需要做两件事。第一个当然是把报文转发给客户端而第二个就是把报文存入自己的Cache里。
下一次再有相同的请求代理服务器就可以直接发送304或者缓存数据不必再从源服务器那里获取。这样就降低了客户端的等待时间同时节约了源服务器的网络带宽。
在HTTP的缓存体系中缓存代理的身份十分特殊它“既是客户端又是服务器”同时也“既不是客户端又不是服务器”。
说它“即是客户端又是服务器”是因为它面向源服务器时是客户端在面向客户端时又是服务器所以它即可以用客户端的缓存控制策略也可以用服务器端的缓存控制策略也就是说它可以同时使用第20讲的各种“Cache-Control”属性。
但缓存代理也“即不是客户端又不是服务器”因为它只是一个数据的“中转站”并不是真正的数据消费者和生产者所以还需要有一些新的“Cache-Control”属性来对它做特别的约束。
## 源服务器的缓存控制
[第20讲](https://time.geekbang.org/column/article/106804)介绍了4种服务器端的“Cache-Control”属性max-age、no-store、no-cache和must-revalidate你应该还有印象吧
这4种缓存属性可以约束客户端也可以约束代理。
但客户端和代理是不一样的,客户端的缓存只是用户自己使用,而代理的缓存可能会为非常多的客户端提供服务。所以,需要对它的缓存再多一些限制条件。
首先,我们要区分客户端上的缓存和代理上的缓存,可以使用两个新属性“**private**”和“**public**”。
“private”表示缓存只能在客户端保存是用户“私有”的不能放在代理上与别人共享。而“public”的意思就是缓存完全开放谁都可以存谁都可以用。
比如你登录论坛返回的响应报文里用“Set-Cookie”添加了论坛ID这就属于私人数据不能存在代理上。不然别人访问代理获取了被缓存的响应就麻烦了。
其次缓存失效后的重新验证也要区分开即使用条件请求“Last-modified”和“ETag”“**must-revalidate**”是只要过期就必须回源服务器验证,而新的“**proxy-revalidate**”只要求代理的缓存过期后必须验证,客户端不必回源,只验证到代理这个环节就行了。
再次,缓存的生存时间可以使用新的“**s-maxage**”s是share的意思注意maxage中间没有“-”只限定在代理上能够存多久而客户端仍然使用“max-age”。
还有一个代理专用的属性“**no-transform**”。代理有时候会对缓存下来的数据做一些优化比如把图片生成png、webp等几种格式方便今后的请求处理而“no-transform”就会禁止这样做不许“偷偷摸摸搞小动作”。
这些新的缓存控制属性比较复杂,还是用“便利店冷柜”来举例好理解一些。
水果上贴着标签“private, max-age=5”。这就是说水果不能放进冷柜必须直接给顾客保鲜期5天过期了还得去超市重新进货。
冻鱼上贴着标签“public, max-age=5, s-maxage=10”。这个的意思就是可以在冰柜里存10天但顾客那里只能存5天过期了可以来便利店取只要在10天之内就不必再找超市。
排骨上贴着标签“max-age=30, proxy-revalidate, no-transform”。因为缓存默认是public的那么它在便利店和顾客的冰箱里就都可以存30天过期后便利店必须去超市进新货而且不能擅自把“大排”改成“小排”。
下面的流程图是完整的服务器端缓存控制策略,可以同时控制客户端和代理。
<img src="https://static001.geekbang.org/resource/image/09/35/09266657fa61d0d1a720ae3360fe9535.png" alt="">
我还要提醒你一点源服务器在设置完“Cache-Control”后必须要为报文加上“Last-modified”或“ETag”字段。否则客户端和代理后面就无法使用条件请求来验证缓存是否有效也就不会有304缓存重定向。
## 客户端的缓存控制
说完了服务器端的缓存控制策略,稍微歇一口气,我们再来看看客户端。
客户端在HTTP缓存体系里要面对的是代理和源服务器也必须区别对待这里我就直接上图了来个“看图说话”。
<img src="https://static001.geekbang.org/resource/image/47/92/47c1a69c800439e478c7a4ed40b8b992.png" alt="">
max-age、no-store、no-cache这三个属性在[第20讲](https://time.geekbang.org/column/article/106804)已经介绍过了,它们也是同样作用于代理和源服务器。
关于缓存的生存时间,多了两个新属性“**max-stale**”和“**min-fresh**”。
“max-stale”的意思是如果代理上的缓存过期了也可以接受但不能过期太多超过x秒也会不要。“min-fresh”的意思是缓存必须有效而且必须在x秒后依然有效。
比如草莓上贴着标签“max-age=5”现在已经在冰柜里存了7天。如果有请求“max-stale=2”意思是过期两天也能接受所以刚好能卖出去。
但要是“min-fresh=1”这是绝对不允许过期的就不会买走。这时如果有另外一个菠萝是“max-age=10”那么“7+1&lt;10”在一天之后还是新鲜的所以就能卖出去。
有的时候客户端还会发出一个特别的“**only-if-cached**”属性表示只接受代理缓存的数据不接受源服务器的响应。如果代理上没有缓存或者缓存过期就应该给客户端返回一个504Gateway Timeout
## 实验环境
信息量有些大到这里你是不是有点头疼了好在我们还有实验环境用URI“/22-1”试一下吧。
它设置了“Cache-Control: public, max-age=10, s-maxage=30”数据可以在浏览器里存10秒在代理上存30秒你可以反复刷新看看代理和源服务器是怎么响应的同样也可以配合Wireshark抓包。
代理在响应报文里还额外加了“X-Cache”“X-Hit”等自定义头字段表示缓存是否命中和命中率方便你观察缓存代理的工作情况。
<img src="https://static001.geekbang.org/resource/image/4d/e8/4d210fa1adccb7299d632ed7e66391e8.png" alt="">
## 其他问题
缓存代理的知识就快讲完了,下面再简单说两个相关的问题。
第一个是“**Vary**”字段,在[第15讲](https://time.geekbang.org/column/article/104024)曾经说过,它是内容协商的结果,相当于报文的一个版本标记。
同一个请求经过内容协商后可能会有不同的字符集、编码、浏览器等版本。比如“Vary: Accept-Encoding”“Vary: User-Agent”缓存代理必须要存储这些不同的版本。
当再收到相同的请求时代理就读取缓存里的“Vary”对比请求头里相应的“ Accept-Encoding”“User-Agent”等字段如果和上一个请求的完全匹配比如都是“gzip”“Chrome”就表示版本一致可以返回缓存的数据。
另一个问题是“**Purge**”,也就是“缓存清理”,它对于代理也是非常重要的功能,例如:
- 过期的数据应该及时淘汰,避免占用空间;
- 源站的资源有更新,需要删除旧版本,主动换成最新版(即刷新);
- 有时候会缓存了一些本不该存储的信息,例如网络谣言或者危险链接,必须尽快把它们删除。
清理缓存的方法有很多比较常用的一种做法是使用自定义请求方法“PURGE”发给代理服务器要求删除URI对应的缓存数据。
## 小结
1. 计算机领域里最常用的性能优化手段是“时空转换”也就是“时间换空间”或者“空间换时间”HTTP缓存属于后者
1. 缓存代理是增加了缓存功能的代理服务,缓存源服务器的数据,分发给下游的客户端;
1. “Cache-Control”字段也可以控制缓存代理常用的有“private”“s-maxage”“no-transform”等同样必须配合“Last-modified”“ETag”等字段才能使用
1. 缓存代理有时候也会带来负面影响,缓存不良数据,需要及时刷新或删除。
## 课下作业
1. 加入了代理后HTTP的缓存复杂了很多试着用自己的语言把这些知识再整理一下画出有缓存代理时浏览器的工作流程图加深理解。
1. 缓存的时间策略很重要,太大太小都不好,你觉得应该如何设置呢?
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/54/b8/54fddf71fc45f1055eff0b59b67dffb8.png" alt="unpreview">

View File

@@ -0,0 +1,132 @@
<audio id="audio" title="30 | 时代之风HTTP/2特性概览" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/eb/d5/ebc13babfb4a88680afcf18dde166fd5.mp3"></audio>
在[第14讲](https://time.geekbang.org/column/article/103746)里我们看到HTTP有两个主要的缺点安全不足和性能不高。
刚结束的“安全篇”里的HTTPS通过引入SSL/TLS在安全上达到了“极致”但在性能提升方面却是乏善可陈只优化了握手加密的环节对于整体的数据传输没有提出更好的改进方案还只能依赖于“长连接”这种“落后”的技术参见[第17讲](https://time.geekbang.org/column/article/104949))。
所以在HTTPS逐渐成熟之后HTTP就向着性能方面开始“发力”走出了另一条进化的道路。
在[第1讲](https://time.geekbang.org/column/article/97837)的HTTP历史中你也看到了“秦失其鹿天下共逐之”Google率先发明了SPDY协议并应用于自家的浏览器Chrome打响了HTTP性能优化的“第一枪”。
随后互联网标准化组织IETF以SPDY为基础综合其他多方的意见终于推出了HTTP/1的继任者也就是今天的主角“HTTP/2”在性能方面有了一个大的飞跃。
## 为什么不是HTTP/2.0
你一定很想知道为什么HTTP/2不像之前的“1.0”“1.1”那样叫“2.0”呢?
这个也是很多初次接触HTTP/2的人问的最多的一个问题对此HTTP/2工作组特别给出了解释。
他们认为以前的“1.0”“1.1”造成了很多的混乱和误解让人在实际的使用中难以区分差异所以就决定HTTP协议不再使用小版本号minor version只使用大版本号major version从今往后HTTP协议不会出现HTTP/2.0、2.1只会有“HTTP/2”“HTTP/3”……
这样就可以明确无误地辨别出协议版本的“跃进程度”让协议在一段较长的时期内保持稳定每当发布新版本的HTTP协议都会有本质的不同绝不会有“零敲碎打”的小改良。
## 兼容HTTP/1
由于HTTPS已经在安全方面做的非常好了所以HTTP/2的唯一目标就是改进性能。
但它不仅背负着众多的期待同时还背负着HTTP/1庞大的历史包袱所以协议的修改必须小心谨慎兼容性是首要考虑的目标否则就会破坏互联网上无数现有的资产这方面TLS已经有了先例为了兼容TLS1.2不得不进行“伪装”)。
那么HTTP/2是怎么做的呢
因为必须要保持功能上的兼容所以HTTP/2把HTTP分解成了“语义”和“语法”两个部分“语义”层不做改动与HTTP/1完全一致即RFC7231。比如请求方法、URI、状态码、头字段等概念都保留不变这样就消除了再学习的成本基于HTTP的上层应用也不需要做任何修改可以无缝转换到HTTP/2。
特别要说的是与HTTPS不同HTTP/2没有在URI里引入新的协议名仍然用“http”表示明文协议用“https”表示加密协议。
这是一个非常了不起的决定,可以让浏览器或者服务器去自动升级或降级协议,免去了选择的麻烦,让用户在上网的时候都意识不到协议的切换,实现平滑过渡。
在“语义”保持稳定之后HTTP/2在“语法”层做了“天翻地覆”的改造完全变更了HTTP报文的传输格式。
## 头部压缩
首先HTTP/2对报文的头部做了一个“大手术”。
通过“进阶篇”的学习你应该知道HTTP/1里可以用头字段“Content-Encoding”指定Body的编码方式比如用gzip压缩来节约带宽但报文的另一个组成部分——Header却被无视了没有针对它的优化手段。
由于报文Header一般会携带“User Agent”“Cookie”“Accept”“Server”等许多固定的头字段多达几百字节甚至上千字节但Body却经常只有几十字节比如GET请求、204/301/304响应成了不折不扣的“大头儿子”。更要命的是成千上万的请求响应报文里有很多字段值都是重复的非常浪费“长尾效应”导致大量带宽消耗在了这些冗余度极高的数据上。
所以HTTP/2把“**头部压缩**”作为性能改进的一个重点,优化的方式你也肯定能想到,还是“压缩”。
不过HTTP/2并没有使用传统的压缩算法而是开发了专门的“**HPACK**”算法在客户端和服务器两端建立“字典”用索引号表示重复的字符串还釆用哈夫曼编码来压缩整数和字符串可以达到50%~90%的高压缩率。
## 二进制格式
你可能已经很习惯于HTTP/1里纯文本形式的报文了它的优点是“一目了然”用最简单的工具就可以开发调试非常方便。
但HTTP/2在这方面没有“妥协”决定改变延续了十多年的现状不再使用肉眼可见的ASCII码而是向下层的TCP/IP协议“靠拢”全面采用二进制格式。
这样虽然对人不友好,但却大大方便了计算机的解析。原来使用纯文本的时候容易出现多义性,比如大小写、空白字符、回车换行、多字少字等等,程序在处理时必须用复杂的状态机,效率低,还麻烦。
而二进制里只有“0”和“1”可以严格规定字段大小、顺序、标志位等格式“对就是对错就是错”解析起来没有歧义实现简单而且体积小、速度快做到“内部提效”。
以二进制格式为基础HTTP/2就开始了“大刀阔斧”的改革。
它把TCP协议的部分特性挪到了应用层把原来的“Header+Body”的消息“打散”为数个小片的**二进制“帧”**Frame用“HEADERS”帧存放头数据、“DATA”帧存放实体数据。
这种做法有点像是“Chunked”分块编码的方式参见[第16讲](https://time.geekbang.org/column/article/104456)也是“化整为零”的思路但HTTP/2数据分帧后“Header+Body”的报文结构就完全消失了协议看到的只是一个个的“碎片”。
<img src="https://static001.geekbang.org/resource/image/8f/96/8fe2cbd57410299a1a36d7eb105ea896.png" alt="">
## 虚拟的“流”
消息的“碎片”到达目的地后应该怎么组装起来呢?
HTTP/2为此定义了一个“**流**”Stream的概念**它是二进制帧的双向传输序列**同一个消息往返的帧会分配一个唯一的流ID。你可以把它想象成是一个虚拟的“数据流”在里面流动的是一串有先后顺序的数据帧这些数据帧按照次序组装起来就是HTTP/1里的请求报文和响应报文。
因为“流”是虚拟的实际上并不存在所以HTTP/2就可以在一个TCP连接上用“**流**”同时发送多个“碎片化”的消息,这就是常说的“**多路复用**”( Multiplexing——多个往返通信都复用一个连接来处理。
在“流”的层面上看,消息是一些有序的“帧”序列,而在“连接”的层面上看,消息却是乱序收发的“帧”。多个请求/响应之间没有了顺序关系,不需要排队等待,也就不会再出现“队头阻塞”问题,降低了延迟,大幅度提高了连接的利用率。
<img src="https://static001.geekbang.org/resource/image/d8/bc/d8fd32a4d044f2078b3a260e4478c5bc.png" alt="">
为了更好地利用连接加大吞吐量HTTP/2还添加了一些控制帧来管理虚拟的“流”实现了优先级和流量控制这些特性也和TCP协议非常相似。
HTTP/2还在一定程度上改变了传统的“请求-应答”工作模式服务器不再是完全被动地响应请求也可以新建“流”主动向客户端发送消息。比如在浏览器刚请求HTML的时候就提前把可能会用到的JS、CSS文件发给客户端减少等待的延迟这被称为“**服务器推送**”Server Push也叫Cache Push
## 强化安全
出于兼容的考虑HTTP/2延续了HTTP/1的“明文”特点可以像以前一样使用明文传输数据不强制使用加密通信不过格式还是二进制只是不需要解密。
但由于HTTPS已经是大势所趋而且主流的浏览器Chrome、Firefox等都公开宣布只支持加密的HTTP/2所以“事实上”的HTTP/2是加密的。也就是说互联网上通常所能见到的HTTP/2都是使用“https”协议名跑在TLS上面。
为了区分“加密”和“明文”这两个不同的版本HTTP/2协议定义了两个字符串标识符“h2”表示加密的HTTP/2“h2c”表示明文的HTTP/2多出的那个字母“c”的意思是“clear text”。
在HTTP/2标准制定的时候2015年已经发现了很多SSL/TLS的弱点而新的TLS1.3还未发布所以加密版本的HTTP/2在安全方面做了强化要求下层的通信协议必须是TLS1.2以上还要支持前向安全和SNI并且把几百个弱密码套件列入了“黑名单”比如DES、RC4、CBC、SHA-1都不能在HTTP/2里使用相当于底层用的是“TLS1.25”。
## 协议栈
下面的这张图对比了HTTP/1、HTTPS和HTTP/2的协议栈你可以清晰地看到HTTP/2是建立在“HPack”“Stream”“TLS1.2”基础之上的比HTTP/1、HTTPS复杂了一些。
<img src="https://static001.geekbang.org/resource/image/83/1a/83c9f0ecad361ba8ef8f3b73d6872f1a.png" alt="">
虽然HTTP/2的底层实现很复杂但它的“语义”还是简单的HTTP/1之前学习的知识不会过时仍然能够用得上。
我们的实验环境在新的域名“**www.metroid.net**”上启用了HTTP/2协议你可以把之前“进阶篇”“安全篇”的测试用例都走一遍再用Wireshark抓一下包实际看看HTTP/2的效果和对老协议的兼容性例如“[http://www.metroid.net/11-1](http://www.metroid.net/11-1)”)。
在今天这节课专用的URI“/30-1”里你还可以看到服务器输出了HTTP的版本号“2”和标识符“h2”表示这是加密的HTTP/2如果改用“[https://www.chrono.com/30-1](https://www.chrono.com/30-1)”访问就会是“1.1”和空。
<img src="https://static001.geekbang.org/resource/image/fd/d1/fdf1a6916c3ac22b6fb7628de3d7ddd1.png" alt="">
你可能还会注意到URI里的一个小变化端口使用的是“8443”而不是“443”。这是因为443端口已经被“www.chrono.com”的HTTPS协议占用Nginx不允许在同一个端口上根据域名选择性开启HTTP/2所以就不得不改用了“8443”。
## 小结
今天我简略介绍了HTTP/2的一些重要特性比较偏重理论下一次我会用Wireshark抓包具体讲解HTTP/2的头部压缩、二进制帧和流等特性。
1. HTTP协议取消了小版本号所以HTTP/2的正式名字不是2.0
1. HTTP/2在“语义”上兼容HTTP/1保留了请求方法、URI等传统概念
1. HTTP/2使用“HPACK”算法压缩头部信息消除冗余数据节约带宽
1. HTTP/2的消息不再是“Header+Body”的形式而是分散为多个二进制“帧”
1. HTTP/2使用虚拟的“流”传输消息解决了困扰多年的“队头阻塞”问题同时实现了“多路复用”提高连接的利用率
1. HTTP/2也增强了安全性要求至少是TLS1.2,而且禁用了很多不安全的密码套件。
## 课下作业
1. 你觉得明文形式的HTTP/2h2c有什么好处应该如何使用呢
1. 你觉得应该怎样理解HTTP/2里的“流”为什么它是“虚拟”的
1. 你能对比一下HTTP/2与HTTP/1、HTTPS的相同点和不同点吗
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/78/42/781da6191d342d71d3be2675cb610742.png" alt="unpreview">
<img src="https://static001.geekbang.org/resource/image/56/63/56d766fc04654a31536f554b8bde7b63.jpg" alt="unpreview">

View File

@@ -0,0 +1,177 @@
<audio id="audio" title="31 | 时代之风HTTP/2内核剖析" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/1a/ba/1a3b423c943a63c41e476a1693c89cba.mp3"></audio>
今天我们继续上一讲的话题深入HTTP/2协议的内部看看它的实现细节。
<img src="https://static001.geekbang.org/resource/image/89/17/8903a45c632b64c220299d5bc64ef717.png" alt="">
这次实验环境的URI是“/31-1”我用Wireshark把请求响应的过程抓包存了下来文件放在GitHub的“wireshark”目录。今天我们就对照着抓包来实地讲解HTTP/2的头部压缩、二进制帧等特性。
## 连接前言
由于HTTP/2“事实上”是基于TLS所以在正式收发数据之前会有TCP握手和TLS握手这两个步骤相信你一定已经很熟悉了所以这里就略过去不再细说。
TLS握手成功之后客户端必须要发送一个“**连接前言**”connection preface用来确认建立HTTP/2连接。
这个“连接前言”是标准的HTTP/1请求报文使用纯文本的ASCII码格式请求方法是特别注册的一个关键字“PRI”全文只有24个字节
```
PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
```
在Wireshark里HTTP/2的“连接前言”被称为“**Magic**”,意思就是“不可知的魔法”。
所以就不要问“为什么会是这样”了只要服务器收到这个“有魔力的字符串”就知道客户端在TLS上想要的是HTTP/2协议而不是其他别的协议后面就会都使用HTTP/2的数据格式。
## 头部压缩
确立了连接之后HTTP/2就开始准备请求报文。
因为语义上它与HTTP/1兼容所以报文还是由“Header+Body”构成的但在请求发送前必须要用“**HPACK**”算法来压缩头部数据。
“HPACK”算法是专门为压缩HTTP头部定制的算法与gzip、zlib等压缩算法不同它是一个“有状态”的算法需要客户端和服务器各自维护一份“索引表”也可以说是“字典”这有点类似brotli压缩和解压缩就是查表和更新表的操作。
为了方便管理和压缩HTTP/2废除了原有的起始行概念把起始行里面的请求方法、URI、状态码等统一转换成了头字段的形式并且给这些“不是头字段的头字段”起了个特别的名字——“**伪头字段**”pseudo-header fields。而起始行里的版本号和错误原因短语因为没什么大用顺便也给废除了。
为了与“真头字段”区分开来,这些“伪头字段”会在名字前加一个“:”,比如“:authority” “:method” “:status”分别表示的是域名、请求方法和状态码。
现在HTTP报文头就简单了全都是“Key-Value”形式的字段于是HTTP/2就为一些最常用的头字段定义了一个只读的“**静态表**”Static Table
下面的这个表格列出了“静态表”的一部分这样只要查表就可以知道字段名和对应的值比如数字“2”代表“GET”数字“8”代表状态码200。
<img src="https://static001.geekbang.org/resource/image/76/0c/769dcf953ddafc4573a0b4c3f0321f0c.png" alt="">
但如果表里只有Key没有Value或者是自定义字段根本找不到该怎么办呢
这就要用到“**动态表**”Dynamic Table它添加在静态表后面结构相同但会在编码解码的时候随时更新。
比如说第一次发送请求时的“user-agent”字段长是一百多个字节用哈夫曼压缩编码发送之后客户端和服务器都更新自己的动态表添加一个新的索引号“65”。那么下一次发送的时候就不用再重复发那么多字节了只要用一个字节发送编号就好。
<img src="https://static001.geekbang.org/resource/image/5f/6f/5fa90e123c68855140e2b40f4f73c56f.png" alt="">
你可以想象得出来随着在HTTP/2连接上发送的报文越来越多两边的“字典”也会越来越丰富最终每次的头部字段都会变成一两个字节的代码原来上千字节的头用几十个字节就可以表示了压缩效果比gzip要好得多。
## 二进制帧
头部数据压缩之后HTTP/2就要把报文拆成二进制的帧准备发送。
HTTP/2的帧结构有点类似TCP的段或者TLS里的记录但报头很小只有9字节非常地节省可以对比一下TCP头它最少是20个字节
二进制的格式也保证了不会有歧义,而且使用位运算能够非常简单高效地解析。
<img src="https://static001.geekbang.org/resource/image/61/e3/615b49f9d13de718a34b9b98359066e3.png" alt="">
帧开头是3个字节的**长度**但不包括头的9个字节默认上限是2^14最大是2^24也就是说HTTP/2的帧通常不超过16K最大是16M。
长度后面的一个字节是**帧类型**,大致可以分成**数据帧**和**控制帧**两类HEADERS帧和DATA帧属于数据帧存放的是HTTP报文而SETTINGS、PING、PRIORITY等则是用来管理流的控制帧。
HTTP/2总共定义了10种类型的帧但一个字节可以表示最多256种所以也允许在标准之外定义其他类型实现功能扩展。这就有点像TLS里扩展协议的意思了比如Google的gRPC就利用了这个特点定义了几种自用的新帧类型。
第5个字节是非常重要的**帧标志**信息可以保存8个标志位携带简单的控制信息。常用的标志位有**END_HEADERS**表示头数据结束相当于HTTP/1里头后的空行“\r\n”**END_STREAM**表示单方向数据发送结束即EOSEnd of Stream相当于HTTP/1里Chunked分块结束标志“0\r\n\r\n”
报文头里最后4个字节是**流标识符**也就是帧所属的“流”接收方使用它就可以从乱序的帧里识别出具有相同流ID的帧序列按顺序组装起来就实现了虚拟的“流”。
流标识符虽然有4个字节但最高位被保留不用所以只有31位可以使用也就是说流标识符的上限是2^31大约是21亿。
好了把二进制头理清楚后我们来看一下Wireshark抓包的帧实例
<img src="https://static001.geekbang.org/resource/image/57/03/57b0d1814567e6317c8de1e3c04b7503.png" alt="">
在这个帧里开头的三个字节是“00010a”表示数据长度是266字节。
帧类型是1表示HEADERS帧负载payload里面存放的是被HPACK算法压缩的头部信息。
标志位是0x25转换成二进制有3个位被置1。PRIORITY表示设置了流的优先级END_HEADERS表示这一个帧就是完整的头数据END_STREAM表示单方向数据发送结束后续再不会有数据帧即请求报文完毕不会再有DATA帧/Body数据
最后4个字节的流标识符是整数1表示这是客户端发起的第一个流后面的响应数据帧也会是这个ID也就是说在stream[1]里完成这个请求响应。
## 流与多路复用
弄清楚了帧结构后我们就来看HTTP/2的流与多路复用它是HTTP/2最核心的部分。
在上一讲里我简单介绍了流的概念,不知道你“悟”得怎么样了?这里我再重复一遍:**流是二进制帧的双向传输序列**。
要搞明白流关键是要理解帧头里的流ID。
在HTTP/2连接上虽然帧是乱序收发的但只要它们都拥有相同的流ID就都属于一个流而且在这个流里帧不是无序的而是有着严格的先后顺序。
比如在这次的Wireshark抓包里就有“0、1、3”一共三个流实际上就是分配了三个流ID号把这些帧按编号分组再排一下队就成了流。
<img src="https://static001.geekbang.org/resource/image/68/33/688630945be2dd51ca62515ae498db33.png" alt="">
在概念上一个HTTP/2的流就等同于一个HTTP/1里的“请求-应答”。在HTTP/1里一个“请求-响应”报文来回是一次HTTP通信在HTTP/2里一个流也承载了相同的功能。
你还可以对照着TCP来理解。TCP运行在IP之上其实从MAC层、IP层的角度来看TCP的“连接”概念也是“虚拟”的。但从功能上看无论是HTTP/2的流还是TCP的连接都是实际存在的所以你以后大可不必再纠结于流的“虚拟”性把它当做是一个真实存在的实体来理解就好。
HTTP/2的流有哪些特点呢我给你简单列了一下
1. 流是可并发的一个HTTP/2连接上可以同时发出多个流传输数据也就是并发多请求实现“多路复用”
1. 客户端和服务器都可以创建流,双方互不干扰;
1. 流是双向的,一个流里面客户端和服务器都可以发送或接收数据帧,也就是一个“请求-应答”来回;
1. 流之间没有固定关系,彼此独立,但流内部的帧是有严格顺序的;
1. 流可以设置优先级让服务器优先处理比如先传HTML/CSS后传图片优化用户体验
1. 流ID不能重用只能顺序递增客户端发起的ID是奇数服务器端发起的ID是偶数
1. 在流上发送“RST_STREAM”帧可以随时终止流取消接收或发送
1. 第0号流比较特殊不能关闭也不能发送数据帧只能发送控制帧用于流量控制。
这里我又画了一张图把上次的图略改了一下显示了连接中无序的帧是如何依据流ID重组成流的。
<img src="https://static001.geekbang.org/resource/image/b4/7e/b49595a5a425c0e67d46ee17cc212e7e.png" alt="">
从这些特性中,我们还可以推理出一些深层次的知识点。
比如说HTTP/2在一个连接上使用多个流收发数据那么它本身默认就会是长连接所以永远不需要“Connection”头字段keepalive或close
你可以再看一下Wireshark的抓包里面发送了两个请求“/31-1”和“/favicon.ico”始终用的是“56095&lt;-&gt;8443”这个连接对比一下[第8讲](https://time.geekbang.org/column/article/100502),你就能够看出差异了。
又比如下载大文件的时候想取消接收在HTTP/1里只能断开TCP连接重新“三次握手”成本很高而在HTTP/2里就可以简单地发送一个“RST_STREAM”中断流而长连接会继续保持。
再比如因为客户端和服务器两端都可以创建流而流ID有奇数偶数和上限的区分所以大多数的流ID都会是奇数而且客户端在一个连接里最多只能发出2^30也就是10亿个请求。
所以就要问了ID用完了该怎么办呢这个时候可以再发一个控制帧“GOAWAY”真正关闭TCP连接。
## 流状态转换
流很重要也很复杂。为了更好地描述运行机制HTTP/2借鉴了TCP根据帧的标志位实现流状态转换。当然这些状态也是虚拟的只是为了辅助理解。
HTTP/2的流也有一个状态转换图虽然比TCP要简单一点但也不那么好懂所以今天我只画了一个简化的图对应到一个标准的HTTP“请求-应答”。
<img src="https://static001.geekbang.org/resource/image/d3/b4/d389ac436d8100406a4a488a69563cb4.png" alt="">
最开始的时候流都是“**空闲**”idle状态也就是“不存在”可以理解成是待分配的“号段资源”。
当客户端发送HEADERS帧后有了流ID流就进入了“**打开**”状态两端都可以收发数据然后客户端发送一个带“END_STREAM”标志位的帧流就进入了“**半关闭**”状态。
这个“半关闭”状态很重要,意味着客户端的请求数据已经发送完了,需要接受响应数据,而服务器端也知道请求数据接收完毕,之后就要内部处理,再发送响应数据。
响应数据发完了之后也要带上“END_STREAM”标志位表示数据发送完毕这样流两端就都进入了“**关闭**”状态,流就结束了。
刚才也说过流ID不能重用所以流的生命周期就是HTTP/1里的一次完整的“请求-应答”,流关闭就是一次通信结束。
下一次再发请求就要开一个新流而不是新连接流ID不断增加直到到达上限发送“GOAWAY”帧开一个新的TCP连接流ID就又可以重头计数。
你再看看这张图是不是和HTTP/1里的标准“请求-应答”过程很像只不过这是发生在虚拟的“流”上而不是实际的TCP连接又因为流可以并发所以HTTP/2就可以实现无阻塞的多路复用。
## 小结
HTTP/2的内容实在是太多了为了方便学习我砍掉了一些特性比如流的优先级、依赖关系、流量控制等。
但只要你掌握了今天的这些内容以后再看RFC文档都不会有难度了。
1. HTTP/2必须先发送一个“连接前言”字符串然后才能建立正式连接
1. HTTP/2废除了起始行统一使用头字段在两端维护字段“Key-Value”的索引表使用“HPACK”算法压缩头部
1. HTTP/2把报文切分为多种类型的二进制帧报头里最重要的字段是流标识符标记帧属于哪个流
1. 流是HTTP/2虚拟的概念是帧的双向传输序列相当于HTTP/1里的一次“请求-应答”;
1. 在一个HTTP/2连接上可以并发多个流也就是多个“请求-响应”报文,这就是“多路复用”。
## 课下作业
1. HTTP/2的动态表维护、流状态转换很复杂你认为HTTP/2还是“无状态”的吗
1. HTTP/2的帧最大可以达到16M你觉得大帧好还是小帧好
1. 结合这两讲谈谈HTTP/2是如何解决“队头阻塞”问题的。
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/3d/49/3dfab162c427fb3a1fa16494456ae449.png" alt="unpreview">

View File

@@ -0,0 +1,140 @@
<audio id="audio" title="32 | 未来之路HTTP/3展望" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3e/4d/3e41a53d39f155573182179cc6c8634d.mp3"></audio>
在前面的两讲里我们一起学习了HTTP/2你也应该看到了HTTP/2做出的许多努力比如头部压缩、二进制分帧、虚拟的“流”与多路复用性能方面比HTTP/1有了很大的提升“基本上”解决了“队头阻塞”这个“老大难”问题。
## HTTP/2的“队头阻塞”
等等,你可能要发出疑问了:为什么说是“基本上”,而不是“完全”解决了呢?
这是因为HTTP/2虽然使用“帧”“流”“多路复用”没有了“队头阻塞”但这些手段都是在应用层里而在下层也就是TCP协议里还是会发生“队头阻塞”。
这是怎么回事呢?
让我们从协议栈的角度来仔细看一下。在HTTP/2把多个“请求-响应”分解成流交给TCP后TCP会再拆成更小的包依次发送其实在TCP里应该叫segment也就是“段”
在网络良好的情况下包可以很快送达目的地。但如果网络质量比较差像手机上网的时候就有可能会丢包。而TCP为了保证可靠传输有个特别的“丢包重传”机制丢失的包必须要等待重新传输确认其他的包即使已经收到了也只能放在缓冲区里上层的应用拿不出来只能“干着急”。
我举个简单的例子:
客户端用TCP发送了三个包但服务器所在的操作系统只收到了后两个包第一个包丢了。那么内核里的TCP协议栈就只能把已经收到的包暂存起来“停下”等着客户端重传那个丢失的包这样就又出现了“队头阻塞”。
由于这种“队头阻塞”是TCP协议固有的所以HTTP/2即使设计出再多的“花样”也无法解决。
Google在推SPDY的时候就已经意识到了这个问题于是就又发明了一个新的“QUIC”协议让HTTP跑在QUIC上而不是TCP上。
而这个“HTTP over QUIC”就是HTTP协议的下一个大版本**HTTP/3**。它在HTTP/2的基础上又实现了质的飞跃真正“完美”地解决了“队头阻塞”问题。
不过HTTP/3目前还处于草案阶段正式发布前可能会有变动所以今天我尽量不谈那些不稳定的细节。
这里先贴一下HTTP/3的协议栈图让你对它有个大概的了解。
<img src="https://static001.geekbang.org/resource/image/d2/03/d263202e431c84db0fd6c7e6b1980f03.png" alt="">
## QUIC协议
从这张图里你可以看到HTTP/3有一个关键的改变那就是它把下层的TCP“抽掉”了换成了UDP。因为UDP是无序的包之间没有依赖关系所以就从根本上解决了“队头阻塞”。
你一定知道UDP是一个简单、不可靠的传输协议只是对IP协议的一层很薄的包装和TCP相比它实际应用的较少。
不过正是因为它简单,不需要建连和断连,通信成本低,也就非常灵活、高效,“可塑性”很强。
所以QUIC就选定了UDP在它之上把TCP的那一套连接管理、拥塞窗口、流量控制等“搬”了过来“去其糟粕取其精华”打造出了一个全新的可靠传输协议可以认为是“**新时代的TCP**”。
<img src="https://static001.geekbang.org/resource/image/fd/7a/fd99221ede55272a998760cc6aaa037a.png" alt="unpreview">
QUIC最早是由Google发明的被称为gQUIC。而当前正在由IETF标准化的QUIC被称为iQUIC。两者的差异非常大甚至比当年的SPDY与HTTP/2的差异还要大。
gQUIC混合了UDP、TLS、HTTP是一个应用层的协议。而IETF则对gQUIC做了“清理”把应用部分分离出来形成了HTTP/3原来的UDP部分“下放”到了传输层所以iQUIC有时候也叫“QUIC-transport”。
接下来要说的QUIC都是指iQUIC要记住它与早期的gQUIC不同是一个传输层的协议和TCP是平级的。
## QUIC的特点
QUIC基于UDP而UDP是“无连接”的根本就不需要“握手”和“挥手”所以天生就要比TCP快。
就像TCP在IP的基础上实现了可靠传输一样QUIC也基于UDP实现了可靠传输保证数据一定能够抵达目的地。它还引入了类似HTTP/2的“流”和“多路复用”单个“流”是有序的可能会因为丢包而阻塞但其他“流”不会受到影响。
为了防止网络上的中间设备Middle Box识别协议的细节QUIC全面采用加密通信可以很好地抵御窜改和“协议僵化”ossification
而且因为TLS1.3已经在去年2018正式发布所以QUIC就直接应用了TLS1.3顺便也就获得了0-RTT、1-RTT连接的好处。
但QUIC并不是建立在TLS之上而是内部“包含”了TLS。它使用自己的帧“接管”了TLS里的“记录”握手消息、警报消息都不使用TLS记录直接封装成QUIC的帧发送省掉了一次开销。
## QUIC内部细节
由于QUIC在协议栈里比较偏底层所以我只简略介绍两个内部的关键知识点。
QUIC的基本数据传输单位是**包**packet和**帧**frame一个包由多个帧组成包面向的是“连接”帧面向的是“流”。
QUIC使用不透明的“**连接ID**”来标记通信的两个端点客户端和服务器可以自行选择一组ID来标记自己这样就解除了TCP里连接对“IP地址+端口”(即常说的四元组)的强绑定,支持“**连接迁移**”Connection Migration
<img src="https://static001.geekbang.org/resource/image/ae/3b/ae0c482ea0c3b8ebc71924b19feb9b3b.png" alt="">
比如你下班回家手机会自动由4G切换到WiFi。这时IP地址会发生变化TCP就必须重新建立连接。而QUIC连接里的两端连接ID不会变所以连接在“逻辑上”没有中断它就可以在新的IP地址上继续使用之前的连接消除重连的成本实现连接的无缝迁移。
QUIC的帧里有多种类型PING、ACK等帧用于管理连接而STREAM帧专门用来实现流。
QUIC里的流与HTTP/2的流非常相似也是帧的序列你可以对比着来理解。但HTTP/2里的流都是双向的而QUIC则分为双向流和单向流。
<img src="https://static001.geekbang.org/resource/image/9a/10/9ab3858bf918dffafa275c400d78d910.png" alt="">
QUIC帧普遍采用变长编码最少只要1个字节最多有8个字节。流ID的最大可用位数是62数量上比HTTP/2的2^31大大增加。
流ID还保留了最低两位用作标志第1位标记流的发起者0表示客户端1表示服务器第2位标记流的方向0表示双向流1表示单向流。
所以QUIC流ID的奇偶性质和HTTP/2刚好相反客户端的ID是偶数从0开始计数。
## HTTP/3协议
了解了QUIC之后再来看HTTP/3就容易多了。
因为QUIC本身就已经支持了加密、流和多路复用所以HTTP/3的工作减轻了很多把流控制都交给QUIC去做。调用的不再是TLS的安全接口也不是Socket API而是专门的QUIC函数。不过这个“QUIC函数”还没有形成标准必须要绑定到某一个具体的实现库。
HTTP/3里仍然使用流来发送“请求-响应”但它自身不需要像HTTP/2那样再去定义流而是直接使用QUIC的流相当于做了一个“概念映射”。
HTTP/3里的“双向流”可以完全对应到HTTP/2的流而“单向流”在HTTP/3里用来实现控制和推送近似地对应HTTP/2的0号流。
由于流管理被“下放”到了QUIC所以HTTP/3里帧的结构也变简单了。
帧头只有两个字段:类型和长度,而且同样都采用变长编码,最小只需要两个字节。
<img src="https://static001.geekbang.org/resource/image/26/5b/2606cbaa1a2e606a3640cc1825f5605b.png" alt="">
HTTP/3里的帧仍然分成数据帧和控制帧两类HEADERS帧和DATA帧传输数据但其他一些帧因为在下层的QUIC里有了替代所以在HTTP/3里就都消失了比如RST_STREAM、WINDOW_UPDATE、PING等。
头部压缩算法在HTTP/3里升级成了“**QPACK**”使用方式上也做了改变。虽然也分成静态表和动态表但在流上发送HEADERS帧时不能更新字段只能引用索引表的更新需要在专门的单向流上发送指令来管理解决了HPACK的“队头阻塞”问题。
另外QPACK的字典也做了优化静态表由之前的61个增加到了98个而且序号从0开始也就是说“:authority”的编号是0。
## HTTP/3服务发现
讲了这么多不知道你注意到了没有HTTP/3没有指定默认的端口号也就是说不一定非要在UDP的80或者443上提供HTTP/3服务。
那么该怎么“发现”HTTP/3呢
这就要用到HTTP/2里的“扩展帧”了。浏览器需要先用HTTP/2协议连接服务器然后服务器可以在启动HTTP/2连接后发送一个“**Alt-Svc**”帧包含一个“h3=host:port”的字符串告诉浏览器在另一个端点上提供等价的HTTP/3服务。
浏览器收到“Alt-Svc”帧会使用QUIC异步连接指定的端口如果连接成功就会断开HTTP/2连接改用新的HTTP/3收发数据。
## 小结
HTTP/3综合了我们之前讲的所有技术HTTP/1、SSL/TLS、HTTP/2包含知识点很多比如队头阻塞、0-RTT握手、虚拟的“流”、多路复用算得上是“集大成之作”需要多下些功夫好好体会。
1. HTTP/3基于QUIC协议完全解决了“队头阻塞”问题弱网环境下的表现会优于HTTP/2
1. QUIC是一个新的传输层协议建立在UDP之上实现了可靠传输
1. QUIC内含了TLS1.3只能加密通信支持0-RTT快速建连
1. QUIC的连接使用“不透明”的连接ID不绑定在“IP地址+端口”上,支持“连接迁移”;
1. QUIC的流与HTTP/2的流很相似但分为双向流和单向流
1. HTTP/3没有指定默认端口号需要用HTTP/2的扩展帧“Alt-Svc”来发现。
## 课下作业
1. IP协议要比UDP协议省去8个字节的成本也更通用QUIC为什么不构建在IP协议之上呢
1. 说一说你理解的QUIC、HTTP/3的好处。
1. 对比一下HTTP/3和HTTP/2各自的流、帧有什么相同点和不同点。
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/58/df/5857f14a3b06b6c0dd38e00b4a6124df.png" alt="unpreview">

View File

@@ -0,0 +1,151 @@
<audio id="audio" title="33 | 我应该迁移到HTTP/2吗" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/41/28/413e8241fbe9202ebe252b2b3dd0ad28.mp3"></audio>
这一讲是“飞翔篇”的最后一讲而HTTP的所有知识也差不多快学完了。
前面你已经看到了新的HTTP/2和HTTP/3协议了解了它们的特点和工作原理如果再联系上前几天“安全篇”的HTTPS你可能又会发出疑问
“刚费了好大的力气升级到HTTPS这又出了一个HTTP/2还有再次升级的必要吗
与各大浏览器“强推”HTTPS的待遇不一样HTTP/2的公布可谓是“波澜不惊”。虽然它是HTTP协议的一个重大升级但Apple、Google等科技巨头并没有像HTTPS那样给予大量资源的支持。
直到今天HTTP/2在互联网上还是处于“不温不火”的状态虽然已经有了不少的网站改造升级到了HTTP/2但普及的速度远不及HTTPS。
所以你有这样的疑问也是很自然的升级到HTTP/2究竟能给我们带来多少好处呢到底“值不值”呢
## HTTP/2的优点
前面的几讲主要关注了HTTP/2的内部实现今天我们就来看看它有哪些优点和缺点。
首先要说的是HTTP/2最大的一个优点是**完全保持了与HTTP/1的兼容**在语义上没有任何变化之前在HTTP上的所有投入都不会浪费。
因为兼容HTTP/1所以HTTP/2也具有HTTP/1的所有优点并且“基本”解决了HTTP/1的所有缺点安全与性能兼顾可以认为是“更安全的HTTP、更快的HTTPS”。
在安全上HTTP/2对HTTPS在各方面都做了强化。下层的TLS至少是1.2而且只能使用前向安全的密码套件即ECDHE这同时也就默认实现了“TLS False Start”支持1-RTT握手所以不需要再加额外的配置就可以自动实现HTTPS加速。
安全有了保障再来看HTTP/2在性能方面的改进。
你应该知道,影响网络速度的两个关键因素是“**带宽**”和“**延迟**”HTTP/2的头部压缩、多路复用、流优先级、服务器推送等手段其实都是针对这两个要点。
所谓的“带宽”就是网络的传输速度。从最早的56K/s到如今的100M/s虽然网速已经是“今非昔比”比从前快了几十倍、几百倍但仍然是“稀缺资源”图片、视频这样的多媒体数据很容易会把带宽用尽。
节约带宽的基本手段就是压缩在HTTP/1里只能压缩body而HTTP/2则可以用HPACK算法压缩header这对高流量的网站非常有价值有数据表明能节省大概5%~10%的流量,这是实实在在的“真金白银”。
与HTTP/1“并发多个连接”不同HTTP/2的“多路复用”特性要求对**一个域名或者IP只用一个TCP连接**所有的数据都在这一个连接上传输这样不仅节约了客户端、服务器和网络的资源还可以把带宽跑满让TCP充分“吃饱”。
这是为什么呢?
我们来看一下在HTTP/1里的长连接虽然是双向通信但任意一个时间点实际上还是单向的上行请求时下行空闲下行响应时上行空闲再加上“队头阻塞”实际的带宽打了个“对折”还不止可参考[第17讲](https://time.geekbang.org/column/article/104949))。
而在HTTP/2里“多路复用”则让TCP开足了马力“全速狂奔”多个请求响应并发每时每刻上下行方向上都有流在传输数据没有空闲的时候带宽的利用率能够接近100%。所以HTTP/2只使用一个连接就能抵得过HTTP/1里的五六个连接。
不过流也可能会有依赖关系可能会存在等待导致的阻塞这就是“延迟”所以HTTP/2的其他特性就派上了用场。
“优先级”可以让客户端告诉服务器哪个文件更重要更需要优先传输服务器就可以调高流的优先级合理地分配有限的带宽资源让高优先级的HTML、图片更快地到达客户端尽早加载显示。
“服务器推送”也是降低延迟的有效手段它不需要客户端预先请求服务器直接就发给客户端这就省去了客户端解析HTML再请求的时间。
## HTTP/2的缺点
说了一大堆HTTP/2的优点再来看看它有什么缺点吧。
听过上一讲HTTP/3的介绍你就知道HTTP/2在TCP级别还是存在“队头阻塞”的问题。所以如果网络连接质量差发生丢包那么TCP会等待重传传输速度就会降低。
另外在移动网络中发生IP地址切换的时候下层的TCP必须重新建连要再次“握手”经历“慢启动”而且之前连接里积累的HPACK字典也都消失了必须重头开始计算导致带宽浪费和时延。
刚才也说了HTTP/2对一个域名只开一个连接所以一旦这个连接出问题那么整个网站的体验也就变差了。
而这些情况下HTTP/1反而不会受到影响因为它“本来就慢”而且还会对一个域名开6~8个连接顶多其中的一两个连接会“更慢”其他的连接不会受到影响。
## 应该迁移到HTTP/2吗
说到这里你对迁移到HTTP/2是否已经有了自己的判断呢
在我看来HTTP/2处于一个略“尴尬”的位置前面有“老前辈”HTTP/1后面有“新来者”HTTP/3即有“老前辈”的“打压”又有“新来者”的“追赶”也就难怪没有获得市场的大力“吹捧”了。
但这绝不是说HTTP/2“一无是处”实际上HTTP/2的性能改进效果是非常明显的Top 1000的网站中已经有超过40%运行在了HTTP/2上包括知名的Apple、Facebook、Google、Twitter等等。仅用了四年的时间HTTP/2就拥有了这么大的市场份额和巨头的认可足以证明它的价值。
因为HTTP/2的侧重点是“性能”所以“是否迁移”就需要在这方面进行评估。如果网站的流量很大那么HTTP/2就可以带来可观的收益反之如果网站流量比较小那么升级到HTTP/2就没有太多必要了只要利用现有的HTTP再优化就足矣。
不过如果你是新建网站我觉得完全可以跳过HTTP/1、HTTPS直接“一步到位”上HTTP/2这样不仅可以获得性能提升还免去了老旧的“历史包袱”日后也不会再有迁移的烦恼。
顺便再多嘴一句HTTP/2毕竟是“下一代”HTTP协议它的很多特性也延续到了HTTP/3提早升级到HTTP/2还可以让你在HTTP/3到来时有更多的技术积累和储备不至于落后于时代。
## 配置HTTP/2
假设你已经决定要使用HTTP/2应该如何搭建服务呢
因为HTTP/2“事实上”是加密的所以如果你已经在“安全篇”里成功迁移到了HTTPS那么在Nginx里启用HTTP/2简直可以说是“不费吹灰之力”只需要在server配置里再多加一个参数就可以搞定了。
```
server {
listen 443 ssl http2;
server_name www.xxx.net;
ssl_certificate xxx.crt;
ssl_certificate_key xxx.key;
```
注意“listen”指令在“ssl”后面多了一个“http2”这就表示在443端口上开启了SSL加密然后再启用HTTP/2。
配置服务器推送特性可以使用指令“http2_push”和“http2_push_preload”
```
http2_push /style/xxx.css;
http2_push_preload on;
```
不过如何合理地配置推送是个难题,如果推送给浏览器不需要的资源,反而浪费了带宽。
这方面暂时没有一般性的原则指导,你必须根据自己网站的实际情况去“猜测”客户端最需要的数据。
优化方面HTTPS的一些策略依然适用比如精简密码套件、ECC证书、会话复用、HSTS减少重定向跳转等等。
但还有一些优化手段在HTTP/2里是不适用的而且还会有反效果比如说常见的精灵图Spriting、资源内联inlining、域名分片Sharding至于原因是什么我把它留给你自己去思考提示与缓存有关
还要注意一点HTTP/2默认启用header压缩HPACK但并没有默认启用body压缩所以不要忘了在Nginx配置文件里加上“gzip”指令压缩HTML、JS等文本数据。
## 应用层协议协商ALPN
最后说一下HTTP/2的“服务发现”吧。
你有没有想过在URI里用的都是HTTPS协议名没有版本标记浏览器怎么知道服务器支持HTTP/2呢为什么上来就能用HTTP/2而不是用HTTP/1通信呢
答案在TLS的扩展里有一个叫“**ALPN**”Application Layer Protocol Negotiation的东西用来与服务器就TLS上跑的应用协议进行“协商”。
客户端在发起“Client Hello”握手的时候后面会带上一个“ALPN”扩展里面按照优先顺序列出客户端支持的应用协议。
就像下图这样最优先的是“h2”其次是“http/1.1”以前还有“spdy”以后还可能会有“h3”。
<img src="https://static001.geekbang.org/resource/image/d8/b0/d8f8606948bbd63c31466e464c1956b0.png" alt="">
服务器看到ALPN扩展以后就可以从列表里选择一种应用协议在“Server Hello”里也带上“ALPN”扩展告诉客户端服务器决定使用的是哪一种。因为我们在Nginx配置里使用了HTTP/2协议所以在这里它选择的就是“h2”。
<img src="https://static001.geekbang.org/resource/image/19/a7/19be1138574589458c96040e1a23b3a7.png" alt="">
这样在TLS握手结束后客户端和服务器就通过“ALPN”完成了应用层的协议协商后面就可以使用HTTP/2通信了。
## 小结
今天我们讨论了是否应该迁移到HTTP/2还有应该如何迁移到HTTP/2。
1. HTTP/2完全兼容HTTP/1是“更安全的HTTP、更快的HTTPS”头部压缩、多路复用等技术可以充分利用带宽降低延迟从而大幅度提高上网体验
1. TCP协议存在“队头阻塞”所以HTTP/2在弱网或者移动网络下的性能表现会不如HTTP/1
1. 迁移到HTTP/2肯定会有性能提升但高流量网站效果会更显著
1. 如果已经升级到了HTTPS那么再升级到HTTP/2会很简单
1. TLS协议提供“ALPN”扩展让客户端和服务器协商使用的应用层协议“发现”HTTP/2服务。
## 课下作业
1. 和“安全篇”的第29讲类似结合自己的实际情况分析一下是否应该迁移到HTTP/2有没有难点
1. 精灵图Spriting、资源内联inlining、域名分片Sharding这些手段为什么会对HTTP/2的性能优化造成反效果呢
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
<img src="https://static001.geekbang.org/resource/image/fb/55/fb986a7575ec902c86c17a937dbca655.png" alt="unpreview">