mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-17 06:33:48 +08:00
del
This commit is contained in:
130
极客时间专栏/geek/透视HTTP协议/基础篇/08 | 键入网址再按下回车,后面究竟发生了什么?.md
Normal file
130
极客时间专栏/geek/透视HTTP协议/基础篇/08 | 键入网址再按下回车,后面究竟发生了什么?.md
Normal 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请求就不需要再发到Apple,CDN就可以直接响应你的请求,把数据发给你。
|
||||
|
||||
由PHP、Java等后台服务动态生成的页面属于“动态资源”,CDN无法缓存,只能从目标网站获取。于是你发出的HTTP请求就要开始在互联网上的“漫长跋涉”,经过无数的路由器、网关、代理,最后到达目的地。
|
||||
|
||||
目标网站的服务器对外表现的是一个IP地址,但为了能够扛住高并发,在内部也是一套复杂的架构。通常在入口是负载均衡设备,例如四层的LVS或者七层的Nginx,在后面是许多的服务器,构成一个更强更稳定的集群。
|
||||
|
||||
负载均衡设备会先访问系统里的缓存服务器,通常有memory级缓存Redis和disk级缓存Varnish,它们的作用与CDN类似,不过是工作在内部网络里,把最频繁访问的数据缓存几秒钟或几分钟,减轻后端应用服务器的压力。
|
||||
|
||||
如果缓存服务器里也没有,那么负载均衡设备就要把请求转发给应用服务器了。这里就是各种开发框架大显神通的地方了,例如Java的Tomcat/Netty/Jetty,Python的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">
|
||||
|
||||
|
||||
217
极客时间专栏/geek/透视HTTP协议/基础篇/09 | HTTP报文是什么样子的?.md
Normal file
217
极客时间专栏/geek/透视HTTP协议/基础篇/09 | HTTP报文是什么样子的?.md
Normal 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">
|
||||
|
||||
|
||||
161
极客时间专栏/geek/透视HTTP协议/基础篇/10 | 应该如何理解请求方法?.md
Normal file
161
极客时间专栏/geek/透视HTTP协议/基础篇/10 | 应该如何理解请求方法?.md
Normal 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">
|
||||
|
||||
|
||||
194
极客时间专栏/geek/透视HTTP协议/基础篇/11 | 你能写出正确的网址吗?.md
Normal file
194
极客时间专栏/geek/透视HTTP协议/基础篇/11 | 你能写出正确的网址吗?.md
Normal 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的默认端口号是80,HTTPS的默认端口号是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值用字符“**&**”连接,浏览器和服务器都可以按照这个格式把长串的查询参数解析成可理解的字典或关联数组形式。
|
||||
|
||||
你可以在实验环境里用Chrome试试下面这个加了query参数的URI:
|
||||
|
||||
```
|
||||
http://www.chrono.com:8080/11-1?uid=1234&name=mario&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&enc=utf-8&qrst=1&rt=1&stop=1&vt=2&wq=openresty&psort=3&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里出现“@&?"等起界定符作用的字符,会导致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里对“@&/”等特殊字符和汉字必须要做编码,否则服务器收到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="">
|
||||
|
||||
|
||||
141
极客时间专栏/geek/透视HTTP协议/基础篇/12 | 响应状态码该怎么用?.md
Normal file
141
极客时间专栏/geek/透视HTTP协议/基础篇/12 | 响应状态码该怎么用?.md
Normal 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">
|
||||
104
极客时间专栏/geek/透视HTTP协议/基础篇/13 | HTTP有哪些特点?.md
Normal file
104
极客时间专栏/geek/透视HTTP协议/基础篇/13 | HTTP有哪些特点?.md
Normal 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/S(Client/Server)系统架构,请求方作为客户端、应答方作为服务器。所以,随着互联网的发展就出现了B/S(Browser/Server)架构,用轻量级的浏览器代替笨重的客户端应用,实现零维护的“瘦”客户端,而服务器则摈弃私有通信协议转而使用HTTP协议。
|
||||
|
||||
此外,请求-应答模式也完全符合RPC(Remote Procedure Call)的工作模式,可以把HTTP请求处理封装成远程函数调用,导致了WebService、RESTful和gRPC等的出现。
|
||||
|
||||
## 无状态
|
||||
|
||||
第五个特点,HTTP协议是无状态的。
|
||||
|
||||
这个所谓的“状态”应该怎么理解呢?
|
||||
|
||||
“状态”其实就是客户端或者服务器里保存的一些数据或者标志,记录了通信过程中的一些变化信息。
|
||||
|
||||
你一定知道,TCP协议是有状态的,一开始处于CLOSED状态,连接成功后是ESTABLISHED状态,断开连接后是FIN-WAIT状态,最后又是CLOSED状态。
|
||||
|
||||
这些“状态”就需要TCP在内部用一些数据结构去维护,可以简单地想象成是个标志量,标记当前所处的状态,例如0是CLOSED,2是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">
|
||||
124
极客时间专栏/geek/透视HTTP协议/基础篇/14 | HTTP有哪些优点?又有哪些缺点?.md
Normal file
124
极客时间专栏/geek/透视HTTP协议/基础篇/14 | HTTP有哪些优点?又有哪些缺点?.md
Normal 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、状态码、原因短语、头字段等每一个核心组成要素都没有被“写死”,允许开发者任意定制、扩充或解释,给予了浏览器和服务器最大程度的信任和自由,也正好符合了互联网“自由与平等”的精神——缺什么功能自己加个字段或者错误码什么的补上就是了。
|
||||
|
||||
“请勿跟踪”所使用的头字段 DNT(Do 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">
|
||||
|
||||
|
||||
138
极客时间专栏/geek/透视HTTP协议/安全篇/23 | HTTPS是什么?SSL|TLS又是什么?.md
Normal file
138
极客时间专栏/geek/透视HTTP协议/安全篇/23 | HTTPS是什么?SSL|TLS又是什么?.md
Normal 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/TLS,HTTPS自然就“手到擒来”。
|
||||
|
||||
## 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">
|
||||
|
||||
|
||||
146
极客时间专栏/geek/透视HTTP协议/安全篇/24 | 固若金汤的根本(上):对称加密与非对称加密.md
Normal file
146
极客时间专栏/geek/透视HTTP协议/安全篇/24 | 固若金汤的根本(上):对称加密与非对称加密.md
Normal 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等几种分组模式,但都陆续被发现有安全漏洞,所以现在基本都不怎么用了。最新的分组模式被称为AEAD(Authenticated Encryption with Associated Data),在加密的同时增加了认证的功能,常用的是GCM、CCM和Poly1305。
|
||||
|
||||
把上面这些组合起来,就可以得到TLS密码套件中定义的对称加密算法。
|
||||
|
||||
比如,AES128-GCM,意思是密钥长度为128位的AES算法,使用的分组模式是GCM;ChaCha20-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位。
|
||||
|
||||
ECC(Elliptic Curve Cryptography)是非对称加密里的“后起之秀”,它基于“**椭圆曲线离散对数**”的数学难题,使用特定的曲线方程和基点生成公钥和私钥,子算法ECDHE用于密钥交换,ECDSA用于数字签名。
|
||||
|
||||
目前比较常用的两个曲线是P-256(secp256r1,在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+7,y^2=x^3-x
|
||||
|
||||
比起RSA,ECC在安全强度和性能上都有明显的优势。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">
|
||||
|
||||
|
||||
152
极客时间专栏/geek/透视HTTP协议/安全篇/25 | 固若金汤的根本(下):数字签名与证书.md
Normal file
152
极客时间专栏/geek/透视HTTP协议/安全篇/25 | 固若金汤的根本(下):数字签名与证书.md
Normal 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用来生成伪随机数(PRF,pseudo random function)。
|
||||
|
||||
你一定在日常工作中听过、或者用过MD5(Message-Digest 5)、SHA-1(Secure 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、Let’s 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="">
|
||||
|
||||
## 证书体系的弱点
|
||||
|
||||
证书体系(PKI,Public 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=>一级CA=>二级CA),你能详细解释一下证书信任链的验证过程吗?
|
||||
|
||||
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/37/57/37c59439c36e75f610fe84c22009cc57.png" alt="">
|
||||
|
||||
|
||||
192
极客时间专栏/geek/透视HTTP协议/安全篇/26 | 信任始于握手:TLS1.2连接过程解析.md
Normal file
192
极客时间专栏/geek/透视HTTP协议/安全篇/26 | 信任始于握手:TLS1.2连接过程解析.md
Normal 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, "master secret",
|
||||
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">
|
||||
|
||||
|
||||
183
极客时间专栏/geek/透视HTTP协议/安全篇/27 | 更好更快的握手:TLS1.3特性解析.md
Normal file
183
极客时间专栏/geek/透视HTTP协议/安全篇/27 | 更好更快的握手:TLS1.3特性解析.md
Normal 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”)只认老的记录协议格式,更新改造很困难,甚至是不可行(设备僵化)。
|
||||
|
||||
在早期的试验中发现,一旦变更了记录头字段里的版本号,也就是由0x303(TLS1.2)改为0x304(TLS1.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升级为HKDF(HMAC-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">
|
||||
|
||||
|
||||
166
极客时间专栏/geek/透视HTTP协议/安全篇/28 | 连接太慢该怎么办:HTTPS的优化.md
Normal file
166
极客时间专栏/geek/透视HTTP协议/安全篇/28 | 连接太慢该怎么办:HTTPS的优化.md
Normal 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。
|
||||
|
||||
CRL(Certificate 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”,服务器解密后验证有效期,就可以恢复会话,开始加密通信。
|
||||
|
||||
这个过程也可以在实验环境里测试,端口号是442,URI是“[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">
|
||||
|
||||
|
||||
187
极客时间专栏/geek/透视HTTP协议/安全篇/29 | 我应该迁移到HTTPS吗?.md
Normal file
187
极客时间专栏/geek/透视HTTP协议/安全篇/29 | 我应该迁移到HTTPS吗?.md
Normal 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,其中最著名的就是“**Let’s Encrypt**”。
|
||||
|
||||
所谓的“难”,是指HTTPS涉及的知识点太多、太复杂,有一定的技术门槛,不能很快上手。
|
||||
|
||||
这第三个顾虑比较现实,HTTPS背后关联到了密码学、TLS、PKI等许多领域,不是短短几周、几个月就能够精通的。但实施HTTPS也并不需要把这些完全掌握,只要抓住少数几个要点就好,下面我就来帮你逐个解决一些关键的“难点”。
|
||||
|
||||
## 申请证书
|
||||
|
||||
要把网站从HTTP切换到HTTPS,首先要做的就是为网站申请一张证书。
|
||||
|
||||
大型网站出于信誉、公司形象的考虑,通常会选择向传统的CA申请证书,例如DigiCert、GlobalSign,而中小型网站完全可以选择使用“Let’s Encrypt”这样的免费证书,效果也完全不输于那些收费的证书。
|
||||
|
||||
“**Let’s Encrypt**”一直在推动证书的自动化部署,为此还实现了专门的ACME协议(RFC8555)。有很多的客户端软件可以完成申请、验证、下载、更新的“一条龙”操作,比如Certbot、acme.sh等等,都可以在“Let’s Encrypt”网站上找到,用法很简单,相关的文档也很详细,几分钟就能完成申请,所以我在这里就不细说了。
|
||||
|
||||
不过我必须提醒你几个注意事项。
|
||||
|
||||
第一,申请证书时应当同时申请RSA和ECDSA两种证书,在Nginx里配置成双证书验证,这样服务器可以自动选择快速的椭圆曲线证书,同时也兼容只支持RSA的客户端。
|
||||
|
||||
第二,如果申请RSA证书,私钥至少要2048位,摘要算法应该选用SHA-2,例如SHA256、SHA384等。
|
||||
|
||||
第三,出于安全的考虑,“Let’s 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”还是需要有很多的细枝末节的工作要做,比如使用CSP(Content Security Policy)的各种指令和标签来配置安全策略,使用反向代理来集中“卸载”SSL。
|
||||
|
||||
简单小结一下今天的内容:
|
||||
|
||||
1. 从HTTP迁移到HTTPS是“大势所趋”,能做就应该尽早做;
|
||||
1. 升级HTTPS首先要申请数字证书,可以选择免费好用的“Let’s 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">
|
||||
86
极客时间专栏/geek/透视HTTP协议/开篇词/开篇词|To Be a HTTP Hero.md
Normal file
86
极客时间专栏/geek/透视HTTP协议/开篇词/开篇词|To Be a HTTP Hero.md
Normal 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!”
|
||||
|
||||
|
||||
128
极客时间专栏/geek/透视HTTP协议/总结篇/39 | HTTP性能优化面面观(上).md
Normal file
128
极客时间专栏/geek/透视HTTP协议/总结篇/39 | HTTP性能优化面面观(上).md
Normal 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的性能优化。
|
||||
|
||||
由于HTTPS(SSL/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上,最常用的性能测试工具可能就是ab(Apache 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);
|
||||
- 之后就是等待服务器的响应,专有名词叫TTFB(Time 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">
|
||||
123
极客时间专栏/geek/透视HTTP协议/总结篇/40 | HTTP性能优化面面观(下).md
Normal file
123
极客时间专栏/geek/透视HTTP协议/总结篇/40 | HTTP性能优化面面观(下).md
Normal 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">
|
||||
|
||||
|
||||
125
极客时间专栏/geek/透视HTTP协议/探索篇/34 | Nginx:高性能的Web服务器.md
Normal file
125
极客时间专栏/geek/透视HTTP协议/探索篇/34 | Nginx:高性能的Web服务器.md
Normal 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">
|
||||
|
||||
|
||||
130
极客时间专栏/geek/透视HTTP协议/探索篇/35 | OpenResty:更灵活的Web服务器.md
Normal file
130
极客时间专栏/geek/透视HTTP协议/探索篇/35 | OpenResty:更灵活的Web服务器.md
Normal 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("body: ", 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虚拟机,支持JIT(Just In Time)技术,可以把Lua代码即时编译成“本地机器码”,这样就消除了脚本语言解释运行的劣势,让Lua脚本跑得和原生C代码一样快。
|
||||
|
||||
另外,LuaJIT还为Lua语言添加了一些特别的增强,比如二进制位运算库bit,内存优化库table,还有FFI(Foreign Function Interface),让Lua直接调用底层C函数,比原生的压栈调用快很多。
|
||||
|
||||
## 阶段式处理
|
||||
|
||||
和Nginx一样,OpenResty也使用“流水线”来处理HTTP请求,底层的运行基础是Nginx的“阶段式处理”,但它又有自己的特色。
|
||||
|
||||
Nginx的“流水线”是由一个个C模块组成的,只能在静态文件里配置,开发困难,配置麻烦(相对而言)。而OpenResty的“流水线”则是由一个个的Lua脚本组成的,不仅可以从磁盘上加载,也可以从Redis、MySQL里加载,而且编写、调试的过程非常方便快捷。
|
||||
|
||||
下面我画了一张图,列出了OpenResty的阶段,比起Nginx,OpenResty的阶段更注重对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">
|
||||
|
||||
|
||||
182
极客时间专栏/geek/透视HTTP协议/探索篇/36 | WAF:保护我们的网络服务.md
Normal file
182
极客时间专栏/geek/透视HTTP协议/探索篇/36 | WAF:保护我们的网络服务.md
Normal 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;
|
||||
"1.2.3.4" 1;
|
||||
"5.6.7.8" 1;
|
||||
}
|
||||
|
||||
|
||||
if ($blocked) {
|
||||
return 403 "you are blocked.";
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
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">
|
||||
|
||||
|
||||
120
极客时间专栏/geek/透视HTTP协议/探索篇/37 | CDN:加速我们的网络服务.md
Normal file
120
极客时间专栏/geek/透视HTTP协议/探索篇/37 | CDN:加速我们的网络服务.md
Normal 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协议的服务,这个就是我们今天要说的CDN(Content 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,还有新兴的ATS(Apache 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">
|
||||
|
||||
|
||||
151
极客时间专栏/geek/透视HTTP协议/探索篇/38 | WebSocket:沙盒里的TCP.md
Normal file
151
极客时间专栏/geek/透视HTTP协议/探索篇/38 | WebSocket:沙盒里的TCP.md
Normal 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">
|
||||
|
||||
|
||||
175
极客时间专栏/geek/透视HTTP协议/特别放送/44 | 先睹为快:HTTP|3实验版本长什么样子?.md
Normal file
175
极客时间专栏/geek/透视HTTP协议/特别放送/44 | 先睹为快:HTTP|3实验版本长什么样子?.md
Normal 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/2,HTTP/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="">
|
||||
147
极客时间专栏/geek/透视HTTP协议/破冰篇/01 | 时势与英雄:HTTP的前世今生.md
Normal file
147
极客时间专栏/geek/透视HTTP协议/破冰篇/01 | 时势与英雄:HTTP的前世今生.md
Normal 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协议的变革,这也开启了第二次的“浏览器大战”。
|
||||
|
||||
历史再次重演,不过这次的胜利者是Google,Chrome目前的全球的占有率超过了60%。“挟用户以号令天下”,Google借此顺势把SPDY推上了标准的宝座,互联网标准化组织以SPDY为基础开始制定新版本的HTTP协议,最终在2015年发布了HTTP/2,RFC编号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">
|
||||
|
||||
|
||||
141
极客时间专栏/geek/透视HTTP协议/破冰篇/02 | HTTP是什么?HTTP又不是什么?.md
Normal file
141
极客时间专栏/geek/透视HTTP协议/破冰篇/02 | HTTP是什么?HTTP又不是什么?.md
Normal 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<===>B”。
|
||||
|
||||
别小看了这个简单的动作,它也至少包含了两项重要的信息。
|
||||
|
||||
第一点,HTTP协议是一个“**双向协议**”。
|
||||
|
||||
也就是说,有两个最基本的参与者A和B,从A开始到B结束,数据在A和B之间双向而不是单向流动。通常我们把先发起传输动作的A叫做**请求方**,把后接到传输的B叫做**应答方**或者**响应方**。拿我们最常见的上网冲浪来举例子,浏览器就是请求方A,网易、新浪这些网站就是应答方B。双方约定用HTTP协议来通信,于是浏览器把一些数据发送给网站,网站再把一些数据发回给浏览器,最后展现在屏幕上,你就可以看到各种有意思的新闻、视频了。
|
||||
|
||||
第二点,数据虽然是在A和B之间传输,但并没有限制只有A和B这两个角色,允许中间有“中转”或者“接力”。
|
||||
|
||||
这样,传输方式就从“A<===>B”,变成了“A<=>X<=>Y<=>Z<=>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">
|
||||
|
||||
|
||||
146
极客时间专栏/geek/透视HTTP协议/破冰篇/03 | HTTP世界全览(上):与HTTP相关的各种概念.md
Normal file
146
极客时间专栏/geek/透视HTTP协议/破冰篇/03 | HTTP世界全览(上):与HTTP相关的各种概念.md
Normal 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消息,也就是说,它是**一个基于Web(HTTP)的服务架构技术**,既可以运行在内网,也可以在适当保护后运行在外网。
|
||||
|
||||
因为采用了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">
|
||||
|
||||
|
||||
148
极客时间专栏/geek/透视HTTP协议/破冰篇/04 | HTTP世界全览(下):与HTTP相关的各种协议.md
Normal file
148
极客时间专栏/geek/透视HTTP协议/破冰篇/04 | HTTP世界全览(下):与HTTP相关的各种协议.md
Normal 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/TLS,HTTPS也就能够轻松掌握。
|
||||
|
||||
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">
|
||||
|
||||
|
||||
147
极客时间专栏/geek/透视HTTP协议/破冰篇/05 | 常说的“四层”和“七层”到底是什么?“五层”“六层”哪去了?.md
Normal file
147
极客时间专栏/geek/透视HTTP协议/破冰篇/05 | 常说的“四层”和“七层”到底是什么?“五层”“六层”哪去了?.md
Normal 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 layer),IP协议就处在这一层。因为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层的传输单位是帧(frame),IP层的传输单位是包(packet),TCP层的传输单位是段(segment),HTTP的传输单位则是消息或报文(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和三层的TCP,HTTP在第四层;
|
||||
1. OSI分为七层,基本对应TCP/IP,TCP在第四层,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">
|
||||
|
||||
|
||||
136
极客时间专栏/geek/透视HTTP协议/破冰篇/06 | 域名里有哪些门道?.md
Normal file
136
极客时间专栏/geek/透视HTTP协议/破冰篇/06 | 域名里有哪些门道?.md
Normal 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">
|
||||
|
||||
|
||||
155
极客时间专栏/geek/透视HTTP协议/破冰篇/07 | 自己动手,搭建HTTP实验环境.md
Normal file
155
极客时间专栏/geek/透视HTTP协议/破冰篇/07 | 自己动手,搭建HTTP实验环境.md
Normal 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是73,WireShark是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">
|
||||
183
极客时间专栏/geek/透视HTTP协议/答疑篇/41 | Linux|Mac实验环境搭建与URI查询参数.md
Normal file
183
极客时间专栏/geek/透视HTTP协议/答疑篇/41 | Linux|Mac实验环境搭建与URI查询参数.md
Normal 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 "deb http://openresty.org/package/ubuntu $(lsb_release -sc) main"
|
||||
|
||||
|
||||
# 更新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 "http://127.0.0.1/"
|
||||
curl -v "http://www.chrono.com/09-1"
|
||||
curl -k "https://www.chrono.com/24-1?key=1234"
|
||||
curl -v "http://www.chrono.com/41-1"
|
||||
|
||||
```
|
||||
|
||||
## Mac上搭建实验环境
|
||||
|
||||
看完了Linux,我们再来看一下Mac。
|
||||
|
||||
这里我用的是两个环境:Mac mini 和 MacBook Air,不过都是好几年前的“老古董”了,系统是10.13 High Sierra和10.14 Mojave(更早的版本没有测试,但应该也都可以)。
|
||||
|
||||
首先要保证Mac里有第三方包管理工具homebrew,可以用下面的命令安装:
|
||||
|
||||
```
|
||||
#先安装Mac的homebrew
|
||||
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
|
||||
|
||||
```
|
||||
|
||||
然后,要用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">
|
||||
|
||||
|
||||
93
极客时间专栏/geek/透视HTTP协议/答疑篇/42 | DHE|ECDHE算法的原理.md
Normal file
93
极客时间专栏/geek/透视HTTP协议/答疑篇/42 | DHE|ECDHE算法的原理.md
Normal 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 Diffie–Hellman),里面的关键字是“短暂”“椭圆曲线”和“迪菲-赫尔曼”,我先来讲“迪菲-赫尔曼”,也就是DH算法。
|
||||
|
||||
## 离散对数
|
||||
|
||||
DH算法是一种非对称加密算法,只能用于密钥交换,它的数学基础是“**离散对数**”(Discrete logarithm)。
|
||||
|
||||
那么,什么是离散对数呢?
|
||||
|
||||
上中学的时候我们都学过初等代数,知道指数和对数,指数就是幂运算,对数是指数的逆运算,是已知底数和真数(幂结果),反推出指数。
|
||||
|
||||
例如,如果以10作为底数,那么指数运算是y=10^x,对数运算是y=logx,100的对数是2(10^2=100,log100=2),2的对数是0.301(log2≈0.301)。
|
||||
|
||||
对数运算的域是实数,取值是连续的,而“离散对数”顾名思义,取值是不连续的,数值都是整数,但运算具有与实数对数相似的性质。
|
||||
|
||||
离散对数里的一个核心操作是模运算,也就是取余数(mod,在C、Java、Lua等语言里的操作符是“%”)。
|
||||
|
||||
假设有模数17,底数5,那么“5的3次方再对17取余数得6”(5 ^ 3 % 17 = 6)就是在离散整数域上的一次指数运算(5 ^ 3 (mod 17) = 6)。反过来,以5为底,17为模数,6的离散对数就是3(Ind(5, 6) = 3 ( mod 17))。
|
||||
|
||||
这里的(17,5)是离散对数的公共参数,6是真数,3是对数。知道了对数,就可以用幂运算很容易地得到真数,但反过来,知道真数却很难推断出对数,于是就形成了一个“**单向函数**”。
|
||||
|
||||
在这个例子里,选择的模数17很小,使用穷举法从1到17暴力破解也能够计算得到6的离散对数是3。
|
||||
|
||||
但如果我们选择的是一个非常非常大的数,比如说是有1024位的超大素数,那么暴力破解的成本就非常高了,几乎没有什么有效的方法能够快速计算出离散对数,这就是DH算法的数学基础。
|
||||
|
||||
## DH算法
|
||||
|
||||
知道了离散对数,我们来看DH算法,假设Alice和Bob约定使用DH算法来交换密钥。
|
||||
|
||||
基于离散对数,Alice和Bob需要首先确定模数和底数作为算法的参数,这两个参数是公开的,用P和G来代称,简单起见我们还是用17和5(P=17,G=5)。
|
||||
|
||||
然后Alice和Bob各自选择一个随机整数作为**私钥**(必须在1和P-2之间),严格保密。比如Alice选择a=10,Bob选择b=5。
|
||||
|
||||
有了DH的私钥,Alice和Bob再计算幂作为**公钥**,也就是A = (G ^ a % P) = 9,B = (G ^ b % P) = 14,这里的A和B完全可以公开,因为根据离散对数的原理,从真数反向计算对数a和b是非常困难的。
|
||||
|
||||
交换DH公钥之后,Alice手里有五个数:P=17,G=5,a=10,A=9,B=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="">
|
||||
|
||||
|
||||
227
极客时间专栏/geek/透视HTTP协议/答疑篇/43 | 如何进行Docker实验环境搭建?.md
Normal file
227
极客时间专栏/geek/透视HTTP协议/答疑篇/43 | 如何进行Docker实验环境搭建?.md
Normal 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,默认使用的是bash;“–rm”参数表示容器是“用完即扔”,不保存容器实例,一旦退出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 >> /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 >> /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="">
|
||||
71
极客时间专栏/geek/透视HTTP协议/结束语/结束语 | 做兴趣使然的Hero.md
Normal file
71
极客时间专栏/geek/透视HTTP协议/结束语/结束语 | 做兴趣使然的Hero.md
Normal 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">
|
||||
10
极客时间专栏/geek/透视HTTP协议/结束语/结课测试 | 这些HTTP协议知识,你真的掌握了吗?.md
Normal file
10
极客时间专栏/geek/透视HTTP协议/结束语/结课测试 | 这些HTTP协议知识,你真的掌握了吗?.md
Normal 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&exam_id=254)
|
||||
198
极客时间专栏/geek/透视HTTP协议/进阶篇/15 | 海纳百川:HTTP的实体数据.md
Normal file
198
极客时间专栏/geek/透视HTTP协议/进阶篇/15 | 海纳百川:HTTP的实体数据.md
Normal 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/json,application/javascript、application/pdf等,另外,如果实在是不知道数据是什么类型,像刚才说的“黑盒”,就会是application/octet-stream,即不透明的二进制数据。
|
||||
|
||||
但仅有MIME type还不够,因为HTTP在传输时为了节约带宽,有时候还会压缩数据,为了不要让浏览器继续“猜”,还需要有一个“Encoding type”,告诉数据是用的什么编码格式,这样对方才能正确解压缩,还原出原始的数据。
|
||||
|
||||
比起MIME type来说,Encoding type就少了很多,常用的只有下面三种:
|
||||
|
||||
1. gzip:GNU zip压缩格式,也是互联网上最流行的压缩格式;
|
||||
1. deflate:zlib(deflate)压缩格式,流行程度仅次于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等概念。
|
||||
|
||||
欢迎你把自己的答案写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,欢迎你把文章分享给你的朋友。
|
||||
|
||||
|
||||
199
极客时间专栏/geek/透视HTTP协议/进阶篇/16 | 把大象装进冰箱:HTTP传输大文件的方法.md
Normal file
199
极客时间专栏/geek/透视HTTP协议/进阶篇/16 | 把大象装进冰箱:HTTP传输大文件的方法.md
Normal 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">
|
||||
|
||||
|
||||
138
极客时间专栏/geek/透视HTTP协议/进阶篇/17 | 排队也要讲效率:HTTP的连接管理.md
Normal file
138
极客时间专栏/geek/透视HTTP协议/进阶篇/17 | 排队也要讲效率:HTTP的连接管理.md
Normal 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">
|
||||
|
||||
|
||||
169
极客时间专栏/geek/透视HTTP协议/进阶篇/18 | 四通八达:HTTP的重定向和跳转.md
Normal file
169
极客时间专栏/geek/透视HTTP协议/进阶篇/18 | 四通八达:HTTP的重定向和跳转.md
Normal 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=>B=>C=>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">
|
||||
|
||||
|
||||
140
极客时间专栏/geek/透视HTTP协议/进阶篇/19 | 让我知道你是谁:HTTP的Cookie机制.md
Normal file
140
极客时间专栏/geek/透视HTTP协议/进阶篇/19 | 让我知道你是谁:HTTP的Cookie机制.md
Normal 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搜集用户隐私,互联网组织相继提出了DNT(Do Not Track)和P3P(Platform 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">
|
||||
162
极客时间专栏/geek/透视HTTP协议/进阶篇/20 | 生鲜速递:HTTP的缓存控制.md
Normal file
162
极客时间专栏/geek/透视HTTP协议/进阶篇/20 | 生鲜速递:HTTP的缓存控制.md
Normal 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是“**生存时间**”(又叫“新鲜度”“缓存寿命”,类似TTL,Time-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">
|
||||
|
||||
|
||||
146
极客时间专栏/geek/透视HTTP协议/进阶篇/21 | 良心中间商:HTTP的代理服务.md
Normal file
146
极客时间专栏/geek/透视HTTP协议/进阶篇/21 | 良心中间商:HTTP的代理服务.md
Normal 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">
|
||||
135
极客时间专栏/geek/透视HTTP协议/进阶篇/22 | 冷链周转:HTTP的缓存代理.md
Normal file
135
极客时间专栏/geek/透视HTTP协议/进阶篇/22 | 冷链周转:HTTP的缓存代理.md
Normal 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传输链路上,不只是客户端有缓存,服务器上的缓存也是非常有价值的,可以让请求不必走完整个后续处理流程,“就近”获得响应结果。
|
||||
|
||||
特别是对于那些“读多写少”的数据,例如突发热点新闻、爆款商品的详情页,一秒钟内可能有成千上万次的请求。即使仅仅缓存数秒钟,也能够把巨大的访问流量挡在外面,让RPS(request 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<10”,在一天之后还是新鲜的,所以就能卖出去。
|
||||
|
||||
有的时候客户端还会发出一个特别的“**only-if-cached**”属性,表示只接受代理缓存的数据,不接受源服务器的响应。如果代理上没有缓存或者缓存过期,就应该给客户端返回一个504(Gateway 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">
|
||||
|
||||
|
||||
132
极客时间专栏/geek/透视HTTP协议/飞翔篇/30 | 时代之风(上):HTTP|2特性概览.md
Normal file
132
极客时间专栏/geek/透视HTTP协议/飞翔篇/30 | 时代之风(上):HTTP|2特性概览.md
Normal 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/2(h2c)有什么好处,应该如何使用呢?
|
||||
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">
|
||||
177
极客时间专栏/geek/透视HTTP协议/飞翔篇/31 | 时代之风(下):HTTP|2内核剖析.md
Normal file
177
极客时间专栏/geek/透视HTTP协议/飞翔篇/31 | 时代之风(下):HTTP|2内核剖析.md
Normal 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**表示单方向数据发送结束(即EOS,End 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<->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">
|
||||
|
||||
|
||||
140
极客时间专栏/geek/透视HTTP协议/飞翔篇/32 | 未来之路:HTTP|3展望.md
Normal file
140
极客时间专栏/geek/透视HTTP协议/飞翔篇/32 | 未来之路:HTTP|3展望.md
Normal 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">
|
||||
|
||||
|
||||
151
极客时间专栏/geek/透视HTTP协议/飞翔篇/33 | 我应该迁移到HTTP|2吗?.md
Normal file
151
极客时间专栏/geek/透视HTTP协议/飞翔篇/33 | 我应该迁移到HTTP|2吗?.md
Normal 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">
|
||||
|
||||
|
||||
Reference in New Issue
Block a user