This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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