DIY TCP/IP TCP模块的实现8

上一篇:DIY TCP/IP TCP模块的实现7
9.10 TCP滑动窗口的实现2
9.8节实现了TCP三步握手数据帧的交互,完成了TCP连接的建立。主机A关闭GRO后,DIY TCP/IP可以正确接收到携带数据的TCP帧。在实现TCP滑动窗口之前,先完成对携带数据的TCP帧的接收。本节的目标是,接收到携带数据的TCP帧时,检验TCP帧的序号是否是希望接收到的序号,构建TCP ACK,确认收到TCP数据帧。由于不对TCP携带的数据做任何处理,可以认为数据被放入接收缓存后,立刻被读取完毕。构建TCP ACK的window size可以保持初始化的数值65535不变。在滑动窗口实现后,再根据接收缓存的实际大小更新TCP ACK中window size的数值。
根据9.9节的分析,携带数据的TCP帧头部的ACK是置位的,也就是iperf TCP client在发送携带数据的TCP帧时,捎带确认收到DIY TCP/IP发出的TCP ACK。所以在接收携带数据的TCP帧时,如果头部的ACK是置位的,则应检查acknowledge number是否等于DIY TCP/IP发出的sequence number。DIY TCP/IP模拟iperf TCP server,发送的TCP ACK均不携带数据,所以TCP ACK的sequence number是1,所以这里验证携带数据的TCP帧的acknowledge number应为1,即与本地TCP连接的conn->send_seq相等。本节实现完成时,DIY TCP/IP就可以正确接收TCP数据帧,并回复TCP ACK,即支持iperf TCP的测试了。
先来修改tcppkt_recv函数

 int tcppkt_recv(void *pkt, unsigned int sz)
 {
     int ret = 0;
     iphdr_t *ippkt = NULL;
     tcp_pseudo_hdr_t pseudo_hdr;
     unsigned short tcppkt_len = 0;
     tcphdr_t *tcppkt = NULL;
     tcp_conn_t *conn = NULL;
 
     if (pkt == NULL || sz == 0) {
         log_printf(ERROR, "TCP receive packet failed,"
                 "invalid parameters\n");
         ret = -1;
         goto out;
     }
     ippkt = (iphdr_t *)pkt;
     tcppkt_len  = NTOHS(ippkt->total_len) - sizeof(iphdr_t);
     tcppkt = strip_header(ippkt, sizeof(iphdr_t));
     /* build tcp pesudo header */
     memset(&pseudo_hdr, 0, sizeof(pseudo_hdr));
     memcpy(pseudo_hdr.src_ip, ippkt->src_ip, sizeof(ippkt->src_ip));
     memcpy(pseudo_hdr.dst_ip, ippkt->dst_ip, sizeof(ippkt->dst_ip));
     pseudo_hdr.proto = ippkt->proto;
     pseudo_hdr.len = HSTON(tcppkt_len);
     /* tcp checksum validation */
     if (tcp_cksum(&pseudo_hdr, tcppkt, tcppkt_len)) {
         log_printf(ERROR, "Invalid TCP checksum\n");
         ret = -1;
         goto out;
     }
     /* find established tcp connection */
     conn = tcp_find_established_conn(NTOHS(tcppkt->dst_port));
     if (conn)
             return tcp_process_data_pkt(conn, tcppkt, tcppkt_len);
     /* find listenning tcp connection */
     conn = tcp_find_listen_conn(NTOHS(tcppkt->dst_port));
     if (conn == NULL) {
         log_printf(ERROR, "%u port not available\n",
             NTOHS(tcppkt->dst_port));
         ret = -1;
         goto out;
         
     }
     memcpy(conn->r_ip, ippkt->src_ip, sizeof(ippkt->src_ip));
     memcpy(conn->l_ip, ippkt->dst_ip, sizeof(ippkt->dst_ip));
     /* process connect/disconnect tcp packet */
     if (tcp_process_conn_pkt(conn, tcppkt, tcppkt_len)) {
         ret = -1;
         goto out;
     }
 out:
     return ret;
 }

高亮显示的部分是本节对tcppkt_recv的修改,该函数其余代码与9.5节实现一致。tcppkt_recv接收到TCP数据帧时,先根据TCP数据帧头部的目标端口查找已经建立的TCP连接,如果找到对应的TCP连接,则调用tcp_process_data_pkt处理TCP数据帧。
总结一下tcppkt_recv收到TCP数据帧时的处理逻辑,首先根据目标端口查找已经建立的TCP连接。如果找到,则认为该TCP数据帧是携带数据的TCP数据帧,也有可能是断开TCP连接的TCP数据帧,调用tcp_process_data_pkt处理。如果没有找到,则查找监听目标口,但尚未建立连接的conn。如果找到则,进入三步握手的处理过程。如果即没有找到已经建立连接的conn,也没有找到监听目标端口的conn,则丢弃该TCP数据帧。断开TCP连接的处理过程在后续章节扩展。

 static tcp_conn_t *tcp_find_established_conn(unsigned short port)
 {
         int i;
         tcp_conn_t *conn = NULL;
 
         if (tcp_connections == NULL)
                 return NULL;
         for (i = 0; i < tcp_conn_num; i ++) {
                 if (tcp_connections[i].l_port == port &&
                         tcp_connections[i].state == STATE_ESTABLISHED) {
                         conn = &tcp_connections[i];
                         break;
                 }
         }
         return conn;
 }

tcp_find_established_conn与tcp_find_listen_conn类似,遍历tcp_connections表,查找与目的端口对应的处于连接状态的conn。

 static int tcp_process_data_pkt(tcp_conn_t *conn,
                 tcphdr_t *tcppkt, unsigned short tcppkt_len)
 {
 /* TIMESTAMP + 2*NOP */
 #define TCPACK_OPT_SZ 12
         int ret = 0;
         pdbuf_t *pdbuf = NULL;
         tcphdr_t *tcpack = NULL;
         tcphdr_flags_t tcpack_flags;
         unsigned int tcpack_len = 0;
         optmap_t opts;
 
         if (conn == NULL || tcppkt == NULL || tcppkt_len == 0)
                 return -1;
         /* parse tcp options */
         if ((ret = tcp_parse_options(conn, tcppkt, tcppkt_len)))
         goto out;
         /* TBD: copy data to sliding window */
         /* update ack number */
         conn->ack_seq = get_acknum(tcppkt, tcppkt_len);
         /* build tcp ack */
         opts.v = 0;
         tcpack_flags.v = 0;
         tcpack_len = sizeof(tcphdr_t);
         if ((ret = tcp_find_option(tcppkt,
                         tcppkt_len, TCP_OPT_TIMESTAMP)) == 0) {
                 tcpack_len += TCPACK_OPT_SZ;
                 opts.b.ts = 1;
         }
         pdbuf = pdbuf_alloc(tcpack_len, 0);
         if (pdbuf == NULL) {
                 log_printf(ERROR, "No memory for tcp ack\n");
                 ret = -1;
                 goto out;
         }
         pdbuf_push(pdbuf, tcpack_len);
         tcpack = (tcphdr_t *)pdbuf->payload;
         tcpack_flags.b.ack = 1;
         tcpack_flags.b.hdr_len = (tcpack_len >> 2);
         build_tcp_header(conn, tcpack, 0, tcpack_flags.v);
         if (opts.v) {
                 if ((ret = tcp_add_options(opts.v,
                         conn, tcpack, TCPACK_OPT_SZ)) < 0) {
                         log_printf(ERROR, "Failed to add"
                                 " tcp options for tcp ack\n");
                         goto out;
                 }
         }
         return tcppkt_send(conn, pdbuf);
 out:
         if (pdbuf)
                 pdbuf_free(pdbuf);
         return ret;
 }

tcp_process_data_pkt函数第一个参数是与目标端口对应的处于连接状态的conn,第二个参数tcppkt指向接收到的TCP数据帧的首字节地址,第三个参数是接收到的TCP数据帧的长度,包括TCP头部和携带的数据部分的长度。tcp_process_data_pkt处理接收到的TCP数据帧,构建并回复TCP ACK。
Line 1-14: TCPACK_OPT_SZ宏预先定义TCP ACK选项字段的长度。根据9.9节的分析,如果收到的TCP数据帧头部携带TIMESTAMP选项字段,回复的TCP ACK也需要添加相应的TIMESTAMP选项字段。TIMESTAMP选项是10个字节,不带选项字段的TCP头部为20个字节,所以需要再添加两个字节的NOP选项,将TCP ACK头部对齐到4个字节。pdbuf存放构建的TCP ACK,tcpack存放构建的TCP ACK的首字节地址,tcpack_flags用于构建TCP ACK头部的flags字段,tcpack_len存放TCP ACK的长度。函数的三个参数均合法时,继续执行。
Line 15-17: 调用tcp_parse_optoins解析接收到的TCP数据帧的头部,本节对tcp_parse_options函数也进行了修改,在tcp_parse_options函数中判断TCP数据帧头部flags字段中ACK是否置位,如果置位则说明接收到的TCP数据帧捎带了ACK确认的信息,在tcp_parse_options函数中检查acknowledge number是否合法,并解析TCP数据帧头部选项字段的值,保存在conn结构中。
Line 18-20: 调用get_acknum计算需要回复的TCP ACK的acknowledge number,如果接收到的TCP数据帧的sequence number为x,携带的数据部分的长度为y,get_acknum返回x + y做为构建的TCP ACK的acknowledge number。回复给发送方,表明从序号为x到x + y -1的字节全部收到,希望收到序号为x + y开始的字节。
Line 21-29: 设置选项位图,选项位图的初始值为0,tcpack_len的初始值为不带任何选项字段的TCP头部长度20个字节。调用tcp_find_option查找接收到的TCP数据帧头部是否带有TIMESTAMP选项。如果有,则增加tcpack_len到32个字节,并设置选项位图中的timestamp位,用于为TCP ACK添加TIMESTAMP选项。
Line 30-40: 调用pdbuf_alloc为TCP ACK申请内存空间,DIY TCP/IP做为iperf tcp server,不向iperf tcp client发送数据,所以回复的TCP ACK不携带数据部分。pdbuf_push将pdbuf->payload向地址减小的方向移动tcpack_len个字节,设置tcpack指向pdbuf->payload,即TCP ACK的首字节地址。设置TCP ACK头部flags字段中的ACK位为1,flags字段中hdr_len的长度TCP ACK的长度除以4。调用build_tcp_header为TCP ACK构建TCP头部,TCP ACK的sequence number是conn->send_seq,回顾tcppkt_send函数,每发送一个数据帧就根据发送数据帧携带的数据长度更新conn->send_seq。所以conn->send_seq存放即将发送的下一个TCP数据帧的序号,build_tcp_header函数可以直接使用conn->send_seq做为TCP ACK的序号。
Line 41-48: 如果选项位图不为0,则表明接收到的TCP数据帧含有TIMESTAMP选项,tcp_process_data_pkt开始处已经解析过TCP数据帧的头部选项,提取了TIMESTAMP字段中TSval的数值。调用tcp_add_option为TCP ACK添加TIMESTAMP选项,第一个时间戳为接收到的TSval,第二个时间戳是本地时间。
Line 49-54: 调用tcppkt_send,计算TCP ACK头部校验和,更新TCP连接本地的发送序号,由于回复的TCP ACK的数据部分的长度为0,所以更新后的本地TCP连接的发送序号仍为1。tcp_process_data_pkt出错时,释放申请的pdbuf内存空间。
tcp_process_data_pkt中用到的tcp_parse_options,tcp_add_options,build_tcp_header,tcppkt_send在9.7节均有介绍。本节改动了tcp_parse_options,新增了tcp_find_option函数,接下来看这两个函数的实现。

 static int tcp_parse_options(tcp_conn_t *conn, 
             tcphdr_t *tcppkt, unsigned short pkt_len)
 {
     int ret = 0;
     unsigned char *popt = NULL;
     unsigned short MSS = 0;
     unsigned char SACK = 0;
     unsigned short data_len = 0;
     unsigned short data_offset = 0;
     unsigned short opt_sz = 0;
     unsigned char shift_count = 0;
     unsigned int TSval = 0, TSecr = 0;
     unsigned int ack_num = 0;
     tcphdr_flags_t flags;
 
     if (conn == NULL || tcppkt == NULL || pkt_len ==0 )
         return -1;
 
     flags.v = NTOHS(tcppkt->flags_len);
     data_offset = flags.b.hdr_len << 2;
     data_len = pkt_len - data_offset;
     opt_sz = data_offset - sizeof(tcphdr_t);
 
     if (flags.b.ack) {
         ack_num = NTOHL(tcppkt->ack_num);
         if (ack_num != conn->send_seq) {
                 log_printf(ERROR, "TCP packet: %u not acked, "
                 "expect ack: %u, received ack: %u\n",
                         conn->send_seq, ack_num);
                 return -1;
         }
     }
     
     if ((popt = get_opt(tcppkt->options,
                 opt_sz, TCP_OPT_MSS))) {
         MSS = NTOHS(*(unsigned short *)popt);
         conn->r_mss = MSS;
     }
     if ((popt = get_opt(tcppkt->options,
                 opt_sz, TCP_OPT_SACK))) {
         SACK = 1;
         conn->r_sack = SACK;
     }
     if ((popt = get_opt(tcppkt->options,
                 opt_sz, TCP_OPT_WSCALE))) {
         shift_count = *(unsigned char *)popt;
         conn->r_shift_cn = shift_count;
     }
     if ((popt = get_opt(tcppkt->options,
                 opt_sz, TCP_OPT_TIMESTAMP))) {
         TSval = NTOHL(*(unsigned int *)popt);
         TSecr = NTOHL(*((unsigned int *)&popt[4]));
         conn->time_echo = TSval;
     }
     conn->r_port = NTOHS(tcppkt->src_port);
     conn->l_port = NTOHS(tcppkt->dst_port);
     conn->r_wnd_sz = NTOHS(tcppkt->wnd_sz);
     conn->r_hflag = flags.v;
     conn->ack_seq = NTOHL(tcppkt->seq_num) + data_len;
 
     log_printf(INFO, "%u -> %u: Seq=%u Win=%u Len=%u MSS=%u "
             "SACK_PERM=%u TSval=%u TSecr=%u WS=%u\n",
             conn->r_port, conn->l_port,
             NTOHL(tcppkt->seq_num), conn->r_wnd_sz,
             data_len, conn->r_mss, conn->r_sack,
             conn->time_echo, TSecr,
             1 << conn->r_shift_cn);
     return ret;
 }

tcp_parse_options高亮显示的部分是本节新增的代码,其余代码与9.4节一致。新增代码在解析TCP头部选项字段之前,判断TCP数据帧头部flags字段的ACK是否置位。如果ACK置位,则表明接收到的TCP数据帧有捎带确认ACK的信息。DIY TCP/IP是iperf tcp server,接收到iperf TCP client发送的数据后,回复TCP ACK,iperf TCP client接收到TCP ACK后,接着发送TCP数据帧并捎带确认DIY TCP/IP发出的TCP ACK。在介绍tcp_process_data_pkt和process_tcp_syn函数中,都有构建TCP ACK数据帧。由于DIY TCP/IP没有数据要发送给iperf tcp client,所以构建的TCP ACK携带的数据长度为0,因此tcppkt_send发送TCP ACK后更新本地TCP连接的发送序号始终为1。
conn->send_seq即是本地TCP连接要发送的下一个TCP数据帧的发送序号,也是希望收到的TCP ACK的确认序号。conn->send_seq应该与接收到的带有捎带ACK信息的TCP数据帧的acknowledge number相等,都为1。Iperf tcp client发送数据帧时携带的acknowledge number为1,表明接收到了DIY TCP/IP发出的TCP ACK,iperf tcp client 希望收到从DIY TCP/IP发出的序号为1开始的数据帧。
高亮显示的代码检验接收到的带有捎带ACK信息的TCP数据帧的acknowldege number,检验正确时继续执行,否则丢弃该TCP数据帧。

 static int tcp_find_option(tcphdr_t *tcppkt,
                         unsigned int pkt_len,
                         unsigned int opt_type)
 {
         int ret = 0;
         unsigned short hdr_len = 0;
         unsigned short opt_sz = 0;
         tcphdr_flags_t flags;
 
         if (tcppkt == NULL || pkt_len ==0)
                 return -1;
 
         flags.v = NTOHS(tcppkt->flags_len);
         hdr_len = flags.b.hdr_len << 2;
         opt_sz = hdr_len - sizeof(tcphdr_t);
         if (opt_sz == 0)
                 return -1;
         if ((get_opt(tcppkt->options,
                         opt_sz, opt_type) == NULL))
                 ret = -1;
         return ret;
 }

tcp_find_option用于查找TCP头部是否包含指定的TCP选项,根据接收到的TCP数据帧的选项字段,为回复的TCP数据帧添加需要的选项。成功查找到指定选项时返回0,失败返回时-1。tcp_process_data_pkt函数调用tcp_find_option,判断接收到的TCP数据帧头部是否含有TIMESTAMP选项,如果有,则构建TCP ACK时,对应的添加TIMESTAMP选项。
本节的测试方法与9.2节一致,主机A运行DIY TCP/IP,虚拟IP地址为192.168.0.7,监听端口7000。主机C(192.168.0.111)运行iperf TCP client,向192.168.0.7:7000发送TCP数据帧。先来看DIY TCP/IP的运行结果:
DIY TCP/IP Rx TCP Data runtime log0
DIY TCP/IP Rx TCP data runtime log1
先收到主机C(192.168.0.111)发出的ARP Request,回复ARP Reply后,DIY TCP/IP接收到TCP三步握手的第一帧TCP SYN,回复TCP SYN-ACK后,收到主机C发出的TCP ACK,完成三步握手,建立TCP连接<192.168.0.111: 53190, 192.168.0.7:7000>。
TCP Client(主机C)开始向TCP Server(DIY TCP/IP)发送TCP数据帧,TCP Client发送的第一帧TCP 数据帧携带的数据度为24,开始序号为161007191,DIY TCP/IP回复TCP ACK,确认序号为 161007191 + 24,表明161007215之前的字节都已经收到,希望收到序号从161007215开始的字节。DIY TCP/IP发送的TCP ACK不携带任何数据,DIY TCP/IP发出的TCP ACK的序号均为1,acknowledge number是主机C发出的序号加上携带的数据长度。运行log表明,DIY TCP/IP可以正确的接收主机C发出的TCP数据帧,并回复TCP ACK。
DIY TCP/IP Iperf client runtime log
主机C上的iperf TCP client的打印可以看出,192.168.0.111 port 53190 connected with 192.168.0.7 port 7000。主机C与DIY TCP/IP建立了TCP连接,吞吐量虽然不高,但已经有数据传送了。表明DIY TCP/IP已经可以支持局域网内的iperf TCP测试了。这里给出TCP吞吐量不高的一个原因,是DIY TCP/IP对每个收到的TCP数据帧均立刻回复TCP ACK,可以实现delay ack,在收到两个或更多TCP数据帧时回复TCP ACK,可以提高TCP吞吐量。
DIY TCP/IP tcpdump for DIY TCP/IP Rx TCP data0
DIY TCP/IP tcpdump for DIY TCP/IP Rx TCP data1
下一篇:DIY TCP/IP TCP模块的实现9

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值