仓库:https://gitee.com/fan-junchuang/1010-dpdk
rwnd、cwnd、swnd、BIF这四个在一个tcp报文中的体现就是wind size、seq、ack这三个字段。
window scale体现在tcp报文中option中。
struct tcp_conn{ /* 发送窗口 */ uint32_t iss; // 初始序列号,SYN 占用 iss uint32_t snd_una; // 最早未被确认的序列号 uint32_t snd_nxt; // 下一个待发送序列号(已被 nsend 分配,可能还在队列里) uint32_t snd_max; // 已经真正发到网线上的下一个序列号(纯 ACK 的 seq 用它) uint32_t snd_wnd; // 对端通告的窗口 // MTU(1500) - etherhdr(8) - iphdr(14) - tcphdr(20) = 1460 uint32_t snd_mss; // 本端发送 MSS 本端发送方向的最大报文段长度 /* 接收窗口 */ uint32_t rcv_nxt; // 期望收到的下一个序列号 uint32_t rcv_wnd; // 本端接收窗口,通告给对端 };首先看我们实现了什么,实现了一个结构体struct tcp_conn,
里面的参数都是和窗口有关的,是实现滑动窗口、拥塞控制、慢启动等的基础。
snd_una、snd_max、snd_nxt将发送队列分成了四个部分。
示意图如下。
已发送已确认 已发送未确认 马上发送 暂时不发送 ──────────────┼────────────┼───────────┼───────────────► │ │ │ snd_una snd_max snd_nxt snd_una + swnd (左沿) (线上沿) (分配沿) (右沿)参数归属: snd_una ── 窗口左沿,由对端 ack 推进 snd_max ── 已上线沿,由 tcp_pkt_send 推进,纯ACK的seq用它 snd_nxt ── 已分配沿,由 nsend/nclose 入队成功后推进 snd_wnd ── 对端 rwnd,报文 window 字段来 swnd=min(cwnd,snd_wnd) cwnd ── 右沿的另一半,本端拥塞窗口(当前缺字段!) rcv_wnd ── 不是这四个区域的参数,是本端通告给对端的窗口 iss ── 整个序列号空间的原点,snd_una/snd_nxt/snd_max 初值 snd_mss ── 区域3→2 每次搬多少字节的上限(分段用,当前未接线)已发送已确认 ——[0, snd_una)
这些字节对端已经 ACK,本端可以彻底丢弃。
snd_una是这个区域的右边界,也是"滑动窗口左沿"。
已发送未确认 ——[snd_una, snd_max)
这些字节已经真正发到网线上,但对端还没 ACK。它们是在途字节(BIF, bytes in flight)
BIF = snd_max - snd_una这个区域是重传定时器(RTO)的作用范围:超时后从snd_una重发到snd_max。
马上发送 ——[snd_max, snd_nxt)
这些字节已经被nsend分配了序列号、拷进了 sendbuf 队列,但还没被tcp_pkt_send取出来发到网线。它们"马上要发"。
暂时不发送 ——[snd_nxt, snd_una + swnd)
这些字节还没被分配序列号(还没nsend)
纯 ACK 报文不消耗序列号(SYN/FIN 各消耗 1 个序号,data 消耗 len 个序号):
**序列号(Sequence Number)**: 自己下一次要发送的起始序号 。
seq=自己已经发送的数据 之后一个的序号
对于携带数据的报文:
**序列号(Sequence Number)**: 自己这次发送数据中的第一个数据的序号 。
seq=本段第一个数据字节的序号
对于携带数据又携带ack的报文 和 上面携带数据的报文中seq定义一样。
**确认号(Acknowledgment Number)**:期望对方下一次从这个起始序号开始发送。
ack=自己期望从对方收到的下一个序号
| 报文字段 | 本端变量 |
|---|---|
| seq | 发送时填snd_nxt(数据段)/snd_max(纯 ACK) |
| ack | 接收时推进snd_una;发送时填rcv_nxt |
| window | 接收时存snd_wnd;发送时填rcv_wnd |
在nsocket时,创建server entry时需要create server tcpcon。
在listen cd中,create clietn entry,也需要create client tcpcon。
在tcp server端nsendto时候,更新snd nxt。
在把数据写入out ring时,更新snd max。
iss就是第二次握手发送的初始seq,常值。
在初始化时,snd_una、snd_max、snd_nxt三个值都相等都是iss。
从接收和发送数据的视角看:
如果接收的是第一次握手SYN,需要更新snd_wnd,rcv_nxt。
seq = iss;
ack = ntohl(tcphdr->sent_seq) + 1;
rcv_nxt = ack;
如果接收到的是纯ack包,需要更新snd_una,snd_wnd。
snd_una = ack;
snd_wnd = ntohs(tcphdr->rx_win);
如果接收的是数据包并且携带ack,需要更新snd_wnd,rcv_nxt,snd_una。
snd_una = ack;
snd_wnd = ntohs(tcphdr->rx_win);
rcv_nxt += payloadlen;
当发送纯ack时,不需要更新。
seq = snd_max;
ack = rcv_nxt;
当发送数据,在nsend中把数据加入sendbuf,需要更新snd_nxt。
seq = snd_nxt;
ack = rcv_nxt;
snd_nxt += len;
当发送的数据加入out ring进行发送后,需要更新snd_max。
snd_max = seq + len;
从参数的视角看:
snd_una:
初始化为iss。
只有接收到ack时才会更新。
snd_una = acknum。
snd_max:
初始化为iss。
在数据发送出去后才会更新。
snd_max = seqnum + len;
snd_nxt:
在数据接入到sendbuf后才会更新。
snd_nxt += len;
rcv_nxt:
只要接收到数据就要更新。
rcv_nxt += len;
snd_wnd:
只有接收到ACK才会更新。
snd_wnd = ntohs(tcphdr->rx_win);
文章参考<零声教育>的C/C++linux服务期高级架构系统教程学习:0voice · GitHub