arg是要发送的数据的指针;len 是要发送的数据的长度,以字节为单位;flags是TCP数据段头部中的标识字段,主要用 于连接建立或断开的握手;apiflags表示要对该数据段做的操作,包括是否拷贝数据、是否设置PUSH标志;optdata表示TCP头部中的选项字段的值,optlen表示选项字段的长度。
tcp_enqueue 首先确认要发送的数据长度len 是否小于当前连接能用的数据发送缓冲区大小将要发送的数据段的序号 字段设置为pcb->snd_lbb,然后判断pcb->snd_queuelen值是否超过了所允许挂接的数据包
的上限值TCP_SND_QUEUELEN,如果超过了该上限值,则函数也不会对这个要发送的数
据段进行处理。接下来tcp_enqueue 函数会将数据组装成为tcp_seg 类型的数据段,根据数据长度的大小不同,可能需要几个tcp_seg类型结构才能描述完所有的数据,每个数据段中的TCP头部部分字段值要在这里都要被设置,包括数据序号、标志字段。最后,所有创建好的tcp_seg类型结构都是连接在queue队列上的,queue是函数的一个临时变量函数tcp_enqueue需要将queue队列上的数据段挂接到TCP控制块的unsent队列上这有好几种情况,即unsent 队列是否为空的情况,若为空,则直接挂接,若不为空,则需要 将queue挂接在unsent队列的最后一个tcp_seg 之后,如果
挂接点处相邻两个tcp_seg 所包的数据大小小于最长发送段大小pcb->mss,且相邻的两个段都不是FIN包或SYN包,则 需要将两个段合并为一个段。最后,函数需要调整TCP控制块中的相关字段的值,这点也是我最关心的地方, if ((flags & TCP_SYN) || (flags & TCP_FIN)) { //发送SYN或FIN包被认为数据长度为1 ++len; }
if (flags & TCP_FIN) { // 若为FIN包,则设置flags字段为相应值 pcb->flags |= TF_FIN; }
pcb->snd_lbb += len; // 下一个要被缓冲数据的序号,注意与snd_nxt不同
pcb->snd_buf -= len; // 减小空闲的发送缓冲数,注意这个缓冲区并不是真正存在的
pcb->snd_queuelen = queuelen; // ?发送队列中的pbuf个数
cp_output 函数有个唯一的参数,即某个链接的TCP控制块指针pcb,函数把该控制块的unsent队列上数据段发送出去
或直接发送一个ACK数据段。如果调用该函数时,控制块的flags 字段设置了TF_ACK_NOW标志,则函数必须马上发出去一个带有ACK标志。因此,如果此时unsent队列中无数据发送或者发送窗口此时不允许发送数据,则函数需要发出去一个不?任何数据的ACK数据报。当没有TF_ACK_NOW置位,或者TF_ACK_NOW置位但该ACK能和数据段一起发送出去时,则此时函数会取下unsent 队列上的数据段发送出去(这里先暂时不考虑nagle算法)。发送一个具体的数据段是通过调用函数tcp_output_segment实现的,这个函数主要是填充待发送数据段的TCP头部中的确认序号为pcb->rcv_nxt,通告窗口大小为pcb->rcv_ann_wnd,校验和字段,最后tcp_output_segment将数据包递交给IP层发送
tcp_receive这个函数简单的来说就是操作TCP控制块中的unsent、unacked、ooseq字段,这三个字段用于连接TCP的各种数据段。unsent用于连接还?被发送出去的数据段、unacked用于连接已经发送出去但是还?被确认的数据段、 ooseq 用于连接接收到的无序的数据段。这个三个字段都是tcp_seg 类型的指针,结构体tcp_seg用于描述一个TCP数据段源代码如下: struct tcp_seg {
struct tcp_seg *next; // 用来建立链表的指针 struct pbuf *p; // 数据段pbuf指针
void *dataptr; // 指向TCP段的数据区 u16_t len; // TCP 段的数据长度 struct tcp_hdr *tcphdr; // 指向TCP头部 };
TCP滑动窗口
发送窗口相关字段有:snd_wl1、snd_wl2、snd_nxt、snd_max、lastack、snd_lbb、snd_wnd是否需要提取数据段中的窗口通 告并进行窗口更新,就要看snd_wl1和snd_wl2字段的值与数据段中的seqno和ackno间的大小关系了,如果满足窗口更新条件,则发送窗口被更新;字段snd_wnd保存了窗口大小。lastack字段表示最后一个有效确认的ackno;snd_nxt表示下一个将要发送的数据起始编号;snd_max 表示表示发送了的最大数据序号+1(这貌似和snd_nxt 的值一样,但为什么要用snd_max字段,表示不解),snd_lbb表示在下一个将被缓存且在发送窗口内的数据编号
慢启动算法和拥塞避免算法:每个TCP控制块有两个字段 cwnd和ssthresh,当数据发送时,发送方只能取cwnd和接
收方通告窗口大小中的较小者作为发送上限。当有确认返回时,若此时cwnd 值小于等于ssthresh,则做慢启动算法,即每收到一个确认,cwnd都加1;若此时cwnd值大于ssthresh,则做拥塞避免算法,即每收到一个确认,cwnd都加1/cwnd,这保证了在一个RTT估计内,cwnd增加值不超过1。当cwnd 增加到某个值时拥塞发生,则按照3)所示来更新cwnd和ssthresh的值