LwIP 2.1.0 TCP学习摘要
参考:lwIP Wiki | FANDOM powered by Wikia
参考:LwIP源代码文件目录解析 - jrunw的博客 - CSDN博客
参考:LwIP协议栈开发嵌入式网络的三种方法分析 - wangyw - 博客园
参考:LWIP使用经验---变态级(好文章) - yangzhao0001的博客 - CSDN博客
参考:《LwIP协议栈源码详解——TCP/IP协议的实现》TCP坚持与保活定时器 - bian1029的专栏 - CSDN博客
参考:《嵌入式网络那些事Lwip深度剖析与实战演练》
项目Git路径:lwip.git - lwIP - A Lightweight TCPIP stack
1.文件构成
TCP处理依赖于tcp.c、tcp_in.c、tcp_out.c三个文件。
tcp.c:通用的TCP函数定义。
tcp_in.c:负责TCP的输入,通常以这样的顺序被调用。
(ip_input() ->)* tcp_input() -> * tcp_process() -> tcp_receive() (-> application)
tcp_out.c:负责TCP的输出,支持两种方式。一种是入队pcb->unsent的segment的输出,基于另一种是控制帧的直接输出。
2.TCP报文格式
TCP报文3.TCP基本数据结构
控制块
/** the TCP protocol control block */
struct tcp_pcb {
IP_PCB; /** IP Protocol Control Block,包括源IP地址、目的IP地址等 */
struct tcp_pcb *next; /* 用于将控制块组成链表 */
void *callback_arg; /* 指向用户自定义数据,函数回调时被使用 */
enum tcp_state state; /* 连接状态 */
u8_t prio; /* 优先级,用于回收低优先级控制块 */
u16_t local_port; /* 本地端口 */
u16_t remote_port; /* 远端端口 */
tcpflags_t flags; /* 控制块状态、标志字段。refer to TF_XXX */
/* 定时器 */
u8_t polltmr, pollinterval; /* polltmr会周期性递增,当其值超过pollinterval时,poll函数被回调 */
u8_t last_timer; /* 上一次活动时的系统时间 */
u32_t tmr; /* 基础计数器,其他计数器的值都基于该值 */
/* 接受窗口相关 */
u32_t rcv_nxt; /* 下一个期待接受的字节序号 */
tcpwnd_size_t rcv_wnd; /* 当前接受窗口大小 */
tcpwnd_size_t rcv_ann_wnd; /* 将向对方通告的接受窗口大小 */
u32_t rcv_ann_right_edge; /* 上次窗口通告时窗口的右边界值 */
/* SACK ranges to include in ACK packets (entry is invalid if left==right) */
struct tcp_sack_range rcv_sacks[LWIP_TCP_MAX_SACK_NUM];
s16_t rtime; /* 重传定时器,该值大于rto时重传报文 */
u16_t mss; /* 对方可接受的最大报文段大小 */
/* RTT 估计相关 */
u32_t rttest; /* RTT估计时,以500ms为周期递增 */
u32_t rtseq; /* 用于测试RTT的报文段序号 */
s16_t sa, sv; /* RTT估计出的平均值和其时间差 */
s16_t rto; /* 重发超时时间,使用上面几个值计算得来 */
u8_t nrtx; /* 重发次数。多次重发时,将使用该字段设置rto的值 */
/* 快速重传与恢复相关 */
u8_t dupacks; /* 上述最大确认号被重复收到的次数 */
u32_t lastack; /* 接收到的最大确认号 */
/* 阻塞控制相关 */
tcpwnd_size_t cwnd; /* 当前的阻塞窗口大小 */
tcpwnd_size_t ssthresh; /* 拥塞避免算法启动阈值 */
u32_t rto_end; /* first byte following last rto byte */
/* 发送窗口相关 */
u32_t snd_nxt; /* 下一个要发送的数据的序号 */
u32_t snd_wl1, snd_wl2; /* 上次窗口更新时收到的数据序号和确认序号 */
u32_t snd_lbb; /* 下一个被缓冲的应用程序数据的编号 */
tcpwnd_size_t snd_wnd; /* 发送窗口大小 */
tcpwnd_size_t snd_wnd_max; /* 远端通知过的最大发送窗口 */
tcpwnd_size_t snd_buf; /* 可使用的发送缓冲区大小 */
u16_t snd_queuelen; /* 缓冲数据已占用的pbuf个数 */
u16_t unsent_oversize; /* Extra bytes available at the end of the last pbuf in unsent. */
tcpwnd_size_t bytes_acked;
/* These are ordered by sequence number: */
struct tcp_seg *unsent; /* 未发送的报文段队列 */
struct tcp_seg *unacked; /* 发送但未收到确认的报文段队列 */
struct tcp_seg *ooseq; /* 接收到的无序报文段序列 */
struct pbuf *refused_data; /* 指向上一次成功接收但未被应用层取用的数据pbuf */
struct tcp_pcb_listen* listener; /* 处于listen状态的连接 */
tcp_sent_fn sent; /* 数据被成功发送后被调用 */
tcp_recv_fn recv; /* 接收到数据后被调用 */
tcp_connected_fn connected; /* 连接建立后被调用 */
tcp_poll_fn poll; /* 被内核周期性调用 */
tcp_err_fn errf; /* 连接发生错误时调用 */
u32_t ts_lastacksent; /* timestamp of last ack sent */
u32_t ts_recent; /* recent timestamp */
u32_t keep_idle; /* 保活计时器上限,一般为2小时 */
u32_t keep_intvl;
u32_t keep_cnt;
u8_t persist_cnt; /* 坚持定时器计数值 */
u8_t persist_backoff; /* Persist timer back-off */
u8_t persist_probe; /* Number of persist probes */
u8_t keep_cnt_sent; /* 保活报文发送次数 */
u8_t snd_scale;
u8_t rcv_scale;
};
报文段
struct tcp_seg {
struct tcp_seg *next; /* 用于将报文段组织成队列形式 */
struct pbuf *p; /* 指向装在报文段的pbuf */
u16_t len; /* 报文段中数据长度 */
u8_t flags; /* 报文段选项属性 */
#define TF_SEG_OPTS_MSS (u8_t)0x01U /* Include MSS option (only used in SYN segments) */
#define TF_SEG_OPTS_TS (u8_t)0x02U /* Include timestamp option. */
#define TF_SEG_DATA_CHECKSUMMED (u8_t)0x04U /* ALL data (not the header) is checksummed into 'chksum' */
#define TF_SEG_OPTS_WND_SCALE (u8_t)0x08U /* Include WND SCALE option (only used in SYN segments) */
#define TF_SEG_OPTS_SACK_PERM (u8_t)0x10U /* Include SACK Permitted option (only used in SYN segments) */
struct tcp_hdr *tcphdr; /* 指向报文段的TCP首部 */
};
控制块与报文段关系图解
LwIP报文段的缓冲队列全局变量
static u8_t tcp_timer; /** 用于tcp_slowtmr */
static u8_t tcp_timer_ctr; /** 用于tcp_fasttmr */
struct tcp_pcb *tcp_bound_pcbs; /** 连接所有进行了端口绑定,但未发起连接(主动连接)或进入侦听状态(被动连接)的控制块 */
union tcp_listen_pcbs_t tcp_listen_pcbs; /** 连接所有进入侦听状态(被动连接)的控制块 */
struct tcp_pcb *tcp_active_pcbs; /** 连接所有处于其他状态的控制块. */
struct tcp_pcb *tcp_tw_pcbs; /** 连接所有处于TIME-WAIT状态的控制块 */
4.基本操作函数
创建连接
struct tcp_pcb * tcp_new(void);
创建一个MEMP_TCP_PCB但是不加入任何链表。链表的追加在tcp_bind()时候完成。
绑定
err_t tcp_bind(struct tcp_pcb *pcb, const ip_addr_t *ipaddr, u16_t port);
为pcb绑定地址和端口。将pcb链接到tcp_bound_pcbs上。
监听
struct tcp_pcb * tcp_listen_with_backlog_and_err(struct tcp_pcb *pcb, u8_t backlog, err_t *err);
设置state为LISTEN,以便可以accept之后到来的连接请求。原有(输入参数)的pcb会被释放,一个新的、更小的pcb会被返回用来监听连接。
原有pcb会被从tcp_bound_pcbs移除,新的pcb拷贝了多数旧pcb的参数信息,并被追加到tcp_listen_pcbs链表中。
接受连接
void tcp_accept (struct tcp_pcb *pcb, tcp_accept_fn accept);
指定一个回调函数,当监听态变为连接态时被调用。
发起连接
err_t tcp_connect (struct tcp_pcb *pcb, const ip_addr_t *ipaddr, u16_t port, tcp_connected_fn connected);
与远端host进行连接,事实上发送SYN变会成功返回。连接成功的话会回调connected函数,如果连接无法建立则会回调pcb->errf()。
创建SYN的segment,并链在pcb->unsent链表中待发送。改变state为SENT,从tcp_bound_pcbs中移除pcb后追加到tcp_active_pcbs中,
最后调用tcp_output()将待发送的segment发出。
发送数据
err_t tcp_write (struct tcp_pcb *pcb, const void *dataptr, u16_t len, u8_t apiflags);
写入发送数据(但不表示一定会立即送出),为了提高发送效率,TCP会期待更多的数据到来以便一并发送出去。如果需要立即发送,
可先调用tcp_output()将待发送的segment发出,然后再调用tcp_write()。入队数据由dataptr指定,长度为len。
apiflags=TCP_WRITE_FLAG_COPY表示是否需要开辟新的内存用来保存入队数据,如果未指定需要保证未收到远端的ack之前不能改变dataptr数据内容。
apiflags=TCP_WRITE_FLAG_MORE表示还有更多数据在后面,如果设置该位,PSH标志不会被设置。
由于tcp_write()有可能buffer空间不足返回ERR_MEM,可以事先使用tcp_sndbuf()查询输出队列的可用空间大小。
err_t tcp_output(struct tcp_pcb *pcb);
发送pcb->unsent中待发送的数据。
void tcp_sent (struct tcp_pcb *pcb, tcp_sent_fn sent);
指定callback函数用来在收到远端发来的ack时进行回调。
接受数据
void tcp_recv(struct tcp_pcb *pcb, tcp_recv_fn recv);
设定一个接受到数据时进行回调的callback函数。回调发生时,如果callback的pbuf被传入NULL,表明远端连接已关闭。
callback返回ERR_OK or ERR_ABRT时需要手动释放pbuf。
void tcp_recved(struct tcp_pcb *pcb, u16_t len);
应用层在处理完接收到的数据时调用此函数,以触发一个接受窗口已变大的通知。
应用轮循与定时器
void tcp_poll(struct tcp_pcb *pcb, tcp_poll_fn poll, u8_t interval);
指定一个callback函数用来进行应用轮循。可以用来杀死长时间idle的连接,或作为一个等待内存可用的方式。
比如,调用tcp_write()时由于内存不足失败时,应用程序可以使用该轮循再次调用tcp_write()。poll在tcp_slowtmr()中被执行。
void tcp_slowtmr(void);
每500ms被执行一次,实现重传timer。递增保活定时器,并实现保活重试。
管理坚持定时器,并发送探测报文。
移除处于SYN_SENT,且超过SYN重传次数(6)的pcb连接。
移除超过最大重传次数(12)的pcb连接。
移除处于FIN_WAIT_2状态过久(20s)的pcb连接。
移除超过保活定时上限(2小时+75秒*10次)的pcb连接。
移除处于SYN_RCVD过久(20s)的pcb连接。
移除处于LAST_ACK过久(2MSL=120s)的pcb连接。
遍历处于TIME_WAIT状态下,删除超过2MSL的pcb连接。
void tcp_fasttmr(void);
每250ms被执行一次,用来处理此前被应用层拒绝的数据、发送延迟的ACK或未决的FIN。
关闭和退出
err_t tcp_close(struct tcp_pcb *pcb);
关闭连接。如果是Listening pbc或者未完成连接的Connection pbc,pbc会被释放并无法再被引用。
如果是已经建立的连接,连接被关闭并将state置为closing,pbc会被tcp_slowtmr自动释放。
如果函数ERR_MEM返回,应用程序有必要用poll等方式进行重试。
void tcp_abort(struct tcp_pcb *pcb);
向远端发出RST,退出连接。pcb会被释放。
void tcp_err(struct tcp_pcb *pcb, tcp_err_fn err);
指定一个异常发生时回调的callback函数。
5.TCP输出处理
TCP报文段的输出有两种形式,缓存输出和直接输出。
缓存输出
用来传输数据或包含SYN或FIN的报文段。它们被创建为pbuf,并跟着相关tcp_seg入队到pcb的unsent队列中。
tcp_write():创建报文段
tcp_split_unsent_seg():对报文段进行分割
tcp_enqueue_flags():创建SYN-only报文段或FIN-only报文段
tcp_output():发送报文段,Find out what we can send and send it。
直接输出
直接发送的报文段不包含数据而只是进行连接控制。它们会被创建为pbuf并且不会进行入队操作。
tcp_send_empty_ack():发送ACK-only报文段
tcp_rst():发送RST报文段
tcp_keepalive():发送keepalive报文段
tcp_zero_window_probe():发送窗口探查报文
6.TCP输入处理
TCP函数调用流程tcp_in.c的全局变量
static struct tcp_seg inseg; /* 输入报文段 */
static struct tcp_hdr *tcphdr; /* 输入的报文段首部 */
static u16_t tcphdr_optlen;
static u16_t tcphdr_opt1len;
static u8_t *tcphdr_opt2;
static u16_t tcp_optidx;
static u32_t seqno, ackno; /* 报文段TCP首部中的序号字段和确认号字段值 */
static tcpwnd_size_t recv_acked;
static u16_t tcplen; /* TCp报文段长度 */
static u8_t flags; /* 报文段TCP首部中标志字段值 */
static u8_t recv_flags; /* 该变量记录了所有函数对当前报文段的处理结果 */
static struct pbuf *recv_data; /* 指向报文段中的数据pbuf */
struct tcp_pcb *tcp_input_pcb; /* 处理当前报文段的控制块 */
tcp_input
tcp_input函数作为TCP层总的输入函数,会为报文段寻找匹配的TCP控制块,并根据控制块状态的不同调用tcp_timewait_input(当连接处于TIME_WAIT状态)、tcp_listen_input(当连接处于LISTEN状态)或tcp_process处理报文段。tcp_input完成报文向各个控制块的分发,并等待控制块对报文的处理结果。若仍未找到匹配的控制块,则tcp_input会调用tcp_rst发送一个复位报文。
tcp_input函数会等待tcp_process函数执行结束,执行结果(控制块的状态变迁)会被保存在recv_flags中,同时接受结果会保存在recv_data中,被当前报文确认的已发送数据的长度被保存在控制块的acked字段中。tcp_input函数会根据这些值分贝回调用户注册的热女、sent等函数
tcp_receive
tcp_receive函数首先检查报文中携带的确认号是否确认了对垒unacked中的数据,如果是,则释放掉被确认的数据空间,并设置acked字段以便tcp_input回调用户函数;同时,如果报文段中有数据且数据有序,这些数据会被记录在recv_data中,以便用户程序处理;如果控制块的ooseq队列上的报文段因为新的报文段的到来而变得有序,则这些报文段的数据也会被一起连接在recv_data中,在函数退出后由tcp_input递交给应用程序处理;如果新报文段不是有序的,则报文段将被插入到队列ooseq上,该报文段的引用指针将被加1,防止在其他地方被删除。最后,还有很多其他工作也需要在该函数中完成,例如当前确认号包含了对正在进行RTT估计的报文段的确认,则RTT需要被重新计算;如果收到重复的ACK,这可能会在函数中启动快重传算法等。
7.TCP状态迁移
TCP的十一种状态
enum tcp_state {
CLOSED = 0, /* 没有连接 */
LISTEN = 1, /* 服务器进入侦听状态,等待客户端的连接请求 */
SYN_SENT = 2, /* 连接请求已发送,等待确认 */
SYN_RCVD = 3, /* 已收到对方的连接请求 */
ESTABLISHED = 4, /* 连接已建立 */
FIN_WAIT_1 = 5, /* 程序已关闭该连接 */
FIN_WAIT_2 = 6, /* 另一端已接受关闭该连接 */
CLOSE_WAIT = 7, /* 等待程序关闭连接 */
CLOSING = 8, /* 两端同时受到对方的关闭请求 */
LAST_ACK = 9, /* 服务器等待对方接受关闭操作 */
TIME_WAIT = 10 /* 关闭成功,等待网络中可能出现的剩余数据 */
};
状态迁移
状态机主要由tcp_in.c的tcp_process()进行管理。
TCP状态迁移图8.可靠传输
TCP三次握手
三次握手TCP四次挥手
四次挥手关于CLOSING
滑动窗口
当接受到数据后,数据会放在接收窗口中等待上层取用。
rcv_next:表示期望收到的下一个数据字节序号
rcv_wnd:表示接收窗口大小
rcv_ann_wnd:表示将要通告的窗口大小,被用来填充首部窗口大小字段
rcv_ann_right_edge:记录上一次窗口通告时窗口右边界取值
接收窗口例当收到接收方的一个有效ACK后,lastack会相应增加,指向下一个待确认的数据编号。当发送完一个数据后,snd_nxt也会相应增加,指向下一个待发送的数据编号。snd_nxt与lastack之间的差值不能超过snd_wnd的大小。
lastack:记录了被接收方确认的最高序列号
snd_nxt:表示将要发送的下一个数据的起始编号
snd_wnd:表示当前的发送窗口大小,常被设置为接收方通过的接收窗口大小
snd_lbb:记录下一个将被应用程序缓存的数据的起始编号
发送窗口例超时重传与RTT估计
发送端为每一个发送出去的报文设置一个超时定时器,当定时器溢出而报文的确认还没有返回,它就重传该报文段。为了确定合理的超时时间间隔,所以有了RTT(往返时间),代表了某字节数据发送出去到对应确认返回其间的时间间隔。
TCP控制块内部的多个字段与超时重传有关。
rtime:表示重传定时器,该值每500ms被内核加1,当值超过rto时,报文重传。
rttest:用于对某个报文段计时,测算该报文段在两台主机间的往返时间。TCP会根据往返时间的估计值动态设置各个报文段的超时时间rto。
rtseq:当前正在进行往返时间估计的报文段序号。
sa、sv:用于超时时间rto的计算。
rto:表示超时时间间隔。
nrtx:报文段被重传的次数。
慢启动与拥塞避免
每个TCP控制块有两个字段cwnd和ssthresh,当发送数据时,发送方只能取cwnd和接收方通告窗口大小中的较小者作为发送上限。当有确认返回时,若此时cwnd值小于等于ssthresh,则做慢启动算法,及每收到一个确认,cwnd都加1(指数式增加:1、2、4、8...);若此时cwnd值大于ssthresh,则做拥塞避免算法,即每收到一个确认,cwnd都加1/cwnd,这保证了在一个RTT估计内,cwnd增加值不超过1。当cwnd增加到某个值时拥塞发生,则ssthresh被设置为当前有效发送窗口的一半(但至少为2个报文段),cwnd被设置为1个报文段大小。
快速重传与快速恢复
如果发送方一连接收到3个或3个以上的重复ACK,发送方就重传丢失的报文段,而无需等待超时定时器溢出,这就是快速重传。
快速重传发生时,阻塞窗口直接被设置为有效窗口的一半(或更大),如果在这样的情况下仍然收到重复的ACK,则每个ACK都将阻塞窗口增加一个报文段。这说明收发两端之间仍然有流动的数据,所以没有执行拥塞避免。
当控制块退出快速重传模式,阻塞窗口不会像超时那样被置为1,相反会设为ssthresh,直接进入拥塞避免阶段,此为快速恢复。
糊涂窗口与避免
当TCP接收方通告了一个小窗口,而发送方立即发送数据填充该窗口时,TCP数据流中会充斥很多小长度的报文段,IP首部和TCP首部占了大量空间,而真正的有效数据很少。这就是糊涂窗口综合征SWS(Silly Window Syndrome)。
SWS可能由TCP连接双方任一方引起,所以接收方和发送方在任一方采取措施都可以消除SWS的发生。
接收方的解决办法是不通告小窗口,比如直到窗口可以增加到一个最大报文段或者可以增加接收缓存空间的一半时,才向发送方通告。
接收方也可以使用推迟确认(delayed acknowledge)的方法避免SWS,做法是当接收窗口未达到满足要求的通告大小时,TCP推迟确认的发送。推迟确认需要把握好推迟时间,TCP标准中规定,最多只能推迟500ms。同时为了不影响发送端RTT的计算,接收方最好保证每隔一个报文进行一次确认。
LwIP作为接收方时,采用推迟确认的方法避免SWS发生。控制块flags字段的TF_ACK_DELAY位表示当前有ACK被延迟。
发送方避免SWS的措施是推迟小报文段的发送,TCP尽量组织后续数据成为一个大的报文段发送。至于推迟多久,通常发送方采用一个自适应的方法,利用确认的到来去触发其余分组的传输。应用程序产生的新数据放入输出缓冲区并不立即发送,而是等到这些数据能够填充一个最大长度的报文段之后,才把缓冲区中的数据组织成一个报文段发送出去,这就是Nagle算法。
零窗口探查
为了防止接收方非0窗口通知丢失所引起的死锁情况,发送方使用一个坚持定时器(persist timer)来周期性的向接收方查询,以便发现窗口是否已经增大,这些从发送方发出的报文段成为窗口探查(window probe)报文。
控制块中的persist_cnt和persist_backoff与坚持定时器有关。persist_cnt表示坚持定时器计数,当超过某值时发送窗口探查报文。persist_backoff表示坚持定时器是否被启动(是否大于0)以及已经发送的探查报文个数。
保活机制
服务器端为了知道客户端主机的运行状况,从而合理分配资源。如果某条连接在两个小时之内没有任何动作,则服务器向客户端发送一个保活探查报文。
TCP控制块中,keep_idle、keep_interval、keep_cnt、keep_cnt_sent四个字段与保活有关。keep_idle记录多久后进行保活探查,一一般为2个小时。keep_cnt_sent表示已经发送的保活探查报文个数。keep_interval和keep_cnt分别用来记录用户自定义的保活时间间隔和保活最大报文数。
定时器
TCP为每条连接总共建立7个定时器
1)连接建立定时器在服务器响应一条SYN握手并试图建立一条新连接时启动,如果在SYN_RCVD状态下75秒仍未收到响应,连接将终止。
2)重传定时器
3)数据组装定时器在ooseq不为空时有效,该队列都是失序报文,该连接长时间没有数据交互,则报文需要删除
4)坚持定时器
5)保活定时器
6)FIN_WAIT_2定时器,连接从FIN_WAIT_1迁移到FIN_WAIT_2时启动,定时器超时时关闭连接。
7)TIME_WAIT定时器,即2MSL定时器