趣谈网络协议


趣谈网络协议

这篇只是个人的一个总结,原版内容可以查看 https://time.geekbang.org/column/intro/85 喜欢可以购买作者专栏,在此做个申明。

应用层: DNS, Http, Https 所在的层为应用层.

传输层: 应用层封装成包之后会交给传输层(通过Socket编程来实现), 传输层有两种协议:

操作系统往往通过端口来判断得到的包应该给哪个进程。

网络层: 网络层的协议是IP协议, 会有源IP地址和目标IP地址。

操作系统启动的时候会被 DHCP 协议配置IP地址, 以及默认网关的IP地址(192.168.1.1).

MAC层: 操作系统通过 ARP 协议将IP地址发送给网关, 将 IP包交给下一层 Mac层.

ifconfig 命令

Windows上通过ipconfig, Linux上通过ifconfig命令或者ip addr命令 来查看IP`地址.

IP地址是一个网卡在网络世界的通讯地址,相当于我们现实世界的门牌号。

无类型域间选路(CIDR)

32位的IP地址分为网络考和主机好两部分。类似于 10.100.122.2/24, 前24位表示网络号,后8位表示主机号.

公有IP地址和私有IP地址

Ip分类

公有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网络里的所有机器都能收到。

ICMP与ping

ping 是基于ICMP(Internet Control Message Protocol, 互联网控制报文协议)协议工作的. ICMP 报文封装在 IP 包里面, 有很多的类型,不同类型有不同的代码。最常用的类型是主动请求为8, 主动请求的应答为0. 常用的ping就是查询报文,是一种主动请求,并且获得主动应答的ICMP协议.

ping的主动请求,进行抓包称为 ICMP ECHO REQUEST.同理主动请求的回复,称为ICMP ECHO REPLY.比起原生的ICMP, 这里多了两个字段:

ICMP差错报文类型

ping: 查询报文类型的使用

主机AIP地址是192.168.1.1, 主机BIP地址是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头和IP头的细节

MAC头: 里先是目标MAC地址,然后是源MAC地址和一个协议类型。用来说明里面是IP协议。 IP头: 里面的版本号, 目前主流的是IPv4, 服务类型TOS,TTL8标识协议.

任何一台机器上,当要访问另一个IP地址的时候,都会先判断,这个目标IP地址和当前机器的IP地址是否在同一网段,需要使用CIDR和子网掩码。

UDP 协议

TCP是面向连接的, UDP是面向无连接的。在互通之前,面向连接的协议会先建立连接,例如:TCP会三次握手,而UDP不会。

所谓的建立连接,是为了在客户端和服务端维护连接,而建立一定的数据结构来维护双方交互的状态,用这样的数据结构来保证所谓的面向连接的特性。

UDP包头

当我们发送的UDP包到达目标机器后,发现MAC地址匹配,于是取下来,将剩下的包传给处理IP层的代码。把IP头取消来,里面有个8位协议,会标示出数据到底是TCP还是UDP

无论是TCP传数据还是UDP传数据,都要监听一个端口。TCP 还是 UDP 包头里有个端口号, 根据端口号,将数据交给相应的应用程序。

UDP三大特点

UDP三大使用场景

UDP的五个例子

TCP 协议

TCP包头格式

TCP协议需要重点关注以下几个问题:

TCP三次握手

我们假设通路非常不靠谱,A要发起一个连接,当发了第一个请求了无音讯的时候,会有很多可能性,比如第一个请求包丢了, 超时了还是B没有响应,不想和我连接

A不能确认结果,于是再发。终于,又一个请求包到了B, 但是请求包到了B这个事情,A是不知道的,A还有可能再发。

B收到请求包,就知道A的存在,并且知道A要和它建立连接。如果B不乐意建立连接,则A会重试一段时间后放弃,连接建立失败,没有问题;如果B乐意建立连接,则会发送应答包给A

对于B来说,这个应答包也是不知道能不能到达A。这个时候B自然不能认为连接建立好了,因为应答包仍然会丢,会绕弯路,或者A已经挂了。这个时候B还碰到一个诡异的现象,AB原来建立连接,做了简单通信后,结束连接。A建立连接时,请求包重复发了几次,有的请求包绕了一大圈又回来,B会认为这是一个正常的请求,因此建立连接,可以想象这个连接不会进行下去,也没有终止的时候。

B发送的应答可能会发送多次,但只要一次到达A, A就认为连接已经建立,因为对于A 来说,他的消息有去有回,A会给B应答的应答,只有当B等到这个消息才会建立连接。

三次握手除了双方建立连接外,主要为了沟通TCP包的序号问题。每个连接都要有不同的序号。这个序号的起始序号是随着时间变化的,可以看成一个32位的计数器,每4ms加一。

TCP 四次挥手

A开始说”不玩了”,B说”知道了”。这个是没有什么问题的,因为在此之前,双方还处于合作状态,如果A说”不玩了”,没有收到回复,就会重新发送。但是这个会和结束之后,就可能有异常情况:

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后,依然没有收到它发的FINACK, 按照TCP的原理,B当然会重发FIN, 这个时候A再收到这个包之后,A就直接发送RST, B就知道A早跑了。

如何实现靠谱的协议

TCP协议为了保证顺序性,每一个包都有一个ID。在建立连接的时候,会商定起始的ID是什么,然后按照ID一个个发送。为了保证不丢包,对于发送的包都要进行应答,但是这个应答也不是一个个来的,而是会应答某个之前的ID, 表示都收到了, 这种模式称为累计确认或累计应答(cumulative acknowledgment)。为了记录所有发送和接收的包,TCP也需要发送和接收端分别都有缓存来保存这些记录。发送端的缓存里是按照包的ID一个个排列,根据处理情况分成4个部分:

于是发送端需要保持下面的数据结构:

对于接收端来说,它的缓存里记录的内容要简单一些:

对应的数据结构就是:

  • MaxRcvBuffer: 最大缓存的量
  • LastByteRead: 之后是已经接收了,但还没有被应用层读取的。
  • NextByteExpected: 第1部分和第2部分的分界线。

Socket套接字

Socket编程进行端到端通信,往往意识不到中间经过多少局域网,多少路由器,因而能够设置的参数,也只能是端到端协议之上网络层和传输层。

在网络层,Socket函数需要指定到底是IPv4还是IPv6, 分别对应设置为AF_INETAF_INET6。另外,还要指定到底是TCP(基于数据流*,设置为SOCK_STREAM)还是UDP(基于数据报**,设置为SOCK_DGRAM)。

基于TCP协议的Socket程序函数调用过程

两端创建Socket之后, TCP服务端先监听一个端口,一般先调用bind函数, 给这个Socket赋一个IP端口。原因如下:

当服务器有IP端口号, 就可以调用listen函数进行监听。在TCP的状态图里,有个listen状态,当调用这个函数之后, 服务器就进入这个状态, 客户端就可以发起连接。

内核中为每个Socket维护两个队列:

接下来,服务端调用accept函数,拿出一个已完成的连接进行处理,如还没完成就要等着。在服务端等待的时候,客户端可以通过connect函数发起连接,先在参数中指明要连接的IP地址端口号, 然后开始发起三次握手,内核会给客户端分配一个临时的端口。一旦握手成功,服务端的accept就会返回另一个Socket。监听的Socket和真正用来传数据的Socket是两个,一个叫做监听Socket, 一个叫做已连接Socket。连接建立成功之后,双方开始通过readwrite函数来读写数据,就像往一个文件流里写东西一样。

Socket

在内核中Socket是一个文件,对应就有文件描述符。每个进程都有一个数据结构task_struct,里面指向一个文件描述符数组,来列出这个进程打开的所有文件的文件描述符。文件描述符是个整数,是这个数组的下标。这个数组中的内容是一个指针,指向内核中所有打开的文件列表。既然是一个文件,就会有一个inode, 只不过Socket对应的inode不像真正的文件系统一样,保存在硬盘上,而是在内存中。这个inode指向Socket在内核中的Socket的结构。

这个结构里,主要是两个队列,1.发送队列 2.接收队列。在这两个队列里保存的是一个缓存sk_buff。这个缓存里能够看到完整的包结构。

基于UDP协议的Socket程序函数调用过程

UDP是没有连接的,不需要三次握手,也就不需要调用listenconnect, 但UDP交互仍需要IP端口号就需要bindUDP是没有维护连接状态的,因而不需要每对连接建立一组Socket, 而是只要有一个Socket就能和多个客户端通信。也正是因为没有连接状态,每次通信时,都调用sendtorecvfrom, 都可以传入IP地址和端口。

服务器如何连接更多项目

最大连接数, 系统会用一个四元祖来标识一个TCP连接。

{本机IP, 本机端口,对端IP, 对端端口}

服务器通常固定在某个本地端口上监听,等待客户端的连接请求。因此,服务端TCP连接四元祖中只有对端IP(客户端IP)和 对端端口(客户端端口)是可变的。因此:

最大TCP连接数 = 客户端IP数 x 客户端端口数

对于IPv4,客户端IP数最多为2^32, 客户端端口数最多为2^16, 也就是服务端单机最大TCP连接数,约为 2^48

当然,服务端最大并发TCP连接数远不能达到理论上限。首先主要是文件描述符限制, 按照上面的原理,Socket都是文件,所以首先要通过ulimit配置文件描述符数目;另一个限制是内存, 按照上面的数据结构,每个TCP连接都要占用一定内存,操作系统内存是有限的。

可以通过以下方式来降低每个项目消耗的资源数目:

方式一

方式二

上面基于进程或线程模型,其实还是有问题的。新到来一个TCP连接,就需要分配一个进程或线程。一台机器无法创建很多进程或线程。有个C10K(一台机器要维护1万个连接,就要创建1万个进程或线程,操作系统是无法承受的)。

方式四

HTTP协议

登陆 www.163.com 这个 URL(同一资源定位符), 浏览器会将 www.163.com 这个域名发送给 DNS服务器, 让他解析IP地址. HTTP 先通过三次握手建立 TCP连接. (目前使用的HTTP协议大部分都是1.1, 是默认开启 Keep-Alive, 建立的TCP连接,可以在多次请求中复用)。

HTTP的报文可以分为3大部分

HTTP请求发送: HTTP协议 是基于TCP协议的, 使用面向连接的方式发送请求, 通过 stream 二进制流的方式传给对方, 到了TCP层, 会把二进制流变成一个个报文段发送给服务器.

HTTP返回的构建

HTTP 返回报文也有一定格式, 也是基于HTTP 1.1.

HTTP 1.1 在应用层以纯文本的形式进行通信。 每次通信都要带完整的HTTP头, 不考虑pipeline模式的话, 每次的过程总是一来一回, 在实时性, 并发性上都存在问题。 HTTP 2.0 会对 HTTP头进行一定的压缩, 将原来每次都要携带得的大量的 key value 在两端建立一个索引表, 对相同的头只发送索引表中的索引。

GoogleQUIC协议, 从 TCP 切换到 UDP, 有如下几个特点:

HTTPS协议

加密分为两种方式:

所以对称加密算法相比非堆成加密算法来说,效率高很多,性能也好,所以交互的场景下多用对称加密。

数字证书

不对称加密也会有同样的问题,何如将不对称加密的公钥给对方?一种是放在一个公网的地址上,让对方下载;另一种是在建立连接时传给对方。两种方法有相同的问题,作为一个普通的网民,怎么鉴别公钥是对的,例如,我自己搭建一个网站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协议的总体思路: Https

当你登陆外卖网站时,由于是HTTPS,客户端会发送Client Hello消息到服务器,以明文传输TLS版本信息,加密套件候选列表,压缩算法候选列表等信息。另外,还会有一个随机数,在协商对称秘钥时使用。然后,外卖网站返回Server Hello消息告诉客户端,服务器选择使用的协议版本,加密套件,压缩算法等,还有一个随机数,用于后续的密钥协商。然后外卖网站会给一个服务端的证书说:”Server Hello Done, 我这里就这些信息了”。

你当然不相信这个证书,于是从自己信任的CA仓库中,拿CA的证书里面的公钥去解密外卖网站的证书。如果能够成功,说明外卖网站是可信的。这个过程中,可能会不断往上追溯CA, CACA, CACACA,反正直到一个授信的CA

证书验证完毕之后,觉得这个外卖网站可信,于是客户端计算产生随机数字Pre-master, 发送Client Key Exchange, 用证书中的公钥加密,再发送给服务器,服务器可以通过私钥解密出来。

到目前为止,无论是客户端还是服务器,都有三个随机数,分别是:自己的,对端的,以及刚生成的Pre-master随机数。通过这三个随机数,可以在客户端和服务器产生相同的对称密钥。有了对称密钥,客户端就可以说:”Change Clipher Spec”, 咱们以后都采用协商的通信密钥和加密算法进行加密通信。然后发送一个Encrypted Handshake Message, 将已经商定好的参数等,采用协商密钥进行加密,发送给服务器用于数据与握手验证。同样,服务器也可以发送Change Cipher Spec, 说:”没问题,咱们以后都采用协商的通信密钥和加密算法进行加密通信”,并且也发送Encrypted Handshake Message的消息试试。当双方握手结束之后,就可以通过对称密钥进行加密传输。这个过程除了加密解密外,其他的过程和HTTP是一样的,过程也非常复制。

上面的过程只包含HTTPS的单向认证,也即客户端验证服务端的证书,是大部分的场景,也可以在更加严格安全要求的情况下,启用双向认证,双方互相验证证书。

重放与篡改

有了加密和解密, 黑客截获包也打不开,但它可以发送N次。这个往往通过TimestampNonce随机数联合起来,然后做一个不可逆的签名来保证。Nonce随机数保证唯一,或者TimestampNonce合起来保证唯一,同样的,请求只接收一次,于是服务器多次受到相同的TimestampNonce, 则视为无效即可。如果有人想篡改TimestampNonce, 还有签名保证不可篡改性,如果改了用签名算法解出来,就对不上了,可以丢弃。

流媒体协议

无论是直播还是点播,其实都是对于视频数据的传输。视频技术的三个名词系列:

视屏其实就是快速播放的一连串连续的图片。每一张图片,我们称为一。只要每秒钟帧的数据足够多,也即播放的足够快。比如每秒30帧,以人眼的敏感程度,是看不出这是一张张独立的图片的,这就是我们常说的帧率。每一张图片都是由像素组成的,假设为1024x768(这个像素不算多)。每个像素由RGB组成,每个8位,共24位。

我们来算一下,每秒钟的视频有多大?

30帧 x 1024 x 768 x 24 = 566,231,040(Bits) = 70,778,880(Bytes)

如果是一分钟就是4,246,732,800Bytes, 已经是4G。这个数据量实在太大,根本没办法存储和传输。如果这样存储,硬盘很快就满了。这样传输,多少带宽也不够用。可以使用编码来用尽量上的Bit数保存视频,使播放时画面看起来仍然很精美。编码是一个压缩的过程

视屏和图片压缩过程中的特点:

视频压缩

总之,用于编码的算法非常复杂,而且多种多样,但是编码过程其实都是类似的。

视频编码两大流派:

后来,ITU-T(国际电信联盟电信标准化部门,ITU Telecommunication Standardization Sector) 与 MPEG联合制定了H.264/MPEG-4 AVC,这才是我们这里关注的重点。进过编码之后,一帧帧的图像就变成一串串二进制,这个二进制可以放在一个文件里,按照一定的格式保存起来,这就是名词系列一。其实这些就是视频保存成文件的格式。例如,前几个字节是什么意义,后几个字节是什么意义,然后是数据,数据中保存的就是编码好的结果。

直播流程

当然,这个二进制也可以通过某种网络协议进行封装,放在互联网上传输,这个时候就可以进行网络直播了。网络协议将编码好的视频流,从主播端推送到服务器,在服务器上有个运行了同样协议的服务端来接收这些网络包,从而得到里面的视频流,这个过程称为接流

服务端接到视频流之后,可以对视频流进行一定的处理,例如转码(从一个编码格式,转成另一种格式)。因为观众使用的客户端千差万别,要保证他们都能看到直播。流处理完毕之后,就可以等待观众的客户端来请求这些视频流。观众的客户端请求的过程称为拉流

如果有非常多的观众,同时看一个视频直播,那都从一个服务器上拉流,压力太大,因而需要一个视频的分发网络,将视频预先加载到就近的边缘节点,这样大部分观众看的视频,是从边缘节点拉取的,就能降低服务器的压力。

当观众的客户端将视频流拉下来之后,就需要进行解码, 也即通过上述过程的逆过程,将一串串看不懂的二进制再转变成一帧帧生动的图片,在客户端播放出来。

接下来,我们依次看下每个过程:

直播流程

可以看出,I帧最完整,B帧压缩率最高,而压缩后帧的序列,应该在IBBP的间隔出现的。这就是通过时序进行编码。在一帧中,分成多个片,每个片中分成多个宏块,每个宏块分成多个子块,这样将一张大的图分解成一个个小块, 方便进行空间上的编码。,

尽管时空非常立体的组成了一个序列,但是总归还是要压缩成一个二进制流。这个流是有结构的,是一个个网络提取层单元(NALU, Network Abstraction Layer Unit)。变成这种格式就是为了传输,因为网络上的传输,默认的是一个个的包,因而这里也就分成了一个个单元。

NALU

每个NALU首先是一个起始标识符,用于表示NALU之间的间隔;然后是NALU的头,里面主要配置NALU的类型;最终Payload里面是NALU承载的数据。在NALU头里,主要内的是容类型NAL Type:

在传输视频流之前,必须要传输这两类参数,不然无法解码。为了保证容错性,每个I帧前面,都会传一遍这两个参数集合。如果NALU Header里面的表示类型是SPSPPS, 则Payload中就是真正的参数集内容。如果类型是帧,则Payload中才是真的视频数据,当然也是一帧帧存放的,前面说了,一帧的内容还是挺多的,因而每一个NALU里保存的是一片。对于每一片,到底是I帧, 还是P帧,还是B帧,在片结构里也有个Header, 里面有个类型, 然后是片的内容。

这样,整个格式就出来了,一个视频,可以拆分成一系列的帧,每帧拆分成一系列的片,每片都放在一个NALU里面,NALU之间都是通过特殊的起始标识符分隔,在每个I帧的第一篇前面,要插入单独保存SPSPPSNALU,最终形成一个长长的NALU序列

RTMP为啥需要建立一个单独的连接?因为它们需要商量一些事情,保证以后传输能正常进行,主要就是两个事情:1.版本号,如果客户端服务器的版本号不一致,则不能工作。2.时间戳,视频播放中,时间是很重要的,后面的数据流互通时,经常要带上时间戳的差值,因而一开始双方就要知道对方的时间戳。

未来沟通这些事情,需要发送六条消息;客户端发送C0,C1,C2, 服务器发送S0,S1,S2。首先客户端发送C0表示自己的版本号,不必等对方的回复,然后发送C1表示自己的时间戳。服务器只有在收到C0时,才能返回S0,表明自己的版本号,如果版本号不匹配,可以断开连接。服务器发送完S0后,也不用等什么,就直接发送自己的时间戳S1。客户端收到S1时,发一个知道对方时间戳的ACK C2。同理服务器收到C1时,发送一个知道对方时间戳的ACK S2

RTMP

握手之后,双方需要互相传递一些控制信息,例如Chunk块的大小,窗口大小等;真正传输时,还需要创建一个流Stream, 然后通过这个Stream来推流publish推流的过程就是将NALU放在Message里面发送,这个也称为RTMP Packet包Message的格式如下:

Message

发送时去掉NALU起始标识符,因为这部分对于RTMP协议来说没有用。接下来,将SPSPPS参数集封装成一个RTMP包发送,然后发送一个个片的NALURTMP在收发数据时并不是以Message为单位的,而是把Message拆分为Chunk发送,而且必须在一个Chunk发送完成之后,才能开始发送下一个Chunk.每个Chunk中都带有Message ID, 表示属于哪个Message, 接收端也会按照这个IDChunk组成Message。前面连接时,设置的Chunk块大小就是指这个Chunk,将大的消息变为小的块再发送,可以在低带宽的情况下,减少网络拥塞。如下是一个分块的例子:

假设一个视频的消息长度为307, 但是Chunk大小约定为128,于是会拆分为三个Chunk。第一个ChunkType = 0, 表示Chunk头是完整的;头里Timestamp1000,总长度Length307, 类型为9, 是个视频,Stream ID12346, 正文部分承担128字节的Data。第二个Chunk也要发送128个字节,Chunk头由于和第一个Chunk一样,因此采用Chunk Type = 3,表示头一样就不再发送。第三个Chunk要发送的Data的长度为307-128-128 = 51个字节,还是采用Type = 3

Message

就这样数据就源源不断到达流媒体服务器,整个过程就像这样。

Message

这时,大量观看直播的观众就可以通过RTMP协议从流媒体服务器上拉取,但这么多的用户量,都去同一个地方拉取,服务器压力会很大,而且用户分布在全国甚至全球,如果都去统一的一个地方下载,也会时延比较长,需要有分发网络。分发网络分为中心边缘两层。边缘层服务器部署在全国各地及横跨各大运营商里,和用户距离很近。中心层是流媒体服务集群,负责内容的转发。智能负载均衡系统,根据用户的地理位置信息,就近选择边缘服务器,为用户提供 推/拉 流服务。中心层也负责转码服务,例如,把RMTP协议的码流转换为HLS码流。

Message

这套机制在后面的DNS, HTTPDNS, CDN会详细描述。

Message

先读到的是H.264的解码参数,例如SPSPPS,然后对收到的NALU组成一个个帧,进行解码,交给播放器播放,一个绚丽多彩的视频画面就出来了。

P2P 协议

下载一部电影,最简单的方式就是通过HTTP进行下载,但是通过浏览器下载时,只要文件稍微大点,下载速度就奇慢无比。还有种下载文件的方式,就是通过FTP(文件传输协议), FTP采用两个TCP连接来传输一个文件。

FTP 的两种工作方式

每传输一个文件,都要建立一个全新的数据连接。FTP有两种工作模式,分为主动模式(PORT)被动模式(PASV),这些都是站在FTP服务器的角度来说的:

P2P是什么?

无论是HTTP方式,还是FTP方式,都有一个比较大的缺点,就是难以解决单一服务器的带宽压力,因为它们使用的都是传统的客户端服务器的方式。 后来,一种创新的,称为P2P的方式流行起来。P2P(peer-to-peer)资源开始并不集中地存储在某些设备上,而是分散地存储在多台设备上。这些设备我们称为peer。想要下载一个文件时,只要得到那些已经存在文件的peer,并和这些peer之间,建立点对点的连接,而不需要到中心服务器上,就可以就近下载文件。一旦下载了一个文件,你也会称为peer中的一员,你旁边的那些机器,也可能会选择从你这里下载文件,所以当你使用P2P软件时,例如BitTorrent, 往往能够看到,既有下载流量,也有上传流量,也即你自己也加入了这个P2P网络,自己从别人那里下载,同时也提供给其他人下载。这种方式,参与的人越多,下载速度越快。

种子(.torrent)文件

要想知道哪些peer有这些文件,需要用到种子(.torrent文件), .torrent文件由两部分组成:

下载时,BT客户端首先解析.torrent文件,得到tracker地址,然后连接tracker服务器。tracker服务器回应下载者的请求,将其他下载者(包括发布者)的IP提供给下载者。下载者再连接其他下载者,根据.torrent文件,两者分别告知对方自己已经有的块,然后交换对方没有的数据。此时不需要其他服务器参与,并分散了单个线路上的数据流量,减轻了服务器负担。

下载者每得到一个块, 需要算出下载块的Hash验证码,并与.torrent文件中的对比。如果一样,则说明块正确,不一样则需重新下载这个块。这规定是为了解决下载内容的准确性问题。从这个过程也可以看出,这种方式特别依赖trackertracker需要收集下载者信息的服务器,并将此信息提供给其他下载者,使下载者们相互连接起来传输数据。虽然下载的过程是非中心化的,但加入这个P2P网络时,都需要借助tracker中心服务器,这个服务器是用来登记有哪些用户在请求哪些资源。这种工作方式有一个弊端,一旦tracker服务器出现故障或线路遭到屏蔽,BT工具就无法正常工作。

去中心化网络(DHT)

DHT(Distributed Hash Table): 每个加入这个DHT网络的人,都要负责存储这个网络里的资源信息和其他成员的联系信息,相当于所有人一起构成一个庞大的分布式存储数据库。Kademlia协议是一种著名的DHT协议:

任何一个BitTorrent启动之后,都有两个角色。一是peer,监听一个TCP端口,用来上传和下载文件,这个角色表明,这里有某个文件。另外的角色是DHT node, 监听一个UDP端口,通过这个角色,这个节点加入一个DHT网络。

DHT网络里,每一个DHT node都有一个ID, 这个ID是一个很长的串。每个DHT node都有责任掌握一些知识,也就是文件索引, 就是它应该知道某些文件是保存在哪些节点上。只需要有这些知识就可以了,而它自己本身不一定保存这个文件节点。

DHT

哈希值

每个DHT node不会有全局知识(不知道所有文件保存在哪里), 只需要知道一部分。应该知道哪一部分需要用哈希值计算出来,DHT node的ID是和哈希值相同长度的串DHT算法是这样规定的:如果一个文件计算出一个哈希值,则和这个哈希值一样的那个DHT node, 就有责任知道从哪里下载这个文件,即便它自己没有保存这个文件。可能一摸一样的DHT node下线了,所以DHT算法还规定:除了一摸一样的那个DHT node应该知道,ID和这个哈希值非常接近的NDHT node也应该知道。

哈希值接近的定义: 例如只修改最后一位,就很接近;修改倒数2位也不远; 修改倒数3位也可以接收。总之,凑齐规定的N个数就行。

分析上图, 文件1通过哈希运算,得到匹配IDDHT nodenode C, 当然还会有其他的。所以node C有责任知道文件1的存放地址,虽然node C本身没有存放文件1。同理,文件2通过哈希运算,得到匹配IDDHT nodenode E,但是node Dnode EID值很近, 所以node D也知道。当然,文件2本身没有必要一定在node Dnode E里,但是碰巧在node E里有一份。

接下来一个新的节点node new上线了。如果想下载文件1,首先要加入DHT网络;

这种模式下,种子.torrent文件里就不再是tracker地址,而是一个listnode地址,而所有这些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 CID很像的节点,它们也知道如何下载文件1

node C上,告诉node new, 下载文件1,要去B, D, F, 于是node new选择和node B进行peer连接,开始下载,它一旦开始下载,自己本地也有文件1, 于是node new告诉node C以及和node CID很像的那些节点,我也有文件1了,可以加入那些文件拥有者列表了。但你会发现node new上没有文件索引,但根据哈希算法,一定会有某些文件的哈希值是和node newID匹配上的,在DHT网络中,会有节点告诉它,你既然加入这个网络,你也有责任知道某些文件的下载地址。一切都是分布式的。

里面有几个细节问题:

DHT网络是按照距离分层的,Kademlia算法中,每个节点只有4个指令:

DHT网络中,怎么更新节点数据?

这个机制保证任意节点介入和离开都不影响整体网络。

DNS 协议

DNS服务器一定要设置成高可用,高并发和分布式的。树状层级结构:

为了提高DNS解析性能, 很多网络多会就近部署DNS缓存服务器。DNS的解析流程如下:

负载均衡

站在客户端的角度, 上面的过程是一次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地址

HTTPDNS

DNS有两项功能:

有时DNS也会指错路:当我们发出请求解析DNS时,首先会先连接到运营商本地的DNS服务器,有这个服务器帮我们去整颗DNS树上进行解析,然后将解析结果返回给客户端。但是本地导游有自己的”小心思”:

但是A运营商偷懒,将解析的请求转发给B运营商, B运营商去权威DNS服务器查询的话,会返回一个在B运营商的网站地址,客户每次都要跨运营商访问,速度会慢很多

HTTPDNS的工作模式

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服务器能够准确知道这些信息,因而可以做精准的全局负载均衡。 HTTPDNS 当然,当所有这些都不工作时,可以切换到传统的LocalDNS来解析,那HTTPDNS是如何解决上面的问题的?

其实归结起来就是两大问题。1.解析速度和更新速度的平衡问题 2.智能调度问题,对应的解决方案是HTTPDNS的缓存设计和调度设计。

HTTPDNS的缓存设计: 解析DNS过程复杂,通信次数多,对解析速度造成很大影响。为了加快解析,因而有了缓存,但是这又回产生缓存更新速度不及时的问题。最要命的是,这两个方面都掌握在别人的手中,也即本地DNS服务器手中,它不会为你定制。而HTTPDNS就是将解析速度和更新速度全部掌握在自己手中。一方面,解析的过程,不需要本地DNS服务器递归调用一大圈,一个HTTP的请求直接搞定,要实时更新时,马上就能起作用;另一方面为了提高解析速度,本地也有缓存,缓存实在客户端SDK维护的,过期时间,更新时间都可以自己控制。

HTTPDNS的缓存设计策略也是咱们做应用架构中常用的缓存设计模式,也即分为客户端,缓存,数据源三层:

SDK中缓存会严格按照缓存过期时间,如果缓存没有命中,或者已经过期,而且客户端不允许使用过期的记录,则会发起一次解析 ,保障记录是更新的。解析可以同步进行,也可以直接调用HTTPDNS接口,返回最新的记录,更新缓存;也可以异步进行,添加一个解析任务到后台,由后台任务调用HTTPDNS接口。

同步更新的有点是实时性好,缺点是如果有多个请求都发现过期时,同时会请求HTTPDNS多次。同步更新的方式对应到应用架构中缓存的Cache-Aside 机制, 也即先读缓存,不命中读数据库,同时将结果写入缓存。

异步更新的优点是,可以同时在多个请求都发现过期的情况下,合并为一个对于HTTPDNS的请求任务,只执行一次,减少HTTPDNS的压力。同时可以在即将过期时,就创建一个任务进行预加载,防止过期之后再刷新。它的缺点是当前请求拿到过期数据时,如果客户端允许使用过期数据,需要冒一次险。如果过期数据还能请求,就没问题;如果不能请求,则失败一次,等下次缓存更新后,再请求才能成功。

异步更新机制对应到应用框架中缓存的Refresh-Ahead 机制, 即业务仅访问缓存,当过期时定期刷新。在著名的应用缓存Guava Cache中,有个RefreshAfterWrite机制,对于并发情况下,多个缓存访问不命中从而引发并发回源的情况。可以采用只有一个请求回源的模式。在应用架构的缓存中,也常常用数据预热预加载机制。

HTTPDNS的调度设计

由于客户端嵌入SDK, 因而就不会因为本地DNS的各种缓存,转发,NAT,让权威DNS服务器误会客户端所在的位置和运营商,可以拿到第一手资料。在客户端,可以知道手机是哪个国家,哪个运营商,哪个省,甚至哪个市,HTTPDNS服务器可以根据这些信息,选择最佳的服务节点返回。

如果有多个节点,还会考虑错误率,请求时间,服务器压力,网络状况等,进行综合选择,而非仅仅考虑地理位置。当有一个节点宕机或性能下降时,可以尽快进行切换。要做到这一点,需要客户端使用HTTPDNS返回的IP访问业务应用。客户端SDK回收集网络请求数据,如错误率,请求时间等网络请求质量数据,并发送到统计后台,进行分析,聚合,以此查看不同IP的服务质量。

HTTPDNS

服务端, 应用可以通过调用HTTPDNS的管理接口,配置不同服务质量的优先级,权重。HTTPDNS会根据这些策略综合地理位置和线路状况算出一个排序,优先访问当前那先优质的,时延低的IP地址。HTTPDNS通过智能调度之后返回的结果,也会缓存在客户端。为了不让缓存使得调度失真,客户端可以根据不同的移动网络运营商WIFISSID来分维度缓存。不同运营商或WIFI解析出来的结果会不同。

CDN

当一个用户想访问一个网站时,指定这个网站的域名,DNS就会将这个域名解析为地址,然后用户请求这个地址,返回一个网页。那这里面还有没有可以优化的地方?

例如去电商网站下单买个东西,这个东西一定要从电商总部的中心仓库送过来?原来基本是这样的,每一单都是单独配送,所以可能要很久才能收到东西。但是后来电商网站的物流在全国各地建立了很多仓库,而不是只有总部的中心仓库才可以发货。

我们先说,我们的网站访问可以借鉴”就近配送”这个思路: 全球有这么多的数据中心,无论在哪里上网,临近不远的地方基本都有数据中心。是不是可以在这些数据中心里部署几台机器,形成一个缓存的集群来缓存部分数据,那么用户访问数据时,就可以就近访问?

当然是可以的,这些分布在各个地方的各个数据中心的节点,就称为边缘节点。由于边缘节点数目比较多,但是每个集群规模比较小,不可能缓存下来所有东西,因而可能无法命中,这样就会在边缘节点之上,有区域节点,规模就要更大,缓存的数据会更多,命中的概率也就更大。在区域节点之上是中心节点,规模更大,缓存数据更多。如果还不命中,就只好回源网站访问了。

CDN

这就是CDN的分发系统架构CDN系统的缓存,也是一层层的,能不访问后端真正的源,就不打扰他。这也是电商网站物流系统的思路。有了这个分发系统之后,接下来就是,客户端如果找到相应的边缘节点进行访问?

还记得我们讲过的基于DNS的全局负载均衡吗?这个负载均衡主要用来选择一个就近的同样运营商的服务器进行访问。你会发现,CDN分发网络也是一个分布在多个区域,多个运营商的分布式系统,也可以用相同的思路选择最合适的边缘节点。

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地址。本地DNS服务器缓存这个IP地址,然后将IP返回给客户端,客户端去访问这些边缘节点,下载资源。缓存服务器响应用户请求,将用户所需内容传送到用户终端。如果这台缓存服务器上并没有用户想要的内容,那么这台服务器就要向它的上一级缓存服务器请求内容,直至追溯到网站的源服务器将内容拉到本地。

CDN可以进行缓存的内容有很多种:保质期长的日用品比较容易缓存,因为不容易过期,对应到就像电商仓库系统里,就是静态页面,图片等,因为这些东西也不怎么变,所以适合缓存。

CDN

还记得这个接入层缓存的架构吗? 在进入数据中心时,我们希望通过最外层接入层的缓存,将大部分静态资源的访问拦在边缘。而CDN则是更进一步,将这些静态资源缓存到离用户更近的数据中心外。越接近客户,访问性能越好,时延越低。

但是静态内容中,有一种特殊的内容,也大量使用CDN, 这就是前面讲过的流媒体CDN支持流媒体协议, 例如前面讲过的RTMP协议。在很多情况下,这相当于一个代理,从上一级缓存读取内容,转发给用户。由于流媒体往往是连续的,因而可以进行预先缓存的策略,也可以预先推送到用户的客户端。

对于静态页面来说,内容的分发往往采取拉取的方式,也即当发现微命中时,再去上一级进行拉取。但是,流媒体数据量大,如果出现回源, 压力回比较大,所以往往采取主动推送的模式,将热点数据主动推送到边缘节点。

对于流媒体来说,很多CDN还提供预处理服务,也即文件在分发之前,经过一定的处理。例如将视频转换为不同的码流,以适应不同网络带宽的用户需求;再比如对视频进行分片,降低存储压力,也使得客户端可以选择使用不同的码率加载不同的分片。

对于流媒体CDN来说,有个关键的问题是防盗链问题。因为视频是要花大价钱买版权的,为了挣点钱, 收点广告费,如果流媒体被其他的网站盗走,在人家的网站播放,那就损失惨重了;最常用也是最简单的方式就是HTTP头的refer字段,当浏览器发送请求时,一般会带上refer, 告诉服务器是从哪个页面链接过来的,服务器基于此可以获得一些信息用于处理。如果refer信息不是来自本站,就阻止访问或跳到其他链接。

refer的机制相对比较容易破解,所以还需要配合其他的机制

一种常用的机制是时间戳防盗链。使用CDN的管理员可以在配置界面上和CDN厂商约定一个加密字符串。客户端取出当前的时间戳,要访问的资源及其路径,连同加密字符串进行签名算法得到一个字符串,然后生成一个下载链接,带上这个签名字符串和截止时间戳去访问CDN。在CDN服务器,根据取出过期时间和当前CDN节点时间进行比较,确认请求是否过期。然后CDN服务端有了资源及路径,时间戳,以及约定的加密字符串,根据相同的签名算法计算签名,如果匹配则一致,访问合法,才会将资源返回给客户。

然而比如在电商仓库中,我在前面提过,有关生鲜的缓存就是非常麻烦的,这对应的是动态的数据。比较难以缓存。现在也有动态CDN, 主要有两种模式:

数据中心

无论是看新闻,下订单,看视频,下载文件,最终访问的目的地都在数据中心里。数据中心是一个大杂烩,几乎用到前面所学的所有知识。讲办公室网络时,我们知道办公室里有很多电脑,如果要访问外网,需要经过一个叫网关的东西,而网关往往是一个路由器。

数据中心里也有一大堆电脑,但它和我们办公室里的笔记本或台式机不一样。数据中心里是服务器。服务器被放在一个个叫做机架(Rack)的架子上。数据中心的入口和出口也是路由器,由于在数据中心的边界,就像在一个国家的边境,称为边界路由器(Border Router)**。为了高可用,边界路由器会有多个。

一般家里只会连接一个运营商的网络,而为了高可用,为了一个运营商出问题时,还可以通过另外一个运营商提供服务,所以数据中心的边界路由器会连接多个运营商网络。既然是路由器,就需要跑路由协议,数据中心往往就是路由协议中的自治区域(As)。数据中心里的机器要想访问外面的网站,数据中心里也是有对外提供服务的机器,都可以通过BGP协议,获取内外互通的路由信息。这就是我们常听到的多线BGP的概念。

如果数据中心非常简单,没几台机器,那就像家里或宿舍一样,所有的服务器都直接连到路由器上就可以了。但是数据中心里往往有非常多的机器。当塞满一机架时,需要有交换机将这些服务器连接起来,可以互相通信。

这些交换机往往是放在机架顶端的,所以经常称为TOR(Top Of Rack)交换机。这一层的交换机常常称为接入层(Access Layer)。注意这个接入层和原来讲过的应用的接入层不是一个概念。

数据中心

当一个机架放不下时,就需要多个机架,还需要有交换机将多个机架连接在一起。这些交换机对性能的要求更高,带宽也更大。这些交换机称为汇聚层交换机(Aggregation Layer)。数据中心里每一个连接都是需要考虑高可用的。这里首先要考虑的是,如果一台机器只有一个网卡,上面连着一条网线,接入到TOR交换机上。如果网卡坏了,或者不小心网线掉了,机器就上不去了。所以,需要至少两个网卡,两个网线插到TOR交换机上,但是两个网卡要工作的像一张网卡一样,这就是常说的网卡绑定(bond)

这就需要服务器和交换机都支持一种协议LACP(Link Aggregation Control Protocol)。他们互相通信,将多个网卡聚合称为一个网卡,多个网线聚合成一个网线,在网线之间可以进行负载均衡,也可以为了高可用做准备。

LACP

网卡有了高可用保证,但交换机还有问题。如果一个机架只有一个交换机,它挂了,那整个机架都不能上网了。因而TOR交换机也需要高可用,同理接入层和汇聚层的连接也需要高可用性,也不能单线连着。最传统的方法是,部署两个接入交换机,两个汇聚交换机。服务器和两个交换机都连接,接入交换机和两个汇聚交换机都连接,当然这样会形成环,所以需要启用STP协议,去除环,但是这样两个汇聚就只能一主一备。STP协议里只有一条路会其作用。

STP

交换机有一种技术叫做堆叠,所以另一种方法是,将多个交换机形成一个逻辑的交换机,服务器通过多根线分配连到多个接入层交换机上,而接入层交换机多根线分别连接到多个交换机上,并且通过堆叠的私有协议,形成双活的连接方式。

堆叠

由于对带宽要求更大,而且挂了影响也更大,所以两个堆叠可能就不够了,可以就会有更多的,比如四个堆叠为一个逻辑的交换机。汇聚层将大量的计算节点相互连接在一起,形成一个集群。在这个集群里,服务器之间通过二层互通,这个区域常称为一个POD(Point Of Delivery),有时候也称为一个可用区(Available Zone)

当节点数目再多时,一个可用区放不下,需要将多个可用区连在一起,连接多个可用区的交换机称为核心交换机

核心交换机

核心交换机吞吐量更大,高可用要求更高,肯定需要堆叠,但往往仅仅堆叠,不足以满足吞吐量,因而还是需要部署多组核心交换机。核心和汇聚交换机之间为了高可用,也是全互联模式的。

这时出现环路怎么办?:一种方式是,不同的可用区在不同的二层网络,需要分配不同的网段。汇聚和核心之间通过三层网络互通,二层都不在一个广播域里,不会存在二层环路的问题。三层有环是没有问题的,只要通过路由协议选择最佳的路径就可以了。那为啥二层不能有环路,三层可以呢?

OSPF

如图,核心层和汇聚层之间通过内部的路由协议OSPF, 找到最佳的路径进行访问,而且还可以通过ECMP等价路由,在多个路由之间进行负载均衡和高可用。但是随着数据中心里的机器越来越多,尤其是有了云计算,大数据,集群规模非常大,而且都要求在一个二层网络里。这就需要二层互连从汇聚层上升为核心层, 也即在核心以下,全部是二层互连,全部在一个广播域里,这就是常说的大二层

大二层

如果大二层横向流量不大,核心交换机数目不多,可以做堆叠,但如果横向流量很大,仅仅堆叠满足不了,就需要部署多组核心交换机,而且要和汇聚层进行全互连。由于堆叠只解决一个核心交换机组内的无环问题,而组之间全互连,还需要其他机制进行解决。如果是STP,那部署多组核心无法扩大横向流量的能力,因为还是只有一组起作用。

于是大二层就引入了TRILL(Transparent Interconnection of Lots of Link),即多链路透明互联协议。它的基本思路是二层环有问题,三层环没有问题,就把三层的路由能力模拟在二层实现。运行TRILL协议的交换机称为RBridge, 是具有路由转发特性的网桥设备,只不过这个路由是根据MAC地址来的,不是根据IP来的。

RBridage之间通过链路状态协议运作。通过它可以学习整个大二层的拓扑,知道访问哪个MAC应该从哪个网桥走;还可以计算最短路径,也可以通过等价的路由进行负载均衡和高可用性。

TRILL

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和主机BMAC地址以及内层的VLAN

在外层首先加上一个TRILL头,里面描述这个包从RBridge 1进来的,要从RBridge 3出去,并且像三层的IP地址一样有跳数。然后再外面,目的MACRBridge 2, 源MAcRBridge 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防护等。这是整个数据中心的屏障,防止来自外来的攻击。核心交换机上往往还有负载均衡器。

在有的数据中心里,对于存储设备,还会有一个存储网络,用来连接SANNAS。但对于新的云计算来说,往往不使用传统的SANNAS,而使用部署在x86机器上的软件定义的存储,这样存储也是服务器了,而且可以和计算节点融合在一个机架上,从而更加有效率,也就没有了单独的存储网络了。

数据中心

这是一个典型的三层网络结构。这里的三层不是指IP层,而是指接入层,汇聚层,核心层三层。这种模式非常有利于外部流量请求到内部应用。这个类型的流量,是从外到内或从内到外,对应到上图,就是从上到下,从下到上,上北下南,所以称为南北流量

但是随着云计算和大数据的发展,节点之间的交互越来越多,例如大数据计算经常在不同节点将数据拷来拷去,这样需要经过交换机,使得数据从左到右,从右到左,左西右东,所以称为东西流量。为了解决东西流量问题,演进出了叶脊网络(Spine/Leaf):

叶脊网络

传统的三层网络架构是垂直结构,而叶脊网络架构是扁平的结构,更易于水平扩展。

VPN

有的公司有多个数据中心,需要将多个数据中心连接起来,或需要办公室和数据中心连接起来要怎么做?

VPN

VPN(Virtual Private Network, 虚拟专用网)就是利用开放的公众网络,建立专用数据传输通道,将远程的分支机构,移动办公人员等连接起来。

VPN是如何工作的?

VPN通过隧道技术在公众网络上仿真一条点到点的专线,是通过利用一种协议来传输另外一种协议的技术,这是涉及三种协议: 乘客协议, 隧道协议承载协议

IPsec

我们以IPsec协议来说:知道如何通过自驾进行海南游吗?这其中,车怎么通过琼州海峡?这里用到轮渡,其实就是隧道协议。在广州这边开车是有”协议”的,例如靠右行驶,红灯停,绿灯行,这就相当于”被封装”的乘客协议。当然在海南那面,开车也是同样的协议。这就相当于需要连接在一起的一个公司的两个分部。但在海上坐船航行,也有它的协议,例如要看灯塔,要按航道航行等。这就是外层的承载协议

那我的车如何从广州到海南呢? 这就需要遵循开车的协议,将车开上轮渡,所有通过轮渡的车都关在船舱里,按照既定的规则排列好,这就是隧道协议。在大海上,车是关在船舱里的,就像在隧道里一样,这时内部的乘客协议,没啥用处,只需要船遵从外层的承载协议,到达海南就可以了。到达之后,外部的承载协议的任务就结束了,打开船舱,将车开出来,就相当于取下承载协议隧道协议的头。接下来,在海南怎么开车,遵守内部的乘客协议就可以了。

前面说过,直接使用公网太不安全,所以接下来我们来看一种十分安全的VPN - IPsec VPN。这是基于IP协议的安全隧道协议, 为了保证在公网上面信息的安全,因而采用一定的机制来保证安全性:

如何保证对方局势真正的那个人?

基于以上三个特性,组成了IPsec VPN的协议簇

IPsec VPN

这个协议簇里,有两种协议,区别在于封装网络包的格式不一样:

这个协议簇里有两类算法,分别是加密算法摘要算法。这个协议簇还包含两大组件,一个用于VPN的双方要进行对称密钥的交换IKE组件, 另一个是VPN双方要对连接进行维护的SA(Security Association)组件

IPsec VPN 的建立过程

DH

到此客户端和服务端可以根据已有的信息,各自独立算出相同的结果K, 就是对称密钥。但这个过程,对称密钥从来没在通道上传输过,只传输了生成密钥的材料,通过这些材料,截获的人是无法算出的。

IPsec SA里有以下内容:

IPsce SA

IPsec建立好,接下来就可以开始打包封装传输了。

IPsce SA

左边是原始的IP包,在IP头里,会指定上一层协议为TCPESP要对IP包进行封装,因而IP头里的上一层协议为ESP。在ESP的正文里,ESP的头部有双方商讨好的SPI, 以及这次传输的序列号。接下来全部是加密的内容。可以通过对称加密进行解密,解密后在正文的最后,指明了里面的协议是什么。如果是IP, 则需要先解析IP头,然后解析TCP头,这是从隧道出来后解封装的过程。

有了IPsec VPN之后,客户端发送的明文的IP包,都会被加上ESP头和IP头,在公网上传输,由于加密,可以保证不被窃取,到了对端后,去掉ESP头,进行解密。

IPsce VPN

这种点对点的基于IPVPN, 能满足互通的要求,但是速度往往比较慢,这是由底层IP协议的特性决定的。IP不是面向连接的,是尽力而为的协议,每个IP包自由选择路径,到每个路由器,都自己去找下一跳,丢就丢了,是靠上层TCP的重发来保证可靠性的。

IP

因为IP网络从设计时,就认为是不可靠的,所以即使同一个连接,也可能选择不同的道路,这样的好处是,一条道路崩溃时,总有其他路可以走。当然,带来的代价就是,不断的路由查找,效率比较差。和IP对应的另一种技术称为ATM。这种协议和IP协议的不同在于,它是面向连接的。TCP也是面向连接的,但是ATMIP是一个层次,和TCP不是一个层次。 另外TCP所谓的面向连接,是不停的重试来保证成功,其实下层的IP还是不面向连接的。ATM是传输之前先建立一个连接,形成一个虚拟的通路,一旦连接建立,所有的包都按照相同的路径走,不会分头行事。

ATM

好处是不需要每次都查路由表,虚拟路径已经建立,打上标签了,后续的包傻傻的跟着走就是了,不用像IP包一样,每个包都思考下一步怎么走,都按相同的路径走,效率会高很多。但是一旦虚拟路径上的某个路由器坏了,则这个连接就断了,什么也发不过去了,因为其他的包会按原来的路径走,不会选择其他的路径。ATM技术虽然没有成功,但其摒弃了繁琐的路由查找,改为简单快速的标签交换,将具有全局意义的路由表改为只有本地意义的标签表,这些都可以大大提高一台路由器的转发功力。

多协议标签交换(MPLS, Multi-Protocol Label Switching)将两者的优点结合起来。MPLS在原始的IP头之外,多了MPLS头,里面可以打标签。

MPLS

在二层头里,有类型字段,0x0800表示IP, 0x8847表示MPLS Label。在MPLS头里,首先是标签值占20位,接着是3位实验位,再接下来是1位栈底标志位,表示当前标签是否位于栈底了。这样就允许多个标签被编码到同一个数据包中,形成标签栈。最后是8TTL存活时间字段,如果标签数据包的出发TTL值为0,那么该数据包在网络中的生命期被认为已过期。有了标签,还需要设备认这个标签,并且能够根据这个标签转发,这种能够转发标签的路由器称为标签交换路由器(LSR, Label Switching Router)

这种路由器会有两个表格,一个就是传统的FIB, 也即路由表,另一个是LFIB, 标签转发表。有了这两个表,既可以进行普通的路由转发,也可以进行基于标签的转发。

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), 是一个动态的生成标签的协议。其实LDPIP中的路由协议十分相似,通过LSR的交互,互相告知去哪里应该打哪个标签,称为标签分发,往往是从下游开始的。

LSP

如果有一个边缘节点发现自己的路由表中出现了新的目的地址,它就要给别人说,我能到达一条新的路径了。如果此边缘节点存在上游LSR, 并且尚有可供分配的标签,则该节点为新的路径分配标签,并向上游发出标签映射消息,其中包含分配的标签等信息。收到标签映射消息的LSR记录相应的标签映射信息,在其标签转发表中增加相应的条目。此LSR为它的上游LSR分配标签,并继续向上游LSR发送标签映射消息。当入口LSR收到标签映射消息时,在标签转发表中增加相应的条目。这时,就完成了LSP的建立。有了标签,转发轻松多了,但是这个和VPN有什么关系?

可以想象,如果我们VPN通道里包的转发,都通过标签的方式进行的,效率会高很多。所以要想个办法把MPLS应用于VPN

VPN

MPLS VPN中,网络中的路由器分成以下几类:

我们发现,在运营商网络里(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路由,在相互沟通的消息中,在一般32IPv4的地址之前加上一个客户标示的区分符用于客户地址的区分,这种称为VPN_IPv4地址族,这样PE路由器会收到如下消息,机构A192.168.101.0/24应该往这面走,机构B192.168.101.0/24则应该去另一个方向。

另外困惑的是路由表,当两个客户的IP包到达PE时,PE就困惑了,因为网段是重复的。如何区分那些路由是属于哪些客户VPN内的? 如何保证VPN业务路由与普通路由不相互干扰?

PE上,可以通过VRF(VPN Routing&Fowarding Instance)建立每个客户一个路由表,与其它VPN客户路由和普通路由相互区分。可以理解为专属于客户的小路由器。远端PE通过MP-BGP协议把业务路由放到近端PE, 近端PE根据不同的客户选择除相关客户的业务路由放到相应的VRF路由表中。

VPN报文转发采用两层标签方式:

VPN

MPLS VPN的发包过程:

ShunFa Zhang 29 June 2018
blog comments powered by Disqus