mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-19 07:33:47 +08:00
del
This commit is contained in:
@@ -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>
|
||||
|
||||
欢迎你留言和我讨论。趣谈网络协议,我们下期见!
|
||||
@@ -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>
|
||||
|
||||
欢迎你留言和我讨论。趣谈网络协议,我们下期见!
|
||||
@@ -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>
|
||||
流派一:ITU(International Telecommunications Union)的VCEG(Video Coding Experts Group),这个称为**国际电联下的VCEG**。既然是电信,可想而知,他们最初做视频编码,主要侧重传输。名词系列二,就是这个组织制定的标准。
|
||||
</li>
|
||||
<li>
|
||||
流派二:ISO(International Standards Organization)的MPEG(Moving 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的Type=0,表示Chunk头是完整的;头里面Timestamp为1000,总长度Length 为307,类型为9,是个视频,Stream ID为12346,正文部分承担128个字节的Data。
|
||||
|
||||
第二个Chunk也要发送128个字节,Chunk头由于和第一个Chunk一样,因此采用Chunk Type=3,表示头一样就不再发送了。
|
||||
|
||||
第三个Chunk要发送的Data的长度为307-128-128=51个字节,还是采用Type=3。
|
||||
|
||||
<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>
|
||||
|
||||
欢迎你留言和我讨论。趣谈网络协议,我们下期见!
|
||||
@@ -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是一个随机选择的160bits(20字节)空间,文件的哈希也使用这样的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推荐给A,A请求D进行下一步查找。
|
||||
|
||||
Kademlia的这种查询机制,是通过折半查找的方式来收缩范围,对于总的节点数目为N,最多只需要查询log2(N)次,就能够找到。
|
||||
|
||||
例如,图中这个最差的情况。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/dc/b4/dc6d713751d09ea5dd0d79c65433aeb4.jpg" alt="">
|
||||
|
||||
A和B每一位都不一样,所以相差31,A找到的朋友C,不巧正好在中间。和A的距离是16,和B距离为15,于是C去自己朋友圈找的时候,不巧找到D,正好又在中间,距离C为8,距离B为7。于是D去自己朋友圈找的时候,不巧找到E,正好又在中间,距离D为4,距离B为3,E在朋友圈找到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>
|
||||
|
||||
我们的专栏马上更新过半了,不知你掌握得如何?每节课后我留的思考题,你都有没有认真思考,并在留言区写下答案呢?我会从已发布的文章中选出一批认真留言的同学,赠送学习奖励礼券和我整理的独家网络协议知识图谱。
|
||||
|
||||
欢迎你留言和我讨论。趣谈网络协议,我们下期见!
|
||||
Reference in New Issue
Block a user