本文原文链接通过两个图来梳理TCP-IP
协议相关知识。TCP
通信过程包括三个步骤:建立TCP
连接通道,传输数据,断开TCP
连接通道。如图1所示,给出了TCP
通信过程的示意图。
图1主要包括三部分:建立连接、传输数据、断开连接。
建立TCP
连接很简单,通过三次握手便可建立连接。
建立好连接后,开始传输数据。TCP
数据传输牵涉到的概念很多:超时重传、快速重传、流量控制、拥塞控制等等。
断开连接的过程也很简单,通过四次握手完成断开连接的过程。
第一次握手:客户端发送syn
包(seq=x
)到服务器,并进入SYN_SEND
状态,等待服务器确认;
第二次握手:服务器收到syn
包,必须确认客户的SYN
(ack=x+1
),同时自己也发送一个SYN
包(seq=y
),即SYN+ACK
包,此时服务器进入SYN_RECV
状态;
第三次握手:客户端收到服务器的SYN+ACK
包,向服务器发送确认包ACK
(ack=y+1
),此包发送完毕,客户端和服务器进入ESTABLISHED
状态,完成三次握手。
握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP
连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP
连接都将被一直保持下去。
超时重传机制用来保证TCP
传输的可靠性。每次发送数据包时,发送的数据报都有seq
号,接收端收到数据后,会回复ack
进行确认,表示某一seq
号数据已经收到。发送方在发送了某个seq
包后,等待一段时间,如果没有收到对应的ack
回复,就会认为报文丢失,会重传这个数据包。
接受数据一方发现有数据包丢掉了。就会发送ack
报文告诉发送端重传丢失的报文。如果发送端连续收到标号相同的ack
包,则会触发客户端的快速重传(不用等到超时计时器结束即可确认数据包已经丢失)。比较超时重传和快速重传,可以发现超时重传是发送端在傻等超时,然后触发重传;而快速重传则是接收端主动告诉发送端数据没收到,然后触发发送端重传。
这里主要说TCP
滑动窗流量控制。TCP
头里有一个字段叫Window
,又叫Advertised-Window
,这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。 滑动窗可以是提高TCP
传输效率的一种机制。
滑动窗用来做流量控制。流量控制只关注发送端和接受端自身的状况,而没有考虑整个网络的通信情况。拥塞控制,则是基于整个网络来考虑的。考虑一下这样的场景:某一时刻网络上的延时突然增加,那么,TCP
对这个事做出的应对只有重传数据,但是,重传会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,于是,这个情况就会进入恶性循环被不断地放大。试想一下,如果一个网络内有成千上万的TCP
连接都这么行事,那么马上就会形成“网络风暴”,TCP
这个协议就会拖垮整个网络。为此,TCP
引入了拥塞控制策略。拥塞策略算法主要包括:慢启动,拥塞避免,拥塞发生,快速恢复。
第一次挥手:主动关闭方发送一个FIN
,用来关闭主动方到被动关闭方的数据传送,也就是主动关闭方告诉被动关闭方:我已经不会再给你发数据了(当然,在fin
包之前发送出去的数据,如果没有收到对应的ack
确认报文,主动关闭方依然会重发这些数据),但此时主动关闭方还可以接受数据。
第二次挥手:被动关闭方收到FIN
包后,发送一个ACK
给对方,确认序号为收到序号+1
(与SYN
相同,一个FIN
占用一个序号)。
第三次挥手:被动关闭方发送一个FIN
,用来关闭被动关闭方到主动关闭方的数据传送,也就是告诉主动关闭方,我的数据也发送完了,不会再给你发数据了。
第四次挥手:主动关闭方收到FIN
后,发送一个ACK
给被动关闭方,确认序号为收到序号+1
,至此,完成四次挥手。
图2给出了TCP
通信过程中的状态转移图,理解此图是我们理解TCP-IP
协议的关键。
状态图详细解读:
CLOSED
:起始点,在超时或者连接关闭时候进入此状态。
LISTEN
:服务端在等待连接过来时候的状态,服务端为此要调用socket
,bind
,listen
函数,就能进入此状态。此称为应用程序被动打开(等待客户端来连接)。
SYN_SENT
:客户端发起连接,发送SYN
给服务器端。如果服务器端不能连接,则直接进入CLOSED
状态。
SYN_RCVD
:跟3对应,服务器端接受客户端的SYN
请求,服务器端由LISTEN
状态进入SYN_RCVD
状态。同时服务器端要回应一个ACK
,同时发送一个SYN
给客户端;另外一种情况,客户端在发起SYN
的同时接收到服务器端得SYN
请求,客户端就会由SYN_SENT
到SYN_RCVD
状态。
ESTABLISHED
:服务器端和客户端在完成3次握手进入状态,说明已经可以开始传输数据了。
以上是建立连接时服务器端和客户端产生的状态转移说明。相对来说比较简单明了,如果你对三次握手比较熟悉,建立连接时的状态转移还是很容易理解。
下面,我们来看看连接关闭时候的状态转移说明,关闭需要进行4次双方的交互,还包括要处理一些善后工作(
TIME_WAIT
状态),注意,这里主动关闭的一方或被动关闭的一方不是指特指服务器端或者客户端,是相对于谁先发起关闭请求来说的:
FIN_WAIT_1
:主动关闭的一方,由状态5进入此状态。具体的动作是发送FIN
给对方。
FIN_WAIT_2
:主动关闭的一方,接收到对方的FIN-ACK
(即fin
包的回应包),进入此状态。
CLOSE_WAIT
:接收到FIN
以后,被动关闭的一方进入此状态。具体动作是接收到FIN
,同时发送ACK
。(之所以叫close_wait
可以理解为被动关闭方此时正在等待上层应用发出关闭连接指令)
LAST_ACK
:被动关闭的一方,发起关闭请求,由状态8进入此状态。具体动作是发送FIN
给对方,同时在接收到ACK
时进入CLOSED
状态。
CLOSING
:两边同时发起关闭请求时,会由FIN_WAIT_1
进入此状态。具体动作是接收到FIN
请求,同时响应一个ACK
。
TIME_WAIT
:最纠结的状态来了。从状态图上可以看出,有3个状态可以转化成它,我们一一来分析:
a. 由FIN_WAIT_2
进入此状态:在双方不同时发起FIN
的情况下,主动关闭的一方在完成自身发起的关闭请求后,接收到被动关闭一方的FIN
后进入的状态。
b. 由CLOSING
状态进入:双方同时发起关闭,都做了发起FIN
的请求,同时接收到了FIN
并做了ACK
的情况下,由CLOSING
状态进入。
c. 由FIN_WAIT_1
状态进入:同时接受到FIN
(对方发起),ACK
(本身发起的FIN
回应),与b
的区别在于本身发起的FIN
回应的ACK
先于对方的FIN
请求到达,而b
是FIN
先到达。这种情况概率最小。
关闭的4次连接最难理解的状态是TIME_WAIT
,存在TIME_WAIT
的2个理由:
TCP
全双工连接的终止。Slow Start
首先,我们来看一下TCP
的慢热启动。慢启动的意思是,刚刚加入网络的连接,一点一点地提速,不要一上来就像那些特权车一样霸道地把路占满。新同学上高速还是要慢一点,不要把已经在高速上的秩序给搞乱了。
慢启动的算法如下(cwnd
全称Congestion Window)
:
1)连接建好的开始先初始化cwnd = 1
,表明可以传一个MSS
大小的数据。
2)每当收到一个ACK
,cwnd++
; 呈线性上升
3)每当过了一个RTT
,cwnd = cwnd*2
; 呈指数让升
4)还有一个ssthresh
(slow start threshold
),是一个上限,当cwnd >= ssthresh
时,就会进入“拥塞避免算法”(后面会说这个算法)
所以,我们可以看到,如果网速很快的话,
ACK
也会返回得快,RTT
也会短,那么,这个慢启动就一点也不慢。
前面说过,还有一个ssthresh
(slow start threshold
),是一个上限,当cwnd >= ssthresh
时,就会进入“拥塞避免算法”。一般来说ssthresh
的值是65535,单位是字节,当cwnd
达到这个值时后,算法如下:
1)收到一个ACK
时,cwnd = cwnd + 1/cwnd
2)当每过一个RTT
时,cwnd = cwnd + 1
这样就可以避免增长过快导致网络拥塞,慢慢的增加调整到网络的最佳值。很明显,是一个线性上升的算法。
前面我们说过,当丢包的时候,会有两种情况:
1)等到RTO
超时,重传数据包。TCP
认为这种情况太糟糕,反应也很强烈。
sshthresh = cwnd /2 cwnd 重置为 1 进入慢启动过程
2)Fast Retransmit
算法,也就是在收到3个duplicate ACK
时就开启重传,而不用等到RTO
超时。
TCP Tahoe
的实现和RTO
超时一样。
TCP Reno的实现是:
cwnd = cwnd /2
sshthresh = cwnd
进入快速恢复算法——Fast Recovery
上面我们可以看到RTO
超时后,sshthresh
会变成cwnd
的一半,这意味着,如果cwnd<=sshthresh
时出现的丢包,那么TCP
的sshthresh
就会减了一半,然后等cwnd
又很快地以指数级增涨爬到这个地方时,就会成慢慢的线性增涨。我们可以看到,TCP
是怎么通过这种强烈地震荡快速而小心得找到网站流量的平衡点的。
这个算法定义在RFC5681
。快速重传和快速恢复算法一般同时使用。快速恢复算法是认为,你还有3个Duplicated Acks
说明网络也不那么糟糕,所以没有必要像RTO
超时那么强烈。 注意,正如前面所说,进入Fast Recovery
之前,cwnd
和sshthresh
已被更新:
cwnd = cwnd /2
sshthresh = cwnd
然后,真正的Fast Recovery
算法如下:
cwnd = sshthresh + 3 * MSS
(3的意思是确认有3个数据包被收到了)
重传Duplicated ACKs
指定的数据包.如果再收到 duplicated Acks
,那么cwnd = cwnd +1
。如果收到了新的Ack
,那么,cwnd = sshthresh
,然后就进入了拥塞避免的算法了。
如果你仔细思考一下上面的这个算法,你就会知道,上面这个算法也有问题,那就是——它依赖于3个重复的Acks
。注意,3个重复的Acks
并不代表只丢了一个数据包,很有可能是丢了好多包。但这个算法只会重传一个,而剩下的那些包只能等到RTO
超时,于是,进入了恶梦模式——超时一个窗口就减半一下,多个超时会超成TCP
的传输速度呈级数下降,而且也不会触发Fast Recovery
算法了。
于是,1995年,TCP New Reno
(参见RFC 6582
)算法提出来,主要就是在没有SACK
的支持下改进Fast Recovery
算法的——
当sender
这边收到了3个Duplicated Acks
,进入Fast Retransimit
模式,开发重传重复Acks
指示的那个包。如果只有这一个包丢了,那么,重传这个包后回来的Ack
会把整个已经被sender
传输出去的数据ack
回来。如果没有的话,说明有多个包丢了。我们叫这个ACK
为Partial ACK
。
一旦Sender
这边发现了Partial ACK
出现,那么,sender
就可以推理出来有多个包被丢了,于是乎继续重传sliding window
里未被ack
的第一个包。直到再也收不到了Partial Ack
,才真正结束Fast Recovery
这个过程。
我们可以看到,这个“Fast Recovery的变更”
是一个非常激进的玩法,他同时延长了Fast Retransmit
和Fast Recovery
的过程。