网络传输管理之TCP协议
TCP(Transmission Control Protocol)与UDP(User Datagram Protocol)的区别相当大,它充分实现了数据传输时各种控制功能,可以进行丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制,而这些在UDP中都没有。此外,TCP作为一种面向有连接的协议,只有在确认通信对端存在时才会发送数据,从而可以控制通信流量的浪费。根据TCP的这些机制,在IP这种无连接的网络上也能够实现高可靠的通信。
为了通过IP数据报实现可靠性传输,需要考虑很多问题,例如数据的破坏、丢包、重复以及分片顺序混乱等问题。TCP通过校验和、序列号、确认应答、重发控制、连接管理、窗口控制等机制实现可靠性传输。
1.1 TCP协议简介
1.正面确认与超时重传
在TCP中,当发送端的数据到达接收主机时,接收端主机会返回一个已收到消息的通知,这个消息叫做确认应答(ACK)。TCP通过肯定的确认应答实现可靠的数据传输,当发送端将数据发出之后会等待对端的确认应答,如果有确认应答说明数据已经成功到达对端,反之则说明数据丢失的可能性很大。在一定时间内没有等到确认应答,发送端就可以认为数据已经丢失并进行重发,由此即使产生了丢包仍能保证数据能够到达对端,实现可靠传输。
未收到确认应答并不意味着数据一定丢失,也有可能是数据对方已经收到,只是返回的确认应答在途中丢失,这种情况也会导致发送到因没有收到确认应答而认为数据没有到达目的地,从而进行重新发送。也有可能因为一些其他原因导致确认应答延迟到达,在源主机重发数据以后才到达的情况也屡见不鲜。此时,源发送主机只要按照机制重发数据即可,但目标主机会反复收到相同的数据,为了对上层应用提供可靠的传输必须放弃重复的数据。为此,就必须引入一种机制,它能够识别是否已经接收数据,又能够判断是否需要接收。
上述这些确认应答处理、重传控制以及重复控制等功能都可以通过序列号实现。序列号是按顺序给发送数据的每一个字节都标上号码的编号。接收端查询接收数据TCP首部中的序列号和数据的长度,将自己下一步应该接收的序号作为确认应答返送回去,这样通过序列号和确认应答号,TCP可以实现可靠传输,整个过程如下图所示:
前面说到发送端在一定时间内没有等到确认应答就会进行数据重发,在重发数据之前等待确认应答到来的特定时间间隔就叫重发超时。那么这个重发超时的具体时间长度又是如何确定的呢?
最理想的是,找到一个最小时间,它能保证确认应答一定能在这个时间内返回,然而这个时间长短随着数据包途经的网络环境的不同而有所变化,例如跟网络的距离、带宽、拥堵程度等都有关系。TCP要求不论处在何种网络环境下都要提供高性能通信,并且不论网络拥堵情况发生何种变化,都必须保持这一特性。为此,它在每次发包时都会计算往返时间及其偏差(往返时间RTT估计)。将这个往返时间和偏差相加,重发超时时间就是比这个总和要稍大一点的值。往返时间的计算与重发超时的时间推移过程如下图所示:
1.2 TCP协议实现
1.TCP报文格式
TCP协议有着自己的数据报组织格式,这里把TCP的数据包称为报文段(Segment),TCP报文段封装在IP数据报中发送。TCP报文段由TCP首部和TCP数据区组成,首部区域包含了连接建立与断开、数据确认、窗口大小通告、数据发送相关的所有标志与控制信息,TCP报文结构如下图所示:
TCP首部相比UDP首部要复杂得多,TCP中没有表示包长度和数据长度的字段,可由IP层获知TCP的包长再由TCP的包长可知数据的长度。TCP首部的大小为20~60字节,在没有任何选项的情况下,首部大小为20字节,与不含选项字段的IP报首部大小相同,TCP数据部分可以为空(比如建立或断开连接时)。
与UDP报文相同,源端口号和目的端口号两个字段用来标识发送端和接收端应用进程分别绑定的端口号。32位序号字段标识了从TCP发送端到TCP接收端的数据字节编号,它的值为当前报文段中第一个数据的字节序号。32位确认序号只有ACK标志置1时才有效,它包含了本机所期望收到的下一个数据序号(即上次已成功收到数据字节序号加1),确认常常和反向数据一起捎带发送。序列号与确认应答号共同为TCP的正面确认、超时重传、有序重组等可靠通信提供支持。
4位首部长度指出了TCP首部的长度,以4字节为单位,若没有任何选项字段则首部长度为5(5*4 = 20字节)。接下来的6bit保留字段暂未使用,为将来保留。再接下来是6个标志比特,它们告诉了接收端应该如何解释报文的内容,比如一些报文段携带了确认信息、一些报文段携带了紧急数据、一些报文段包含建立或关闭连接的请求等,6个标志位的意义如下表示:
在TCP发送一个报文时,可在窗口字段中填写相应值以通知对方自己的可用缓冲区大小(以字节为单位),报文接收方需要根据这个值来调整发送窗口的大小。窗口字段是实现流量控制的关键字段,当接收方向发送方通知一个大小为0的窗口时,将完全阻止发送方的数据发送。
16位校验和字段的计算和上一章中UDP校验和计算过程与原理都相同,在UDP首部中校验和的计算是可选的,但在TCP中校验和的计算是必须的、强制的。TCP中校验和包含了伪首部、TCP首部和TCP数据区三部分,伪首部的概念与UDP中完全一样,只是伪首部中的协议字段值为6,与TCP相对应。
16位的紧急指针只有当紧急标志位URG置位时才有效,此时报文中包含紧急数据,紧急数据始终放到报文段数据开始的地方,而紧急指针定义出了紧急数据在数据区中的结束处,用这个值加上序号字段值就得到了最后一个紧急数据的序号。URG位置1的报文段将告诉接收方:这里面的数据是紧急的,你可以优先直接读取,不必把它们放在接收缓冲里面(即该报文段不使用普通的数据流形式被处理)。
TCP首部可包含0个或多个选项信息,选项总长度可达40字节,用来把附加信息传递给对方。每条TCP选项由三部分组成:1字节的选项类型 + 1字节的选项总长度 + 选项数据,具有代表性的选项如下表所示:
其中类型代码为2的选项是最大报文段长度(MSS),每个连接通常都在通信的第一个报文段(包含SYN标志的连接握手报文)中指明这个选项,用来向对方指明自己所能接受的最大报文段,如果没有指明则使用默认MSS为536,前面提到的客户端与服务器协商确定MSS的功能就是通过该选项实现的。
类型代码为3的选项是窗口扩大因子选项,可以让通信双方声明更大的窗口,首部中的窗口字段长度16bit,即接收窗口最大值为65535字节,在许多高速场合下,这样的窗口还是太小,会影响发送端的发送速度。使用该选项可以向对方通告更大的窗口,此时通告窗口大小值(假设为N)为首部中窗口大小字段值(假设为W)乘以2的窗口扩大因子值(假设为A)次幂(即N = W * 2^A)。
2.TCP数据报描述
在LWIP中用于描述TCP首部的数据结构如下:
// rt-thread\components\net\lwip-1.4.1\src\include\lwip\tcp_impl.h /* Fields are (of course) in network byte order. * Some fields are converted to host byte order in tcp_input(). */ PACK_STRUCT_BEGIN struct tcp_hdr { PACK_STRUCT_FIELD(u16_t src); //源端口号 PACK_STRUCT_FIELD(u16_t dest); //目的端口号 PACK_STRUCT_FIELD(u32_t seqno); //序号 PACK_STRUCT_FIELD(u32_t ackno); //确认号 PACK_STRUCT_FIELD(u16_t _hdrlen_rsvd_flags); //首部长度、6位保留位以及6位标志位 PACK_STRUCT_FIELD(u16_t wnd); //窗口大小 PACK_STRUCT_FIELD(u16_t chksum); //校验和 PACK_STRUCT_FIELD(u16_t urgp); //紧急指针 } PACK_STRUCT_STRUCT; PACK_STRUCT_END #define TCP_FIN 0x01U #define TCP_SYN 0x02U #define TCP_RST 0x04U #define TCP_PSH 0x08U #define TCP_ACK 0x10U #define TCP_URG 0x20U #define TCP_ECE 0x40U #define TCP_CWR 0x80U #define TCPH_HDRLEN(phdr) (ntohs((phdr)->_hdrlen_rsvd_flags) >> 12) #define TCPH_FLAGS(phdr) (ntohs((phdr)->_hdrlen_rsvd_flags) & TCP_FLAGS) #define TCPH_HDRLEN_SET(phdr, len) (phdr)->_hdrlen_rsvd_flags = htons(((len) << 12) | TCPH_FLAGS(phdr)) #define TCPH_FLAGS_SET(phdr, flags) (phdr)->_hdrlen_rsvd_flags = (((phdr)->_hdrlen_rsvd_flags & PP_HTONS((u16_t)(~(u16_t)(TCP_FLAGS)))) | htons(flags)) #define TCPH_HDRLEN_FLAGS_SET(phdr, len, flags) (phdr)->_hdrlen_rsvd_flags = htons(((len) << 12) | (flags)) #define TCPH_SET_FLAG(phdr, flags ) (phdr)->_hdrlen_rsvd_flags = ((phdr)->_hdrlen_rsvd_flags | htons(flags)) #define TCPH_UNSET_FLAG(phdr, flags) (phdr)->_hdrlen_rsvd_flags = htons(ntohs((phdr)->_hdrlen_rsvd_flags) | (TCPH_FLAGS(phdr) & ~(flags)) ) #define TCP_TCPLEN(seg) ((seg)->len + ((TCPH_FLAGS((seg)->tcphdr) & (TCP_FIN | TCP_SYN)) != 0)) |
TCP首部中的各个标志位以宏定义的形式表示,同时定义了操作TCP首部各字段的宏定义。
与UDP的内容相同,在TCP实现中也专门使用一个数据结构来描述一个连接,把这个数据结构称为TCP控制块或传输控制块。TCP控制块中包含了双方实现基本通信所需要的信息,如发送窗口、接收窗口、数据缓冲区等,也包含了所有与该连接性能保障相关的字段,如定时器、拥塞控制、滑动窗口控制等。TCP协议实现的本质就是对TCP控制块中各个字段的操作:在接收到TCP报文段时,在所有控制块中查找,以得到和报文目的地相匹配的控制块,并调用控制块上注册的各个函数对报文进行处理;TCP内核维护了一些周期性的定时事件,在定时处理函数中会对所有控制块进行处理,例如把某些控制块中的超时报文段进行重传,把某些控制块中的失序报文段删除。TCP控制块是整个TCP协议的核心,也是整个内核中最大的数据结构,在LwIP中用于描述TCP控制块的数据结构如下:
// rt-thread\components\net\lwip-1.4.1\src\include\lwip\tcp.h /* the TCP protocol control block */ struct tcp_pcb { /** common PCB members */ IP_PCB; /** protocol specific PCB members */ TCP_PCB_COMMON(struct tcp_pcb); /* ports are in host byte order */ u16_t remote_port;
u8_t flags; #define TF_ACK_DELAY ((u8_t)0x01U) /* Delayed ACK. */ #define TF_ACK_NOW ((u8_t)0x02U) /* Immediate ACK. */ #define TF_INFR ((u8_t)0x04U) /* In fast recovery. */ #define TF_TIMESTAMP ((u8_t)0x08U) /* Timestamp option enabled */ #define TF_RXCLOSED ((u8_t)0x10U) /* rx closed by tcp_shutdown */ #define TF_FIN ((u8_t)0x20U) /* Connection was closed locally (FIN segment enqueued). */ #define TF_NODELAY ((u8_t)0x40U) /* Disable Nagle algorithm */ #define TF_NAGLEMEMERR ((u8_t)0x80U) /* nagle enabled, memerr, try to output to prevent delayed ACK to happen */ /* the rest of the fields are in host byte order as we have to do some math with them */ /* Timers */ u8_t polltmr, pollinterval; u8_t last_timer; u32_t tmr; /* receiver variables */ u32_t rcv_nxt; /* next seqno expected */ u16_t rcv_wnd; /* receiver window available */ u16_t rcv_ann_wnd; /* receiver window to announce */ u32_t rcv_ann_right_edge; /* announced right edge of window */ /* Retransmission timer. */ s16_t rtime; u16_t mss; /* maximum segment size */ /* RTT (round trip time) estimation variables */ u32_t rttest; /* RTT estimate in 500ms ticks */ u32_t rtseq; /* sequence number being timed */ s16_t sa, sv; /* @todo document this */ s16_t rto; /* retransmission time-out */ u8_t nrtx; /* number of retransmissions */ /* fast retransmit/recovery */ u8_t dupacks; u32_t lastack; /* Highest acknowledged seqno. */ /* congestion avoidance/control variables */ u16_t cwnd; u16_t ssthresh; /* sender variables */ u32_t snd_nxt; /* next new seqno to be sent */ u32_t snd_wl1, snd_wl2; /* Sequence and acknowledgement numbers of last window update. */ u32_t snd_lbb; /* Sequence number of next byte to be buffered. */ u16_t snd_wnd; /* sender window */ u16_t snd_wnd_max; /* the maximum sender window announced by the remote host */ u16_t acked; u16_t snd_buf; /* Available buffer space for sending (in bytes). */ #define TCP_SNDQUEUELEN_OVERFLOW (0xffffU-3) u16_t snd_queuelen; /* Available buffer space for sending (in tcp_segs). */ /* These are ordered by sequence number: */ struct tcp_seg *unsent; /* Unsent (queued) segments. */ struct tcp_seg *unacked; /* Sent but unacknowledged segments. */ struct tcp_seg *ooseq; /* Received out of sequence segments. */ struct pbuf *refused_data; /* Data previously received but not yet taken by upper layer */ /* Function to be called when more send buffer space is available. */ tcp_sent_fn sent; /* Function to be called when (in-sequence) data has arrived. */ tcp_recv_fn recv; /* Function to be called when a connection has been set up. */ tcp_connected_fn connected; /* Function which is called periodically. */ tcp_poll_fn poll; /* Function to be called whenever a fatal error occurs. */ tcp_err_fn errf; /* idle time before KEEPALIVE is sent */ u32_t keep_idle; /* Persist timer counter */ u8_t persist_cnt; /* Persist timer back-off */ u8_t persist_backoff; /* KEEPALIVE counter */ u8_t keep_cnt_sent; }; struct tcp_pcb_listen { /* Common members of all PCB types */ IP_PCB; /* Protocol specific PCB members */ TCP_PCB_COMMON(struct tcp_pcb_listen); }; /** * members common to struct tcp_pcb and struct tcp_listen_pcb */ #define TCP_PCB_COMMON(type) \ type *next; /* for the linked list */ \ void *callback_arg; \ /* the accept callback for listen- and normal pcbs, if LWIP_CALLBACK_API */ \ DEF_ACCEPT_CALLBACK \ enum tcp_state state; /* TCP state */ \ u8_t prio; \ /* ports are in host byte order */ \ u16_t local_port #define DEF_ACCEPT_CALLBACK tcp_accept_fn accept; 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 }; /* This structure represents a TCP segment on the unsent, unacked and ooseq queues */ struct tcp_seg { struct tcp_seg *next; /* used when putting segements on a queue */ struct pbuf *p; /* buffer containing data + TCP header */ u16_t len; /* the TCP length of this segment */ u8_t flags; #define TF_SEG_OPTS_MSS (u8_t)0x01U /* Include MSS option. */ #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' */ struct tcp_hdr *tcphdr; /* the TCP header */ }; /** Function prototype for tcp accept callback functions. Called when a new * connection can be accepted on a listening pcb. * @param arg Additional argument to pass to the callback function (@see tcp_arg()) * @param newpcb The new connection pcb * @param err An error code if there has been an error accepting. * Only return ERR_ABRT if you have called tcp_abort from within the * callback function! */ typedef err_t (*tcp_accept_fn)(void *arg, struct tcp_pcb *newpcb, err_t err); /** Function prototype for tcp receive callback functions. Called when data has * been received. * @param arg Additional argument to pass to the callback function (@see tcp_arg()) * @param tpcb The connection pcb which received data * @param p The received data (or NULL when the connection has been closed!) * @param err An error code if there has been an error receiving * Only return ERR_ABRT if you have called tcp_abort from within the * callback function! */ typedef err_t (*tcp_recv_fn)(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err); /** Function prototype for tcp sent callback functions. Called when sent data has * been acknowledged by the remote side. Use it to free corresponding resources. * This also means that the pcb has now space available to send new data. * @param arg Additional argument to pass to the callback function (@see tcp_arg()) * @param tpcb The connection pcb for which data has been acknowledged * @param len The amount of bytes acknowledged * @return ERR_OK: try to send some data by calling tcp_output * Only return ERR_ABRT if you have called tcp_abort from within the * callback function! */ typedef err_t (*tcp_sent_fn)(void *arg, struct tcp_pcb *tpcb, u16_t len); /** Function prototype for tcp poll callback functions. Called periodically as * specified by @see tcp_poll. * @param arg Additional argument to pass to the callback function (@see tcp_arg()) * @param tpcb tcp pcb * @return ERR_OK: try to send some data by calling tcp_output * Only return ERR_ABRT if you have called tcp_abort from within the * callback function! */ typedef err_t (*tcp_poll_fn)(void *arg, struct tcp_pcb *tpcb); /** Function prototype for tcp error callback functions. Called when the pcb * receives a RST or is unexpectedly closed for any other reason. * @note The corresponding pcb is already freed when this callback is called! * @param arg Additional argument to pass to the callback function (@see tcp_arg()) * @param err Error code to indicate why the pcb has been closed * ERR_ABRT: aborted through tcp_abort or by a TCP timer * ERR_RST: the connection was reset by the remote host */ typedef void (*tcp_err_fn)(void *arg, err_t err); /** Function prototype for tcp connected callback functions. Called when a pcb * is connected to the remote side after initiating a connection attempt by * calling tcp_connect(). * @param arg Additional argument to pass to the callback function (@see tcp_arg()) * @param tpcb The connection pcb which is connected * @param err An unused error code, always ERR_OK currently ;-) TODO! * Only return ERR_ABRT if you have called tcp_abort from within the * callback function! * @note When a connection attempt fails, the error callback is currently called! */ typedef err_t (*tcp_connected_fn)(void *arg, struct tcp_pcb *tpcb, err_t err); /* The TCP PCB lists. */ /** List of all TCP PCBs bound but not yet (connected || listening) */ struct tcp_pcb *tcp_bound_pcbs; /** List of all TCP PCBs in LISTEN state */ union tcp_listen_pcbs_t tcp_listen_pcbs; /** List of all TCP PCBs that are in a state in which * they accept or send data. */ struct tcp_pcb *tcp_active_pcbs; /** List of all TCP PCBs in TIME-WAIT state */ struct tcp_pcb *tcp_tw_pcbs; |
除了定义tcp_pcb,还定义了tcp_pcb_listen,后者主要是用来描述处于LISTEN状态的连接,处于LISTEN状态的连接只记录本地端口信息,不记录任何远程端口信息,一般只用于在服务器端打开某个端口为客户端服务。处于LISTEN状态的控制块不会对应于任何一条有效连接,它会进行数据发送、连接握手之类的工作,因此描述LISTEN状态的控制块结构体比tcp_pcb相比更小,使用它可以节省内存空间。
TCP控制块中维护了三个缓冲队列,unsent、unacked、ooseq三个字段分别为队列的首指针,unsent用于连接还未被发送出去的报文段,unacked用于连接已经发送出去但还未被确认的报文段,ooseq用于连接接收到的无序报文段,这三个缓冲队列简单的实现了对连接的所有报文段的管理。每个报文段用结构体tcp_seg来描述,并以链表形式组织成队列,tcp_seg报文段不仅包含指向装载报文段的指针pbuf,还包含指向报文段中的TCP首部的指针tcp_hdr,报文段缓冲队列的组织关系如下图所示:
我对此的理解就是,一个TCP控制块tcb_pcb对应了一段连接,unsent、unacked、ooseq对应了一段连接中的三种报文段,由于报文段可能很长,所以用多个tcp_seg组成的单链表保存起来。
为了组织和描述系统内的所有TCP控制块,内核定义了四条链表来连接处于不同状态下的控制块,TCP操作过程通常都包括对链表上控制块的查找。定义四条链表的代码在上面已给出:tcp_bound_pcbs链表用来连接新创建的且绑定了本地端口的控制块,可以认为此时的控制块处于CLOSED状态;tcp_listen_pcbs链表用来连接处于LISTEN状态的控制块,该状态下用结构体tcp_pcb_listen来描述一个本地连接;tcp_tw_pcbs链表用来连接处于TIME_WAIT状态的控制块;tcp_active_pcbs用于连接处于TCP转换图中其它所有状态的控制块,上图展示的就是该链表上的控制块。
3.TCP状态机
前面介绍TCP连接管理时谈到TCP建立连接需要“三次握手”过程:首先客户端发送SYN置1的连接请求报文后,从CLOSED状态迁移到SYN_SENT状态;服务器收到客户端的连接请求报文后返回SYN与ACK都置1的应答报文,并从LISTEN状态迁移到SYN_RCVD状态;客户端收到服务器的SYN应答报文后会再次返回ACK置1的应答报文,当服务器收到该应答报文后双方的连接就建立起来了,此时双方都迁移到ESTABLISHED状态。
TCP断开连接需要“四次握手”过程:首先客户端向服务器发送FIN置1的报文后,从ESTABLISHED状态迁移到FIN_WAIT_1状态;服务器收到FIN报文后返回ACK置1的应答报文,并从ESTABLISHED状态迁移到CLOSE_WAIT状态,客户端收到来自服务器的ACK报文后从FIN_WAIT_1状态迁移到FIN_WAIT_2状态;服务器向上层通告该断开操作并向客户端发送一个FIN置1的报文段,从CLOSE_WAIT状态迁移到LAST_ACK状态;客户端收到来自服务器的FIN报文后返回ACK置1的应答报文,并从FIN_WAIT_2状态迁移到TIME_WAIT状态,服务器收到来自客户端的ACK报文后从LAST_ACK状态迁移到CLOSED状态。
在理解了TCP连接建立与断开流程后,再来看TCP状态迁移图就相对容易了,TCP为每个连接定义了11种状态(上面已给出实现代码),下面给出状态转换图如下:
虽然上面的状态转换图看起来很复杂,但并不是每个连接都会出现图中的所有转换路径,图中有两条最经典的状态转换路径,而TCP绝大部分的状态转换都发生在这两条路径上:第一条路径描述了客户端申请建立连接与断开连接的整个过程,如图中虚线所示;第二条路径描述了服务器接受来自客户端的建立连接请求与断开连接请求的整个过程,如图中粗实线所示。配合前面介绍的建立连接的“三次握手”过程与断开连接的“四次握手”过程,应该更容易理解TCP连接的状态迁移过程。
实现TCP状态迁移的状态机函数实现代码如下:
// rt-thread\components\net\lwip-1.4.1\src\core\tcp_in.c /** * Implements the TCP state machine. Called by tcp_input. In some * states tcp_receive() is called to receive data. The tcp_seg * argument will be freed by the caller (tcp_input()) unless the * recv_data pointer in the pcb is set. * @param pcb the tcp_pcb for which a segment arrived * @note the segment which arrived is saved in global variables, therefore only the pcb * involved is passed as a parameter to this function */ static err_t tcp_process(struct tcp_pcb *pcb) { struct tcp_seg *rseg; u8_t acceptable = 0; err_t err; err = ERR_OK; /* Process incoming RST segments. */ if (flags & TCP_RST) { /* First, determine if the reset is acceptable. */ if (pcb->state == SYN_SENT) { if (ackno == pcb->snd_nxt) { acceptable = 1; } } else { if (TCP_SEQ_BETWEEN(seqno, pcb->rcv_nxt, pcb->rcv_nxt+pcb->rcv_wnd)) { acceptable = 1; } } if (acceptable) { recv_flags |= TF_RESET; pcb->flags &= ~TF_ACK_DELAY; return ERR_RST; } else { return ERR_OK; } } if ((flags & TCP_SYN) && (pcb->state != SYN_SENT && pcb->state != SYN_RCVD)) { /* Cope with new connection attempt after remote end crashed */ tcp_ack_now(pcb); return ERR_OK; }
if ((pcb->flags & TF_RXCLOSED) == 0) { /* Update the PCB (in)activity timer unless rx is closed (see tcp_shutdown) */ pcb->tmr = tcp_ticks; } pcb->keep_cnt_sent = 0; tcp_parseopt(pcb); /* Do different things depending on the TCP state. */ switch (pcb->state) { case SYN_SENT: /* received SYN ACK with expected sequence number? */ if ((flags & TCP_ACK) && (flags & TCP_SYN) && ackno == ntohl(pcb->unacked->tcphdr->seqno) + 1) { pcb->snd_buf++; pcb->rcv_nxt = seqno + 1; pcb->rcv_ann_right_edge = pcb->rcv_nxt; pcb->lastack = ackno; pcb->snd_wnd = tcphdr->wnd; pcb->snd_wnd_max = tcphdr->wnd; pcb->snd_wl1 = seqno - 1; /* initialise to seqno - 1 to force window update */ pcb->state = ESTABLISHED; #if TCP_CALCULATE_EFF_SEND_MSS pcb->mss = tcp_eff_send_mss(pcb->mss, &(pcb->remote_ip)); #endif /* TCP_CALCULATE_EFF_SEND_MSS */ /* Set ssthresh again after changing pcb->mss (already set in tcp_connect * but for the default value of pcb->mss) */ pcb->ssthresh = pcb->mss * 10; pcb->cwnd = ((pcb->cwnd == 1) ? (pcb->mss * 2) : pcb->mss); --pcb->snd_queuelen; rseg = pcb->unacked; pcb->unacked = rseg->next; tcp_seg_free(rseg); /* If there's nothing left to acknowledge, stop the retransmit timer, otherwise reset it to start again */ if(pcb->unacked == NULL) pcb->rtime = -1; else { pcb->rtime = 0; pcb->nrtx = 0; } /* Call the user specified function to call when sucessfully * connected. */ TCP_EVENT_CONNECTED(pcb, ERR_OK, err); if (err == ERR_ABRT) { return ERR_ABRT; } tcp_ack_now(pcb); } /* received ACK? possibly a half-open connection */ else if (flags & TCP_ACK) { /* send a RST to bring the other side in a non-synchronized state. */ tcp_rst(ackno, seqno + tcplen, ip_current_dest_addr(), ip_current_src_addr(), tcphdr->dest, tcphdr->src); } break; case SYN_RCVD: if (flags & TCP_ACK) { /* expected ACK number? */ if (TCP_SEQ_BETWEEN(ackno, pcb->lastack+1, pcb->snd_nxt)) { u16_t old_cwnd; pcb->state = ESTABLISHED; /* Call the accept function. */ TCP_EVENT_ACCEPT(pcb, ERR_OK, err); if (err != ERR_OK) { /* If the accept function returns with an error, we abort * the connection. */ /* Already aborted? */ if (err != ERR_ABRT) { tcp_abort(pcb); } return ERR_ABRT; } old_cwnd = pcb->cwnd; /* If there was any data contained within this ACK, * we'd better pass it on to the application as well. */ tcp_receive(pcb); /* Prevent ACK for SYN to generate a sent event */ if (pcb->acked != 0) { pcb->acked--; } pcb->cwnd = ((old_cwnd == 1) ? (pcb->mss * 2) : pcb->mss); if (recv_flags & TF_GOT_FIN) { tcp_ack_now(pcb); pcb->state = CLOSE_WAIT; } } else { /* incorrect ACK number, send RST */ tcp_rst(ackno, seqno + tcplen, ip_current_dest_addr(), ip_current_src_addr(), tcphdr->dest, tcphdr->src); } } else if ((flags & TCP_SYN) && (seqno == pcb->rcv_nxt - 1)) { /* Looks like another copy of the SYN - retransmit our SYN-ACK */ tcp_rexmit(pcb); } break; case CLOSE_WAIT: /* FALLTHROUGH */ case ESTABLISHED: tcp_receive(pcb); if (recv_flags & TF_GOT_FIN) { /* passive close */ tcp_ack_now(pcb); pcb->state = CLOSE_WAIT; } break; case FIN_WAIT_1: tcp_receive(pcb); if (recv_flags & TF_GOT_FIN) { if ((flags & TCP_ACK) && (ackno == pcb->snd_nxt)) { tcp_ack_now(pcb); tcp_pcb_purge(pcb); TCP_RMV_ACTIVE(pcb); pcb->state = TIME_WAIT; TCP_REG(&tcp_tw_pcbs, pcb); } else { tcp_ack_now(pcb); pcb->state = CLOSING; } } else if ((flags & TCP_ACK) && (ackno == pcb->snd_nxt)) { pcb->state = FIN_WAIT_2; } break; case FIN_WAIT_2: tcp_receive(pcb); if (recv_flags & TF_GOT_FIN) { tcp_ack_now(pcb); tcp_pcb_purge(pcb); TCP_RMV_ACTIVE(pcb); pcb->state = TIME_WAIT; TCP_REG(&tcp_tw_pcbs, pcb); } break; case CLOSING: tcp_receive(pcb); if (flags & TCP_ACK && ackno == pcb->snd_nxt) { tcp_pcb_purge(pcb); TCP_RMV_ACTIVE(pcb); pcb->state = TIME_WAIT; TCP_REG(&tcp_tw_pcbs, pcb); } break; case LAST_ACK: tcp_receive(pcb); if (flags & TCP_ACK && ackno == pcb->snd_nxt) { /* bugfix #21699: don't set pcb->state to CLOSED here or we risk leaking segments */ recv_flags |= TF_CLOSED; } break; default: break; } return ERR_OK; } |
4.TCP数据报操作
TCP的输入/输出处理函数较多,它们之间的调用关系也比较复杂,下面用一个总函数调用流程来展示所有这些函数之间的调用关系:
1)TCP报文段输出处理
用户应用程序可以通过TCP编程函数tcp_connect、tcp_write等构造一个报文段,这个报文可以用于连接建立和断开的握手报文,也可以是双方的数据交互报文,握手报文段的构造由函数tcp_enqueue_flags构造完成并放入到控制块的发送队列中;而数据报文段的构造是函数tcp_write直接完成的,它将TCP数据和首部部分字段填入报文中,并使用tcp_seg结构体将报文段组织在发送缓冲队列上(一个tcp_seg描述一个可独立发送的报文段);当函数tcp_output被调用时,它会在控制块的发送缓冲队列上依次取下报文段发送,这个函数的唯一工作就是判断报文段是否在允许的发送窗口内,然后调用函数tcp_output_segment发送报文段,当发送完成后,tcp_output会把相应报文段放在控制块的未确认队列unacked上;在tcp_output_segment发送报文段时,它会填写首部中的剩余字段,包括确认序号、通告窗口、选项等,最重要的是,它需要与IP层的ip_route函数交互,获得伪首部中的源IP地址字段,计算并填写TCP首部中的校验和。最后,IP层的发送函数ip_output会被调用,用来组装并发送IP数据报。
下面给出构造数据报文段的tcp_write函数的流程图,实现代码较复杂,读者可以根据流程图对照源码理解其逻辑,构造握手报文段的tcp_enqueue_flags函数比tcp_write简单许多,读者可以参考下面的流程图直接阅读源码:
发送报文段的函数是tcp_output,其唯一参数是某个连接的TCP控制块指针pcb,函数把这个控制块unsent队列上的报文段发送出去或只发送一个ACK报文段(unsent队列无数据发送或发送窗口此时不允许发送数据)。报文段实际由tcp_output_segment发送出去后,tcp_output需将发送出去的报文段放入控制块unacked缓冲队列中(需保证队列中的所有报文段序号有序排列),以便后续的重发操作。当unsent队列上的第一个报文段处理完毕,tcp_output会按照上述方法依次处理unsent队列上的剩余报文段,直到数据被全部发送出去或发送窗口被填满。
从整个发送过程来看,tcp_output只是检查某个报文是否满足被发送的条件,然后调用函数tcp_output_segment将报文段发送出去,后者需要填写TCP报文首部中剩下的几个必要字段,然后调用IP层输出函数ip_output发送报文,tcp_output_segment函数的功能有点类似于UDP协议中的udp_sendto函数。
2)TCP报文段输入处理
从上面的TCP函数调用流程图可以看出,与TCP输入相关的函数有5个,TCP报文被IP层递交给tcp_input函数,这个函数可以说是TCP层的总输入函数,它会为报文段寻找一个匹配的TCP控制块,根据控制块状态的不同,调用tcp_timewait_input、tcp_listen_input或tcp_process处理报文段;这里的重点是函数tcp_process,它实现了前面介绍过的TCP状态机(实现源码也在前面给出),函数根据报文信息完成连接状态的变迁,同时若报文中有数据,则函数tcp_receive会被调用;整个过程中的难点在于函数tcp_receive,它完成了TCP中的数据接收、数据重组等工作,同时TCP中各种性能算法的实现也是在该函数中完成。
在IP层收到数据报后,ip_input函数会判断IP首部中的协议字段,把属于TCP的报文通过tcp_input函数传递到TCP层。tcp_input完成报文向各个控制块的分发,并等待控制块对相应报文的处理结果,它会根据处理结果向用户递交数据或向连接另一端输出响应报文。对于每一个待处理报文,tcp_input都将它们的信息记录在一些全局变量中,其它各函数可以直接操作这些全局变量来得到想要的信息,这些全局变量的定义如下:
// rt-thread\components\net\lwip-1.4.1\src\core\tcp_in.c /* These variables are global to all functions involved in the input processing of TCP segments. They are set by the tcp_input() function. */ static struct tcp_seg inseg; static struct tcp_hdr *tcphdr; static struct ip_hdr *iphdr; static u32_t seqno, ackno; static u8_t flags; static u16_t tcplen; static u8_t recv_flags; static struct pbuf *recv_data; struct tcp_pcb *tcp_input_pcb; |
tcp_input函数开始会对IP层递交进来的报文段进行一些基本操作,如丢弃广播或多播数据报、数据校验和验证,同时提取TCP报文首部各个字段填写到上述全局变量中。接下来根据TCP报文段中表示连接的四个字段的值来查找四条链表,在哪条链表上找到对应的控制块则交由相应的函数继续处理。下面给出tcp_input函数的流程图如下:
在TCP内核中,输入报文段中的数据接收和处理都是由函数tcp_receive来完成的,这个函数可以说是整个协议栈内核中代码最长、最难懂的部分了。在前面TCP状态机实现函数tcp_process中可以看到,函数tcp_receive在多个地方被调用来处理报文段中的数据。总结下该函数需要完成的工作:(1)首先检查报文中携带的确认序号是否确认了未确认序列unacked中的数据,如果是则释放掉被确认的数据空间,并设置acked字段值以便tcp_input回调用户函数;(2)同时,如果报文段中有数据且数据有序,这些数据会被记录在recv_data中,以便用户程序处理;(3)如果控制块的ooseq队列上的报文段因为新报文段的到来而变得有序,则这些报文段的数据也会被一起连接在recv_data中,在函数退出后由tcp_input递交给应用程序处理;(4)如果新报文段不是有序的,则报文段将被插入到队列ooseq上,该报文段的引用指针将被加1,防止在其他地方被删除。(5)最后,还有很多其他工作也需要在该函数中完成,例如当前确认序号包含了对正在进行RTT估计的报文段的确认,则RTT需要被计算;如果收到重复的ACK,这可能会在函数中启动快速重传算法等。下面展示了整个tcp_receive函数的处理流程,读者可以参照这个流程图去阅读该函数的源代码:
3)TCP定时器
在TCP函数调用总流程中,TCP报文段输出函数tcp_output是被定时器tcp_tmr周期性调用的。此外,与TCP功能相关的定时器还有很多,比如回调函数poll需要定时器的支持,重传、保活等也都离不开定时器支持。总结来说,TCP为每条连接总共建立了七个定时器,分别如下:
(1)建立连接(connection establishment)定时器:在服务器响应一个SYN握手报文并试图建立一条新连接时启动,此时服务器已发出自己的SYN+ACK并处于SYN_RCVD等待对方ACK的返回,如果在75秒内没有收到响应,连接建立将中止,这也是服务器处理SYN攻击的有效手段;
(2)重传(retransmission)定时器:在TCP发送某个报文时设定,如果该定时器超时而对端的确认还未到达,TCP将重传该报文段。重传间隔是根据RTT估计值动态计算的,且取决于报文段已被重传的次数;
(3)数据组装(assemble)定时器:在接收缓冲队列ooseq不为空时有效,如果连接上很长时间内都没有数据交互,但是失序报文段缓冲队列ooseq上还有失序的报文,则相应的报文需要在队列中删除;
(4)坚持(persist)定时器:在对方通告接收窗口为0,阻止TCP继续发送数据时设定。定时器超时后,将向对方发送1字节的数据,判断对方接收窗口是否已打开;
(5)保活(keep alive)定时器:在TCP控制块的so_options字段设置了SOF_KEEPALIVE选项时生效。如果连接的连续空闲时间超过2小时,则保活定时器超时,此时应向对方发送保活探查报文,强迫对方响应。如果收到期待的响应,TCP可确定对方主机工作正常,重置保活定时器;如果未收到期待的响应,则TCP关闭连接释放资源并通知应用程序对方已断开;
(6)FIN_WAIT_2定时器:当某个连接从FIN_WAIT_1状态变迁到FIN_WAIT_2状态并且不能再接收任何新数据时,FIN_WAIT_2定时器启动,定时器超时后连接被关闭。
(7)TIME_WAIT定时器:一般也称为2MSL(Maximum Segment Lifetime)定时器,当连接转移到TIME_WAIT状态即连接主动关闭时,该定时器启动,超时后TCP控制块被删除,端口号可重新使用。同样,服务器端在断开连接过程中会处于LAST_ACK状态等待对方ACK的返回,如果在该状态下的2MSL时间内未收到对方的响应,连接也会被立即关闭。
所有的7个定时器中,重传定时器使用rtime字段计数,坚持定时器使用persist_cnt字段计数,其它所有5个定时器都使用tmr字段,通过与各自的一个全局变量做比较判断是否超时,超时后执行相应的处理。这几个定时器是在连接处于几种不同的状态时使用的,因此它们可以完全独立的使用tmr字段而不会相互影响,下面是它们的超时上限宏定义:
// rt-thread\components\net\lwip-1.4.1\src\include\lwip\tcp_impl.h #define TCP_TMR_INTERVAL 250 /* The TCP timer interval in milliseconds. */ #define TCP_FAST_INTERVAL TCP_TMR_INTERVAL /* the fine grained timeout in milliseconds */ #define TCP_SLOW_INTERVAL (2*TCP_TMR_INTERVAL) /* the coarse grained timeout in milliseconds */ #define TCP_FIN_WAIT_TIMEOUT 20000 /* milliseconds */ #define TCP_SYN_RCVD_TIMEOUT 20000 /* milliseconds */ #define TCP_OOSEQ_TIMEOUT 6U /* x RTO */ #define TCP_MSL 60000UL /* The maximum segment lifetime in milliseconds */ /* Keepalive values, compliant with RFC 1122. Don't change this unless you know what you're doing */ #define TCP_KEEPIDLE_DEFAULT 7200000UL /* Default KEEPALIVE timer in milliseconds */ #define TCP_KEEPINTVL_DEFAULT 75000UL /* Default Time between KEEPALIVE probes in milliseconds */ #define TCP_KEEPCNT_DEFAULT 9U /* Default Counter for KEEPALIVE probes */ #define TCP_MAXIDLE TCP_KEEPCNT_DEFAULT * TCP_KEEPINTVL_DEFAULT /* Maximum KEEPALIVE probe time */ |
上面介绍的7种定时器包括TCP绝大部分可靠性的保障都是在tcp_slowtmr慢速定时器处理函数中完成的。在tcp_slowtmr函数中,各个定时器的实现都是通过使用全局变量tcp_ticks与tmr字段的差值来实现的,当TCP进入某个状态时,就会将控制块tmr字段设置为以前的全局时钟tcp_ticks的值,所以上面的差值可以有效表示出TCP处于某个状态的时间。各定时器超时后的处理也很类似,即将变量pcb_remove加1,pcb_remove变量是超时处理中最核心的变量,当针对某个控制块做完超时判断后,函数通过判断pcb_remove的值来处理TCP控制块,当pcb_remove值大于1时,则表示该控制块上有超时事件发生,该控制块或被删除或被挂起。
LWIP中包含两个定时器相关函数:一个是上述周期在500ms的慢速定时器函数tcp_slowtmr,它完成了基本所有TCP需要实现的定时功能;第二个是周期为250ms的快速定时器函数tcp_fasttmr,它完成的一个重要功能是让连接上被延迟的ACK立即发送出去,同时未被成功递交的数据也在这里被递交。
为了实现TCP的功能,TCP的上述两个定时器函数需要被周期性的调用,在LwIP的实现中,内核需要以250ms为周期调用tcp_tmr,这个函数会自动完成对tcp_slowtmr和tcp_fasttmr的调用。为了便于用户程序的编写,内核已经将tcp_timer以及其他所有定时调用函数封装到了sys_check_timeouts中,因此在没有操作系统模拟层的支持下,应用程序应至少每隔250ms调用sys_check_timeouts一次,以保证内核机制的正常工作。下面给出tcp_timer的实现代码:
// rt-thread\components\net\lwip-1.4.1\src\core\tcp.c /** Timer counter to handle calling slow-timer from tcp_tmr() */ static u8_t tcp_timer; /** * Called periodically to dispatch TCP timers. */ void tcp_tmr(void) { /* Call tcp_fasttmr() every 250 ms */ tcp_fasttmr(); if (++tcp_timer & 1) { /* Call tcp_tmr() every 500 ms, i.e., every other timer tcp_tmr() is called. */ tcp_slowtmr(); } } |
更多内容想见下一节:LWIP协议栈(九)——内核定时事件管理