一、TCP 是什么
TCP:Transmission Control Protocol ( 传输控制协议 )
TCP 协议和 UDP 协议一样,位于传输层。
我们都知道协议通俗来说,就是对于传输一种约定,将发送方和接收方在某些方面达成共识,才能够正确传输。
二、TCP 特性
特点: 有连接 可靠传输 面向字节流 全双工
TCP 的核心特性在于可靠传输,此外, TCP 也能够做到提升效率,而可靠性和高效率本身就是一个矛盾,因为可靠性一般就要消耗资源或时间来实现,而高效率又更注重速度。所以本篇博客基于 TCP 的机制来探讨可靠性和效率的这两个特性。
三、TCP协议段格式
四、TCP 的十个机制
1. 确认应答机制
TCP 的核心特性在于可靠传输,而可靠传输的核心在于确认应答机制,但这个机制在面试中考的却不多。
确认应答机制:发送方发送数据给接收方后,接收方收到数据就回应一个应答报文,我们称这个应答报文为 ACK. 如果发送方收到了 ACK,那么认为是对方已经收到了。
(1) 理解发送方和接收方之间的交互
情况一:正常情况
A 给 B 连续发了两条信息,第一条:今天晚饭吃的好吗?第二条:明天一起吃饭吗?
然后 B 给 A 连续回了两条信息,第一条:不好。第二条:好。
那么从情况一的结果来看,B 答应了 A 明天要一起吃饭。
情况二:后发先至
A 给 B 连续发了两条信息,第一条:今天晚饭吃的好吗?第二条:明天一起吃饭吗?
从 B 的角度看:B 给 A 连续回了两条信息,第一条:不好。第二条:好。
但由于网络通信未保证实时性,可能会造成后发先至的情况。
即最终以 A 的角度看:B 给 A 连续回了两条信息,第一条:好。第二条:不好。
对于情况二这就出错了,B 答应了 A 明天一起吃饭,而实际上 A 以为 B 拒绝了明天的饭局。
情况三:对数据进行编号
对数据进行编号,一定需要占用内存,那么实际上 TCP 的报头中,【 32 位确认序号 】就表示这些编号。通过编号的这一机制,我们最起码就不会因为受网络波动的影响,从而使传输方和接收方关联的信息不匹配。以此就能来确认应答了。
前面说到,确认应答机制是实现 TCP 可靠性传输的核心。试想:就像我们举的例子,如果没有对应编号,在日常人们使用网络通信的生活中,很可能就会造成发送方和接收方之间的误会。那么确认应答机制的设计还是十分有意义的。
(2) 确认应答机制是如何实现的
我们都知道,TCP 传输数据的时候是面向字节流的,此外,我刚刚提到,TCP 这样的编号实际上是在 TCP 的报头中存放着,因为它也需要占用空间。所以,我们就必须确定:TCP 的序号和确认序号也是以字节为单位进行编号的。详情如下:
发送方在给接收方发送数据的时候,需要加上编号,接着,接收方在接收一串完整数据后,也要进行确认,这就是应答机制的思想,以此来确认数据接收到。这就和别人喊你名字一样,【Hi~ 小明】你也应该礼貌地答应。
而像这样的确认应答实际上是 ACK 数据,也就是说,每一个 ACK 所表示的就是确认应答的意思。它存在的意义就是方便接收方告诉发送方,刚刚收到的数据,接收方准确无误地收到了,以此来让发送方确认传输无误,准备下一条指令。
在下图中,若发送的数据编号为 1 - 1000,那么确认序号为 1001;若发送的数据编号为 1001 - 2000,那么确认序号为 2001.
当确认应答 1001,并返回 ACK 后,主机A 就知道了 1 - 1000 的数据在传输过程中,没有问题了,于是就立即发送 1001 - 2000 的这组数据,之后,接收方再通过确认序号 2001 继续确认…
2. 超时重传机制
由于网络环境是比较复杂的,因此可能会有丢包的情况出现,虽然出现丢包的概率并不大,但在传输过程中出现丢包就会影响我们日常的网络通信,所以必须解决这一问题。
( 在网络通信中,丢包是指一个或多个数据包的数据无法透过网上到达目的地。比方说:你玩 MOBA 类游戏,当你在团战中放大招之后,你当前电脑显示放了,但之后,因为网络丢包,对面并没有因为你的大招掉伤害,你就会发现刚刚的大招实际并没有放出来,由于网络突然卡了或其他原因,你发现你放的大招并没有将数据从网络传输过去。 )
一旦数据发生丢包,就要进入超时重传的机制中。对于刚刚的打 MOBA 类游戏的例子来说,这就会造成延迟。
(1) 两种丢包的场景
(2) 超时重传机制是如何实现的
① 按照编号来重传数据以防数据重复
站在刚刚发送方 A 的角度看待数据传输的问题,对于上面的两种情况,发送方都是一脸懵逼的,他其实是无法确定到底数据在传过去的时候出现了问题,还是数据在传回来的时候出现了问题。
所以重传机制就明确:发送方需要间隔一段时间再次重传,而站在接收方的角度也是一样的。当站在接收方的角度返回数据,那么接收方就变成了发送方。此外,发送方和接收方都按照编号逻辑进行判断是否需要重传数据,这样就可以避免重传数据发生重复的现象。
在下图的两种情况中
- 情况一:左边的例子,主机A 在给 主机B 传输数据的时候,发生丢包,此时,重传数据是没有问题的。因为这并不会造成结果有误,之后会消耗一定的时间而已。
- 情况二:右边的例子,主机A 在给 主机B 传输数据的时候,一切正常;但主机B 在返回 ack 的时候,应答报文出现丢包情况。此时,主机B 已经接收到了 1 - 1000 数据,如果再重传就会发生数据重叠的情况。所以说,给数据编号,有效地解决了这一问题。当 1 - 1000 的数据被编好号了,那么主机B 就接收到了,它就需要应答 1001,这时候,主机B 只需要重传应答的 ack 即可。
② 明确超时重传的时间
不同的操作系统对于超时重传的时间实现的方式不同。
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(服务器) 明天一起吃饭吗? 好的,时间地点告诉我 明天晚上,南门烧烤店不见不散
syn 表示请求连接 正常情况下 syn = 0,在尝试建立连接的请求中,syn = 1 ack 表示确认连接 正常情况下 ack = 0,在确认请求的连接时,ack = 1
① 以打电话的形式来详解三次握手过程
打电话的情况完全符合三次握手的逻辑,读者可以自身体会日常的通信过程。
② 两个状态
- LISTEN:听。
顾名思义,它表示服务器的待连接状态,在 Java 中,当我们创建好 ServerSocket 实例的时候,就进入了此状态。例如:在 A 给 B 打电话的过程中,A 拨号成功,听筒出现了【嘟嘟嘟…】的声音,即 A 尝试与 B 对话的状态,并没有接通,即此状态。 - ESTABLISHED:已确立的。
顾名思义,它表示客户端或服务器已建立连接的状态,在 Java 中,当我们代码中使用 accept 返回一个 Socket 对象的时候,就进入了此状态。例如:在 A 给 B 打电话的时候,A 与 B 已经开始愉快地沟通起来了,即做到了真正意义上的通话,即此状态。
③ 关于三次握手的一些问题
问题一:三次握手的功能与意义 ?
答:三次握手就是建立连接与确立连接的过程,相当于投石问路,通过三次握手的过程,来确认 A和 B 之间的网络传输是否是通畅的,尤其是要确认,A 和 B 各自的发送能力和接收能力是否正常,这在上面的打电话例子中已经形象地体现出来了。
问题二:整个连接的过程可以进行四次握手吗 ?
答:在 B 给 A 传送的 【ack + syn】中,将这两步拆解开来再看,其实就是四次握手了。所以说,四次握手本质上可以达到建立连接、确认连接的目的,但三次握手更加高效!在网络传输中,将过程拆解,那么就涉及到了封装与分用,以此带来的结果就是:传输的开销更大。
问题三:整个连接的过程可以只进行两次握手吗 ?
答:答案是否定的!因为在打电话的例子中,无论你缺了哪一步,都无法让双方都确认彼此的发送与接收状态是否完好。所以说,三次握手是至少的,也是连接的必须步骤。
(2) 四次挥手
顾名思义,三次握手表明尝试建立连接的过程,那么四次挥手就表明尝试断开连接的过程。但我们必须明确:三次握手,必须是客户端先发起请求,而四次挥手,客户端或服务器都能够发起请求,这就是客户端与服务器的特性。
可以看到,此处的 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 - 4000 合成在一起一次发送呢?这是因为将数据拆分开来,既能够方便将数据编号,又能为确认应答提供了便捷。下图为我们展现了这一情况。
发送方在发送前四个段的时候,不需要一个一个等待 ACK,直接打包一起发送。
(1) 注意几个点
① 窗口大小指的是,发送方无需等待接收方的确认应答,而可以继续发送数据的最大值,可以理解为发送方将一组一组的数据打包成一个窗口。 ( 数据按编号顺序不变 ) 上图的窗口大小就是 4000 个字节(四个数据段)。此外,窗口越大,则网络的吞吐率就越高。
② 操作系统内核为了维护这个滑动窗口,需要开辟缓冲区来记录当前还有哪些数据没有应答,只有确认应答过的数据,才能从缓冲区删掉。
③ 发送方在收到第一个 ACK 后,窗口往后滑动,继续发送第五个段的数据,依次类推…( 这样做是为了提高效率,不需要等前四个段全部接收对应的 ACK 后,再执行下一组的发送 ) 所以这也是滑动窗口机制的名字由来,实际上这是一种形象比喻。
在下图中,滑动窗口的模型为我们体现出来了多条传输与多条应答。
理解上图的滑动窗口机制:
情况一:
主机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,它很可能隔几条数据才会确认一下,所以这也是一种高效率的体现。
情况二:发送方在传输数据的过程中出现丢包情况
在下图中,主机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 的应答后,才会将缓冲区的数据删除掉。在上述过程之后,也是正常 " 多发多收 "。
所以说,为了防止传输数据的出错,TCP 这些机制的混用还是较为复杂的,机制之间环环相扣。但正是这些机制,保证了 TCP 传输数据的可靠、高效的特点。
5. 拥塞机制
虽然 TCP 有了滑动窗口这个强大的传输机制,能够高效可靠地发送大量的数据。但是如果在刚开始阶段就发送大量的数据,仍然可能引发问题。因为网络上有很多的计算机正在处于交互状态,某一时刻的网络状态可能就已经比较拥堵。在不清楚当前的网络状态下,贸然发送大量的数据,会使传输数据变得更糟。于是就有了拥塞机制,即 TCP 先引入 慢启动 机制,先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据。
此处引入一个概念:拥塞窗口,发送开始的时候,定义拥塞窗口大小为 1;每次收到一个ACK 应答,拥塞窗口加1;每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送的窗口。下图为窗口的增长:
上图拥塞窗口的增长速度,是指数级别的。" 慢启动 " 只是指初使时慢,但是增长速度非常快。为了不增长的那么快,因此不能使拥塞窗口单纯的加倍。此处引入一个叫做慢启动的阈值
当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长。如下图所示:
当 TCP 开始启动的时候,慢启动阈值等于窗口最大值。
在每次超时重发的时候,慢启动阈值会变成原来的一半,同时拥塞窗口置回1.
举个不恰当的例子:在上图的线图中,我们可以将窗口的增长速度想象成情侣谈恋爱的状态,当情侣刚认识,在一起的时候,他们处于热恋期,即短期时间内,感情升温很快;而在中期,可能感情趋于平淡,但也还好;而有时候,也会闹矛盾吵架,这就是将感情降为冰点;等和好的时候,感情又升温了。
理解拥塞机制
- 少量的丢包,我们仅仅是触发超时重传;大量的丢包,我们就认为网络拥塞;
- 当 TCP 通信开始后,网络吞吐量会逐渐上升;随着网络发生拥堵,吞吐量会立刻下降;
- 拥塞控制,归根结底是 TCP 协议想尽可能快地把数据传输给对方,但是又要避免给网络造成太大压力的折中方案。