这篇只是个人的一个总结,原版内容可以查看 https://time.geekbang.org/column/intro/85 喜欢可以购买作者专栏,在此做个申明。
应用层: DNS, Http, Https 所在的层为应用层.
传输层: 应用层封装成包之后会交给传输层(通过Socket编程来实现), 传输层有两种协议:
UDP协议TCP协议
TCP 协议里有两个端口:
操作系统往往通过端口来判断得到的包应该给哪个进程。
网络层: 网络层的协议是IP协议, 会有源IP地址和目标IP地址。
操作系统启动的时候会被
DHCP协议配置IP地址, 以及默认网关的IP地址(192.168.1.1).
MAC层: 操作系统通过 ARP 协议将IP地址发送给网关, 将 IP包交给下一层 Mac层.
网关往往是一个路由器, 路由器上维护一张路由表, 路由表由 相邻的路由器通过路由协议得到(常用的有 OSPF 和 BGP). 最后一个路由器知道这个网络包要去的地方,目标服务器会回复一个 Mac地址, 通过这个 Mac地址就可以找到目标服务器.
目标服务器发现 Mac地址 对上, 取下 Mac头 发送给操作系统网络层, 发现 IP 也对上, 取下IP头交给传输层(TCP层),传输层对于收到的每个包,都会回复一个包说明收到。如果过一段时间还没到,发送端的TCP层会重新发送这个包。
当网络包平安到达 TCP层 之后, TCP头中有目标端口号. 通过 RPC调用(RPC框架有很多种,有基于HTTP协议的放在HTTP报文里的,有直接封装在TCP报文里的) 来告诉监听这个端口的服务器进程。
Windows上通过ipconfig, Linux上通过ifconfig命令或者ip addr命令 来查看IP`地址.
IP地址是一个网卡在网络世界的通讯地址,相当于我们现实世界的门牌号。
将32位的IP地址分为网络考和主机好两部分。类似于 10.100.122.2/24, 前24位表示网络号,后8位表示主机号.

公有
IP地址有个组织统一分配。192.168.0.x是最常用的私有IP地址.192.168.0.1往往就是私有网络的出口地址,例如:家里的电脑连接Wi-Fi,Wi-Fi路由器的地址就是192.168.0.1.192.168.0.255是广播地址, 一旦发送这个地址, 整个192.168.0网络里的所有机器都能收到。
ping 是基于ICMP(Internet Control Message Protocol, 互联网控制报文协议)协议工作的. ICMP 报文封装在 IP 包里面, 有很多的类型,不同类型有不同的代码。最常用的类型是主动请求为8, 主动请求的应答为0. 常用的ping就是查询报文,是一种主动请求,并且获得主动应答的ICMP协议.
对ping的主动请求,进行抓包称为 ICMP ECHO REQUEST.同理主动请求的回复,称为ICMP ECHO REPLY.比起原生的ICMP, 这里多了两个字段:
3, 具体的原因在代码中的标示
012344115主机A 的IP地址是192.168.1.1, 主机B的IP地址是192.168.1.2, 在同一子网, 主机A 运行 ping 192.168.1.2 发生了啥?
源主机首先会构建一个
ICMP请求数据包,ICMP数据包包含多个字段。最重要的以下两个:
- 类型字段: 对于请求数据包而言该字段为
8- 顺序号: 主要用于区分连续
ping时发出的多个数据包,每发出一个请求数据包,顺序号会自动+1.
为了能计算往返时间
RTT, 会在报文的数据部分插入发送时间。然后,由ICMP协议将数据包连同地址192.168.1.2一同交给IP层。IP层以192.168.1.2作为目的地址,本机IP地址作为源地址,加上一些其他控制信息,构建一个IP数据包。
MAC头: 里先是目标MAC地址,然后是源MAC地址和一个协议类型。用来说明里面是IP协议。
IP头: 里面的版本号, 目前主流的是IPv4, 服务类型TOS,TTL和8标识协议.
任何一台机器上,当要访问另一个IP地址的时候,都会先判断,这个目标IP地址和当前机器的IP地址是否在同一网段,需要使用CIDR和子网掩码。
IP头中,然后通过ARP获得MAC地址,将源MAC和目的MAC放入MAC头中发出去。Gateway, Gateway地址一定和源IP地址在同一网段。网关往往是一个路由器,是一个三层转发设备。但是把网关叫做路由器是不完全准确的 路由器是一台设备,它有五个网口或网卡,相当于有五只手,分别连着五个局域网。每只手的IP地址都和局域网的IP地址在相同的网段,每只手都是它握住哪个局域网的网关TCP是面向连接的, UDP是面向无连接的。在互通之前,面向连接的协议会先建立连接,例如:TCP会三次握手,而UDP不会。
所谓的建立连接,是为了在客户端和服务端维护连接,而建立一定的数据结构来维护双方交互的状态,用这样的数据结构来保证所谓的面向连接的特性。
TCP提供可靠交付。通过TCP连接传输的数据,无差错,不丢失,不重复,并且按序到达。 UDP继承了IP包的特性,不保证不丢失,不保证按顺序到达。TCP是面向字节流的。发送的时候发的是一个流,没头没尾。IP包不是一个流,而是一个个IP包。之所以变成流,是TCP自己状态维护做的事情。UDP继承了IP的特性,基于数据报的, 一个个的发,一个个的收。TCP可以有拥塞控制的。它意识到包丢弃了或者网络环境不好,会根据清空调整自己的行为,看看是不是发快了, 要不要发慢点。 UDP就不会,应用让我发,我就发。TCP其实是一个有状态服务, 而UDP则是无状态服务当我们发送的UDP包到达目标机器后,发现MAC地址匹配,于是取下来,将剩下的包传给处理IP层的代码。把IP头取消来,里面有个8位协议,会标示出数据到底是TCP还是UDP。
无论是TCP传数据还是UDP传数据,都要监听一个端口。TCP 还是 UDP 包头里有个端口号, 根据端口号,将数据交给相应的应用程序。
App的访问: QUIC(Quick UDP Internet Connections,快速UDP互联网连接)是Google提出的一种基于UDP改进的通信协议,目的是降低网络通信延迟,提供更好的用户互动体验。QUIC在应用层上,会自己实现快速连接建立,减少重传时延,自适应拥塞控制。RTMP(也是基于TCP的), 由于TCP的限制,很多直播应用都基于UDP实现自己的视频传输协议。UDP协议,自定义重传策略, 能够把丢包产生的延迟降到最低,尽量减少网路问题对游戏性造成的影响。TCP协议代价太大;另一方面,物联网对实时性要求也很高,Google旗下的Nest建立Thread Group,推出了物联网通信协议 Thread , 就是基于UDP协议的4G网络里,移动流量上网的数据面对的协议GTP-U是基于UDP的。因为移动网络协议比较复杂,而GTP协议本身就包含复杂的手机上线下线的通信协议。如果基于TCP, TCP的机制就显得非常多余。UDP一样。如果没有这两个端口号,数据就不知道应该发给哪个应用。
TCP是可靠的协议,对于TCP来讲,IP层丢不丢包我管不着,我自己会通过不断重传保证可靠性。
SYN是发起一个连接,ACK是回复, RST是重新连接, FIN是结束连接。TCP是面向连接的,双方要维护连接的状态,这些带状态位的包发送,会引起双方的状态变更。TCP要做流量控制,通信双方各申明一个窗口,标示自己当前的处理能力,别发送太快,也别发送太慢。TCP协议需要重点关注以下几个问题:
我们假设通路非常不靠谱,A要发起一个连接,当发了第一个请求了无音讯的时候,会有很多可能性,比如第一个请求包丢了, 超时了还是B没有响应,不想和我连接。
A不能确认结果,于是再发。终于,又一个请求包到了B, 但是请求包到了B这个事情,A是不知道的,A还有可能再发。
B收到请求包,就知道A的存在,并且知道A要和它建立连接。如果B不乐意建立连接,则A会重试一段时间后放弃,连接建立失败,没有问题;如果B乐意建立连接,则会发送应答包给A。
对于B来说,这个应答包也是不知道能不能到达A。这个时候B自然不能认为连接建立好了,因为应答包仍然会丢,会绕弯路,或者A已经挂了。这个时候B还碰到一个诡异的现象,A和B原来建立连接,做了简单通信后,结束连接。A建立连接时,请求包重复发了几次,有的请求包绕了一大圈又回来,B会认为这是一个正常的请求,因此建立连接,可以想象这个连接不会进行下去,也没有终止的时候。
B发送的应答可能会发送多次,但只要一次到达A, A就认为连接已经建立,因为对于A 来说,他的消息有去有回,A会给B应答的应答,只有当B等到这个消息才会建立连接。
三次握手除了双方建立连接外,主要为了沟通
TCP包的序号问题。每个连接都要有不同的序号。这个序号的起始序号是随着时间变化的,可以看成一个32位的计数器,每4ms加一。
A开始说”不玩了”,B说”知道了”。这个是没有什么问题的,因为在此之前,双方还处于合作状态,如果A说”不玩了”,没有收到回复,就会重新发送。但是这个会和结束之后,就可能有异常情况:
A说完”不玩了”之后,直接跑路,就会有问题,B还没有发起结束,也得不到回答,B就不知道怎么办A说完”不玩了”,B直接跑路,也是有问题的,因为A不知道B是还是事情要处理,还是过一会儿会发送结束。
TCP专门设计几种状态来处理这些问题:
- 断开的时候, 当
A说”不玩了”,就进入FIN_WAIT_1的状态,B收到A不玩了的消息后,发送知道了就进入CLOSE_WAIT的状态A收到B说知道了,就进入FIN_WAIT_2状态,如果这个时候B直接跑路,则A将永远在这个状态。TCP协议里没有对这个状态的处理,但是Linux有,可以调整tcp_fin_timeout这个参数, 设置一个超时时间。B没有跑路,发送了B也不玩了的请求到达A时,A发送”知道B也不玩了”的ACK后,从FIN_WAIT_2状态结束,按说A可以跑路了,但是这个最后的ACK万一B收不到呢? 则B会重新发一个B不玩了,这个时候A已经跑路了的话,B就再也收不到ACK了, 因而TCP协议要求A最后等待一段时间TIME_WAIT, 这个时间要足够长,长到如果B没有收到ACK的话,”B 说不玩了”会重发,A会重新发一个ACK并且足够时间到达B。A直接跑路还有一个问题,A的端口直接空出来了,但是B不知道,B发送过来的恶很多包很可能还在路上,如果A的端口被一个新的应用占用,这个新的应用就会收到上个连接中B发过来的包,虽然序列号是重新生成的,但是这里要上一个双保险,防止产生混乱,因而需要等足够长的时间,等到原来B发送的所有包都死翘翘,再空出端口来。等待时间设为2MSL(MSL是Maximum Segment Lifetime, 报文最大生存时间)- 还有个异常就是
B超过2MSL后,依然没有收到它发的FIN的ACK, 按照TCP的原理,B当然会重发FIN, 这个时候A再收到这个包之后,A就直接发送RST,B就知道A早跑了。
TCP协议为了保证顺序性,每一个包都有一个ID。在建立连接的时候,会商定起始的ID是什么,然后按照ID一个个发送。为了保证不丢包,对于发送的包都要进行应答,但是这个应答也不是一个个来的,而是会应答某个之前的ID, 表示都收到了, 这种模式称为累计确认或累计应答(cumulative acknowledgment)。为了记录所有发送和接收的包,TCP也需要发送和接收端分别都有缓存来保存这些记录。发送端的缓存里是按照包的ID一个个排列,根据处理情况分成4个部分:
第
3点和第4点做区分是为了流量控制,把握分寸。在TCP里,接收端会给发送端报一个窗口大小(Advertised Window), 这个窗口的大小应该等于第2点和第3点的总和,超过这个窗口,接收端处理不过来就不能发送。
于是发送端需要保持下面的数据结构:
LastByteAcked: 第1点和第2点的分界线LastByteSent: 第2点和第3点的分界线LastByteSend + AdvertisedWindow: 第3点和第4点的分界线对于接收端来说,它的缓存里记录的内容要简单一些:
对应的数据结构就是:
MaxRcvBuffer: 最大缓存的量LastByteRead: 之后是已经接收了,但还没有被应用层读取的。NextByteExpected: 第1部分和第2部分的分界线。
Socket编程进行端到端通信,往往意识不到中间经过多少局域网,多少路由器,因而能够设置的参数,也只能是端到端协议之上网络层和传输层。
在网络层,
Socket函数需要指定到底是IPv4还是IPv6, 分别对应设置为AF_INET和AF_INET6。另外,还要指定到底是TCP(基于数据流*,设置为SOCK_STREAM)还是UDP(基于数据报**,设置为SOCK_DGRAM)。
两端创建Socket之后, TCP服务端先监听一个端口,一般先调用bind函数, 给这个Socket赋一个IP和端口。原因如下:
IP: 一台机器可能会有多个网卡,也就有多个IP地址,可以选择监听所有的网卡,也可以选择监听一个,只有发给这个网卡的包才会给你。端口: 一个网络包来的时候,内核要通过TCP头里的这个端口,来找到你这个应用程序,把包给你。当服务器有IP和端口号, 就可以调用listen函数进行监听。在TCP的状态图里,有个listen状态,当调用这个函数之后, 服务器就进入这个状态, 客户端就可以发起连接。
内核中为每个Socket维护两个队列:
established状态syn_rcvd状态接下来,服务端调用accept函数,拿出一个已完成的连接进行处理,如还没完成就要等着。在服务端等待的时候,客户端可以通过connect函数发起连接,先在参数中指明要连接的IP地址和端口号, 然后开始发起三次握手,内核会给客户端分配一个临时的端口。一旦握手成功,服务端的accept就会返回另一个Socket。监听的Socket和真正用来传数据的Socket是两个,一个叫做监听Socket, 一个叫做已连接Socket。连接建立成功之后,双方开始通过read和write函数来读写数据,就像往一个文件流里写东西一样。
在内核中Socket是一个文件,对应就有文件描述符。每个进程都有一个数据结构task_struct,里面指向一个文件描述符数组,来列出这个进程打开的所有文件的文件描述符。文件描述符是个整数,是这个数组的下标。这个数组中的内容是一个指针,指向内核中所有打开的文件列表。既然是一个文件,就会有一个inode, 只不过Socket对应的inode不像真正的文件系统一样,保存在硬盘上,而是在内存中。这个inode指向Socket在内核中的Socket的结构。
这个结构里,主要是两个队列,1.发送队列 2.接收队列。在这两个队列里保存的是一个缓存sk_buff。这个缓存里能够看到完整的包结构。
UDP是没有连接的,不需要三次握手,也就不需要调用listen和connect, 但UDP交互仍需要IP和端口号就需要bind。UDP是没有维护连接状态的,因而不需要每对连接建立一组Socket, 而是只要有一个Socket就能和多个客户端通信。也正是因为没有连接状态,每次通信时,都调用sendto和recvfrom, 都可以传入IP地址和端口。
最大连接数, 系统会用一个四元祖来标识一个TCP连接。
{本机IP, 本机端口,对端IP, 对端端口}
服务器通常固定在某个本地端口上监听,等待客户端的连接请求。因此,服务端TCP连接四元祖中只有对端IP(客户端IP)和 对端端口(客户端端口)是可变的。因此:
最大
TCP连接数 = 客户端IP数 x 客户端端口数
对于
IPv4,客户端IP数最多为2^32, 客户端端口数最多为2^16, 也就是服务端单机最大TCP连接数,约为2^48。
当然,服务端最大并发TCP连接数远不能达到理论上限。首先主要是文件描述符限制, 按照上面的原理,Socket都是文件,所以首先要通过ulimit配置文件描述符数目;另一个限制是内存, 按照上面的数据结构,每个TCP连接都要占用一定内存,操作系统内存是有限的。
可以通过以下方式来降低每个项目消耗的资源数目:
Socket, 这时可以创建一个子进程,然后将基于已连接Socket的交互交给这个新的子进程来做。在Linux下,创建子进程使用fork函数(在父进程的基础上完全拷贝一个子进程)。在Linux内核中,会复制文件描述符的列表,也会复制内存空间,还会复制一条记录当前执行到哪行程序的进程。显然, 复制的时候调用fork, 复制完毕之后,父进程和子进程都会记录当前刚刚执行完fork。这两个进程刚复制完的时候,几乎一摸一样,只是根据fork的返回值来区分到底是父进程(返回其他整数),还是子进程(返回0)。因为复制了文件描述符列表,而文件描述符都是指向整个内核统一的打开文件列表的,因而父进程刚才因为accept创建的已连接Socket也是一个文件描述符,同样会被子进程获得。 接下来, 子进程可以通过这个已连接Socket和客户端进行互通了,当通信完毕之后,就可以退出进程。(fork时返回整数就是父进程,这个整数就是子进程的ID, 父进程可通过这个ID查看子进程是否完成项目,是否需要退出)进程来说,创建线程就相当于在同一个公司成立项目组。一个项目做完了,那这个项目组就可以解散,组成另外的项目组,办公家具都可公用。在Linux下,通过pthread_create创建一个线程,也是调用do_fork。 不同的是,虽然新的线程在task列表会新创建一项,但是很多资源,例如文件描述符列表,进程空间,还是共享的,只不过多了一个应用而已。新的线程也可以通过已连接Socket处理请求,从而达到并发处理的目的。上面基于进程或线程模型,其实还是有问题的。新到来一个
TCP连接,就需要分配一个进程或线程。一台机器无法创建很多进程或线程。有个C10K(一台机器要维护1万个连接,就要创建1万个进程或线程,操作系统是无法承受的)。
Socket是文件描述符,因而某个线程盯的所有Socket,都放在一个文件描述符集合fd_set中,这就是项目进度墙,然后调用select函数来监听文件描述符集合是否有变化。一旦变化,就会依次查看每个文件描述符。那些发生变化的文件描述符在fd_set对应的位都设为1,表示Socket可读可写,从而可以进行读写操作,然后调用select进行下一轮。select函数还是有问题,因为每次Socket所在地恶文件描述符集合中Socket发生变化时,都需要通过轮询的方式来查看进度,这大大影响了一个项目组能支撑的最大项目数量。因而使用select, 能够同时盯的项目数量由FD_SETSIZE限制。如果改成事件通知的方式,情况就会好很多,项目组不需要通过轮询挨个盯着这些项目,而是当项目进度发生变化时,主动通知项目组,然后项目组再根据项目进展情况做相应的操作。能完成这件事的函数叫epoll, 它在内核中的实现不是通过轮询的方式,而是通过注册callback函数的方式,当某个文件描述符发送变化时,就会主动通知。如图所示,假设进程打开了Socket m n x等多个文件描述符,现在需要通过epoll来监听是否这些Socket都有事件发生。其中epoll_create创建一个epoll对象,也是一个文件,也对应一个文件描述符,同样对应着打开文件列表中的一项。在这项里有一个红黑树,在红黑树里,要保存这个epoll要监听所有Socket。当epoll_ctl添加一个Socket时,其实是加入这个红黑树,同时红黑树里面的节点指向一个结构,将这个结构挂在被监听的Socket事件列表中。到一个Socket来了一个事件时,可以从这个列表中得到epoll对象,并调用callback通知它。这种通知方式使得监听的Socket数据增加的时候,效率不会大幅度降低,能够同时监听的Socket的数目也非常多。上限为系统定义的,进程打开的最大文件描述符个数。因而,epoll被称为解决C10K问题的利器。登陆 www.163.com 这个 URL(同一资源定位符), 浏览器会将 www.163.com 这个域名发送给 DNS服务器, 让他解析IP地址. HTTP 先通过三次握手建立 TCP连接. (目前使用的HTTP协议大部分都是1.1, 是默认开启 Keep-Alive, 建立的TCP连接,可以在多次请求中复用)。
HTTP的报文可以分为3大部分
URL就是www.163.com,版本为HTTP 1.1, 方法有几种类型:
GET,服务器决定放回什么格式的内容(JSON字符串等)。POST 需要主动告诉服务器一些信息。一般会放在正文里。正文可以用各种各样的格式。常见的格式也是JSON.PUT 向指定资源位置上传最新内容. 但是,HTTP服务器往往不允许上传文件, 所以 PUT 和 POST 都变成要穿东西给服务器的方法。实际使用过程中, POST 往往用来创建一个资源,PUT 用来修改资源。DELETE 用来删除资源。key value 格式,通过冒号分隔。 里面保存一些非常重要的字段。例如:
Accept-Charset 表示客户端可以接收的字符集, 防止产生乱码。Content-Type 表示正文的格式,我们使用 POST 请求, 如果正文是JSON这个值设置为JSON.Cache-control 用来控制缓存If-Modified-Since
HTTP请求发送:HTTP协议是基于TCP协议的, 使用面向连接的方式发送请求, 通过stream二进制流的方式传给对方, 到了TCP层, 会把二进制流变成一个个报文段发送给服务器.
HTTP 返回报文也有一定格式, 也是基于HTTP 1.1.
HTTP请求的结果. 200意味一切顺利; 404 表示服务器无法响应这个请求。Retry-After表示告诉客户端应该在多长时间之后再尝试。503错误表示服务器暂时不再和这个值配合使用。Content-Type 表示返回的是HTML 还是 JSON.
HTTP 1.1在应用层以纯文本的形式进行通信。 每次通信都要带完整的HTTP头, 不考虑pipeline模式的话, 每次的过程总是一来一回, 在实时性, 并发性上都存在问题。HTTP 2.0会对HTTP头进行一定的压缩, 将原来每次都要携带得的大量的key value在两端建立一个索引表, 对相同的头只发送索引表中的索引。
Google 的 QUIC协议, 从 TCP 切换到 UDP, 有如下几个特点:
不再以四元组(
源IP,源端口,目的IP,目的端口)标识,而是以一个64位随机数作为ID来标识,而且UDP是无连接的,所以IP或端口变化时, 只要ID不变, 就不需要重新建立连接.
TCP为了保证可靠性,通过使用序号和应答机制类解决顺序和丢包问题。QUIC定义来一个offset的概念。QUIC是面向连接的,就像TCP一样是一个数据流, 发送的数据在这个数据流里有个偏移量offset, 可以通过offset查看数据发送到哪里, 只要offset包没有来就要重发。
有了自定义的连接和重传机制就可以解决
HTTP 2.0的多路复用问题。
TCP通过滑动窗口协议来实现流量控制。QUIC也通过window_update, 但是是适应自己的多路复用机制的, 不但在一个连接上控制窗口, 还在一个连接中的每个steam控制窗口。
加密分为两种方式:
对称加密算法中,加密和解密使用的密钥是相同的。因此,堆成加密算法要保证安全性的话,密钥要做好保密。只能让使用的人知道,不能对外公开。
加密使用的密钥和解密使用的密钥是不相同的。一把是作为公开的公钥,另一把是作为谁都不能给的私钥。公钥加密的信息,只能私钥才能解密。私钥加密的信息,只有公钥才能解密。客户端也需要有自己的公钥和私钥。
所以对称加密算法相比非堆成加密算法来说,效率高很多,性能也好,所以交互的场景下多用对称加密。
不对称加密也会有同样的问题,何如将不对称加密的公钥给对方?一种是放在一个公网的地址上,让对方下载;另一种是在建立连接时传给对方。两种方法有相同的问题,作为一个普通的网民,怎么鉴别公钥是对的,例如,我自己搭建一个网站cliu8site可以通过以下命令先创建私钥:
openssl genrsa -out cliu8siteprivate.key 1024
然后再根据私钥创建对应的公钥。
openssl rsa -in cliu8siteprivate.key -pubout -outcliu8sitepublic.pem
这个时候就需要权威部门介入,就像每个人都可以打印自己的简历,说自己是谁,但有公安局盖章的,就只有户口本,才能证明你是你。这个由权威部门颁发的称为证书(Certificate)。
证书(Certificate)里应该有公钥, 还有证书的所有者,就像户口本上有你的姓名和身份证号,说明这个户口本是你的;另外还有证书的发布机构和证书的有效期。这个证书是怎么生成的呢? 会不会有人假冒权威机构颁发证书?就像有假身份证,假户口一样。生成证书需要发起一个证书请求,然后将这个请求发给一个权威机构去认证,这个权威机构我们称为CA(Certifcate Authority), 证书请求可以通过命令生成:
openssl req -key cliu8siteprivate.key -new -out cliu8sitecertificate.req
将这个请求发给权威机构,权威机构会给这个证书盖章,我们称为签名算法. 只有使用 CA的私钥签名才能保证是真的权威机构的签名。签名算法大概的工作流程:对信息做一个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的签名,CA的公钥也需要更牛的CA给它签名,然后形成CA的证书,这样层层上去,直到全球皆知的几个著名大CA,称为root CA, 做最后的背书。通过这种 层层授信背书的方式,从而保证了非对称加密模式的正常运转。
除此之外,还有一种证书,称为Self Signed Certificate, 给人一种”我就是我,你爱信不信”。
我们知道,非对称加密在性能上不如对称加密,那是否能将两者结合?例如,公钥私钥主要用于传输对称加密的秘钥,而真正的双方大数据量的通信都是通过对称加密进行的。这就是HTTPS协议的总体思路:
当你登陆外卖网站时,由于是HTTPS,客户端会发送Client Hello消息到服务器,以明文传输TLS版本信息,加密套件候选列表,压缩算法候选列表等信息。另外,还会有一个随机数,在协商对称秘钥时使用。然后,外卖网站返回Server Hello消息告诉客户端,服务器选择使用的协议版本,加密套件,压缩算法等,还有一个随机数,用于后续的密钥协商。然后外卖网站会给一个服务端的证书说:”Server Hello Done, 我这里就这些信息了”。
你当然不相信这个证书,于是从自己信任的CA仓库中,拿CA的证书里面的公钥去解密外卖网站的证书。如果能够成功,说明外卖网站是可信的。这个过程中,可能会不断往上追溯CA, CA的CA, CA的CA的CA,反正直到一个授信的CA。
证书验证完毕之后,觉得这个外卖网站可信,于是客户端计算产生随机数字Pre-master, 发送Client Key Exchange, 用证书中的公钥加密,再发送给服务器,服务器可以通过私钥解密出来。
到目前为止,无论是客户端还是服务器,都有三个随机数,分别是:自己的,对端的,以及刚生成的Pre-master随机数。通过这三个随机数,可以在客户端和服务器产生相同的对称密钥。有了对称密钥,客户端就可以说:”Change Clipher Spec”, 咱们以后都采用协商的通信密钥和加密算法进行加密通信。然后发送一个Encrypted Handshake Message, 将已经商定好的参数等,采用协商密钥进行加密,发送给服务器用于数据与握手验证。同样,服务器也可以发送Change Cipher Spec, 说:”没问题,咱们以后都采用协商的通信密钥和加密算法进行加密通信”,并且也发送Encrypted Handshake Message的消息试试。当双方握手结束之后,就可以通过对称密钥进行加密传输。这个过程除了加密解密外,其他的过程和HTTP是一样的,过程也非常复制。
上面的过程只包含HTTPS的单向认证,也即客户端验证服务端的证书,是大部分的场景,也可以在更加严格安全要求的情况下,启用双向认证,双方互相验证证书。
有了加密和解密, 黑客截获包也打不开,但它可以发送N次。这个往往通过Timestamp和Nonce随机数联合起来,然后做一个不可逆的签名来保证。Nonce随机数保证唯一,或者Timestamp和Nonce合起来保证唯一,同样的,请求只接收一次,于是服务器多次受到相同的Timestamp和Nonce, 则视为无效即可。如果有人想篡改Timestamp和Nonce, 还有签名保证不可篡改性,如果改了用签名算法解出来,就对不上了,可以丢弃。
无论是直播还是点播,其实都是对于视频数据的传输。视频技术的三个名词系列:
AVI, MPEG, RMVB, MP4, MOV, FLV, WebM, WMV, ASF, MKV。例如RMVB和MP4是不是很熟悉?H.261, H.262, H.263, H.264, H.265。这里重点关注 H.264。MPEG-1, MPEG-2, MPEG-4, MPEG-7, MPEG听说过,但是后面的数字是怎么回事?视屏其实就是快速播放的一连串连续的图片。每一张图片,我们称为一帧。只要每秒钟帧的数据足够多,也即播放的足够快。比如每秒30帧,以人眼的敏感程度,是看不出这是一张张独立的图片的,这就是我们常说的帧率。每一张图片都是由像素组成的,假设为1024x768(这个像素不算多)。每个像素由RGB组成,每个8位,共24位。
我们来算一下,每秒钟的视频有多大?
30帧 x1024x768x24=566,231,040(Bits) =70,778,880(Bytes)
如果是一分钟就是
4,246,732,800Bytes, 已经是4G。这个数据量实在太大,根本没办法存储和传输。如果这样存储,硬盘很快就满了。这样传输,多少带宽也不够用。可以使用编码来用尽量上的Bit数保存视频,使播放时画面看起来仍然很精美。编码是一个压缩的过程。
视屏和图片压缩过程中的特点:
总之,用于编码的算法非常复杂,而且多种多样,但是编码过程其实都是类似的。
视频编码两大流派:
ITU(International Telecommunications Union)的VCEG(Video Coding Experts Group), 这个称为国际电联下的VCEG。既然是电信,他们最初做视频编码,主要侧重传输。名词系列二,就是这个组织定的标准。ISO(International Standards Organization)的MPEG(Moving Picture Experts Group), 这个是ISO旗下的MPEG, 本来是做视频存储的。例如,编码后保存在VCD和DVD中。当然后来也慢慢侧重视频传输了。名词系列三就是这个组织制定的标准。后来,ITU-T(国际电信联盟电信标准化部门,ITU Telecommunication Standardization Sector) 与 MPEG联合制定了H.264/MPEG-4 AVC,这才是我们这里关注的重点。进过编码之后,一帧帧的图像就变成一串串二进制,这个二进制可以放在一个文件里,按照一定的格式保存起来,这就是名词系列一。其实这些就是视频保存成文件的格式。例如,前几个字节是什么意义,后几个字节是什么意义,然后是数据,数据中保存的就是编码好的结果。
当然,这个二进制也可以通过某种网络协议进行封装,放在互联网上传输,这个时候就可以进行网络直播了。网络协议将编码好的视频流,从主播端推送到服务器,在服务器上有个运行了同样协议的服务端来接收这些网络包,从而得到里面的视频流,这个过程称为接流。
服务端接到视频流之后,可以对视频流进行一定的处理,例如转码(从一个编码格式,转成另一种格式)。因为观众使用的客户端千差万别,要保证他们都能看到直播。流处理完毕之后,就可以等待观众的客户端来请求这些视频流。观众的客户端请求的过程称为拉流。
如果有非常多的观众,同时看一个视频直播,那都从一个服务器上拉流,压力太大,因而需要一个视频的分发网络,将视频预先加载到就近的边缘节点,这样大部分观众看的视频,是从边缘节点拉取的,就能降低服务器的压力。
当观众的客户端将视频流拉下来之后,就需要进行解码, 也即通过上述过程的逆过程,将一串串看不懂的二进制再转变成一帧帧生动的图片,在客户端播放出来。
接下来,我们依次看下每个过程:
虽然我们说视频是一张张图片的序列,但如果每张图片都完整,就太大了,因而会将视频序列分成三种帧:
P帧表示的是这一帧跟之前的一个关键帧(或P帧)的差别,解码时需要用之前缓存的画面,叠加上和本帧定义的差别,生成最终画面。B帧记录的是本帧与前后帧的差别。要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面的数据与本帧数据的叠加,取得最终的画面。可以看出,
I帧最完整,B帧压缩率最高,而压缩后帧的序列,应该在IBBP的间隔出现的。这就是通过时序进行编码。在一帧中,分成多个片,每个片中分成多个宏块,每个宏块分成多个子块,这样将一张大的图分解成一个个小块, 方便进行空间上的编码。,
尽管时空非常立体的组成了一个序列,但是总归还是要压缩成一个二进制流。这个流是有结构的,是一个个网络提取层单元(NALU, Network Abstraction Layer Unit)。变成这种格式就是为了传输,因为网络上的传输,默认的是一个个的包,因而这里也就分成了一个个单元。
每个NALU首先是一个起始标识符,用于表示NALU之间的间隔;然后是NALU的头,里面主要配置NALU的类型;最终Payload里面是NALU承载的数据。在NALU头里,主要内的是容类型NAL Type:
0x07表示SPS, 是序列参数集,包括一个图像序列的所有信息,如图像尺寸,视频格式等。0x08表示PPS, 是图像参数集,包括一个图像的所有分片的所有相关信息,包括图像类型,序列号等。在传输视频流之前,必须要传输这两类参数,不然无法解码。为了保证容错性,每个
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为啥需要建立一个单独的连接?因为它们需要商量一些事情,保证以后传输能正常进行,主要就是两个事情:1.版本号,如果客户端服务器的版本号不一致,则不能工作。2.时间戳,视频播放中,时间是很重要的,后面的数据流互通时,经常要带上时间戳的差值,因而一开始双方就要知道对方的时间戳。
未来沟通这些事情,需要发送六条消息;客户端发送C0,C1,C2, 服务器发送S0,S1,S2。首先客户端发送C0表示自己的版本号,不必等对方的回复,然后发送C1表示自己的时间戳。服务器只有在收到C0时,才能返回S0,表明自己的版本号,如果版本号不匹配,可以断开连接。服务器发送完S0后,也不用等什么,就直接发送自己的时间戳S1。客户端收到S1时,发一个知道对方时间戳的ACK C2。同理服务器收到C1时,发送一个知道对方时间戳的ACK S2。
握手之后,双方需要互相传递一些控制信息,例如Chunk块的大小,窗口大小等;真正传输时,还需要创建一个流Stream, 然后通过这个Stream来推流publish。推流的过程就是将NALU放在Message里面发送,这个也称为RTMP Packet包。Message的格式如下:
发送时去掉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。
就这样数据就源源不断到达流媒体服务器,整个过程就像这样。
这时,大量观看直播的观众就可以通过RTMP协议从流媒体服务器上拉取,但这么多的用户量,都去同一个地方拉取,服务器压力会很大,而且用户分布在全国甚至全球,如果都去统一的一个地方下载,也会时延比较长,需要有分发网络。分发网络分为中心和边缘两层。边缘层服务器部署在全国各地及横跨各大运营商里,和用户距离很近。中心层是流媒体服务集群,负责内容的转发。智能负载均衡系统,根据用户的地理位置信息,就近选择边缘服务器,为用户提供 推/拉 流服务。中心层也负责转码服务,例如,把RMTP协议的码流转换为HLS码流。
这套机制在后面的DNS, HTTPDNS, CDN会详细描述。
先读到的是H.264的解码参数,例如SPS和PPS,然后对收到的NALU组成一个个帧,进行解码,交给播放器播放,一个绚丽多彩的视频画面就出来了。
下载一部电影,最简单的方式就是通过HTTP进行下载,但是通过浏览器下载时,只要文件稍微大点,下载速度就奇慢无比。还有种下载文件的方式,就是通过FTP(文件传输协议), FTP采用两个TCP连接来传输一个文件。
FTP的端口(21), 客户端则主动发起连接。该连接将命令从客户端传给服务器,并传回服务器的应答。常用的命令有: list(获取文件目录), reter(取一个文件), store(存一个文件)。每传输一个文件,都要建立一个全新的数据连接。FTP有两种工作模式,分为主动模式(PORT)和被动模式(PASV),这些都是站在FTP服务器的角度来说的:
主动模式
客户端随机打开一个大于1024的端口N, 向服务器的命令端口21发起连接,同时开放N+1端口监听,并向服务器发出port N+1命令,由服务器从自己的数据端口20, 主动连接到客户端指定的数据端口N+1
被动模式
当开启一个FTP连接时,客户端打开两个任意的本地端口(大于1024)和N+1.第一端口连接服务器的21端口,提交PASV命令。然后,服务器会开启一个任意的端口P(大于1024),返回”227 entering passive mode”消息,里面有FTP服务器开放的用来进行数据传输的端口。客户端收到消息取得端口号之后,会通过N+1号端口连接服务器端口P, 然后在两个端口之间进行数据传输。
无论是HTTP方式,还是FTP方式,都有一个比较大的缺点,就是难以解决单一服务器的带宽压力,因为它们使用的都是传统的客户端服务器的方式。 后来,一种创新的,称为P2P的方式流行起来。P2P(peer-to-peer)资源开始并不集中地存储在某些设备上,而是分散地存储在多台设备上。这些设备我们称为peer。想要下载一个文件时,只要得到那些已经存在文件的peer,并和这些peer之间,建立点对点的连接,而不需要到中心服务器上,就可以就近下载文件。一旦下载了一个文件,你也会称为peer中的一员,你旁边的那些机器,也可能会选择从你这里下载文件,所以当你使用P2P软件时,例如BitTorrent, 往往能够看到,既有下载流量,也有上传流量,也即你自己也加入了这个P2P网络,自己从别人那里下载,同时也提供给其他人下载。这种方式,参与的人越多,下载速度越快。
要想知道哪些peer有这些文件,需要用到种子(.torrent文件), .torrent文件由两部分组成:
BitTorrent(简称BT)协议把一个文件分成很多个小段,然后分段下载SHA-1哈希值拼在一起。下载时,
BT客户端首先解析.torrent文件,得到tracker地址,然后连接tracker服务器。tracker服务器回应下载者的请求,将其他下载者(包括发布者)的IP提供给下载者。下载者再连接其他下载者,根据.torrent文件,两者分别告知对方自己已经有的块,然后交换对方没有的数据。此时不需要其他服务器参与,并分散了单个线路上的数据流量,减轻了服务器负担。
下载者每得到一个块, 需要算出下载块的
Hash验证码,并与.torrent文件中的对比。如果一样,则说明块正确,不一样则需重新下载这个块。这规定是为了解决下载内容的准确性问题。从这个过程也可以看出,这种方式特别依赖tracker。tracker需要收集下载者信息的服务器,并将此信息提供给其他下载者,使下载者们相互连接起来传输数据。虽然下载的过程是非中心化的,但加入这个P2P网络时,都需要借助tracker中心服务器,这个服务器是用来登记有哪些用户在请求哪些资源。这种工作方式有一个弊端,一旦tracker服务器出现故障或线路遭到屏蔽,BT工具就无法正常工作。
DHT(Distributed Hash Table): 每个加入这个DHT网络的人,都要负责存储这个网络里的资源信息和其他成员的联系信息,相当于所有人一起构成一个庞大的分布式存储数据库。Kademlia协议是一种著名的DHT协议:
任何一个
BitTorrent启动之后,都有两个角色。一是peer,监听一个TCP端口,用来上传和下载文件,这个角色表明,这里有某个文件。另外的角色是DHT node, 监听一个UDP端口,通过这个角色,这个节点加入一个DHT网络。
在
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和node E的ID值很近, 所以node D也知道。当然,文件2本身没有必要一定在node D和node E里,但是碰巧在node 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问。在DHT网络中,每个node都保存一定的联系方式,但肯定没有node的所有联系方式。DHT网络中,节点之间通过互相通信,也会交流联系方式,也会删除联系方式。有个理论是,社交网络中,任何两个人直接的距离不超过6。所以,node new想联系node C, 就去万能的朋友圈问。如果找不到node C, 也能找到和node 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网络中,会有节点告诉它,你既然加入这个网络,你也有责任知道某些文件的下载地址。一切都是分布式的。
里面有几个细节问题:
节点ID是一个随机选择的160bits(20字节)空间,文件的哈希也使用这样的160bits空间。Kademlia网络中,距离是通过异或(XOR)计算的
01010与01000的距离就是两个ID之间的异或值,为00010 = 2, 高位不同表示距离更远,低位不同表示距离更近,总的距离为最后计算后的值。位置近不算近,只有ID近才算近。
DHT网络是按照距离分层的,Kademlia算法中,每个节点只有4个指令:
节点ID查找一个节点KEY查找数据DHT网络中,怎么更新节点数据?
bucket里的节点,都按最后一次接触时间倒序排列,相当于朋友圈最近联系的人是关系最好的k-bucket中;如果在,将他移到k-bucket列表的最底(最新的位置); 如果不在,新的联系人要不要加到通讯录里?如果通讯录满了,PING一下最旧的节点。如果PING通了,将旧节点移到列表最底,并丢弃新节点,如果PING不通,删除旧节点,并将新节点加入列表。这个机制保证任意节点介入和离开都不影响整体网络。
DNS服务器一定要设置成高可用,高并发和分布式的。树状层级结构:
DNS服务器: 返回顶级域 DNS 服务器的 IP 地址DNS服务器: 返回权威DNS 服务器的 IP 地址DNS服务器: 返回相应主机的IP地址为了提高DNS解析性能, 很多网络多会就近部署DNS缓存服务器。DNS的解析流程如下:
DNS请求,问 http://www.163.com 的 IP 是啥?并发给本地域名服务器(本地DNS: 如果通过DHCP配置,本地DNS由网络服务商ISP,如电信,移动等自动分配,通常在网络服务商的某个机房)DNS收到来自客户端的请求。这台服务器上缓存一张域名与与之对应IP地址的大表格, 如果能找到,直接返回IP地址. 如果没有,本地DNS会去根域名服务器找, 根域名服务器是最高层次, 全球共有13套。它不直接用于域名解析,但能知名一条道路。DNS收到来自本地DNS的请求,发现后缀是.com, 发现是由.com区域管理, 获得顶级域名服务器地址DNS转向问顶级域名服务器,顶级域名服务器就是比如.com, .net, .org 这些一级域名,它负责管理二级域名, 比如163.com。DNS服务器地址DNS转向权威DNS服务器,163.com 的权威DNS服务器是域名解析结果的源出处DNS服务器查询后将相应的IP地址 X.X.X.X告诉本地DNS.DNS再将IP地址返回客户端,客户端和目标建立连接.站在客户端的角度, 上面的过程是一次DNS递归查询过程。这个过程中,DNS 除了可以通过名称映射为IP地址,还可以实现负载均衡.
DNS首先可以做内部负载均衡 , 还可以实现全局负载均衡。
DNS访问数据中心中对象存储上的静态资源:假设全国有多个数据中心,托管在多个运营商, 每个数据中心有三个可用区(
Available Zone)。对象存储通过跨可用区部署,实现高可用性。 在每个数据中心中, 都至少部署两个内部负载均衡器,内部负载均衡器后面对接多个对象存储的前置服务器。
- 当一个客户端要访问
object.yourcompany.com时,需要将域名转换为IP地址进行访问,所以要请求本地DNS解析器本地DNS解析器先查看本地的缓存是否有这个记录。如果有直接使用- 如果本地无缓存,则需要请求
本地DNS服务器本地DNS服务器一般部署在你的数据中心或者你所在运营商的网络中,本地DNS服务器也需要看本地是否有缓存, 如果有则返回- 如果本地没有,
本地DNS才需要递归从根DNS服务器查到.com的顶级域名服务器,最终查到yourcompany.com的权威DNS服务器给本地DNS服务器,权威DNS服务器会返回真实要访问的IP地址
DNS有两项功能:
有时DNS也会指错路:当我们发出请求解析DNS时,首先会先连接到运营商本地的DNS服务器,有这个服务器帮我们去整颗DNS树上进行解析,然后将解析结果返回给客户端。但是本地导游有自己的”小心思”:
DNS服务器,而是访问过一次就把结果缓存到自己本地,当其他人来时,直接返回缓存数据。DNS服务器中查找,只不过不是每次都要查找。可以说这还是大导游,大中介。还有一些小导游,小中介,有了请求之后,直接转发给其他运营商去做解析,自己只是外包出去。
这样的问题是,如果是
A运营商的客户,访问自己运营商的DNS服务器,如果A运营商去权威DNS服务器查询的话,会返回一个部署在A运营商的网站地址,这样针对相同运营商的访问,速度就会快很多。
但是
A运营商偷懒,将解析的请求转发给B运营商,B运营商去权威DNS服务器查询的话,会返回一个在B运营商的网站地址,客户每次都要跨运营商访问,速度会慢很多
NAT(网络地址转换), 使得从这个网管出去的包,都换成新的IP地址, 当然请求返回时,在这个网关,再将IP地址转换回去,所以对于访问来说没有任何问题。但是一旦做了网络地址转换,权威DNS服务器,就没办法通过这个地址来判断客户到底是来自哪个运营商,而且极有可能因为转换后的地址误判运营商,导致跨运营商的访问。DNS服务器由不同地区,不同运营商独立部署的。对域名解析缓存的处理上,实现策略也有区别,有的会偷懒,忽略域名解析结果的TTL时间限制,在权威DNS服务器解析变更时,解析结果在全网生效的周期非常漫长。但有时,在DNS的切换中,场景多生效时间要求比较高。
例如双机房部署时,跨机房的负载均衡和容灾多使用
DNS来做。当一个机房出问题之后,需要修改权威DNS, 将域名指向新的IP地址,如果更新太慢,很多用户会出现访问异常。
DNS查询过程需要递归遍历多个DNS服务器,才能获得最终的解析结果,这会带来一定的时延,甚至会解析超时。HTTPDNS其实就是不走传统的DNS解析,而是自己搭建基于HTTP协议的DNS服务器集群,分布在多个地点和多个运营商。当客户需要DNS解析时,直接通过HTTP协议进行请求这个服务器集群,得到就近的地址。
这就相当于每家基于HTTP协议,自己实现自己的域名解析,自己做一个自己的地址簿,而不使用统一的地址簿。但是默认的域名解析都是走DNS的,因而使用HTTPDNS需要绕过默认的DNS路径,就不能使用默认的客户端。使用HTTPDNS的,往往是手机应用,需要在手机端嵌入支持HTTPDNS的客户端SDK。
通过自己的HTTPDNS服务器和自己的SDK,实现了从依赖本地导游,到自己上网查询做旅游攻略,进行自由行。这样就能避免依赖导游,而导游又不专业的尴尬。
HTTPDNS的工作模式:
在客户端
SDK里动态请求服务器,获取HTTPDNS服务器的IP列表,缓存到本地。随着不断解析域名,SDK也会在本地缓存DNS域名解析的结果。当手机应用要访问一个地址时,首先查看是否有本地缓存,如果有就直接返回。这个缓存和本地DNS的缓存不一样的是,这个是手机应用自己做的,而不是这个运营商统一做的。如何更新,何时更新,手机应用的客户端可以和服务器协调来做这件事情。
如果本地没有,就需要请求
HTTPDNS的服务器,在本地HTTPDNS服务器的IP列表中,选择一个发出HTTP的请求,会返回一个要访问网站的IP列表。
curl http://106.2.xxx.xxx/d?dn=c.m.163.com
{
"dns": [
{
"host":"c.m.163.com",
"ips":["223.252.199.12"],
"ttl":300,
"http2":0
}],
"client": {
"ip":"106.2.81.50",
"line":269692944
}
}
手机客户端自然知道手机在哪个运营商,哪个地址。由于是直接的HTTP通信,HTTPDNS服务器能够准确知道这些信息,因而可以做精准的全局负载均衡。
当然,当所有这些都不工作时,可以切换到传统的
LocalDNS来解析,那HTTPDNS是如何解决上面的问题的?
其实归结起来就是两大问题。1.解析速度和更新速度的平衡问题 2.智能调度问题,对应的解决方案是
HTTPDNS的缓存设计和调度设计。
HTTPDNS的缓存设计: 解析DNS过程复杂,通信次数多,对解析速度造成很大影响。为了加快解析,因而有了缓存,但是这又回产生缓存更新速度不及时的问题。最要命的是,这两个方面都掌握在别人的手中,也即本地DNS服务器手中,它不会为你定制。而HTTPDNS就是将解析速度和更新速度全部掌握在自己手中。一方面,解析的过程,不需要本地DNS服务器递归调用一大圈,一个HTTP的请求直接搞定,要实时更新时,马上就能起作用;另一方面为了提高解析速度,本地也有缓存,缓存实在客户端SDK维护的,过期时间,更新时间都可以自己控制。
HTTPDNS的缓存设计策略也是咱们做应用架构中常用的缓存设计模式,也即分为客户端,缓存,数据源三层:
Tomcat, Redis, MySQLHTTPDNS来讲,就是手机客户端,DNS缓存,HTTPDNS服务器。只要是缓存模式,就存在缓存过期,更新,不一致的问题,解决思路也很像。
例如
DNS缓存在内存中,也可以持久化到存储上,从而APP重启之后,能够尽快从存储中加载上次累计的经常访问的网站的解析结果,就不需要每次全部解析一遍,再变成缓存。这有点像Redis是基于内存的缓存,但同样提供持久化的能力,使得重启或主备切换时,数据不会完全丢失。
SDK中缓存会严格按照缓存过期时间,如果缓存没有命中,或者已经过期,而且客户端不允许使用过期的记录,则会发起一次解析 ,保障记录是更新的。解析可以同步进行,也可以直接调用HTTPDNS接口,返回最新的记录,更新缓存;也可以异步进行,添加一个解析任务到后台,由后台任务调用HTTPDNS接口。
同步更新的有点是实时性好,缺点是如果有多个请求都发现过期时,同时会请求
HTTPDNS多次。同步更新的方式对应到应用架构中缓存的Cache-Aside 机制, 也即先读缓存,不命中读数据库,同时将结果写入缓存。
异步更新的优点是,可以同时在多个请求都发现过期的情况下,合并为一个对于
HTTPDNS的请求任务,只执行一次,减少HTTPDNS的压力。同时可以在即将过期时,就创建一个任务进行预加载,防止过期之后再刷新。它的缺点是当前请求拿到过期数据时,如果客户端允许使用过期数据,需要冒一次险。如果过期数据还能请求,就没问题;如果不能请求,则失败一次,等下次缓存更新后,再请求才能成功。
异步更新机制对应到应用框架中缓存的Refresh-Ahead 机制, 即业务仅访问缓存,当过期时定期刷新。在著名的应用缓存Guava Cache中,有个RefreshAfterWrite机制,对于并发情况下,多个缓存访问不命中从而引发并发回源的情况。可以采用只有一个请求回源的模式。在应用架构的缓存中,也常常用数据预热或预加载机制。
由于客户端嵌入SDK, 因而就不会因为本地DNS的各种缓存,转发,NAT,让权威DNS服务器误会客户端所在的位置和运营商,可以拿到第一手资料。在客户端,可以知道手机是哪个国家,哪个运营商,哪个省,甚至哪个市,HTTPDNS服务器可以根据这些信息,选择最佳的服务节点返回。
如果有多个节点,还会考虑错误率,请求时间,服务器压力,网络状况等,进行综合选择,而非仅仅考虑地理位置。当有一个节点宕机或性能下降时,可以尽快进行切换。要做到这一点,需要客户端使用HTTPDNS返回的IP访问业务应用。客户端SDK回收集网络请求数据,如错误率,请求时间等网络请求质量数据,并发送到统计后台,进行分析,聚合,以此查看不同IP的服务质量。
在服务端, 应用可以通过调用HTTPDNS的管理接口,配置不同服务质量的优先级,权重。HTTPDNS会根据这些策略综合地理位置和线路状况算出一个排序,优先访问当前那先优质的,时延低的IP地址。HTTPDNS通过智能调度之后返回的结果,也会缓存在客户端。为了不让缓存使得调度失真,客户端可以根据不同的移动网络运营商WIFI的SSID来分维度缓存。不同运营商或WIFI解析出来的结果会不同。
当一个用户想访问一个网站时,指定这个网站的域名,DNS就会将这个域名解析为地址,然后用户请求这个地址,返回一个网页。那这里面还有没有可以优化的地方?
例如去电商网站下单买个东西,这个东西一定要从电商总部的中心仓库送过来?原来基本是这样的,每一单都是单独配送,所以可能要很久才能收到东西。但是后来电商网站的物流在全国各地建立了很多仓库,而不是只有总部的中心仓库才可以发货。
我们先说,我们的网站访问可以借鉴”就近配送”这个思路: 全球有这么多的数据中心,无论在哪里上网,临近不远的地方基本都有数据中心。是不是可以在这些数据中心里部署几台机器,形成一个缓存的集群来缓存部分数据,那么用户访问数据时,就可以就近访问?
当然是可以的,这些分布在各个地方的各个数据中心的节点,就称为边缘节点。由于边缘节点数目比较多,但是每个集群规模比较小,不可能缓存下来所有东西,因而可能无法命中,这样就会在边缘节点之上,有区域节点,规模就要更大,缓存的数据会更多,命中的概率也就更大。在区域节点之上是中心节点,规模更大,缓存数据更多。如果还不命中,就只好回源网站访问了。
这就是CDN的分发系统架构。CDN系统的缓存,也是一层层的,能不访问后端真正的源,就不打扰他。这也是电商网站物流系统的思路。有了这个分发系统之后,接下来就是,客户端如果找到相应的边缘节点进行访问?
还记得我们讲过的基于DNS的全局负载均衡吗?这个负载均衡主要用来选择一个就近的同样运营商的服务器进行访问。你会发现,CDN分发网络也是一个分布在多个区域,多个运营商的分布式系统,也可以用相同的思路选择最合适的边缘节点。
在没有CDN的情况下, 用户向浏览器输入www.web.com这个域名,客户端访问本地DNS服务器时,如果本地DNS服务器有缓存,则返回网站地址;如果没有,递归查询到网站的权威DNS服务器,这个权威DNS服务器时负责web.com的,它回返回网站的IP地址。本地DNS服务器缓存下IP地址,将IP地址返回,然后客户端直接访问这个IP地址,就访问到了这个网站。
然而有了CDN之后,情况发生了变化。在web.com这个权威DNS服务器上,会设置一个CNAME别名,指向另外一个域名www.web.cdn.com,返回给本地DNS服务器。当本地DNS服务器拿到这个新的域名时,需要继续解析这个新的域名。这时,再访问的就不是web.com的权威DNS服务器了,而是web.cdn.com的权威DNS服务器,这是CDN自己的权威DNS服务器,在这个服务器上,还是会设置一个CNAME, 指向另外一个域名,也即CDN网络的全局负载均衡器。
接下来,本地DNS服务器去请求CDN的全局负载均衡器解析域名,全局负载均衡器会为用户选择一台合适的缓存服务器提供服务,选择的依据包括:
IP地址,判断哪一台服务器距用户最近URL中携带的内容名称,判断哪一台服务器上有用户所需的内容基于以上这些条件,进行综合分析之后,全局负载均衡器会返回一台缓存服务器的IP地址。本地DNS服务器缓存这个IP地址,然后将IP返回给客户端,客户端去访问这些边缘节点,下载资源。缓存服务器响应用户请求,将用户所需内容传送到用户终端。如果这台缓存服务器上并没有用户想要的内容,那么这台服务器就要向它的上一级缓存服务器请求内容,直至追溯到网站的源服务器将内容拉到本地。
CDN可以进行缓存的内容有很多种:保质期长的日用品比较容易缓存,因为不容易过期,对应到就像电商仓库系统里,就是静态页面,图片等,因为这些东西也不怎么变,所以适合缓存。
还记得这个接入层缓存的架构吗? 在进入数据中心时,我们希望通过最外层接入层的缓存,将大部分静态资源的访问拦在边缘。而CDN则是更进一步,将这些静态资源缓存到离用户更近的数据中心外。越接近客户,访问性能越好,时延越低。
但是静态内容中,有一种特殊的内容,也大量使用CDN, 这就是前面讲过的流媒体。CDN支持流媒体协议, 例如前面讲过的RTMP协议。在很多情况下,这相当于一个代理,从上一级缓存读取内容,转发给用户。由于流媒体往往是连续的,因而可以进行预先缓存的策略,也可以预先推送到用户的客户端。
对于静态页面来说,内容的分发往往采取拉取的方式,也即当发现微命中时,再去上一级进行拉取。但是,流媒体数据量大,如果出现回源, 压力回比较大,所以往往采取主动推送的模式,将热点数据主动推送到边缘节点。
对于流媒体来说,很多CDN还提供预处理服务,也即文件在分发之前,经过一定的处理。例如将视频转换为不同的码流,以适应不同网络带宽的用户需求;再比如对视频进行分片,降低存储压力,也使得客户端可以选择使用不同的码率加载不同的分片。
对于流媒体CDN来说,有个关键的问题是防盗链问题。因为视频是要花大价钱买版权的,为了挣点钱, 收点广告费,如果流媒体被其他的网站盗走,在人家的网站播放,那就损失惨重了;最常用也是最简单的方式就是HTTP头的refer字段,当浏览器发送请求时,一般会带上refer, 告诉服务器是从哪个页面链接过来的,服务器基于此可以获得一些信息用于处理。如果refer信息不是来自本站,就阻止访问或跳到其他链接。
refer的机制相对比较容易破解,所以还需要配合其他的机制
一种常用的机制是时间戳防盗链。使用CDN的管理员可以在配置界面上和CDN厂商约定一个加密字符串。客户端取出当前的时间戳,要访问的资源及其路径,连同加密字符串进行签名算法得到一个字符串,然后生成一个下载链接,带上这个签名字符串和截止时间戳去访问CDN。在CDN服务器,根据取出过期时间和当前CDN节点时间进行比较,确认请求是否过期。然后CDN服务端有了资源及路径,时间戳,以及约定的加密字符串,根据相同的签名算法计算签名,如果匹配则一致,访问合法,才会将资源返回给客户。
然而比如在电商仓库中,我在前面提过,有关生鲜的缓存就是非常麻烦的,这对应的是动态的数据。比较难以缓存。现在也有动态CDN, 主要有两种模式:
CDN的网络,对路径进行优化。因为CDN节点较多,能够找到离用户很近的边缘节点。中间的链路完全由CDN来规划,选择一个更加可靠的路径,使用类似专线的方式进行访问。对于常用的TCP连接,在公网上传输时经常会丢数据,导致TCP的窗口始终很小,发送速度上不去。根据前面的TCP流量控制和拥塞控制的原理,在CDN加速网络中可以调整TCP的参数,使得TCP可以更加激进地传输数据。可以通过多个请求复用一个连接,保证每次动态请求到达时。连接都已经建立了,不必临时三次握手或建立过多的连接,增加服务器的压力。另外,可以通过对传输数据进行压缩,增加传输效率。所有这些手段就像冷链运输,整个物流优化了,全程冷冻高速运输。不管生鲜是从你旁边的超市送到你家的,还是从产地送的,保证到你家是新鲜的。无论是看新闻,下订单,看视频,下载文件,最终访问的目的地都在数据中心里。数据中心是一个大杂烩,几乎用到前面所学的所有知识。讲办公室网络时,我们知道办公室里有很多电脑,如果要访问外网,需要经过一个叫网关的东西,而网关往往是一个路由器。
数据中心里也有一大堆电脑,但它和我们办公室里的笔记本或台式机不一样。数据中心里是服务器。服务器被放在一个个叫做机架(Rack)的架子上。数据中心的入口和出口也是路由器,由于在数据中心的边界,就像在一个国家的边境,称为边界路由器(Border Router)**。为了高可用,边界路由器会有多个。
一般家里只会连接一个运营商的网络,而为了高可用,为了一个运营商出问题时,还可以通过另外一个运营商提供服务,所以数据中心的边界路由器会连接多个运营商网络。既然是路由器,就需要跑路由协议,数据中心往往就是路由协议中的自治区域(As)。数据中心里的机器要想访问外面的网站,数据中心里也是有对外提供服务的机器,都可以通过BGP协议,获取内外互通的路由信息。这就是我们常听到的多线BGP的概念。
如果数据中心非常简单,没几台机器,那就像家里或宿舍一样,所有的服务器都直接连到路由器上就可以了。但是数据中心里往往有非常多的机器。当塞满一机架时,需要有交换机将这些服务器连接起来,可以互相通信。
这些交换机往往是放在机架顶端的,所以经常称为TOR(Top Of Rack)交换机。这一层的交换机常常称为接入层(Access Layer)。注意这个接入层和原来讲过的应用的接入层不是一个概念。
当一个机架放不下时,就需要多个机架,还需要有交换机将多个机架连接在一起。这些交换机对性能的要求更高,带宽也更大。这些交换机称为汇聚层交换机(Aggregation Layer)。数据中心里每一个连接都是需要考虑高可用的。这里首先要考虑的是,如果一台机器只有一个网卡,上面连着一条网线,接入到TOR交换机上。如果网卡坏了,或者不小心网线掉了,机器就上不去了。所以,需要至少两个网卡,两个网线插到TOR交换机上,但是两个网卡要工作的像一张网卡一样,这就是常说的网卡绑定(bond)。
这就需要服务器和交换机都支持一种协议LACP(Link Aggregation Control Protocol)。他们互相通信,将多个网卡聚合称为一个网卡,多个网线聚合成一个网线,在网线之间可以进行负载均衡,也可以为了高可用做准备。
网卡有了高可用保证,但交换机还有问题。如果一个机架只有一个交换机,它挂了,那整个机架都不能上网了。因而TOR交换机也需要高可用,同理接入层和汇聚层的连接也需要高可用性,也不能单线连着。最传统的方法是,部署两个接入交换机,两个汇聚交换机。服务器和两个交换机都连接,接入交换机和两个汇聚交换机都连接,当然这样会形成环,所以需要启用STP协议,去除环,但是这样两个汇聚就只能一主一备。STP协议里只有一条路会其作用。
交换机有一种技术叫做堆叠,所以另一种方法是,将多个交换机形成一个逻辑的交换机,服务器通过多根线分配连到多个接入层交换机上,而接入层交换机多根线分别连接到多个交换机上,并且通过堆叠的私有协议,形成双活的连接方式。
由于对带宽要求更大,而且挂了影响也更大,所以两个堆叠可能就不够了,可以就会有更多的,比如四个堆叠为一个逻辑的交换机。汇聚层将大量的计算节点相互连接在一起,形成一个集群。在这个集群里,服务器之间通过二层互通,这个区域常称为一个POD(Point Of Delivery),有时候也称为一个可用区(Available Zone)。
当节点数目再多时,一个可用区放不下,需要将多个可用区连在一起,连接多个可用区的交换机称为核心交换机。
核心交换机吞吐量更大,高可用要求更高,肯定需要堆叠,但往往仅仅堆叠,不足以满足吞吐量,因而还是需要部署多组核心交换机。核心和汇聚交换机之间为了高可用,也是全互联模式的。
这时出现环路怎么办?:一种方式是,不同的可用区在不同的二层网络,需要分配不同的网段。汇聚和核心之间通过三层网络互通,二层都不在一个广播域里,不会存在二层环路的问题。三层有环是没有问题的,只要通过路由协议选择最佳的路径就可以了。那为啥二层不能有环路,三层可以呢?
如图,核心层和汇聚层之间通过内部的路由协议OSPF, 找到最佳的路径进行访问,而且还可以通过ECMP等价路由,在多个路由之间进行负载均衡和高可用。但是随着数据中心里的机器越来越多,尤其是有了云计算,大数据,集群规模非常大,而且都要求在一个二层网络里。这就需要二层互连从汇聚层上升为核心层, 也即在核心以下,全部是二层互连,全部在一个广播域里,这就是常说的大二层。
如果大二层横向流量不大,核心交换机数目不多,可以做堆叠,但如果横向流量很大,仅仅堆叠满足不了,就需要部署多组核心交换机,而且要和汇聚层进行全互连。由于堆叠只解决一个核心交换机组内的无环问题,而组之间全互连,还需要其他机制进行解决。如果是STP,那部署多组核心无法扩大横向流量的能力,因为还是只有一组起作用。
于是大二层就引入了TRILL(Transparent Interconnection of Lots of Link),即多链路透明互联协议。它的基本思路是二层环有问题,三层环没有问题,就把三层的路由能力模拟在二层实现。运行TRILL协议的交换机称为RBridge, 是具有路由转发特性的网桥设备,只不过这个路由是根据MAC地址来的,不是根据IP来的。
RBridage之间通过链路状态协议运作。通过它可以学习整个大二层的拓扑,知道访问哪个MAC应该从哪个网桥走;还可以计算最短路径,也可以通过等价的路由进行负载均衡和高可用性。
TRILL协议在原来的MAC头外加上自己的头,以及外层的MAC头。TRILL头里的Ingress RBridge,有点像IP头里的源IP地址,Egress RBridge是目标IP地址,这两个地址是端到端的,在中间路由时,不会发生改变。而外层MAC, 可以有吓一跳的Bridge, 就像路由的下一跳,也是通过MAC地址来呈现的。
上图所示的过程,有一个包要从主机A发送到主机B, 中间经过RBridge1, RBridge2, RBridge X等,直到RBridge 3。在RBridge 2收到的包里,分内外两层,内层就是传统的主机A和主机B的MAC地址以及内层的VLAN。
在外层首先加上一个TRILL头,里面描述这个包从RBridge 1进来的,要从RBridge 3出去,并且像三层的IP地址一样有跳数。然后再外面,目的MAC是RBridge 2, 源MAc是RBridge 1, 以及外层的VLAN。
当RBridge 2收到这个包之后,首先看MAC是否是自己的MAC, 如果是,要看自己是不是Egress RBridge,也即是不是最后一跳;如果不是,查看跳数是不是大于0, 然后通过类似路由查找的方式找到下一跳RBridge X, 然后将包发出去。
RBridge 2发出去的包,内层的信息是不变的,外层的TRILL头里。同样,描述这个包从RBridge 1进来,要从RBridge 3出去,但是跳数要-1。外层的目标MAC变成RBridge X, 源MAC变成RBridge 2。
如此一直转发,直到RBridge 3, 将外层解出来,发送内层的包给主机B。
对于大二层的广播包,也需要通过分发树的技术来实现。我们知道STP是将一个有环图,通过去掉边形成一棵树,而分发树是一个有环的图形成多棵树,通过的树有不同的VLAN,有的广播包从VLAN A广播,有的从VLAN B广播,实现负载均衡和高可用。
核型交换机之外,就是边界路由器了。至此从服务器到数据中心边界的层次情况已经清楚了。在核心交换机上,往往会挂一些安全设备,例如入侵检测,DDoS防护等。这是整个数据中心的屏障,防止来自外来的攻击。核心交换机上往往还有负载均衡器。
在有的数据中心里,对于存储设备,还会有一个存储网络,用来连接SAN和NAS。但对于新的云计算来说,往往不使用传统的SAN和NAS,而使用部署在x86机器上的软件定义的存储,这样存储也是服务器了,而且可以和计算节点融合在一个机架上,从而更加有效率,也就没有了单独的存储网络了。
这是一个典型的三层网络结构。这里的三层不是指IP层,而是指接入层,汇聚层,核心层三层。这种模式非常有利于外部流量请求到内部应用。这个类型的流量,是从外到内或从内到外,对应到上图,就是从上到下,从下到上,上北下南,所以称为南北流量。
但是随着云计算和大数据的发展,节点之间的交互越来越多,例如大数据计算经常在不同节点将数据拷来拷去,这样需要经过交换机,使得数据从左到右,从右到左,左西右东,所以称为东西流量。为了解决东西流量问题,演进出了叶脊网络(Spine/Leaf):
L2/L3网络的分界点在叶子交换机上,叶子交换机之上是三层网络。ECMP动态选择多条路径。脊交换机现在只是为叶子交换机提供一个弹性的L3路由网络。南北流量可以不用直接从脊交换机发出,而是通过与Leaf交换机并行的交换机,再接到边界路由器出去。传统的三层网络架构是垂直结构,而叶脊网络架构是扁平的结构,更易于水平扩展。
有的公司有多个数据中心,需要将多个数据中心连接起来,或需要办公室和数据中心连接起来要怎么做?
VPN来连接,这种方法比较折中, 安全又不贵VPN(Virtual Private Network, 虚拟专用网)就是利用开放的公众网络,建立专用数据传输通道,将远程的分支机构,移动办公人员等连接起来。
VPN通过隧道技术在公众网络上仿真一条点到点的专线,是通过利用一种协议来传输另外一种协议的技术,这是涉及三种协议: 乘客协议, 隧道协议 和 承载协议。
我们以IPsec协议来说:知道如何通过自驾进行海南游吗?这其中,车怎么通过琼州海峡?这里用到轮渡,其实就是隧道协议。在广州这边开车是有”协议”的,例如靠右行驶,红灯停,绿灯行,这就相当于”被封装”的乘客协议。当然在海南那面,开车也是同样的协议。这就相当于需要连接在一起的一个公司的两个分部。但在海上坐船航行,也有它的协议,例如要看灯塔,要按航道航行等。这就是外层的承载协议。
那我的车如何从广州到海南呢? 这就需要遵循开车的协议,将车开上轮渡,所有通过轮渡的车都关在船舱里,按照既定的规则排列好,这就是隧道协议。在大海上,车是关在船舱里的,就像在隧道里一样,这时内部的乘客协议,没啥用处,只需要船遵从外层的承载协议,到达海南就可以了。到达之后,外部的承载协议的任务就结束了,打开船舱,将车开出来,就相当于取下承载协议和隧道协议的头。接下来,在海南怎么开车,遵守内部的乘客协议就可以了。
前面说过,直接使用公网太不安全,所以接下来我们来看一种十分安全的VPN - IPsec VPN。这是基于IP协议的安全隧道协议, 为了保证在公网上面信息的安全,因而采用一定的机制来保证安全性:
HTTPS时,说过加密分为对称加密和非对称加密。对称加密速度快一些。而VPN一旦建立,需要传输大量数据,因而采用对称加密。但同样,对称加密还是存在加密密钥如何传输的问题,这里需要用到因特网秘要交换(IKE, Internet Key Exchange)协议hash运算,产生类似指纹的数据摘要,以保证数据的完整性如何保证对方局势真正的那个人?
基于以上三个特性,组成了IPsec VPN的协议簇:
这个协议簇里,有两种协议,区别在于封装网络包的格式不一样:
这个协议簇里有两类算法,分别是加密算法和摘要算法。这个协议簇还包含两大组件,一个用于VPN的双方要进行对称密钥的交换IKE组件, 另一个是VPN双方要对连接进行维护的SA(Security Association)组件。
SA用来维护一个通过身份认证和安全保护的通道,为第二阶段提供服务。这个阶段,通过DH(Diffie-Hellman)算法计算出一个对称密钥K. DH算法是一个比较巧妙的算法。客户端和服务器约定两个公开的质数p和q, 然后客户端随机产生一个数a作为自己的私钥,服务端随机产生一个b作为自己的私钥,客户端可以根据p,q和a计算出公钥A, 服务端根据p,q和b计算出公钥B,然后双方交换公钥A和B。到此客户端和服务端可以根据已有的信息,各自独立算出相同的结果
K, 就是对称密钥。但这个过程,对称密钥从来没在通道上传输过,只传输了生成密钥的材料,通过这些材料,截获的人是无法算出的。
SA里,双方会生成一个随机的对称密钥M, 由K加密传给对方,然后使用M进行双方接下来通信的数据,对称密钥M是有过期时间的,会过一段时间,重新生成一次,从而防止被破解。IPsec SA里有以下内容:
IPsec SA, 重新生成对称密钥当IPsec建立好,接下来就可以开始打包封装传输了。
左边是原始的IP包,在IP头里,会指定上一层协议为TCP。ESP要对IP包进行封装,因而IP头里的上一层协议为ESP。在ESP的正文里,ESP的头部有双方商讨好的SPI, 以及这次传输的序列号。接下来全部是加密的内容。可以通过对称加密进行解密,解密后在正文的最后,指明了里面的协议是什么。如果是IP, 则需要先解析IP头,然后解析TCP头,这是从隧道出来后解封装的过程。
有了IPsec VPN之后,客户端发送的明文的IP包,都会被加上ESP头和IP头,在公网上传输,由于加密,可以保证不被窃取,到了对端后,去掉ESP头,进行解密。
这种点对点的基于IP的VPN, 能满足互通的要求,但是速度往往比较慢,这是由底层IP协议的特性决定的。IP不是面向连接的,是尽力而为的协议,每个IP包自由选择路径,到每个路由器,都自己去找下一跳,丢就丢了,是靠上层TCP的重发来保证可靠性的。
因为IP网络从设计时,就认为是不可靠的,所以即使同一个连接,也可能选择不同的道路,这样的好处是,一条道路崩溃时,总有其他路可以走。当然,带来的代价就是,不断的路由查找,效率比较差。和IP对应的另一种技术称为ATM。这种协议和IP协议的不同在于,它是面向连接的。TCP也是面向连接的,但是ATM和IP是一个层次,和TCP不是一个层次。 另外TCP所谓的面向连接,是不停的重试来保证成功,其实下层的IP还是不面向连接的。ATM是传输之前先建立一个连接,形成一个虚拟的通路,一旦连接建立,所有的包都按照相同的路径走,不会分头行事。
好处是不需要每次都查路由表,虚拟路径已经建立,打上标签了,后续的包傻傻的跟着走就是了,不用像IP包一样,每个包都思考下一步怎么走,都按相同的路径走,效率会高很多。但是一旦虚拟路径上的某个路由器坏了,则这个连接就断了,什么也发不过去了,因为其他的包会按原来的路径走,不会选择其他的路径。ATM技术虽然没有成功,但其摒弃了繁琐的路由查找,改为简单快速的标签交换,将具有全局意义的路由表改为只有本地意义的标签表,这些都可以大大提高一台路由器的转发功力。
多协议标签交换(MPLS, Multi-Protocol Label Switching)将两者的优点结合起来。MPLS在原始的IP头之外,多了MPLS头,里面可以打标签。
在二层头里,有类型字段,0x0800表示IP, 0x8847表示MPLS Label。在MPLS头里,首先是标签值占20位,接着是3位实验位,再接下来是1位栈底标志位,表示当前标签是否位于栈底了。这样就允许多个标签被编码到同一个数据包中,形成标签栈。最后是8位TTL存活时间字段,如果标签数据包的出发TTL值为0,那么该数据包在网络中的生命期被认为已过期。有了标签,还需要设备认这个标签,并且能够根据这个标签转发,这种能够转发标签的路由器称为标签交换路由器(LSR, Label Switching Router)。
这种路由器会有两个表格,一个就是传统的FIB, 也即路由表,另一个是LFIB, 标签转发表。有了这两个表,既可以进行普通的路由转发,也可以进行基于标签的转发。
有了标签转发表,转发的过程如图所示,就不用每次都进行普通路由的查找了。这里我们区分MPLS区域和非MPLS区域。在MPLS区域中间,使用标签进行转发,非MPLS区域,使用普通路由转发,在边缘节点上,需要有能力将对于普通路由的转发,变成对于标签的转发。例如图中要访问114.1.1.1, 在边界上查找普通路由,发现马上要进入MPLS区域了,进去了对应标签1, 于是在IP头外面加一个标签1, 在区域里,标签1要变成标签3, 标签3到达出口边缘,将标签去掉,按照路由发出。
这样一个通过标签转换而建立的路径称为LSP(标签交换路径)。在一条LSP上,沿数据包传送的方向,相邻的LSR分别叫上游LSR(upstream LSR)和下游LSR(downstream LSR)。有了标签,转发是很简单的事,但如何生成标签,却是MPLS中最难的部分。在MPLS中,这部分被称为LDP(Label Distribution Protocol), 是一个动态的生成标签的协议。其实LDP和IP中的路由协议十分相似,通过LSR的交互,互相告知去哪里应该打哪个标签,称为标签分发,往往是从下游开始的。
如果有一个边缘节点发现自己的路由表中出现了新的目的地址,它就要给别人说,我能到达一条新的路径了。如果此边缘节点存在上游LSR, 并且尚有可供分配的标签,则该节点为新的路径分配标签,并向上游发出标签映射消息,其中包含分配的标签等信息。收到标签映射消息的LSR记录相应的标签映射信息,在其标签转发表中增加相应的条目。此LSR为它的上游LSR分配标签,并继续向上游LSR发送标签映射消息。当入口LSR收到标签映射消息时,在标签转发表中增加相应的条目。这时,就完成了LSP的建立。有了标签,转发轻松多了,但是这个和VPN有什么关系?
可以想象,如果我们VPN通道里包的转发,都通过标签的方式进行的,效率会高很多。所以要想个办法把MPLS应用于VPN。
在MPLS VPN中,网络中的路由器分成以下几类:
PE相连接的边缘设备PE之外的其他运营商网络设备。我们发现,在运营商网络里(P Router之间),使用标签是没有问题的,因为都在运营商的管控之下,对于网段,路由都可以自己控制。但一旦客户要接入这个网络,就复杂很多:
首先是客户地址重复问题。客户所使用的大多数都是私有网的地址(
192.168.X.X;10.X.X.X;172.X.X.X), 而且很多情况下都会与其它的客户重复。比如,机构A和机构B都使用了192.168.101.0/24网段的地址,这就发生了地址空间重叠(Overlapping Address Spaces)。首先困惑的是BGP协议,既然VPN将两个数据中心连起来,应该看起来像一个数据中心一样,那如何达到另一端需要通过BGP将路由广播过去,传统BGP无法正确处理地址空间重叠的VPN路由。
假设
机构A和机构B都使用了192.168.101.0/24网段的地址,并各自发布了一条去往此网段的路由,BGP将只会选择其中一条路由,从而导致去往另一个VPN的路由丢失。所以PE路由器之间使用特殊的MP-BGP来发布VPN路由,在相互沟通的消息中,在一般32位IPv4的地址之前加上一个客户标示的区分符用于客户地址的区分,这种称为VPN_IPv4地址族,这样PE路由器会收到如下消息,机构A的192.168.101.0/24应该往这面走,机构B的192.168.101.0/24则应该去另一个方向。
另外困惑的是路由表,当两个客户的IP包到达PE时,PE就困惑了,因为网段是重复的。如何区分那些路由是属于哪些客户VPN内的? 如何保证VPN业务路由与普通路由不相互干扰?
在
PE上,可以通过VRF(VPN Routing&Fowarding Instance)建立每个客户一个路由表,与其它VPN客户路由和普通路由相互区分。可以理解为专属于客户的小路由器。远端PE通过MP-BGP协议把业务路由放到近端PE, 近端PE根据不同的客户选择除相关客户的业务路由放到相应的VRF路由表中。
VPN报文转发采用两层标签方式:
PE到对端PE的一条LSP。VPN报文利用这层标签,可以沿LSP到达对端PE;PE到达CE时使用,在PE上,通过查找VRF表项,指示报文应被送到哪个VPN用户,或更具体一些,到达哪个CE。这样,对端PE根据内层标签可以找到转发报文的接口。MPLS VPN的发包过程:
机构A和机构B都发出一个目的地址 192.168.101.0/24 的IP报文,分别由各自的CE将报文发送至PEPE会根据报文到达的接口及目的地址查找VPN实例表项VRF, 匹配后将报文转发出去,同时打上内层和外层两个标签。假设通过MP-BGP配置的路由,两个报文在骨干网走相同的路径MPLS网络利用报文的外层标签,将报文传送到出口PE, 报文在到达出口PE 2前一跳时已经被剥离外层标签,仅含内层标签PE根据内层标签和目的地址查找VPN实例表项VRF,确定报文的出接口,将报文转发至各自的CECE根据正常的IP转发过程将报文传送到目的地