DIY TCP/IP TCP模块的实现3

上一篇:DIY TCP/IP TCP模块的实现2
9.5 TCP连接状态机的实现
DIY TCP/IP TCP Connection State Machine
TCP是面向连接的,即TCP连接的双方各自维护一个连接状态机,约定在各个状态可以接收和处理的数据帧,以及接收到特定数据帧时需要转换到的状态。上图描述了TCP的连接状态机转换图,由于是全双工连接,TCP连接的建立和断开都有主动和被动的区别。按照先连接,后断开,先主动,后被动的顺序,解释状态转换图。
连接建立之前,双方都处于Closed状态,主动发起TCP连接的一方为active open,发出SYN数据帧后,转换到SYN-Sent状态,在SYN-Sent状态收到SYN-ACK数据帧后,表明对方收到了SYN数据帧并回复了SYN-ACK,回复对SYN-ACK的确认ACK数据帧,完成三步握手,由SYN-Sent转换到Established连接建立状态。
被动发起TCP连接的一方为passive open,打开TCP连接后由Closed转换到Listen状态,监听指定的TCP端口等待TCP连接请求。在Listen状态接收到SYN数据帧后,回复SYN-ACK并转换到SYN-Reieved状态,在SYN-Received状态接收到对SYN-ACK的确认ACK后,完成三步握手,由SYN-Received转换到立Established状态。
在连接建立状态双方可以交互TCP数据帧,也可以主动或被动的断开TCP连接,先来看主动断开TCP连接的情况。
应用程序主动关闭TCP连接,在Linux kernel中的实现是应用程序通过close系统调用关闭socket描述符,触发TCP/IP协议栈发出TCP FIN数据帧。DIY TCP/IP本身运行在Linux kernel的用户空间,可以将DIY TCP/IP编译成动态库,通过给其他应用程序提供类似close的接口,关闭TCP连接。总之主动关闭TCP连接的一方先发出TCP FIN数据帧,从Established转换到FIN-WAIT1状态。TCP是全双工连接,先发出FIN的一方不再有任何需要发送的TCP数据帧,该状态仍然可以接收对方发出的TCP数据帧。在FIN-WAIT1状态,接收到对FIN的确认帧ACK后转换到FIN-WAIT2状态,主动断开TCP连接的一方一直可以接收对方发出的TCP数据帧,直到对方也发出FIN,关闭TCP连接。FIN-WAIT2状态收到对方发出的TCP FIN数据帧后,表明对方也不再有任何需要发送的TCP数据帧,回复对FIN的确认ACK后,由FIN-WAIT2转换到TIME-WAIT状态,TIME-WAIT状态是为了保证四步握手的最后一个ACK被对方收到,如果最后一个ACK丢失,TIME-WAIT状态会再次收到FIN数据帧,并重传ACK数据帧。如果TIME-WAIT状态不再收到任何TCP数据帧,则等待超时后转换到Closed状态。
主动关闭TCP连接的一方在FIN-WAIT1状态,等待对发出的TCP FIN数据帧的确认ACK,如果此时也收到了对方发出的TCP FIN,则表明TCP连接的双方几乎同时发出断开TCP连接的TCP FIN数据帧。这种情况由FIN-WAIT1转换到Closing状态,在Closing状态等待TCP FIN数据帧的确认ACK,在Closing状态收到ACK后,转换到TIME-WAIT,等待超时,并最终转换到Closed状态。
被动关闭TCP连接的情况是在Established状态收到TCP FIN数据帧,回复确认ACK后,转换到Close-wait状态,等待应用程序不再有任何需要发送的TCP数据帧后关闭TCP连接,在Close-wait状态收到应用程序关闭TCP连接的请求后,发出TCP FIN数据帧。由Close-wait转换到Last-wait状态,等待对发出对TCP FIN的确认ACK,收到ACK后由Last-wait转换到Closed状态。
本节TCP连接状态机基本按照状态转换图实现,DIY TCP/IP现阶段并没有编译成动态库,而是做为独立的应用程序运行,一些状态处理在代码实现时会做相应的简化。本节的代码实现仍然是在TCP模块的tcp.c文件中。

 int tcppkt_recv(void *pkt, unsigned int sz)
 {
…
     /* tcp checksum validation */
…
     /* TBD: find established tcp connection */
     /* find listenning tcp connection */
…
     /* process connect/disconnect tcp packet */
     if (tcp_process_conn_pkt(conn, tcppkt, tcppkt_len)) {
         ret = -1;
         goto out;
     }
 out:
     return ret;
 }

先来看本节对tcppkt_recv函数的改动,移除tcp_parse_options函数的调用,并不是每个TCP数据帧都需要解析选项字段,该函数的调用放在高亮显示新增函数tcp_process_conn_pkt内,其余部分与9.4节一致。tcppkt_recv接收TCP数据帧,先构造伪头部,检验TCP头部校验和。检验通过后,根据目标端口查找已经建立的TCP连接,如果没有找到已经建立的TCP连接,则查找正在建立的TCP连接,即状态数值小于ESTABLISHED的TCP连接。如果查找到对应的conn指针,则说明目标端口对应的TCP连接正处于三步握手的过程中,调用tcp_process_conn_pkt处理TCP数据帧。
tcp_process_conn_pkt是本节的新增函数,用于处理TCP连接建立时三步握手和TCP连接断开时的四步握手的数据帧,函数第一个参数是根据目标端口查找到的连接指针,第二个参数是指向TCP数据帧首字节地址的指针,第三个参数是TCP数据帧的长度。函数返回值为0代表成功,非0代表失败。

 static int tcp_process_conn_pkt(tcp_conn_t *conn,
         tcphdr_t *tcppkt, unsigned short tcppkt_len)
 {
     int ret = 0;
     tcphdr_flags_t flags;
 
     if (conn == NULL || tcppkt == NULL || tcppkt_len == 0)
         return -1;
     flags.v = NTOHS(tcppkt->flags_len);
     switch (conn->state) {
         case STATE_CLOSED:
             break;
         case STATE_LISTEN:
             /* receive SYN */
             if (flags.b.syn) {
                 state_transfer(conn, STATE_SYN_RECEIVED);
                 ret = process_tcp_syn(conn, tcppkt, tcppkt_len);
                 /* TBD: send SYN-ACK */
             }
             break;
         case STATE_SYN_SENT:
             /* receive SYN */
             if (flags.b.syn && !flags.b.ack) {
                 state_transfer(conn, STATE_SYN_RECEIVED);
                 /* TBD: send ACK */
             } else if (flags.b.syn && flags.b.ack) {
                 state_transfer(conn, STATE_ESTABLISHED);
                 /* TBD: send ACK */
             }
             break;
         case STATE_SYN_RECEIVED:
             /* receive ACK */
             if (flags.b.ack) {
                 state_transfer(conn, STATE_ESTABLISHED);
             }
             break;
         case STATE_ESTABLISHED:
             /* receive FIN */
             if (flags.b.fin) {
                 state_transfer(conn, STATE_CLOSE_WAIT);
                 /* TBD: send ACK */
             }
             break;
         case STATE_CLOSE_WAIT:
             /* TDB: App close to send FIN */
             state_transfer(conn, STATE_LAST_ACK);
             break;
         case STATE_LAST_ACK:
             /* receive ACK for FIN */
             if (flags.b.ack) {
                 state_transfer(conn, STATE_CLOSED);
             }
             break;
         case STATE_FIN_WAIT1:
             if (flags.b.ack) {
             /* received ACK for FIN */
                 state_transfer(conn, STATE_FIN_WAIT2);
             } else if (flags.b.fin) {
             /* received FIN */
                 state_transfer(conn, STATE_CLOSING);
                 /* TBD: send ACK */
             }
             break;
         case STATE_FIN_WAIT2:
             /* received FIN */
             if (flags.b.fin) {
                 state_transfer(conn, STATE_TIME_WAIT);
                 /* TBD: send ACK */
             }
             break;
         case STATE_CLOSING:
             /* received ACK for FIN */
             if (flags.b.ack) {
                 state_transfer(conn, STATE_TIME_WAIT);
             }
             break;
         case STATE_TIME_WAIT:
             /* TBD: timer expire */
             state_transfer(conn, STATE_CLOSED);
             break;
     }
 out:
     return ret;
 }


本节tcp_process_conn_pkt的实现只是调用state_transfer完成状态转换,对TCP数据帧的处理均用注释代替,后面章节做相应的扩展。
Line 1-20: 首先判断函数的3个参数是否为空,不为空时继续执行。将TCP数据帧头部flags_len字段的值,转换为小端数值,赋值给flags.v成员,解析TCP头部的标志位。根据连接指针conn->state的值,做相应的状态转换和数据帧的处理。不论是passive open或者是active open的TCP连接,能接收到TCP数据帧时,连接均不会处于closed状态,如果连接处于closed状态,不做任何处理。如果连接处于STATE_LISTEN状态,表明是passive open的TCP连接,正在监听目标端口,再判断收到的TCP数据帧是否是SYN,如果是则调用state_transfer将conn->state的值设置为STATE_SYN_RECEIVED,再处理SYN数据帧,并且回复SYN ACK数据帧。如果不是SYN数据帧,则不做任何处理。
Line 21-30: 如果连接处于STATE_SYN_SENT状态,则是active open的TCP连接,判断接收到的TCP数据帧是否为SYN ACK,如果是则表明发出的SYN数据帧得到了对端的确认,当前收到的是三步握手的第二个数据帧,状态转换为STATE_ESTABLISHED,回复ACK确认收到对方的SYN ACK数据帧,完成三步握手。
Line 31-36: 如果连接处于STATE_SYN_RECEIVED状态,则表明是passive open的TCP连接,并且已经完成了三步握手的前两个数据帧的交互,判断收到的TCP数据帧是否为ACK,如果是,则表明对方收到了SYN ACK并且回复了确认ACK,状态转换到ESTABLISHED,完成三步握手。
Line 37-43: 连接处于ESTABLISHED状态,只处理被动关闭TCP连接,即接收到FIN数据帧的情况,主动关闭TCP连接发出FIN数据帧的情况后面章节做为扩展实现。如果接收到FIN数据帧,状态转换到STATE_CLOSE_WAIT,并回复对FIN数据帧的确认ACK。
Line 44-47: 连接处于STATE_CLOSE_WAIT状态,等待应用程序关闭TCP连接,发出FIN数据帧,再将状态转换为STATE_LAST_ACK。等待应用程序关闭TCP连接的实现在后面章节扩展实现。
Line 48-53: 连接处于STATE_LAST_ACK时,是被动关闭TCP连接的情况,说明已经收到对方发出的FIN,也回复了确认ACK,本地也发出了FIN数据帧,等待对发出的FIN数据帧的确认ACK。如果判断收到的ACK是对四步握手中第三帧FIN的确认时,状态转换为STATE_CLOSED。
Line 54-63: 连接处于STATE_FIN_WAIT1,是主动关闭TCP连接的情况,说明本地已经发出了FIN数据帧,如果在该状态收到了对FIN的确认则转换到STATE_FIN_WAIT2等待对方发出FIN数据帧,如果在收到对本地发出的FIN的确认ACK之前,先收到对方发出的FIN,则回复ACK,状态转换为STATE_CLOSING。
Line 64-70: 连接处于STATE_FIN_WAIT2,是主动关闭TCP连接的情况,说明本地已经发出FIN,并且收到了对方对FIN的确认,在该状态如果收到对方发出的FIN数据帧,则回复ACK,完成四步握手的最后一帧,状态转换为STATE_TIME_WAIT等待超时。
Line 71-76: 连接处于STATE_CLOSING,是主动关闭TCP连接的情况,说明本地发出了FIN,也收到了对方发出的FN,但没有收到对方对本地发出的FIN的确认。如果判断收到了ACK是对本地发出FIN的确认,状态转换为STATE_TIME_WAIT等待超时。
Line 77-84: 连接处于STATE_TIME_WAIT状态,在该状态,说明是本地主动断开TCP连接,并且已经发出了四步握手的最后一个ACK数据帧,等待超时或者重传最后一个ACK。如果超时时间内没有收到对方发出的FIN,则状态转换为STATE_CLOSED,完成四步握手断开TCP连接。
state_transfer函数内定义了字符指针数组,数组下标与11个连接状态的数值对应,第一个参数是指向conn连接结构体的指针,第二个参数是要转换到的目标状态的值。函数先打印状态转换信息,再设置conn->state成员为目标状态值。

 static void state_transfer(tcp_conn_t *conn, int new_state)
 {
     char *state_str[] = {
         "CLOSED",
         "LISTEN",
         "SYN_SENT",
         "SYN_RECEIVED",
         "ESTABLISHED",
         "CLOSE_WAIT",
         "LAST_ACK",
         "FIN_WAIT1",
         "FIN_WAIT2",
         "CLOSING",
         "TIME_WAIT",
     };
     log_printf(INFO, "%s -> %s\n",
         state_str[conn->state],
         state_str[new_state]);
     conn->state = new_state;
 }

tcppkt_recv函数移除了解析TCP SYN头部选项的函数调用,因为tcppkt_recv不仅仅用于接收TCP建立连接或断开连接的数据帧,绝大部分情况用于接收TCP数据帧。Tcp_parse_options主要用于解析三步握手时TCP数据帧中的选项字段,并初始化TCP连接的参数。TCP连接建立后,大部分情况是查看TCP数据帧中的Time Stamp选项,而不再关心其他选项。所以将选项的解析放在tcp_process_conn_pkt函数中。在被动打开TCP连接监听目标端口情况下,收到TCP SYN的数据帧时调用process_tcp_syn函数处理TCP SYN数据帧时,解析选项字段。

 static int process_tcp_syn(tcp_conn_t *conn,
         tcphdr_t *syn, unsigned short syn_len)
 {
     int ret = 0;
     if (tcp_parse_options(conn, syn, syn_len)) {
         log_printf(ERROR, "Failed to parse TCP options\n");
         ret = -1;
         goto out;
     }
 out:
     return ret;
 }

process_tcp_syn仅仅是对tcp_parse_options函数的封装,没有实现对TCP SYN数据帧的处理,该函数将在下节TCP三步握手的实现时扩展。
本节tcp_process_conn_pkt只是调用state_transfer完成状态转换,确保连接状态机的状态转换是正确的。各个状态对收到的数据帧处理,等待应用程序主动关闭TCP连接,以及超时重传等具体实现在后续章节扩展实现。
9.4节已经能够正确接收TCP SYN数据帧,并解析TCP SYN数据帧选项字段的值。本节在9.4节的基础上,添加了连接状态机的实现,打印状态转换的信息,所以本节的测试方法与9.4节一致,期望的结果是收到TCP SYN数据帧时,在9.4节运行结果的基础上,打印出”STATE_LISTEN->STATE_SYN_RECEIVED”信息。因此不再细述DIY TCP/IP的运行结果与wireshark软件的抓包数据的对比,直接看DIY TCP/IP的运行结果。
DIY TCP/IP Runtime Log
同样是主机C运行Iperf的TCP client端,主机A运行DIY TCP/IP,指定虚拟IP地址192.168.0.7,监听TCP端口7000。可以看到,主机C首先发出ARP Request请求虚拟IP地址192.168.0.7的硬件地址,DIY TCP/IP回复正确的ARP Reply后,主机C上建立了192.168.0.7与主机A上eth0硬件地址的映射。然后主机C发出TCP SYN请求建立TCP连接,DIY TCP/IP的TCP模块tcppkt_recv正确接收了TCP数据帧,通过头部校验和后,将TCP SYN数据帧交给tcp_process_conn_pkt处理,状态机进入STATE_LISTEN分支,判断是TCP SYN数据帧后,连接状态机由STATE_LISTEN转换到STATE_SYN_RECEIVED状态。调用process_tcp_syn解析TCP SYN头部选项字段,并正确打印选项信息。符合预期,部分验证了本节代码实现的正确性。
下一篇:DIY TCP/IP TCP模块的实现4

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值