TCP之前端系列,知其然,知其所以然

225 阅读14分钟

1 TCP 和 UDP 的区别

基本的区别:

TCP是一个面向连接的、可靠的、基于字节流的传输层协议。

而UDP是一个面向无连接的传输层协议。然后 UDP 协议只是数据报文的搬运工,不保证有序且不丢失的传递到对端,并且UDP 协议也没有任何控制流量的算法,总的来说 UDP 相较于 TCP 更加的轻便。(就这么简单,其它TCP的特性也就没有了)。

具体来分析,和 UDP 相比,TCP 的三大核心特性:

  1. 面向连接。

所谓的连接,指的是客户端和服务器的连接,在双方互相通信之前,TCP 需要三次握手建立连接,

而 UDP 没有相应建立连接的过程,想发数据就可以开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作。

具体来说就是:

  • 在发送端,应用层将数据传递给传输层的 UDP 协议,UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了
  • 在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作
  1. 可靠性。

TCP 花了非常多的功夫保证连接的可靠,这个可靠性体现在哪些方面呢?一个是有状态,另一个是可控制。

TCP 会精准记录哪些数据发送了,哪些数据被对方接收了,哪些没有被接收到,而且保证数据包按序到达,不允许半点差错。这是有状态。

当意识到丢包了或者网络环境不佳,TCP 会根据具体情况调整自己的行为,控制自己的发送速度或者重发。这是可控制。

相应的,UDP 就是无状态, 不可控的。

首先不可靠性体现在无连接上,通信都不需要建立连接,想发就发,这样的情况肯定不可靠。

并且收到什么数据就传递什么数据,并且也不会备份数据,发送数据也不会关心对方是否已经正确接收到数据了。

再者网络环境时好时坏,但是 UDP 因为没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP。

  1. 面向字节流。

UDP 的数据传输是基于数据报的,这是因为仅仅只是继承了 IP 层的特性,而 TCP 为了维护状态,将一个个 IP 包变成了字节流。

  1. 高效

虽然 UDP 协议不是那么的可靠,但是正是因为它不是那么的可靠,所以也就没有 TCP 那么复杂了,需要保证数据不丢失且有序到达。

因此 UDP 的头部开销小,只有八字节,相比 TCP 的至少二十字节要少得多,在传输数据报文时是很高效的。

  1. 传输方式

UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。

udp适合使用的场景

UDP 虽然对比 TCP 有很多缺点,但是正是因为这些缺点造就了它高效的特性,在很多实时性要求高的地方都可以看到 UDP 的身影。

直播和王者荣耀,想必大家都看过直播吧,大家可以考虑下如果直播使用了基于 TCP 的协议会发生什么事情?

TCP 会严格控制传输的正确性,一旦有某一个数据对端没有收到,就会停止下来直到对端收到这个数据。这种问题在网络条件不错的情况下可能并不会发生什么事情,但是在网络情况差的时候就会变成画面卡住,然后再继续播放下一帧的情况。

但是对于直播来说,用户肯定关注的是最新的画面,而不是因为网络条件差而丢失的老旧画面,所以 TCP 在这种情况下无用武之地,只会降低用户体验。

2 TCP三次握手和四次挥手

tcp头部

image.png 对于 TCP 头部来说,以下几个字段是很重要的

  1. 序列号 seq(Sequence number)

指的是本报文段第一个字节的序列号,用来表示TCP源端向目标端发送的字节流,发起方发送数据时对此进行标记。

作用:

  • 这个序号保证了 TCP 传输的报文都是有序的,对端可以通过序号顺序的拼接报文
  • 在 SYN 报文中交换彼此的初始序列号。
  1. 确认号 ack(Acknowledgement Number) 只有ACK标识符为1时,确认序号字段才有效,确认方ack=发起方seq+1

用来告知对方下一个期望接收的序列号,小于ACK的所有字节已经全部收到。

Window Size,窗口大小,表示还能接收多少字节的数据,用于流量控制

标识符

  • URG=1:该字段为一表示本数据报的数据部分包含紧急信息,是一个高优先级数据报文,此时紧急指针有效。紧急数据一定位于当前数据包数据部分的最前面,紧急指针标明了紧急数据的尾部。
  • ACK=1:该字段为一表示确认号字段有效。此外,TCP 还规定在连接建立后传送的所有报文段都必须把 ACK 置为一。
  • PSH=1:该字段为一表示接收端应该立即将数据 push 给应用层,而不是等到缓冲区满后再提交。
  • RST=1:该字段为一表示当前 TCP 连接出现严重问题,可能需要重新建立 TCP 连接,也可以用于拒绝非法的报文段和拒绝连接请求。
  • SYN=1:当SYN=1,ACK=0时,表示当前报文段是一个建立连接请求报文。当SYN=1,ACK=1时,表示当前报文段是一个同意建立连接的应答报文。
  • FIN=1:该字段为一表示此报文段是一个释放连接的请求报文。

总结:

SYN表示建立连接,ACK表示确认序号有效、响应,FIN表示关闭连接,

位码即tcp标志位,有6种标示:SYN(synchronous建立连接) ACK(acknowledgement 确认) PSH(push传送) FIN(finish关闭连接) RST(reset重置) URG(urgent紧急)Sequence number(顺序号码) Acknowledge number(确认号码)

三次握手

三次握手过程

作用

进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。

流程

image.png

  1. 一开始双方处于 CLOSED 状态,然后服务端开始监听某个端口进入 LISTEN 状态
  2. 第一次握手,然后客户端主动发起连接,发送 SYN,初始序列号seq = x,然后自己变为 SYN-SENT
  3. 第二次握手,服务端收到之后,返回 SYN seq = y 和 ACK ack = x + 1(对于客户端发来的 SYN),自己变成 SYN-REVD
  4. 第三次握手,之后客户端再次发送 ACK seq = x + 1, ack = y + 1给服务端,自己变成 EASTABLISHED 状态,服务端收到 ACK,也进入 ESTABLISHED

另外需要提醒你注意的是,从图中可以看出,SYN 是需要消耗一个序列号的,下次发送对应的 ACK 序列号要加1,为什么呢?只需要记住一个规则:

SYN 需要对端确认,所以 ACK 的序列化要加一,凡是需要对端确认的,一定消耗TCP报文的序列号。

SYN 需要对端的确认, 而 ACK 并不需要,因此 SYN 消耗一个序列号而 ACK 不需要。

为什么不是两次

无法确认客户端的接收能力。

如果首先客户端发送了 SYN 报文,但是滞留在网络中,TCP 以为丢包了,然后重传,两次握手建立了连接。

等到客户端关闭连接了。但是之后这个包如果到达了服务端,那么服务端接收到了,然后发送相应的数据表,就建立了链接,但是此时客户端已经关闭连接了,所以带来了链接资源的浪费。

为什么不是四次

四次以上都可以,只不过 三次就够了

四次挥手

image.png

四次挥手过程

  1. 一开始都处于 ESTABLISH 状态,然后客户端发送 FIN 报文,带上 seq = p,状态变为 FIN-WAIT-1
  2. 服务端收到之后,发送 ACK 确认,ack = p + 1,然后进入 CLOSE-WAIT 状态
  3. 客户端收到之后进入 FIN-WAIT-2 状态
  4. 过了一会等数据处理完,服务端再次发送 FIN、ACK,seq = q,ack = p + 1,进入 LAST-ACK 阶段
  5. 客户端收到 FIN 之后,客户端收到之后进入 TIME_WAIT(等待 2MSL),然后发送 ACK 给服务端 ack = 1 + 1
  6. 服务端收到之后进入 CLOSED 状态

客户端这个时候还需要等待两次 MSL 之后,如果没有收到服务端的重发请求,就表明 ACK 成功到达,挥手结束,客户端变为 CLOSED 状态,否则进行 ACK 重发

为什么不是三次

因为服务端在接收到FIN, 往往不会立即返回FIN, 必须等到服务端所有的报文都发送完毕了,才能发FIN。因此先发一个ACK表示已经收到客户端的FIN,延迟一段时间才发FIN。这就造成了四次挥手。

如果是三次挥手会有什么问题?

如果是三次的话,那么服务端的 ACK 和 FIN 合成一个挥手,那么长时间的延迟可能让 TCP 以为 FIN 没有达到服务器端,然后让客户的不断的重发 FIN

为什么需要等待 2MSL(Maximum Segement Lifetime):

2MSL(Maximum Segement Lifetime)报文最大生存时间

因为如果不等待的话,如果服务端还有很多数据包要给客户端发,且此时客户端端口被新应用占据,那么就会接收到无用的数据包,造成数据包混乱,所以说最保险的方法就是等服务器发来的数据包都死翘翘了再启动新应用。

  • 1个 MSL 保证四次挥手中主动关闭方最后的 ACK 报文能最终到达对端
  • 1个 MSL 保证对端没有收到 ACK 那么进行重传的 FIN 报文能够到达

3 流量控制

TCP滑动窗口

你对 TCP 滑动窗口有了解嘛?

在 TCP 链接中,对于发送端和接收端而言,TCP 需要把发送的数据放到发送缓存区, 将接收的数据放到接收缓存区。而经常会存在发送端发送过多,而接收端无法消化的情况,所以就需要流量控制,就是在通过接收缓存区的大小,控制发送端的发送。如果对方的接收缓存区满了,就不能再继续发送了。而这种流量控制的过程就需要在发送端维护一个发送窗口,在接收端维持一个接收窗口。

TCP 滑动窗口分为两种: 发送窗口接收窗口

发送窗口

发送端的滑动窗口结构如下:

image.png

其中包含四大部分:

  • 已发送且已确认
  • 已发送但未确认
  • 未发送但可以发送
  • 未发送也不可以发送

其中有一些重要的概念,我标注在图中:

image.png

发送窗口就是图中被框住的范围。SND 即send, WND 即window, UNA 即unacknowledged, 表示未被确认,NXT 即next, 表示下一个发送的位置。

接收窗口

接收端的窗口结构如下:

image.png

REV 即 receive,NXT 表示下一个接收的位置,WND 表示接收窗口大小。

流量控制过程

这里我们不用太复杂的例子,以一个最简单的来回来模拟一下流量控制的过程,方便大家理解。

首先双方三次握手,初始化各自的窗口大小,均为 200 个字节。

假如当前发送端给接收端发送 100 个字节,那么此时对于发送端而言,SND.NXT 当然要右移 100 个字节,也就是说当前的可用窗口减少了 100 个字节,这很好理解。 现在这 100 个到达了接收端,被放到接收端的缓冲队列中。不过此时由于大量负载的原因,接收端处理不了这么多字节,只能处理 40 个字节,剩下的 60 个字节被留在了缓冲队列中。

注意了,此时接收端的情况是处理能力不够用啦,你发送端给我少发点,所以此时接收端的接收窗口应该缩小,具体来说,缩小 60 个字节,由 200 个字节变成了 140 字节,因为缓冲队列还有 60 个字节没被应用拿走。

因此,接收端会在 ACK 的报文首部带上缩小后的滑动窗口 140 字节,发送端对应地调整发送窗口的大小为 140 个字节。

此时对于发送端而言,已经发送且确认的部分增加 40 字节,也就是 SND.UNA 右移 40 个字节,同时发送窗口缩小为 140 个字节。

这也就是流量控制的过程。尽管回合再多,整个控制的过程和原理是一样的。

具体操作是,接收端主机向发送端主机通知自己可以接收数据的大小,于是发送端会发送不超过这个限度的数据,改大小限度就被称作窗口大小。

4 拥塞控制

拥塞控制原理

原因是有可能整个网络环境特别差,容易丢包,那么发送端就应该注意了。而这,也正是拥塞控制需要处理的问题。

主要有以下几种方法:

  • 慢启动+拥塞避免
  • 快速重传
  • 快速恢复复

慢启动 + 拥塞避免

对于拥塞控制来说,TCP 主要维护两个核心状态:

  • 拥塞窗口(cwnd)
  • 慢启动阈值(ssthresh)

在发送端使用拥塞窗口来控制发送窗口的大小。

然后采用一种比较保守的慢启动算法来慢慢适应这个网络,在开始传输的一段时间,发送端和接收端会首先通过三次握手建立连接,确定各自接收窗口大小,然后初始化双方的拥塞窗口,接着每经过一轮 RTT(收发时延),拥塞窗口大小翻倍,直到达到慢启动阈值。

然后开始进行拥塞避免,拥塞避免具体的做法就是之前每一轮 RTT,拥塞窗口翻倍,现在每一轮就加一个。

快速重传

在 TCP 传输过程中,如果发生了丢包,接收端就会重复发送之前 ACK,比如 第 5 个包丢了,6、7 达到,然后接收端会为 5,6,7 都发送第四个包的 ACK,这个时候发送端受到了 3 个重复的 ACK,意识到丢包了,就会马上进行重传,而不用等到 RTO (超时重传的时间)

选择性重传:报文首部可选性中加入 SACK 属性,通过 left edge 和 right edge 标志那些包到了,然后重传没到的包

快速恢复

如果发送端收到了 3 个重复的 ACK,发现了丢包,觉得现在的网络状况已经进入拥塞状态了,那么就会进入快速恢复阶段:

  1. 会将拥塞阈值降低为 拥塞窗口的一半
  2. 然后拥塞窗口大小变为拥塞阈值
  3. 接着 拥塞窗口再进行线性增加,以适应网络状况

参考资料

TCP协议灵魂之问,巩固你的网路底层基础
三次握手四次挥手