mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-17 06:33:48 +08:00
mod
This commit is contained in:
130
极客时间专栏/透视HTTP协议/基础篇/08 | 键入网址再按下回车,后面究竟发生了什么?.md
Normal file
130
极客时间专栏/透视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
极客时间专栏/透视HTTP协议/基础篇/09 | HTTP报文是什么样子的?.md
Normal file
217
极客时间专栏/透视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
极客时间专栏/透视HTTP协议/基础篇/10 | 应该如何理解请求方法?.md
Normal file
161
极客时间专栏/透视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
极客时间专栏/透视HTTP协议/基础篇/11 | 你能写出正确的网址吗?.md
Normal file
194
极客时间专栏/透视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
极客时间专栏/透视HTTP协议/基础篇/12 | 响应状态码该怎么用?.md
Normal file
141
极客时间专栏/透视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
极客时间专栏/透视HTTP协议/基础篇/13 | HTTP有哪些特点?.md
Normal file
104
极客时间专栏/透视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
极客时间专栏/透视HTTP协议/基础篇/14 | HTTP有哪些优点?又有哪些缺点?.md
Normal file
124
极客时间专栏/透视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">
|
||||
|
||||
|
||||
Reference in New Issue
Block a user