逾時與重傳
TCP是面向連接配接的可靠的運輸層。當資料丢失時,TCP需要重傳包。TCP通過設定定時器解決這種問題。
對每個連接配接,TCP有4個不同的定時器:
1)重傳定時器:用于當希望收到另一端的确認,而沒有收到時。
2)堅持定時器:使視窗大小資訊保持不斷流動。
3)保活定時器:可檢測空閑連接配接另一端何時崩潰或重新開機。
4)2MSL定時器:測量TIME_WAIT狀态的時間。
PTCP本身是沒有提供定時器的,而通過方法GetNextClock讓調用者擷取下一個定時器觸發的時機,當定時器觸發下一個逾時時,需要調用方法NotifyClock。
逾時時間設定
TCP設定獲得确認ACK包的逾時時間設定序列可能為1.5S,3S,6S,12S,24S,48S,64S,當逾時持續時間多于9分鐘時,TCP會被複位(RST),即“指數退避”。
那麼這個逾時值是怎麼計算呢?
如果能很好的估計RTT話,如果确認包在一個RTT之内沒有收到回報,那麼可以認為丢包發生。
TCP最初的RTT估算方法為
R = aR+(1-a)M
其中平滑因子a取為90%,M表示這次測量的RTT,即這個包發送到擷取ACK的時間間隔。
這個算法通過平滑因子來避免R的值受新的M的浮動過大的影響。然而這恰恰在RTT浮動比較大的連接配接中無法及時的反應連接配接情況。并且網絡處于飽和狀态時,頻繁重傳會導緻火上燒油。Jacobson對此設計了新的算法:
Err = M - A
A = A+g*Err
D = D + h(|Err| -D)
RTO = A + 4D
增量g為0.125(1/8),Err為上一個得到的值和新的RTT的差。A為上一個測到的增量後的資料,h為0.25。
當RTT變化大時,Err也會變大,導緻D也會變大,導緻RTO快速上升。某一次連接配接的估值和真正的RTT關系估下:

PTCP實作如下:
PTCP設定最大逾時時間為60S。當收到ACK時,計算RTT是通過PTCP頭部的TimeStamp內插補點計算,是以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
下面的代碼實作,有一定的不同,但仔細分析和上面算法是一緻的。
bool PseudoTcp::process(Segment& seg) {
......
// Check if this is a valuable ack
if ((seg.ack > m_snd_una) && (seg.ack <= m_snd_nxt)) {
// Calculate round-trip time
if (seg.tsecr) {
long rtt = talk_base::TimeDiff(now, seg.tsecr);//計算RTT
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);
}
}
}
當重傳後,仍然逾時時,PTCP也采用指數退避算法。
擁塞避免算法
擁塞避免算法通常和慢啟動算法一起使用,主要是限制發送方的流量。慢啟動的目的是,不要過快的發送資料導緻中間的路由器填滿緩沖,而擁塞避免算法是當發現到網絡被擁塞時限制發送方處理丢失分組的一種方法。
擁塞避免算法和慢啟動算法同時在一個連接配接上維護兩個變量cwnd和ssthresh。
1)對一個給定連接配接cwnd初始化為1。
2)當擁塞發生時(逾時或者受到重複的第三個ack)時ssthreth取目前視窗的一半,如果逾時引起的擁塞,則cwnd取為1。
3)當新的資料包受到确認時,如果cwnd<ssthreth則進行慢啟動算法,否則cwnd在每個确認增加1/cwnd。
快速重傳與快速恢複算法
為什麼上面判斷擁塞時,獲得三個以上重複的ACK時,認為産生擁塞了呢?
因為,當接收方收到失序的封包段時,立即發送需要收到的下一個封包段,然而發送方發送兩個以上封包時,因封包的路由不一樣,會産生短暫的失序,為了避免是以而産生的重傳,把擁塞判斷設定為3個以上。
當收到三個以上重複封包段時,發送方認為此包被丢失,于是立即重傳丢失封包段,不會等到逾時定時器溢出。這就是快速重傳算法。
當發送方重傳後,會持續發送後面沒有發送的資料,而不啟動慢啟動,等待ACK,是因為發送方收到了連續的3個以上ACK說明,接收方收到了3個以上資料封包,并緩存起來了。這就是快速恢複算法,實作如下:
1)當收到3個重複ACK時ssthreth設定為目前視窗的一半,并cwnd設定為ssthresh+3。
2)每次收到另一個重複的ACK時,cwnd增加一個封包段并重傳。
3)當下一個ACK到達時cwdn設定為ssthreth,即采用擁塞避免,速率減半。
對于重傳PTCP有一點不同,就是上述第一步,當收到重複3個ACK時,ssthresh設定為還未确認的位元組數的一半。
if ((seg.ack > m_snd_una) && (seg.ack <= m_snd_nxt)) {//當收到合法的ACK時
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); // cwnd設定為ssthreth
m_dup_acks = 0;//重複ACK計數器清零
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;
// Slow start, congestion avoidance
if (m_cwnd < m_ssthresh) {//慢啟動
m_cwnd += m_mss;
m_cwnd += talk_base::_max<uint32>(1, m_mss * m_mss / m_cwnd);//擁塞避免,增加1/cwnd
}
else if (seg.ack == m_snd_una) {
// !?! Note, tcp says don't do this... but otherwise how does a closed window become open?
m_snd_wnd = static_cast<uint32>(seg.wnd) << m_swnd_scale;
// Check duplicate acks
if (seg.len > 0) {
// it's a dup ack, but with a data payload, so don't modify m_dup_acks
} else if (m_snd_una != m_snd_nxt) {
m_dup_acks += 1;
if (m_dup_acks == 3) { //當收到3個重複的ACK時進行快速重傳
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);//ssthresh設定為2個MSS和cwnd的最小值
m_cwnd = m_ssthresh + 3 * m_mss;//cwnd設定為ssthresh加3
} else if (m_dup_acks > 3) {
m_cwnd += m_mss;//當收到發送重傳後的重複的ACK時,隻增加一個MSS,即快速恢複算法
m_dup_acks = 0;
重新分組
當TCP逾時重傳時,可以允許以更大的且不大于MSS的封包發送,即合并後續的資料一起發送,PTCP也是如此處理的。