一,简介
上一章提到的tcp_write()
函数的作用是将数据插入unsent队列,其并无真正的把数据交给ip层发送。tco_output()
的功能就是将unsent队列中的tcp报文交给ip层发送。
一般情况下,tcp_output()函数会在500ms定时任务中被循环调用。所以当用户使用tcp_write()
将数据写入unsent队列后,会在定时任务中将数据传递给ip层发送。
二,源码分析
首先需要先判断能不能发送unsent队列,若当前的PCB正在接收数据或者当前发送窗口小于要发送的报文长度,都不能发送数据。代码如下:
//发送控制块中的所有unsents报文段
err_t
tcp_output(struct tcp_pcb *pcb)
{
struct tcp_seg *seg, *useg;//seg:要发送的报文段,useg:未确认的报文段
u32_t wnd, snd_nxt; //wnd:真实发送窗口,snd_nxt:下一个发送的序号
err_t err;
struct netif *netif; //发送的网络接口
LWIP_ASSERT("don't call tcp_output for listen-pcbs",
pcb->state != LISTEN);
//如果该tcp正在接收数据,则不发送
if (tcp_input_pcb == pcb) {
return ERR_OK;
}
//真实发送窗口是发送窗口和拥塞窗口的最小
wnd = LWIP_MIN(pcb->snd_wnd, pcb->cwnd);
//取第一个报文段
seg = pcb->unsent;
//如果tcp使用TF_ACK_NOW标志,或者本地不能发送数据(unsent为null或者当前窗口无法发送seg),则发送一个不带任何数据的ack
if (pcb->flags & TF_ACK_NOW &&
(seg == NULL ||
lwip_ntohl(seg->tcphdr->seqno) - pcb->lastack + seg->len > wnd)) {
return tcp_send_empty_ack(pcb);
}
//将useg指向unacked队尾
useg = pcb->unacked;
if (useg != NULL) {
for (; useg->next != NULL; useg = useg->next);
}
//找到最佳的网络接口
netif = ip_route(&pcb->local_ip, &pcb->remote_ip);
if (netif == NULL) {
return ERR_RTE;
}
//检查本地ip地址
if (ip_addr_isany(&pcb->local_ip)) {
const ip_addr_t *local_ip = ip_netif_get_local_ip(netif, &pcb->remote_ip);
if (local_ip == NULL) {
return ERR_RTE;
}
ip_addr_copy(pcb->local_ip, *local_ip);
}
//发送窗口太小而不能发送当前报文,等待对方接收窗口变大,所以启动零窗口探测,坚持定时器开启
if (seg != NULL &&
lwip_ntohl(seg->tcphdr->seqno) - pcb->lastack + seg->len > wnd &&
wnd > 0 && wnd == pcb->snd_wnd && pcb->unacked == NULL) {
/* Start the persist timer */
//启动坚持定时器
if (pcb->persist_backoff == 0) {
pcb->persist_cnt = 0;
pcb->persist_backoff = 1;
}
//由于发送窗口太小,函数实际上无输出
goto output_done;
}
接下来就是遍历unsent队列,将tcp报文发送,直到发送窗口不满足发送调节
while (seg != NULL &&
lwip_ntohl(seg->tcphdr->seqno) - pcb->lastack + seg->len <= wnd) {
LWIP_ASSERT("RST not expected here!",
(TCPH_FLAGS(seg->tcphdr) & TCP_RST) == 0);
//如果nagle算法有效或者缓存有错误(这是由write导致的内存错误),停止发送
if ((tcp_do_output_nagle(pcb) == 0) &&
((pcb->flags & (TF_NAGLEMEMERR | TF_FIN)) == 0)) {
break;
}
//!SYN_SENT状态下发送的报文的ack位无效
if (pcb->state != SYN_SENT) {
TCPH_SET_FLAG(seg->tcphdr, TCP_ACK);
}
//发送一个tcp报文
err = tcp_output_segment(seg, pcb, netif);
if (err != ERR_OK) {
/* segment could not be sent, for whatever reason */
pcb->flags |= TF_NAGLEMEMERR;
return err;
}
pcb->unsent = seg->next; //队列头部已经发送,更新
//!SYN_SENT状态下发送的报文的ack位无效
if (pcb->state != SYN_SENT) {
pcb->flags &= ~(TF_ACK_DELAY | TF_ACK_NOW); //清除应答标志位
}
snd_nxt = lwip_ntohl(seg->tcphdr->seqno) + TCP_TCPLEN(seg); //计算下一个要发送的序号
//如果pcb->snd_nxt比snd_nxt小,则更新下一个要发送的序号
if (TCP_SEQ_LT(pcb->snd_nxt, snd_nxt)) {
pcb->snd_nxt = snd_nxt;
}
//将已发送报文插入unacked队列
if (TCP_TCPLEN(seg) > 0) {
seg->next = NULL;
//unacked为空,seg则放入队首
if (pcb->unacked == NULL) {
pcb->unacked = seg;
useg = seg;
} else {
//unacked队列的排队是小号在前,大号在后的模式
//若seg<useg则在循环中中找到合适的位置
if (TCP_SEQ_LT(lwip_ntohl(seg->tcphdr->seqno), lwip_ntohl(useg->tcphdr->seqno))) {
//如果当前报文序号比较低,则查找适合的位置插入,序号越大越靠后
struct tcp_seg **cur_seg = &(pcb->unacked);
//找到cur_seg>seg的的报文,并在该报文前插入seg
while (*cur_seg &&
TCP_SEQ_LT(lwip_ntohl((*cur_seg)->tcphdr->seqno), lwip_ntohl(seg->tcphdr->seqno))) {
cur_seg = &((*cur_seg)->next );
}
seg->next = (*cur_seg);
(*cur_seg) = seg;
} else { //
//seg>useg,seg插入unacked队尾
useg->next = seg;
useg = useg->next; //更新unacked
}
}
} else {
//对于空的报文段直接删除不需要插入unacked
tcp_seg_free(seg);
}
seg = pcb->unsent; //发送下一个报文
}
以上代码中,tcp_output_segment()是真正将报文传递给ip层的函数,该函数主要做的是,填充即将发送的tcp报文首部的通告窗口,确认序号,mss以及校验和。同时开启PCB的超时重传和rtt估算,最后移动payload指针到首部,并将报文的pbuf传递给ip层。
//通过调用ip层发送tcp报文
static err_t
tcp_output_segment(struct tcp_seg *seg, struct tcp_pcb *pcb, struct netif *netif)
{
err_t err;
u16_t len;
u32_t *opts;
//TODO ref为1才能发送
if (seg->p->ref != 1) {
return ERR_OK;
}
//填充tcp首部确认号字段
seg->tcphdr->ackno = lwip_htonl(pcb->rcv_nxt);
//填充tcp首部通告窗口
seg->tcphdr->wnd = lwip_htons(TCPWND_MIN16(RCV_WND_SCALE(pcb, pcb->rcv_ann_wnd)));
//pcb通告接收窗口右边界=下一个接收序号+通告接收窗口长度
pcb->rcv_ann_right_edge = pcb->rcv_nxt + pcb->rcv_ann_wnd;
opts = (u32_t *)(void *)(seg->tcphdr + 1); //选项字段
//填充mss选项
if (seg->flags & TF_SEG_OPTS_MSS) {
u16_t mss;
mss = tcp_eff_send_mss(TCP_MSS, &pcb->local_ip, &pcb->remote_ip); //针对ip路径计算mss大小
mss = TCP_MSS;
*opts = TCP_BUILD_MSS_OPTION(mss); //构建适用于tcp首部的mss
opts += 1; //opts移动
}
//如果超时重传定时器没开就给老资开
if (pcb->rtime < 0) {
pcb->rtime = 0;
}
//如果该tcp的rtt计时器关闭了则开启rtt估测
if (pcb->rttest == 0) {
pcb->rttest = tcp_ticks; //记录当前时间
pcb->rtseq = lwip_ntohl(seg->tcphdr->seqno); //被估算的报文段的编号
LWIP_DEBUGF(TCP_RTO_DEBUG, ("tcp_output_segment: rtseq %"U32_F"\n", pcb->rtseq));
}
LWIP_DEBUGF(TCP_OUTPUT_DEBUG, ("tcp_output_segment: %"U32_F":%"U32_F"\n",
lwip_htonl(seg->tcphdr->seqno), lwip_htonl(seg->tcphdr->seqno) +
seg->len));
len = (u16_t)((u8_t *)seg->tcphdr - (u8_t *)seg->p->payload); //计算首部占用的字节空间
//如果首部为0那肯定有问题
if (len == 0) {
MIB2_STATS_INC(mib2.tcpoutsegs);
}
//pbuf的len指的是tcp数据的长度
seg->p->len -= len;
seg->p->tot_len -= len;
//移动payload到tcp首部,因为需要传递给ip层
seg->p->payload = seg->tcphdr;
seg->tcphdr->chksum = 0;
//计算tcp校验和
seg->tcphdr->chksum = ip_chksum_pseudo(seg->p, IP_PROTO_TCP,
seg->p->tot_len, &pcb->local_ip, &pcb->remote_ip);
TCP_STATS_INC(tcp.xmit);
NETIF_SET_HWADDRHINT(netif, &(pcb->addr_hint));
//调用ip层发送该报文的pbuf
err = ip_output_if(seg->p, &pcb->local_ip, &pcb->remote_ip, pcb->ttl,
pcb->tos, IP_PROTO_TCP, netif);
NETIF_SET_HWADDRHINT(netif, NULL);
return err;
}
ip_output_if()函数就是将tcp报文传递给ip层,tcp报文传递给ip层时数据变化如下,tcp_hdr中被填入相应的参数,payload移动到tcp_hdr
三,小结
tcp_output();将unsent队列的报文填充完整,然后交给ip层,实现数据的发送