超時(shí)與重傳
TCP是面向連接的可靠的運(yùn)輸層。當(dāng)數(shù)據(jù)丟失時(shí),TCP需要重傳包。TCP通過設(shè)置定時(shí)器解決這種問題。
對(duì)每個(gè)連接,TCP有4個(gè)不同的定時(shí)器:
1)重傳定時(shí)器:用于當(dāng)希望收到另一端的確認(rèn),而沒有收到時(shí)。
2)堅(jiān)持定時(shí)器:使窗口大小信息保持不斷流動(dòng)。
3)保活定時(shí)器:可檢測(cè)空閑連接另一端何時(shí)崩潰或重啟。
4)2MSL定時(shí)器:測(cè)量TIME_WAIT狀態(tài)的時(shí)間。
PTCP本身是沒有提供定時(shí)器的,而通過方法GetNextClock讓調(diào)用者獲取下一個(gè)定時(shí)器觸發(fā)的時(shí)機(jī),當(dāng)定時(shí)器觸發(fā)下一個(gè)超時(shí)時(shí),需要調(diào)用方法NotifyClock。
超時(shí)時(shí)間設(shè)置
TCP設(shè)置獲得確認(rèn)ACK包的超時(shí)時(shí)間設(shè)置序列可能為1.5S,3S,6S,12S,24S,48S,64S,當(dāng)超時(shí)持續(xù)時(shí)間多于9分鐘時(shí),TCP會(huì)被復(fù)位(RST),即“指數(shù)退避”。
那么這個(gè)超時(shí)值是怎么計(jì)算呢?
如果能很好的估計(jì)RTT話,如果確認(rèn)包在一個(gè)RTT之內(nèi)沒有收到回報(bào),那么可以認(rèn)為丟包發(fā)生。
TCP最初的RTT估算方法為
R = aR+(1-a)M
其中平滑因子a取為90%,M表示這次測(cè)量的RTT,即這個(gè)包發(fā)送到獲取ACK的時(shí)間間隔。
這個(gè)算法通過平滑因子來避免R的值受新的M的浮動(dòng)過大的影響。然而這恰恰在RTT浮動(dòng)比較大的連接中無法及時(shí)的反應(yīng)連接情況。并且網(wǎng)絡(luò)處于飽和狀態(tài)時(shí),頻繁重傳會(huì)導(dǎo)致火上燒油。Jacobson對(duì)此設(shè)計(jì)了新的算法:
Err = M - A
A = A+g*Err
D = D + h(|Err| -D)
RTO = A + 4D
增量g為0.125(1/8),Err為上一個(gè)得到的值和新的RTT的差。A為上一個(gè)測(cè)到的增量后的數(shù)據(jù),h為0.25。
當(dāng)RTT變化大時(shí),Err也會(huì)變大,導(dǎo)致D也會(huì)變大,導(dǎo)致RTO快速上升。某一次連接的估值和真正的RTT關(guān)系估下:

PTCP實(shí)現(xiàn)如下:
PTCP設(shè)置最大超時(shí)時(shí)間為60S。當(dāng)收到ACK時(shí),計(jì)算RTT是通過PTCP頭部的TimeStamp差值計(jì)算,所以Karn算法在此不管用。RTO的算法和上面所述一致:
1)Err = rtt - m_rx_srtt
2)D=D+0.25*(abs(Err-D))
3)m_rx_srtt = m_rx_srtt + err/8
4)RTO = m_rx_srtt+D
下面的代碼實(shí)現(xiàn),有一定的不同,但仔細(xì)分析和上面算法是一致的。
- bool PseudoTcp::process(Segment& seg) {
- ......
-
- if ((seg.ack > m_snd_una) && (seg.ack <= m_snd_nxt)) {
-
- if (seg.tsecr) {
- long rtt = talk_base::TimeDiff(now, seg.tsecr);
- if (rtt >= 0) {
- if (m_rx_srtt == 0) {
- m_rx_srtt = rtt;
- m_rx_rttvar = rtt / 2;
- } else {
- m_rx_rttvar = (3 * m_rx_rttvar + abs(long(rtt - m_rx_srtt))) / 4;
- m_rx_srtt = (7 * m_rx_srtt + rtt) / 8;
- }
- m_rx_rto = bound(MIN_RTO, m_rx_srtt +
- talk_base::_max<uint32>(1, 4 * m_rx_rttvar), MAX_RTO);
- } else {
- ASSERT(false);
- }
- }
- ......
- }
當(dāng)重傳后,仍然超時(shí)時(shí),PTCP也采用指數(shù)退避算法。
擁塞避免算法
擁塞避免算法通常和慢啟動(dòng)算法一起使用,主要是限制發(fā)送方的流量。慢啟動(dòng)的目的是,不要過快的發(fā)送數(shù)據(jù)導(dǎo)致中間的路由器填滿緩沖,而擁塞避免算法是當(dāng)發(fā)現(xiàn)到網(wǎng)絡(luò)被擁塞時(shí)限制發(fā)送方處理丟失分組的一種方法。
擁塞避免算法和慢啟動(dòng)算法同時(shí)在一個(gè)連接上維護(hù)兩個(gè)變量cwnd和ssthresh。
1)對(duì)一個(gè)給定連接cwnd初始化為1。
2)當(dāng)擁塞發(fā)生時(shí)(超時(shí)或者受到重復(fù)的第三個(gè)ack)時(shí)ssthreth取當(dāng)前窗口的一半,如果超時(shí)引起的擁塞,則cwnd取為1。
3)當(dāng)新的數(shù)據(jù)包受到確認(rèn)時(shí),如果cwnd<ssthreth則進(jìn)行慢啟動(dòng)算法,否則cwnd在每個(gè)確認(rèn)增加1/cwnd。
快速重傳與快速恢復(fù)算法
為什么上面判斷擁塞時(shí),獲得三個(gè)以上重復(fù)的ACK時(shí),認(rèn)為產(chǎn)生擁塞了呢?
因?yàn)椋?dāng)接收方收到失序的報(bào)文段時(shí),立即發(fā)送需要收到的下一個(gè)報(bào)文段,然而發(fā)送方發(fā)送兩個(gè)以上報(bào)文時(shí),因報(bào)文的路由不一樣,會(huì)產(chǎn)生短暫的失序,為了避免因此而產(chǎn)生的重傳,把擁塞判斷設(shè)置為3個(gè)以上。
當(dāng)收到三個(gè)以上重復(fù)報(bào)文段時(shí),發(fā)送方認(rèn)為此包被丟失,于是立即重傳丟失報(bào)文段,不會(huì)等到超時(shí)定時(shí)器溢出。這就是快速重傳算法。
當(dāng)發(fā)送方重傳后,會(huì)持續(xù)發(fā)送后面沒有發(fā)送的數(shù)據(jù),而不啟動(dòng)慢啟動(dòng),等待ACK,是因?yàn)榘l(fā)送方收到了連續(xù)的3個(gè)以上ACK說明,接收方收到了3個(gè)以上數(shù)據(jù)報(bào)文,并緩存起來了。這就是快速恢復(fù)算法,實(shí)現(xiàn)如下:
1)當(dāng)收到3個(gè)重復(fù)ACK時(shí)ssthreth設(shè)置為當(dāng)前窗口的一半,并cwnd設(shè)置為ssthresh+3。
2)每次收到另一個(gè)重復(fù)的ACK時(shí),cwnd增加一個(gè)報(bào)文段并重傳。
3)當(dāng)下一個(gè)ACK到達(dá)時(shí)cwdn設(shè)置為ssthreth,即采用擁塞避免,速率減半。
對(duì)于重傳PTCP有一點(diǎn)不同,就是上述第一步,當(dāng)收到重復(fù)3個(gè)ACK時(shí),ssthresh設(shè)置為還未確認(rèn)的字節(jié)數(shù)的一半。
- bool PseudoTcp::process(Segment& seg) {
- ......
- if ((seg.ack > m_snd_una) && (seg.ack <= m_snd_nxt)) {
- ......
- if (m_dup_acks >= 3) {
- if (m_snd_una >= m_recover) {
- uint32 nInFlight = m_snd_nxt - m_snd_una;
- m_cwnd = talk_base::_min(m_ssthresh, nInFlight + m_mss);
- m_dup_acks = 0;
- } else {
- if (!transmit(m_slist.begin(), now)) {
- closedown(ECONNABORTED);
- return false;
- }
- m_cwnd += m_mss - talk_base::_min(nAcked, m_cwnd);
- }
- } else {
- m_dup_acks = 0;
-
- if (m_cwnd < m_ssthresh) {
- m_cwnd += m_mss;
- } else {
- m_cwnd += talk_base::_max<uint32>(1, m_mss * m_mss / m_cwnd);
- }
- }
- }
- else if (seg.ack == m_snd_una) {
-
- m_snd_wnd = static_cast<uint32>(seg.wnd) << m_swnd_scale;
-
- if (seg.len > 0) {
-
- } else if (m_snd_una != m_snd_nxt) {
- m_dup_acks += 1;
- if (m_dup_acks == 3) {
- if (!transmit(m_slist.begin(), now)) {
- closedown(ECONNABORTED);
- return false;
- }
- m_recover = m_snd_nxt;
- uint32 nInFlight = m_snd_nxt - m_snd_una;
- m_ssthresh = talk_base::_max(nInFlight / 2, 2 * m_mss);
- m_cwnd = m_ssthresh + 3 * m_mss;
- } else if (m_dup_acks > 3) {
- m_cwnd += m_mss;
- }
- } else {
- m_dup_acks = 0;
- }
- }
- ......
- }
重新分組
當(dāng)TCP超時(shí)重傳時(shí),可以允許以更大的且不大于MSS的報(bào)文發(fā)送,即合并后續(xù)的數(shù)據(jù)一起發(fā)送,PTCP也是如此處理的。