这一节正式踏入LWIP协议TCP部分的大门。先来看看它是怎样来描述一个TCP连接的。这个结构非常的复杂,这里的简单描述,也并不全面,并不能清晰说明各个字段的作用,在后续的TCP相关内容中,会对每个用到的字段详加讲解。结构体tcp_pcb
的源代码如下:
struct tcp_pcb {
IP_PCB; // 这是一个宏,描述了连接的IP相关信息,包括双方IP地址,TTL等信息
struct tcp_pcb *next; // 用于连接各个TCP控制块的链表指针
enum tcp_state state; // TCP连接的状态,即为状态图中描述的那些状态
u8_t prio; // 该控制块的优先级
void *callback_arg; //
u16_t local_port; // 本地端口
u16_t remote_port; // 远程端口
u8_t flags; // 附加状态信息,如连接是快速恢复、一个被延迟的 ACK是否被发送等
#define TF_ACK_DELAY (u8_t)0x01U // 这些宏定义是为flags字段
#define TF_ACK_NOW (u8_t)0x02U // 定义的掩码
#define TF_INFR (u8_t)0x04U
#define TF_RESET (u8_t)0x08U
#define TF_CLOSED (u8_t)0x10U
#define TF_GOT_FIN (u8_t)0x20U
#define TF_NODELAY (u8_t)0x40U
// 接收相关字段
u32_t rcv_nxt; // 期望接收的下一个字节,即它向发送端ACK的序号
u16_t rcv_wnd; // 接收窗口
u16_t rcv_ann_wnd; // 通告窗口大小,较低版本中无该字段
u32_t tmr; // 该字段记录该PCB被创建的时刻
u8_t polltmr, pollinterval; // 三个定时器,后续讲解
u16_t rtime; // 重传定时,该值随时间增加,当大于rto的值时则重传发生
u16_t mss; // 最大数据段大小
//RTT估计相关的参数
u32_t rttest; // 估计得到的500ms滴答数
u32_t rtseq; // 用于测试RTT的包的序号
s16_t sa, sv; // RTT估计出的平均值及其时间差
u16_t rto; // 重发超时时间,利用前面的几个值计算出来
u8_t nrtx; // 重发的次数,该字段在数据包多次超时时被使用到,与设置rto的值相关
// 快速重传/恢复相关的参数
u32_t lastack; // 最大的确认序号,该字段不解
u8_t dupacks; // 上面这个序号被重传的次数
// 阻塞控制相关参数
u16_t cwnd; //连接的当前阻塞窗口
u16_t ssthresh; // 慢速启动阈值
// 发送相关字段
u32_t snd_nxt, // 下一个将要发送的字节序号
snd_max, // 最高的发送字节序号
snd_wnd, // 发送窗口
snd_wl1, snd_wl2, // 上次窗口更新时的数据序号和确认序号
snd_lbb; // 发送队列中最后一个字节的序号
u16_t acked; //
u16_t snd_buf; // 可用的发送缓冲字节数
u8_t snd_queuelen; // 可用的发送包数
struct tcp_seg *unsent; // 未发送的数据段队列
struct tcp_seg *unacked; // 发送了未收到确认的数据队列
struct tcp_seg *ooseq; // 接收到序列以外的数据包队列
#if LWIP_CALLBACK_API // 回调函数,部分函数在较低版本没定义
err_t (* sent)(void *arg, struct tcp_pcb *pcb, u16_t space);
err_t (* recv)(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err); // 数据包接收回调函数
err_t (* connected)(void *arg, struct tcp_pcb *pcb, err_t err);
err_t (* accept)(void *arg, struct tcp_pcb *newpcb, err_t err);
err_t (* poll)(void *arg, struct tcp_pcb *pcb);
void (* errf)(void *arg, err_t err);
#endif
// 剩下的所有字段在较低版本中均未定义,用到时再讲解
u32_t keep_idle;
#if LWIP_TCP_KEEPALIVE
u32_t keep_intvl; // 保活定时器,用于检测空闲连接的另一端是否崩溃
u32_t keep_cnt;
#endif
u32_t persist_cnt; // 这两个字段可以使窗口大小信息保持不断流动
u8_t persist_backoff;
u8_t keep_cnt_sent;
};
先说说和接收数据相关的字段rcv_nxt
, rcv_wnd
, rcv_ann_wnd
和数据发送的相关字段snd_nxt
,snd_max
,snd_wnd
,acked
。这些字段都和TCP中有名的滑动窗口协议有密切关系。如下图所示,连接的双方都维持一个窗口用于数据的发送。滑动窗口把整个序列分成三部分:左边的是发送了并且被确认的分组,窗口右边是还没发送的分组,窗口内部是待确认的分组,窗口内部又分成已经发送待确认的,和未发送但将立即发送。TCP是通过正面确认和重传技术来保证可靠性的,滑动窗口可以使发送方在收到前一个分组的确认信息前发送下一个分组,这样提高了网络的带宽利用率。
除了发送窗口外,TCP连接的双方还各自维护了一个接收窗口,如下图,接收方的接收窗口和发送方的发送窗口对比起来看看数据包的交互过程。
在接收方,rev_wnd
表示了自己接收窗口的大小,它可以在给发送方的ACK
包中通告自己的窗口大小值,发送方接收到该值后,就以此设子自己的发送窗口大小值snd_wnd
。发送方的发送窗口内包含的数据发送序列是与ACK
序号密切相关的,即它将ACK
序号以后的snd_wnd
个字节序号包括在窗口内。发送方的acked
字段就表示已经接收到的最高的ACK
序号,snd_nxt
表示发送方即将发送数据的序号,acked
与snd_nxt
之间的数据表示已经被发送但还未接收到ACK,发送方也必须将他们包括在滑动窗内,以方便超时重发,snd_nxt
到发送窗口末端表示还未发送的数据。在接收方,接收处于滑动窗内编号的数据,当某个序号以前的所有序号都已经接收到后,则接收方可以ACK该序号,并将接收窗口向后滑动。发送方也接收到该ACK后,也将自己的发送窗口向后滑动。在接收方,re_nxt
表示希望接收到的下个字节序号,rev_ann_wnd
表示对方通告的窗口大小。
与发送相关的还有cwnd
字段,这就涉及到慢启动的概念了。当发送方接收到接收方的窗口通告后,并不会一下子把窗口内允许的数据全部发送出去,因为这样做的话可能由于中间路由器转发拥塞等原因,造成网络吞吐量不稳定,带宽利用率低等不良现象。发送端的做法是用cwnd
字段保存一个拥塞窗口,发送方取拥塞窗口与通告窗口中的最小值作为发送上限,拥塞窗口初始值一般取1,并在每次收到接收方的一个ACK后加上一个值。关于慢启动还会在后续内容中讲解。