TCP(Transmission Control Protocol,传输控制协议) 是计算机网络的的重要组成部分,也是网络编程的重要内容,还有咱们平时接触最多的 HTTP 也是基于 TCP 实现的。TCP 能够说是最重要的传输层协议,既然如此,做为开发人员,就有必要把 TCP 的核心概念和原理搞清楚。除此以外,诸如三次握手、四次挥手、滑动窗口和拥塞控制这些概念更是高频面试题,这就更有理由深刻学习一下 TCP 了,本文就为你们详细梳理一下 TCP 的核心概念和原理。注:因为本文图片较多,标注有 by HYN 的图片为做者自制,其它来自于网络或参考资料,侵删面试
第一部分先为你们介绍一下 TCP 的主要概念,并讲解一下 TCP 的三个重要特性——1. 面向链接;2. 基于字节流;3. 可靠性。算法
关于网络分层的概念实在是老生常谈了,下图就是两种经典的分层模型,能够看到 TCP 在网络分层中的位置。编程
网络分层模型
本文重点对 TCP 进行介绍,从图中能够看到 TCP 位于传输层,并且构建于网络层的 IP 协议之上,对于 TCP 最多见的介绍就是 “TCP 是一种面向链接的、可靠的、基于字节流的传输层通讯协议”,那这三个形容词到底是什么意思呢?缓存
面向链接意味着两个使用 TCP 的应用 (一般是一个客户端和一个服务器) 在彼此交换数据以前必须先创建一个 TCP 链接。这一过程与打电话很类似,先拨号响铃,等对方应答后再说明是谁。详细的三次握手、四次挥手过程将在第二部分——链接管理部分进行介绍。服务器
TCP 链接双方的数据交换格式是以字节 (byte,1byte = 8 bit)构成的有序但无结构的字节流。TCP 不在字节流中插入记录标识符,这被称为字节流服务(byte stream service)。若是一方的应用程序先传 10 字节,又传 20 字节,再传 50 字节 ,链接的另外一方将没法了解发方每次发送了多少字节。收方能够分 4 次接收这 80 个字节,每次接收 20 字节。一端将字节流放到 TCP 链接上,一样的字节流将出如今 TCP 链接的另外一端。另外,TCP 对字节流的内容不做任何解释,TCP 没法知道传输的数据字节流是二进制数据,仍是 ASCI I 字符。cookie
若是以为上面这段话比较抽象的话,能够拿 TCP 的字节流和 UDP 的报文 (message) 进行比较(UDP:User Datagram Protocol,用户数据报协议,和 TCP 同为传输层的协议,后面会提供二者的全面对比)。TCP 的字节流相似于自来水,链接双方都有缓冲区,能够类比成蓄水池,发送方的发送频率和每次的发送量没有固定要求,接收方也能够自由决定本身的接收频率和每次的接收量,只要把全部的数据接收完毕便可。而 UDP 的数据报则相似于瓶装水 (好比农夫山泉),发送方发送一瓶,接收方就要相应地接收一瓶。网络
下图描述了 TCP 链接中数据的传输过程以及 TCP 在整个过程当中所扮演的角色。并发
TCP 在网络数据传输中的位置和角色
按照图中的流程,好比咱们在浏览B站,在 TCP 链接创建以后,客户端的应用层协议能够向 TCP 发送无特殊格式的字节流,TCP 会将这些字节打包成报文段(segment),报文段大小视状况而定,这些报文段会被网络层的 IP 封装成 IP 数据报(IP Datagram),而后通过网络传输给服务器,而接下来服务器的操做至关于客户端的逆操做,先从 IP 数据报中拆分出 TCP 报文段,再把 TCP 报文段还原成字节流并发送给上层的应用层协议。服务器向客户端发送数据的流程也是同样的,发送方和接收方的角色互换便可。ide
上面屡次提到了报文段的概念,其结构很是重要,后面的链接过程和拥塞控制等内容也要用到相关概念,先在这里介绍一下。函数
TCP 报文段结构
图的上半部分显示 TCP 报文段被封装在 IP 数据报中,图的下半部分则显示了 TCP 报文段和 TCP 首部的结构,TCP 首部的固定数据有20字节,加上选项部分最大可达60字节,而有效数据部分则是被打包的应用层数据。下面介绍一下 TCP 首部的结构:
控制位 (Control Bits):在三次握手和四次挥手中会常常看到 SYN、ACK 和 FIN 的身影,一共有 6 个标志位,它们表示的意义以下:
咱们都知道 TCP 是具备可靠性的通讯协议,它主要经过如下方式确保可靠性,这里先了解一下可靠性的原理,其中细节部分后文会讲:
上面为你们介绍了 TCP 最重要的三个特色,在本文第一部分的最后,再来看看 TCP 和 UDP 的对比吧。
UDP | TCP | |
---|---|---|
是否链接 | 无链接 | 面向链接 |
是否可靠 | 不可靠,没有确认机制、流量控制和拥塞控制 | 可靠,有确认机制、流量控制和拥塞控制 |
链接对象个数 | 支持一对一,一对多,多对一和多对多交互通讯 | 只支持一对一通讯 |
传输方式 | 面向报文 | 面向字节流 |
首部开销 | 首部开销小,固定8字节 | 首部开销较大,最小20字节,最大60字节 |
适用场景 | 适用于实时应用(IP电话、视频会议、直播等) | 适用于要求可靠传输的应用,如文件传输等 |
这个问题简直太经典了,若是你在面试中只被问到了一个关于 TCP 的问题,那大几率就是关于三次握手的问题。TCP 的重要特性之一就是面向链接,链接双方在发送数据以前必须经历握手的阶段,那具体的过程是怎样的呢?先来看图,你们最好能够动手简单画画这个图,固然还有后文四次挥手的图,帮助加深记忆。
三次握手过程
如图所示,双方之间的三个蓝色箭头就表示了三次握手过程当中所发生的数据交换:
常见面试题 1: TCP 创建链接为何要三次握手而不是两次?
答:网上大多数资料对这个问题的回答只有简单的一句:防止已过时的链接请求报文忽然又传送到服务器,于是产生错误,这既不够全面也不够具体。下面给出比较详细而全面的回答:
在双方两次握手便可创建链接的状况下,假设客户端发送 A 报文段请求创建链接,因为网络缘由形成 A 暂时没法到达服务器,服务器接收不到请求报文段就不会返回确认报文段,客户端在长时间得不到应答的状况下从新发送请求报文段 B,此次 B 顺利到达服务器,服务器随即返回确认报文并进入 ESTABLISHED 状态,客户端在收到 确认报文后也进入 ESTABLISHED 状态,双方创建链接并传输数据,以后正常断开链接。此时姗姗来迟的 A 报文段才到达服务器,服务器随即返回确认报文并进入 ESTABLISHED 状态,可是已经进入 CLOSED 状态的客户端没法再接受确认报文段,更没法进入 ESTABLISHED 状态,这将致使服务器长时间单方面等待,形成资源浪费。
第一次握手:客户端只是发送处请求报文段,什么都没法确认,而服务器能够确认本身的接收能力和对方的发送能力正常;
第二次握手:客户端能够确认本身发送能力和接收能力正常,对方发送能力和接收能力正常;
第三次握手:服务器能够确认本身发送能力和接收能力正常,对方发送能力和接收能力正常;
可见三次握手才能让双方都确认本身和对方的发送和接收能力所有正常,这样就能够愉快地进行通讯了。
TCP 实现了可靠的数据传输,缘由之一就是 TCP 报文段中维护了序号字段和确认序号字段,也就是图中的 seq 和 ack,经过这两个字段双方均可以知道在本身发出的数据中,哪些是已经被对方确认接收的。这两个字段的值会在初始序号值得基础递增,若是是两次握手,只有发起方的初始序号能够获得确认,而另外一方的初始序号则得不到确认。
常见面试题2: TCP 创建链接为何要三次握手而不是四次?
答:相比上个问题而言,这个问题就简单多了。由于三次握手已经能够确认双方的发送接收能力正常,双方都知道彼此已经准备好,并且也能够完成对双方初始序号值得确认,也就无需再第四次握手了。
常见面试题3: 有一种网络攻击是利用了 TCP 创建链接机制的漏洞,你了解吗?这个问题怎么解决?
答:在三次握手过程当中,服务器在收到了客户端的 SYN 报文段后,会分配并初始化链接变量和缓存,并向客户端发送 SYN + ACK 报文段,这至关因而打开了一个“半开链接 (half-open connection)”,会消耗服务器资源。若是客户端正常返回了 ACK 报文段,那么双方能够正常创建链接,不然,服务器在等待一分钟后会终止这个“半开链接”并回收资源。这样的机制为 SYN洪泛攻击 (SYN flood attack)提供了机会,这是一种经典的 DoS攻击 (Denial of Service,拒绝服务攻击),所谓的拒绝服务攻击就是经过进行攻击,使受害主机或网络不能提供良好的服务,从而间接达到攻击的目的。在 SYN 洪泛攻击中,攻击者发送大量的 SYN 报文段到服务器请求创建链接,可是却不进行第三次握手,这会致使服务器打开大量的半开链接,消耗大量的资源,最终没法进行正常的服务。
解决方法:SYN Cookies,如今大多数主流操做系统都有这种防护系统。SYN Cookies 是对 TCP 服务器端的三次握手作一些修改,专门用来防范 SYN 洪泛攻击的一种手段。它的原理是,在服务器接收到 SYN 报文段并返回 SYN + ACK 报文段时,再也不打开一个半开链接,也不分配资源,而是根据这个 SYN 报文段的重要信息 (包括源和目的 IP 地址,端口号可一个秘密数),利用特定散列函数计算出一个 cookie 值。这个 cookie 做为将要返回的SYN + ACK 报文段的初始序列号(ISN)。当客户端返回一个 ACK 报文段时,服务器根据首部字段信息计算 cookie,与返回的确认序号(初始序列号 + 1)进行对比,若是相同,则是一个正常链接,而后分配资源并创建链接,不然拒绝创建链接。
这是 TCP 创建链接的特殊状况,有时会出现两台机器同时执行主动打开的状况,不过几率很是小,这种状况你们仅做了解便可。在这种状况下就无所谓发送方和接收方了,双放均可以称为客户端和服务器,同时打开的过程以下:
同时打开的过程
如图所示,双方在同一时刻发送 SYN 报文段,并进入 SYN-SENT 状态,在收到 SYN 后,状态变为 SYN-RECEIVED,同时它们都再发送一个 SYN + ACK 的报文段,状态都变为 ESTABLISHED,链接成功创建。在此过程当中双方一共交换了4个报文段,比三次握手多一个。
创建一个链接须要三次握手,而终止一个链接要通过 4次握手。这由 TCP 的半关闭( half-close) 形成的。既然一个 TCP 链接是全双工 (即数据在两个方向上能同时传递), 所以每一个方向必须单独地进行关闭。这原则就是当一方完成它的数据发送任务后就能发送一个 FIN 来终止这个方向链接。当一端收到一个 FIN,它必须通知应用层另外一端已经终止了数据传送。理论上客户端和服务器均可以发起主动关闭,可是更多的状况下是客户端主动发起。
四次挥手过程
四次挥手详细过程以下:
常见面试题1: 为何 TCP 关闭链接为何要四次而不是三次?
答:服务器在收到客户端的 FIN 报文段后,可能还有一些数据要传输,因此不能立刻关闭链接,可是会作出应答,返回 ACK 报文段,接下来可能会继续发送数据,在数据发送完后,服务器会向客户单发送 FIN 报文,表示数据已经发送完毕,请求关闭链接,而后客户端再作出应答,所以一共须要四次挥手。
常见面试题2: 客户端为何须要在 TIME-WAIT 状态等待 2MSL 时间才能进入 CLOSED 状态?
答:按照常理,在网络正常的状况下,四个报文段发送完后,双方就能够关闭链接进入 CLOSED 状态了,可是网络并不老是可靠的,若是客户端发送的 ACK 报文段丢失,服务器在接收不到 ACK 的状况下会一直重发 FIN 报文段,这显然不是咱们想要的。所以客户端为了确保服务器收到了 ACK,会设置一个定时器,并在 TIME-WAIT 状态等待 2MSL 的时间,若是在此期间又收到了来自服务器的 FIN 报文段,那么客户端会从新设置计时器并再次等待 2MSL 的时间,若是在这段时间内没有收到来自服务器的 FIN 报文,那就说明服务器已经成功收到了 ACK 报文,此时客户端就能够进入 CLOSED 状态了。
以前在介绍 TCP 创建链接的时候会有一种特殊状况,那就是同时打开,与之对应地, TCP 关闭时也会有一种特殊状况,那就是同时关闭,这种状况仅做了解便可,流程图以下:
同时关闭过程
这种状况下,双方应用层同时发出关闭命令,这将致使双方各发送一个 FIN,两端均从 ESTABLISHED 变为 FIN_WAIT_1,两个 FIN 通过网络传送后分别到达另外一端。收到 FIN 后,状态由 FIN_WAIT_1 变迁到 CLOSING,并发送最后的 ACK,当收到最后的 ACK 时,为确保对方也收到 ACK,状态变化为 TIME_WAIT,并等待 2MSL 时间,若是一切正常,随后会进入 CLOSED 状态。
TCP 链接双方的主机都为该链接设置了发送缓存和接收缓存,这些缓存起到了蓄水池的做用,咱们确定不能把上层应用程序发来的数据一古脑儿发送到网络中,而是利用发送缓存将其缓存起来,而后再按必定的速率经过网络发送给对方,而接收缓存的做用是把对方传来的数据先缓存起来,等到己方应用程序有空的时候再来取走数据。示意图以下:
TCP 缓存示意图
在此过程当中,若是接收方应用程序读取数据的速度小于发送方的数据发送速度,将致使接收方的接收缓存溢出,形成数据丢失,这显然不是咱们想看到的。所以 TCP 为应用程序提供了流量控制服务 (flow-control service),以消除发送方使接收方的接收缓存溢出的可能性。简单来讲流量控制的目的就是协调发送方的数据发送速度,使其与接收方的数据处理速度相匹配,避免数据丢失,那么如何实现流量控制呢?
顾名思义就是发送方在发送一个数据包后就中止发送,等待对方响应 ACK,而后才能继续发送数据。这种模式的具体实现为 Positive Acknowledgment With Retransmission (PAR),意为带重传的确定确认协议,其实现方式以下图所示:
PAR 示意图
这种实现很简单,发送方在发送数据包 (图中的msg)时会设置一个计时器,而后等待接收方的 ACK,接收方在收到数据后会返回 ACK 做为应答,发送方在收到 ACK 后会发送下一个数据包。若是因为网络缘由形成数据包或者 ACK 丢失时,计时器会超时,而后发送方会从新发送未被确认的数据包。能够看到,这种模式虽然能够确保数据传输的可靠性,可是有个致命的缺点,那就是效率过低?若是是你,你会怎么对这个方案进行优化呢?
既然每次发送只一个数据包效率过低,那就多发送几个,而后给这些数据包编上号,接收端必须对每个包进行确认,这样设备 A 一次多发送几个片断,而没必要等候 ACK,同时接收端也要告知它可以收多少,这样发送端发起来也有个限制,固然还须要保证顺序性,不要乱序,对于乱序的情况,咱们能够容许等待必定状况下的乱序,好比说先缓存提早到的数据,而后去等待须要的数据,若是必定时间没来就丢掉乱序的数据,来保证顺序性,这样的话,数据传输效率就能够大大提升。不过 TCP 也没有采用这种方案,而是在此基础上实现更加复杂的滑动窗口。
首先给你们推荐一个视频,讲得很不错 https://www.bilibili.com/vide...
咱们能够把发送方的发送缓存中的字节分为如下四类,每一个编号对应一个字节:
发送缓存中的字节分类
发送窗口
发送窗口:图中的黑色框就是发送方的发送窗口,其大小由两个因素决定:一、接收方的提供的窗口大小 (TCP 报文段首部中的 window 字段),发送方在三次握手阶段首次获得这个值,以后的通讯过程当中接收方会根据本身的可用缓存对这个值进行动态调整;二、发送方会根据网络状况维护一个拥塞窗口变量 (后文介绍)。发送窗口的大小取这两个值的最小值。对于发送方来讲,发送窗口分为两部分,分别是已经发送的部分(已经发送了,可是没有收到ACK)和可用窗口,接收端容许发送可是没有发送的那部分称为可用窗口。
接收窗口:对于接收端也是有一个接收窗口的,相似发送端,接收端的数据有3个分类,由于接收端并不须要等待ACK因此它没有相似的接收并确认了的分类,状况以下
滑动窗口的滑动过程
累积确认概念:TCP 并非每个报文段都会回复一个 ACK ,可能会对两个报文段发送一个ACK,也可能会对多个报文段发送 1 个 ACK,这称为累积确认。好比说发送方有 1/2/3 3 个报文段,先发送了2,3 两个报文段,可是接收方指望收到1报文段,这个时候 2/3 报文段就只能放在缓存中等待报文1的空洞被填上,若是报文段1一直不来,报文2/3也将被丢弃,若是报文1来了,那么会发送一个 ACK 对第3个报文段进行确认,就表明对这三个报文段所有进行了确认。
下面举例说明一下窗口滑动的过程:
接下来就是重复上述过程,直到 TCP 字节流的全部数据发送完毕。在这个过程当中,接收方会根据本身接收缓存的剩余空间动态调整窗口值,对发送方进行流量控制。文字描述可能不够直观,你们能够参考上文推荐的视频。另外推荐一个动图演示的网站 动画地址,能够观看滑动窗口的动态效果,以下图 (此演示未考虑丢包的状况):
滑动窗口动画效果
当数据从一个大的管道 (好比一个快速局域网)向一个较小的管道 (好比较慢的广域网)发送的时候就会发生拥塞,还有一种状况就是当多个输入流到达一个路由器,而路由器的输出流小于这些输入流的总和时,也会发生拥塞。举个例子就好理解了,第一种状况就好像源源不断的车流从八车道进入四车道,若是不进行控制,必然形成道路拥堵;第二种状况相似于不少车辆汇入十字路口,若是进的速度大于出的速度,再不加以控制,必然也会形成拥堵。因而 TCP 提供了响应的机制来应对这种状况,也就是 TCP 的拥塞控制。
TCP 一共使用了四种算法来实现拥塞控制:一、慢开始 (slow-start);二、拥塞避免 (congestion avoidance);三、快速重传 (fast retransmit);四、快速恢复 (fast recovery)。
这里先介绍一下拥塞窗口 (congestion window,简写为 cwnd)的概念:拥塞窗口是由发送方根据网络情况维护的一个变量,用于控制本身的数据发送速率。前文提到了发送方的发送窗口受两个变量约束,一是接收方通告的窗口大小值,二就是发送方自身的拥塞窗口,实际的发送窗口大小取两者最小值。
在早期的 TCP Tahoe 版本中,只用到了前两种算法,如图所示:
慢开始和拥塞避免
如图所示,在刚开始,TCP 采用慢开始算法。慢开始不是指拥塞窗口的增加速度慢(增加速度是指数增加,很是快),而是指 TCP 开始发送设置 cwnd=1。思路就是不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大 逐渐增长拥塞窗口的大小。这里用报文段的个数的拥塞窗口大小举例说明慢启动算法,实时拥塞窗口大小是以字节为单位的。为了防止 cwnd 增加过大引发网络拥塞,设置一个慢开始门限(slow start threshold,简写为 ssthresh) ,
当cnwd < ssthresh,使用慢开始算法
当 cnwd = ssthresh,既可以使用慢开始算法,也可使用拥塞避免算法
当 cnwd > ssthresh,使用拥塞避免算法
当拥塞窗口大小达到初始 ssthresh 值时,转而采用拥塞避免算法。拥塞避免并不是彻底可以避免拥塞,是说在拥塞避免阶段将拥塞窗口控制为按线性规律增加,使网络比较不容易出现拥塞,思路:让拥塞窗口 cwnd 缓慢地增大,即每通过一个往返时间 RTT 就把发送方的拥塞窗口加一。不管是在慢开始阶段仍是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有收到确认,虽然没有收到确承认能是其余缘由的分组丢失,可是由于没法断定,因此都当作拥塞来处理),就把慢开始门限设置为出现拥塞时的发送窗口大小的一半。而后把拥塞窗口设置为 1,执行慢开始算法。
TCP Reno 应用了四种算法
有时候的发送方未收到某个报文段的确认也并必定就说明必定是出现了网络拥塞,也多是其余缘由,因此直接执行慢开始算法会影响总体效率,后来的 TCP Reno 版本解决了这一问题,那就是采用快速重传和快速恢复算法。
快速重传要求接收方在收到一个失序的报文段后就当即发出重复确认(为的是使发送方及早知道有报文段没有到达对方),而不要等到本身发送数据时捎带确认。快重传算法规定,发送方只要一连收到三个重复确认就应当当即重传对方还没有收到的报文段,而没必要继续等待设置的重传计时器时间到期。因为不须要等待设置的重传计时器到期,能尽早重传未被确认的报文段,能提升整个网络的吞吐量。
当发送方连续收到三个重复确认时,就执行“乘法减少”算法,把 ssthresh 门限减半。 可是接下去并不执行慢开始算法。考虑到若是网络出现拥塞的话就不会收到好几个重复的确认,因此发送方如今认为网络可能没有出现拥塞。因此此时不执行慢开始算法,而是将 cwnd 设置为 ssthresh 的大小, 而后执行拥塞避免算法。
咱们知道 TCP 是以字节流的方式传输数据,传输的最小单位为一个报文段(segment)。TCP 首部 中有个选项 (Options)的字段,常见的选项为 MSS (Maximum Segment Size最大消息长度),它是收发双方协商通讯时每个报文段所能承载的最大有效数据的长度。数据链路层每次传输的数据有个最大限制MTU (Maximum Transmission Unit),通常是1500比特,超过这个量要分红多个报文段,MSS 则是这个最大限制减去 TCP 的首部,光是要传输的数据的大小,通常为1460比特。换算成字节,也就是180多字节。
MSS = MTU - Header
TCP 为提升性能,发送端会将须要发送的数据发送到发送缓存,等待缓存满了以后,再将缓存中的数据发送到接收方。同理,接收方也有接收缓存这样的机制,来接收数据。
上面这些是发生 TCP 粘包和拆包的前提,下面是具体的缘由:
本文为你们梳理了 TCP 的核心概念和原理,也分享了一些高频面试题,但愿对你有帮助。尽管文章字数超过了一万字,可是 TCP 的不少细节还远远没有介绍到,若是想进一步了解,能够参考一下下面参考资料中提到的几本书。
参考资料: