文章写了很久,后面有大量的图帮助大家理解,如有错误的地方,还望大神批评指正
目录
TCP协议
TCP字节流
TCP头部
流量控制
TCP连接的建立与终止
三次握手
四次断开
RST
TCP超时与重传:
慢启动和拥塞避免算法
慢启动与拥塞避免
丢包
快速恢复算法
滑动窗口算法
TCP提供一种面向连接的、可靠的字节流服务。面向连接意味着两个使用TCP的应用(通常是一个客户端和一个服务器)在彼此交换数据之前必须先建立一个TCP连接。
在网络传输的每个TCP数据包,必须要接收方回送确认数据包。
TCP通过下列方式来提供可靠性:(下面的每句话你都能在后面找到答案,在这里没看懂没关系,你只需要知道有这个东西,实在不想看的可以跳过)
1、应用程序被分割成TCP认为最合适发送的数据块。
2、当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。
3、当TCP收到发自TCP连接另一端的数据,它将发送一个确认。这个确认不是立即发送,通常将推迟几分之一秒。
4、TCP将保持它首部和数据的检验和。
5、既然TCP报文段作为IP数据报来传输,而IP数据报的到达可能会失序,因此TCP报文段的到达也可能会失序。如果必要,TCP将对收到的数据进行重新排序,将收到的数据以正确的顺序交给应用层。
6、既然IP数据报会发生重复,TCP的接收端必须丢弃重复的数据。
7、TCP还能提供流量控制。TCP连接的每一方都有固定大小的缓冲空间。
8、TCP的接收端只允许另一端发送接收端缓冲区所能接纳的数据。这将防止较快主机致使较慢主机的缓冲区溢出。
应用程序交给TCP的数据,TCP会根据自己的判断进行重新组装,然后发送给另一方,然后由另一方TCP重排序,交给应用程序,TCP不会对字节流的内容做任何解释,TCP只负责传输。
这里就跟UDP完全不一样,UDP对于应用程序传递下来的包,无论大小,都只会加一个UDP头部,然后交给IP,意思就是说,怎么传输跟我UDP没关系,这是你IP的事,IP拿到UDP数据报后,在传输过程中如果数据报过大就会产生IP分片,由于本章主要讲TCP对于IP分片不做过多的阐述。
TCP则没有UDP这样不负责任,TCP会在自己这一层就把数据分割或组装成合适发送的数据块。这里对应于1
了解TCP头部是必须,这对你理解三次握手和四次断开有很大的帮助
假设TCP头部没有携带数据和选项,最小也是20字节。携带数据最大为60个字节
TCP前4个字节是端口号,在IP头部中存在IP地址,帧头部存在MAC地址
序列号和确认序列号:假设序列号从0开始,当发送方发送了一个500大小的包给接收方(发送序列号就是500),接受方接受到了这个包,会回复一个501的确认序列号,表示前500个包我收到了,期待501之后的数据包。为什么会设置一个序列号一个确认序列号呢,TCP创建连接后是全双工的,双方都可以发送数据,自然就需要两个了,看下图
客户端发送一个500大小的包给服务端,服务端收到后回复一个确认序列号为501,表示前500个包我收到了,期待收到501之后的包,但是序列号为100,表示服务端在回复的收到的包时,也携带了自己想给客户端的数据大小为100,这里就对应前面的第1条TCP会直接组装认为合适大小的数据进行发送,还有前面第3条,为什么会推迟发送确认呢,假如收到数据马上发送确认是非常浪费资源的,因为确认数据包没有任何数据,但是光TCP头部就有20个字节(还有IP头部也有20个字节),所以推迟发送确认是为了等待应用程序接下来有没有要发送的数据,如果有的话,这个数据就可以搭乘顺风车,跟着确认数据包一起回送给客户端。
但通过真实的抓包,我们发现序列号并不是从0开始的,而是一个随机数,在电脑开机后,操作系统就会维护一个序列号,这个序列号会按照一定的规律进行增长,当某一个时刻有TCP想建立连接,就会使用操作系统的序列号去建立连接。
6个状态位,设置为1表示有效
URG:紧急指针有效(基本用不到)
*ACK:确认序号有效
PSH:接收方应尽快将这个报文交给应用层(这个基本没卵用,想插队门都没有,后面排队去,应用程序基本不理)
*RST:重新连接
*SYN:同步序号用来发起一个连接(TCP三次连接,开启SYN)
*FIN:发起端完成发送任务(四次断开,结束FIN)
TCP通过窗口大小来进行流量控制,窗口大小是16位,也就表示发送方最多一次发送65535的数据,当发送方发送了数据之后,接受方通过回复确认包的时候,就会告诉发送方我接收到了多少数据,我这里有多少数据被应用程序读走了,我这里还有多少TCP缓存(窗口大小),这样发送方也就知道下次最多一次发送多少数据了。(注意,这里说的一次最多发送,不是一次发送,而是一直在发包,但是大小不能超过窗口大小)
看下图,发送方给接收方发送了2000大小的数据,接受方应用程序读了1000,但是还有1000在TCP缓存中,那么在回送确认数据包的时候,就会告诉发送方,我的窗口大小还有4000,你接下来可以发送数据,但是不能超过4000
当发送方接收到一个0的窗口大小就会停止发送数据(因为这个时候接收方的缓存已经满了),会等待一个窗口更新的包(应用程序把数据读走了,缓存又空出来了),才能继续按照新的窗口大小发送数据。
窗口扩大因子:通过窗口大小知道,这个值太小了,在现在的高速网络环境下,发一下又要停一下,效率非常低,所以在TCP首部的选项中,就会有一个窗口扩大因子,将窗口大小放大多少倍
校验和
校验和是用来检测数据包在网络上面传输是否被篡改,TCP计算校验和是将TCP头部和数据一起计算校验和,IP是只计算IP头部
MTU:路由器最大传输传输单元
MSS:最长报文大小,每个连接方通常都在通信的第一个报文段中指明这个选项,它指本段所能接收的最大长度报文段,在后续的发包过程中,如果数据过大,在TCP层面就会控制数据大小到MSS大小之下,所以TCP出现IP分片(在IP层面)的可能性很小。MSS会选择通信双方最小的MSS为基准进行发送。MSS一般是MTU的大小-IP头部20字节-TCP头部20字节。
序列号本来是标识接受了多少数据的,将数据重排序的依据,但如果发送的数据包只有TCP首部没有TCP数据,在SYN位或者FIN位为1时,也会将序列号+1
有了前面的知识,三次握手每次数据包发送了什么,就一目了然了
1、客户端向服务端发送连接请求,客户端初始化序列号,客户端窗口大小,SYN=1,TCP头部选项中包含发送方的MSS、窗口扩大因子
2、服务端回复确认消息,服务端初始化序列号,确认序列号为“客户端初始化序列号+1”(前面说过SYN=1的情况,即使没有数据也需要+1),服务端窗口大小,SYN=1,ACK=1,TCP首部选项中包含接受方MSS、窗口扩大因子
3、客户端回复确认消息,客户端初始化序列号+1,确认序列号为“服务端初始化序列号+1”,ACK=1,TCP头部选项中包含窗口扩大因子
用大白话来描述就是
A:您好,我是A。
B:您好A,我是B。
A:您好B。
三次握手状态时序图
连接建立之后,一切顺利,现在就要开始发包了,根据协商好的MSS大小合理的控制数据包的大小,根据接收方的窗口大小,进行流量控制,发送的总数据包不会超过接收方的缓存大小。但是出现了下面的这种情况
在三次握手完成后,根据协商,MSS应该为min(1000,1500)-40=960,可是中间路由器的MTU大小为500,显然过不去啊,如果是UDP的话肯定会执行IP分片,但是TCP不会,在IP头部中有一个标志位为DF,当DF设置为1的时候表示不分片,如果过不去,你就把这个包丢掉,然后回送一个ICMP差错报文告诉我是什么原因丢的包就可以,如果是MTU的问题,ICMP报文一般会将丢包路由器的MTU大小回送回去,这样子发送方就知道MSS设置的太大了,就会在TCP头部选项中放置最新的MSS大小,这样子接收方也就知道该按照多大的MSS去发包了。
如果你理解了三次握手,再看下面的四次断开,就知道它每一步在干什么了
因为TCP是全双工的,对于断开连接来说,就需要断开两边的连接
四次断开状态时序图
现在问题又来了,为什么客户端发送完最后一个ACK,还需要等待2MSL的时间,什么是MSL?
MSL:报文最大生存时间,当数据包在网络上传输超过了这个时间就会被丢弃
我们先假设客户端发送完最后一个ACK,就跑路了,但是很不辛这个包在半路丢了,服务端等啊等,还是没有收到回复的ACK包,他就会重新发送断开连接请求FIN,但是因为客户端已经跑路了,这个端口号肯定被释放掉了,这个请求就会有两种结果,一、端口号不存在,主机收到TCP数据包后,发现我并没有这个端口啊,就会将这个包丢掉,并回复ICMP差错报文“端口不可达”,二、端口号被新的应用程序占用了,FIN请求就会错误的发送到新的应用程序上面来,这个应用程序肯定很懵,哎你这个人怎么回事还没建立连接就发数据,就会回复一个RST=1的包,强行将连接释放掉,服务端收到RST=1的包时就知道,这小子肯定跑路了。
所以为了避免将包错误的发送到其它的应用程序,TCP规定客户端发送完最后一个ACK后,必须等待2MSL的时间,为什么是2MSL呢,请看下图
当发送完最后一个ACK,到服务端重发FIN的最长时间不过是2MSL,所以等待2MSL是最保险一定能收到服务端重发的FIN,可是万一等待超过了2MSL才收到服务端重发的FIN,这个时候客户端也不会回复ACK,我已经等了这么久了仁至义尽了,就会直接发送RST=1的包,将连接释放掉。
听到这里听懂的小伙伴可能就有问题了,“你不是说TCP回复确认数据包的时候会等待一段时间,也就是三次握手第二次的时候会把回复的确认数据包和想要连接的SYN放在一个包中传递,所以是三次握手,那为什么四次断开的时候,服务端回复确认数据包和关闭连接FIN要分开传输呢,合并一次传输不就是三次断开了吗?”
如果你能想到这个问题就说明你对前面的知识有了自己的理解,这非常好,让我们再来回顾一下前面的的断开连接,客户端发起断开连接FIN请求,我们可以说客户端是主动关闭,而服务端是被动关闭,根据TCP协议的约定,每一个数据包都必须要进行确认,所以在收到FIN请求时,就必须回复一个ACK请求,此时服务端很想快点发送FIN断开连接,但它不能,它还有数据要发给客户端,所以就会在主动发送FIN之前,已经发送了ACK之后,将数据发送给客户端,客户端可能也不会理睬这些数据,在数据发完之后客户端就会发送FIN主动释放连接了,所以在服务端没有数据发送给客户端的时候,也可以做到三次断开。
复位报文段:在TCP首部中的RST用于复位,如果一个TCP报文段出现错误,TCP都会发出一个复位报文段,用来释放一个连接或者重新连接
当客户端试图去连接一个没有开启的服务器(应用程序没有开启,端口不存在),主机(防火墙)就会回复一个RST直接关闭这个连接
异常关闭:当还在执行关闭连接操作的两端收到另外一端发来的RST包后,会立刻丢弃想要发的数据包,将连接关闭。
四个定时器
重传定时器:会为每个包设置一个重传定时器,当超过定时器的时间,就会重新发送这个包,定时器的时间会采样RTT的大小,并根据网络变化进行加权计算,被称为自适应重传算法。
坚持定时器:主要防止窗口更新的包被丢弃。假如一个接收方窗口被占满了,在下一个ACK包中,会回复一个窗口大小为0的包,发送方看到窗口为0就不会发送数据了,当接收方的窗口大小又腾出来了,就会发送一个窗口更新包,假如这个包丢失了,可能会让双方处于停滞的状态,这个时候就需要坚持定时器,发送方根据坚持定时器,每隔一段时间就会发送一个窗口试探的包,保证能拿到窗口的最新大小。
保活定时器:如果TCP连接的双方都没有向对方发送任何数据,则在两个TCP模块之间不会交换任何数据,这意味着TCP的连接会一直保持,还有可能连接的双方突然某一方主机崩溃重启了,也可以利用保活定时器及时探测到,但是保活定时器并不是TCP的规范,探测的工作应该交给应用程序去实现,不应该在TCP层面实现。
2MSL定时器:这个定时器请参考前面2MSL问题
两种算法目的不同、独立的算法。但是当拥塞发生时,我们希望降低分组进入网络的传输速率,于是可以调用慢启动来做这一点。在实际中这两个算法通常一起实现
用大白话来说就是,流量控制是为了避免发送方发送的太快,把接收方的缓存占满,而拥塞避免和慢启动是为了避免把中间路由器的缓存占满
慢启动的目标是通过观察到新分组进入网络的速率应该与另一端返回确认的速率相同而进行工作,意思就是慢启动的目标就是希望找到一个发送包的速度和接收方返回确认的速度一样,最好的状态就是发送方发送一个包之后,就可以收到上一个包的确认包。
慢启动依靠拥塞窗口(cwnd)来决定每次发送几个包,拥塞窗口处于发送端(前面说的流量控制依靠接收方的窗口,叫做通告窗口Advertised window)
在建立连接之后拥塞窗口初始化为1个数据包的大小,之后每接收到一个ACK确认包拥塞窗口就+1,表示每次可以发送的数据包的数量,加到什么时候为止呢,有一个值ssthresh为65535个字节,当超过这个值之后,就不会再+1,慢启动算法结束,接下来就执行拥塞避免算法,拥塞避免算法不会将cwnd+1,而是加1/cwnd,但是请注意,cwnd不会超过接受方的缓存大小也就是通告窗口的大小。
丢包会有两种形式
第一种是重传定时器到时间了,没有收到确认包
第二种是收到了3个重复的ACK
无论出现那一种情况,ssthresh的值都会被设置成窗口大小的一半(窗口大小是值cwnd和接收方通告窗口大小的最小值,但是最少2个报文段)
假设发生的是第一种情况,TCP就会认为现在的网络状态并不是很好,就会将cwnd置为1,然后开始执行慢启动,重传丢失的数据包
假设发生的是第二种情况,这种情况大概就是这样的发送方发送了1,2,3,4四个包,但是不知道什么原因第2个包丢失了,1、3、4都接收到了,这个时候接收方就会发送3个一样的ACK(确认序列号为2的序列号+1),发送方收到三个重复的ACK就知道2这个包丢失了,这个时候TCP会认为网络状态还是可以的,只是有一点小插曲,如果直接执行慢启动,反而会让速度减下来,出现卡顿的情况,所以如果是收到三个重复的ACK就会去执行我们下面要说的快速恢复算法
当收到3个重复的ACK的时候,会执行快速重传
ssthresh的值都会被设置成窗口大小的一半,重传丢失的报文,设置cwnd为ssthresh加上3个数据包大小,很明显这之后走的还是拥塞避免算法
大家都知道TCP的流量控制是使用滑动窗口算法来实现的,前面我们只是举了一个很简单的例子,下面我们来举一个更加复杂的例子,来帮助大家理解滑动窗口算法
滑动窗口将要发送的数据分为四个部分
1、已经发送的并且已经确认过的
2、已经发送过的,但是还未收到确认的
3、可以发送,但是还没有发送的数据
4、不能发送的数据
滑动窗口对于一个滑动窗口来说,只能进行的操作是合拢和张开,收缩一般是不支持的
什么是合拢呢,当发送方发送了数据并且收到了确认包,就会执行合拢
什么是张开呢,当接收方应用程序将数据读走了,就会执行张开
看上图滑动窗口,假设刚刚三次握手完毕建立了连接(这里我们抛开上面说过的慢启动之类的),发送方肯定知道最多可以发送9个数据
发送方发送了1,2,3三个数据
接收方回复了第1,2个包的ACK,执行合拢,但是数据并没有被应用程序读走,就不会执行张开,我们来看看第2个包的确认包,确认序列号应该为3,窗口大小是7(3这个数据包还没有到达接收方),这个时候接收方还能发送的数据应该等于:接收方可发送数据=确认的ACK窗口大小7-发送未确认的数据包1=6
假设接收方应用程序把1,2读走了,就会执行张开操作,并且在回复的ACK中将窗口大小回复回去
怎么样,通过上面的举例,你是不是对滑动窗口协议有了新的认识呢
各位看官都看到这里了,点个赞再走呗