详解 TCP 协议

一、TCP 是什么

TCP:Transmission Control Protocol ( 传输控制协议 )

TCP 协议和 UDP 协议一样,位于传输层。

我们都知道协议通俗来说,就是对于传输一种约定,将发送方和接收方在某些方面达成共识,才能够正确传输。

二、TCP 特性

特点:

有连接
可靠传输
面向字节流
全双工

TCP 的核心特性在于可靠传输,此外, TCP 也能够做到提升效率,而可靠性和高效率本身就是一个矛盾,因为可靠性一般就要消耗资源或时间来实现,而高效率又更注重速度。所以本篇博客基于 TCP 的机制来探讨可靠性和效率的这两个特性。

三、TCP协议段格式

1

四、TCP 的十个机制

1. 确认应答机制

TCP 的核心特性在于可靠传输,而可靠传输的核心在于确认应答机制,但这个机制在面试中考的却不多。

确认应答机制:发送方发送数据给接收方后,接收方收到数据就回应一个应答报文,我们称这个应答报文为 ACK. 如果发送方收到了 ACK,那么认为是对方已经收到了。

(1) 理解发送方和接收方之间的交互

情况一:正常情况

A 给 B 连续发了两条信息,第一条:今天晚饭吃的好吗?第二条:明天一起吃饭吗?
然后 B 给 A 连续回了两条信息,第一条:不好。第二条:好。

那么从情况一的结果来看,B 答应了 A 明天要一起吃饭。

1

情况二:后发先至

A 给 B 连续发了两条信息,第一条:今天晚饭吃的好吗?第二条:明天一起吃饭吗?
从 B 的角度看:B 给 A 连续回了两条信息,第一条:不好。第二条:好。
但由于网络通信未保证实时性,可能会造成后发先至的情况。
即最终以 A 的角度看:B 给 A 连续回了两条信息,第一条:好。第二条:不好。

对于情况二这就出错了,B 答应了 A 明天一起吃饭,而实际上 A 以为 B 拒绝了明天的饭局。

2

情况三:对数据进行编号

对数据进行编号,一定需要占用内存,那么实际上 TCP 的报头中,【 32 位确认序号 】就表示这些编号。通过编号的这一机制,我们最起码就不会因为受网络波动的影响,从而使传输方和接收方关联的信息不匹配。以此就能来确认应答了。

3

前面说到,确认应答机制是实现 TCP 可靠性传输的核心。试想:就像我们举的例子,如果没有对应编号,在日常人们使用网络通信的生活中,很可能就会造成发送方和接收方之间的误会。那么确认应答机制的设计还是十分有意义的。

(2) 确认应答机制是如何实现的

我们都知道,TCP 传输数据的时候是面向字节流的,此外,我刚刚提到,TCP 这样的编号实际上是在 TCP 的报头中存放着,因为它也需要占用空间。所以,我们就必须确定:TCP 的序号和确认序号也是以字节为单位进行编号的。详情如下:

发送方在给接收方发送数据的时候,需要加上编号,接着,接收方在接收一串完整数据后,也要进行确认,这就是应答机制的思想,以此来确认数据接收到。这就和别人喊你名字一样,【Hi~ 小明】你也应该礼貌地答应。

而像这样的确认应答实际上是 ACK 数据,也就是说,每一个 ACK 所表示的就是确认应答的意思。它存在的意义就是方便接收方告诉发送方,刚刚收到的数据,接收方准确无误地收到了,以此来让发送方确认传输无误,准备下一条指令。

在下图中,若发送的数据编号为 1 - 1000,那么确认序号为 1001;若发送的数据编号为 1001 - 2000,那么确认序号为 2001.
当确认应答 1001,并返回 ACK 后,主机A 就知道了 1 - 1000 的数据在传输过程中,没有问题了,于是就立即发送 1001 - 2000 的这组数据,之后,接收方再通过确认序号 2001 继续确认…

4

2. 超时重传机制

由于网络环境是比较复杂的,因此可能会有丢包的情况出现,虽然出现丢包的概率并不大,但在传输过程中出现丢包就会影响我们日常的网络通信,所以必须解决这一问题。

( 在网络通信中,丢包是指一个或多个数据包的数据无法透过网上到达目的地。比方说:你玩 MOBA 类游戏,当你在团战中放大招之后,你当前电脑显示放了,但之后,因为网络丢包,对面并没有因为你的大招掉伤害,你就会发现刚刚的大招实际并没有放出来,由于网络突然卡了或其他原因,你发现你放的大招并没有将数据从网络传输过去。 )

一旦数据发生丢包,就要进入超时重传的机制中。对于刚刚的打 MOBA 类游戏的例子来说,这就会造成延迟。

(1) 两种丢包的场景

1

(2) 超时重传机制是如何实现的

① 按照编号来重传数据以防数据重复

站在刚刚发送方 A 的角度看待数据传输的问题,对于上面的两种情况,发送方都是一脸懵逼的,他其实是无法确定到底数据在传过去的时候出现了问题,还是数据在传回来的时候出现了问题。

所以重传机制就明确:发送方需要间隔一段时间再次重传,而站在接收方的角度也是一样的。当站在接收方的角度返回数据,那么接收方就变成了发送方。此外,发送方和接收方都按照编号逻辑进行判断是否需要重传数据,这样就可以避免重传数据发生重复的现象。

在下图的两种情况中

  • 情况一:左边的例子,主机A 在给 主机B 传输数据的时候,发生丢包,此时,重传数据是没有问题的。因为这并不会造成结果有误,之后会消耗一定的时间而已。

  • 情况二:右边的例子,主机A 在给 主机B 传输数据的时候,一切正常;但主机B 在返回 ack 的时候,应答报文出现丢包情况。此时,主机B 已经接收到了 1 - 1000 数据,如果再重传就会发生数据重叠的情况。所以说,给数据编号,有效地解决了这一问题。当 1 - 1000 的数据被编好号了,那么主机B 就接收到了,它就需要应答 1001,这时候,主机B 只需要重传应答的 ack 即可。

3

② 明确超时重传的时间

不同的操作系统对于超时重传的时间实现的方式不同。
Linux中(BSD Unix 和 Windows也是如此),超时以 500ms 为一个单位进行控制,每次判定
超时重发的超时时间都是 500ms 的整数倍。
如果重发一次之后,仍然得不到应答,等待 2*500ms 后再进行重传。
如果仍然得不到应答,等待 4 * 500ms 进行重传。以此类推,以指数形式递增。

我们都知道 500ms 即 0.5s,那么 0.5s,作为用户的主观感受,可能是一个比较短的时间,但相对于计算机的网络通信,0.5s 却是一个很长的时间。玩 MOBA 类游戏的小伙伴可以试想,一波团战从开始到结束,可能 10s 不到,那么在这 10s 内,你想要主动开团,可能摁个大招就行,1s 都不要。但如果因为数据丢包,那么这波团战损失就很大了,因为多个 0.5s 就让你错失了很多开团、输出、切后排等等的机会!

如果 2 -3 次超时重传之后,却发现存在连续丢包的情况,很可能就是因为接收方的网络出现物理层面等故障,此时发送方和接收方就会关闭当前网络连接,尝试重新连接。

这很好理解,就和我们打 MOBA 类游戏一样,你摁大招摁不出来,摁闪现也摁不出来,发信息沟通也发不出来,那指定是断网了…最终我们只能检查网络,然后把当前游戏进程退了,重新进入游戏…

3. 连接管理机制 (面试最常考)

在之前提到 TCP 的特性时,我们知道 TCP 是有连接的,也就是说:等两边连接好了,客户端与服务器再进行通信,那么连接是怎么进行的呢?

连接管理机制就是:建立连接需要三次握手,断开连接需要四次挥手。而这就是网络部分的最高频问题,没有之一。

(1) 三次握手

想象一个场景:

A(客户端)							B(服务器)

明天一起吃饭吗?		

									好的,时间地点告诉我
									
明天晚上,南门烧烤店不见不散

1

syn 表示请求连接
正常情况下 syn = 0,在尝试建立连接的请求中,syn = 1

ack 表示确认连接
正常情况下 ack = 0,在确认请求的连接时,ack = 1

① 以打电话的形式来详解三次握手过程

打电话的情况完全符合三次握手的逻辑,读者可以自身体会日常的通信过程。

2

② 两个状态

  • LISTEN:听。
    顾名思义,它表示服务器的待连接状态,在 Java 中,当我们创建好 ServerSocket 实例的时候,就进入了此状态。例如:在 A 给 B 打电话的过程中,A 拨号成功,听筒出现了【嘟嘟嘟…】的声音,即 A 尝试与 B 对话的状态,并没有接通,即此状态。

  • ESTABLISHED:已确立的。
    顾名思义,它表示客户端或服务器已建立连接的状态,在 Java 中,当我们代码中使用 accept 返回一个 Socket 对象的时候,就进入了此状态。例如:在 A 给 B 打电话的时候,A 与 B 已经开始愉快地沟通起来了,即做到了真正意义上的通话,即此状态。

1

③ 关于三次握手的一些问题

问题一:三次握手的功能与意义 ?

答:三次握手就是建立连接与确立连接的过程,相当于投石问路,通过三次握手的过程,来确认 A和 B 之间的网络传输是否是通畅的,尤其是要确认,A 和 B 各自的发送能力和接收能力是否正常,这在上面的打电话例子中已经形象地体现出来了。

问题二:整个连接的过程可以进行四次握手吗 ?

答:在 B 给 A 传送的 【ack + syn】中,将这两步拆解开来再看,其实就是四次握手了。所以说,四次握手本质上可以达到建立连接、确认连接的目的,但三次握手更加高效!在网络传输中,将过程拆解,那么就涉及到了封装与分用,以此带来的结果就是:传输的开销更大。

问题三:整个连接的过程可以只进行两次握手吗 ?

答:答案是否定的!因为在打电话的例子中,无论你缺了哪一步,都无法让双方都确认彼此的发送与接收状态是否完好。所以说,三次握手是至少的,也是连接的必须步骤。

(2) 四次挥手

顾名思义,三次握手表明尝试建立连接的过程,那么四次挥手就表明尝试断开连接的过程。但我们必须明确:三次握手,必须是客户端先发起请求,而四次挥手,客户端或服务器都能够发起请求,这就是客户端与服务器的特性。

1

可以看到,此处的 ACK 和 FIN 是分开的,在这一点上,它与三次挥手不同,而这是因为,对于服务器 B 来说,ACK 和 FIN 的触发时机是不一样的。

① 当 B 收到 A 发出的 FIN 后,才回应 ACK,这个回应的步骤实际上是在系统的内核中完成的。
② 当 B 回应 ACK 之后,才发送 FIN,而发送 FIN 这个步骤实际上是通过用户代码控制的,在使用类似于 socket.close() 这样的代码时,即发送 FIN。

① 注意

我们必须明确一件事情,FIN 的触发,虽然是通过 socket.close() 这个代码来实现的,但这个代码实际上又与内核中释放了对应的进程 PCB 的文件描述符相关联。

我们试想一种情况:在 Java 中,代码没有调用 close,但是 socket 对象被垃圾回收机制回收了,这也是会关闭并释放对应的文件描述符的,当然,这种情况相比于使用 close 方法要来得慢。

我们再试想另一种情况:代码没有调用 close,但进程通过其他方式结束了。而当前进程结束,其在内核中对应的 PCB 就要被销毁,那么,PCB 中的文件描述符表就要被销毁,所以文件描述符也就被销毁了,最终同样也会触发 FIN 。

所以说,传统意义上的四次挥手是一个 TCP 较为正常的流程,但实际上,上面所说的情况,就证明了:有时候,TCP 连接也会异常断开。这很好理解:正如我们通过网络使用微信通话一样,建立连接,必须要满足,【 双方网络通畅、两边的人同时能够听、能够说… 】,而断开连接,【可能有一个人断网了,也可能两个人同时挂断电话,结束通话】…

所以说, TCP 建立连接的过程需要满足很多要求,而断开连接的过程中除了正常流程外,也还有异常、未知的情况。

② 说明两个状态

  • CLOSE_WAIT:服务器 B 收到客户端 A 发来的 FIN 之后,进入的状态。

此状态正在等待用户代码调用 close,以此来发送 FIN,顾名思义,它是在等待被关闭。

  • TIME_WAIT:表示客户端 A 收到了服务器 B 发来的 FIN 之后进入的状态。

此状态存在的意义主要是为了处理最后一个 ACK 丢包问题。
说明:即使进程已经退出了,TIME_WAIT 状态仍然会存在,即客户端与服务器之间的 TCP 连接不会立即销毁。TIME_WAIT 的存在会等待一定的时间,如果一定时间之内也没有重传的 FIN 过来,才会真正销毁。这个机制用来防止最后一步的丢包问题。
等待时间一般为:2 * MSL,而在 Linux 中这个 MSL 默认是1min,当然,这个 MSL 可以根据程序员自己配置。

所以说,如果服务器上出现大量的 CLOSE_WAIT,这是什么情况呢?这其实是代码出现了问题,close 没有及时被调用到,所以服务器一直在等待被关闭。

③ 拓展

使用 TCP 的时候,我们都是先关闭客户端,再关闭服务器,这是为什么?

我们必须明确:客户端和服务器谁先关闭,谁就进入 TIME_ WAIT 状态。
如果我们先关闭服务器,那么服务器就会进入到 TIME_ WAIT 状态,而原来的连接占据着端口,接下来如果服务器重新启动,新的进程又会尝试重新绑定当前被占据的端口。而我们都知道,端口号就好像一个人的电话号码,是唯一的。因此,如果我们先关闭服务器,再去关闭客户端,可能就会存在端口绑定失败的情况。

4. 滑动窗口

前三个机制的设定是为了保证 TCP 传输的可靠性,而滑动窗口机制的设定是为了在可靠性的基础上,提升通信效率。

在确认应答策略中,客户端每发送一个数据段给服务器的时候,服务器都要给一个 ACK 确认应答。当客户端收到 ACK 应答后,它就知道了服务器已经收到了刚刚的数据段,于是客户端才能再次发送下一个数据段。应答机制这样设定当然没问题,但这样做有一个缺点,就是性能较差,效率较低。尤其是数据往返时间较长的时候,客户端可能需要等很长时间才能进行下一次发送。因为客户端发一次,就要等待服务器回应一次,这就限制了同一时间段的数据传输。

下图为我们展示了这一情况:

1

我们将 " 一发一收 " 的策略改成 " 多发多收 " 的策略,即一次多发几条数据段,收到也是同样的情况。那为什么不把数据 1 - 4000 合成在一起一次发送呢?这是因为将数据拆分开来,既能够方便将数据编号,又能为确认应答提供了便捷。下图为我们展现了这一情况。

发送方在发送前四个段的时候,不需要一个一个等待 ACK,直接打包一起发送。

2

(1) 注意几个点

① 窗口大小指的是,发送方无需等待接收方的确认应答,而可以继续发送数据的最大值,可以理解为发送方将一组一组的数据打包成一个窗口。 ( 数据按编号顺序不变 ) 上图的窗口大小就是 4000 个字节(四个数据段)。此外,窗口越大,则网络的吞吐率就越高。

② 操作系统内核为了维护这个滑动窗口,需要开辟缓冲区来记录当前还有哪些数据没有应答,只有确认应答过的数据,才能从缓冲区删掉。

③ 发送方在收到第一个 ACK 后,窗口往后滑动,继续发送第五个段的数据,依次类推…( 这样做是为了提高效率,不需要等前四个段全部接收对应的 ACK 后,再执行下一组的发送 ) 所以这也是滑动窗口机制的名字由来,实际上这是一种形象比喻。

在下图中,滑动窗口的模型为我们体现出来了多条传输与多条应答。

3

理解上图的滑动窗口机制:

情况一:

主机B 将 2001 这个 ACK 传回了 主机A ,那么 主机A 就知道了,1001 - 2000 这个数据已经被主机 B 收到了,此时 主机A 也就不用继续等这个数据了,接下来就立即再发一个 5001 - 6000;此时主机 A 仍然保证窗口大小是 四个段,仍然保证当前同时等待 四个段 的 ACK.

情况二:那么如果是后发先至的情况呢?

假设 主机B 先返回了 2001 的 ACK,再返回了 3001 的 ACK. 但主机 A 先收到了 3001 的 ACK,再收到了 2001 的 ACK. 这其实不影响 1 - 2000 数据,也不会造成让 主机A 重传数据的情况。我们必须明确确认序号的含义,当 主机 A 收到了 3001 的 ACK,这就表示 3001 之前的数据已被主机 B 接收,显然,1 - 2000 的数据没有什么传输差错。因为后发先至,我们是站在 主机B 的角度来看 " 发 ",主机A 的角度来看 " 至 "。

(2) 丢包情况

情况一:接收方返回 ACK 的过程中出现丢包情况

在下图中,我们可以看到 主机A 在传输数据的时候没问题,但 主机B 在返回 1001 ,3001,4001 这些 ACK 的时候,出现了丢包问题。这就导致了 主机A 在某一数据段传输之后,并不知道 主机B 有没有收到刚刚的数据段。而实际上,当最终的 6001 的 ACK 在返回的过程中没有出现问题,这也就证明了 1 - 6000 的传输没有问题。这也是确认序号设计的一个巧妙之处,不管中间过程如何应答 ACK,只要最终发送方收到应答,说明传输就没有问题。

而实际上,TCP 协议为了提高效率,主机B 在接收一条数据段后,并不会返回当前的确认 ACK,它很可能隔几条数据才会确认一下,所以这也是一种高效率的体现。

4

情况二:发送方在传输数据的过程中出现丢包情况

在下图中,主机A 在传输 1001 - 2000 的数据时,发生了丢包,所以在 主机B 一直在尝试确认 1001 这个应答,因为站在 主机B 的角度看,它并没有接收到 1001 - 2000 的这些数据。然而,站在 主机A 的角度看,我需要一直发送数据,直到 主机A 收到 主机B 三次重复的应答请求后,它就会采取重传机制,重新发送了 1001 - 2000 的数据。接着,当 主机B 真正收到 1001 - 2000 的数据之后,它就准备返回 7001 的 ACK,这就说明了,1 - 7000 的数据已经正确传输,准备告诉 主机A,之前的传输都已无误。那么,在刚刚的过程中,2001 - 7000 的这些数据实际上被放在了缓冲区,只有等待 主机B 确认了 7001 的应答后,才会将缓冲区的数据删除掉。在上述过程之后,也是正常 " 多发多收 "。

5

所以说,为了防止传输数据的出错,TCP 这些机制的混用还是较为复杂的,机制之间环环相扣。但正是这些机制,保证了 TCP 传输数据的可靠、高效的特点。

5. 拥塞机制

虽然 TCP 有了滑动窗口这个强大的传输机制,能够高效可靠地发送大量的数据。但是如果在刚开始阶段就发送大量的数据,仍然可能引发问题。因为网络上有很多的计算机正在处于交互状态,某一时刻的网络状态可能就已经比较拥堵。在不清楚当前的网络状态下,贸然发送大量的数据,会使传输数据变得更糟。于是就有了拥塞机制,即 TCP 先引入 慢启动 机制,先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据。

此处引入一个概念:拥塞窗口,发送开始的时候,定义拥塞窗口大小为 1;每次收到一个ACK 应答,拥塞窗口加1;每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送的窗口。下图为窗口的增长:

1

上图拥塞窗口的增长速度,是指数级别的。" 慢启动 " 只是指初使时慢,但是增长速度非常快。为了不增长的那么快,因此不能使拥塞窗口单纯的加倍。此处引入一个叫做慢启动的阈值
当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长。如下图所示:

2

当 TCP 开始启动的时候,慢启动阈值等于窗口最大值。
在每次超时重发的时候,慢启动阈值会变成原来的一半,同时拥塞窗口置回1.

举个不恰当的例子:在上图的线图中,我们可以将窗口的增长速度想象成情侣谈恋爱的状态,当情侣刚认识,在一起的时候,他们处于热恋期,即短期时间内,感情升温很快;而在中期,可能感情趋于平淡,但也还好;而有时候,也会闹矛盾吵架,这就是将感情降为冰点;等和好的时候,感情又升温了。

理解拥塞机制

  • 少量的丢包,我们仅仅是触发超时重传;大量的丢包,我们就认为网络拥塞;
  • 当 TCP 通信开始后,网络吞吐量会逐渐上升;随着网络发生拥堵,吞吐量会立刻下降;
  • 拥塞控制,归根结底是 TCP 协议想尽可能快地把数据传输给对方,但是又要避免给网络造成太大压力的折中方案。

6. 流量控制机制

接收端处理数据的速度是有限的,如果发送端发的太快,导致接收端的缓冲区被放满,这个时候如果发送端继续发送,就会造成丢包的问题,继而引起丢包重传等等一系列连锁反应。而如果发送端发的太慢,却又会使得传输数据的效率较低。因此 TCP 可以根据接收端的处理能力,来决定发送端的发送速度。这个机制就叫做流量控制。

  • 接收端将接收缓冲区的剩余大小放入 TCP 首部中的 " 窗口大小 " 字段,之后,通过 ACK 通知发送端。
  • 窗口的字段越大,说明网络的吞吐量越高;窗口字段越小,传输数据的效率越低。
  • 接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端,此时,发送端接收到这个窗口之后,就会减慢自己的发送速度。
  • 如果接收端缓冲区满了,就会将窗口置为0;这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端。

1

我们可以将流量控制这一机制想象成生产者消费者模型,一边往缓冲区里放数据,另一边从缓冲区里取数据。放数据可以想象成往水池中注水,取数据可以想象成水池出水。如果注水的速率大于出水的速率,水池最终会变满,那么水就会溢出,对于数据来说,就会发生丢包这样的情况;如果注水的速率小于出水的速率,那么取水的时候,效率就会变慢。而我们只能通过将注水和出水的速率控制到一个相对接近的数值,才能够保持一个均衡态。

2

流量控制机制是如何实现的

在上图中,我们可以看到窗口的大小为 4000,那么接收缓冲区的大小就为 4000. 当1 - 1000 的数据传入缓冲区的时候,缓冲区还剩余 3000,那么在返回的 ack 中,主机B 会把缓冲区剩余容量告诉 主机A. 接着,主机A 再次传输数据的时候,就按照窗口大小为 3000 的数据来发送,以此类推…

如果缓冲区满了,即剩余容量为 0,此时,按上面的理解,主机A 应该停止给 主机B 发送数据了。但实际上 主机A 从最开始,就会定期发送一个窗口探测包,以此来看看当前的窗口大小。当接收缓冲区数据满了,窗口探测包在某个时间段就能够探测到窗口为 0,那么,主机B 就通过在返回的 ack 告诉 主机A,这样一来,窗口大小又可以更新了,所以缓冲区也会进行更新,不再是满状态了;那么发送方又可以正常发送数据了。

举个例子:小明是一个送货员,他每天早上开车负责送冰淇淋给超市。
第一天,超市老板告诉送货员,说:冰箱总共能装下 100 只冰淇淋,但现在只有 50 只,我从你的车里面取 50 只冰淇淋就可以了。
第二天,老板发现冰箱剩余 10只 冰淇淋了,就往小明的车里取 90 只冰淇淋。
第三天,老板有事,超市关门一天。
第四天,小明来到超市的时候,老板说:冰箱里已经放不下冰淇淋了,就不从小明车里拿了。
第五天,第六天,第七天…

之后的每一天,小明送货之前,都要先打个电话问问老板,冰箱需要多少冰淇淋,根据需求来送,冰箱冰淇淋较多,就送少一点;冰箱冰淇淋较少,就送多一些;但是,即使冰箱满了,小明依旧每一天需要打电话问问,这就相当于窗口探测包,探测某一时间段的情况,再决定后面怎么发送数据。

7. 延迟应答机制

首先,我们必须明确:延迟应答机制中表示的是:延迟接收方 ACK 的应答。

在前面我们提到流量控制机制的时候,窗口大小实际上就是接收缓冲区的剩余空间大小,而窗口大小又作为下一次发送方即将发送的数据量。而站在接收方的角度来看,接收方从缓冲区中取数据。那么随着时间推移,只要缓冲区中有数据,那么接收方就会不断地从缓冲区中取数据。

那么延迟应答机制,顾名思义,当发送方传输数据给接收方后,如果接收方立刻返回 ACK 应答,此时下一次发送方的窗口大小,即为当前缓冲区剩余的空间。但如果延迟一会,接收方再 返回 ACK 应答,情况就不同了,在刚刚延迟的时间中,接收方能够消耗掉一部分数据,此时的缓冲区的剩余空间就更大了。那么下一次对于发送方的窗口大小,就能够提高一些,以此来提高效率。

理解延迟应答机制

这就和之前的注水和出水的逻辑是一样的,发送方往水池中注水,接收方从缓冲区中取水。那么只要水池中有水,那么它就会源源不断地出水。例子如下:

1

接收方正常取水 共 1L,那么发送方第二次注水 1L
接收方延迟取水 共 4L,那么发送方第二次注水 4L

以此看来,将接收方的 ack 应答延迟一会返回给发送方,这样会带来效率的提升,当然,这个延迟时间也需要控制在较少时间内,否则就会造成数据在传输过程中出现问题。所以说,虽然这个机制能够提高效率,但也得基于 TCP 可靠性的前提下,才能这么做。

8. 捎带应答机制

在延迟应答的基础上,我们发现,很多情况下,客户端服务器在应用层也是 " 一发一收 " 的。所以,捎带应答机制就实现了将两个应答合并在一起,返回给客户端。但我们必须明确:捎带应答机制本身就是一个 " 概率性的机制 ",并不会每次都能触发,而我们只需要理解其内在思想即可,下图阐明了这个 " 捎带 " 逻辑。

1

同样地,在我们之前说到四次挥手,使得客户端与服务器断开连接的时候,四次挥手就有可能会变成三次挥手。当然,我也说明过,这是一个概率触发事件,因为这和 ACK 应答,和你正在使用应用程序响应的时间都有关系。

2

9. TCP 的粘包问题

首先我们需要明确,粘包问题中的 " 包 " ,是指的应用层的数据包。在 TCP 协议中,没有如同 UDP 一样的 " 报文长度 " 这样的字段,但 TCP 有一个序号这样的字段。

站在传输层的角度,TCP 是一个一个报文传输过来的。按照序号排好序放在接收缓冲区中。站在应用层的角度,它看到的只是一串连续的字节数据。那么应用程序看到了这么一连串的字节数据,就不知道某个数据包的具体的开始位置和结束位置了。那么,这些应用层的数据包就会粘在一起,所以我们形象地把这个问题叫做粘包。而 TCP 是面向字节流的,所以说,这并不是 TCP 独有的缺点,而是面向字节流所带来的缺点。

1

(1) 如何解决粘包问题

设计一个应用层协议来明确包的边界,这实际上就是一个约定,告诉发送方采取事先设计好的规则,与此同时,也告诉接收方按照此规则读取数据即可。

方法一:为应用层数据设定 " 分隔符或结束符 "

1

方法二:为应用层数据设定 " 数据长度 "

2

(2) 区别于 UDP

3

10. TCP 的异常情况

(1) 进程终止:不管进程是通过什么方式终止的,本质上都会释放内核中的 PCB,也会释放对应的文件描述符,这样一来,就会触发四次挥手。我们必须明确:进程终止不代表连接立即就终止,因为它还未通过四次挥手这一过程。而进程终止其实就相当于调用了 socket.close( ),它只是相当于关闭了文件而已。

(2) 机器重启:机器重启就是主动关闭了进程,所以异常和进程终止的情况相同。

(3) 机器掉电 / 网线断开:这是一个突发情况,机器来不及进行任何动作反应。

我们再分为两种情况讨论机器掉电的情况:

情况一:掉电的是接收方

接收方一旦掉电,此时发送方还在发送数据,显然发送方不会再有接收方返回的 ACK,于是发送方就会超时重传,重传几次之后,就会通过复位报文段 RST 来尝试重置连接。而显然接收方已经断开连接了,那么最终,发送方也就会放弃这个连接,把连接资源回收。

情况二:掉电的是发送方

此时接收方正在尝试接收数据,显然它接收不到任何数据,那么接收方如何知道,发送方是断开连接了?还是暂时还没发呢?

此时接收方采取的策略,就是 " 心跳包 " 机制,也叫做 " 保活 " 机制。即接收方每隔一段时间,向对方发送一个 PING 包,期待对方返回一个 PONG 包。如果接收方 PING包 发过去了很长时间,发送方还没有返回 PONG,并且重试几次也解决不了问题,此时就认为发送方已经断开连接了。而 " 心跳包 " 机制,这是一个应用非常广泛的机制,不仅仅是在 TCP.

五、TCP 机制总结

  • 确认应答 [ 可靠性 ]
  • 超时重传 [ 可靠性 ]
  • 连接管理 [ 可靠性 ]
  • 滑动窗口 [ 效率 ]
  • 流量控制 [ 可靠性 ]
  • 拥塞控制 [ 可靠性 ]
  • 延时应答 [ 效率 ]
  • 捎带应答 [ 效率 ]
  • 面向字节流 ( 粘包问题 ) [ 其他 ]
  • 异常情况 ( 心跳机制 ) [ 其他 ]

六、一些面试题

1. 如何基于 UDP 协议实现可靠传输

题目虽然在问你 UDP,但实际上在问你 TCP,你只需要把 TCP 的思想往 UDP 上靠即可。也就是问你怎么通过自己的代码将 UDP 在应用层中附加上一些 TCP 的机制?当然,并不是让你真的将代码写出来,只需要阐明思路即可。

  • 实现确认应答机制,接收方把每个数据接收到之后,都要为发送方反馈一个 ACK ( 这不是内核返回的了,而是应用程序自己定义一个 ACK 包,发送回去 )
  • 实现发送序号以及确认序号,以及实现去重
  • 实现超时重传
  • 实现连接管理
  • 要想提高效率,实现滑动窗口
  • 为了限制滑动窗口,实现流量控制和拥塞控制
  • 实现延时应答、捎带应答、心跳机制…

2. 什么样的场景中适合使用 TCP 和 UDP

① 如果需要可靠性,首选 TCP

② 如果传输的单个数据报较长 ( 超过64K ),使用TCP

③ 如果特别注重效率,优先考虑 UDP

UDP 的典型适用场景:机房内部的主机之间通信,具有网络环境简单、带宽充裕、数据丢包概率较低的特点。此外,机房内部主机之间的通信,往往传输数据量更大,需要更快的速度。

④ 如果需要广播,优先考虑 UDP,因为此时的场景需要将一份数据同时发给多个主机,而UDP 自身就支持广播的,但 TCP 自身不支持,就只能在应用层程序中设计实现。

3. 注意

传输层协议,并不仅仅是 TCP 和 UDP 两种,只是这两种协议十分广泛。而除了 TCP 和 UDP之外,还有很多其他的传输层协议,可以尽可能的兼顾到可靠性和效率。然而,既能够兼顾可靠性,又能够兼顾高效率,付出的代价可能就是更多的机器资源。而有些传输协议在特定的场景下,大佬可以自己来实现。

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

十七ing

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值