三、传输层(tcp_receive)
tcp_receive()函数
tcp_receive( )只会被tcp_process函数调用,用于进一步完成对输入报文的处理,具体来说,该函数主要是完成输入报文的冗余截断,管理unacked、unsent、ooseq三张链表
void tcp_receive(struct tcp_pcb *pcb)
{
struct tcp_seg *next;
struct tcp_seg *prev, *cseg;
struct pbuf *p;
s32_t off;
s16_t m;
u32_t right_wnd_edge; // 本地发送窗口右边界
u16_t new_tot_len;
int found_dupack = 0; // 重复ack标志,置1表示是重复ack
// 首先检测报文是否包含ACK标志
if (flags & TCP_ACK)
{
right_wnd_edge = pcb->snd_wl2 + pcb->snd_wnd; // 获取本地发送窗口右边界
// 有3种情况可以导致本地发送窗口更新
if (TCP_SEQ_LT(pcb->snd_wl1, seqno)|| // snd_wl1小于新seqno,说明对方有发来数据
(pcb->snd_wl1 == seqno && TCP_SEQ_LT(pcb->snd_wl2, ackno))|| // snd_wl1等于新seqno且snd_wl2小于新ackno,说明对方没有发送数据,只是在收到数据后发送一个确认
(pcb->snd_wl2 == ackno && tcphdr->wnd > pcb->snd_wnd)) // snd_wl2等于新ackno且snd_wnd小于报文首部的窗口通告wnd,说明我方没有发数据过去,但被对方告知接收窗口变大
{
pcb->snd_wnd = tcphdr->wnd; // 更新本地发送窗口大小 ,跟对方发来的接收窗口通告匹配
pcb->snd_wl1 = seqno; // 更新接收到的序号
pcb->snd_wl2 = ackno; // 更新接收到的确认号
// 如果发送窗口非0,且探察开启
if (pcb->snd_wnd > 0 && pcb->persist_backoff > 0)
{
pcb->persist_backoff = 0; // 停止窗口探察
}
}
// 判断是否是一个重复的ACK,需要满足5个条件
// 1.如果ackno小于等于lastack,即没有确认新数据
if (TCP_SEQ_LEQ(ackno, pcb->lastack))
{
pcb->acked = 0; // 没有确认新数据,那么acked为0
// 2.如果报文段中没有数据
if (tcplen == 0)
{
// 3.本地发送窗口没有更新
if (pcb->snd_wl2 + pcb->snd_wnd == right_wnd_edge)
{
// 4.如果重传定时器正在运行,即本地有数据正等待被确认
if (pcb->rtime >= 0)
{
// 5.如果ackno等于lastack
if (pcb->lastack == ackno)
{
// 此时可以确定这是一个重复的ack,说明报文发生了丢失
found_dupack = 1;
// 该ack被重复收到的次数自增
if (pcb->dupacks + 1 > pcb->dupacks)
++pcb->dupacks;
// 如果该ack重复收到超过3次,说明发生了拥塞
if (pcb->dupacks > 3)
{
if ((u16_t)(pcb->cwnd + pcb->mss) > pcb->cwnd)
{
pcb->cwnd += pcb->mss;
}
}
// 如果该ack重复第3次收到,执行快速重传算法
else if (pcb->dupacks == 3)
{
tcp_rexmit_fast(pcb);
}
}
}
}
}
// 如果没有确认新数据但又不属于重复ack
if (!found_dupack)
{
pcb->dupacks = 0; // 将ack重复收到的次数清0
}
}
// 如果是正常情况的ACK,lastack+1<=ackno<=snd_nxt
else if (TCP_SEQ_BETWEEN(ackno, pcb->lastack+1, pcb->snd_nxt))
{
// 如果控制块处于快速重传状态 ,则关闭重传状态、拥塞功能
if (pcb->flags & TF_INFR)
{
pcb->flags &= ~TF_INFR;
pcb->cwnd = pcb->ssthresh;
}
pcb->nrtx = 0; // 重传次数清0
pcb->rto = (pcb->sa >> 3) + pcb->sv; // 复位重传超时时间
pcb->acked = (u16_t)(ackno - pcb->lastack); // 更新acked字段为被确认的已发送数据长度
pcb->snd_buf += pcb->acked; // 更新可用的发送空间
pcb->dupacks = 0; // 将ack重复收到的次数清0
pcb->lastack = ackno; // 更新接收到的ackno
// 如果处于TCP连接已经建立状态,调整拥塞算法功能模块
if (pcb->state >= ESTABLISHED)
{
if (pcb->cwnd < pcb->ssthresh)
{
if ((u16_t)(pcb->cwnd + pcb->mss) > pcb->cwnd)
{
pcb->cwnd += pcb->mss;
}
}
else
{
u16_t new_cwnd = (pcb->cwnd + pcb->mss * pcb->mss / pcb->cwnd);
if (new_cwnd > pcb->cwnd)
{
pcb->cwnd = new_cwnd;
}
}
}
// 遍历unacked队列,将所有数据编号小于等于ackno的报文段移除
while (pcb->unacked != NULL && TCP_SEQ_LEQ(ntohl(pcb->unacked->tcphdr->seqno) + TCP_TCPLEN(pcb->unacked), ackno))
{
// 将满足要求的报文从unacked链表取出
next = pcb->unacked;
pcb->unacked = pcb->unacked->next;
// 如果该报文包含FIN标志,意味着当前收到的ACK对FIN做了确认,则acked字段减1,即不需要提交上层使知道FIN被对方成功接收
if ((pcb->acked != 0) && ((TCPH_FLAGS(next->tcphdr) & TCP_FIN) != 0))
{
pcb->acked--;
}
pcb->snd_queuelen -= pbuf_clen(next->p); // 释放被该报文占用的发送空间
tcp_seg_free(next); // 释放被该报文占用的tcp报文段
}
// 当所有满足要求的报文段移除成功后,判断unacked队列是否为空
if(pcb->unacked == NULL)
pcb->rtime = -1; // 若为空,关闭重传定时器
else
pcb->rtime = 0; // 否则复位重传定时器
pcb->polltmr = 0; // 复位轮询定时器
}
// 如果该ACK既不是重复ACK,又不是正常ACK,则acked字段清0,即该ACK不确认任何已发送数据
else
{
pcb->acked = 0;
}
// 遍历unsent队列,将所有数据编号小于等于ackno的报文段移除
// 这是因为对于需要重传的报文段,lwip直接将它们挂在unsent队列上,所以收到的ACK可能是对已超时报文段的确认
while (pcb->unsent != NULL && TCP_SEQ_BETWEEN(ackno, ntohl(pcb->unsent->tcphdr->seqno) + TCP_TCPLEN(pcb->unsent), pcb->snd_nxt))
{
// 将满足要求的报文从unsent链表取出
next = pcb->unsent;
pcb->unsent = pcb->unsent->next;
// 如果该报文包含FIN标志,意味着当前收到的ACK对FIN做了确认,则acked字段减1,即不需要提交上层使知道FIN被对方成功接收
if ((pcb->acked != 0) && ((TCPH_FLAGS(next->tcphdr) & TCP_FIN) != 0))
{
pcb->acked--;
}
pcb->snd_queuelen -= pbuf_clen(next->p); // 释放被该报文占用的发送空间
tcp_seg_free(next); // 释放被该报文占用的tcp报文段
}
// RTT计算,暂略
if (pcb->rttest && TCP_SEQ_LT(pcb->rtseq, ackno))
{
m = (s16_t)(tcp_ticks - pcb->rttest);
m = m - (pcb->sa >> 3);
pcb->sa += m;
if (m < 0) {
m = -m;
}
m = m - (pcb->sv >> 2);
pcb->sv += m;
pcb->rto = (pcb->sa >> 3) + pcb->sv;
pcb->rttest = 0;
}
}
// 如果该输入报文还包含了数据,则要继续对数据进行处理
if (tcplen > 0)
{
// 如果seqno + 1 <= rcv_nxt <= seqno + tcplen - 1,意味着收到的数据区域头部有无效数据(收到的数据有部分处于本地左侧接收窗口外),需要截断数据头
if (TCP_SEQ_BETWEEN(pcb->rcv_nxt, seqno + 1, seqno + tcplen - 1))
{
off = pcb->rcv_nxt - seqno; // 需要截掉的数据长度
p = inseg.p; // 获取收到的报文段的pbuf链表头
// 判断需要截断的长度是否超出了第一个pbuf中存储的数据长度
if (inseg.p->len < off)
{
new_tot_len = (u16_t)(inseg.p->tot_len - off); // 截断重复数据后的有效数据长度
// 如果超出,则需要遍历pbuf链表,依次摘除数据,直到最后一个包含摘除数据的pbuf
while (p->len < off)
{
off -= p->len; // 剩余摘除长度
p->tot_len = new_tot_len; // 更新当前pbuf中的数据总长,
p->len = 0; // 因为数据被摘除,所以当前pbuf中的数据分长清0
p = p->next; // 指向下一个pbuf
}
// 处理最后一个包含摘除数据的pbuf,就是调整数据指针略过摘除数据
pbuf_header(p, (s16_t)-off);
}
else
{
// 如果未超出,则调整第一个pbuf中的数据指针略过摘除数据
pbuf_header(inseg.p, (s16_t)-off);
}
inseg.len -= (u16_t)(pcb->rcv_nxt - seqno); // 更新TCP报文段数据总长
inseg.tcphdr->seqno = seqno = pcb->rcv_nxt; // 更新TCP头中的seqno,指向接收窗口头位置
}
else
{
// 如果seqno < rcv_nxt,意味着seqno+tcplen-1 < rcv_nxt,说明这是个完全重复的报文段
if (TCP_SEQ_LT(seqno, pcb->rcv_nxt))
{
tcp_ack_now(pcb); // 只回复一个ACK给对方(这里是否应该直接返回不再运行下去)
}
}
// 如果数据起始编号在接收窗口内
if (TCP_SEQ_BETWEEN(seqno, pcb->rcv_nxt, pcb->rcv_nxt + pcb->rcv_wnd - 1))
{
// 如果该报文数据处于接收起始位置,意味着该报文是连续到来的
if (pcb->rcv_nxt == seqno)
{
tcplen = TCP_TCPLEN(&inseg); // 更新该报文的总数据长度
// 如果总长大于接收窗口大小,就需要做尾部截断处理,这里包含对FIN和SYN两种标志的不同处理结果,注意体会
if (tcplen > pcb->rcv_wnd)
{
// 如果TCP头中带FIN标志,清除FIN标志,因为对方还有数据要发过来
if (TCPH_FLAGS(inseg.tcphdr) & TCP_FIN)
{
TCPH_FLAGS_SET(inseg.tcphdr, TCPH_FLAGS(inseg.tcphdr) &~ TCP_FIN);
}
inseg.len = pcb->rcv_wnd; // 根据接收窗口调整数据长度
// 如果TCP头中带SYN标志,报文段数据长度减1
if (TCPH_FLAGS(inseg.tcphdr) & TCP_SYN)
{
inseg.len -= 1;
}
pbuf_realloc(inseg.p, inseg.len); // 因为数据被截断,pbuf中的参数需要相应调整
tcplen = TCP_TCPLEN(&inseg); // 再次更新该报文的总数据长度
}
// 如果无序报文段队列ooseq上存在报文段
if (pcb->ooseq != NULL)
{
// 判断当前有序报文段的TCP头中是否带FIN标志
if (TCPH_FLAGS(inseg.tcphdr) & TCP_FIN)
{
// 如果该有序报文段带FIN标志,意味着单向TCP连接结束
// 不可能再从对方收到新的报文段,ooseq队列中的报文段没有成为有序报文段可能,只能作废
while (pcb->ooseq != NULL)
{
struct tcp_seg *old_ooseq = pcb->ooseq;
pcb->ooseq = pcb->ooseq->next;
tcp_seg_free(old_ooseq);
}
}
else
{
next = pcb->ooseq;
// 遍历ooseq链表,删除序号被当前有序报文段完全覆盖的报文段
while (next && TCP_SEQ_GEQ(seqno + tcplen,next->tcphdr->seqno + next->len))
{
// 如果这些即将被删除的报文段带FIN标志且当前有序报文段不带SYN标志
if (TCPH_FLAGS(next->tcphdr) & TCP_FIN &&(TCPH_FLAGS(inseg.tcphdr) & TCP_SYN) == 0)
{
TCPH_SET_FLAG(inseg.tcphdr, TCP_FIN); // 在当前有效报文段的TCP头中添加FIN标志
tcplen = TCP_TCPLEN(&inseg); // 再次更新该报文的总数据长度
}
prev = next;
next = next->next;
tcp_seg_free(prev);
}
// 如果当前有序报文段尾部与ooseq中的报文段存在部分重叠
if (next && TCP_SEQ_GT(seqno + tcplen,next->tcphdr->seqno))
{
inseg.len = (u16_t)(next->tcphdr->seqno - seqno); // 截断当前有序报文段尾部的重叠部分,得到有效部分长度
// 如果当前有序报文段TCP头中带SYN标志,报文段数据长度减1
if (TCPH_FLAGS(inseg.tcphdr) & TCP_SYN)
{
inseg.len -= 1;
}
pbuf_realloc(inseg.p, inseg.len); // 因为数据被截断,pbuf中的参数需要相应调整
tcplen = TCP_TCPLEN(&inseg); // 再次更新该报文的总数据长度
}
pcb->ooseq = next;
}
}
pcb->rcv_nxt = seqno + tcplen; // 更新下一个期望接收到的序号,也就是接收窗口左边界
pcb->rcv_wnd -= tcplen; // 更新当前可用接收窗口
tcp_update_rcv_ann_wnd(pcb); // 更新公告窗口
// 如果该有序报文段中存在数据
if (inseg.p->tot_len > 0)
{
recv_data = inseg.p; // 将全局指针recv_data指向报文段中的数据pbuf
inseg.p = NULL;
}
// 如果该有序报文段的TCP头中带FIN标志
if (TCPH_FLAGS(inseg.tcphdr) & TCP_FIN)
{
recv_flags |= TF_GOT_FIN; // 则在报文处理结果变量recv_flags添加TF_GOT_FIN标志
}
// 遍历ooseq队列,取出所有有序的报文段
// (通过比较ooseq队列中报文段的seqno和当前TCP控制块中保存的rcv_nxt来判定该报文段是否有序)
while (pcb->ooseq != NULL && pcb->ooseq->tcphdr->seqno == pcb->rcv_nxt)
{
cseg = pcb->ooseq;
seqno = pcb->ooseq->tcphdr->seqno; // 更新序号
pcb->rcv_nxt += TCP_TCPLEN(cseg); // 更新下一个期望接收到的序号
pcb->rcv_wnd -= TCP_TCPLEN(cseg); // 更新当前可用接收窗口
tcp_update_rcv_ann_wnd(pcb); // 更新公告窗口
// 如果该有序报文段中存在数据,则通过全局指针recv_data向上层提交数据
if (cseg->p->tot_len > 0)
{
// 判断全局指针recv_data是否为空
if (recv_data)
{
// 如果不为空,意味着有更早的数据准备向上提交
pbuf_cat(recv_data, cseg->p); // 将当前数据pbuf挂到recv_data指向的数据链表的尾部
}
else
{
// 如果为空,直接将当前数据pbuf赋给recv_data
recv_data = cseg->p;
}
cseg->p = NULL;
}
// 如果该有序报文段的TCP头中带FIN标志
if (TCPH_FLAGS(cseg->tcphdr) & TCP_FIN)
{
recv_flags |= TF_GOT_FIN; // 则全局变量recv_flags添加TF_GOT_FIN标志
// 如果当前TCP处于ESTABLISHED状态,则变成CLOSE_WAIT状态
if (pcb->state == ESTABLISHED)
{
pcb->state = CLOSE_WAIT;
}
}
pcb->ooseq = cseg->next;
tcp_seg_free(cseg);
}
// 以上都执行完毕后,向源端返回一个ACK,此处其实只是先在TCP控制块中添加ACK标志
tcp_ack(pcb);
}
// 如果该报文数据不处于接收起始位置,意味着该报文不是有序的
else
{
// 首先向源端返回一个立即ACK
tcp_send_empty_ack(pcb);
// 然后将该报文段放入ooseq队列
if (pcb->ooseq == NULL)
{
// 如果ooseq为空,则拷贝该报文段到新开辟的报文段空间,并将新开辟报文段作为ooseq起始单元
pcb->ooseq = tcp_seg_copy(&inseg);
}
else
{
prev = NULL; // 定义为ooseq链表中上一个报文段,这里首先清空
// 遍历ooseq队列,选择合适位置插入该报文段
for(next = pcb->ooseq; next != NULL; next = next->next)
{
// 依次比较两个报文段的起始序号seqno,如果相等
if (seqno == next->tcphdr->seqno)
{
// 继续比较两个报文段的数据长度
if (inseg.len > next->len)
{
// 如果输入报文段数据长度更长
// 拷贝该报文段到新开辟的报文段空间
cseg = tcp_seg_copy(&inseg);
// 插入ooseq链表
if (cseg != NULL)
{
// 如果不是ooseq上的第一个报文段
if (prev != NULL)
{
prev->next = cseg; // 插入ooseq链表的上一个报文段之后
}
// 如果是第一个
else
{
pcb->ooseq = cseg; // 直接替换原有的第一个
}
tcp_oos_insert_segment(cseg, next); // 处理好插入后与原有的下一个报文段的影响,简单来说,就是切掉冗余,释放内存
}
break; // 退出循环
}
else
{
// 如果输入报文段数据长度更短,则直接丢弃,并退出循环
break;
}
}
// 如果不相等
else
{
// 如果是ooseq上的第一个报文段
if (prev == NULL)
{
// 如果该报文段的起始序号大于要插入的报文段起始序号
if (TCP_SEQ_LT(seqno, next->tcphdr->seqno))
{
cseg = tcp_seg_copy(&inseg); // 拷贝要插入的报文段到新开辟的报文段空间
if (cseg != NULL)
{
pcb->ooseq = cseg; // 将新报文段插到ooseq第一个位置
tcp_oos_insert_segment(cseg, next); // 处理好插入后与原有的第一个报文段的影响
}
break; // 退出循环
}
}
// 如果不是第一个
else
{
// 如果待插入报文段起始序号在前一个和后一个报文段起始序号之间
if (TCP_SEQ_BETWEEN(seqno, prev->tcphdr->seqno+1, next->tcphdr->seqno-1))
{
cseg = tcp_seg_copy(&inseg); // 拷贝要插入的报文段到新开辟的报文段空间
if (cseg != NULL)
{
// 如果与前一个报文段有数据重合
if (TCP_SEQ_GT(prev->tcphdr->seqno + prev->len, seqno))
{
prev->len = (u16_t)(seqno - prev->tcphdr->seqno); // 截断前一个报文段尾部
pbuf_realloc(prev->p, prev->len); // 因为数据被截断,pbuf中的参数需要相应调整
}
prev->next = cseg; // 将新报文段插入前一个报文段之后
tcp_oos_insert_segment(cseg, next); // 处理好插入后与原有的下一个报文段的影响
}
break;
}
}
// 如果已经是ooseq上的最后一个报文段
// 且待插入的报文段起始序号大于该报文起始序号(其实函数运行到这里该条件必然成立)
if (next->next == NULL && TCP_SEQ_GT(seqno, next->tcphdr->seqno))
{
// 如果该报文的TCP头中有FIN标志,则直接丢弃待插入的报文段,退出循环
if (TCPH_FLAGS(next->tcphdr) & TCP_FIN)
{
break;
}
next->next = tcp_seg_copy(&inseg); // 拷贝要插入的报文段到新开辟的报文段空间,并插在队列尾部
// 如果新插入的报文段不为空
if (next->next != NULL)
{
// 如果与前一个报文段有数据重合
if (TCP_SEQ_GT(next->tcphdr->seqno + next->len, seqno))
{
next->len = (u16_t)(seqno - next->tcphdr->seqno); // 截断前一个报文段尾部
pbuf_realloc(next->p, next->len); // 因为数据被截断,pbuf中的参数需要相应调整
}
// 如果新插入的报文段数据长度超出了当前接收窗口大小
if ((u32_t)tcplen + seqno > pcb->rcv_nxt + (u32_t)pcb->rcv_wnd)
{
// 如果新插入的报文段的TCP头中有FIN标志
if (TCPH_FLAGS(next->next->tcphdr) & TCP_FIN)
{
TCPH_FLAGS_SET(next->next->tcphdr, TCPH_FLAGS(next->next->tcphdr) &~ TCP_FIN); // 去掉TCP头中的FIN标志
}
next->next->len = pcb->rcv_nxt + pcb->rcv_wnd - seqno; // 根据接收窗口大小调制新插入的报文段数据长度
pbuf_realloc(next->next->p, next->next->len); // 因为数据被截断,pbuf中的参数需要相应调整
tcplen = TCP_TCPLEN(next->next); // 再次更新该报文的总数据长度
}
}
break;
}
}
prev = next; // 以上都不满足,则遍历ooseq链表中下一个
}
}
}
}
// 如果数据不在接收范围内
else
{
tcp_send_empty_ack(pcb); // 直接向源端返回一个立即确认ACK
}
}
// 如果输入的报文段中不包含数据
else
{
// 且序号位于接收窗口之内
if(!TCP_SEQ_BETWEEN(seqno, pcb->rcv_nxt, pcb->rcv_nxt + pcb->rcv_wnd-1))
{
tcp_ack_now(pcb); // 回一个ACK
}
}
}
err_t tcp_output(struct tcp_pcb *pcb)
{
struct tcp_seg *seg,*useg;
u32_t wnd,snd_nxt;
if(tcp_input_pcb == pcb)
{
return ERR_OK;
}
wnd = LWIP_MIN(pcb->snd_wnd,pcb->cwnd);
seg = pcb->unsent;
if(pcb->flags & TF_ACK_NOW && (seg = NULL || ntohl(seg->tcphdr->seqno) ))
{
}
}