1、tcp通告窗口/接收窗口/发送窗口
接收端有一个接收窗口大小,接收端只能接收这么多数据,接收窗口的数据需要被上层接收后才释放更大接收空间,才可以接收更多数据;接收窗口之前的数据已经被接收,再次接收到接收窗口之前的数据可以认为是重复发送的,不处理,接收窗口之后的数据不能接收,超出接收范围直接丢弃。
接收端通过tcp首部通告窗口字段告诉对端本地可以可以接收多少数据,用于控制发送端的发送窗口大小。
发送端有一个发送窗口,发送窗口大小即为对端通告窗口的大小,只有发送窗口内的tcp报文才可以发送。发送窗口之前的数据为已经发送并且被确认的数据,不需要再次确认,如果收到再次确认可能是网络延迟等造成的,发送窗口之后的数据不能发送,需要等发送窗口数据被确认后,释放更多发送窗口空间才可以继续往发送窗口发送数据。
1.1、tcp接收窗口/通告窗口大小发送
在客户连接服务器时,connect调用tcp_connect发起第一次握手时,设置接收窗口/通告窗口大小(连接过程请参考/arm7star/article/details/116560454);
调用tcp_output发送报文时,调用tcp_output_set_header设置tcp首部,设置报文通告窗口大小,建立连接时就是接收窗口大小。
第一次握手设置接收/通告窗口大小。
iss = tcp_next_iss(); // 初始化发送序号pcb->rcv_nxt = 0;pcb->snd_nxt = iss;pcb->lastack = iss - 1;pcb->snd_lbb = iss - 1;pcb->rcv_wnd = TCP_WND; // 设置接收窗口大小pcb->rcv_ann_wnd = TCP_WND; // 设置通告窗口大小pcb->rcv_ann_right_edge = pcb->rcv_nxt;pcb->snd_wnd = TCP_WND; // 设置默认发送窗口大小(收到对端通告窗口大小后会更新发送窗口)
tcp首部内容如下:
tcp报文首部结构体如下:
struct tcp_hdr {PACK_STRUCT_FIELD(u16_t src); // 16位源端口号PACK_STRUCT_FIELD(u16_t dest); // 16位目的端口号PACK_STRUCT_FIELD(u32_t seqno); // 32位序号(发送端数据的序号)PACK_STRUCT_FIELD(u32_t ackno); // 32位确认序号(对端下一个报文的序号,ackno之前不包括ackno的数据已经被本地接收,对端可以释放ackno之前不包括ackno的发送数据,对于tcp协议,可以发送的报文存放在unacked队列里面,unacked队列里面的报文可能已经通过网卡发送出去了没有得到应答,也可能还没通过网卡发送出去)PACK_STRUCT_FIELD(u16_t _hdrlen_rsvd_flags); // 4位首部长度、6位保留位、6位标志位(URG/ACK/PSH/RST/SYN/FIN),共16位PACK_STRUCT_FIELD(u16_t wnd); // 16位窗口大小,即通告窗口,通告对端本地能接收多少数据PACK_STRUCT_FIELD(u16_t chksum); // 16位校验和PACK_STRUCT_FIELD(u16_t urgp); // 16位紧急指针} PACK_STRUCT_STRUCT;
1.2、tcp发送窗口大小设置
客户发起连接后,tcp_listen_input处理连接的SYN报文,新建一个tcp_pcb用于客户与服务器之间的连接(accept会返回一个新的socket,新的tcp_pcb即用于accept返回的socket),客户的SYN报文带有客户的通告窗口大小,使用客户通告窗口大小作为服务器的发送窗口大小。
第一次握手更新服务器发送窗口。
ip_addr_set(&(npcb->local_ip), &(iphdr->dest));npcb->local_port = pcb->local_port;ip_addr_set(&(npcb->remote_ip), &(iphdr->src));npcb->remote_port = tcphdr->src;npcb->state = SYN_RCVD;npcb->rcv_nxt = seqno + 1; // seqno为客户SYN数据的sequence number,SYN报文虽然没有数据,但是占用一个sequence number,rcv_nxt为期望收到的下一个sequence numbernpcb->rcv_ann_right_edge = npcb->rcv_nxt;npcb->snd_wnd = tcphdr->wnd; // 设置发送窗口大小(发送窗口大小即为对端通告窗口大小)npcb->ssthresh = npcb->snd_wnd; // 慢启动门限设置为发送窗口大小npcb->snd_wl1 = seqno - 1;/* initialise to seqno-1 to force window update */ // 用于强制更新发送窗口npcb->callback_arg = pcb->callback_arg;
1.3、tcp发送窗口更新
客户收到第二次握手SYN|ACK报文,SYN_SENT状态下,tcp_process函数更新客户发送窗口大小为服务器通告窗口大小(客户发起连接请求时并没有服务器通告窗口大小信息,使用默认值作为发送窗口大小,收到服务器第二次握手SYN|ACK报文时,报文里面有服务器的通告窗口大小,即可更新发送窗口的大小),然后发送ACK第三次握手报文。
第二次握手时更新客户发送窗口大小。
case SYN_SENT:LWIP_DEBUGF(TCP_INPUT_DEBUG, ("SYN-SENT: ackno %"U32_F" pcb->snd_nxt %"U32_F" unacked %"U32_F"\n", ackno,pcb->snd_nxt, ntohl(pcb->unacked->tcphdr->seqno)));/* received SYN ACK with expected sequence number? */if ((flags & TCP_ACK) && (flags & TCP_SYN)&& ackno == ntohl(pcb->unacked->tcphdr->seqno) + 1) {pcb->snd_buf++; // 发送缓存大小加1(SYN报文占一个字节,SYN被服务器确认后,增加1个字节发送缓存大小)pcb->rcv_nxt = seqno + 1; // 期望接收的服务器的下一个报文的sequence numberpcb->rcv_ann_right_edge = pcb->rcv_nxt;pcb->lastack = ackno; // 等待被确认的下一个报文的seqnopcb->snd_wnd = tcphdr->wnd; // 用对端通告窗口大小设置为本地发送窗口大小pcb->snd_wl1 = seqno - 1; /* initialise to seqno - 1 to force window update */ // 用于强制更新发送窗口,seqno为SYN|ACK的sequence number,服务器下次发送报文的sequence number不小于seqno,设置snd_wl1小于seqno,下次接收到服务器的报文时,snd_wl1小于服务器报文的sequence number时,接收函数会更新snd_wnd,实际就是握手之后强制更新本地的发送窗口pcb->state = ESTABLISHED;
1.4、tcp服务器再次更新发送窗口
客户收到第二次握手的SYN|ACK报文后,发送第三次握手ACK报文,服务器收到ACK报文后,服务器在SYN_RCVD状态调用tcp_receive接收报文时更新发送窗口。
2、发送窗口更新
发送窗口更新涉及两个变量,snd_wl1、snd_wl2,snd_wl1记录的是之前用于更新发送窗口的报文的seqno,snd_wl2记录的是之前用于更新发送窗口的报文的ackno。
tcp_receive根据当前收到报文的seqno及ackno来确定是否更新发送窗口大小。
如果当前收到的报文的seqno大于等于之前用于更新发送窗口的报文的seqno,那么用该报文的通告窗口大小更新本地发送窗口大小;(seqno用于判断报文发送的先后顺序,seqno越大,发送时间越后)
如果当前收到的报文的seqno等于之前用于更新发送窗口的报文的seqno,那么用ackno大的报文的通过窗口大小更新本地发送窗口大小;(ackno用于判断ACK报文发送的先后顺序,ackno越大,发送时间越后)
如果当前收到的报文的seqno及ackno都相等,那么用通告窗口大的报文的通告窗口大小更新本地发送窗口大小;(ACK可能在网络中阻塞,对重复报文再次发ACK,两个ACK的seqno、ackno相同,通告窗口可能不一样,两个ACK可能都会被接收到,没办法区分哪个后发送)
tcp_receive更新发送窗口代码如下。
if (flags & TCP_ACK) { // ACK报文right_wnd_edge = pcb->snd_wnd + pcb->snd_wl2;/* Update window. */if (TCP_SEQ_LT(pcb->snd_wl1, seqno) || // seqno大于收到的报文的最大seqno,该报文发送时间更后(pcb->snd_wl1 == seqno && TCP_SEQ_LT(pcb->snd_wl2, ackno)) || // seqno等于收到的报文的最大seqno,ackno大于之前收到的报文的最大ackno(pcb->snd_wl2 == ackno && tcphdr->wnd > pcb->snd_wnd)) { // seqno、ackno都等于收到的报文的最大seqno、acknopcb->snd_wnd = tcphdr->wnd; // 更新发送窗口pcb->snd_wl1 = seqno; // snd_wl1记录收到的报文的最大seqnopcb->snd_wl2 = ackno; // snd_wl2记录收到的报文的最大acknoif (pcb->snd_wnd > 0 && pcb->persist_backoff > 0) { // 如果发送窗口大于0并且坚持定时器已经启动,那么关闭坚持定时器(坚持定时器用于发送报文去探测对端的接收窗口,对端收到报文会发送一个ACK,ACK带有对端通告窗口大小)pcb->persist_backoff = 0;}
3、接收窗口/接收通告窗口
接收窗口涉及rcv_nxt、rcv_wnd、rcv_ann_wnd、rcv_ann_right_edge几个变量,rcv_nxt为下一个等待接收报文的seqno,rcv_wnd为接收窗口大小(未被上层接收的、缓存在接收窗口里面的数据大小),rcv_ann_wnd为接收通告窗口的大小(receiver window to announce,告诉对端本地接收窗口还可以接收多少数据),rcv_ann_right_edge为接收通告窗口的右边缘(通常情况下指向接收窗口的右边缘,接收通告窗口更新后,还没发送给对端的情况下,rcv_ann_right_edge指向的是上一次发送通告窗口时的接收窗口右边缘,通告窗口发送后,rcv_ann_right_edge指向当前接收窗口的右边缘)。
3.1、接收窗口的更新
tcp接收到报文后,接收窗口减小。如下图所示,"已被接收的数据“表示已被上层应用程序接收的数据,"等待接收"为接收端期待接收的下一个数据,"out of sequence segments"为接收序列之外的数据(接收端按顺序接收1、2、3、4、5序号的数据,如果在没有收到1的时候收到其他报文,那么其他报文都是out of sequence segments,当”等待接收“的数据收到后,”等待接收“的数据与out of sequence segments组成一个连续的可接受的报文,发送数据给应用层,在被应用程序接收前,这些报文都属于接收窗口里面的报文)
当收到”等待接收“的报文时,这一部分数据是期望接收的,可接受的,那么接收窗口需要减去这段报文长度才是接收窗口当前的长度。tcp_receive处理过程如下。
收到的报文有数据。
/* If the incoming segment contains data, we must process itfurther. */if (tcplen > 0) {
当前收到报文包含期望接收的序号。(当前报文序号为期望接收的序号,或者当前报文有部分已经接收的数据,部分是期望待接收的数据,那么丢弃已经接收的数据,获取等待接收的数据即可)
/* if (TCP_SEQ_LT(seqno, pcb->rcv_nxt)){if (TCP_SEQ_LT(pcb->rcv_nxt, seqno + tcplen)) {*/if (TCP_SEQ_BETWEEN(pcb->rcv_nxt, seqno + 1, seqno + tcplen - 1)){
释放报文中之前已经接收过了的数据。
off = pcb->rcv_nxt - seqno; // 获取等待接收的数据在报文数据中的偏移p = inseg.p; // p指向报文数据LWIP_ASSERT("inseg.p != NULL", inseg.p);LWIP_ASSERT("insane offset!", (off < 0x7fff));if (inseg.p->len < off) { // inseg第一个pbuf节点长度小于off,已接收的数据占pbuf几个节点,inseg.p里面全是已经接收过的数据,需要完全丢弃该pbuf节点LWIP_ASSERT("pbuf too short!", (((s32_t)inseg.p->tot_len) >= off));new_tot_len = (u16_t)(inseg.p->tot_len - off); // 需要读取的全部数据的长度while (p->len < off) { // p->len < off表明该p里面的数据都是已经被接收了的数据,需要丢弃,循环直到丢弃off数据为止off -= p->len; // 减去p里面包含的已经接收的数据,off为inseg里面还剩余的已经被接收了的数据/* KJM following line changed (with addition of new_tot_len var)to fix bug #9076inseg.p->tot_len -= p->len; */p->tot_len = new_tot_len; // 更新p里面总数据长度p->len = 0; // p->len设置为0(没有释放内存,仅标记有效数据为0,遍历pbuf时直接跳过该节点)p = p->next; // 指向pbuf的下一个节点}if(pbuf_header(p, (s16_t)-off)) { // p里面包含部分需要接收的数据,部分需要丢弃的数据,释放需要丢弃的那部分数据/* Do we need to cope with this failing? Assert for now */LWIP_ASSERT("pbuf_header failed", 0);}} else { // inseg已经接收了的数据都在第一个pbuf节点里面,直接释放第一个节点已经接收的数据即可(长度减少,有效数据偏移往后移,不需要真正释放内存)if(pbuf_header(inseg.p, (s16_t)-off)) { // ”释放“off个已经接收了的数据/* Do we need to cope with this failing? Assert for now */LWIP_ASSERT("pbuf_header failed", 0);}}
更新报文序号数据等(删除部分已经被接收的数据后,报文的序号等需要更新)。
/* KJM following line changed to use p->payload rather than inseg->p->payloadto fix bug #9076 */inseg.dataptr = p->payload; // 报文数据指针inseg.len -= (u16_t)(pcb->rcv_nxt - seqno); // 报文有效数据长度inseg.tcphdr->seqno = seqno = pcb->rcv_nxt; // 报文起始序号
检查报文是否在接收窗口内以及是否是期待接收的报文(之前是删除已接收的数据,out of sequence segments以及更新后的报文都走下面代码处理)。
/* The sequence number must be within the window (above rcv_nxtand below rcv_nxt + rcv_wnd) in order to be furtherprocessed. */if (TCP_SEQ_BETWEEN(seqno, pcb->rcv_nxt, pcb->rcv_nxt + pcb->rcv_wnd - 1)){ // 接收到的报文的序号是否在接收窗口里面if (pcb->rcv_nxt == seqno) { // 接收的报文的seqno是其期待收的下一个序号,可接受的报文,可以提交给上层处理
报文可接受,获取报文长度。
accepted_inseq = 1; // 报文可接受/* The incoming segment is the next in sequence. We check ifwe have to trim the end of the segment and update rcv_nxtand pass the data to the application. */tcplen = TCP_TCPLEN(&inseg); // 获取报文长度
报文长度超过接收窗口大小,释放超出接收窗口的数据。
if (tcplen > pcb->rcv_wnd) {LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_receive: other end overran receive window""seqno %"U32_F" len %"U32_F" right edge %"U32_F"\n",seqno, tcplen, pcb->rcv_nxt + pcb->rcv_wnd));if (TCPH_FLAGS(inseg.tcphdr) & TCP_FIN) {/* Must remove the FIN from the header as we're trimming * that byte of sequence-space from the packet */TCPH_FLAGS_SET(inseg.tcphdr, TCPH_FLAGS(inseg.tcphdr) &~ TCP_FIN);}/* Adjust length of segment to fit in the window. */inseg.len = pcb->rcv_wnd; // 报文长度设置为接收窗口大小,超出的将被丢弃if (TCPH_FLAGS(inseg.tcphdr) & TCP_SYN) {inseg.len -= 1;}pbuf_realloc(inseg.p, inseg.len); // 重新申请报文内存(pbuf_realloc把超出长度的丢弃了,接收窗口内的数据还在)tcplen = TCP_TCPLEN(&inseg); // 重新设置报文长度LWIP_ASSERT("tcp_receive: segment not trimmed correctly to rcv_wnd\n",(seqno + tcplen) == (pcb->rcv_nxt + pcb->rcv_wnd));}
out of sequence segments报文处理(乱序收到的报文被缓存在ooseq里面),检查当前报文是否与out of sequence segments有重叠,只需要判断out of sequence segments的第一个报文即可,释放当前报文重叠部分数据。
if (pcb->ooseq != NULL) { // 有out of sequence segments报文if (TCPH_FLAGS(inseg.tcphdr) & TCP_FIN) { // 当前收到的是FIN报文,FIN之后的out of sequence segments报文全部丢弃LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_receive: received in-order FIN, binning ooseq queue\n"));/* Received in-order FIN means anything that was received* out of order must now have been received in-order, so* bin the ooseq queue */while (pcb->ooseq != NULL) {struct tcp_seg *old_ooseq = pcb->ooseq;pcb->ooseq = pcb->ooseq->next;memp_free(MEMP_TCP_SEG, old_ooseq);}} else if (TCP_SEQ_LEQ(pcb->ooseq->tcphdr->seqno, seqno + tcplen)) { // out of sequence segments与当前报文有重叠,需要删除inseg里面重叠的数据,保留更早收到的out of sequence segments里面的数据if (pcb->ooseq->len > 0) {/* We have to trim the second edge of the incoming segment. */LWIP_ASSERT("tcp_receive: trimmed segment would have zero length\n",TCP_SEQ_GT(pcb->ooseq->tcphdr->seqno, seqno));/* FIN in inseg already handled by dropping whole ooseq queue */inseg.len = (u16_t)(pcb->ooseq->tcphdr->seqno - seqno); // out of sequence segments没有等待接收的部分数据,inseg保留out of sequence segments缺少的那部分数据即可if (TCPH_FLAGS(inseg.tcphdr) & TCP_SYN) {inseg.len -= 1;}pbuf_realloc(inseg.p, inseg.len); // 重新分配内存,释放重叠数据tcplen = TCP_TCPLEN(&inseg);LWIP_ASSERT("tcp_receive: segment not trimmed correctly to ooseq queue\n",(seqno + tcplen) == pcb->ooseq->tcphdr->seqno);} else { // out of sequence segments没有数据/* does the ooseq segment contain only flags that are in inseg also? */if ((TCPH_FLAGS(inseg.tcphdr) & (TCP_FIN|TCP_SYN)) ==(TCPH_FLAGS(pcb->ooseq->tcphdr) & (TCP_FIN|TCP_SYN))) { // 如果当前报文的flags与out of sequence segments的flags相等,那么丢掉out of sequence segments里面的报文,out of sequence segments指向下一个out of sequence segments报文struct tcp_seg *old_ooseq = pcb->ooseq; // ooseq指向下一个out of sequence segments报文pcb->ooseq = pcb->ooseq->next;memp_free(MEMP_TCP_SEG, old_ooseq); // 释放没有数据的报文}}}}
接收窗口更新(已接收发送端的数据,接下来会发送ACK给对端,对端的发送窗口释放部分已经被确认的报文,对方发送窗口实际已经恢复了,但是本地接收窗口里面的数据还没被处理,还在接收窗口,需要减小接收端的接收通告窗口大小(对端发送窗口大小))。
pcb->rcv_nxt = seqno + tcplen; // 接收窗口左边缘右移(接收窗口右边缘没有移动,此处缩小了接收窗口)/* Update the receiver's (our) window. */LWIP_ASSERT("tcp_receive: tcplen > rcv_wnd\n", pcb->rcv_wnd >= tcplen);pcb->rcv_wnd -= tcplen; // 接收窗口边缘移动后更新接收窗口大小,减去已经接收的数据长度tcp_update_rcv_ann_wnd(pcb); // 更新接收通告窗口
接收窗口的恢复是在调用recv后,数据被上层接收后,接收窗口恢复已经被接收的部分数据大小,由tcp_recved函数恢复。该函数同样会更新通告接收窗口。后面处理out of sequence segments代码时,out of sequence segments跟当前报文组成一个大的连续的可接受的报文时,同样会更新接收窗口。
3.2、接收通告窗口更新
在接收端有收到可接受的数据时以及数据被上层接收后,lwip调用tcp_update_rcv_ann_wnd更新通告接收窗口。
u32_t tcp_update_rcv_ann_wnd(struct tcp_pcb *pcb){u32_t new_right_edge = pcb->rcv_nxt + pcb->rcv_wnd; // 新的接收窗口右边缘if (TCP_SEQ_GEQ(new_right_edge, pcb->rcv_ann_right_edge + pcb->mss)) { // 对于上层接收数据,恢复接收窗口大小,当恢复接收窗口大于等于一个mss报文时,更新接收通告窗口大小为当前新的接收窗口大小/* we can advertise more window */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;上层接收报文数据,恢复接收窗口后,接收窗口不足一个mss报文,接收通告窗口设置为0/* Can happen due to other end sending out of advertised window,* but within actual available (but not yet advertised) window */pcb->rcv_ann_wnd = 0; // 通告窗口设置为0,不允许再发送数据到本地,需要等接收通告窗口滑动后,有足够接收通告窗口才行} else { // 接收报文时,接收窗口没有满,以当前接收窗口大小设置接口通告窗口大小;恢复接收窗口时,原来的接收接窗口没有满,用恢复后的接收窗口大小设置接收通告窗口大小(这里并没有判断是否够一个mss报文)/* keep the right edge of window constant */pcb->rcv_ann_wnd = pcb->rcv_ann_right_edge - pcb->rcv_nxt; // 更新接收通告窗口大小为当前接收窗口大小}return 0;}}
发送请求时,pcb->rcv_ann_right_edge = pcb->rcv_nxt + pcb->rcv_ann_wnd,初始化情况下,接收通告窗口的右边缘与接收窗口的右边缘对齐。
tcp接收到报文时,接收窗口左边缘右移,接收窗口大小减小,调用tcp_update_rcv_ann_wnd更新接收通告窗口,pcb->rcv_nxt右移与rcv_wnd减小正好抵消,tcp_update_rcv_ann_wnd计算的new_right_edge与之前的rcv_ann_right_edge相等,执行“TCP_SEQ_GT(pcb->rcv_nxt, pcb->rcv_ann_right_edge)”比较,检查下一个期望接收的数据的序号是否超过接收通告窗口右边缘(默认情况下,接收通告窗口右边缘与接收窗口对齐,这里直接可以理解为下一个期望接收的序号是否超过接收窗口),如果超过,则接收窗口已满,接收通告窗口设置为0,否则接收通告窗口设置为当前接收窗口的大小。
4、发送窗口
发送端的发送窗口大小是以对端通告窗口大小为准的,发送窗口大小即为对端通告窗口大小。
发送窗口涉及snd_nxt、snd_wnd、snd_lbb、unacked、lastack等几个变量,snd_nxt为下一个可发送报文的序号(未发送/待发送的第一个报文的seqno),snd_wnd为发送窗口大小(未发送可以发送的、未被确认的数据的大小),snd_lbb为下一个缓存报文的seqno(待缓存的报文,tcp_enqueue时使用),unacked为正在发送的、没有被确认的报文队列,lastack为被确认的序号(确认已被对端接收的数据)。
4.1、报文发送/加入发送队列
lwip调用tcp_output发送unsent报文并加入unacked发送队列。
发送队列窗口大小获取(发送窗口/拥塞窗口取较小值),获取已发送未被确认的报文队列的队尾,获取未发送的报文队列。
wnd = LWIP_MIN(pcb->snd_wnd, pcb->cwnd); // 取发送窗口和拥塞窗口的较小值,发送报文的总大小不能超过发送窗口大小及拥塞窗口大小;发送窗口用于控制发送报文到对端(对端还没处理完报文,等待对端处理报文),拥塞窗口用于控制报文发送到网络(网络拥塞不能发送太多报文到网络,发送多了也只会造成网络拥塞,不能加快发送报文,也不能增加发送带宽,网络拥塞情况下,报文可能被丢弃)seg = pcb->unsent; // 获取未发送的报文队列/* useg should point to last segment on unacked queue */useg = pcb->unacked; // 获取已经发送未被确认的报文队列if (useg != NULL) {for (; useg->next != NULL; useg = useg->next); // useg指向最后一个已发送未被确认的报文}
TF_ACK_NOW报文发送,如果未发送队列为空或者报文序号超出了窗口,发送一个snd_nxt序号的报文,不经过unacked队列,不保证传达目的地。
if (pcb->flags & TF_ACK_NOW &&(seg == NULL || // 没有未发送报文(不在正在发送的报文里面加ACK标志;lwip unsent队列里面会存在unacked重传的报文以及没有发送的报文,但是unacked队列里面的报文一定是被发送过的)ntohl(seg->tcphdr->seqno) - pcb->lastack + seg->len > wnd)) { // lastack可以理解为发送窗口的左边缘,“ntohl(seg->tcphdr->seqno) - pcb->lastack”即为当前报文之前还有多少正在发送的报文,“seg->tcphdr->seqno) - pcb->lastack + seg->len”即为包括当前报文在内,有多少数据需要发送(正在发送/未发送,当前报文未发送),如果大于窗口,那么需要创建一个ACK报文#if LWIP_TCP_TIMESTAMPSif (pcb->flags & TF_TIMESTAMP)optlen = LWIP_TCP_OPT_LENGTH(TF_SEG_OPTS_TS);#endifp = pbuf_alloc(PBUF_IP, TCP_HLEN + optlen, PBUF_RAM); // 申请一个tcp的ACK报文的内存if (p == NULL) {LWIP_DEBUGF(TCP_OUTPUT_DEBUG, ("tcp_output: (ACK) could not allocate pbuf\n"));return ERR_BUF;}LWIP_DEBUGF(TCP_OUTPUT_DEBUG, ("tcp_output: sending ACK for %"U32_F"\n", pcb->rcv_nxt));/* remove ACK flags from the PCB, as we send an empty ACK now */pcb->flags &= ~(TF_ACK_DELAY | TF_ACK_NOW); // 本次将发送ACK报文,清除TF_ACK_NOW标记,清除TF_ACK_DELAY标记(收到数据时,不马上发送ACK报文,如果有收到更多数据,将多个数据一起ACK,减少发送ACK报文的次数)tcphdr = tcp_output_set_header(pcb, p, optlen, htonl(pcb->snd_nxt)); // tcp首部设置(seqno为pcb->snd_nxt,这里并没有对pcb->snd_nxt加1操作,下一个报文的序号仍为pcb->snd_nxt,不带数据的ACK报文不占用seqno)/* NB. MSS option is only sent on SYNs, so ignore it here */#if LWIP_TCP_TIMESTAMPSpcb->ts_lastacksent = pcb->rcv_nxt;if (pcb->flags & TF_TIMESTAMP)tcp_build_timestamp_option(pcb, (u32_t *)(tcphdr + 1));#endif #if CHECKSUM_GEN_TCPtcphdr->chksum = inet_chksum_pseudo(p, &(pcb->local_ip), &(pcb->remote_ip),IP_PROTO_TCP, p->tot_len);#endif#if LWIP_NETIF_HWADDRHINTip_output_hinted(p, &(pcb->local_ip), &(pcb->remote_ip), pcb->ttl, pcb->tos,IP_PROTO_TCP, &(pcb->addr_hint));#else /* LWIP_NETIF_HWADDRHINT*/ip_output(p, &(pcb->local_ip), &(pcb->remote_ip), pcb->ttl, pcb->tos,IP_PROTO_TCP); // 调用ip_output发送ACK报文,不经过缓存,因此ACK报文没有超时重传机制#endif /* LWIP_NETIF_HWADDRHINT*/pbuf_free(p);return ERR_OK;}
检查未发送报文是否超过窗口大小(发送窗口/拥塞窗口),如果没有超过,则可以发送。
/* data available and window allows it to be sent? */while (seg != NULL && // 未发送报文不为空ntohl(seg->tcphdr->seqno) - pcb->lastack + seg->len <= wnd) { // 待发送报文的数据没有超过窗口大小wnd
不能发送的报文直接返回,不需要立即发送的可延迟发送的报文延迟发送。
if((tcp_do_output_nagle(pcb) == 0) && // 延迟发送、小报文等判断,不需要立即发送的、不能发送的报文等直接返回((pcb->flags & (TF_NAGLEMEMERR | TF_FIN)) == 0)){ // 非TF_NAGLEMEMERR、TF_FIN报文可以暂缓发送break;
从未发送unsent队列移除当前报文,unsent指针移动到下一个未发送报文,非SYN_SENT状态的报文设置ACK标志,清除pcb的TF_ACK_DELAY、TF_ACK_NOW标志。
pcb->unsent = seg->next;if (pcb->state != SYN_SENT) {TCPH_SET_FLAG(seg->tcphdr, TCP_ACK);pcb->flags &= ~(TF_ACK_DELAY | TF_ACK_NOW); // 当前报文已经有ACK,不再需要TF_ACK_DELAY、 TF_ACK_NOW(TF_ACK_DELAY、 TF_ACK_NOW都由当前报文发送)}
发送报文,更新pcb->snd_nxt。
tcp_output_segment(seg, pcb);snd_nxt = ntohl(seg->tcphdr->seqno) + TCP_TCPLEN(seg);if (TCP_SEQ_LT(pcb->snd_nxt, snd_nxt)) {pcb->snd_nxt = snd_nxt; // 如果当前发送的报文的下一个报文的seqno大于pcb->snd_nxt,更新pcb->snd_nxt}
带数据的tcp报文需要加入unacked队列里面等待应答。(需要保证数据传输到对端,如果一定时间没没有应答,需要超时重传,tcp_rexmit_rto将所有没有被确认的数据插入到unsent队列前面,再次发送超时报文)
/* put segment on unacknowledged list if length > 0 */if (TCP_TCPLEN(seg) > 0) { // 报文数据长度大于0seg->next = NULL; // 报文的next设置为NULL(之前指向未发送队列里面的报文)/* unacked list is empty? */if (pcb->unacked == NULL) {pcb->unacked = seg; // unacked队列为空,当前报文即为第一个unacked报文useg = seg; // useg指向unacked队列/* unacked list is not empty? */} else {/* In the case of fast retransmit, the packet should not go to the tail* of the unacked queue, but rather somewhere before it. We need to check for* this case. -STJ Jul 27, */if (TCP_SEQ_LT(ntohl(seg->tcphdr->seqno), ntohl(useg->tcphdr->seqno))){ // 如果当前发送报文的序号小于unacked报文的序号,需要将当前报文按顺序插入unacked队列里面/* add segment to before tail of unacked list, keeping the list sorted */struct tcp_seg **cur_seg = &(pcb->unacked); // unacked队列头部while (*cur_seg &&TCP_SEQ_LT(ntohl((*cur_seg)->tcphdr->seqno), ntohl(seg->tcphdr->seqno))) { // cur_seg序号小于当前报文的序号cur_seg = &((*cur_seg)->next ); // 查找下一个unacked报文}seg->next = (*cur_seg); // 当前报文插入cur_seg之前(*cur_seg) = seg; // 当前报文替换cur_seg} else { // 当前报文插入unacked队列末尾/* add segment to tail of unacked list */useg->next = seg;useg = useg->next;}}/* do not queue empty segments on the unacked list */} else { // 没有数据的报文直接释放,不保证传输到对端
不考虑拥塞窗口的情况下,发送窗口有多大就能发送多少数据。
4.2、收到应答(ACK)
发送窗口除了在通告窗口代码中会更新外,其他地方都不更新;数据被确认接收时,只是更新lastack,lastack ~lastack +snd_wnd即可理解为发送窗口,lastack ~lastack +snd_wnd内的数据即可发送。
收到报文时,调用tcp_receive处理报文,如果带有ACK,检查ackno是否有确认unacked队列里面的报文。
检查ackno是否在unacked报文之间(ackno大于等于第一个需要被确认的报文的序号,小于下一个待发送报文的序号),在之间,那么就有unacked报文被对端确认已经接收;重置超时重传次数、超时重传时间等,计算已经确认的数据大小及下一个待确认数据的序号,dupacks清零。
} else if (TCP_SEQ_BETWEEN(ackno, pcb->lastack+1, pcb->snd_nxt)){ // ackno判断,检查是否有数据被确认接收/* We come here when the ACK acknowledges new data. *//* Reset the "IN Fast Retransmit" flag, since we are no longerin fast retransmit. Also reset the congestion window to theslow start threshold. */if (pcb->flags & TF_INFR) {pcb->flags &= ~TF_INFR;pcb->cwnd = pcb->ssthresh;}/* Reset the number of retransmissions. */pcb->nrtx = 0; // 超时重传次数设置为0/* Reset the retransmission time-out. */pcb->rto = (pcb->sa >> 3) + pcb->sv; // 重新设置超时重传时间/* Update the send buffer space. Diff between the two can never exceed 64K? */pcb->acked = (u16_t)(ackno - pcb->lastack); // 被确认已经接收的数据长度pcb->snd_buf += pcb->acked; // 发送缓存增加已经确认的数据长度/* Reset the fast retransmit variables. */pcb->dupacks = 0; // 收到非重复的ACK,dupacks清零pcb->lastack = ackno; // 下一个待确认的序号
拥塞窗口更新。开始时启用慢启动,拥塞窗口渐渐加大(线性增加),超过慢启动门限后,启用拥塞避免,拥塞窗口大小增加减缓(每次增加大小减少)。
/* Update the congestion control variables (cwnd andssthresh). */if (pcb->state >= ESTABLISHED) {if (pcb->cwnd < pcb->ssthresh) { // 拥塞窗口小于慢启动门限ssthresh,《TCP-IP详解卷 1:协议.pdf》 21.6 拥塞避免算法,当拥塞发生时(超时或收到重复确认) , ssthresh被设置为当前窗口大小的一半,如果是超时引起了拥塞,则cwnd被设置为1个报文段if ((u16_t)(pcb->cwnd + pcb->mss) > pcb->cwnd) { // 加mss没有越界pcb->cwnd += pcb->mss; // 《TCP-IP详解卷 1:协议.pdf》 20.6 慢启动,每收到一个ACK拥塞窗口cwnd增加一个报文段mss大小(lwip没有按指数增加拥塞窗口)}LWIP_DEBUGF(TCP_CWND_DEBUG, ("tcp_receive: slow start cwnd %"U16_F"\n", pcb->cwnd));} else { // 《TCP-IP详解卷 1:协议.pdf》 21.6 拥塞避免算法,拥塞窗口大于等于慢启动门限ssthresh,开始执行拥塞避免算法u16_t new_cwnd = (pcb->cwnd + pcb->mss * pcb->mss / pcb->cwnd); // 每次收到一个确认时,cwnd增加pcb->mss * pcb->mss / pcb->cwnd,cwnd在增加,pcb->mss * pcb->mss不变,因此每次收到ACK,cwnd增加越来越慢if (new_cwnd > pcb->cwnd) {pcb->cwnd = new_cwnd;}LWIP_DEBUGF(TCP_CWND_DEBUG, ("tcp_receive: congestion avoidance cwnd %"U16_F"\n", pcb->cwnd));}}
释放unacked队列中已经被确认接收的数据,如果unacked队列为空,停止超时重传定时器,否则超时次数重置为0(unacked队列仍有数据待确认,超时重传定时器重新开始)。(lwip unsent里面可能存在已经发送过的数据,unacked超时重传会把unacked的报文放到unsent队列里面,确认时需要检查unsent队列里面是否有数据被确认接收)
/* Remove segment from the unacknowledged list if the incomingACK acknowlegdes them. */while (pcb->unacked != NULL &&TCP_SEQ_LEQ(ntohl(pcb->unacked->tcphdr->seqno) +TCP_TCPLEN(pcb->unacked), ackno)) {LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_receive: removing %"U32_F":%"U32_F" from pcb->unacked\n",ntohl(pcb->unacked->tcphdr->seqno),ntohl(pcb->unacked->tcphdr->seqno) +TCP_TCPLEN(pcb->unacked)));next = pcb->unacked;pcb->unacked = pcb->unacked->next;LWIP_DEBUGF(TCP_QLEN_DEBUG, ("tcp_receive: queuelen %"U16_F" ... ", (u16_t)pcb->snd_queuelen));LWIP_ASSERT("pcb->snd_queuelen >= pbuf_clen(next->p)", (pcb->snd_queuelen >= pbuf_clen(next->p)));pcb->snd_queuelen -= pbuf_clen(next->p);tcp_seg_free(next);LWIP_DEBUGF(TCP_QLEN_DEBUG, ("%"U16_F" (after freeing unacked)\n", (u16_t)pcb->snd_queuelen));if (pcb->snd_queuelen != 0) {LWIP_ASSERT("tcp_receive: valid queue length", pcb->unacked != NULL ||pcb->unsent != NULL);}}/* If there's nothing left to acknowledge, stop the retransmittimer, otherwise reset it to start again */if(pcb->unacked == NULL)pcb->rtime = -1;elsepcb->rtime = 0;pcb->polltmr = 0;
接下来的代码就是之前接收数据、接收窗口更新的内容了。
如果觉得《TCP/IP传输层协议实现 - TCP接收窗口/发送窗口/通告窗口(lwip)》对你有帮助,请点赞、收藏,并留下你的观点哦!