TCP/IP传输层协议实现 - TCP报文接收/发送(lwip)
Posted arm7star
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了TCP/IP传输层协议实现 - TCP报文接收/发送(lwip)相关的知识,希望对你有一定的参考价值。
(tcp的收发与接收窗口/发送窗口/通告窗口关联比较紧密,接收/发送过程在《TCP/IP传输层协议实现 - TCP接收窗口/发送窗口/通告窗口(lwip)》https://blog.csdn.net/arm7star/article/details/117153533 都有介绍,本文对收发过程进行更详细一步介绍。)
1、滑动窗口
1.1、接收窗口(接收滑动窗口)
接收窗口是本地可以接收数据的窗口,接收端只接收窗口内的数据,窗口外的丢弃。
接收到数据,接收窗口左边沿右移,接收窗口减小。
(1)、接收到数据前(4、5、6、7、8、9可接收) | (2)、接收窗口左边沿右移,接收窗口减小(收到4、5、6数据,可接收7、8、9) |
数据被应用层接收,接收窗口大小恢复,接收窗口右边沿右移。
(1)、应用层接收数据前(4、5、6待应用层接收,可接收7、8、9) | (2)、接收窗口恢复,接收窗口右边沿右移(4、5、6被应用层接收,恢复3个字节数据,接收窗口大小恢复3个字节,右边沿右移) |
1.2、发送窗口(发送滑动窗口)
发送窗口与接收窗口类似,发送窗口内的数据才能发送。不考虑拥塞窗口的前提下,如下图所示,接收方通告接收窗口大小为6个字节大小(接收方还可以再接收6个字节的数据,发送方只能再发送6个字节的数据),发送方发送的1、2、3被接收方接收并被确认,发送方的4、5、6已经发送但未被确认,7、8、9为能够发送的数据,10、11不能发送(发送窗口外的数据需要发送时,协议栈会缓存在发送缓存里面,10、11虽然不能发送,但是如果发送缓存够的话,会缓存到发送缓存里面,当数据被确认时,发送窗口移动,缓存的数据如果在发送窗口内,那么就可以发送)。
2、报文接收
tcp报文输入调用tcp_input函数,tcp_input依次查找tcp_active_pcbs、tcp_tw_pcbs、tcp_listen_pcbs链表,已连接的tcp信息保存在tcp_active_pcbs里面(收发双方的IP地址及端口)。(连接建立与终止参考https://blog.csdn.net/arm7star/article/details/116560454 《TCP/IP传输层协议实现 - TCP连接的建立与终止(lwip)》)
2.1、报文解析接收
(释放已被确认接收的报文,更新发送缓存等,接收数据...)
获取tcp首部。
iphdr = p->payload; // 获取ip首部
tcphdr = (struct tcp_hdr *)((u8_t *)p->payload + IPH_HL(iphdr) * 4); // 获取tcp首部
解析tcp首部。(源地址,目的地址、seqno、ackno、flags、tcplen)
/* Convert fields in TCP header to host byte order. */
tcphdr->src = ntohs(tcphdr->src);
tcphdr->dest = ntohs(tcphdr->dest);
seqno = tcphdr->seqno = ntohl(tcphdr->seqno);
ackno = tcphdr->ackno = ntohl(tcphdr->ackno);
tcphdr->wnd = ntohs(tcphdr->wnd);
flags = TCPH_FLAGS(tcphdr);
tcplen = p->tot_len + ((flags & (TCP_FIN | TCP_SYN)) ? 1 : 0);
初始化接收报文的tcp_seg。(输入报文长度、报文数据指针、tcp首部,对于接收窗口内的可接收的报文,输入的报文会拷贝到inseg;如果数据可接受,那么recv_data会指向可接受的数据,recv_flags设置为可接受报文的flags)
/* Set up a tcp_seg structure. */
inseg.next = NULL;
inseg.len = p->tot_len;
inseg.dataptr = p->payload;
inseg.p = p;
inseg.tcphdr = tcphdr;
recv_data = NULL; // 可接受数据的指针(如果期待的下一个序号为4,那么序号为4的报文即为可接受的报文)
recv_flags = 0;
检查是否有被应用层拒绝接收的数据refused_data。(收到可接受的数据recv_data,协议栈会将数据推送到应用层,应用层因为某些原因没办法接收该数据,那么被拒绝接收的recv_data数据会保存在refused_data里面;处理报文前需要先检查是否有被拒绝接收的refused_data数据,如果有,那么需要再次将refused_data推送到应用层,推送成功才处理当前收到的报文(如果推送失败的话,应用层应该没办法处理更多数据,即使当前输入报文可能有数据要接收,但是应用层没办法接收,协议栈直接丢弃该报文即可,让发送方超时重传))
/* If there is data which was previously "refused" by upper layer */
if (pcb->refused_data != NULL) {
/* Notify again application with data previously received. */
LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_input: notify kept packet\\n"));
TCP_EVENT_RECV(pcb, pcb->refused_data, ERR_OK, err); // 发送数据到应用层
if (err == ERR_OK) { // 发送成功
pcb->refused_data = NULL; // 重置refused_data
} else {
/* drop incoming packets, because pcb is "full" */
LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_input: drop incoming packets, because pcb is \\"full\\"\\n"));
TCP_STATS_INC(tcp.drop);
snmp_inc_tcpinerrs();
pbuf_free(p); // 发送数据到应用层失败,释放报文,直接丢弃报,返回
return;
}
}
调用tcp_process处理输入报文(报文标志位、连接状态检查,tmr定时器复位、KEEPALIVE counter重置,处理tcp报文选项),ESTABLISHED状态下的连接调用tcp_receive处理输入报文。
发送窗口更新。(细节参考https://blog.csdn.net/arm7star/article/details/117153533)
if (flags & TCP_ACK) {
right_wnd_edge = pcb->snd_wnd + pcb->snd_wl2;
......
重复ack处理,3个以上重复ACK执行快速恢复算法(快速重传、拥塞避免)。(《TCP-IP详解卷 1:协议》21.7 快速重传与快速恢复算法)
if (pcb->lastack == ackno) { // 没有新的数据被确认接收
pcb->acked = 0; // 被确认的数据为0
if (pcb->snd_wl2 + pcb->snd_wnd == right_wnd_edge){ // 重复ACK判断
++pcb->dupacks;
if (pcb->dupacks >= 3 && pcb->unacked != NULL) { // 如果一连串收到3个或3个以上的重复ACK,就非常可能是发送的报文段丢失了
if (!(pcb->flags & TF_INFR)) {
/* This is fast retransmit. Retransmit the first unacked segment. */
LWIP_DEBUGF(TCP_FR_DEBUG, ("tcp_receive: dupacks %"U16_F" (%"U32_F"), fast retransmit %"U32_F"\\n",
(u16_t)pcb->dupacks, pcb->lastack,
ntohl(pcb->unacked->tcphdr->seqno)));
tcp_rexmit(pcb); // 重传丢失的数据报文段,而无需等待超时定时器溢出
/* Set ssthresh to max (FlightSize / 2, 2*SMSS) */
/*pcb->ssthresh = LWIP_MAX((pcb->snd_max -
pcb->lastack) / 2,
2 * pcb->mss);*/
/* Set ssthresh to half of the minimum of the current cwnd and the advertised window */
if (pcb->cwnd > pcb->snd_wnd)
pcb->ssthresh = pcb->snd_wnd / 2;
else
pcb->ssthresh = pcb->cwnd / 2; // 慢启动门限ssthresh设置为拥塞窗口的一半
/* The minimum value for ssthresh should be 2 MSS */
if (pcb->ssthresh < 2*pcb->mss) { // 慢启动门限小于2个mss
LWIP_DEBUGF(TCP_FR_DEBUG, ("tcp_receive: The minimum value for ssthresh %"U16_F" should be min 2 mss %"U16_F"...\\n", pcb->ssthresh, 2*pcb->mss));
pcb->ssthresh = 2*pcb->mss; // 设置慢启动门限为2个mss
}
pcb->cwnd = pcb->ssthresh + 3 * pcb->mss; // 拥塞窗口设置为pcb->ssthresh + 3 * pcb->mss(拥塞窗口大于慢启动门限ssthresh时,执行拥塞避免算法)
pcb->flags |= TF_INFR; // 快速恢复算法(In fast recovery)
} else {
/* Inflate the congestion window, but not if it means that
the value overflows. */
if ((u16_t)(pcb->cwnd + pcb->mss) > pcb->cwnd) {
pcb->cwnd += pcb->mss;
}
}
}
} else {
LWIP_DEBUGF(TCP_FR_DEBUG, ("tcp_receive: dupack averted %"U32_F" %"U32_F"\\n",
pcb->snd_wl2 + pcb->snd_wnd, right_wnd_edge));
}
} else if (TCP_SEQ_BETWEEN(ackno, pcb->lastack+1, pcb->snd_nxt)){
有数据被确认接收。(ackno >= pcb->lastack + 1 && ackno <= pcb->snd_nxt)
} else if (TCP_SEQ_BETWEEN(ackno, pcb->lastack+1, pcb->snd_nxt)){ // ackno确认(, ackno - 1]区间的数据被接收,[pcb->lastack, pcb->snd_nxt - 1]区间的数据等待被确认,因此有数据被确认的话,ackno - 1 >= pcb->lastack并且ackno - 1 <= pcb->snd_nxt - 1,即ackno >= pcb->lastack + 1 && ackno <= pcb->snd_nxt。
/* We come here when the ACK acknowledges new data. */
/* Reset the "IN Fast Retransmit" flag, since we are no longer
in fast retransmit. Also reset the congestion window to the
slow start threshold. */
if (pcb->flags & TF_INFR) {
pcb->flags &= ~TF_INFR;
pcb->cwnd = pcb->ssthresh;
}
有数据被确认接收后重置超时重传定时器,恢复发送缓存,更新lastack,更新拥塞窗口cwnd等(慢启动、拥塞避免)。
/* Reset the number of retransmissions. */
pcb->nrtx = 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;
pcb->lastack = ackno;
/* Update the congestion control variables (cwnd and
ssthresh). */
if (pcb->state >= ESTABLISHED) {
if (pcb->cwnd < pcb->ssthresh) {
if ((u16_t)(pcb->cwnd + pcb->mss) > pcb->cwnd) {
pcb->cwnd += pcb->mss; // 慢启动算法
}
LWIP_DEBUGF(TCP_CWND_DEBUG, ("tcp_receive: slow start cwnd %"U16_F"\\n", pcb->cwnd));
} else {
u16_t new_cwnd = (pcb->cwnd + pcb->mss * pcb->mss / pcb->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));
}
}
LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_receive: ACK for %"U32_F", unacked->seqno %"U32_F":%"U32_F"\\n",
ackno,
pcb->unacked != NULL?
ntohl(pcb->unacked->tcphdr->seqno): 0,
pcb->unacked != NULL?
ntohl(pcb->unacked->tcphdr->seqno) + TCP_TCPLEN(pcb->unacked): 0));
被确认接收的数据从unacked队列删除。
/* Remove segment from the unacknowledged list if the incoming
ACK 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);
}
}
接下来被确认接收的数据从unsent删除。(超时重传,unacked队列的数据会插入unsent表头,因此unsent里面可能包含已发送但unacked的数据)
如果报文有数据,那么处理报文里面的数据。报文处理参考https://blog.csdn.net/arm7star/article/details/117153533 《TCP/IP传输层协议实现 - TCP接收窗口/发送窗口/通告窗口(lwip)》"3.1、接收窗口的更新"。
2.2、报文接收后处理
数据被确认接收以及有可接受的数据时,需要通知应用层。
tcp_process处理报文后返回tcp_input,tcp_input对报文处理结果进行处理。
有数据被确认接收,需要调用sent函数(select(lwip_select)函数有监听sent事件的功能,应用层阻塞在lwip_select等待写事件时,需要调用TCP_EVENT_SENT唤醒应用层)
/* If the application has registered a "sent" function to be
called when new send buffer space is available, we call it
now. */
if (pcb->acked > 0) {
TCP_EVENT_SENT(pcb, pcb->acked, err); // sent事件处理
}
接收到数据recv_data,需要发送到应用层(报文如果有PSH标志,需要将PSH标志传递到应用层)。
if (recv_data != NULL) { // 有可接受的数据
if(flags & TCP_PSH) { // 报文含有PSH标志
recv_data->flags |= PBUF_FLAG_PUSH; // 传递PSH标志到应用层
}
/* Notify application that data has been received. */
TCP_EVENT_RECV(pcb, recv_data, ERR_OK, err); // 发送数据到应用层(lwip内部为mail box)
/* If the upper layer can't receive this data, store it */
if (err != ERR_OK) { // 应用层拒绝接收数据(mail box已经满了)
pcb->refused_data = recv_data; // 拒收的数据保存在refused_data里面,下次收到报文时再次尝试发送到应用层,如果下次仍发送失败,则丢弃下次收到的报文
LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_input: keep incoming packet, because pcb is \\"full\\"\\n"));
}
}
FIN报文处理。(应用层接读数据阻塞时,如果传输层收到FIN报文,需要让应用层返回,lwip通过发送一个NULL数据给应用层,应用层收到NULL的数据时,认为收到了FIN标记,读数据将返回)
/* If a FIN segment was received, we call the callback
function with a NULL buffer to indicate EOF. */
if (recv_flags & TF_GOT_FIN) { // 报文带有FIN标记
TCP_EVENT_RECV(pcb, NULL, ERR_OK, err); // 发送一个NULL数据包到应用层
}
发送更多报文。(有数据被确认接收了,发送窗口里面有更多数据可以发送(unsent发送窗口里面的数据可以发送),调用tcp_output发送数据;不考虑拥塞窗口的情况下,总有发送窗口大小的数据在发送,tcp_output会把发送窗口占满,如果发送窗口太大,导致网络拥塞,则拥塞窗口会限制同一时间能发送报文的数量)
/* If there were no errors, we try to send something out. */
if (err == ERR_OK) {
tcp_output(pcb);
}
3、报文发送
在数据被确认接收后,tcp_input会调用tcp_output发送更多unsent的数据,因此tcp_output可以认为是发送缓存里面的数据到发送窗口。发送unsent队列里面的报文比较简单,如果unsent里面的报文在发送窗口/拥塞窗口范围内,那么就发送;另外,tcp_output有调用tcp_do_output_nagle,tcp_do_output_nagle主要在发送端用于避免糊涂窗口综合症(《TCP-IP详解卷 1:协议》22.3 糊涂窗口综合症);unsent队列发送可以参考https://blog.csdn.net/arm7star/article/details/117153533《TCP/IP传输层协议实现 - TCP接收窗口/发送窗口/通告窗口(lwip)》"4.1、报文发送/加入发送队列"。
应用层数据发送到unsent队列是通过tcp_enqueue函数实现。tcp_enqueue函数原型为“err_t tcp_enqueue(struct tcp_pcb *pcb, void *arg, u16_t len, u8_t flags, u8_t apiflags, u8_t optflags)”,arg即为应用层write/sengmsg的数据,len即为应用层write/sengmsg数据的长度,socket封装了对pcb及其他参数的处理,应用层调用write/sengmsg函数,协议栈会找到socket对应的tcp_pcb并调用tcp_enqueue。
发送队列/发送缓存主要涉及snd_lbb、snd_buf、unsent,snd_lbb(Sequence number of next byte to be buffered)为下一个缓存报文的序号(unsent、unacked之外的第一个序号),snd_buf为发送缓存的大小(可用缓存的大小,数据加入发送队列时,发送缓存减小,数据被确认接收时,恢复发送缓存),unsent前面介绍过,如果unsent队列不为空、当前发送的数据比较少,unsent最末一个报文可能可以装下当前的数据,那么可能需要合并成一个报文。
加入发送缓存/发送队列的过程如下。
3.1、发送的数据组装成tcp报文队列
发送缓存、缓存报文序号等获取。
/* fail on too much data */
if (len > pcb->snd_buf) { // 本次要发送的数据长度大于可用的缓存
LWIP_DEBUGF(TCP_OUTPUT_DEBUG | 3, ("tcp_enqueue: too much data (len=%"U16_F" > snd_buf=%"U16_F")\\n", len, pcb->snd_buf));
pcb->flags |= TF_NAGLEMEMERR;
return ERR_MEM; // 返回内存错误(内存不够)
}
left = len; // 剩余需要缓存的数据left(一次发送的数据可能过大,需要缓存成多个报文)
ptr = arg; // 数据指针
optlen = LWIP_TCP_OPT_LENGTH(optflags);
/* seqno will be the sequence number of the first segment enqueued
* by the call to this function. */
seqno = pcb->snd_lbb; // 当前缓存报文的seqno
发送队列长度检查。(虽然发送缓存够大,但是发送队列有限制,因此也不能缓存超过发送队列的数据;发送队列包括unacked、unsent;发送队列长度是按pbuf节点来计算的,就是内存链表有多少个节点,简单理解就是发送队列(unacked、unsent)里面有多少不连续的内存数据块;pbuf_clen计算一个pbuf有多少内存节点)
/* If total number of pbufs on the unsent/unacked queues exceeds the
* configured maximum, return an error */
queuelen = pcb->snd_queuelen; // 发送队列长度
/* check for configured max queuelen and possible overflow */
if ((queuelen >= TCP_SND_QUEUELEN) || (queuelen > TCP_SNDQUEUELEN_OVERFLOW)) {
LWIP_DEBUGF(TCP_OUTPUT_DEBUG | 3, ("tcp_enqueue: too long queue %"U16_F" (max %"U16_F")\\n", queuelen, TCP_SND_QUEUELEN));
TCP_STATS_INC(tcp.memerr);
pcb->flags |= TF_NAGLEMEMERR;
return ERR_MEM;
}
应用层发送的数据组装成tcp报文,放入queue队列(queue队列保存的是当前发送的数据的tcp报文链表;发送的数据过大时,会将数据拆分保存在一个个的tcp报文里面,这些报文组成一个queue链表)。
while (queue == NULL || left > 0) {
/* The segment length (including options) should be at most the MSS */
seglen = left > (pcb->mss - optlen) ? (pcb->mss - optlen) : left; // 如果当前剩余数据加上选项数据长度大于一个mss报文,那么拆分剩余的数据,先组装一个mss大小的报文
/* Allocate memory for tcp_seg, and fill in fields. */
seg = memp_malloc(MEMP_TCP_SEG);
if (seg == NULL) {
LWIP_DEBUGF(TCP_OUTPUT_DEBUG | 2,
("tcp_enqueue: could not allocate memory for tcp_seg\\n"));
goto memerr;
}
seg->next = NULL; // next为空
seg->p = NULL; // 负载为空(还没挂接数据)
/* first segment of to-be-queued data? */
if (queue == NULL) {
queue = seg; // 当前报文为本次缓存的第一个报文,queue指向第一个拆分的报文
}
/* subsequent segments of to-be-queued data */
else {
/* Attach the segment to the end of the queued segments */
LWIP_ASSERT("useg != NULL", useg != NULL);
useg->next = seg; // useg指向前一次循环拆分的报文,当前报文链接到前一个拆分报文的后面
}
/* remember last segment of to-be-queued data for next iteration */
useg = seg; // useg指向本次循环拆分的报文,下次循环时,useg就指向前一次循环拆分的报文
/* If copy is set, memory should be allocated
* and data copied into pbuf, otherwise data comes from
* ROM or other static memory, and need not be copied. */
if (apiflags & TCP_WRITE_FLAG_COPY) { // 写拷贝,应用层的数据需要拷贝到报文里面
if ((seg->p = pbuf_alloc(PBUF_TRANSPORT, seglen + optlen, PBUF_RAM)) == NULL) {
LWIP_DEBUGF(TCP_OUTPUT_DEBUG | 2,
("tcp_enqueue : could not allocate memory for pbuf copy size %"U16_F"\\n", seglen));
goto memerr;
}
LWIP_ASSERT("check that first pbuf can hold the complete seglen",
(seg->p->len >= seglen + optlen));
queuelen += pbuf_clen(seg->p); // pbuf_clen计算Count number of pbufs in a chain,非内存大小,增加queuelen
if (arg != NULL) {
MEMCPY((char *)seg->p->payload + optlen, ptr, seglen); // 拷贝应用层数据到报文里面
}
seg->dataptr = seg->p->payload; // 更新dataptr
}
/* do not copy data */
else {
/* First, allocate a pbuf for the headers. */
if ((seg->p = pbuf_alloc(PBUF_TRANSPORT, optlen, PBUF_RAM)) == NULL) { // 不需要拷贝数据,只分配optlen大小的内存来保存选项数据
LWIP_DEBUGF(TCP_OUTPUT_DEBUG | 2,
("tcp_enqueue: could not allocate memory for header pbuf\\n"));
goto memerr;
}
queuelen += pbuf_clen(seg->p); // 增加queuelen
/* Second, allocate a pbuf for holding the data.
* since the referenced data is available at least until it is sent out on the
* link (as it has to be ACKed by the remote party) we can safely use PBUF_ROM
* instead of PBUF_REF here.
*/
if (left > 0) {
if ((p = pbuf_alloc(PBUF_RAW, seglen, PBUF_ROM)) == NULL) { // 申请一个pbuf(没有申请保存数据的内存空间,应用层数据不拷贝到报文里面,pbuf的数据指针指向应用层数据地址)
/* If allocation fails, we have to deallocate the header pbuf as well. */
pbuf_free(seg->p);
seg->p = NULL;
LWIP_DEBUGF(TCP_OUTPUT_DEBUG | 2,
("tcp_enqueue: could not allocate memory for zero-copy pbuf\\n"));
goto memerr;
}
++queuelen; // 应用层的数据是连续的,只占一个连续内存块,因此queuelen只需要加1即可
/* reference the non-volatile payload data */
p->payload = ptr; // payload指向应用层数据地址
seg->dataptr = ptr; // dataptr指向应用层数据地址
/* Concatenate the headers and data pbufs together. */
pbuf_cat(seg->p/*header*/, p/*data*/);
p = NULL;
}
}
/* Now that there are more segments queued, we check again if the
length of the queue exceeds the configured maximum or overflows. */
if ((queuelen > TCP_SND_QUEUELEN) || (queuelen > TCP_SNDQUEUELEN_OVERFLOW)) {
LWIP_DEBUGF(TCP_OUTPUT_DEBUG | 2, ("tcp_enqueue: queue too long %"U16_F" (%"U16_F")\\n", queuelen, TCP_SND_QUEUELEN));
goto memerr;
}
seg->len = seglen; // 设置报文长度
/* build TCP header */
if (pbuf_header(seg->p, TCP_HLEN)) { // 申请tcp首部空间
LWIP_DEBUGF(TCP_OUTPUT_DEBUG | 2, ("tcp_enqueue: no room for TCP header in pbuf.\\n"));
TCP_STATS_INC(tcp.err);
goto memerr;
}
seg->tcphdr = seg->p->payload; // tcp首部设置
seg->tcphdr->src = htons(pcb->local_port);
seg->tcphdr->dest = htons(pcb->remote_port);
seg->tcphdr->seqno = htonl(seqno);
seg->tcphdr->urgp = 0;
TCPH_FLAGS_SET(seg->tcphdr, flags);
/* don't fill in tcphdr->ackno and tcphdr->wnd until later */
seg->flags = optflags;
/* Set the length of the header */
TCPH_HDRLEN_SET(seg->tcphdr, (5 + optlen / 4));
LWIP_DEBUGF(TCP_OUTPUT_DEBUG | LWIP_DBG_TRACE, ("tcp_enqueue: queueing %"U32_F":%"U32_F" (0x%"X16_F")\\n",
ntohl(seg->tcphdr->seqno),
ntohl(seg->tcphdr->seqno) + TCP_TCPLEN(seg),
(u16_t)flags));
left -= seglen; // 剩余数长度减去已拆分成tcp报文的数据长度(剩余的left数据需要组装tcp报文)
seqno += seglen; // 更新下个报文的seqno
ptr = (void *)((u8_t *)ptr + seglen); // 下一个报文数据的指针
}
3.2、本次发送数据的报文队列插入unsent队列
获取queue报文插入的位置。(如果unsent队列不为空,那么需要插入到unsent队列末尾,否则queue就是未发送的队列)
/* Now that the data to be enqueued has been broken up into TCP
segments in the queue variable, we add them to the end of the
pcb->unsent queue. */
if (pcb->unsent == NULL) { // unsent队列为空
useg = NULL;
}
else {
for (useg = pcb->unsent; useg->next != NULL; useg = useg->next); // 找到unsent队列的尾节点(unsent不为空,待发送数据的报文插入unsent末尾)
}
unsent末尾节点报文与queue的第一个节点合并。(TCP_SYN|TCP_FIN相关的报文不能合并,合并的两个报文不能超过一个mss最大报文,合并的两个报文flags要相等;不能合并时,直接将queue插入到unsent末尾即可)
if (useg != NULL &&
TCP_TCPLEN(useg) != 0 &&
!(TCPH_FLAGS(useg->tcphdr) & (TCP_SYN | TCP_FIN)) &&
!(flags & (TCP_SYN | TCP_FIN)) &&
/* fit within max seg size */
(useg->len + queue->len <= pcb->mss) &&
/* only concatenate segments with the same options */
(useg->flags == queue->flags)) {
/* Remove TCP header from first segment of our to-be-queued list */
if(pbuf_header(queue->p, -(TCP_HLEN + optlen))) {
/* Can we cope with this failing? Just assert for now */
LWIP_ASSERT("pbuf_header failed\\n", 0);
TCP_STATS_INC(tcp.err);
goto memerr;
}
if (queue->p->len == 0) {
/* free the first (header-only) pbuf if it is now empty (contained only headers) */
struct pbuf *old_q = queue->p;
queue->p = queue->p->next;
old_q->next = NULL;
queuelen--;
pbuf_free(old_q);
}
LWIP_ASSERT("zero-length pbuf", (queue->p != NULL) && (queue->p->len > 0));
pbuf_cat(useg->p, queue->p);
useg->len += queue->len;
useg->next = queue->next;
LWIP_DEBUGF(TCP_OUTPUT_DEBUG | LWIP_DBG_TRACE | LWIP_DBG_STATE, ("tcp_enqueue: chaining segments, new len %"U16_F"\\n", useg->len));
if (seg == queue) {
seg = useg;
seglen = useg->len;
}
memp_free(MEMP_TCP_SEG, queue);
}
3.3、发送缓存更新
发送缓存、seqno更新。
if ((flags & TCP_SYN) || (flags & TCP_FIN)) { // TCP_SYN|TCP_FIN报文占一个字节
++len;
}
if (flags & TCP_FIN) {
pcb->flags |= TF_FIN;
}
pcb->snd_lbb += len; // 更新下一个缓存报文的seqno
pcb->snd_buf -= len; // 发送缓存大小减去len(非TCP_SYN|TCP_FIN报文,len就是数据的长度,TCP_SYN|TCP_FIN报文,len就需要在数据长度上加1个字节,TCP_SYN|TCP_FIN占一个数据)
3.4、PSH标志设置
应用层已经发送完一次数据,最后一次的sendmsg不带MORE标志,最后一个报文添加PSH标志。(对端收到PSH报文会立即返回,而不等待接收足够数据,PSH就是发送端期望接收端收完这些数据后应该尽快处理,不用等待更多数据,接下来收到的数据与当前没有关联(例如发送端希望发送两个消息“hello”和“world”,语义上“hello”和“world”是独立的,接收端收到“hello”即可返回,下次在接收"world",而不是一次接收“helloworld”);write操作理论上每个数据后面都有设置PSH标志,write没办法设置more参数;ceph异步消息就使用sendmsg发送消息,ceph消息是buffer list结构,而且一个完整的消息不一定在一个buffer list里面,因此buffer list前面的消息都设置一个more标志,消息的最后那部分数据才不设置more标志)
/* Set the PSH flag in the last segment that we enqueued, but only
if the segment has data (indicated by seglen > 0). */
if (seg != NULL && seglen > 0 && seg->tcphdr != NULL && ((apiflags & TCP_WRITE_FLAG_MORE)==0)) {
TCPH_SET_FLAG(seg->tcphdr, TCP_PSH);
}
报文加入未发送队列后,调用tcp_output发送报文。
以上是关于TCP/IP传输层协议实现 - TCP报文接收/发送(lwip)的主要内容,如果未能解决你的问题,请参考以下文章
TCP/IP应用层协议实现 - 数据收发send/recv(lwip)