面向连接的可靠的运输协议 TCP
TCP被称为面向链接的,这是因为在一个应用进程可以开始向另一个应用进程发送数据之前,这两个进程必须相互“握手”,即他们必须相互发送某些预备报文段,以建立确保数据传输的参数。作为TCP连接建立的一部分,连接的双方都将初始化与TCP连接相关的许多TCP状态变量。
由于TCP协议只在端系统中运行,而不在中间的网络元素(路由器和交换机)中运行,所以中间的网络元素不会维持TCP连接状态。事实上,中间路由器对TCP连接完全视而不见,他们看到的是数据,而不是连接。
TCP连接提供的是全双工服务:如果一台主机上的进程A与另一台主机上的进程B存在一条TCP连接,那么应用层数据就可以从进程B流向进程A的同时,也从进程A流向进程B。TCP连接也总是点对点的。即在单个发送方与接收方之间的连接。所谓“多播”,即在一次发送操作中,从一个发送方将数据传送给多个接收方,对TCP来说这是不可能的。对于TCP而言,两台主机是一对,而三台主机则太多!
TCP协议详解
特点
TCP是面向连接的协议
TCP的一个连接有两端(点对点通信)
TCP提供可靠的传输服务
TCP协议提供全双工的通信
TCP是面向字节流的协议
TCP首部格式
序号:
- 一共32位,范围是0-2^32-1
- 一个字节一个序号
- 代表数据首字节序号
确认号
0-2^32-1
一个字节一个序号
期望收到数据的首字节序号(确认号为N:则表示N-1序号的数据都已经收到)
数据偏移
占4位:0-15,单位为:32位字
数据偏离首部的距离
TCP标记
占6位,每位各有不同意义
窗口
- 占16位:0-2^16-1
- 窗口指明允许发送的数据量
- 确认号:501 ,窗口:1000,那么501~1500字节的数据都可以传输
校验和
紧急指针
- 紧急数据(URG = 1)
- 指定紧急数据在报文的位置
TCP选项
最多40字节
支持未来的拓展
TCP 可靠传输基本原理
ARQ协议:自动重传请求是OSI模型中数据链路层和传输层的错误纠正协议之一。它通过使用确认和超市这两个机制,在不可靠服务的基础上实现可靠的信息传输。如果发送方在发送一段时间之内没有收到确认帧,他通常会重新发送。ARQ包括停止等待ARQ协议和连续ARQ协议。
停止等待ARQ协议
- 停止等待协议是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认(回复ACK)。如果过了一段时间(超时时间后),还是没有收到ACK确认,说明没有发送成功,需要重新发送,直到收到确认后再发下一个分组。
- 在停止等待协议中,若接收方收到重复分组,就丢弃该分组,但同时还要发送确认;
优点:简单
缺点:信道利用率低,等待时间长。
连续ARQ协议
- 连续ARQ协议可提高信道利用率。发送方维持一个发送窗口(滑动窗口协议)内的分组,凡位于发送窗口内的分组可以连续发送出去,而不需要等待对方确认。接收方一般采用累计确认,对按序到达的最后一个分组发送确认,表明到这个分组为止的所有分组已经正确收到了。
优点: 信道利用率高,容易实现,即使确认丢失,也不必重传。
缺点:不能向发送方反映出接收方已经正确收到所有分组的信息。比如:发送方发送了5条消息,中间第三条丢失了,这时接收方只能对前两个发送确认。发送方无法知道后三个分组的下落,而只好把后三个全部重传一次。这也叫 回退N,表示需要退回来重传已经发送过的N个消息,显然单个分组的差错就能够引起重传大量分组,许多分组根本没有必要重传。
选择重传协议
- 选择重传协议通过让发送方仅重传那些它怀疑在接收方出错的分组而避免了不必要的重传。接收方将确认一个正确接受的分组而不管其是否按序。失序的分组将被缓存知道所有丢失分组皆被收到为止。
TCP的差错回复机制最好被分类位滑动窗口(GBN)协议与选择重传(SR)协议的结合体。
流量控制
如果发送者发送数据过快,接收者来不及接收,那么就会有分组丢失。为了避免分组丢失,控制发送者的发送速度,使得接收者来得及接收,这就是流量控制。流量控制的根本目的是防止分组丢失,他是构成TCP可靠性的一方面。
如何实现流量控制?
由滑动窗口协议(连续ARQ协议)实现。滑动窗口协议即保证了分组无差错、有序接受,也实现了流量控制。主要的方式就是接收方返回的ACK中包含自己的接收窗口的大小,并且利用大小来控制发送方的数据发送。
流量控制引发的死锁?怎么避免死锁的发生?
- 当发送者收到了一个窗口为0的应答,发送者便停止发送,等待接收者的下一个应答。但是如果这个窗口不为0的应答在传输过程丢失,发送者一直等待下去,而接收者以为发送者已经收到该应答,等待接收新数据,这样双方就相互等待,从而产生死锁。
为了避免流量控制引发的死锁,TCP使用了持续计时器。每当发送者收到一个零窗口的应答后就启动该计时器。时间一到便主动发送报文询问接收者的窗口大小。若接收者仍然返回零窗口,则重置该计时器继续等待;若窗口不为0,则表示应答报文丢失了,此时重置发送窗口后开始发送,这样就避免了死锁的产生。
拥塞控制
拥塞控制和流量控制的区别
发送方维持一个叫做拥塞控制窗口的状态变量。拥塞窗口的大小却决于网络的拥塞程度,并且动态的在变化,发送方让自己的发送窗口等于拥塞控制,另外考虑到接收方的接收能力,发送窗口可能小于拥塞窗口。
TCP的拥塞控制采用四种算法,即 满开始、拥塞避免、快充次、快恢复
拥塞控制算法
-慢开始算法的思路是,不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是由小到大逐渐增加拥塞窗口的大小
为了防止cwnd增长过大引起网络拥塞,还需设置一个慢开始门限ssthresh状态变量。ssthresh的用法如下:当cwnd<ssthresh时,使用慢开始算法。
当cwnd>ssthresh时,改用拥塞避免算法。
当cwnd=ssthresh时,慢开始与拥塞避免算法任意
拥塞避免算法
拥塞避免算法让拥塞窗口缓慢增长,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍。这样拥塞窗口按线性规律缓慢增长。
无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有按时收到确认,虽然没有收到确认可能是其他原因的分组丢失,但是因为无法判定,所以都当做拥塞来处理),就把慢开始门限ssthresh设置为出现拥塞时的发送窗口大小的一半(但不能小于2)。然后把拥塞窗口cwnd重新设置为1,执行慢开始算法。这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生拥塞的路由器有足够时间把队列中积压的分组处理完毕。
(1)拥塞窗口cwnd初始化为1个报文段,慢开始门限初始值为16
(2)执行慢开始算法,指数规律增长到第4轮,即cwnd=16=ssthresh,改为执行拥塞避免算法,拥塞窗口按线性规律增长
(3)假定cwnd=24时,网络出现超时(拥塞),则更新后的ssthresh=12,cwnd重新设置为1,并执行慢开始算法。当cwnd=12=ssthresh时,改为执行拥塞避免算法
TCP三次握手
TCP的连接建立,我们常常称为三次握手。
A: 您好,我是A。
B:您好A,我是B。
A:您好B
为什么要三次,而不是两次?按说两个人打招呼,一来一回就可以了,为什么不是四次?
假设这个通路是非常不可靠的,A要发起一个连接,当发了第一个请求渺无音信的时候,会有很多的可能性,比如第一个请求包丢了,再如果没有丢,但是绕了弯路,超时了,还有B没有响应,不想和我连接。
A不能确认结果,于是再发,再发。终于有一个请求包到了B,但是请求包到了B的这个事情,目前A还是不知道,A还有可能再发。
B收到了请求包,就知道了A的存在,并且知道A要和它建立连接。如果B不愿意建立连接,则A会重试一阵后放弃,连接建立失败,没有是问题;如果B是乐意建立连接,则会发送应答包给A。
当然对于B来说,这个应答包也是一如网络深似海,不知道能不能到达A。这个时候B自然不能认为连接是建立好了,因为应答包仍然会丢,会绕弯路,或者A已经挂了都有可能。
而且这个时候B还能碰到一个诡异的现象就是,A和B原来建立了连接,做了简单的通信,结束了连接。还记得吗? A建立连接的时候,请求包重复发了几次,有的请求包绕了一个大圈又回来了,B会认为这也是一个正常的请求话,因此建立了连接,可以想象这个连接会不会进行下去,也没有终结的时候,纯属单相思了。因而两次握手肯定不行。
B发送的应答可能会发送很多次,但是只要一次到达A,A就认为连接已经建立了,因为对与A来说,他的消息有去有回。A会给B发送应答之应答,而B也在等这个消息,才能确认连接建立,只有等到了这个消息,对于B来讲,才算他的消息有去有回。
当然A发送给B的应答之应答也会丢,也会绕路,甚至B挂了。按理来说,还应该有个应答之应答之应答,这样下去没底了。所以四次握手是可以的,四十次都可以,关键四百次也不能保证就真的可靠了。只要双方的消息都有去有回,就基本可以了。
好在大部分情况下,A和B建立连接之后,A会马上发送数据的,一旦A发送数据,则很多问题都得到了解决。例如A发送给B的应答丢了,当A后续发送的数据到达的时候,B可以认为这个连接已经建立,或者B压根就挂了,A发送的数据,会报错,说B不可达,A就知道B出事情了。
当然你可以说A比较坏,就是不发数据,建立连接后空着。我们在程序设计的时候,可以要求开启keepalive机制,即使没有真实的数据包,也有探活包。
三次握手除了双方建立连接外,主要还是为了沟通一件事情,就是TCP包的序号问题。
A要告诉B我这面发起的包的序号起始是从哪个号开始的,B同样也要告诉A,B发起的包的序号是从哪个号开始的。为什么序号都不能从1开始呢?因为这样往往会出现冲突。
例如,A连上B之后,发送了1,2,3三个包,但是发送3的时候,中间丢了,或者绕路了,于是重新发送,后来A掉线了,重新连上B后,序号又从1开始,然后发送2,但是压根没想发送3,但是上次绕路的那个3又回来了,发给了B,B自然认为,这就是下一个包,于是发送了错误。
因而,每个连接都要有不同的序号。这个序号的起始序号是随着时间变化的,可以看成一个32位的计数器,没4微秒加一,如果计算下,如果到重复,需要4个多小时,那个绕路的包早就死翘翘了,因为我们都知道IP包头里有个TTL,也即生存时间。
TCP四次挥手
A:啊,我不想玩了。
B:噢,你不想玩了我知道了。
这个时候,还只是A不想玩了,也即A不会再放松数据了,但是B能不能在ACK的时候直接关闭呢?当然不可以了,很有可能A是发完了最后的数据就准备不玩了,但是B还没做完自己的事情,还是可以发送数据的,所以称为半关闭的状态。
这个时候A可以选择不再接受数据了,也可以选择在接收一段数据,等待B也主动关闭。
B:A啊,好吧,我也不玩了,拜拜。
A:好的,拜拜。
这样整个连接就关闭了。但是这个过程有没有异常情况呢?当然有,上面是和平分手的场面。
A开始说 “不玩了”,B说“知道了”,这个回合没什么问题,因为在此前,双方还处于合作状态,如果A说“不玩了”,没有收到回复,则A会重新发送“不玩了”。但是这个回合结束之后,就有可能出现异常情况了,因为已经有乙方率先撕破脸。
一种情况是,A说完“不玩了”之后,直接跑路,是会有问题的,因为B还没有发起结束,而如果A跑路,B就算发起结束,也得不到回答,B就不知道该怎么办了。另一种情况,A说完“不玩了”,B直接跑路,也是有问题的,因为A不知道B是还有事情要处理,还是过一会儿会发送结束。
那怎么解决这些问题?TCP协议专门设计了几个状态来处理这些问题。
断开的时候,我们可以看到,当A说“不玩了”,就会进入 FIN_WAIT_1的状态,B收到“A不玩”的消息后,发送知道了,就进入CLOSE_WAIT的状态。
A收到“B说知道了”,就进入FIN_WAIT_2的状态,如果这个时候B直接跑路,则A将永远在这个状态。TCP协议里面并没有对这个状态的处理,但是Linux有,可以调整tcp_fin_timeout这个参数,设置一个超时时间。
如果B没有跑路,发送了“B也不玩了”的请求到达A是,A发送“知道B也不玩了”的ACK后,从FIN_WAIT_2状态结束,按说A可以跑路了,但是最后的这个ACK万一B收不到呢?则B会重新发送一个“B不玩了”,这个时候A已经跑路的话,B就在也收不到ACK了,因而TCP协议要求A最后等待一段时间TIME_WAIT,这个时间要足够长,长到如果B没收到ACK的话,“B说不玩了”会重发的,A会重新发一个ACK并且足够时间到达B。
A直接跑路还是有一个问题是,A的端口就直接空出来了,但是B不知道,B原来发送的很多的包还有可能在路上,如果A的端口被一个新的应用占用了,这个新的应用会受到上个连接B发过来的包,虽然序列号是重新生成的,但是这里要上一个双保险,防止产生混乱,因而也需要等足够长的时间,等到原来B发送的所有的包都死翘翘了,再空处端口来。
等待的时间设为2MSL,MSL是Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为TCP报文基于是IP协议的,而IP头中有一个TTL域,是IP数据包可以经过的最大路由数,每经过一个处理他的路由器此值就减1,当此值位0则数据包将被丢弃,同时发送ICMP报文通知源主机。协议规定MSL位2分钟,实际应用中常用的是30秒,1分钟和两分钟等。
还有一种情况就是,B超过了2MSL的时间,依然没有收到他发的FIN的ACK,怎么办呢?按照TCP的原理,B当然还会重发FIN,这个时候A再收到这个包之后,A就表示,我已经在这里等了这么长时间了,已经仁至义尽了,之后的我都不会认了。于是就直接发送RST,B就知道A早就跑路了。