应用层的数据发送需要通告tcp传递,在已经建立tcp连接中,通过tcp_write()函数向对方发送数据。
一,简介
tcp_write()通过已建立连接的tcp控制块给对方发送数据。代码的实现逻辑是将数据复制到控制块的unsent队列,代码中为节省内存,分多种情况将发送数据连接到unsent。
二,代码分析
发送数据插入unsent队列时,需要考虑三种情况:
1,写入最后一个pbuf
由于tcp是面向字节流的传输协议,所以unsent队列中最后一个pbuf如果有剩余的内存可以使用,则将数据填充进去。
u16_t space; //报文可用的内存空间,是一个抽象的数值
u16_t unsent_optlen; //选项长度
//找到unsent队列最后一个成员
for (last_unsent = pcb->unsent; last_unsent->next != NULL;
last_unsent = last_unsent->next);
unsent_optlen = LWIP_TCP_OPT_LENGTH(last_unsent->flags); //最后一个报文的选项长度
LWIP_ASSERT("mss_local is too small", mss_local >= last_unsent->len + unsent_optlen);
//last_unsent的报文剩余内存 = 本地最大报文长度-(last_unsent里tcp数据长度+选项长度)
space = mss_local - (last_unsent->len + unsent_optlen);
//将数据复制到最后一个pbuf的剩余内存中,这里并不直接复制,而是记录复制所需的参数(oversize_used)
oversize = pcb->unsent_oversize; //最后一个pbuf剩下的空间,是一个真实的内存
//如果最后的pbuf中还有剩余内存
if (oversize > 0) {
LWIP_ASSERT("inconsistent oversize vs. space", oversize <= space);
seg = last_unsent;
//理解space,oversize大小可能不同,取其中的最小值,len一般都比较大
oversize_used = LWIP_MIN(space, LWIP_MIN(oversize, len));
pos += oversize_used; //pos记录数据移动字节
oversize -= oversize_used; //更新oversize
space -= oversize_used;
}
这里需要区别好space,oversize两个变量。一个报文有大小限制,其下可以挂有多个pbuf,space表示该报文还剩下多少的空间;oversize表示一个pbuf中,剩余内存的大小。所以space与oversize的大小关系是不确定。
2,新建pbuf
经过1之后,若仍有剩余数据未加入unsent,且最后一个报文段还能继续添加pbuf,则在新建pbuf。
这里还有一个特殊情况,就是发送的数据在内存上正好与最后的pbuf连续,这种特殊情况下,不需要内存复制,只需要将pbuf的大小修改。
/*
* 将一个新的pbuf连接到pcb->unsent尾部
* 这里分复制内存和引用内存两种情况:
* 复制内存即:复制数据到新的内存空间
* 引用即:申请新的内存空间,并把指针指向数据的内存地址
*/
//最后一个pbuf已经满了且剩余有数据未复制,并且当前的报文还能再放pbuf的话,就在当前报文再添加一个pbuf
if ((pos < len) && (space > 0) && (last_unsent->len > 0)) {
u16_t seglen = LWIP_MIN(space, len - pos); //计算新的pbuf大小,要么是当前报文剩余的大小,要么是剩下数据的大小
seg = last_unsent;
//使用内存复制
if (apiflags & TCP_WRITE_FLAG_COPY) {
/* Data is copied */
//申请内存大小为seglen的pbuf
if ((concat_p = tcp_pbuf_prealloc(PBUF_RAW, seglen, space, &oversize, pcb, apiflags, 1)) == NULL) {
LWIP_DEBUGF(TCP_OUTPUT_DEBUG | LWIP_DBG_LEVEL_SERIOUS,
("tcp_write : could not allocate memory for pbuf copy size %"U16_F"\n",
seglen));
goto memerr;
}
//复制数据到新pbuf
TCP_DATA_COPY2(concat_p->payload, (const u8_t*)arg + pos, seglen, &concat_chksum, &concat_chksum_swapped);
queuelen += pbuf_clen(concat_p); //计算concat_p中pbuf的长度并添加到报文
} else {
//使用地址引用的方式
struct pbuf *p;
for (p = last_unsent->p; p->next != NULL; p = p->next); //找到last_unsent的最后一个pbuf
//!如果该pbuf是PBUF_ROM类型且内存与数据的内存是连续的,则直接扩展该pbuf的长度,不用新建
if (p->type == PBUF_ROM && (const u8_t *)p->payload + p->len == (const u8_t *)arg) {
LWIP_ASSERT("tcp_write: ROM pbufs cannot be oversized", pos == 0);
extendlen = seglen; //记下扩展长度
} else {
//其他类型的pbuf
//申请一块PBUF_ROM类型的pbuf,不给payload分配内存
if ((concat_p = pbuf_alloc(PBUF_RAW, seglen, PBUF_ROM)) == NULL) {
LWIP_DEBUGF(TCP_OUTPUT_DEBUG | LWIP_DBG_LEVEL_SERIOUS,
("tcp_write: could not allocate memory for zero-copy pbuf\n"));
goto memerr;
}
//将新的pbuf的payload指向数据地址,省去了复制的步骤
((struct pbuf_rom*)concat_p)->payload = (const u8_t*)arg + pos;
queuelen += pbuf_clen(concat_p); //计算concat_p中pbuf的长度并添加到报文
}
}
pos += seglen; //更新pos
}
} else {
//最后的pbuf足够放下数据
}
3,新建报文
若经过1,2步骤后仍有数据未被放入unsent,则在循环中新建报文存放数据,并将该报文段插入unsent队尾。(这一步并无真正入队,只是创建了一个本地的队列)
//循环创建报文,将剩余数据放入报文,报文入队
//pos已入队的数据,len数据总长度
while (pos < len) {
struct pbuf *p;
u16_t left = len - pos; //剩余数据长度
u16_t max_len = mss_local - optlen; //一个报文的最大长度
u16_t seglen = LWIP_MIN(left, max_len); //当前报文的大小,要么是最大的报文长度,要么是数据剩余长度
//使用内存复制
if (apiflags & TCP_WRITE_FLAG_COPY) {
//新建一个pbuf,大小是报文大小加上tcp首部
if ((p = tcp_pbuf_prealloc(PBUF_TRANSPORT, seglen + optlen, mss_local, &oversize, pcb, apiflags, queue == NULL)) == NULL) {
LWIP_DEBUGF(TCP_OUTPUT_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("tcp_write : could not allocate memory for pbuf copy size %"U16_F"\n", seglen));
goto memerr;
}
LWIP_ASSERT("tcp_write: check that first pbuf can hold the complete seglen",
(p->len >= seglen));
//复制数据到新pbuf
TCP_DATA_COPY2((char *)p->payload + optlen, (const u8_t*)arg + pos, seglen, &chksum, &chksum_swapped);
} else {
//使用引用:p2,p分别是tcp内容和tcp首部的pbuf,p2的payload指向数据地址
struct pbuf *p2;
//分配p2,PBUF_ROM类型不会给payload分配空间
if ((p2 = pbuf_alloc(PBUF_TRANSPORT, seglen, PBUF_ROM)) == NULL) {
LWIP_DEBUGF(TCP_OUTPUT_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("tcp_write: could not allocate memory for zero-copy pbuf\n"));
goto memerr;
}
//p2的payload指向数据区
((struct pbuf_rom*)p2)->payload = (const u8_t*)arg + pos;
//给报文头部分配p,如果分配失败,先释放p2
if ((p = pbuf_alloc(PBUF_TRANSPORT, optlen, PBUF_RAM)) == NULL) {
pbuf_free(p2);
LWIP_DEBUGF(TCP_OUTPUT_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("tcp_write: could not allocate memory for header pbuf\n"));
goto memerr;
}
//将首部和报文内容通过链表连接
pbuf_cat(p/*header*/, p2/*data*/);
}
queuelen += pbuf_clen(p); //更新报文队列长度
//如果发送队列或者缓存超出限制,则释放内存
if ((queuelen > TCP_SND_QUEUELEN) || (queuelen > TCP_SNDQUEUELEN_OVERFLOW)) {
LWIP_DEBUGF(TCP_OUTPUT_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("tcp_write: queue too long %"U16_F" (%d)\n",
queuelen, (int)TCP_SND_QUEUELEN));
pbuf_free(p);
goto memerr;
}
//给新的pbuf创建一个tcp_seg报文
if ((seg = tcp_create_segment(pcb, p, 0, pcb->snd_lbb + pos, optflags)) == NULL) {
goto memerr;
}
//如果是队列第一个报文段,直接引用
if (queue == NULL) {
queue = seg;
} else {
//将新报文连接到链尾
LWIP_ASSERT("prev_seg != NULL", prev_seg != NULL);
prev_seg->next = seg;
}
prev_seg = seg; //更新最后一个报文
LWIP_DEBUGF(TCP_OUTPUT_DEBUG | LWIP_DBG_TRACE, ("tcp_write: queueing %"U32_F":%"U32_F"\n",
lwip_ntohl(seg->tcphdr->seqno),
lwip_ntohl(seg->tcphdr->seqno) + TCP_TCPLEN(seg)));
pos += seglen; //移动pos
}
4,将数据入队
细心的小伙伴们发现,之前的代码都没有真正把数据添加到unsent队列,在1中我们记录的oversize_used用来表示最后一个pbuf填入的数据长度,在2中,我们创建了一个新的pbuf,在3中,我们创建了一个报文队列。
在接下来的代码,则是将以上三种情况的数据一一入unsent队。
填充第一阶段的pbuf数据
//如果last_unsent的最后一个pbuf有数据需要填入
if (oversize_used > 0) {
struct pbuf *p;
//找到unsent最后一个pbuf
for (p = last_unsent->p; p; p = p->next) {
p->tot_len += oversize_used; //所有pbuf的tot_len(理解)都要加上oversize_used
if (p->next == NULL) {
//在最后一个pbuf复制oversize_used大小的数据
TCP_DATA_COPY((char *)p->payload + p->len, arg, oversize_used, last_unsent);
p->len += oversize_used;
}
}
last_unsent->len += oversize_used; //报文长度也增加
}
pcb->unsent_oversize = oversize; //更新报文oversize
将第二阶段的pbuf连接到报文上
//把新增了concat_p连接到该报文的pbuf链表
if (concat_p != NULL) {
LWIP_ASSERT("tcp_write: cannot concatenate when pcb->unsent is empty",
(last_unsent != NULL));
pbuf_cat(last_unsent->p, concat_p);
last_unsent->len += concat_p->tot_len; //更新链接后的报文长度
} else if (extendlen > 0) {
//内存扩展的情况: 内存连接在一起,直接把这个pbuf扩展了
struct pbuf *p;
LWIP_ASSERT("tcp_write: extension of reference requires reference",
last_unsent != NULL && last_unsent->p != NULL);
//所有pbuf的totlen都增加
for (p = last_unsent->p; p->next != NULL; p = p->next) {
p->tot_len += extendlen;
}
//由于数据地址与pbuf内存是连续的,所以不需要复制内存
p->tot_len += extendlen;
p->len += extendlen;
last_unsent->len += extendlen; //报文长度增加
}
将本地的报文队列插到unsent,最后更新tcp的发送窗口
if (last_unsent == NULL) {
pcb->unsent = queue;
} else {
last_unsent->next = queue;
}
pcb->snd_lbb += len;
pcb->snd_buf -= len; //发送buffer减少
pcb->snd_queuelen = queuelen; //更新发送队列长度
三,小结
看到这就会理解,tcp_write()函数只将数据插入unsent队列,并未真正将数据发送出去,而真正将数据发送出去的函数是tcp_output();