前面章節太長了,不得不分開。
這裡已原始碼為主,預設讀者已知曉概念或原理,概念或原理可以參考前面章節,有分析。
參考:李柱明部落格:https://www.cnblogs.com/lizhuming/p/17438743.html
lwip的時鐘機制可以翻看前面章節。
lwip的TCP可靠傳傳輸的實現離不開兩個時鐘處理常式:
tcp_fasttmr()
TCP_FAST_INTERVAL
,預設250ms。tcp_slowtmr()
TCP_SLOW_INTERVAL
,預設500ms。原理參考前面大章節。
控制塊中RTT和RTO相關變數:
/* RTT (round trip time) 估算 */
u32_t rttest; /* RTT測量,傳送時的時間戳。精度500ms */
u32_t rtseq; /* 開始計算RTT時對應的seq號 */
/* RTT估計出的平均值和時間差。
注意:sa為演演算法中8倍的均值;sv為4倍的方差。再去分析LWIP實現RTO的演演算法。 */
s16_t sa, sv; /* @see "Congestion Avoidance and Control" by Van Jacobson and Karels */
s16_t rto; /* 重傳超時時間。節拍宏:TCP_SLOW_INTERVAL。初始超時時間宏:LWIP_TCP_RTO_TIME *//* retransmission time-out (in ticks of TCP_SLOW_INTERVAL) */
u8_t nrtx; /* 重發次數 */
在tcp_output_segment()
傳送報文段時,如果需要計算RTT,就記錄傳送當前報文的時間搓:
/* 計算RTT */
if (pcb->rttest == 0) {
pcb->rttest = tcp_ticks; /* 記錄當前時間戳 */
pcb->rtseq = lwip_ntohl(seg->tcphdr->seqno); /* 記錄當前傳送的起始seq號 */
}
在tcp_receive()
收到新的ACK,這個ACK包含了我們用於計算RTT的報文時,即可計算RTT:
(s16_t)(tcp_ticks - pcb->rttest);
tcp_ticks
會在TCP慢時鐘tcp_slowtmr()
中計算(500ms),所以RTT精度也就500ms。 /* RTT測量:如果當前ACK已經把我們附帶RTT測量的報文也ACK了,則可以計算RTT */
if (pcb->rttest && TCP_SEQ_LT(pcb->rtseq, ackno)) {
/* RTT值不應該超過32K,因為這是tcp計時器滴答和往返不應該那麼長… */
m = (s16_t)(tcp_ticks - pcb->rttest); /* 算出RTT */
LWIP_DEBUGF(TCP_RTO_DEBUG, ("tcp_receive: experienced rtt %"U16_F" ticks (%"U16_F" msec).\n",
m, (u16_t)(m * TCP_SLOW_INTERVAL)));
/* RTO演演算法有很多種,LWIP使用的是Jacobson提出的,具體格式如下: */
/* M:某次測量的RTT值。A:RTT平均值。D:RTT估計方差。g:常數1/8。h:常數1/4。 */
/* 說明:pcb->sa是8倍的RTT平均值。pcb->sv是4倍的方差。 */
/* ERR = M-A */
/* A = A+g*ERR */
/* D = D+h*(|ERR|-D) */
/* RTO = A+4*D */
/* 算出平滑RTT */
m = (s16_t)(m - (pcb->sa >> 3)); /* 偏差 = RTT - 均值 */
pcb->sa = (s16_t)(pcb->sa + m); /* 均值 = 原均值 + (1/8)偏差 */
/* 絕對差 = 差值取絕對值 */
if (m < 0) {
m = (s16_t) - m;
}
m = (s16_t)(m - (pcb->sv >> 2));
pcb->sv = (s16_t)(pcb->sv + m); /* 方差 = 原方差 + (1/4)(絕對差 - 原方差) */
pcb->rto = (s16_t)((pcb->sa >> 3) + pcb->sv); /* RTO = 均值 + 4*方差 */
LWIP_DEBUGF(TCP_RTO_DEBUG, ("tcp_receive: RTO %"U16_F" (%"U16_F" milliseconds)\n",
pcb->rto, (u16_t)(pcb->rto * TCP_SLOW_INTERVAL)));
/* 本次RTT測量完畢,關閉本次RTT測量 */
pcb->rttest = 0;
}
上面只是每次RTT計算出來的RTO,適用於沒有傳送超時的情況下。
而當發生傳送超時時,RTO並不是維持RTT計算的結果,而是超時後每次超時都會按照RTO退避指數來放大RTO。
RTO退避指數:
static const u8_t tcp_backoff[13] =
{ 1, 2, 3, 4, 5, 6, 7, 7, 7, 7, 7, 7, 7};
發生超時重傳後的RTO計算:
tcp_slowtmr()
函數處理超時重傳時,RTO會根據本次的重傳次數來選擇RTO退避指數來放大RTO。/* TCP使用者端發起的SYN不納入RTO演演算法範圍 */
if (pcb->state != SYN_SENT) {
/* RTO計算 */
u8_t backoff_idx = LWIP_MIN(pcb->nrtx, sizeof(tcp_backoff) - 1);
int calc_rto = ((pcb->sa >> 3) + pcb->sv) << tcp_backoff[backoff_idx];
pcb->rto = (s16_t)LWIP_MIN(calc_rto, 0x7FFF);
}
在PCB控制塊:
/* 超時重傳計時器值,當該值大於RTO值時,重傳報文 */
s16_t rtime;
存在空中資料時,就會一直開啟這個超時定時器,在慢時鐘tcp_slowtmr()
中計時。
在收到新的ACK時,會復位這個定時器值。
如果在超過RTO值都還沒收到新的ACK,則表示超時,需要重傳。
由於lwip的特點(輕量)每條TCP只有一個重傳定時器,而不是每個報文段都有一個獨立的定時器,所以只要發生超時重傳,就會把當前空中連結串列pcb->unacked
中的所有空中資料全部挪回傳送緩衝區pcb->unsent
,哪怕是剛剛才傳送出去的也要挪回。其原始碼根據參考tcp_rexmit_rto_prepare()
即可。
在tcp_slowtmr()
函數中,會檢查超時重傳,超時值比當前RTO值大就表示超時,需要觸發超時重傳演演算法:
pcb->ssthresh
減半。但是不能低於2個MSS。pcb->cwnd
降到1個MSS。/* 如果開啟了重傳計時器,則計時 */
if ((pcb->rtime >= 0) && (pcb->rtime < 0x7FFF)) {
++pcb->rtime;
}
if (pcb->rtime >= pcb->rto) {
/* 發生超時 */
LWIP_DEBUGF(TCP_RTO_DEBUG, ("tcp_slowtmr: rtime %"S16_F
" pcb->rto %"S16_F"\n",
pcb->rtime, pcb->rto));
/* 如果unacked佇列報文遷移成功
或
PCB還有unsent報文,但是沒有unacked報文(這意味著存在某種原因導致傳送報文段失敗
(如:可追蹤下tcp_output_segment(),開啟RTO,但是傳送失敗)) */
if ((tcp_rexmit_rto_prepare(pcb) == ERR_OK) || ((pcb->unacked == NULL) && (pcb->unsent != NULL))) {
/* TCP使用者端發起的SYN不納入RTO演演算法範圍 */
if (pcb->state != SYN_SENT) {
/* RTO計算 */
u8_t backoff_idx = LWIP_MIN(pcb->nrtx, sizeof(tcp_backoff) - 1);
int calc_rto = ((pcb->sa >> 3) + pcb->sv) << tcp_backoff[backoff_idx];
pcb->rto = (s16_t)LWIP_MIN(calc_rto, 0x7FFF);
}
/* 復位超時計時器 */
pcb->rtime = 0;
/* 發生重傳,觸發擁塞避免演演算法:更新慢啟動上門限值為有效視窗的一半 */
eff_wnd = LWIP_MIN(pcb->cwnd, pcb->snd_wnd);
pcb->ssthresh = eff_wnd >> 1;
/* 慢啟動上門限不能低於2個MSS, */
if (pcb->ssthresh < (tcpwnd_size_t)(pcb->mss << 1)) {
pcb->ssthresh = (tcpwnd_size_t)(pcb->mss << 1);
}
/* 超時引起的擁塞避免演演算法:擁塞視窗需要更新為一個MSS。重新進行慢啟動。 */
pcb->cwnd = pcb->mss;
LWIP_DEBUGF(TCP_CWND_DEBUG, ("tcp_slowtmr: cwnd %"TCPWNDSIZE_F
" ssthresh %"TCPWNDSIZE_F"\n",
pcb->cwnd, pcb->ssthresh));
/* 復位上次成功傳送的位元組數為0(因為unacked都為NULL) */
pcb->bytes_acked = 0;
/* 呼叫能統計重傳次數的API把資料再次傳送出去 */
tcp_rexmit_rto_commit(pcb);
}
}
保活定時器在PCB控制塊中的變數:
u32_t keep_idle
:
TCP_KEEPIDLE_DEFAULT
,預設7200秒,即是兩小時。setsocketopt()
搭配TCP_KEEPIDLE
即可修改該值。u32_t keep_intvl
:
TCP_KEEPINTVL_DEFAULT
,預設75秒。keep_intvl
秒會傳送一個保活探測報文。setsocketopt()
搭配TCP_KEEPINTVL
即可修改該值。u32_t keep_cnt
:
TCP_KEEPCNT_DEFAULT
,預設9次。keep_cnt
次保活探測報文,超過後都未收到對端響應,則斷開當前連線。setsocketopt()
搭配TCP_KEEPCNT
即可修改該值。/* keepalive計時器的上限值 */
u32_t keep_idle;
#if LWIP_TCP_KEEPALIVE
/* keepalive探測間隔 */
u32_t keep_intvl;
/* keepalive探測的上限次數 */
u32_t keep_cnt;
#endif /* LWIP_TCP_KEEPALIVE */
當然,除了上面三個引數外,還有兩個關鍵引數:
/* 儲存這控制塊的TCP節拍起始值。用於當前PCB的時基初始值參考 */
/* 活動計時器,收到合法報文時自動更新。 */
u32_t tmr;
/* Incremented every coarse grained timer shot (typically every 500 ms). */
u32_t tcp_ticks;
tcp_ticks
- pcb->tmr
就是當前連線的持續空閒時間了。
在tcp_slowtmr()
函數中,實現保活機制:
/* Check if KEEPALIVE should be sent */
if (ip_get_option(pcb, SOF_KEEPALIVE) &&
((pcb->state == ESTABLISHED) ||
(pcb->state == CLOSE_WAIT))) {
if ((u32_t)(tcp_ticks - pcb->tmr) >
(pcb->keep_idle + TCP_KEEP_DUR(pcb)) / TCP_SLOW_INTERVAL) {
LWIP_DEBUGF(TCP_DEBUG, ("tcp_slowtmr: KEEPALIVE timeout. Aborting connection to "));
ip_addr_debug_print_val(TCP_DEBUG, pcb->remote_ip);
LWIP_DEBUGF(TCP_DEBUG, ("\n"));
++pcb_remove;
++pcb_reset;
} else if ((u32_t)(tcp_ticks - pcb->tmr) >
(pcb->keep_idle + pcb->keep_cnt_sent * TCP_KEEP_INTVL(pcb))
/ TCP_SLOW_INTERVAL) {
err = tcp_keepalive(pcb);
if (err == ERR_OK) {
pcb->keep_cnt_sent++;
}
}
}
呼叫tcp_keepalive()
函數即可傳送保活探測報文。
保活探測報一般是包含一個位元組的TCP資料,但是該位元組的SEQ已經被對端ACK過了的(程式碼證明如下),所以傳送該SEQ到對端並不影響對端的位元組流,但是對端如果收到會響應一個ACK回來,我們便可判斷對端主機線上,可重新計時保活探測。
tcp_keepalive()
:pcb->snd_nxt - 1
p = tcp_output_alloc_header(pcb, optlen, 0, lwip_htonl(pcb->snd_nxt - 1));
在PCB控制塊中:
u8_t persist_cnt
:
tcp_slowtmr()
中計時,精度500ms。u8_t persist_backoff
:
tcp_persist_backoff[]
陣列的索引,也表示本次視窗探測報文的時間間隔的節拍數。u8_t persist_probe
:
TCP_MAXRTX
,預設12次,超過也沒收到對端響應,則關閉當前連線。 /* 堅持定時器:用於解決遠端接收視窗為0時,定時詢問使用 */
u8_t persist_cnt; /* 堅持定時器節拍計數值 */
u8_t persist_backoff; /* 堅持定時器探查報文時間間隔列表索引及開關 */
u8_t persist_probe; /* 堅持定時器視窗0時發出的探查報文次數 */
堅持定時器時間間隔節拍數陣列:
/* 堅持定時器的阻塞時長列表,傳送視窗探查報文越來越稀疏 */
static const u8_t tcp_persist_backoff[7] = { 3, 6, 12, 24, 48, 96, 120 };
在tcp_slowtmr()
函數中,實現零視窗探測:
if (pcb->persist_backoff > 0) {
LWIP_ASSERT("tcp_slowtimr: persist ticking with in-flight data", pcb->unacked == NULL);
LWIP_ASSERT("tcp_slowtimr: persist ticking with empty send buffer", pcb->unsent != NULL);
if (pcb->persist_probe >= TCP_MAXRTX) {
++pcb_remove; /* max probes reached */
} else {
u8_t backoff_cnt = tcp_persist_backoff[pcb->persist_backoff - 1];
if (pcb->persist_cnt < backoff_cnt) {
pcb->persist_cnt++;
}
if (pcb->persist_cnt >= backoff_cnt) {
int next_slot = 1; /* increment timer to next slot */
/* If snd_wnd is zero, send 1 byte probes */
if (pcb->snd_wnd == 0) {
if (tcp_zero_window_probe(pcb) != ERR_OK) {
/* 傳送視窗探查失敗,即是本次堅持定時器相關報文傳送失敗,不能清空現有計時數值,因為下次進入需要馬上補回視窗探查報文的傳送 */
next_slot = 0; /* try probe again with current slot */
}
/* snd_wnd not fully closed, split unsent head and fill window */
} else {
/* 視窗不夠大,那切割也得傳送 */
if (tcp_split_unsent_seg(pcb, (u16_t)pcb->snd_wnd) == ERR_OK) {
if (tcp_output(pcb) == ERR_OK) {
/* 切割後,傳送成功會關閉堅持定時器清理相關值,這裡標記下後面不用重新整理堅持定時器相關值了 */
next_slot = 0;
}
}
}
if (next_slot) {
/* 堅持定時器本次輪詢已經成功發出相關報文了,進入下次輪詢計時 */
pcb->persist_cnt = 0;
if (pcb->persist_backoff < sizeof(tcp_persist_backoff)) {
pcb->persist_backoff++;
}
}
}
}
}
呼叫tcp_zero_window_probe()
即可傳送零視窗探測報文。
視窗探測報文的是包含一位元組TCP資料的,該位元組就是待傳送的下一個位元組。
tcp_zero_window_probe()
函數原始碼就不貼了,給出大概實現的流程:
整個過程中,就算攜帶了一位元組的資料,也不會將當前封包加入pcb->unacked佇列,也就是本地不會監聽這個位元組的ack確認,因為沒必要,等待視窗放開後,這個位元組也會被正常傳送過去。
在TIME_WAIT狀態下會開啟2MSL計時來清除當前連線的PCB。
當然,也是需要兩個PCB變數來輔助:
/* 儲存這控制塊的TCP節拍起始值。用於當前PCB的時基初始值參考 */
/* 活動計時器,收到合法報文時自動更新。 */
u32_t tmr;
/* Incremented every coarse grained timer shot (typically every 500 ms). */
u32_t tcp_ticks;
tcp_ticks
- pcb->tmr
就是當前連線的持續空閒時間了。
原始碼也是在tcp_slowtmr()
函數中實現:
TCP_MSL
:預設為60秒。 pcb = tcp_tw_pcbs;
while (pcb != NULL) {
LWIP_ASSERT("tcp_slowtmr: TIME-WAIT pcb->state == TIME-WAIT", pcb->state == TIME_WAIT);
pcb_remove = 0;
/* Check if this PCB has stayed long enough in TIME-WAIT */
if ((u32_t)(tcp_ticks - pcb->tmr) > 2 * TCP_MSL / TCP_SLOW_INTERVAL) {
++pcb_remove;
}
/* If the PCB should be removed, do it. */
if (pcb_remove) {
struct tcp_pcb *pcb2;
tcp_pcb_purge(pcb);
/* Remove PCB from tcp_tw_pcbs list. */
if (prev != NULL) {
LWIP_ASSERT("tcp_slowtmr: middle tcp != tcp_tw_pcbs", pcb != tcp_tw_pcbs);
prev->next = pcb->next;
} else {
/* This PCB was the first. */
LWIP_ASSERT("tcp_slowtmr: first pcb == tcp_tw_pcbs", tcp_tw_pcbs == pcb);
tcp_tw_pcbs = pcb->next;
}
pcb2 = pcb;
pcb = pcb->next;
tcp_free(pcb2);
} else {
prev = pcb;
pcb = pcb->next;
}
PCB控制塊中:
tcpwnd_size_t cwnd; /* 擁塞視窗大小 */
tcpwnd_size_t ssthresh; /* 擁塞避免演演算法啟動閾值。也叫慢啟動上門限值。 */
慢啟動時,擁塞視窗cwnd
起始為1MSS,收到多少ACK就擴大多少(但是一般都是以MSS為步伐、單位)(lwip實際實現得看原始碼,下面有),直至達到慢啟動上門限ssthresh
後才進入擁塞避免,每次最大隻追加1MSS。
慢啟動擁塞視窗cwnd
起始為1MSS原始碼在SYN_SENT
狀態下收到SYN和ACK時設定的,具體在tcp_process()
函數中:
/* 計算初始擁塞視窗 */
pcb->cwnd = LWIP_TCP_CALC_INITIAL_CWND(pcb->mss);
此時的擁塞視窗還是PCB初始化時設定的初始值:預設為傳送緩衝區size TCP_SND_BUF
。
/* RFC 5618建議設定ssthresh值儘可能高,比如設定為最大可能的視窗通告值大小(可以理解為最大可能的傳送視窗大小 )。 */
/* 這裡先設定為本地傳送緩衝區大小,即是最大飛行資料量。後面進行視窗縮放和自動調優時自動調整。 */
pcb->ssthresh = TCP_SND_BUF;
慢啟動擁塞視窗變化是在收到新ACK中處理,即是tcp_receive()
函數:包含慢啟動和擁塞避免:
cwnd
,如果在超時重傳狀態下,僅增大1MSS;如果在正常狀態下,會增大2MSS。cwnd
增大1MSS,然後重新累計ACK。 /* 更新擁塞控制欄位:擁塞視窗cwnd 和 慢啟動上門限ssthresh */
if (pcb->state >= ESTABLISHED) { /* 連線處於ESTABLISHED狀態 */
if (pcb->cwnd < pcb->ssthresh) { /* 慢啟動演演算法 */
tcpwnd_size_t increase;
/* 參考:RFC 3465, section 2.2 Slow Start */
/* 如果是超時重傳後的慢啟動,則選1MSS;
如果是正常狀態下的慢啟動,選2MSS */
u8_t num_seg = (pcb->flags & TF_RTO) ? 1 : 2;
/* 擁塞視窗增長:ACK新資料的量 和 nMSS 中的最小值 */
increase = LWIP_MIN(acked, (tcpwnd_size_t)(num_seg * pcb->mss));
TCP_WND_INC(pcb->cwnd, increase);
LWIP_DEBUGF(TCP_CWND_DEBUG, ("tcp_receive: slow start cwnd %"TCPWNDSIZE_F"\n", pcb->cwnd));
} else { /* 擁塞避免演演算法 */
/* 參考:RFC 3465, section 2.1 Congestion Avoidance */
/* 如果累計ACK新資料量不少於一個擁塞視窗,
則累計ACK新資料流減一個擁塞視窗值;擁塞視窗加一個MSS */
TCP_WND_INC(pcb->bytes_acked, acked);
if (pcb->bytes_acked >= pcb->cwnd) {
pcb->bytes_acked = (tcpwnd_size_t)(pcb->bytes_acked - pcb->cwnd);
TCP_WND_INC(pcb->cwnd, pcb->mss);
}
LWIP_DEBUGF(TCP_CWND_DEBUG, ("tcp_receive: congestion avoidance cwnd %"TCPWNDSIZE_F"\n", pcb->cwnd));
}
}
上面pcb->bytes_acked
變數是累計ACK新資料的量。擁塞避免時,用於判斷擁塞視窗cwnd
是否需要+1MSS。
當擁塞視窗增大到慢開始上門限值ssthresh
時,就開始擁塞避免演演算法。每次只增加1MSS。這裡的每次是指每累計收到一個擁塞視窗量的ACK。
其原始碼在tcp_receive()
函數中,慢啟動中有分析。
擁塞傳送包括超時重傳和快重傳,這兩者要區別起來,因為前者的演演算法會嚴重影響效能。
超時重傳參考本章前面小節,有分析,這裡續上分析快重傳。
快重傳在PCB中的變數:
u8_t dupacks; /* 收到最大重複ACK的次數:一般收1-2次認為是重排序引起的。收到3次後,可以確認為失序,需要立即重傳。然後執行擁塞避免演演算法中的快恢復。 */
PCB快重傳標誌位:TF_INFR
當收到對端連續三次ACK同一個SEQ時,我們就能判斷為傳送了網路丟包,這時就不用等待超時,不用執行超時重傳的擁塞演演算法了,而是執行快速重傳的擁塞發生演演算法:
cwnd
設為原來的一半:cwnd /= 2
;ssthresh = cwnd
;(cwnd為減半後的擁塞視窗)cwnd = ssthresh + 3
(3:每收到1個ACK,可以認為對端收到1次TCP包,網路上就少了1個TCP包,一個包最大為1個報文段,所以快恢復的擁塞視窗就追加3個報文段)既然是需要判斷收到三次重複ACK,那麼原始碼肯定就在tcp_receive()
函數中實現:
重複ACK的判斷條件、做法如下:
/* (From Stevens TCP/IP Illustrated Vol II, p970.)
* 通過以下條件可以判斷是否是重複的ACK:
* 1) 沒有ACK新資料;
* 2) 沒有TCP資料,也沒有SYN、FIN標誌;
* 3) 前面更新視窗演演算法中,本地傳送視窗沒有更新;(看具體原始碼)
* 4) 本地還有unacked資料,並且重傳計時器在跑;
* 5) 當前收到的ACK,是本次連線歷史最大的ACK。
*
* 如果上面5個條件都滿足,則是一個重複的ACK:
* a) 重複 < 3次:do nothing
* b) 重複 == 3次: 快重傳
* c) 重複 > 3次: 擁塞視窗CWND+1MSS(擁塞避免演演算法)
*
* 如果只滿足條件1、2、3:重置重複ACK計數器。(並新增到統計中,但是LWIP沒有做這個統計)
*
* 如果只滿足條件1:重置重複ACK計數器。
*
*/
具體原始碼:
/* Clause 1:沒有ACK新資料 */
if (TCP_SEQ_LEQ(ackno, pcb->lastack)) {
/* Clause 2:報文段中沒有資料,也沒有SYN、FIN */
if (tcplen == 0) {
/* Clause 3:本地傳送視窗沒有更新 */
if (pcb->snd_wl2 + pcb->snd_wnd == right_wnd_edge) {
/* Clause 4:本地還有unacked資料,重傳計時器還在跑 */
if (pcb->rtime >= 0) {
/* Clause 5:收到的ACK是本連線歷史最大的ACK */
if (pcb->lastack == ackno) {
if ((u8_t)(pcb->dupacks + 1) > pcb->dupacks) { /* 防溢位 */
++pcb->dupacks; /* 收到重複的ACK */
}
if (pcb->dupacks > 3) {
/* Inflate the congestion window */
/* 擁塞避免:擁塞視窗cwnd+一個MSS */
TCP_WND_INC(pcb->cwnd, pcb->mss);
}
if (pcb->dupacks >= 3) {
/* 快重傳:是檢查unacked和TF_INFR標誌位來確定是否觸發快重傳。 */
tcp_rexmit_fast(pcb);
}
}
}
}
}
}
呼叫的是tcp_rexmit_fast()
來實現快重傳:
/**
* 收到3個及以上重複ACK才會呼叫當前函數實現快重傳演演算法。
*/
void
tcp_rexmit_fast(struct tcp_pcb *pcb)
{
LWIP_ASSERT("tcp_rexmit_fast: invalid pcb", pcb != NULL);
/* 存在未被ACK的資料 && 快重傳標誌位沒有被標記 */
if (pcb->unacked != NULL && !(pcb->flags & TF_INFR)) {
/* 重傳pcb->unacked佇列中第一個報文 */
LWIP_DEBUGF(TCP_FR_DEBUG,
("tcp_receive: dupacks %"U16_F" (%"U32_F
"), fast retransmit %"U32_F"\n",
(u16_t)pcb->dupacks, pcb->lastack,
lwip_ntohl(pcb->unacked->tcphdr->seqno)));
if (tcp_rexmit(pcb) == ERR_OK) {
/* 設定慢啟動上門限pcb->ssthresh = MIN(當前擁塞視窗,傳送視窗) 的一半。
但是不能低於2個MSS */
pcb->ssthresh = LWIP_MIN(pcb->cwnd, pcb->snd_wnd) / 2;
/* The minimum value for ssthresh should be 2 MSS */
if (pcb->ssthresh < (2U * pcb->mss)) {
LWIP_DEBUGF(TCP_FR_DEBUG,
("tcp_receive: The minimum value for ssthresh %"TCPWNDSIZE_F
" should be min 2 mss %"U16_F"...\n",
pcb->ssthresh, (u16_t)(2 * pcb->mss)));
pcb->ssthresh = 2 * pcb->mss;
}
/* 擁塞視窗更新為 = 慢啟動上門限 + 3MSS */
pcb->cwnd = pcb->ssthresh + 3 * pcb->mss;
/* 標記PCB處於快重傳狀態 */
tcp_set_flags(pcb, TF_INFR);
/* 重置超時重傳計時器 */
pcb->rtime = 0;
}
}
}
快速重傳和快速恢復演演算法一般同時使用。
因為快恢復演演算法認為,能收到三個ACK,說明網路還不是很差,沒必要像RTO一樣搞得那麼僵。
快恢復演演算法:
cwnd += 1
。cwnd = ssthresh
。然後進入擁塞避免演演算法。上面是推薦演演算法,下面才是lwip實際演演算法。
原始碼當然還是在tcp_receive()
函數中:
/* 需要退出快重傳狀態 */
if (pcb->flags & TF_INFR) {
tcp_clear_flags(pcb, TF_INFR); /* 退出快重傳狀態 */
pcb->cwnd = pcb->ssthresh; /* 快恢復演演算法:擁塞視窗重置為慢啟動上門限值 */
pcb->bytes_acked = 0; /* 重置被ACK的資料長度的統計 */
}
nagle演演算法: 儘可能組合更多資料合到同一個報文段中。所以,該演演算法是在TCP出口函數中實現的,所以檢視tcp_output()
函數即可:
/* 如果nagle演演算法生效,則延遲傳送。
* 打破nagle演演算法生效的條件(即是nagle生效,也要馬上傳送的條件)之一:
* - 如果之前呼叫tcp_write()時有記憶體錯誤未能成功傳送,為了防止延遲ACK超時,需要立即傳送。
* - 如果FIN已經在佇列中了,則沒必要再延遲傳送了,立即把資料發出,加速閉環。
* 注意:SYN一直都是單獨報文段的。所以要麼不存在SYN,如存在未傳送資料seg->next != NULL; 要麼只存在SYN,即是還沒有傳送資料,如pcb->unacked == NULL;。
* 注意:RST是不會通過tcp_wirte()和tcp_output()傳送的。
*/
if ((tcp_do_output_nagle(pcb) == 0) &&
((pcb->flags & (TF_NAGLEMEMERR | TF_FIN)) == 0)) {
/* nagle演演算法生效 && 上次傳送記憶體正常 && 還沒有FIN */
break;
}
判斷Nagle是否生效實現在tcp_do_output_nagle()
函數中:Nagle失效條件如下(即是可以立即傳送的條件):
TF_NODELAY
標誌。(該標誌表示關閉nagle演演算法)TF_INFR
標誌。(該標誌表示正在快恢復)#define tcp_do_output_nagle(tpcb) ((((tpcb)->unacked == NULL) || \
((tpcb)->flags & (TF_NODELAY | TF_INFR)) || \
(((tpcb)->unsent != NULL) && (((tpcb)->unsent->next != NULL) || \
((tpcb)->unsent->len >= (tpcb)->mss))) || \
((tcp_sndbuf(tpcb) == 0) || (tcp_sndqueuelen(tpcb) >= TCP_SND_QUEUELEN)) \
) ? 1 : 0)
如果Nagle演演算法生效,則會延遲確認,延遲的確認會在tcp_fasttmr()
傳送出去,該函數週期預設為250ms,表示延遲的確認在(0:250]ms內會傳送出去。
tcp_fasttmr()
:
/* 如果存在延遲傳送的ACK,需要傳送這個延遲的ACK */
if (pcb->flags & TF_ACK_DELAY) {
LWIP_DEBUGF(TCP_DEBUG, ("tcp_fasttmr: delayed ACK\n"));
tcp_ack_now(pcb); /* 標記立即傳送ACK */
tcp_output(pcb); /* 傳送資料 */
tcp_clear_flags(pcb, TF_ACK_DELAY | TF_ACK_NOW); /* 清空相關標誌位 */
}
LWIP原始碼中有兩個宏可能會導致讀者混淆,所以我在這裡說明下,希望能有助於你理解:
TF_ACK_NOW
:不管未傳送佇列中是否有無資料,也不管視窗是否滿足,都必須立即響應一個ACK,哪怕是純粹的ACK。
TF_ACK_DELAY
:如果收到資料,需要響應ACK,但是開啟了擁塞控制,Nagle演演算法生效,就需要開啟延遲ACK。lwip 開啟了一個 250ms 的定時器,如果在超時前都還沒滿足傳送,超時時必須響應ACK。是為了讓lwip的tcp_fasttmr()
定時器超時時,檢查當前連線是否存在延遲ACK,如果存在,則響應ACK。
TF_ACK_DELAY
,且一直未滿足傳送,並不是此刻起等待250ms後才傳送ACK,而是下一個250ms定時器到來時就傳送這個延遲的ACK了,可能下一個ms就到了。
作為接收方時的解決:小視窗不通告。
在滑動更新接收視窗size時,小視窗不通告。而更新接收視窗是在應用層從TCP接收緩衝區成功提取資料時更新的,所以檢視tcp_recved()
即可:
/* 更新滑動視窗。支援糊塗視窗避免演演算法。 */
wnd_inflation = tcp_update_rcv_ann_wnd(pcb);
tcp_update_rcv_ann_wnd()
:
LWIP_MIN((TCP_WND / 2), pcb->mss)
才通告。/**
* 視窗滑動。視窗滑動閾值:LWIP_MIN((TCP_WND / 2), pcb->mss)
* 返回視窗滑動偏移值。
*
* 通俗點:如果視窗滑動後能接收大於等於 LWIP_MIN((TCP_WND / 2), pcb->mss) 這麼多資料時,才滑動視窗通告值。
* 如果視窗滑動後,只能接收一點點資料,還不如不滑動呢。
*/
u32_t
tcp_update_rcv_ann_wnd(struct tcp_pcb *pcb)
{
u32_t new_right_edge;
LWIP_ASSERT("tcp_update_rcv_ann_wnd: invalid pcb", pcb != NULL);
/* 新的接收視窗右邊沿 */
new_right_edge = pcb->rcv_nxt + pcb->rcv_wnd;
if (TCP_SEQ_GEQ(new_right_edge, pcb->rcv_ann_right_edge + LWIP_MIN((TCP_WND / 2), pcb->mss))) {
/* 新視窗右邊沿比舊視窗右邊沿多出一個MSS時(或1/2 宏定義接收視窗大小時),更新視窗通告值大小為當前新的接收視窗大小 */
pcb->rcv_ann_wnd = pcb->rcv_wnd;
return new_right_edge - pcb->rcv_ann_right_edge;
} else { /* 新、舊視窗右邊沿還沒拉開足夠距離,不更新通告視窗 */
if (TCP_SEQ_GT(pcb->rcv_nxt, pcb->rcv_ann_right_edge)) {
/* 接收視窗已滿 */
/* 視窗通告值設為0,不允許再傳送資料到本地,等待視窗滑動後再傳送 */
pcb->rcv_ann_wnd = 0;
} else {
/* 視窗未滿,而且滑動的長度不滿足滑動閾值,保持視窗右邊沿,不滑動 */
u32_t new_rcv_ann_wnd = pcb->rcv_ann_right_edge - pcb->rcv_nxt;
#if !LWIP_WND_SCALE
LWIP_ASSERT("new_rcv_ann_wnd <= 0xffff", new_rcv_ann_wnd <= 0xffff);
#endif
pcb->rcv_ann_wnd = (tcpwnd_size_t)new_rcv_ann_wnd;
}
return 0;
}
}
作為傳送方: