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

View File

@@ -0,0 +1,216 @@
<audio id="audio" title="第14讲 | HTTP协议看个新闻原来这么麻烦" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4f/34/4f71470a46d7f82c603d9d27b2756034.mp3"></audio>
前面讲述完**传输层**,接下来开始讲**应用层**的协议。从哪里开始讲呢就从咱们最常用的HTTP协议开始。
HTTP协议几乎是每个人上网用的第一个协议同时也是很容易被人忽略的协议。
既然说看新闻,咱们就先登录 [http://www.163.com](http://www.163.com) 。
[http://www.163.com](http://www.163.com) 是个URL叫作**统一资源定位符**。之所以叫统一是因为它是有格式的。HTTP称为协议www.163.com是一个域名表示互联网上的一个位置。有的URL会有更详细的位置标识例如 [http://www.163.com/index.html](http://www.163.com/index.html) 。正是因为这个东西是统一的,所以当你把这样一个字符串输入到浏览器的框里的时候,浏览器才知道如何进行统一处理。
## HTTP请求的准备
浏览器会将www.163.com这个域名发送给DNS服务器让它解析为IP地址。有关DNS的过程其实非常复杂这个在后面专门介绍DNS的时候我会详细描述这里我们先不管反正它会被解析成为IP地址。那接下来是发送HTTP请求吗
不是的HTTP是基于TCP协议的当然是要先建立TCP连接了怎么建立呢还记得第11节讲过的三次握手吗
目前使用的HTTP协议大部分都是1.1。在1.1的协议里面默认是开启了Keep-Alive的这样建立的TCP连接就可以在多次请求中复用。
学习了TCP之后你应该知道TCP的三次握手和四次挥手还是挺费劲的。如果好不容易建立了连接然后就做了一点儿事情就结束了有点儿浪费人力和物力。
## HTTP请求的构建
建立了连接以后浏览器就要发送HTTP的请求。
请求的格式就像这样。
<img src="https://static001.geekbang.org/resource/image/85/c1/85ebb0396cbaa45ce00b505229e523c1.jpeg" alt="">
HTTP的报文大概分为三大部分。第一部分是**请求行**,第二部分是请求的**首部**,第三部分才是请求的**正文实体**。
### 第一部分:请求行
在请求行中URL就是 [http://www.163.com](http://www.163.com) 版本为HTTP 1.1。这里要说一下的,就是方法。方法有几种类型。
对于访问网页来讲,最常用的类型就是**GET**。顾名思义GET就是去服务器获取一些资源。对于访问网页来讲要获取的资源往往是一个页面。其实也有很多其他的格式比如说返回一个JSON字符串到底要返回什么是由服务器端的实现决定的。
例如在云计算中如果我们的服务器端要提供一个基于HTTP协议的API获取所有云主机的列表这就会使用GET方法得到返回的可能是一个JSON字符串。字符串里面是一个列表列表里面是一项的云主机的信息。
另外一种类型叫做**POST**。它需要主动告诉服务端一些信息而非获取。要告诉服务端什么呢一般会放在正文里面。正文可以有各种各样的格式。常见的格式也是JSON。
例如我们下一节要讲的支付场景客户端就需要把“我是谁我要支付多少我要买啥”告诉服务器这就需要通过POST方法。
再如在云计算里如果我们的服务器端要提供一个基于HTTP协议的创建云主机的API也会用到POST方法。这个时候往往需要将“我要创建多大的云主机多少CPU多少内存多大硬盘”这些信息放在JSON字符串里面通过POST的方法告诉服务器端。
还有一种类型叫**PUT**就是向指定资源位置上传最新内容。但是HTTP的服务器往往是不允许上传文件的所以PUT和POST就都变成了要传给服务器东西的方法。
在实际使用过程中这两者还会有稍许的区别。POST往往是用来创建一个资源的而PUT往往是用来修改一个资源的。
例如云主机已经创建好了我想对这个云主机打一个标签说明这个云主机是生产环境的另外一个云主机是测试环境的。那怎么修改这个标签呢往往就是用PUT方法。
再有一种常见的就是**DELETE**。这个顾名思义就是用来删除资源的。例如我们要删除一个云主机就会调用DELETE方法。
### 第二部分:首部字段
请求行下面就是我们的首部字段。首部是key value通过冒号分隔。这里面往往保存了一些非常重要的字段。
例如,**Accept-Charset**,表示**客户端可以接受的字符集**。防止传过来的是另外的字符集,从而导致出现乱码。
再如,**Content-Type**是指**正文的格式**。例如我们进行POST的请求如果正文是JSON那么我们就应该将这个值设置为JSON。
这里需要重点说一下的就是**缓存**。为啥要使用缓存呢?那是因为一个非常大的页面有很多东西。
例如,我浏览一个商品的详情,里面有这个商品的价格、库存、展示图片、使用手册等等。商品的展示图片会保持较长时间不变,而库存会根据用户购买的情况经常改变。如果图片非常大,而库存数非常小,如果我们每次要更新数据的时候都要刷新整个页面,对于服务器的压力就会很大。
对于这种高并发场景下的系统,在真正的业务逻辑之前,都需要有个接入层,将这些静态资源的请求拦在最外面。
这个架构的图就像这样。
<img src="https://static001.geekbang.org/resource/image/ca/1d/caec3ba1086557cbf694c621e7e01e1d.jpeg" alt="">
其中DNS、CDN我在后面的章节会讲。和这一节关系比较大的就是Nginx这一层它如何处理HTTP协议呢对于静态资源有Vanish缓存层。当缓存过期的时候才会访问真正的Tomcat应用集群。
在HTTP头里面**Cache-control**是用来**控制缓存**的。当客户端发送的请求中包含max-age指令时如果判定缓存层中资源的缓存时间数值比指定时间的数值小那么客户端可以接受缓存的资源当指定max-age值为0那么缓存层通常需要将请求转发给应用集群。
另外,**If-Modified-Since**也是一个关于缓存的。也就是说如果服务器的资源在某个时间之后更新了那么客户端就应该下载最新的资源如果没有更新服务端会返回“304 Not Modified”的响应那客户端就不用下载了也会节省带宽。
到此为止我们仅仅是拼凑起了HTTP请求的报文格式接下来浏览器会把它交给下一层传输层。怎么交给传输层呢其实也无非是用Socket这些东西只不过用的浏览器里这些程序不需要你自己写有人已经帮你写好了。
## HTTP请求的发送
HTTP协议是基于TCP协议的所以它使用面向连接的方式发送请求通过stream二进制流的方式传给对方。当然到了TCP层它会把二进制流变成一个个报文段发送给服务器。
在发送给每个报文段的时候都需要对方有一个回应ACK来保证报文可靠地到达了对方。如果没有回应那么TCP这一层会进行重新传输直到可以到达。同一个包有可能被传了好多次但是HTTP这一层不需要知道这一点因为是TCP这一层在埋头苦干。
TCP层发送每一个报文的时候都需要加上自己的地址即源地址和它想要去的地方即目标地址将这两个信息放到IP头里面交给IP层进行传输。
IP层需要查看目标地址和自己是否是在同一个局域网。如果是就发送ARP协议来请求这个目标地址对应的MAC地址然后将源MAC和目标MAC放入MAC头发送出去即可如果不在同一个局域网就需要发送到网关还要需要发送ARP协议来获取网关的MAC地址然后将源MAC和网关MAC放入MAC头发送出去。
网关收到包发现MAC符合取出目标IP地址根据路由协议找到下一跳的路由器获取下一跳路由器的MAC地址将包发给下一跳路由器。
这样路由器一跳一跳终于到达目标的局域网。这个时候最后一跳的路由器能够发现目标地址就在自己的某一个出口的局域网上。于是在这个局域网上发送ARP获得这个目标地址的MAC地址将包发出去。
目标的机器发现MAC地址符合就将包收起来发现IP地址符合根据IP头中协议项知道自己上一层是TCP协议于是解析TCP的头里面有序列号需要看一看这个序列包是不是我要的如果是就放入缓存中然后返回一个ACK如果不是就丢弃。
TCP头里面还有端口号HTTP的服务器正在监听这个端口号。于是目标机器自然知道是HTTP服务器这个进程想要这个包于是将包发给HTTP服务器。HTTP服务器的进程看到原来这个请求是要访问一个网页于是就把这个网页发给客户端。
## HTTP返回的构建
HTTP的返回报文也是有一定格式的。这也是基于HTTP 1.1的。
<img src="https://static001.geekbang.org/resource/image/6b/63/6bc37ddcb4e7a61ca3275790820f2263.jpeg" alt="">
状态码会反映HTTP请求的结果。“200”意味着大吉大利而我们最不想见的就是“404”也就是“服务端无法响应这个请求”。然后短语会大概说一下原因。
接下来是返回首部的**key value**。
这里面,**Retry-After**表示告诉客户端应该在多长时间以后再次尝试一下。“503错误”是说“服务暂时不再和这个值配合使用”。
在返回的头部里面也会有**Content-Type**表示返回的是HTML还是JSON。
构造好了返回的HTTP报文接下来就是把这个报文发送出去。还是交给Socket去发送还是交给TCP层让TCP层将返回的HTML也分成一个个小的段并且保证每个段都可靠到达。
这些段加上TCP头后会交给IP层然后把刚才的发送过程反向走一遍。虽然两次不一定走相同的路径但是逻辑过程是一样的一直到达客户端。
客户端发现MAC地址符合、IP地址符合于是就会交给TCP层。根据序列号看是不是自己要的报文段如果是则会根据TCP头中的端口号发给相应的进程。这个进程就是浏览器浏览器作为客户端也在监听某个端口。
当浏览器拿到了HTTP的报文。发现返回“200”一切正常于是就从正文中将HTML拿出来。HTML是一个标准的网页格式。浏览器只要根据这个格式展示出一个绚丽多彩的网页。
这就是一个正常的HTTP请求和返回的完整过程。
## HTTP 2.0
当然HTTP协议也在不断的进化过程中在HTTP1.1基础上便有了HTTP 2.0。
HTTP 1.1在应用层以纯文本的形式进行通信。每次通信都要带完整的HTTP的头而且不考虑pipeline模式的话每次的过程总是像上面描述的那样一去一回。这样在实时性、并发性上都存在问题。
为了解决这些问题HTTP 2.0会对HTTP的头进行一定的压缩将原来每次都要携带的大量key value在两端建立一个索引表对相同的头只发送索引表中的索引。
另外HTTP 2.0协议将一个TCP的连接中切分成多个流每个流都有自己的ID而且流可以是客户端发往服务端也可以是服务端发往客户端。它其实只是一个虚拟的通道。流是有优先级的。
HTTP 2.0还将所有的传输信息分割为更小的消息和帧,并对它们采用二进制格式编码。常见的帧有**Header帧**用于传输Header内容并且会开启一个新的流。再就是**Data帧**用来传输正文实体。多个Data帧属于同一个流。
通过这两种机制HTTP 2.0的客户端可以将多个请求分到不同的流中,然后将请求内容拆成帧,进行二进制传输。这些帧可以打散乱序发送, 然后根据每个帧首部的流标识符重新组装,并且可以根据优先级,决定优先处理哪个流的数据。
我们来举一个例子。
假设我们的一个页面要发送三个独立的请求一个获取css一个获取js一个获取图片jpg。如果使用HTTP 1.1就是串行的但是如果使用HTTP 2.0,就可以在一个连接里,客户端和服务端都可以同时发送多个请求或回应,而且不用按照顺序一对一对应。
<img src="https://static001.geekbang.org/resource/image/9a/1a/9a54f97931377dyy2fde0de93f4ecf1a.jpeg" alt="">
HTTP 2.0其实是将三个请求变成三个流将数据分成帧乱序发送到一个TCP连接中。
<img src="https://static001.geekbang.org/resource/image/3d/d3/3da001fac5701949b94e51caaee887d3.jpeg" alt="">
HTTP 2.0成功解决了HTTP 1.1的队首阻塞问题同时也不需要通过HTTP 1.x的pipeline机制用多条TCP连接来实现并行请求与响应减少了TCP连接数对服务器性能的影响同时将页面的多个数据css、js、 jpg等通过一个数据链接进行传输能够加快页面组件的传输速度。
## QUIC协议的“城会玩”
HTTP 2.0虽然大大增加了并发性但还是有问题的。因为HTTP 2.0也是基于TCP协议的TCP协议在处理包时是有严格顺序的。
当其中一个数据包遇到问题TCP连接需要等待这个包完成重传之后才能继续进行。虽然HTTP 2.0通过多个stream使得逻辑上一个TCP连接上的并行内容进行多路数据的传输然而这中间并没有关联的数据。一前一后前面stream 2的帧没有收到后面stream 1的帧也会因此阻塞。
于是就又到了从TCP切换到UDP进行“城会玩”的时候了。这就是Google的QUIC协议接下来我们来看它是如何“城会玩”的。
### 机制一:自定义连接机制
我们都知道一条TCP连接是由四元组标识的分别是源 IP、源端口、目的 IP、目的端口。一旦一个元素发生变化时就需要断开重连重新连接。在移动互联情况下当手机信号不稳定或者在WIFI和 移动网络切换时,都会导致重连,从而进行再次的三次握手,导致一定的时延。
这在TCP是没有办法的但是基于UDP就可以在QUIC自己的逻辑里面维护连接的机制不再以四元组标识而是以一个64位的随机数作为ID来标识而且UDP是无连接的所以当IP或者端口变化的时候只要ID不变就不需要重新建立连接。
### 机制二:自定义重传机制
前面我们讲过TCP为了保证可靠性通过使用**序号**和**应答**机制,来解决顺序问题和丢包问题。
任何一个序号的包发过去,都要在一定的时间内得到应答,否则一旦超时,就会重发这个序号的包。那怎么样才算超时呢?还记得我们提过的**自适应重传算法**吗?这个超时是通过**采样往返时间RTT**不断调整的。
其实在TCP里面超时的采样存在不准确的问题。例如发送一个包序号为100发现没有返回于是再发送一个100过一阵返回一个ACK101。这个时候客户端知道这个包肯定收到了但是往返时间是多少呢是ACK到达的时间减去后一个100发送的时间还是减去前一个100发送的时间呢事实是第一种算法把时间算短了第二种算法把时间算长了。
QUIC也有个序列号是递增的。任何一个序列号的包只发送一次下次就要加一了。例如发送一个包序号是100发现没有返回再次发送的时候序号就是101了如果返回的ACK 100就是对第一个包的响应。如果返回ACK 101就是对第二个包的响应RTT计算相对准确。
但是这里有一个问题就是怎么知道包100和包101发送的是同样的内容呢QUIC定义了一个offset概念。QUIC既然是面向连接的也就像TCP一样是一个数据流发送的数据在这个数据流里面有个偏移量offset可以通过offset查看数据发送到了哪里这样只要这个offset的包没有来就要重发如果来了按照offset拼接还是能够拼成一个流。
<img src="https://static001.geekbang.org/resource/image/80/2c/805aa4261yyb30a2a0e5a2f06ce5162c.jpeg" alt="">
### 机制三:无阻塞的多路复用
有了自定义的连接和重传机制我们就可以解决上面HTTP 2.0的多路复用问题。
同HTTP 2.0一样同一条QUIC连接上可以创建多个stream来发送多个 HTTP 请求。但是QUIC是基于UDP的一个连接上的多个stream之间没有依赖。这样假如stream2丢了一个UDP包后面跟着stream3的一个UDP包虽然stream2的那个包需要重传但是stream3的包无需等待就可以发给用户。
### 机制四:自定义流量控制
TCP的流量控制是通过**滑动窗口协议**。QUIC的流量控制也是通过window_update来告诉对端它可以接受的字节数。但是QUIC的窗口是适应自己的多路复用机制的不但在一个连接上控制窗口还在一个连接中的每个stream控制窗口。
还记得吗在TCP协议中接收端的窗口的起始点是下一个要接收并且ACK的包即便后来的包都到了放在缓存里面窗口也不能右移因为TCP的ACK机制是基于序列号的累计应答一旦ACK了一个序列号就说明前面的都到了所以只要前面的没到后面的到了也不能ACK就会导致后面的到了也有可能超时重传浪费带宽。
QUIC的ACK是基于offset的每个offset的包来了进了缓存就可以应答应答后就不会重发中间的空档会等待到来或者重发即可而窗口的起始位置为当前收到的最大offset从这个offset到当前的stream所能容纳的最大缓存是真正的窗口大小。显然这样更加准确。
<img src="https://static001.geekbang.org/resource/image/a6/22/a66563b46906e7708cc69a02d43afb22.jpg" alt="">
另外还有整个连接的窗口需要对于所有的stream的窗口做一个统计。
## 小结
好了,今天就讲到这里,我们来总结一下:
<li>
HTTP协议虽然很常用也很复杂重点记住GET、POST、 PUT、DELETE这几个方法以及重要的首部字段
</li>
<li>
HTTP 2.0通过头压缩、分帧、二进制编码、多路复用等技术提升性能;
</li>
<li>
QUIC协议通过基于UDP自定义的类似TCP的连接、重试、多路复用、流量控制技术进一步提升性能。
</li>
接下来,给你留两个思考题吧。
<li>
QUIC是一个精巧的协议所以它肯定不止今天我提到的四种机制你知道它还有哪些吗
</li>
<li>
这一节主要讲了如何基于HTTP浏览网页如果要传输比较敏感的银行卡信息该怎么办呢
</li>
欢迎你留言和我讨论。趣谈网络协议,我们下期见!

View File

@@ -0,0 +1,171 @@
<audio id="audio" title="第15讲 | HTTPS协议点外卖的过程原来这么复杂" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f3/8d/f32cb9ab428dd51e209d6c1ce260ae8d.mp3"></audio>
用HTTP协议看个新闻还没有问题但是换到更加严肃的场景中就存在很多的安全风险。例如你要下单做一次支付如果还是使用普通的HTTP协议那你很可能会被黑客盯上。
你发送一个请求,说我要点个外卖,但是这个网络包被截获了,于是在服务器回复你之前,黑客先假装自己就是外卖网站,然后给你回复一个假的消息说:“好啊好啊,来来来,银行卡号、密码拿来。”如果这时候你真把银行卡密码发给它,那你就真的上套了。
那怎么解决这个问题呢?当然一般的思路就是**加密**。加密分为两种方式一种是**对称加密**,一种是**非对称加密**。
在对称加密算法中,加密和解密使用的密钥是相同的。也就是说,加密和解密使用的是同一个密钥。因此,对称加密算法要保证安全性的话,密钥要做好保密。只能让使用的人知道,不能对外公开。
在非对称加密算法中,加密使用的密钥和解密使用的密钥是不相同的。一把是作为公开的公钥,另一把是作为谁都不能给的私钥。公钥加密的信息,只有私钥才能解密。私钥加密的信息,只有公钥才能解密。
因为对称加密算法相比非对称加密算法来说,效率要高得多,性能也好,所以交互的场景下多用对称加密。
## 对称加密
假设你和外卖网站约定了一个密钥,你发送请求的时候用这个密钥进行加密,外卖网站用同样的密钥进行解密。这样就算中间的黑客截获了你的请求,但是它没有密钥,还是破解不了。
这看起来很完美,但是中间有个问题,你们两个怎么来约定这个密钥呢?如果这个密钥在互联网上传输,也是很有可能让黑客截获的。黑客一旦截获这个秘钥,它可以佯作不知,静静地等着你们两个交互。这时候你们之间互通的任何消息,它都能截获并且查看,就等你把银行卡账号和密码发出来。
我们在谍战剧里面经常看到这样的场景,就是特工破译的密码会有个密码本,截获无线电台,通过密码本就能将原文破解出来。怎么把密码本给对方呢?只能通过**线下传输**。
比如,你和外卖网站偷偷约定时间地点,它给你一个纸条,上面写着你们两个的密钥,然后说以后就用这个密钥在互联网上定外卖了。当然你们接头的时候,也会先约定一个口号,什么“天王盖地虎”之类的,口号对上了,才能把纸条给它。但是,“天王盖地虎”同样也是对称加密密钥,同样存在如何把“天王盖地虎”约定成口号的问题。而且在谍战剧中一对一接头可能还可以,在互联网应用中,客户太多,这样是不行的。
## 非对称加密
所以,只要是对称加密,就会永远在这个死循环里出不来,这个时候,就需要非对称加密介入进来。
非对称加密的私钥放在外卖网站这里,不会在互联网上传输,这样就能保证这个密钥的私密性。但是,对应私钥的公钥,是可以在互联网上随意传播的,只要外卖网站把这个公钥给你,你们就可以愉快地互通了。
比如说你用公钥加密,说“我要定外卖”,黑客在中间就算截获了这个报文,因为它没有私钥也是解不开的,所以这个报文可以顺利到达外卖网站,外卖网站用私钥把这个报文解出来,然后回复,“那给我银行卡和支付密码吧”。
先别太乐观,这里还是有问题的。回复的这句话,是外卖网站拿私钥加密的,互联网上人人都可以把它打开,当然包括黑客。那外卖网站可以拿公钥加密吗?当然不能,因为它自己的私钥只有它自己知道,谁也解不开。
另外,这个过程还有一个问题,黑客也可以模拟发送“我要定外卖”这个过程的,因为它也有外卖网站的公钥。
为了解决这个问题,看来一对公钥私钥是不够的,客户端也需要有自己的公钥和私钥,并且客户端要把自己的公钥,给外卖网站。
这样,客户端给外卖网站发送的时候,用外卖网站的公钥加密。而外卖网站给客户端发送消息的时候,使用客户端的公钥。这样就算有黑客企图模拟客户端获取一些信息,或者半路截获回复信息,但是由于它没有私钥,这些信息它还是打不开。
## 数字证书
不对称加密也会有同样的问题,如何将不对称加密的公钥给对方呢?一种是放在一个公网的地址上,让对方下载;另一种就是在建立连接的时候,传给对方。
这两种方法有相同的问题,那就是,作为一个普通网民,你怎么鉴别别人给你的公钥是对的。会不会有人冒充外卖网站,发给你一个它的公钥。接下来,你和它所有的互通,看起来都是没有任何问题的。毕竟每个人都可以创建自己的公钥和私钥。
例如我自己搭建了一个网站cliu8site可以通过这个命令先创建私钥。
```
openssl genrsa -out cliu8siteprivate.key 1024
```
然后,再根据这个私钥,创建对应的公钥。
```
openssl rsa -in cliu8siteprivate.key -pubout -outcliu8sitepublic.pem
```
这个时候就需要权威部门的介入了,就像每个人都可以打印自己的简历,说自己是谁,但是有公安局盖章的,就只有户口本,这个才能证明你是你。这个由权威部门颁发的称为**证书****Certificate**)。
证书里面有什么呢?当然应该有**公钥**,这是最重要的;还有证书的**所有者**,就像户口本上有你的姓名和身份证号,说明这个户口本是你的;另外还有证书的**发布机构**和证书的**有效期**,这个有点像身份证上的机构是哪个区公安局,有效期到多少年。
这个证书是怎么生成的呢?会不会有人假冒权威机构颁发证书呢?就像有假身份证、假户口本一样。生成证书需要发起一个证书请求,然后将这个请求发给一个权威机构去认证,这个权威机构我们称为**CA** **Certificate Authority**)。
证书请求可以通过这个命令生成。
```
openssl req -key cliu8siteprivate.key -new -out cliu8sitecertificate.req
```
将这个请求发给权威机构,权威机构会给这个证书卡一个章,我们称为**签名算法。**问题又来了那怎么签名才能保证是真的权威机构签名的呢当然只有用只掌握在权威机构手里的东西签名了才行这就是CA的私钥。
签名算法大概是这样工作的一般是对信息做一个Hash计算得到一个Hash值这个过程是不可逆的也就是说无法通过Hash值得出原来的信息内容。在把信息发送出去时把这个Hash值加密后作为一个签名和信息一起发出去。
权威机构给证书签名的命令是这样的。
```
openssl x509 -req -in cliu8sitecertificate.req -CA cacertificate.pem -CAkey caprivate.key -out cliu8sitecertificate.pem
```
这个命令会返回Signature ok而cliu8sitecertificate.pem就是签过名的证书。CA用自己的私钥给外卖网站的公钥签名就相当于给外卖网站背书形成了外卖网站的证书。
我们来查看这个证书的内容。
```
openssl x509 -in cliu8sitecertificate.pem -noout -text
```
这里面有个Issuer也即证书是谁颁发的Subject就是证书颁发给谁Validity是证书期限Public-key是公钥内容Signature Algorithm是签名算法。
这下好了你不会从外卖网站上得到一个公钥而是会得到一个证书这个证书有个发布机构CA你只要得到这个发布机构CA的公钥去解密外卖网站证书的签名如果解密成功了Hash也对的上就说明这个外卖网站的公钥没有啥问题。
你有没有发现又有新问题了。要想验证证书需要CA的公钥问题是你怎么确定CA的公钥就是对的呢
所以CA的公钥也需要更牛的CA给它签名然后形成CA的证书。要想知道某个CA的证书是否可靠要看CA的上级证书的公钥能不能解开这个CA的签名。就像你不相信区公安局可以打电话问市公安局让市公安局确认区公安局的合法性。这样层层上去直到全球皆知的几个著名大CA称为**root CA**,做最后的背书。通过这种**层层授信背书**的方式,从而保证了非对称加密模式的正常运转。
除此之外,还有一种证书,称为**Self-Signed Certificate**,就是自己给自己签名。这个给人一种“我就是我,你爱信不信”的感觉。这里我就不多说了。
## HTTPS的工作模式
我们可以知道,非对称加密在性能上不如对称加密,那是否能将两者结合起来呢?例如,公钥私钥主要用于传输对称加密的秘钥,而真正的双方大数据量的通信都是通过对称加密进行的。
当然是可以的。这就是HTTPS协议的总体思路。
<img src="https://static001.geekbang.org/resource/image/df/b4/df1685dd308cef1db97e91493f911ab4.jpg" alt="">
当你登录一个外卖网站的时候由于是HTTPS客户端会发送Client Hello消息到服务器以明文传输TLS版本信息、加密套件候选列表、压缩算法候选列表等信息。另外还会有一个随机数在协商对称密钥的时候使用。
这就类似在说:“您好,我想定外卖,但你要保密我吃的是什么。这是我的加密套路,再给你个随机数,你留着。”
然后外卖网站返回Server Hello消息, 告诉客户端,服务器选择使用的协议版本、加密套件、压缩算法等,还有一个随机数,用于后续的密钥协商。
这就类似在说“您好保密没问题你的加密套路还挺多咱们就按套路2来吧我这里也有个随机数你也留着。”
然后外卖网站会给你一个服务器端的证书然后说“Server Hello Done我这里就这些信息了。”
你当然不相信这个证书于是你从自己信任的CA仓库中拿CA的证书里面的公钥去解密外卖网站的证书。如果能够成功则说明外卖网站是可信的。这个过程中你可能会不断往上追溯CA、CA的CA、CA的CA的CA反正直到一个授信的CA就可以了。
证书验证完毕之后觉得这个外卖网站可信于是客户端计算产生随机数字Pre-master发送Client Key Exchange用证书中的公钥加密再发送给服务器服务器可以通过私钥解密出来。
到目前为止无论是客户端还是服务器都有了三个随机数分别是自己的、对端的以及刚生成的Pre-Master随机数。通过这三个随机数可以在客户端和服务器产生相同的对称密钥。
有了对称密钥客户端就可以说“Change Cipher Spec咱们以后都采用协商的通信密钥和加密算法进行加密通信了。”
然后发送一个Encrypted Handshake Message将已经商定好的参数等采用协商密钥进行加密发送给服务器用于数据与握手验证。
同样服务器也可以发送Change Cipher Spec“没问题咱们以后都采用协商的通信密钥和加密算法进行加密通信了”并且也发送Encrypted Handshake Message的消息试试。当双方握手结束之后就可以通过对称密钥进行加密传输了。
这个过程除了加密解密之外其他的过程和HTTP是一样的过程也非常复杂。
上面的过程只包含了HTTPS的单向认证也即客户端验证服务端的证书是大部分的场景也可以在更加严格安全要求的情况下启用双向认证双方互相验证证书。
## 重放与篡改
其实,这里还有一些没有解决的问题,例如重放和篡改的问题。
没错有了加密和解密黑客截获了包也打不开了但是它可以发送N次。这个往往通过Timestamp和Nonce随机数联合起来然后做一个不可逆的签名来保证。
Nonce随机数保证唯一或者Timestamp和Nonce合起来保证唯一同样的请求只接受一次于是服务器多次收到相同的Timestamp和Nonce则视为无效即可。
如果有人想篡改Timestamp和Nonce还有签名保证不可篡改性如果改了用签名算法解出来就对不上了可以丢弃了。
## 小结
好了,这一节就到这里了,我们来总结一下。
<li>
加密分对称加密和非对称加密。对称加密效率高,但是解决不了密钥传输问题;非对称加密可以解决这个问题,但是效率不高。
</li>
<li>
非对称加密需要通过证书和权威机构来验证公钥的合法性。
</li>
<li>
HTTPS是综合了对称加密和非对称加密算法的HTTP协议。既保证传输安全也保证传输效率。
</li>
最后,给你留两个思考题:
<li>
HTTPS协议比较复杂沟通过程太繁复这样会导致效率问题那你知道有哪些手段可以解决这些问题吗
</li>
<li>
HTTP和HTTPS协议的正文部分传输个JSON什么的还好如果播放视频就有问题了那这个时候应该使用什么协议呢
</li>
欢迎你留言和我讨论。趣谈网络协议,我们下期见!

View File

@@ -0,0 +1,231 @@
<audio id="audio" title="第16讲 | 流媒体协议:如何在直播里看到美女帅哥?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/27/cf/271e9364c1c9a01d23780338e74a91cf.mp3"></audio>
最近直播比较火,很多人都喜欢看直播,那一个直播系统里面都有哪些组成部分,都使用了什么协议呢?
无论是直播还是点播,其实都是对于视频数据的传输。一提到视频,大家都爱看,但是一提到视频技术,大家都头疼,因为名词实在是太多了。
## 三个名词系列
我这里列三个名词系列,你先大致有个印象。
<li>
**名词系列一**AVI、MPEG、RMVB、MP4、MOV、FLV、WebM、WMV、ASF、MKV。例如RMVB和MP4看着是不是很熟悉
</li>
<li>
**名词系列二**H.261、 H.262、H.263、H.264、H.265。这个是不是就没怎么听过了别着急你先记住要重点关注H.264。
</li>
<li>
**名词系列**三MPEG-1、MPEG-2、MPEG-4、MPEG-7。MPEG好像听说过但是后面的数字是怎么回事是不是又熟悉又陌生
</li>
这里,我想问你个问题,视频是什么?我说,其实就是快速播放一连串连续的图片。
每一张图片,我们称为一**帧**。只要每秒钟帧的数据足够多也即播放得足够快。比如每秒30帧以人的眼睛的敏感程度是看不出这是一张张独立的图片的这就是我们常说的**帧率****FPS**)。
每一张图片,都是由**像素**组成的假设为1024*768这个像素数不算多。每个像素由RGB组成每个8位共24位。
我们来算一下,每秒钟的视频有多大?
30帧 × 1024 × 768 × 24 = 566,231,040Bits = 70,778,880Bytes
如果一分钟呢4,246,732,800Bytes已经是4个G了。
是不是不算不知道,一算吓一跳?这个数据量实在是太大,根本没办法存储和传输。如果这样存储,你的硬盘很快就满了;如果这样传输,那多少带宽也不够用啊!
怎么办呢?人们想到了**编码**就是看如何用尽量少的Bit数保存视频使播放的时候画面看起来仍然很精美。**编码是一个压缩的过程。**
## 视频和图片的压缩过程有什么特点?
之所以能够对视频流中的图片进行压缩,因为视频和图片有这样一些特点。
<li>
**空间冗余**:图像的相邻像素之间有较强的相关性,一张图片相邻像素往往是渐变的,不是突变的,没必要每个像素都完整地保存,可以隔几个保存一个,中间的用算法计算出来。
</li>
<li>
**时间冗余**:视频序列的相邻图像之间内容相似。一个视频中连续出现的图片也不是突变的,可以根据已有的图片进行预测和推断。
</li>
<li>
**视觉冗余**:人的视觉系统对某些细节不敏感,因此不会每一个细节都注意到,可以允许丢失一些数据。
</li>
<li>
**编码冗余**:不同像素值出现的概率不同,概率高的用的字节少,概率低的用的字节多,类似[霍夫曼编码Huffman Coding](https://zh.wikipedia.org/wiki/%E9%9C%8D%E5%A4%AB%E6%9B%BC%E7%BC%96%E7%A0%81)的思路。
</li>
总之,用于编码的算法非常复杂,而且多种多样,但是编码过程其实都是类似的。
<img src="https://static001.geekbang.org/resource/image/46/9e/46be417bde30b0a22f25928c30a3049e.jpg" alt="">
## 视频编码的两大流派
能不能形成一定的标准呢?要不然开发视频播放的人得累死了。当然能,我这里就给你介绍,视频编码的两大流派。
<li>
流派一ITUInternational Telecommunications Union的VCEGVideo Coding Experts Group这个称为**国际电联下的VCEG**。既然是电信,可想而知,他们最初做视频编码,主要侧重传输。名词系列二,就是这个组织制定的标准。
</li>
<li>
流派二ISOInternational Standards Organization的MPEGMoving Picture Experts Group这个是**ISO旗下的MPEG**本来是做视频存储的。例如编码后保存在VCD和DVD中。当然后来也慢慢侧重视频传输了。名词系列三就是这个组织制定的标准。
</li>
后来ITU-T国际电信联盟电信标准化部门ITU Telecommunication Standardization Sector与MPEG联合制定了H.264/MPEG-4 AVC这才是我们这一节要重点关注的。
经过编码之后,生动活泼的一帧一帧的图像,就变成了一串串让人看不懂的二进制,这个二进制可以放在一个文件里面,按照一定的格式保存起来,这就是名词系列一。
其实这些就是视频保存成文件的格式。例如,前几个字节是什么意义,后几个字节是什么意义,然后是数据,数据中保存的就是编码好的结果。
## 如何在直播里看到帅哥美女?
当然,这个二进制也可以通过某种网络协议进行封装,放在互联网上传输,这个时候就可以进行网络直播了。
网络协议将**编码**好的视频流,从主播端推送到服务器,在服务器上有个运行了同样协议的服务端来接收这些网络包,从而得到里面的视频流,这个过程称为**接流**。
服务端接到视频流之后,可以对视频流进行一定的处理,例如**转码**,也即从一个编码格式,转成另一种格式。因为观众使用的客户端千差万别,要保证他们都能看到直播。
**流处理**完毕之后,就可以等待观众的客户端来请求这些视频流。观众的客户端请求的过程称为**拉流**
如果有非常多的观众,同时看一个视频直播,那都从一个服务器上**拉流**,压力太大了,因而需要一个视频的**分发**网络,将视频预先加载到就近的边缘节点,这样大部分观众看的视频,是从边缘节点拉取的,就能降低服务器的压力。
当观众的客户端将视频流拉下来之后,就需要进行**解码**,也即通过上述过程的逆过程,将一串串看不懂的二进制,再转变成一帧帧生动的图片,在客户端**播放**出来,这样你就能看到美女帅哥啦。
整个直播过程,可以用这个的图来描述。
<img src="https://static001.geekbang.org/resource/image/a9/5c/a90e05f4496baf25df15e0871a5e205c.jpg" alt=""><br>
接下来,我们依次来看一下每个过程。
### 编码:如何将丰富多彩的图片变成二进制流?
虽然我们说视频是一张张图片的序列,但是如果每张图片都完整,就太大了,因而会将视频序列分成三种帧。
<li>
**I帧**,也称关键帧。里面是完整的图片,只需要本帧数据,就可以完成解码。
</li>
<li>
**P帧**前向预测编码帧。P帧表示的是这一帧跟之前的一个关键帧或P帧的差别解码时需要用之前缓存的画面叠加上和本帧定义的差别生成最终画面。
</li>
<li>
**B帧**双向预测内插编码帧。B帧记录的是本帧与前后帧的差别。要解码B帧不仅要取得之前的缓存画面还要解码之后的画面通过前后画面的数据与本帧数据的叠加取得最终的画面。
</li>
可以看出I帧最完整B帧压缩率最高而压缩后帧的序列应该是在IBBP的间隔出现的。这就是**通过时序进行编码**。
<img src="https://static001.geekbang.org/resource/image/10/4f/10abca08bddaac3214cd69bb9a7b8a4f.jpg" alt="">
在一帧中,分成多个片,每个片中分成多个宏块,每个宏块分成多个子块,这样将一张大的图分解成一个个小块,可以方便进行**空间上的编码**。
尽管时空非常立体地组成了一个序列,但是总归还是要压缩成一个二进制流。这个流是有结构的,是一个个的**网络提取层单元****NALU****Network Abstraction Layer Unit**)。变成这种格式就是为了传输,因为网络上的传输,默认的是一个个的包,因而这里也就分成了一个个的单元。
<img src="https://static001.geekbang.org/resource/image/4d/c1/4df30b99a606504cb0bbbc611c27d8c1.jpg" alt="">
每一个NALU首先是一个起始标识符用于标识NALU之间的间隔然后是NALU的头里面主要配置了NALU的类型最终Payload里面是NALU承载的数据。
在NALU头里面主要的内容是类型**NAL Type**。
<li>
0x07表示SPS是序列参数集 包括一个图像序列的所有信息,如图像尺寸、视频格式等。
</li>
<li>
0x08表示PPS是图像参数集包括一个图像的所有分片的所有相关信息包括图像类型、序列号等。
</li>
在传输视频流之前必须要传输这两类参数不然无法解码。为了保证容错性每一个I帧前面都会传一遍这两个参数集合。
如果NALU Header里面的表示类型是SPS或者PPS则Payload中就是真正的参数集的内容。
如果类型是帧则Payload中才是正的视频数据当然也是一帧一帧存放的前面说了一帧的内容还是挺多的因而每一个NALU里面保存的是一片。对于每一片到底是I帧还是P帧还是B帧在片结构里面也有个Header这里面有个类型然后是片的内容。
这样,整个格式就出来了,**一个视频可以拆分成一系列的帧每一帧拆分成一系列的片每一片都放在一个NALU里面NALU之间都是通过特殊的起始标识符分隔在每一个I帧的第一片前面要插入单独保存SPS和PPS的NALU最终形成一个长长的NALU序列**。
### 推流:如何把数据流打包传输到对端?
那这个格式是不是就能够直接在网上传输到对端,开始直播了呢?其实还不是,还需要将这个二进制的流打包成网络包进行发送,这里我们使用**RTMP协议**。这就进入了第二个过程,**推流**。
RTMP是基于TCP的因而肯定需要双方建立一个TCP的连接。在有TCP的连接的基础上还需要建立一个RTMP的连接也即在程序里面你需要调用RTMP类库的Connect函数显示创建一个连接。
RTMP为什么需要建立一个单独的连接呢
因为它们需要商量一些事情,保证以后的传输能正常进行。主要就是两个事情,一个是**版本号**,如果客户端、服务器的版本号不一致,则不能工作。另一个就是**时间戳**,视频播放中,时间是很重要的,后面的数据流互通的时候,经常要带上时间戳的差值,因而一开始双方就要知道对方的时间戳。
未来沟通这些事情需要发送六条消息客户端发送C0、C1、 C2服务器发送S0、 S1、 S2。
首先客户端发送C0表示自己的版本号不必等对方的回复然后发送C1表示自己的时间戳。
服务器只有在收到C0的时候才能返回S0表明自己的版本号如果版本不匹配可以断开连接。
服务器发送完S0后也不用等什么就直接发送自己的时间戳S1。客户端收到S1的时候发一个知道了对方时间戳的ACK C2。同理服务器收到C1的时候发一个知道了对方时间戳的ACK S2。
于是,握手完成。
<img src="https://static001.geekbang.org/resource/image/ba/84/ba03ba5f2b2515df3669c469301e5784.jpg" alt="">
握手之后双方需要互相传递一些控制信息例如Chunk块的大小、窗口大小等。
真正传输数据的时候还是需要创建一个流Stream然后通过这个Stream来推流publish。
推流的过程就是将NALU放在Message里面发送这个也称为**RTMP Packet包**。Message的格式就像这样。
<img src="https://static001.geekbang.org/resource/image/20/b3/206yy043f9fdfa79d984156fb2fea4b3.jpg" alt="">
发送的时候去掉NALU的起始标识符。因为这部分对于RTMP协议来讲没有用。接下来将SPS和PPS参数集封装成一个RTMP包发送然后发送一个个片的NALU。
RTMP在收发数据的时候并不是以Message为单位的而是把Message拆分成Chunk发送而且必须在一个Chunk发送完成之后才能开始发送下一个Chunk。每个Chunk中都带有Message ID表示属于哪个Message接收端也会按照这个ID将Chunk组装成Message。
前面连接的时候设置的Chunk块大小就是指这个Chunk。将大的消息变为小的块再发送可以在低带宽的情况下减少网络拥塞。
这有一个分块的例子,你可以看一下。
假设一个视频的消息长度为307但是Chunk大小约定为128于是会拆分为三个Chunk。
第一个Chunk的Type0表示Chunk头是完整的头里面Timestamp为1000总长度Length 为307类型为9是个视频Stream ID为12346正文部分承担128个字节的Data。
第二个Chunk也要发送128个字节Chunk头由于和第一个Chunk一样因此采用Chunk Type3表示头一样就不再发送了。
第三个Chunk要发送的Data的长度为307-128-128=51个字节还是采用Type3。
<img src="https://static001.geekbang.org/resource/image/18/06/186f198d54300f772dcddcb6ec476a06.jpg" alt="">
就这样数据就源源不断到达流媒体服务器,整个过程就像这样。
<img src="https://static001.geekbang.org/resource/image/0b/64/0b4642e6c5130aeac225b9db1126fc64.jpg" alt="">
这个时候大量观看直播的观众就可以通过RTMP协议从流媒体服务器上拉取但是这么多的用户量都去同一个地方拉取服务器压力会很大而且用户分布在全国甚至全球如果都去统一的一个地方下载也会时延比较长需要有分发网络。
分发网络分为**中心**和**边缘**两层。边缘层服务器部署在全国各地及横跨各大运营商里,和用户距离很近。中心层是流媒体服务集群,负责内容的转发。智能负载均衡系统,根据用户的地理位置信息,就近选择边缘服务器,为用户提供推/拉流服务。中心层也负责转码服务例如把RTMP协议的码流转换为HLS码流。
<img src="https://static001.geekbang.org/resource/image/01/56/01b67e494f4705e71243f89b9cd1a556.jpg" alt="">
这套机制在后面的DNS、HTTPDNS、CDN的章节会更有详细的描述。
### 拉流:观众的客户端如何看到视频?
接下来我们再来看观众的客户端通过RTMP拉流的过程。
<img src="https://static001.geekbang.org/resource/image/2c/db/2cdaf16cc2ee71512a4bdf5995549fdb.jpg" alt="">
先读到的是H.264的解码参数例如SPS和PPS然后对收到的NALU组成的一个个帧进行解码交给播发器播放一个绚丽多彩的视频画面就出来了。
## 小结
好了,今天的内容就到这里了,我们来总结一下:
<li>
视频名词比较多,编码两大流派达成了一致,都是通过时间、空间的各种算法来压缩数据;
</li>
<li>
压缩好的数据为了传输组成一系列NALU按照帧和片依次排列
</li>
<li>
排列好的NALU在网络传输的时候要按照RTMP包的格式进行包装RTMP的包会拆分成Chunk进行传输
</li>
<li>
推送到流媒体集群的视频流经过转码和分发可以被客户端通过RTMP协议拉取然后组合为NALU解码成视频格式进行播放。
</li>
最后,给你留两个思考题:
<li>
你觉得基于RTMP的视频流传输的机制存在什么问题如何进行优化
</li>
<li>
在线看视频之前,大家都是把电影下载下来看的,电影这么大,你知道如何快速下载吗?
</li>
欢迎你留言和我讨论。趣谈网络协议,我们下期见!

View File

@@ -0,0 +1,215 @@
<audio id="audio" title="第17讲 | P2P协议我下小电影99%急死你" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/70/af/70a9034e8e3d8cd4c67174c1e09bfcaf.mp3"></audio>
如果你想下载一个电影,一般会通过什么方式呢?
当然,最简单的方式就是通过**HTTP**进行下载。但是相信你有过这样的体验,通过浏览器下载的时候,只要文件稍微大点,下载的速度就奇慢无比。
还有种下载文件的方式,就是通过**FTP**,也即**文件传输协议**。FTP采用两个TCP连接来传输一个文件。
<li>
**控制连接**服务器以被动的方式打开众所周知用于FTP的端口21客户端则主动发起连接。该连接将命令从客户端传给服务器并传回服务器的应答。常用的命令有list——获取文件目录reter——取一个文件store——存一个文件。
</li>
<li>
**数据连接**:每当一个文件在客户端与服务器之间传输时,就创建一个数据连接。
</li>
## FTP的两种工作模式
每传输一个文件都要建立一个全新的数据连接。FTP有两种工作模式分别是**主动模式****PORT**)和**被动模式****PASV**这些都是站在FTP服务器的角度来说的。
主动模式下客户端随机打开一个大于1024的端口N向服务器的命令端口21发起连接同时开放N+1端口监听并向服务器发出 “port N+1” 命令由服务器从自己的数据端口20主动连接到客户端指定的数据端口N+1。
被动模式下当开启一个FTP连接时客户端打开两个任意的本地端口N大于1024和N+1。第一个端口连接服务器的21端口提交PASV命令。然后服务器会开启一个任意的端口P大于1024返回“227 entering passive mode”消息里面有FTP服务器开放的用来进行数据传输的端口。客户端收到消息取得端口号之后会通过N+1号端口连接服务器的端口P然后在两个端口之间进行数据传输。
## P2P是什么
但是无论是HTTP的方式还是FTP的方式都有一个比较大的缺点就是**难以解决单一服务器的带宽压力** 因为它们使用的都是传统的客户端服务器的方式。
后来一种创新的、称为P2P的方式流行起来。**P2P**就是**peer-to-peer**。资源开始并不集中地存储在某些设备上而是分散地存储在多台设备上。这些设备我们姑且称为peer。
想要下载一个文件的时候你只要得到那些已经存在了文件的peer并和这些peer之间建立点对点的连接而不需要到中心服务器上就可以就近下载文件。一旦下载了文件你也就成为peer中的一员你旁边的那些机器也可能会选择从你这里下载文件所以当你使用P2P软件的时候例如BitTorrent往往能够看到既有下载流量也有上传的流量也即你自己也加入了这个P2P的网络自己从别人那里下载同时也提供给其他人下载。可以想象这种方式参与的人越多下载速度越快一切完美。
## 种子(.torrent文件
但是有一个问题当你想下载一个文件的时候怎么知道哪些peer有这个文件呢
这就用到**种子**啦,也即咱们比较熟悉的**.torrent文件**。.torrent文件由两部分组成分别是**announce****tracker URL**)和**文件信息**。
文件信息里面有这些内容。
<li>
**info区**:这里指定的是该种子有几个文件、文件有多长、目录结构,以及目录和文件的名字。
</li>
<li>
**Name字段**:指定顶层目录名字。
</li>
<li>
**每个段的大小**BitTorrent简称BT协议把一个文件分成很多个小段然后分段下载。
</li>
<li>
**段哈希值**将整个种子中每个段的SHA-1哈希值拼在一起。
</li>
下载时BT客户端首先解析.torrent文件得到tracker地址然后连接tracker服务器。tracker服务器回应下载者的请求将其他下载者包括发布者的IP提供给下载者。下载者再连接其他下载者根据.torrent文件两者分别对方告知自己已经有的块然后交换对方没有的数据。此时不需要其他服务器参与并分散了单个线路上的数据流量因此减轻了服务器的负担。
下载者每得到一个块需要算出下载块的Hash验证码并与.torrent文件中的对比。如果一样则说明块正确不一样则需要重新下载这个块。这种规定是为了解决下载内容的准确性问题。
从这个过程也可以看出这种方式特别依赖tracker。tracker需要收集下载者信息的服务器并将此信息提供给其他下载者使下载者们相互连接起来传输数据。虽然下载的过程是非中心化的但是加入这个P2P网络的时候都需要借助tracker中心服务器这个服务器是用来登记有哪些用户在请求哪些资源。
所以这种工作方式有一个弊端一旦tracker服务器出现故障或者线路遭到屏蔽BT工具就无法正常工作了。
## 去中心化网络DHT
那能不能彻底非中心化呢?
于是,后来就有了一种叫作**DHT****Distributed Hash Table**的去中心化网络。每个加入这个DHT网络的人都要负责存储这个网络里的资源信息和其他成员的联系信息相当于所有人一起构成了一个庞大的分布式存储数据库。
有一种著名的DHT协议叫**Kademlia协议**。这个和区块链的概念一样,很抽象,我来详细讲一下这个协议。
任何一个BitTorrent启动之后它都有两个角色。一个是**peer**监听一个TCP端口用来上传和下载文件这个角色表明我这里有某个文件。另一个角色**DHT node**监听一个UDP的端口通过这个角色这个节点加入了一个DHT的网络。
<img src="https://static001.geekbang.org/resource/image/80/27/80ecacb45587d201cbb9a08c31476d27.jpg" alt=""><br>
在DHT网络里面每一个DHT node都有一个ID。这个ID是一个很长的串。每个DHT node都有责任掌握一些知识也就是**文件索引**,也即它应该知道某些文件是保存在哪些节点上。它只需要有这些知识就可以了,而它自己本身不一定就是保存这个文件的节点。
## 哈希值
当然每个DHT node不会有全局的知识也即不知道所有的文件保存在哪里它只需要知道一部分。那应该知道哪一部分呢这就需要用哈希算法计算出来。
每个文件可以计算出一个哈希值,而**DHT node的ID是和哈希值相同长度的串**。
DHT算法是这样规定的如果一个文件计算出一个哈希值则和这个哈希值一样的那个DHT node就有责任知道从哪里下载这个文件即便它自己没保存这个文件。
当然不一定这么巧总能找到和哈希值一模一样的有可能一模一样的DHT node也下线了所以DHT算法还规定除了一模一样的那个DHT node应该知道ID和这个哈希值非常接近的N个DHT node也应该知道。
什么叫和哈希值接近呢例如只修改了最后一位就很接近修改了倒数2位也不远修改了倒数3位也可以接受。总之凑齐了规定的N这个数就行。
刚才那个图里文件1通过哈希运算得到匹配ID的DHT node为node C当然还会有其他的我这里没有画出来。所以node C有责任知道文件1的存放地址虽然node C本身没有存放文件1。
同理文件2通过哈希运算得到匹配ID的DHT node为node E但是node D和E的ID值很近所以node D也知道。当然文件2本身没有必要一定在node D和E里但是碰巧这里就在E那有一份。
接下来一个新的节点node new上线了。如果想下载文件1它首先要加入DHT网络如何加入呢
在这种模式下,种子.torrent文件里面就不再是tracker的地址了而是一个list的node的地址而所有这些node都是已经在DHT网络里面的。当然随着时间的推移很可能有退出的有下线的但是我们假设不会所有的都联系不上总有一个能联系上。
node new只要在种子里面找到一个DHT node就加入了网络。
node new会计算文件1的哈希值并根据这个哈希值了解到和这个哈希值匹配或者很接近的node上知道如何下载这个文件例如计算出来的哈希值就是node C。
但是node new不知道怎么联系上node C因为种子里面的node列表里面很可能没有node C但是它可以问DHT网络特别像一个社交网络node new只有去它能联系上的node问你们知道不知道node C的联系方式呀
在DHT网络中每个node都保存了一定的联系方式但是肯定没有node的所有联系方式。DHT网络中节点之间通过互相通信也会交流联系方式也会删除联系方式。和人们的方式一样你有你的朋友圈你的朋友有它的朋友圈你们互相加微信就互相认识了过一段时间不联系就删除朋友关系。
有个理论是,社交网络中,任何两个人直接的距离不超过六度,也即你想联系比尔盖茨,也就六个人就能够联系到了。
所以node new想联系node C就去万能的朋友圈去问并且求转发朋友再问朋友很快就能找到。如果找不到C也能找到和C的ID很像的节点它们也知道如何下载文件1。
在node C上告诉node new下载文件1要去B、D、 F于是node new选择和node B进行peer连接开始下载它一旦开始下载自己本地也有文件1了于是node new告诉node C以及和node C的ID很像的那些节点我也有文件1了可以加入那个文件拥有者列表了。
但是你会发现node new上没有文件索引但是根据哈希算法一定会有某些文件的哈希值是和node new的ID匹配上的。在DHT网络中会有节点告诉它你既然加入了咱们这个网络你也有责任知道某些文件的下载地址。
好了,一切都分布式了。
这里面遗留几个细节的问题。
- DHT node ID以及文件哈希是个什么东西
节点ID是一个随机选择的160bits20字节空间文件的哈希也使用这样的160bits空间。
- 所谓ID相似具体到什么程度算相似
在Kademlia网络中距离是通过异或XOR计算的。我们就不以160bits举例了。我们以5位来举例。
01010与01000的距离就是两个ID之间的异或值为00010也即为2。 01010与00010的距离为01000也即为8,。01010与00011的距离为01001也即8+1=9 。以此类推,高位不同的,表示距离更远一些;低位不同的,表示距离更近一些,总的距离为所有的不同的位的距离之和。
这个距离不能比喻为地理位置因为在Kademlia网络中位置近不算近ID近才算近所以我把这个距离比喻为社交距离也即在朋友圈中的距离或者社交网络中的距离。这个和你住的位置没有关系和人的经历关系比较大。
还是以5位ID来举例就像在领英中排第一位的表示最近一份工作在哪里第二位的表示上一份工作在哪里然后第三位的是上上份工作第四位的是研究生在哪里读第五位的表示大学在哪里读。
如果你是一个猎头,在上面找候选人,当然最近的那份工作是最重要的。而对于工作经历越丰富的候选人,大学在哪里读的反而越不重要。
## DHT网络中的朋友圈是怎么维护的
就像人一样虽然我们常联系人的只有少数但是朋友圈里肯定是远近都有。DHT网络的朋友圈也是一样远近都有并且**按距离分层**。
假设某个节点的ID为01010如果一个节点的ID前面所有位数都与它相同只有最后1位不同。这样的节点只有1个为01011。与基础节点的异或值为00001即距离为1对于01010而言这样的节点归为“k-bucket 1”。
如果一个节点的ID前面所有位数都相同从倒数第2位开始不同这样的节点只有2个即01000和01001与基础节点的异或值为00010和00011即距离范围为2和3对于01010而言这样的节点归为“k-bucket 2”。
如果一个节点的ID前面所有位数相同从倒数第i位开始不同这样的节点只有2^(i-1)个,与基础节点的距离范围为[2^(i-1), 2^i)对于01010而言这样的节点归为“k-bucket i”。
最终到从倒数160位就开始都不同。
你会发现差距越大陌生人越多但是朋友圈不能都放下所以每一层都只放K个这是参数可以配置。
## DHT网络是如何查找朋友的
假设node A 的ID为00110要找node B ID为10000异或距离为10110距离范围在[2^4, 2^5)所以这个目标节点可能在“k-bucket 5”中这就说明B的ID与A的ID从第5位开始不同所以B可能在“k-bucket 5”中。
然后A看看自己的k-bucket 5有没有B。如果有太好了找到你了如果没有在k-bucket 5里随便找一个C。因为是二进制C、B都和A的第5位不同那么C的ID第5位肯定与B相同即它与B的距离会小于2^4相当于比A、B之间的距离缩短了一半以上。
再请求C在它自己的通讯录里按同样的查找方式找一下B。如果C知道B就告诉A如果C也不知道B那C按同样的搜索方法可以在自己的通讯录里找到一个离B更近的D朋友D、B之间距离小于2^3把D推荐给AA请求D进行下一步查找。
Kademlia的这种查询机制是通过折半查找的方式来收缩范围对于总的节点数目为N最多只需要查询log2(N)次,就能够找到。
例如,图中这个最差的情况。
<img src="https://static001.geekbang.org/resource/image/dc/b4/dc6d713751d09ea5dd0d79c65433aeb4.jpg" alt="">
A和B每一位都不一样所以相差31A找到的朋友C不巧正好在中间。和A的距离是16和B距离为15于是C去自己朋友圈找的时候不巧找到D正好又在中间距离C为8距离B为7。于是D去自己朋友圈找的时候不巧找到E正好又在中间距离D为4距离B为3E在朋友圈找到F距离E为2距离B为1最终在F的朋友圈距离1的地方找到B。当然这是最最不巧的情况每次找到的朋友都不远不近正好在中间。
如果碰巧了在A的朋友圈里面有G距离B只有3然后在G的朋友圈里面一下子就找到了B两次就找到了。
在DHT网络中朋友之间怎么沟通呢
Kademlia算法中每个节点只有4个指令。
<li>
PING测试一个节点是否在线还活着没相当于打个电话看还能打通不。
</li>
<li>
STORE要求一个节点存储一份数据既然加入了组织有义务保存一份数据。
</li>
<li>
FIND_NODE根据节点ID查找一个节点就是给一个160位的ID通过上面朋友圈的方式找到那个节点。
</li>
<li>
FIND_VALUE根据KEY查找一个数据实则上跟FIND_NODE非常类似。KEY就是文件对应的160位的ID就是要找到保存了文件的节点。
</li>
DHT网络中朋友圈如何更新呢
<li>
每个bucket里的节点都按最后一次接触的时间倒序排列这就相当于朋友圈里面最近联系过的人往往是最熟的。
</li>
<li>
每次执行四个指令中的任意一个都会触发更新。
</li>
<li>
当一个节点与自己接触时检查它是否已经在k-bucket中也就是说是否已经在朋友圈。如果在那么将它挪到k-bucket列表的最底也就是最新的位置刚联系过就置顶一下方便以后多联系如果不在新的联系人要不要加到通讯录里面呢假设通讯录已满的情况PING一下列表最上面也即最旧的一个节点。如果PING通了将旧节点挪到列表最底并丢弃新节点老朋友还是留一下如果PING不通删除旧节点并将新节点加入列表这人联系不上了删了吧。
</li>
这个机制保证了任意节点加入和离开都不影响整体网络。
## 小结
好了,今天的讲解就到这里了,我们总结一下:
<li>
下载一个文件可以使用HTTP或FTP这两种都是集中下载的方式而P2P则换了一种思路采取非中心化下载的方式
</li>
<li>
P2P也是有两种一种是依赖于tracker的也即元数据集中文件数据分散另一种是基于分布式的哈希算法元数据和文件数据全部分散。
</li>
接下来,给你留两个思考题:
<li>
除了这种去中心化分布式哈希的算法,你还能想到其他的应用场景吗?
</li>
<li>
在前面所有的章节中要下载一个文件都需要使用域名。但是网络通信是使用IP的那你知道怎么实现两者的映射机制吗
</li>
我们的专栏马上更新过半了,不知你掌握得如何?每节课后我留的思考题,你都有没有认真思考,并在留言区写下答案呢?我会从已发布的文章中选出一批认真留言的同学,赠送学习奖励礼券和我整理的独家网络协议知识图谱。
欢迎你留言和我讨论。趣谈网络协议,我们下期见!