TCP的可靠机制
前言
要了解TCP的可靠机制,我们必须要先熟悉TCP的报文,在这篇文章中有详细介绍TCP的报文 :点击链接
并且确认应答机制也在该文章中提到,所以这篇文章就不会再介绍确认应答了。
超时重传
我们都知道,报文在网络中传输是有可能丢包的。如果丢包了,那么TCP又该如何处理?
首先我们要分为2种情况,1种情况是数据丢了,一种情况是服务器发的应答丢了。
情况1.数据丢了
这个问题很好解决,因为TCP有确认应答机制。收到了一条数据那么就必须要发送一个应答告诉对方收到了这条报文。如果没有收到,那么发送数据发会认为自己的数据并没有被收到,此时间隔一段时间(超时时间)进行报文重传。
情况2.应答丢了
这种情况就是数据已经到达了对端,但是对端给的应答丢了。
这情况发送数据方依旧在超时时间后重新发送报文。因为无论是应答丢了还是数据丢了,发送方是不知道的。而当应答丢了的时候,又发送了一则数据。那么对端就收到了重复的报文,此时对端会通过32位序列号对报文进行排序,如果序列号相同的则会进行去重。
超时时间
知道了在数据或者应答丢失的情况下,发送方会隔一段时间进行重传。 那么这 一段时间 是多久呢?
如果超时时间设置太短,那么可能会发送重复的包。
如果超时时间设置太长,那么又可能造成效率问题。
并且网络中的环境也是即使变化的。
所以TCP为了保证无论在任何坏境下都能比较高性能的通信,会动态计算这个最大超时时间。
在Linux中,超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是以500ms的整数倍。
第一次重发的时间是500ms ,如果第一次重发后没有得到回应,那么第二次就会第 2 * 500ms进行重发。第三次则是 4 * 500ms… 直到到达一定次数时,TCP认为网络或者对端主机出现异常,强制关闭连接。
连接管理
TCP是面向连接的,TCP每次与一个客户端连接成功,那么就会创建一个连接。那么这个连接要不要管理?要!怎么管理?先描述,再组织。创建一个描述连接的结构体,由操作系统进行管理。也就是每一个连接都有自己对应的数据结构!每一个连接都占用着操作系统的一份资源!
上面说的连接建立成功之后的情况,那么如何建立连接?
三次握手
在正常情况下,TCP连接是要经过三次握手的过程。断开时要经历四次挥手过程。而三次握手过程必然是客户端先发起的,不存在有服务器去连接客户端的情况。你好比你访问了一个页面,那么访问请求肯定是先从你的浏览器发起的。因为服务器根本不知道你的IP和端口,只有你先发送报文过后,服务器才能获得你的IP + 端口。服务器和客户端是1 : N 的关系。
三次握手过程
1.客户端向服务器发送带有SYN的报文,并自己处于SYN_SENT状态
2.服务器收到了客户端发送的SYN报文,此时服务器处于SYN_RCVD状态,并向客户端发送SYN+ACK
3.客户端收到服务器发送的SYN+ACK报文,此时客户端建立连接成功,此时客户端处于ESTABUSHED状态,并向服务器发送ACK
4.服务器收到客户端建立连接成功后发送的ACK报文,知道了客户端建立连接成功,此时服务器处于ESTABUSHED状态
当到达第四步的时候!客户端与服务器之间的连接就已经建立好了!而accept函数是从底层获取连接,而不是建立连接的函数!
为什么是三次握手,而不能是一次,两次,或者四次?
先来说为什么不能是一次握手
如果是一次握手的话,客户端只要发送连接,服务器就认为连接成功了。那么客户端是不是可以同时发送大量的SYN连接请求,而服务器每收到一个连接请求就建立一个连接。因为每建立一个连接就要消耗一份系统资源,而客户端那边发起连接请求几乎没有成本。所以服务器最终会不会因为连接请求过多而导致崩溃呢?答案是会的!
而同时发送大量SYN连接请求这种行为,也被称为SYN洪水攻击
为什么不能是2次握手?
2次握手和1次握手的本质是一样的。只要客户端发送SYN,服务器就认为已经连接成功了!至于ACK,客户端压根可以不关心!2次握手照样可以通过SYN洪水攻击让服务器产生大量的连接最后导致服务器崩溃。而客户端这边几乎没有成本,因为只是仅仅发送一个SYN报文而已。
三次握手为什么又可以了呢?
一次握手和2次握手的本质是不清楚服务器和客户端的收发能力!
一次握手的时候,服务器知道客户端有发送数据的能力和自己有收数据的能力,但不知道自己发送数据的能力和客户端接收数据的能力
二次握手客户端知道服务器有收数据和发送数据的能力,服务器知道自己有收数据的能力,但不知道自己有发数据的能力
而三次握手,服务器和客户端都自己彼此的收数据和发送数据的能力。
并且我们可以看到,客户端在收到服务器发来的ACK+SYN后,就已经建立了连接!此时客户端再给服务器发送ACK,服务器收到ACK才开始建立连接。 这就意味着,客户端在服务器之前建立连接!也就说明了,客户端建立连接是有成本的!这样一来,客户端和服务器就都要维护等量的连接,一旦你客户端连接过多,对端也可以进行相应的判断处理。
TCP能保证安全吗?
很遗憾,不能。三次握手只能让一台主机和服务器承受同等的连接压力。但无法抑制多台主机的同时访问。假设有1W台主机,同一时间给你的2核2G服务器发送连接请求。那么你的服务器就同时来了1W个连接,最后因为超负荷饮恨被操作系统KILL掉。而这种攻击手段想必大家都听过,就是DDOS攻击
为什么不是四次握手?
三次握手可以做到的事情,为什么要四次握手呢?多一次握手就相当于要多发一条报文,而全世界那么多人每天都在使用网络。无论是四次握手,五次还是六次,都是对网络资源的一种浪费。就好比有两家小卖铺摆在你面前,同一样东西,一家只要1块钱,另一家要10块钱。肯定选1块钱的那家啊。人傻钱多当我没说=。=
四次挥手
建立连接时有三次握手,那么关闭连接也会有四次挥手(你拔电脑电源让电脑秒关机不算)。而四次挥手的过程和三次握手类似。只不过三次握手必须由客户端发起,而四次挥手无论是客户端还是服务器都可以发起。所以下面我们假设是客户端发起四次挥手。
四次挥手过程
1. 客户端关闭连接(close fd),状态变成FIN_WAIT_1 ,并向服务器发送携带FIN被置为1的报文
2. 服务器收到客户端的FIN报文,状态变为CLOSE_WAIT,并向客户端发送ACK确认。
3. 客户端收到ACK后,状态变为FIN_WAIT_2
4. 服务器端Close连接后,向客户端再次发送FIN报文,并状态被置为LAST_ACK
5. 客户端收到服务器发送的FIN,进入TIME_WAIT状态。并向服务器发送ACK
6. 服务器收到ACK,四次挥手过程结束
当我们的客户端断开连接时,也就是调用了close(fd),那么就会向服务器发送FIN,并把自己置为FIN_WAIT_1。此时的服务器处于CLOSE_WAIT状态。
CLOSE_WAIT
如果我们的服务器不进行close(fd)关闭文件描述符,那么就会造成该连接一直处于CLOSE_WAIT状态而无法释放。而客户端也因为没有收到服务器发来的FIN,一直处于FIN_WAIT_2状态。直到超时被关闭。
TIME_WAIT
当发起断开连接的那一端收到对端发来的FIN时,则会向对端发送一条ACK。自己处于TIME_WAIT状态,一般TIME_WAIT状态会持续2MSL(报文最大生存时间)。因为此时关闭连接端向对端发送的ACK是有可能丢包的,所以需要处于TIME_WAIT状态一段时间。如果ACK报文丢包了,则会进行重传。
所以这也就可以解释了,为什么我的服务器有时候不能立即重启。因为当服务器主动断开连接时,如果此时有客户端还没有关闭,那么服务器会处于TIME_WAIT状态,此时重启就会绑定出错,我们可以用地址复用的方法来解决。
滑动窗口
如果我们发送数据,在没有收到应答之前,是可能出现丢包的情况,如果丢包,就要超时重传。为了支持超时重传,我们是不是要把数据保存起来?保存在哪里呢? 保存在TCP的发送缓冲区
**这个发送缓冲区至少被分成三个区域,第一段区域是已经发送&&收到应答的数据,第二段区域是已经发送,但还没有收到应答的区域。第三段是数据尚未发送的区域。**当然还有第四段,空区域,新发送的数据会被拷贝到这段区域。
而中间那段已经发送但没有收到应答的区域,被称之为滑动窗口
1.如何看待滑动窗口?
发送缓冲区的本质就是一个数组,而滑动窗口则代表的一段区间,我们可以用win_start和win_end两个指针来表示这段区间。
2.滑动窗口的开始大小是怎么设定的?之后怎么变化?
很简单,我们上面说了滑动窗口是win_start - win_end的区间。那么我们开始就把 win_start = 0,win_end = win_start + tcp_win(16位窗口大小的值)。之后无论滑动窗口怎么滑动,都必须保证在对方的接受能力范围之内。
**窗口的大小就等于对方的接受能力,也就是tcp_win16位窗口大小字段(不考虑网络拥塞的情况下)。 **
3.窗口一定会向右滑动吗?会向左滑动吗?
首先,滑动窗口左边的内容是已经发送并且收到应答了的,再次发送没有意义,所以滑动窗口不会向左滑动
滑动窗口大部分情况下是会向右滑动的,但是如果对方的接收能力为0呢?无法往对方发送数据,所以此时的滑动
窗口是不变的。所以滑动窗口可能向右滑动,也可能保持不变
4. 滑动窗口会一直不变吗?会变大吗?会变小吗?为什么?
滑动窗口可能会不变,但不可能一直不变!
会变大吗? 会! 当对端的上层突然把缓冲区数字拿走时,对端的接收能力大大提升,当发送端重新获得对方的窗口大小时。滑动窗口也会跟着变化,因为对方的接受能力变强,所以发送端的滑动窗口也会变大!
会变小吗?会!当对端的上层迟迟不拿数据,而发送端不断的发数据,那么win_start就会越来越大,而win_end却没有变,这也就意味滑动窗口在慢慢变小。直到最后对端的缓冲区被打满了,发送端的滑动窗口也会变为0。
5…收到应答确认的时候,如果不是最左侧发送的报文的确认,而是中间的,结尾的怎么办?要滑动吗?
这种时候我们要分两种情况,一种是应答丢了,数据没丢,一种是数据丢了。
应答丢了,数据没丢
如果是应答丢了,数据没丢的时候,没关系。因为TCP有32位确认序号。即使的前面的应答丢了,但是后面的应答的32位确认序号填的是后面数据的,那么发送数据方就知道确认序号之前的数据已经全部被对方收到了。
所以应答丢的情况,滑动窗口会照样滑动。
数据丢了
TCP是有32位序号的,在收到一批报文后会对报文进行排序。如果中间出现丢包的情况,那么确认序号就不能填收到的,而是丢包之前的那一个。假设现在收到4个报文 0 - 1000 , 1001 - 2000 , 3001 - 4000,而在对报文进行排序时发现中间的 2001 - 3000丢了,那么 3000之后的报文应答的确认序号只能填 2001 ,当对端连续收到3个2001时,就知道自己 2001 - 3000的报文丢包了。所以就会进行重传。
当收到好几个重复的确认序号时,发送端就知道自己之前的报文丢包了,所以会进行重传机制。而滑动窗口也只是滑到2001的位置。并且重新发送数据。
6. 滑动窗口会一直向后滑动吗?如果空间不够了怎么办?
从物理结构上看,滑动窗口是一个数组。但是从逻辑结构上看,滑动窗口是一个环形结构。我们可以通过 %数组的长度,来实现一个环形结构的数组。
流量控制
流量控制其实在上篇文章介绍16位窗口大小就已经提到过,通过16位窗口大小知道对方的接收能力。但这还不够,因为我们考虑到了对方的接收能力,但却没有考虑的网络的接收能力。如果网络当前坏境较差,发送大量数据会加重网络环境。那么此时只能按照网络能接收的能力来,而这一块我们会在下面拥塞控制中得到。所以滑动窗口的大小 = Min(对方接收能力,网络接收能力)
拥塞控制
前面我们一直在说客户端与服务端之间的可靠性。可是你有没有想过,无论是哪端发送数据,数据都是要在网络中传输的。如果网络出现问题了呢?如果网络因为大量报文而造成网络拥塞了呢?那么TCP也要有一种机制来保证在网络中传输的可靠性,这种机制就是拥塞控制
TCP如何判断网络拥塞
我们都了解当报文丢失时会触发超时重传。但这是少量报文丢失的情况,如果大量报文丢失的情况呢?这时候也要进行重传吗?答案是绝对不可行!因为此时网络可能已经出现问题了!本来就因为大量数据而造成网络拥塞,而你此时又重传大量的报文。要知道,全世界那么多主机,大部分都是使用的TCP协议。也就是说如果每台主机都去重传大量的报文,那么就会造成网络瘫痪。
举个例子:
当你们学校考试,假如考的是数据结构这门课。如果全班50人只有1-2个人挂科了,那么你会觉得这种情况是正常的。而如果只有1 - 2 个人没挂科,那么你们一定会认为这一定是老师的问题,老师出的题太难太偏。
所以TCP判断网络拥塞也是这样一个道理,当你发了1000个报文,丢失了3个报文。那么会认为是正常的丢包,进行超时重传。如果丢失的是999个报文,这时候就绝对不能进行重传了!因为这种情况肯定是网络发生拥塞,要进行拥塞控制。
TCP的慢启动机制
慢启动机制就是先发送少量数据探探路,摸清当前的网络状态,再决定按照多大的速度传输数据。
- 引入一个概念,拥塞窗口(cwnd)
- 发送刚开始的时候,拥塞窗口大小为1
- 每收到一个ACK应答,拥塞窗口 + 1。相当于每次发送的报文 * 2
- 每次发送数据包的时候,将拥塞窗口和对端发来的窗口大小作比较,取较小值作为实际发送的窗口
像上面这样的拥塞窗口速度,是指数级别的,刚开始很慢,但后面会非常快。所以我们要引入一个慢启动的阈值 ,这个阈值被称为ssthresh。
TCP刚开始启动的时候,阈值为滑动窗口的最大值。拥塞窗口则为1,此时拥塞窗口呈指数级增长,直到增长的阈值时呈线性增长。在线性增长过程中如果遇到了网络拥塞,则会进行乘法减小。把阈值更改为遇到网络拥塞时的一半,拥塞窗口从1开始继续慢启动。重复上面的步骤。
遇到网络拥塞时,是不能发送太多数据的,所以从1开始发送数据进行慢启动,阈值之前呈指数增长,阈值之后呈线性增长。前期的指数增长速度其实是比较慢的,可以有效的缓解网络的压力。而中后期网络恢复,又可以快速的恢复通信。因为指数增长后期非常的快,所以引入了阈值,超过阈值则进行线性增长。
当然,如果网络非常的好,而发送的数据又不多的话。拥塞窗口会一直缓慢增长,但这并不是大问题。因为实际的发送窗口是对端窗口大小和拥塞窗口的较小值。