Jiang's blog

《趣谈网络协议》学习笔记之--最重要的传输层(上)

Word count: 5.9kReading time: 20 min
2020/03/22 Share

学习自极客时间《趣谈网络协议》 作者:刘超

1. UDP协议

1.1 TCP 和 UDP的区别

  • TCP 是面向连接的,UDP 是面向无连接的。

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

  • TCP提供可靠交付,通过 TCP 连接传输的数据,无差错、不丢失、不重复、并且按序到达。UDP 继承了 IP 包的特性,不保证不丢失,不保证按顺序到达。

  • TCP是面向字节流的,而 UDP 继承了 IP 的特性,基于数据报的,一个一个地发,一个一个地收。

  • TCP 是可以有拥塞控制的。它意识到包丢弃了或者网络的环境不好了,就会根据情况调整自己的行为,看看是不是发快了,要不要发慢点。UDP 就不会,应用让我发,我就发,管它洪水滔天。

  • TCP 其实是一个有状态服务,精确地记着哪个包发送了没有,接收到没有,发送到哪个了,应该接收哪个了,错一点儿都不行。而 UDP 则是无状态服务。通俗地说是没脑子的,天真无邪的,发出去就发出去了。

1.2 UDP 包头是什么样的?

8I6Wan.png

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

1.3 UDP 的三大使用场景

  1. 需要资源少,在网络情况比较好的内网,或者对于丢包不敏感的应用。DHCP 就是基于 UDP 协议的。一般的获取 IP 地址都是内网请求,而且一次获取不到 IP 又没事,过一会儿还有机会。PXE 可以在启动的时候自动安装操作系统,操作系统镜像的下载使用的 TFTP,这个也是基于 UDP 协议的。在还没有操作系统的时候,客户端拥有的资源很少,不适合维护一个复杂的状态机,而且因为是内网,一般也没啥问题。

  2. 不需要一对一沟通,建立连接,而是可以广播的应用。UDP 的不面向连接的功能,可以使得可以承载广播或者多播的协议。DHCP 就是一种广播的形式,就是基于 UDP 协议的。

  3. 需要处理速度快,时延低,可以容忍少数丢包,但是要求即便网络拥塞,也毫不退缩,一往无前的时候。

1.4 基于 UDP 的例子

  1. 网页或者 APP 的访问
    原来访问网页和手机 APP 都是基于 HTTP 协议的。HTTP 协议是基于 TCP 的,建立连接都需要多次交互,对于时延比较大的目前主流的移动互联网来讲,建立一次连接需要的时间会比较长,然而既然是移动中,TCP 可能还会断了重连,也是很耗时的。而且目前的 HTTP 协议,往往采取多个数据通道共享一个连接的情况,这样本来为了加快传输速度,但是 TCP 的严格顺序策略使得哪怕共享通道,前一个不来,后一个和前一个即便没关系,也要等着,时延也会加大。

  2. 流媒体的协议
    直播协议多使用 RTMP,而这个 RTMP 协议也是基于 TCP 的。TCP 的严格顺序传输要保证前一个收到了,下一个才能确认,如果前一个收不到,下一个就算包已经收到了,在缓存里面,也需要等着。对于直播来讲,这显然是不合适的,因为老的视频帧丢了其实也就丢了,就算再传过来用户也不在意了,他们要看新的了,如果老是没来就等着,卡顿了,新的也看不了,那就会丢失客户,所以直播,实时性比较比较重要,宁可丢包,也不要卡顿的。还有就是当网络不好的时候,TCP 协议会主动降低发送速度,这对本来当时就卡的看视频来讲是要命的,应该应用层马上重传,而不是主动让步。因而,很多直播应用,都基于 UDP 实现了自己的视频传输协议。

  3. 实时游戏
    游戏对实时要求较为严格的情况下,采用自定义的可靠 UDP 协议,自定义重传策略,能够把丢包产生的延迟降到最低,尽量减少网络问题对游戏性造成的影响。

  4. IoT 物联网
    一方面,物联网领域终端资源少,很可能只是个内存非常小的嵌入式系统,而维护 TCP 协议代价太大;另一方面,物联网对实时性要求也很高,而 TCP 还是因为上面的那些原因导致时延大。Google 旗下的 Nest 建立 Thread Group,推出了物联网通信协议 Thread,就是基于 UDP 协议的。

  5. 移动通信领域
    在 4G 网络里,移动流量上网的数据面对的协议 GTP-U 是基于 UDP 的。因为移动网络协议比较复杂,而 GTP 协议本身就包含复杂的手机上线下线的通信协议。如果基于 TCP,TCP 的机制就显得非常多余。

1.5 问题思考

都说 TCP 是面向连接的,在计算机看来,怎么样才算一个连接呢?

  • TCP/UDP建立连接的本质就是在客户端和服务端各自维护一定的数据结构(一种状态机),来记录和维护这个“连接”的状态 。并不是真的会在这两个端之间有一条类似“网络专线”这么一个东西。
  • 在IP层,网络情况该不稳定还是不稳定,数据传输走的是什么路径上层是控制不了的,TCP能做的只能是做更多判断,更多重试,更多拥塞控制之类的东西。

2. TCP协议(上)

2.1 TCP 包头格式

8IWNN9.png

  • 源端口和目的端口: 两个应用程序通讯少不了端口
  • *序号和确认序号: *编号是为了解决乱序问题。发出去的包应该有确认,如果没有收到就应该重新发送,直到送达。确认序号可以解决不丢包的问题。
  • *状态位: *SYN 是发起一个连接,ACK 是回复,RST 是重新连接,FIN 是结束连接等。TCP 是面向连接的,因而双方要维护连接的状态,这些带状态位的包的发送,会引起双方的状态变更。
  • *窗口大小: *TCP 要做流量控制,通信双方各声明一个窗口,标识自己当前能够的处理能力,别发送的太快,也别发的太慢。除了做流量控制以外,TCP 还会做拥塞控制。

2.2 TCP三次握手

  1. 一开始,客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端口,处于 LISTEN 状态。然后客户端主动发起连接 SYN,之后处于 SYN-SENT 状态。
  2. 服务端收到发起的连接,返回 SYN,并且 ACK 客户端的 SYN,之后处于 SYN-RCVD 状态。
  3. 客户端收到服务端发送的 SYN 和 ACK 之后,发送 ACK 的 ACK,之后处于 ESTABLISHED 状态,因为它一发一收成功了。服务端收到 ACK 的 ACK 之后,处于 ESTABLISHED 状态,因为它也一发一收了。

为了维护这个连接,双方都要维护一个状态机,在连接建立的过程中,双方的状态变化时序图就像这样。
8IO4FP.png

三次握手除了双方建立连接外,主要还是为了沟通一件事情,就是 TCP 包的序号的问题。每个连接都要有不同的序号。这个序号的起始序号是随着时间变化的,可以看成一个 32 位的计数器,每 4ms 加一,如果计算一下,如果到重复,需要 4 个多小时,那个绕路的包早就死翘翘了,因为我们都知道 IP 包头里面有个 TTL,也即生存时间。

为什么要三次,而不是两次?按说两个人打招呼,一来一回就可以了啊?为了可靠,为什么不是四次?

  1. 假设A要与B通讯,当A发起第一个请求是,可能请求到达不了B,或者B没有响应,这时候A就会选择继续重发,终于B收到了A的消息,知道A想要连接,但是A还不知道,可能还会继续重发。

  2. B 收到了请求包,就知道了 A 的存在,并且知道 A 要和它建立连接。如果 B 不乐意建立连接,则 A 会重试一阵后放弃,连接建立失败,没有问题;如果 B 是乐意建立连接的,则会发送应答包给 A。当然对于 B 来说,这个应答包也是一入网络深似海,不知道能不能到达 A。这个时候 B 自然不能认为连接是建立好了,因为应答包仍然会丢,会绕弯路,或者 A 已经挂了都有可能。

  3. 而且这个时候 B 还能碰到一个诡异的现象就是,A 和 B 原来建立了连接,做了简单通信后,结束了连接。还记得吗?A 建立连接的时候,请求包重复发了几次,有的请求包绕了一大圈又回来了,B 会认为这也是一个正常的的请求的话,因此建立了连接,可以想象,这个连接不会进行下去,也没有个终结的时候,纯属单相思了。因而两次握手肯定不行。

  4. 当然 A 发给 B 的应答之应答也会丢,也会绕路,甚至 B 挂了。按理来说,还应该有个应答之应答之应答,这样下去就没底了。所以四次握手是可以的,四十次都可以,关键四百次也不能保证就真的可靠了。只要双方的消息都有去有回,就基本可以了。

2.3 TCP四次挥手

  1. 断开的时候,客户端首先发起FIN请求,接着进入FIN_WAIT_1 的状态。服务器接收到客户端的FIN的请求后,就ACK客户端的请求,然后进入CLOSE_WAIT 的状态。
  2. 客户端收到服务器的ACK后,便进入FIN_WAIT_2 的状态,如果这个时候 服务器 直接跑路,则客户端将永远在这个状态。TCP 协议里面并没有对这个状态的处理,但是 Linux 有,可以调整 tcp_fin_timeout 这个参数,设置一个超时时间。
  3. 服务器也想断开连接,便向客户端发送FIN,表示要结束连接。
  4. 客户端收到服务器发来的FIN,也向服务器发送一个ACK,确认收到了服务器的请求,便从 FIN_WAIT_2 状态结束。为了确保服务器能够收到这个ACK,TCP 协议要求客户端最后等待一段时间 TIME_WAIT,这个时间要足够长,长到如果服务器没收到 ACK 的话,服务器会重发FIN,客户端收到后也会重新发一个 ACK 并且足够时间到达服务器。而且如果客户端没有等待足够的时间,端口就直接空出来了,但是服务器不知道,服务器原来发过的很多包很可能还在路上,如果客户端的端口被一个新的应用占用了,这个新的应用会收到上个连接中服务器发过来的包。等待的时间设为 2MSL,MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。

8Iz25R.png

2.4 TCP 状态机

8oSiIs.jpg

2.5 问题思考

  1. TCP 的连接有这么多的状态,如何在系统中查看某个连接的状态?
  • netstat可以在系统中查看某个连接的状态。

2.如何避免TCP的TIME_WAIT状态(高并发)?
首先服务器可以设置SO_REUSEADDR套接字选项来通知内核,如果端口忙,但TCP连接位于TIME_WAIT状态时可以重用端口。在一个非常有用的场景就是,如果你的服务器程序停止后想立即重启,而新的套接字依旧希望使用同一端口,此时SO_REUSEADDR选项就可以避免TIME_WAIT状态。
如何避免TCP的TIME_WAIT状态(高并发)

  1. 服务器遭到SYN攻击怎么办?如何防御SYN攻击?

TCP SYN flood洪水攻击原理和防御破解

针对SYN洪水攻击的防御措施


3. TCP 协议(下)

3.1 如何实现一个靠谱的协议?

为了保证顺序性,每一个包都有一个 ID。在建立连接的时候,会商定起始的 ID 是什么,然后按照 ID 一个个发送。为了保证不丢包,对于发送的包都要进行应答,但是这个应答也不是一个一个来的,而是会应答某个之前的 ID,表示都收到了,这种模式称为累计确认或者累计应答(cumulative acknowledgment)。

为了记录所有发送的包和接收的包,TCP 也需要发送端和接收端分别都有缓存来保存这些记录。发送端的缓存里是按照包的 ID 一个个排列,根据处理的情况分成四个部分。

  • 第一部分, 发送并确认的。
  • 第二部分, 发送了尚未确认的。
  • 第三部分, 没有发送,但是已经等待发送的。
  • 第四部分, 没有发送,并且暂时还不会发送的。

区分第三和第四部分是因为要流量控制,把握分寸,根据窗口的工作量先评估一下,多了就加,少了就减。

在 TCP 里,接收端会给发送端报一个窗口的大小,叫 Advertised window。这个窗口的大小应该等于上面的第二部分加上第三部分。超过这个窗口的,接收端做不过来,就不能发送了。

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

8oPvDK.jpg

接收方的数据结构如下:

8oFVzR.jpg

AdvertisedWindow=MaxRcvBuffer-((NextByteExpected-1)-LastByteRead)。

其中第二部分里面,由于受到的包可能不是顺序的,会出现空挡,只有和第一部分连续的,可以马上进行回复,中间空着的部分需要等待,哪怕后面的已经来了。

3.2 顺序问题与丢包问题

发送方和接收方可能会出现以下情况:

  • 接收方接受到了包并发送了相应的ACK,但是可能在路上丢失了.
  • 接收方接受了包但是这个包的前一个包没有接收到,出现了乱序,所以只能缓存着不能ACK

顺序问题和丢包问题都有可能发生,所以我们先来看确认与重发的机制。

  1. 一种方法就是超时重试,也即对每一个发送了,但是没有 ACK 的包,都有设一个定时器,超过了一定的时间,就重新尝试。
    估计往返时间,需要 TCP 通过采样 RTT 的时间,然后进行加权平均,算出一个值,而且这个值还是要不断变化的,因为网络状况不断的变化。除了采样 RTT,还要采样 RTT 的波动范围,计算出一个估计的超时时间。由于重传时间是不断变化的,我们称为自适应重传算法。

每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。
有一个可以快速重传的机制,当接收方收到一个序号大于下一个所期望的报文段时,就会检测到数据流中的一个间隔,于是它就会发送冗余的 ACK,仍然 ACK 的是期望接收的报文段。而当客户端收到三个冗余的 ACK 后,就会在定时器过期之前,重传丢失的报文段

  1. 还有一种方式称为 Selective Acknowledgment (SACK)。这种方式需要在 TCP 头里加一个 SACK 的东西,可以将缓存的地图发送给发送方。例如可以发送 ACK6、SACK8、SACK9,有了地图,发送方一下子就能看出来是 7 丢了。

3.3 流量控制问题

  • 当发送方发送包的数量过大,例如将第三部分没有发送但等待发送的包一次性发完,这时候只有当发送方收到接收方发来的ACK,才能继续发送包。
  • 如果接收方实在处理的太慢,导致缓存中没有空间了,可以通过确认信息修改窗口的大小,甚至可以设置为 0,则发送方将暂时停止发送。如果接收端还是一直不处理数据,则随着确认的包越来越多,窗口越来越小,直到为 0。
  • 发送方会定时发送窗口探测数据包,看是否有机会调整窗口的大小。当接收方比较慢的时候,要防止低能窗口综合征,别空出一个字节来就赶快告诉发送方,然后马上又填满了,可以当窗口太小的时候,不更新窗口,直到达到一定大小,或者缓冲区一半为空,才更新窗口。

3.4 拥塞控制问题

拥塞控制的问题,也是通过窗口的大小来控制的,前面的滑动窗口 rwnd 是怕发送方把接收方缓存塞满,而拥塞窗口 cwnd,是怕把网络塞满。

  • 原来发送一个包,从一端到达另一端,假设一共经过四个设备,每个设备处理一个包时间耗费 1s,所以到达另一端需要耗费 4s,如果发送的更加快速,则单位时间内,会有更多的包到达这些中间设备,这些设备还是只能每秒处理一个包的话,多出来的包就会被丢弃,这是我们不想看到的。
  • 这个时候,我们可以想其他的办法,例如这个四个设备本来每秒处理一个包,但是我们在这些设备上加缓存,处理不过来的在队列里面排着,这样包就不会丢失,但是缺点是会增加时延,这个缓存的包,4s 肯定到达不了接收端了,如果时延达到一定程度,就会超时重传,也是我们不想看到的。

TCP 的拥塞控制主要来避免两种现象,包丢失和超时重传。一旦出现了这些现象就说明,发送速度太快了,要慢一点。

窗口调整的办法: 慢启动

  1. 一条 TCP 连接开始,cwnd 设置为一个报文段,一次只能发送一个;当收到这一个确认的时候,cwnd 加一,于是一次能够发送两个;当这两个的确认到来的时候,每个确认 cwnd 加一,两个确认 cwnd 加二,于是一次能够发送四个;当这四个的确认到来的时候,每个确认 cwnd 加一,四个确认 cwnd 加四,于是一次能够发送八个。可以看出这是指数性的增长。
  2. 有一个值 ssthresh 为 65535 个字节,当超过这个值的时候,就要小心一点了,不能倒这么快了,要慢下来。每收到一个确认后,cwnd 增加 1/cwnd,我们接着上面的过程来,一次发送八个,当八个确认到来的时候,每个确认增加 1/8,八个确认一共 cwnd 增加 1,于是一次能够发送九个,变成了线性增长。但是始终都会溢出。
  • 快速重传算法。当接收端发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速的重传,不必等待超时再重传。TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,这时候就将cwnd 减半为 cwnd/2,然后 sshthresh = cwnd,当三个包返回的时候,cwnd = sshthresh + 3,也就是没有一夜回到解放前,而是还在比较高的值,呈线性增长。

但是还是会出现问题;

  • 第一个问题是丢包并不代表着通道满了,也可能是管子本来就漏水。例如公网上带宽不满也会丢包,这个时候就认为拥塞了,退缩了,其实是不对的。
  • 第二个问题是 TCP 的拥塞控制要等到将中间设备都填充满了,才发生丢包,从而降低速度,这时候已经晚了。其实 TCP 只要填满管道就可以了,不应该接着填,直到连缓存也填满。

为了优化这两个问题,后来有了 TCP BBR 拥塞算法。它企图找到一个平衡点,就是通过不断的加快发送速度,将管道填满,但是不要填满中间设备的缓存,因为这样时延会增加,在这个平衡点可以很好的达到高带宽和低时延的平衡。

3.5 问题思考

TCP 的 BBR 听起来很牛,你知道他是如何达到这个最优点的嘛?

1 设备缓存会导致延时?
假如经过设备的包都不需要进入缓存,那么得到的速度是最快的。进入缓存且等待,等待的时间就是额外的延时。BBR就是为了避免这些问题:
充分利用带宽;降低buffer占用率。

2 降低发送packet的速度,为何反而提速了?
标准TCP拥塞算法是遇到丢包的数据时快速下降发送速度,因为算法假设丢包都是因为过程设备缓存满了。快速下降后重新慢启动,整个过程对于带宽来说是浪费的。通过packet速度-时间的图来看,从积分上看,BBR充分利用带宽时发送效率才是最高的。可以说BBR比标准TCP拥塞算法更正确地处理了数据丢包。对于网络上有一定丢包率的公网,BBR会更加智慧一点。
回顾网络发展过程,带宽的是极大地改进的,而最小延迟会受限与介质传播速度,不会明显减少。BBR可以说是应运而生。

3 BBR如何解决延时?
S1:慢启动开始时,以前期的延迟时间为延迟最小值Tmin。然后监控延迟值是否达到Tmin的n倍,达到这个阀值后,判断带宽已经消耗尽且使用了一定的缓存,进入排空阶段。
S2:指数降低发送速率,直至延迟不再降低。这个过程的原理同S1
S3:协议进入稳定运行状态。交替探测带宽和延迟,且大多数时间下都处于带宽探测阶段。

CATALOG
  1. 1. 1. UDP协议
    1. 1.1. 1.1 TCP 和 UDP的区别
    2. 1.2. 1.2 UDP 包头是什么样的?
    3. 1.3. 1.3 UDP 的三大使用场景
    4. 1.4. 1.4 基于 UDP 的例子
    5. 1.5. 1.5 问题思考
  2. 2. 2. TCP协议(上)
    1. 2.1. 2.1 TCP 包头格式
    2. 2.2. 2.2 TCP三次握手
    3. 2.3. 2.3 TCP四次挥手
    4. 2.4. 2.4 TCP 状态机
    5. 2.5. 2.5 问题思考
  3. 3. 3. TCP 协议(下)
    1. 3.1. 3.1 如何实现一个靠谱的协议?
    2. 3.2. 3.2 顺序问题与丢包问题
    3. 3.3. 3.3 流量控制问题
    4. 3.4. 3.4 拥塞控制问题
    5. 3.5. 3.5 问题思考