上一篇:DIY TCP/IP IP模块和ICMP模块的实现6
9. TCP模块的实现
本章实现DIY TCP/IP的TCP模块,目标是DIY TCP/IP可以支持局域网内iperf TCP吞吐量测试。围绕该目标将本章内容分为:TCP数据帧结构介绍;TCP伪首部介绍;TCP三步握手的实现,三步握手的实现还包括,TCP连接数据结构的定义,围绕TCP连接数据结构,实现简单的TCP连接状态机;TCP滑动窗口的实现;接收线程模拟TCP Client读取滑动窗口内的数据。
本章基于前面章节的实现,先简述一下TCP模块的接收和发送逻辑。TCP模块的接收逻辑是,DIY TCP/IP网络设备模块从链路层接收数据帧,剥去以太网头部后交给IP模块。IP模块完成头部校验后剥去IP头部交给TCP模块。TCP模块首先检验伪首部校验和,再根据TCP数据帧的类型,做相应的处理。TCP模块的发送逻辑是,由接收逻辑触发,完成相应处理后,构建TCP头部,计算伪首部校验和,交给IP模块发送,IP模块构造IP头部和以太网头部,最终交给DIY TCP/IP的网络设备模块处理。
测试方法,运行DIY TCP/IP的主机记为A,与主机A处于同一局域网的另外一台Android手机设备记为C。主机C上运行Iperf TCP client向局域网内不存在的IP地址发送数据,DIY TCP/IP模拟iperf TCP server端,接收虚拟IP地址的数据,完成TCP连接的建立和TCP数据的接收和发送。如果主机C上的iperf TCP client端可以正确显示吞吐量数据,则说明本章TCP模块的实现是正确的。
9.1 TCP头部结构
本节介绍TCP数据帧的结构,通过wireshark抓取iperf TCP的连接过程。在主机A的终端中运行iperf client端,命令是”iperf -c 192.168.0.108 -i 1 -t 43200”,主机A是运行DIY TCP/IP的Ubuntu 16.04,此时是用主机A的网络接口eth0真实的IP地址192.168.0.108,不是DIY TCP/IP的虚拟IP地址。主机B是与主机A处于同一局域网的Android 手机,IP地址为192.168.0.111。主机B上运行iperf的server端,命令是“iperf -s -i 1”,wireshark在主机A上的抓包结果如下:
最开始iperf client和iperf server先建立TCP连接,TCP三步握手的实现在TCP的连接一节介绍,本节重点放在TCP数据帧的结构。展开第一个TCP SYN数据帧,查看TCP的头部结构如下:
注意,wireshark展开后的数据帧,中括号包括的字段是wireshark软件自动添加的字段,如果选中中括号包括的字段,右边raw byte不对应任何有效数据。非中括号包括的字段是数据帧的有效字段,在右边raw byte显示一栏对应相应的字节数值和长度。
再将12个bits的flags展开如下:
在wireshark的raw byte显示一栏可以看出,TCP SYN数据帧一共是40个字节,头部包含20个字节的可选字段option。从header length 可以看出,头部长度一共是40个字节。6.1节引入的tcp.h头文件中对TCP头部的结构体的定义,不包含option字段,option是可选字段,DIY TCP/IP的TCP模块实现时,将option看做是数据部分,将TCP的头部看成20个字节,这并不影响TCP校验和的计算和数据帧的接收。
TCP是基于IP的layer 4协议,在DIY TCP/IP中实现为layer 4的模块。从wireshark抓取的TCP数据帧的头部可以看到,TCP头部没有IP地址字段,只有源端口号和目标端口号,分别对应建立TCP连接的两端应用程序(Linux 进程)的TCP端口号,也就是主机A上iperf TCP client进程和主机C上iperf TCP server进程的TCP端口号。四元组<源IP地址,源端口号,目标IP地址,目标端口号>,唯一确定一个TCP连接。
Sequence Number是TCP数据帧的序列号。TCP是字节流协议,Sequence Number实际上就是字节的序号,例如一个TCP数据帧的Sequence Number为N,其包含数据部分的长度为M,则下一个TCP数据帧的序号为N + M。Sequence Number表示TCP数据帧携带数据的字节序号,从wireshark抓包结果可以看到,TCP SYN的数据长度为0,不携带任何数据,但TCP SYN会消耗掉一个Sequence Number,即TCP SYN的序号为N,在TCP 连接建立之后发送的数据的第一个字节的序号为N + 1。
Acknowldege Number,Flags中ACK bit为1时有效,表示希望收到的下一个TCP数据帧的序号,例如发送端发出的TCP数据帧的序号为N,数据长度为M,接收端收到该TCP数据帧时其实就是收到了序号为N到N + M – 1的字节。接收方希望收到的下一个字节的序号为 N + M,接收端回复给发端方的TCP ACK数据帧时,将Acknowledge Number设置为N + M,表示小于等于N + M – 1序号之前的字节已经全部收到,希望收到序号为N + M开始的字节流。
Data Offset是以4byte为单位的数值,即Data offset * 4代表,TCP数据帧携带的数据的起始字节的偏移量,也可以认为是TCP头部的长度,在抓包结果中TCP SYN头部一共是32个字节,Data Offset值为0x8。
Flags和Data Offset一共占2个字节,高位字节的高4位是Data Offset,其余12位是flags中的各个标志位。各个标志位的英文解释,摘抄自RFC 793。再来逐个解释一下,FIN为1时,表示发送方不再有任何数据发送,此时TCP连接处于半关闭状态,发送了TCP FIN数据帧的一端不再发送任何数据,但仍可以接收数据并回复TCP ACK。SYN为1时,表示该TCP数据帧是TCP SYN,用于发起TCP三步握手过程。RST为1时,表示要重新建立TCP连接。PSH为1时,表示接收端的应用程序应立即取走接收缓存中的数据。ACK为1时,Acknowledge Number有效。URG为1时,Urgent Pointer有效。RFC 793中一共定义了从FIN(BIT0)到URG(BIT 5)的6个BIT,剩余6个是保留未用,表中的其余6个Bit的英文解释是摘抄自Wireshark的对TCP头部Flags字段的解析。DIY TCP/IP并未实现对BIT6-BIT11的解释和处理,感兴趣的朋友可以查阅RFC 793。
Window Size是发送方的接收缓存的大小,表明发送方还能再接收多少个字节,用于TCP的流控。Urgent Pointer如表中的解释。checksum单独做为一节与TCP的伪头部结构一起来介绍。
TCP头部除了表中列出的必选字段,还有可选字段options。再来看一下wireshark将TCP数据帧展开后的options字段。TCP头部的Data Offset字段是4个bit,最大值为15,Data Offset以4byte为单位,表明TCP头部最长为60字节。除去必选字段后,TCP头部可以携带的options最多为40个字节。本节通过wireshark抓到的TCP数据帧携带了20个字节的TCP options,也是最常用的TCP options长度。
TCP option是TLV结构,T是type,表征option的类型,L是length,表征包括type,length 和value的总体字节数。V是value,表征option的具体数值。将Maximum segment size 展开可以看到
MSS 的type是0x2,length是0x04,value是0x5B4 (1460)。MSS value共2个字节,所以最大的TCP MSS为2^16 – 1 (65535),对于MTU为1500的以太网而言,TCP MSS 是1460。
1460 = MUT (1500 bytes) – IP头部(20 bytes) – TCP头部(20 bytes,不带任何option数据)。TCP MSS用于通知对端,发送方可以接收的TCP数据的最大长度,这里的数据长度是TCP的payload长度,不包括TCP头部。
TCP SACK option
SACK (Selective Acknowlegement)
SACK option 的type为0x04,length字段为2,一共2个字节。TCP头部中包含该option,表明发送方支持SACK。SACK用于有选择的回复已经接收到的数据,前面已经介绍过如果ACK的数值为N,则表明从1到N-1序号的字节都已经收到,希望收到的下一个字节序号为N。如果N之后的字节也已经被收到,例如N + 1到 N + M序号的节都已经收到,但N没有收到,接收端如果不支持SACK,只能一直回复序号为N的ACK。超时后,发送端需要将N序号之后的字节全部重传。为避免不必要的重传,TCP引入了SACK选项。
TCP Timestamp option
Timestamp option的type为0x08,length为10,剩下的8个字节分别是4个字节的timestamp value和timpestamp echo reply。TCP 的sequence number为4个字节,即TCP发送端传送了2^32 -1 (4G)字节的数据后,序号就会回绕。防止迟到的TCP数据帧与回绕序号后的TCP数据帧混淆接收方,接收方根据Timestamp丢弃迟到的TCP数据帧。接收方回复ACK时只需将timestamp value赋值到time echo reply字段即可,timestamp value赋值为本机时间。
TCP NOP option
NOP option,type为0x1,没有length和value字段,一共占1个字节。用于填充TCP头部。TCP头部的Data Offset字段的单位是4byte,也就是说TCP头部长度是4的整数倍,如果添加了options字段后不是4的整数倍,则需填充TCP NOP选项,TCP头部数据可以根据需要,填充多个NOP选项。
TCP Window Scale option
Window scale option,type为0x03,length为0x03,value是0x08,共3个字节。用于扩展window size。TCP头部的window size共2个字节,即最大长度为65535,如果发送发有更多内存做为TCP接收缓存,则可以通过window scale来扩展window size。接收端收到window scale选项后根据shift count将window size左移shift count位得到发送端的接收缓存大小。
9.2 TCP伪头部与TCP校验和
TCP伪头部,不包含在TCP头部或TCP数据中,也不参与TCP数据帧的传输,只是用于计算或校验TCP校验和。9.1节已经介绍过,TCP头部不包含IP地址信息,TCP数据帧是封装在IP数据帧的数据部分被传输的。当一个IP数据帧从一个路由器转发到另一个路由器时,IP头部中的TTL会发生变化,IP头部的校验和也将被重新计算,路由器属于层3的设备,会修改IP头部的信息。为了防止TCP数据帧在网络传输过程中被中间的路由设备错误的修改,在收到TCP数据帧时先根据IP头部的信息生成TCP伪头部,伪头部参与检验校验和,TCP伪头部中包含源IP地址和目标IP地址,如果TCP数据帧被中间路由器错误的修改,接收端检验校验和时就会出错,从而将其丢弃。
TCP伪头部中,需要注意的字段为TCP Length,这个长度字段包括TCP头部长度和TCP数据长度,不包含TCP伪头部长度。
TCP头部的校验和是根据TCP伪头部,TCP头部和数据部分共同计算得到的。与IP头部校验和的计算方法一致,当收到TCP数据帧时,先根据IP头部的信息生成TCP伪头部,伪头部的12个字节按照两两字节(小端格式)累加。累加和的初始值为0,累加产生的进位再次加到溢出结果的低位,得到的累加和不用取反,做为接下来校验和计算的初始值,便于描述将此累加和记为cksumA。TCP头部和TCP数据,同样按照两两字节(小端格式)累加,累加和的初始值为cksumA,产生的进位再次累加加到溢出结果的低位,最终的累加和取反,得到校验和。回顾ICMP和IP头部校验和的计算,累加和的初始值均为0,此时累加和的初始值为cksumA。
DIY TCP/IP TCP模块的实现0
最新推荐文章于 2024-04-20 08:44:06 发布