上一篇:DIY TCP/IP TCP模块的实现1
9.4 TCP连接的初始化
TCP是面向连接的协议,面向连接即有状态的协议。通信双方通过三步握手,达到连接建立的状态,然后基于这种状态交互TCP数据帧。断开连接是通信双方通过四步握手达到断开连接的状态。9.1节提到<源IP地址,源端口,目标IP地址,目标端口>四元组可以标识通信双方建立的TCP连接。所以定义TCP连接的数据结构应包括四元组信息,另外为保存下节将实现的TCP握手过程中得到的序列号,以及通信双方的TCP options解析出的window size,SACK等信息,将这些信息都加入TCP连接数据结构中。
TCP连接数据结构是TCP模块的核心数据结构,TCP模块的代码实现,连接的建立和维护,TCP连接状态机,三步握手,四步握手,滑动窗口的实现,都是基于本节定义的TCP连接数据结构实现。本节介绍TCP连接数据结构的定义,初始化,为DIY TCP/IP的初始化新增运行时参数,来指定TCP的端口号,用于初始化一条TCP连接。
测试方法与9.4节基本相同,不同点是主机A上运行的DIY TCP/IP除了指定虚拟IP地址之外,还要指定TCP的端口号,记为PX。主机C上运行的iperf TCP client端的参数改为iperf -c <虚拟IP地址> -i 1 -t 43200 -p PX。测试目标是TCP模块能够正确接收主机C发出的TCP SYN数据帧,根据目标端口号找到对应的TCP连接,并打印输出TCP SYN的数据帧头部信息。
TCP连接数据结构是TCP模块的内部数据结构,仅在TCP模块内部使用,定义在tcp.c文件中。
typedef struct _tcp_conn {
/* remote port */
unsigned short r_port;
/* local port */
unsigned short l_port;
/* remote ip */
unsigned char r_ip[4];
/* local ip */
unsigned char l_ip[4];
/* remote max segment size */
unsigned short r_mss;
/* local mss */
unsigned short l_mss;
/* remote window size */
unsigned short r_wnd_sz;
/* local receive window size */
unsigned short l_wnd_sz;
/* remote sack */
unsigned char r_sack;
/* remote window scale */
unsigned char r_shift_cn;
/* local window scale */
unsigned char l_shift_cn;
/* remote tcp header flag */
unsigned short r_hflag;
/* local tcp header flag */
unsigned short l_hflag;
/* time echo reply */
unsigned int time_echo;
/* rx: ack sequence number */
unsigned int ack_seq;
/* tx: next sequence number */
unsigned int send_seq;
/* connection state */
tcp_conn_state_t state;
} tcp_conn_t;
通过typedef将TCP连接数据结构定义为tcp_conn_t类型,结构体中以”r_”开头的成员表示remote端的信息,以”l_”开头的成员表示local端的信息。逐个介绍每个成员的意义,r_port,l_port,r_ip,l_ip即四元组,分别代表,源端口,目标端口,源IP和目标IP。收到TCP SYN后从TCP头部解析出的源端口即TCP SYN发送方的端口,对于TCP模块来讲是remote端口,所以这里的remote和local刚好相反的对应到TCP SYN的源和目标端。r_mss和l_mss是remote和local端的TCP MSS。r_wnd_sz和l_wnd_sz是remote端和local端的接收缓存大小。r_shift_cn和l_shift_cn对应remote和local端的window scale数值,r_sack代表remote端是否支持SACK。r_hflag和l_hflag是remote端和local端TCP头部中的flags字段的值。time_echo保存remote端TCP options中的timestamp数值。next_seq是希望收到的下一个TCP数据帧的序列号,send_seq是l_local端发出的TCP数据帧的序列号。state是TCP连接状态数据结构。
TCP连接状态数据结构也是TCP模块的内部数据结构,定义在tcp.c中。
typedef enum tcp_conn_state {
STATE_CLOSED = 0,
STATE_LISTEN,
STATE_SYN_SENT,
STATE_SYN_RECEIVED,
STATE_ESTABLISHED,
STATE_CLOSE_WAIT,
STATE_LAST_ACK,
STATE_FIN_WAIT1,
STATE_FIN_WAIT2,
STATE_CLOSING,
STATE_TIME_WAIT,
} tcp_conn_state_t;
TCP连接状态定义为枚举类型,从STATE_CLOSED到STATE_TIME_WAIT共11个数值,分别代表TCP连接在不同阶段的状态。TCP连接的状态转换在9.5节实现三步握手时详细介绍,这里先引入数据结构的定义。本节只用到SYN_CLOSED和STATE_LISTEN两个状态,分别代表TCP连接处于关闭状态,和被动监听连接的状态。
DIY TCP/IP的TCP模块将所有的TCP连接存放在一个tcp_conn_t类型的数组中,做为TCP模块的内部静态变量使用。TCP模块新增的接口函数有tcp_init,tcp_deinit用于初始化TCP连接数组,和释放连接数组占用的内存。tcp_conn_open,tcp_conn_close用于打开和关闭TCP连接。新增的接口函数声明在tcp.h中暴露给其他模块使用,函数实现在tcp.c中,先来看函数声明,再介绍每个函数的具体实现。
#ifndef _TCP_H_
#define _TCP_H_
…
int tcppkt_recv(void *pkt, unsigned int sz);
int tcp_init();
void tcp_deinit();
int tcp_conn_open(unsigned short port);
int tcp_conn_close(int idx);
#endif
line 2到line4略去的内容与9.3节一样,高亮显示的部分是本节新增的接口函数的声明。tcp_init没有入参,返回值为0,表示成功初始化TCP连接数组。tcp_deinit没有入参也没有返回值,这两个函数在初始化模块init.c中被调用。tcp_conn_open入参为TCP port,返回值是成功创建的TCP连接在连接数组中的下标,失败时返回负值,tcp_conn_close入参为TCP连接数组的下标,成功时返回0,失败时返回负值。
static tcp_conn_t *tcp_connections = NULL;
static int tcp_conn_num = 0;
…
int tcp_init()
{
int ret = 0;
if (tcp_connections == NULL) {
tcp_connections = (tcp_conn_t *)malloc(sizeof(tcp_conn_t));
if (tcp_connections == NULL) {
log_printf(ERROR, "No memory for TCP module\n");
return -1;
}
memset(tcp_connections, 0, sizeof(tcp_conn_t));
tcp_conn_num = 1;
}
return ret;
}
void tcp_deinit()
{
if (tcp_connections == NULL)
return;
log_printf(INFO, "Destroy TCP Connections\n");
/* TBD: flush packets in rcv window */
free(tcp_connections);
}
Line 1-2: 定义TCP模块的静态变量TCP连接数组,和数组中的连接数目。line82-136略之间略去的部分是tcp_conn_open和tcp_conn_close的实现,稍后介绍。
Line 6-17: TCP模块的初始化函数,目前只用于初始化TCP连接数组,初始化只申请存放一个TCP连接的内存空间,连接数目也初始化为1,通过tcp_conn_open打开新的TCP连接时,扩展连接数组tcp_connections占用的内存空间和数组中的连接数目。将TCP连接的各个成员值初始化为0,tcp_conn_t的state成员值也为0,对应STATE_CLOSED状态。
Line 19-26: TCP模块的销毁函数,目前是用于释放TCP连接数组占用的内存空间,TBD(to be done)的注释部分用于扩展处理各个TCP连接的接收缓存中尚未处理的TCP数据帧。
int tcp_conn_open(unsigned short port)
{
int ret = 0, i;
tcp_conn_t *conn = NULL;
if (tcp_connections == NULL) {
log_printf(ERROR, "TCP module not initialized\n");
return -1;
}
for (i = 0; i < tcp_conn_num; i ++) {
if (tcp_connections[i].l_port == port) {
ret = i;
goto out;
}
}
for (i = 0; i < tcp_conn_num; i ++) {
if (tcp_connections[i].state == STATE_CLOSED) {
conn = &tcp_connections[i];
ret = i;
}
}
if (conn == NULL) {
tcp_connections = realloc(tcp_connections,
(tcp_conn_num + 1) * sizeof(tcp_conn_t));
if (tcp_connections == NULL) {
log_printf(ERROR, "No memory for TCP connection\n");
ret = -1;
goto out;
}
tcp_conn_num += 1;
conn = &tcp_connections[i];
memset(conn, 0, sizeof(tcp_conn_t));
ret = i;
}
conn->state = STATE_LISTEN;
conn->l_port = port;
conn->l_wnd_sz = 65535;
conn->l_shift_cn = 8;
conn->l_mss = get_ifmtu(DEFAULT_IFNAME)
- sizeof(iphdr_t) - sizeof(tcphdr_t);
log_printf(INFO, "TCP listen on port: %u\n", conn->l_port);
out:
return ret;
}
tcp_conn_open用于打开一个TCP连接,DIY TCP/IP的虚拟IP地址本身就是局域网中不存在的IP地址,与虚拟IP地址对应的端口号也是不存在的,所以可以指定需要的端口号,不必担心与Linux kernel中已经使用的端口号发生冲突。
Line 1-41: 首先判断连接数组tcp_connections是否已经初始化,不为空时继续执行。遍历tcp_connections连接数组,查找指定的端口号是否已经存在,如果存在,直接返回对应数组下标。如果指定端口号不存在,再次遍历连接数组,查找数组中可以使用的TCP连接,即处于STATE_CLOSED状态的连接,如果找到,保存数组下标做为返回值。如果没有找到,则说明连接数组中已经没有空闲连接,通过realloc扩展连接数组,扩展的增量为1。代码执行到line120行时,已经确定了空闲连接的下标,初始化新增连接的状态为STATE_LISTEN监听指定的TCP端口号,初始化l_wnd_sz为最大值65535,l_shift_cn为8,即设置接收缓存为65535 << 8 = 16M。通过utils模块的新增函数get_ifmtu获取指定网络接口的MTU数值,一般情况下网络接口的MTU为1500,MTU减去IP头部的20个字节,和TCP头部不带任何选项时的20个字节,得到TCP连接本地端的MSS,最后打印新增连接的监听端口号,返回TCP连接在连接数组中的下标值。
int tcp_conn_close(int idx)
{
int ret = 0;
if (idx >= tcp_conn_num && idx < 0)
ret = -1;
tcp_connections[idx].state = STATE_CLOSED;
/* TBD: close TCP connection, flush packets in rcv window */
return ret;
}
tcp_conn_close判定下标值合法后,将对应连接状态设置为STATE_CLOSED关闭状态,使TCP连接可以循环使用。TBD (to be done)的注释部分,用于扩展处理TCP连接的接收缓存中尚未处理的TCP数据帧。
本节除了新增接口函数之外,由于引入了TCP连接数据结构,TCP数据帧的接收和TCP头部选项内容的解析函数都在9.3节的基础上对做了相应的修改。先来看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;
}
/* TBD: find established tcp connection */
/* 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;
}
if (tcp_parse_options(conn, tcppkt, tcppkt_len)) {
log_printf(ERROR, "Failed to parse TCP options\n");
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));
out:
return ret;
}
Line 8: 对比9.3节,将data_offset和data_len局部变量移除,新增tcp_conn_t *指针,保存查找到的TCP连接的地址,没有高亮显示的代码与9.3节一致。
Line 31-40: TBD(to be done)的注释部分用于扩展在已经建立TCP连接的情况下,处理收到TCP数据帧的情况,TCP数据帧的处理放在滑动窗口的实现一节介绍。本节默认的情况是没有建立TCP连接,首先根据TCP头部的目标端口号,查找是否存在TCP连接正在监听目标端口号,这里不用比较目标IP地址,是因为IP模块在接收TCP数据帧时,已经判断过,目标IP地址是DIY TCP/IP的虚拟IP地址,所以只需根据目标端口号查找被动监听的TCP连接,如果没有找到监听目标端口号的连接,则丢弃该TCP数据帧。
Line 41-50: 执行到这里,说明找到了监听目标端口的TCP连接,9.3节的tcp_prase_options函数只是解析了TCP SYN头部的选项字段,并打印输出,避免重复解析选项字段的值,将解析过的选项如MSS,SACK等保存到TCP连接中备用。tcp_prase_options基于9.3节也做了相应的修改,tcp_parse_options函数返回成功时,将IP头部的源IP地址和目标IP地址保存在TCP连接中。
static tcp_conn_t *tcp_find_listen_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_LISTEN) {
conn = &tcp_connections[i];
break;
}
}
return conn;
}
tcp_find_listen_conn入参是目标端口,成功时返回指向TCP连接数组中查找到的TCP连接的指针,失败时返回空。函数体遍历tcp_connections数组,查找与入参端口号相等的TCP连接,且TCP连接处于STATE_LISTEN监听状态。
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;
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 ((popt = get_opt(tcppkt->options,
opt_sz, TCP_OPT_MSS))) {
MSS = NTOHS(*(unsigned short *)popt);
}
if ((popt = get_opt(tcppkt->options,
opt_sz, TCP_OPT_SACK))) {
SACK = 1;
}
if ((popt = get_opt(tcppkt->options,
opt_sz, TCP_OPT_WSCALE))) {
shift_count = *(unsigned char *)popt;
}
if ((popt = get_opt(tcppkt->options,
opt_sz, TCP_OPT_TIMESTAMP))) {
TSval = NTOHL(*(unsigned int *)popt);
TSecr = NTOHL(*((unsigned int *)&popt[4]));
}
conn->r_port = NTOHS(tcppkt->src_port);
conn->l_port = NTOHS(tcppkt->dst_port);
conn->r_wnd_sz = NTOHS(tcppkt->wnd_sz);
conn->r_mss = MSS;
conn->r_sack = SACK;
conn->r_shift_cn = shift_count;
conn->time_echo = TSval;
conn->next_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\n",
conn->r_port, conn->l_port,
NTOHL(tcppkt->seq_num), conn->r_wnd_sz,
data_len, MSS, SACK,
conn->time_echo, TSecr);
return ret;
}
Line 4-39: tcp_parse_options入参为conn,指向查找到的TCP连接的指针,tcppkt指向接收到的TCP数据帧的首字节地址,pkt_len为TCP数据帧的长度,包括TCP头部的长度和TCP数据部分的长度。tcp_prase_options的参数不同于9.3节,选项字段的长度opt_sz,TCP数据部分的长度data_len,都放在tcp_parse_options函数内部计算,与9.3节tcppkt_recv函数中的计算方法一致。另外添加了对TIMESTAMP,Window Scale选项的解析。
Line 40-47: 初始化TCP连接的各个成员,从TCP数据帧头部获取源端口值,目标端口值,赋值给TCP连接的r_port (remote port )和l_port (local port)。TCP数据帧头部的window size赋值给r_wnd_sz,保存remote端接收缓存的大小。r_mss, r_sack, r_shift_cn分别保存remote端的Max Segment Size,是否支持SACK,Window Scale的数值,r_wnd_sz << r_shift_cn可以计算出remote端扩展后的接收缓存大小。time_echo保存接收到的TCP数据帧的时间戳,回复对应的TCP ACK时,用于填充TCP ACK头部的timestamp选项的timestamp echo reply字段。conn->next_seq字段的值是希望收到的下一个TCP数据帧的Sequence Number,是当前收到的TCP数据帧的Sequence Number加上,当前收到的TCP数据帧的数据部分的长度。
Line 49-56: 打印输出接收到的TCP数据帧的头部信息,和解析后的选项值的信息。
本节的最后一处修改是DIY TCP/IP的初始化模块init.c,添加TCP模块接口函数的调用,高亮显示的部分是本节的新增代码,其余代码与init模块最近一次在7.2节的修改一致。
int main(int argc, char *argv[])
{
int c, ret = 0;
unsigned char ip_cfg = 0;
unsigned char fake_ip[4];
net_device_t *ndev = NULL;
unsigned char *tcp_port = NULL;
if (argc <= 1) {
log_printf(WARNING, "No option configured for DIY User Space TCP/IP stack\n");
goto skip_option;
}
for (;;) {
c = getopt(argc, argv, "i:p:h");
if (c < 0) {
if (optind <= (argc - 1)) {
log_printf(ERROR, "invalid option '%s' found\n", argv[optind]);
ret = -1;
goto out;
}
break;
}
switch (c) {
case 'i':
ip_cfg = 1;
dotip2rawbyte(optarg, fake_ip, sizeof(fake_ip));
break;
case 'p':
tcp_port = optarg;
break;
case 'h':
print_help();
goto out;
default:
log_printf(ERROR, "invalid option: %s\n", optarg);
ret = -1;
goto out;
}
}
skip_option:
signal(SIGINT, signal_handler);
ndev = netdev_init(DEFAULT_IFNAME);
if (ndev == NULL) {
ret = -1;
goto out;
}
if (ip_cfg)
netdev_set_ipaddr(ndev, fake_ip, sizeof(fake_ip));
pdbuf_init();
arp_init();
tcp_init();
if (tcp_port)
tcp_conn_open((unsigned short)atoi(tcp_port));
netdev_start_loop(ndev);
out:
if (ndev)
netdev_deinit(ndev);
arp_deinit();
tcp_deinit();
pdbuf_deinit();
return ret;
}
Line 7: 新增unsigned char *类型的指针tcp_port,指向getopt获取到的-p选项的参数值,做为初始化TCP连接的端口号。
Line 54-56: 调用tcp_init初始化TCP模块,按照本节tcp_init的实现,只是初始化了TCP连接数组。通过C的库函数atoi将tcp_port指向的字符串的值,转换为整型值,传入tcp_conn_open时再强制转换为无符号16位整型值,初始化一条TCP连接,监听tcp_port,等待remote端发起TCP连接请求。tcp_port的数值只要在16位无符号整型值的数值范围内即可,tcp_port端口号是与DIY TCP/IP的虚拟IP地址相关的,实际上虚拟IP地址和tcp_port端口号对于运行DIY TCP/IP的主机A上的linux kernel 而言都是不存在的,所以不必担心指定的端口号与Linux kernel中正在使用的端口号发生冲突。
Line 62: 销毁DIY TCP/IP时,调用tcp_deinit,释放TCP模块占用的内存,按照本节的实现,tcp_deinit释放TCP连接数组占用的内存。
验证本节代码实现之前,再来看一下utils模块新增的两个函数,get_ifmtu和get_currenttime。get_ifmtu已经在tcp_conn_open函数中被用到,get_currenttime本节暂未使用,该函数将在9.7节发送TCP数据帧时用到。utils模块的函数实现都在utils.c文件中。
/* ifreq: /usr/include/net/if.h */
int get_ifmtu(char *ifname)
{
int fd = 0;
int ifmtu = -1;
struct ifreq ifr;
unsigned int ifname_sz;
if (ifname == NULL)
return ifmtu;
if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("Failed to create socket fd: %s (%d)\n",
strerror(errno), errno);
goto out;
}
memset(&ifr, 0, sizeof(ifr));
strncpy(ifr.ifr_name, ifname, MIN(strlen(ifname) + 1, sizeof(ifr.ifr_name)));
if (ioctl(fd, SIOCGIFMTU, &ifr) < 0) {
printf("Failed to get interface: %s mtu %s (%d)\n",
ifr.ifr_name, strerror(errno), errno);
goto out;
}
ifmtu = ifr.ifr_mtu;
out:
if (fd)
close(fd);
return ifmtu;
}
get_ifmtu与7.2节中获取硬件地址的函数get_hw_addr一致,都是通过ioctl系统调用获取指定网络设备ifname的参数,命令字为SIOCGIFMTU。7.2节中已经详细介绍了get_hw_addr的实现,get_ifmtu仅仅是命令字不同。
/* get current time in usec */
unsigned int get_current_time(void)
{
#define MAX_UINT32_VAL (0xFFFFFFFFU)
int ret;
unsigned long usec = 0;
struct timespec now;
if(clock_gettime(CLOCK_REALTIME, &now) < 0) {
printf("Failed to get clock time, %s (%d)\n", strerror(errno), errno);
return MAX_UINT32_VAL;
}
usec = now.tv_sec * 1000000;
usec += (now.tv_nsec / 1000);
/* adjust unsigned long timestamp to the scope of unsigned int */
if (usec > MAX_UINT32_VAL)
usec %= MAX_UINT32_VAL;
return (unsigned long)usec;
}
get_current_time获取当前系统时间,通过librt的库函数clock_gettime实现,获取指定clock_id的时间。这里指定的clock_id是CLOCK_REALTIME,即wall-time,是系统的真实时间。clock_gettime的返回值存放在timespec结构体中,精度是纳秒,将其转换成微妙存放在无符号长整型变量usec中。TCP的timestamp选项存放本地时间戳的字段是4个字节,usec超过32位无符号整型的最大值时,取2^32 – 1的余数,做为返回值。
9.4节已经完成了TCP数据帧头部校验和的检验,能正确接收TCP数据帧,并解析出主机C上运行的iperf客户端发出的TCP SYN的目标端口是iperf TCP server的默认目标端口5001。本节测试在9.4节接收TCP数据帧的基础上,查找TCP SYN目标端口号对应的TCP连接,并打印TCP SYN数据帧的信息。测试方法与9.4节一致,主机A上运行DIY TCP/IP,指定虚拟IP地址,和TCP端口号,命令行为./tcp_ip_stack -i <虚拟IP地址A> -p <端口号P>,与主机A处于同一局域网的Android手机,主机C上运行iperf client端,命令行为iperf -c <虚拟IP地址A> -i 1 -t 43200 -p <端口号P>。
Android手机主机C,运行iperf的截屏如下
主机C运行iperf TCP客户端,指定TCP Server的IP地址是192.168.1.7,端口号为7000。iperf客户端没有打印任何Log,说明该IP地址在局域网中不存在。
同一时间段,运行在主机A上的tcpdump的结果如下:
主机A上tcpdump捕获eth0网络接口的数据帧,与DIY TCP/IP中pcap_open_live指定的网络接口一致。-s 0指定存放数据包的缓存为最大值65535,-w指定将捕获的数据帧存入tcp_recv.pcap文件。从tcpdump的结果来看,一共捕获到了11个数据帧。
同一时间段,运行在主机A上的DIY TCP/IP的结果如下:
从运行结果可以看出,DIY TCP/IP的TCP模块一共收到了4个TCP数据帧,说明TCP头部校验和检验正确,并查找到了监听目标端口号的TCP连接。打印结果显示收到的TCP数据帧的源端口号是47031,目标端口号是7000。4个TCP数据帧的Sequence Number都一样,说明后三个TCP数据帧是重传的,Window Size是65535,Window Scale是8 (1 << 8),数据部分的长度为0,MSS是1460,支持SACK选项,第一个TCP数据帧的时间戳是4770872,后面三个TCP数据帧的时间戳是不断增长的,间隔为100ms,200ms,400ms指数级增长。来看主机A上tcpdump的结果,对比DIY TCP/IP的运行结果。
主机A上tcpdump的结果在wireshark中打开如下
从wireshark的分析中可以看到,主机C(192.168.1.150)首先发出了ARP Reuqest,然后主机A上运行的的DIY TCP/IP正确回复了ARP Reply,建立了虚拟IP地址192.168.1.7与eth0硬件地址的映射。主机C又发出了4个TCP SYN数据帧,其中有3帧是黑色背景,红色前景显示,说明3个TCP SYN是重传的。通过Sequence number也可以看出这四个TCP SYN的序号一样,MSS为1460,支持SACK选项。
这里TCP数据帧的Sequence number被wireshark友好的解析为0,将第一个TCP数据帧展开,来看Sequence number是否与DIY TCP/IP的打印输出一致。
展开后的TCP数据帧的头部可以看到,sequence number 为0,后面括号中的内容是relative sequence number,这是wireshark软件解析的结果,说明该序号是相对值。在右边的raw byte显示栏可以看到sequence number的值为0x2b0ab8df(722122975十进制), TCP数据帧携带的数据长度为0,源端口,目标端口,Window Size 和Window Scale以及TCP头部中各个选项的值都与DIY TCP/IP的输出结果一致。
下一篇:DIY TCP/IP TCP模块的实现3