TCP的特点
TCP是TCP/IP体系中非常复杂的一个协议,TCP的主要特点:
1、TCP是面向连接的传输层协议,在应用程序使用TCP协议之前,必须先建立TCP连接;在传送数据之后必须释放已建立的TCP连接,把TCP理解成打电话就行了。
2、每一条TCP连接只能由两个端点(endpoint),TCP连接时点对点的连接。
- 这个端点有一个名字,名称为套接字(socket)或插口。
- 根据RFC 793的定义,端口号拼接(concatenated with)IP地址即构成了套接字。
- 套接字socket =
(IP地址:端口号)
- 套接字socket =
- 每一条TCP连接唯一地被通信两端地两个端点(即两个套接字)所确定。
- TCP连接 ::=
{socket1, socket2}={(IP1:port1), (IP2:port2)}
- TCP连接 ::=
- 根据RFC 793的定义,端口号拼接(concatenated with)IP地址即构成了套接字。
3、TCP提供可靠交付的服务,该层传输的数据,无差错、不丢失、不重复,并且按序到达。
4、TCP提供全双工通信,TCP允许通信双方的应用进程在任何时候都能发送数据,TCP连接的两端都设有发送缓存和接收缓存,用来临时存放双向通信的数据。
5、TCP面向字节流,流(stream) 指的是流入到进程或从进程流出的字节序列,这一点的意思就是说:
- TCP认为应用程序交下来的数据仅仅是一连串无结构的字节流,具体来说就是,如果看成数据块,TCP不保证发送接收方一致,但看成字节流则保证一致。
TCP并不关心应用进程一次把多长的报文发送到TCP的缓冲中,而是根据对方给出的窗口值和当前网络拥塞的程度来决定一个报文段应该包含多少字节,这里就是后续的拥塞控制部分应该处理的事情了。
TCP连接
TCP把连接作为最基本的抽象,TCP的许多特性都与TCP是面向连接的这个基本特性有关。我们需要了解到的细节就是:
- 同一个IP地址可以有多个不同的TCP连接;
- 同一个端口号也可以出现在多个不同的TCP连接中;
socket这个名词在TCP中表示套接字,但是在现代互联网中socket可以表示多种不同的意思;
TCP报文段首部
TCP本身是面向字节流的,但是TCP传送的数据单元却是报文段,报文段分为首部与数据两部分;
首部格式
先看一张图:
首部前20个字节是固定的,后面有4n字节是根据需要而增加的选项;
下面介绍首部各字段的意义:
- 源端口和目的端口: 各占2个字节,分别写入源端口号和目的端口号;
- 序号: 占4字节,也就是说这4字节全是存储序号信息的,单位为字节;
- 确认号: 期望收到对方下一个报文段的第一个数据字节的序号;
- 数据偏移量: TCP报文段的数据起始处距离TCP报文段的起始处有多远,也就是规定了首部长度,占4位,单位量是4字节;
- 保留字段: 保留为今后使用,目前置为0,占6位;
- 6个控制位: 紧急位
URG、确认位ACK、推送位PSH、复位RST、同步位SYN、终止位FIN;URG为1,代表此报文段有紧急数据,应尽快传送而不按原来的顺序,直接把该报文段插入到最前面;ACK为1,代表确认号字段有效,TCP规定在连接建立后所有传送的报文段都必须把ACK置为1;PSH为1的话是让接收方尽快把报文段交付给应用进程;RST为1,表明TCP连接出现严重问题,必须释放连接,然后重建连接,也用来拒绝一个非法的报文段或拒绝打开一个连接;SYN为1,在TCP连接建立时用来同步序号,配合ACK使用,当SYN为1就表示这是一个连接请求或连接接收报文;FIN为1,释放连接;
- 窗口: 占2字节(16 bit),指的是发送本报文段一方的接收窗口,即告知对方从本报文段首部的确认号开始,接收方允许对方发送的数据量(单位为字节);
- 校验和: 占2字节,校验范围包括首部和数据,计算时同UDP一样,要加上12字节的伪首部;
- 紧急指针: 占2字节,紧急指针仅在URG为1时才有意义,指出本报文段紧急数据(在数据段)的字节数;
- 选项: 长度可变,最长40字节,即按需增加的部分,TCP最初只规定了一种选项,即最大报文段长度MSS,指出每个TCP报文段的数据字段的最大长度;
TCP的可靠传输
TCP发送的报文段是交给IP层传送的,但IP层只能提供最大努力的服务,TCP下面的网络提供的是不可靠的传输。为了尽可能提供到可靠传输的服务,我们需要使用一些可靠传输协议,当出现差错时让发送方重传出现差错的数据。
停止等待协议
发送方每发送完一个分组后就停止发送,等待对方的确认再发送下一个分组,显然效率很一般,但确实尽最大可能努力交付;
出现差错时的处理方案
- 假设A、B分别是发送方接收方,B接收M1出现了差错,B不会返回给A任何信息;
- A只要等待一段时间仍然没有收到确认,则超时重传;
发送方A会暂存已发送的分组的副本,为了超时重传时使用,同时也会对分组进行编号;
确认丢失和确认迟到
此种情况即B返回的M1的确认丢失了,但对于A而言它超时则重传,当B再次接收到这个重传的分组:
- 丢弃这个重复分组,不向上层交付;
- 向A重新发送确认;
通过以上机制,实现在不可靠的传输网络上实现可靠的通信;这种机制称为自动重传请求ARQ;
停止等待协议的这种机制导致信道利用率较低,很多时候都干等着不干活,影响效率;
连续ARQ协议
发送方维护一个发送窗口,该窗口会存在多个分组,多个分组可以很好的提高信道利用率;
连续ARQ协议规定,发送方每收到一个确认,就把发送窗口向前移动一个分组;
接收方一般采用累积确认的方式,在收到几个分组后,对按序到达的最后一个分组发送确认;
- 也因此,对于一个大小为6的一个窗口,如果只有1、2、3收到了确认,4不见了,那么该窗口得回退从4开始重发;
- 这样会影响ARQ的效率;
接下来介绍以字节为单位的滑动窗口:
发送方A
在未收到B的确认的情况下,A会连续把窗口内的数据都发送出去,在未收到确认之前发送的数据都必须暂存以便重传;
发送的数据量取决于接收窗口以及网络拥塞的影响;
接收方B
对于接收方而言,只能对按序收到的数据中的最高序号给出确认;
接收方的窗口也有一个暂存区,主要保存两类数据:
- 按序到达的、但尚未被应用程序接收的数据;
- 未按序到达的数据;
TCP要求接收方要有累积确认的功能,以便减小传输开销;
累积确认: 接收端发送的确认号是发送方的数据序列号+确认收到的字节数;而不是一个序号一个序号确认;
不按序到达的数据的处理方式TCP并无明确规定,考虑到网络利用率,TCP一般先将不按序到达的数据临时存放在接收窗口中,等都到齐了之后再按序向进程提交;
超时重传
TCP采用一种自适应算法,通过收到确认的时间-报文段发出的时间计算往返时间RTT,同时保留一个RTT的加权平均往返时间RTTs(平滑的往返时间),公式:
\[新的RTTs = (1 - \alpha) \times (旧RTTs) + \alpha \times (新的RTT样本)\]RFC 6298推荐的$\alpha$值为1/8;
而超级重传时间RTO应略大于RTTs:
\[RTO = RTTs + 4 \times RTT_D 12\]$RTT_D$是$RTT$的偏差的加权平均值,这里先不叙述怎么计算的;
涉及到重传就需要考虑一个问题: 接收方A收到的确认,是对之前发送的报文段的确认还是对后来重传的报文段的确认?
- 这个问题影响到到对RTTs和RTO的计算;
- 一般的策略是:报文段每重传一次,就把超时重传时间RTO增加1倍;
选择确认SACK
考虑到这么一种情况: 收到的报文段无差错,只是未按序号,是否可以只传送缺少的数据而不重传已经正确到达接收方的数据?
选择确认就是实现这么一个处理方式的策略;
RFC 2018规定:
- 如果要使用选择确认SACK,那么在建立TCP连接时,需要在TCP的首部的选项中增加”允许SACK”的选项;
- TCP报文段的首部要增加SACK选项,以便报告收到的不连续的字节块的边界;
TCP的流量控制
滑动窗口实现流量控制
随着发送方A的不断向前发送,发送方B会给A反馈自身的接收窗口,在这个过程中,需要遵循的一个原则:
- 发送方的发送窗口不能超过接收方给出的接收窗口的数值;
此时需要考虑的一种特殊情况是如果接收窗口返回的窗口值为零的处理方案:
- TCP为每一个连接设有一个持续计时器(persistence timer),接收到零窗口通知就启动该计时器;
- 计时器时间到期则发送一个零窗口探测报文段,对方收到后则给出现当下的窗口值;
主要是为了防止死锁,因A->B以及B->A的确认报文段可能在传输过程中丢失;
TCP的传输效率
TCP对于数据的传输有完全的权限,对于传输有以下三种策略:
- 比如维护一个变量,即最大报文段长度MSS,数据达到MSS字节时就组装成一个TCP报文段发送出去;
- 由发送方的应用进程指明要求发送报文段,即TCP的推送(PUSH)操作;
- 发送方的一个计时器期限到了,就把已有的缓存数据(不超过MSS)装入报文段发送出去
但是最影响传输效率的则是如何控制TCP发送报文段的时机,这部分请看书中229页;
TCP的拥塞控制
拥塞控制的概念
网络中对某一资源的需求超过了该资源所能提供的可用部分,这种情况就叫拥塞(congestion)。
拥塞控制防止过多的数据注入到网络中,使得网络中的路由器或链路不至过载;
拥塞控制与流量控制的区别: 拥塞控制是网络链路上的一个全局性的过程;而流量控制往往是点对点通信量的控制,是端对端的问题;
拥塞控制的方法
拥塞控制的算法主要有四种: 慢开始(slow-start)、拥塞避免(congestion avoidance)、快重传(fast retransmit)、快恢复(fast recovery) ;
同时所有的讨论基于两种原则:
- 数据是单方向传送的,对方只传送确认报文;
- 接收方总是有足够大的缓存空间,因而发送窗口的大小取决于网络的拥塞程度;
以下针对几种算法进行说明:
慢开始和拥塞避免
发送方维护一个拥塞窗口cwnd,当随着网络的拥塞程度不断变化,该值也在不断的发生变化,一般而言判断网络拥塞的依据就是出现了超时;
慢开始的策略是由小到大逐渐增大发送窗口,新的RFC 5681将初始拥塞窗口cwnd设置为不超过2~4个发送最大报文段SMSS的数值,详看232页;
在慢开始阶段,在每收到一个对新的报文段的确认后,将拥塞窗口增加最多一个SMSS的数值:
\[拥塞窗口cwnd每次的增加量 = min(N, SMSS)\]其中$N$是原先未被确认、但现在被刚收到的确认报文段所确认的字节数;
慢开始具体流程:
1、初始
cwnd=1,发送第一个报文段M1,接收方收到后确认M1; 2、发送方收到对M1的确认之后,把
cwnd从1增大到2,发送方接着发送M2和M3两个报文段,接收方收到后确认M2和M3; 3、发送方每收到一个确认就让拥塞窗口加1,因此收到2个确认之后,拥塞窗口变为4,接着发送M4~M7共4个报文段;
从上述流程可以看到每经过一个传输轮次,拥塞窗口
cwnd就加倍;一个传输轮次即往返时间RTT,宏观上来看,第$n$个传输轮次后,拥塞窗口的值:$2^n$;当慢开始进行到一定程度后,需要设置一个慢开始门限ssthresh状态变量,且设定以下规则:
cwnd < ssthresh,使用慢开始算法;cwnd > ssthresh,停止使用慢开始算法而采用拥塞避免算法;cwnd = ssthresh,都可;
拥塞避免算法的思路: 让
cwnd缓慢增大,具体表现为每经过一个RTT,就使得发送方的拥塞窗口cwnd加1,即呈线性规律缓慢增长;拥塞避免无法完全避免拥塞,只是相对而言使得拥塞不易出现;
当网络出现了超时,发送方判断网络出现了拥塞,门限值调整为发生拥塞时
cwnd的1/2,同时重新设置初始的cwnd=1,继续进入慢开始阶段;在传输过程中,个别报文段会在网络中丢失,此时网络其实并未堵塞,如果发送方迟迟收不到确认,那么就会产生超时,误以为发生了堵塞,面对这种情况,TCP采用快重传算法;
快重传算法
内容: 要求接收方不要等待自己发送数据时才稍待确认,而是立即发送确认,即便是收到了失序报文段也要发出对已收到报文段的重复确认;
规定: 发送方只要收到3个重复确认,则立即重传(总共4个确认,后三个是重复确认);
面对上述个别报文段丢失的情形,采用快重传算法可以使得
cwnd不会被置为1,即不执行慢开始,而是开始执行快恢复算法:快恢复算法
内容: 门限值变为1/2的(3-ACK)时的
cwnd,拥塞窗口cwnd的大小等于该门限值,继续执行拥塞避免算法;直到遇到拥塞或者
3-ACK的情形,又继续调整,两种情形的调整策略参考上述内容;
综合流量控制以及拥塞控制来看,我们可以得出:
\[发送方窗口的上限 = Min[rwnd, cwnd]\]- 当
rwnd < cwnd时,接收方的接受能力限制了发送方窗口的最大值; - 当
cwnd < rwnd时,是网络的拥塞程度限制了发生方窗口的最大值;
主动队列管理AQM
该策略是TCP拥塞控制的策略与网络层采取的策略的结合,假设网络系统中的发送方A与接收方B:
- 接收方B中的路由器对某些分组的处理较为耗时,就导致分组中的数据部分需要很长时间才能到达终点
- 这就是上面说的
3-ACK,发送方A一直收到对前一个分组的重复确认,发送端A认为网络发生了堵塞,直接重传;
而这种情况需要与网络层结合分析:
- 网络层的路由器有自己的分组丢弃策略,最简单的情况下,路由器的队列按照先进先出的规则来处理到来的分组;
- 当队列已满时,后续到达的所有分组将被丢弃,这就叫尾部丢弃(tail-drop policy);
路由器的这个策略会导致一连串分组的丢失,这就使得发送方出现超时重传;
在网络中通常有很多TCP连接,而这些TCP连接的报文段复用在网络层的IP数据报,如果发生尾部丢弃,就使得许多TCP同一时间进入了慢开始,这种状态称为全局同步,使得网络的通信量突然下降了很多;
为了解决全局同步问题,提出了主动队列管理AQM,不要等到路由器的队列长达到最大值才不得不丢弃后面到达的分组,应当在队列长度达到某个值得警惕的数值之后就主动丢弃到达的分组;
AQM的曾流行多年的实现方法就是:随机早期检测RED,RED要维持两个参数:队列长度最小门限和最大门限;
- 平均队列长度小于最小门限,新到达的分组放入队列进行排队;
- 平均队列长度大于最大门限,则把新到达的分组丢弃;
- 若在两者之间,则按照某一丢弃概率把新到达的分组丢弃;
TCP的连接管理
分三个阶段:连接建立、数据传输、连接释放;
连接建立阶段
连接建立主要解决三个问题:
- 每一方都能知晓对方的存在;
- 要允许双方协商一些参数(窗口最大值等);
- 能够对运输实体资源进行分配;
定义客户/服务器: 主动发起连接建立的应用进程叫做客户(client),被动等待连接建立的应用进程称为服务器(server);
以下针对连接建立过程中的三次握手进行介绍,假设客户端为A,服务端为B,初始状态我们认为两端的TCP进程都处于CLOSED状态;
连接建立过程中的三次握手:
- A向B发出连接请求报文段,此时首部的同步位
SYN=1,同时选择一个初始序号seq=x,SYN报文段不能携带数据,但会消耗一个序号,此时A进入SYN-SENT状态; - B收到连接请求报文,若同意建立连接,向A发送确认,确认报文段
SYN=1,ACK=1,确认号是ack=x+1,同时也为自己选择一个序号seq=y,B->SYN-RCVD状态; - A收到确认报文段后,向B给出确认,
ACK=1,确认号ack=y+1,而seq=x+1,A进入ESTABLISHED(已建立连接)状态,这时可能会问,SYN字段呢?SYN字段的使命已经完成,已经不会再设置了;- 该ACK报文可携带数据,携带数据则消耗序号,不携带数据则不消耗;
B收到A的ACK之后,也进入ESTABLISHED(已建立连接)状态,这就是三次握手的流程;
在上述的第二次握手过程中,B->A的报文段也可拆分为两个报文段,先发送确认报文段(ACK = 1, ack= x + 1),再发送一个同步报文段(SYN = 1, seq = y),这样就变成了四次握手,效果一致;
连接释放阶段
A的应用进程先向其TCP发出连接释放报文段,停止再发送数据,主动关闭TCP连接,进入四次握手阶段
连接释放阶段的四次握手:
- A向B发出连接释放报文段,该报文段首部终止位
FIN=1,seq=u,A->FIN-WAIT-1(终止等待1)状态,等待B的确认,该报文段消耗一个序号; - B收到连接释放报文段后发出确认,确认位
ACK=1,确认号ack=u+1,seq=v,B->CLOSE-WAIT(关闭等待)状态,TCP通知高层应用进程;A->B的连接就释放了; - 但是B->A的连接还在,此时的TCP连接处于半关闭(half-close)状态,A收到B的确认后,
A->FIN-WAIT-2; - 若B已经没有需要向A发送的数据,其应用进程通知TCP释放连接,此时B向A发出的释放报文段中
FIN=1,ACK=1,ack=u+1,seq=w,B->LAST_ACK; - A收到B的释放连接报文段后,向B再发送确认报文段,
ACK=1,seq=u+1,ack = w + 1,A->TIME-WAIT状态;
经过上述四个握手阶段,TIME-WAIT需要等待时间等待计时器设置的时间2MSL(Maximum Segment Lifetime)后,A才会彻底进入CLOSED状态;
