本文转载于https://www.codedump.info
写的非常详细,关于TCP面试点几乎都有覆盖,可以参考下,本文是直接粘贴的主要是自己看,还有就是推荐书籍《TCP/IP详解》一共三卷,其中卷二、卷三更多偏重于编程细节
网络编程
以上图说明建立TCP连接的过程,其中左边的A为客户端,右边的B为服务器:
以上就是TCP建立连接的三次握手过程,以上流程还需要补充的是:
listen系统调用中,会传入一个backlog参数,man文档对其的解释是:
1 2 3 4 5 6 7 8 9 |
The behavior of the backlog argument on TCP sockets changed with Linux 2.2. Now it specifies the queue length for completely established sockets waiting to be accepted, instead of the number of incomplete connection requests. The maximum length of the queue for incomplete sockets can be set using /proc/sys/net/ipv4/tcp_max_syn_backlog. When syncookies are enabled there is no logical maximum length and this setting is ignored. See tcp(7) for more informa- tion. If the backlog argument is greater than the value in /proc/sys/net/core/somaxconn, then it is silently truncated to that value; the default value in this file is 128. In kernels before 2.4.25, this limit was a hard coded value, SOMAXCONN, with the value 128. |
以上文字简单的翻译:该参数在Linux 2.2内核版本前后有不同的表现。在2.2版本以后表示的是已经建立起连接等待被接受的队列大小,而在之前则是未完成连接队列长度。未完成连接队列的最大长度由系统参数/proc/sys/net/ipv4/tcp_max_syn_backlog指定。而backlog不得大于系统参数/proc/sys/net/core/somaxconn,该参数的默认值为128。
上面提到的“未完成连接”,其实就是处于SYN-RCVD状态的连接,即只收到了客户端的SYN报文的连接。
Linux服务器内部,会维护一个半连接队列,即处于SYN-RCVD状态的连接都维护在这个队列中。同时还会维护一个完全连接队列,即处于ESTABLISH状态的队列。当连接从SYN-RCVD切换到ESTABLISH状态时,连接将从半连接队列转移到全连接队列中。
半连接队列无法由用户指定,而是由系统参数/proc/sys/net/ipv4/tcp_max_syn_backlog控制。 全连接队列大小取listen系统调用的backlog和系统参数/proc/sys/net/core/somaxconn中的最小值。
当客户端建立连接时,server端给客户端的SYN-ACK回报,由于种种原因客户端一直没有收到,此时连接一直处于半连接状态,既不能算连接成功了也不算连接失败,此时server端需要给客户端重传SYN-ACK报文。但是也有一种可能,即客户端恶意的发送大量SYN报文给服务器,让服务器长期有大量处于半连接状态的连接,耗尽服务器资源。Linux有几个参数用于控制重传SYN-ACK报文重传行为的:
1 2 |
net.ipv4.tcp_synack_retries #内核放弃连接之前发送SYN+ACK包的数量 net.ipv4.tcp_syn_retries #内核放弃建立连接之前发送SYN包的数量 |
以上图来说明TCP连接释放的过程:
以上就是TCP释放连接的四次挥手过程,以上流程还需要补充:
TCP连接是全双工的,即一端接收到FIN报时,对端虽然不再能发送数据,但是可以接收数据,所以需要两边都关闭连接才算完全关闭了这条TCP连接。
主动关闭的一方收到对端发出的FIN报之后,就从FIN-WAIT-2状态切换到TIME-WAIT状态了,再等待2MSL时间才再切换到CLOSED状态。这么做的原因在于:
TIME-WAIT状态如果过多,会占用系统资源。Linux下有几个参数可以调整TIME-WAIT状态时间:
然而,从TCP状态转换图可以看出,主动进行关闭的链接才会进入TIME-WAIT状态,所以最好的办法:尽量不要让服务器主动关闭链接,除非一些异常情况,如客户端协议错误、客户端超时等等。
下图给出了TCP协议状态机与系统调用之间的对应关系。
TCP重转涉及到以下几个问题:
以下来分别解释TCP重传机制中的几个问题。
报文段的样本RTT(sampleRTT)就是从某报文段被发出(即交给IP)到对该报文段的确认(ACK)被收到之间的时间。
大多数TCP的实现仅在某一个时刻做一次SampleRTT的测量,而不是为每个发送的报文段测量一个SampleRTT。这就是说,在任意时刻,仅为一个已发送的但尚未被确认的报文段估计SampleRTT,从而产生一个接近每个RTT的新SampleRTT值。另外,TCP绝不为已被重传的报文段计算SampleRTT,它仅为传输一次的报文段测量SampleRTT。
由于路由器的拥塞和系统负载的变化,SampleRTT也会随之波动。因此,TCP维持一个SampleRTT均值(称为EstimateRTT),一旦获得一个新的SampleRTT值,根据以下公式更新EstimateRTT:
1 |
EstimateRTT = (1 - α) * EstimateRTT + α * SampleRTT |
即:新的EstimateRTT值由旧的EstimateRTT值与SampleRTT值加权相加而成。,在RFC 6298中,α参考值是0.125(1/8)。
除了估算RTT之外,还需要测量RTT的变化,RFC 6298定义了RTT偏差DevRTT,用于估算SampleRTT偏离EstimateRTT的程度,公式为:
1 |
DevRTT = (1 - β) * DevRTT + β * | SampleRTT - EstimateRTT| |
如果SampleRTT波动值较小,那么DevRTT的值就会比较小。β的推荐值为0.25。
有了EstimateRTT和DevRTT值,就可以计算出重传时间间隔。显然,这个值应该大于等于EstimateRTT,否则将造成不必要的重传,但是超时时间也不能比EstimateRTT大太多,否则当报文段丢失时,TCP不能很快重传该报文段,导致数据传输时延时太大。因此要求将超时间隔设为EstimateRTT加上一定余量。当SampleRTT值波动较大时,这个余量应该大些,当波动较小时这个余量应该小一些。因此,DevRTT在这里就派上用场了,公式如下:
1 |
TimeoutInterval = EstimateRTT + 4 * DevRTT |
RFC 6298推荐的TimeoutInterval值为1秒。当出现超时后,TimeoutInterval值将加倍,以免即将被确认的后续报文段过早出现超时。
先给出一个最简化版本的重传算法,在这里假设发送方不受TCP流量和拥塞控制的限制,来自上层的数据长度小于MSS,且数据传送只在一个方向进行。在这里,发送方只使用超时来恢复报文段的丢失,后面再给出更全面的描述。
TCP发送方有三个与发送和重传相关的主要事件:
综上,该简化算法如下处理以上三个事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
NextSeqNum = InitialSeqNumber SendBase = InitialSeqNumber 循环: 如果是收到了上层应用程序发出的数据: 使用NextSeqNum创建新的TCP报文 如果当前TCP重传定时器没有在运行: 启动TCP重传定时器 将TCP报文交给IP层 NextSeqNum = NextSeqNum + length(data) 如果TCP重传定时器超时: 重传所有还没有被确认(ACK)的报文中seq最小的那个TCP报文 启动TCP重传定时器 如果收到了ACK报文,其中ACK值=y: 如果 y > SendBase: SendBase = y 如果当前还有没有被ACK的TCP报文: 启动TCP重传定时器 |
如上所述,每当TCP超时定时器被触发,意味着有TCP报文在指定时间内没有收到ACK,此时TCP会重传最小序号的没有ACK的报文。每次TCP重传时都会将下一次超时时间设置为先前值的两倍,而不是使用当前计算得到的EstimateRTT值。
超时触发重传存在的问题之一是超时周期可能相对较长。当一个报文段丢失时,这种长超时周期迫使发送方延迟重传丢失的分组,因而增加了端到端的时延。发送方通常可在超时事件发生之前通过注意所谓冗余ACK来检测丢包情况。
冗余ACK(duplicate ACK)就是再次确认某个报文段的ACK,而发送方之前已经收到对该报文段的ACK。
如果TCP发送方收到相同数据的3个冗余ACK,将以这个做为一个提示,说明跟在这个已被确认过3次的报文段之后的报文已经丢失。一旦收到3个冗余ACK,TCP就执行快速重传(fast retransmit),即在该报文段的定时器过期之前重传丢失的报文段。
有了以上的补充,将前面收到ACK事件的处理修改如下:
1 2 3 4 5 6 7 8 9 |
如果收到了ACK报文,其中ACK值=y: 如果 y > SendBase: SendBase = y 如果当前还有没有被ACK的TCP报文: 启动TCP重传定时器 否则: // 意味着收到了duplicate ACK 递增针对y的duplicate ACK计数 如果该计数 == 3: 重传seq在y之后的报文 |
TCP为它的应用程序提供了流量控制服务(flow control service),以消除发送方使接收方数据溢出的可能性。
流量控制因此是一种速度匹配服务,即发送方的发送速率与接收方应用程序的读取速率相匹配。
另一种控制发送方速度的方式是拥塞控制(congestion control),但是这两者是不同的:
下一节分析拥塞控制,这一节分析流量控制。
TCP让发送方维持一个接收窗口(receive window)的变量来提供流量控制。接收窗口用于给发送方一个提示,该接收方还有多少可用的缓存空间。因为TCP是全双工通信,因此在连接两端都各自维护一个接收窗口,通过TCP头的window字段来通知对方本方的接收窗口大小。
client和server两端都有自己的协议栈buffer,传输时不可能一直无限量的传输数据下去。此时如何让对端知道自己最多能接收多少数据呢?通过TCP协议头中的window字段来通知对端自己当前的接收窗口大小。
从以上可以看出,TCP协议通过TCP头部的window字段来解决流量控制问题。
如上图中,分成了四个部分,其中黑色框住的部分是滑动窗口:
当收到对端确认一部分数据的ACK之后,滑动窗口将向右边移动,如下图中,收到36的ACK,并且发出了46-51字节的数据之后,滑动窗口变化成了:
以下图为例来解释滑动窗口变化的流程:
TCP所采用的方法是让每一个发送方根据所感知的网络拥塞程度来限制其能向连接发送流量的速率。这种方法提出了三个问题:
首先分析TCP发送方如何限制向起连接发送流量的。在发送方的拥塞控制机制中再维护一个变量,即cwnd(congestion window,拥塞窗口),通过它对一个TCP发送方能向网络发送的流量速率进行限制。即:一个发送方中未被确认的数据量不会超过cwnd与rwnd中的最小值:
1 |
LastByteSent - LastBtyeAcked <= min(cwnd, rwnd) |
接下来讨论如何感知出现了拥塞的。我们将一个TCP发送方的“丢包事件”定义为:要么出现超时,要么收到来自接收方的3次冗余ACK(duplicate ACK)。
拥塞控制有以下几个常用的手段:慢启动、拥塞避免、快速恢复。其中慢启动和拥塞避免是TCP的强制部分,两者的差异在于对收到ACK做出反应时增加cwnd的方式,而快速恢复则是推荐部分,对TCP发送方并非是必需的。
慢启动算法的思想是:刚建立的连接,根据对端的应答情况慢慢提速,不要一下子发送大量的数据。
慢启动算法维护一个cwnd(Congestion Window)变量,以及一个慢启动阈值变量ssthresh(slow start threshold),算法的逻辑是:
可以看到,慢启动算法通过对对端应答报文的RTT时间探测,来修改cwnd值。而这个修改,在不超过ssthresh的情况下,是指数增长的。
何时结束这种指数增长呢?有如下三种情况:
当cwnd>=ssthresh,进入拥塞避免阶段,此时cwnd的增长不再像之前那样是指数增长,而是线性增长。
TCP拥塞控制认为网络丢包是由于网络拥塞造成的,有如下两种判定丢包的方式:
超时重传的原理,在上面也简单提到过:在发送一个TCP报文之后,会启动一个计时器,该计时器的超时时间是根据之前预估的几个往返时间RTT相关的参数计算得到的,如果再这个计时器超时之前都没有收到对端的应答,那么就需要重传这个报文。
而如果发送端收到三个以上的重复ACK时,就认为数据已经丢失需要重传,此时会立即重传数据而不是等待前面的超时重传定时器超时,所以被称为“快速重传”。
最早的TCP Tohoe算法是这么处理拥塞状态的,当出现丢包时:
但是由于这个算法过于激进,每次一出现丢包cwnd就变成1,因此后来的TCP Reno算法进行了优化,其优化点在于,在收到三个重复确认ACK时,TCP开启快速重传Fast Retransmit算法:
以下图来解释上面三种状态的处理:
上图中,横轴为传输轮次,纵轴为cwnd大小,按照时间顺序,其过程如下:
这里提到了TCP Reno算法在收到三个重复ACK时,cwnd变成原来的一半并且使用快速恢复算法来处理拥塞,下面就接着分析快速恢复算法。
再次说明:该算法只有TCP Reno版本才用,已经被废弃的TCP Tohoe算法并没有这部分。
在进入快速恢复以前,TCP Reno已经做了如下的事情:
快速恢复算法的逻辑如下:
如上图中:发送端的第五个包丢失,导致发送端收到三个重复的针对第五个包的ACK。此时将ssthresh值设置为当时cwnd的一半,即6/2=3,而cwnd设置为3+3=6。然后重传第五个包。当收到最新的ACK时,即ACK 11,此时将cnwd设置为当前的ssthresh,即3,然后退出快速恢复而进入拥塞避免状态。
有了前面的解释,理解TCP拥塞控制算法的FSM就容易了:
下面对以上FSM进行简单的总结,每个状态转换箭头都做了数字标记,以数字标记为序来分别做解释: