基于tcpdump实例讲解TCP/IP协议

来源:互联网 发布:yum安装软件原理 编辑:程序博客网 时间:2024/06/05 11:25

http://www.cnblogs.com/ggjucheng/archive/2012/02/02/2335495.html

前言

虽然网络编程的socket大家很多都会操作,但是很多还是不熟悉socket编程中,底层TCP/IP协议的交互过程,本文会一个简单的客户端程序和服务端程序的交互过程,使用tcpdump抓包,实例讲解客户端和服务端的TCP/IP交互细节。

TCP/IP协议

IP头和TCP头格式如下:

复制代码
Internet Header Format    0                   1                   2                   3               0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+   |Version|  IHL  |Type of Service|          Total Length         |   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+   |         Identification        |Flags|      Fragment Offset    |   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+   |  Time to Live |    Protocol   |         Header Checksum       |   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+   |                       Source Address                          |   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+   |                    Destination Address                        |   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+   |                    Options                    |    Padding    |   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+   TCP Header Format    0                   1                   2                   3       0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+   |          Source Port          |       Destination Port        |   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+   |                        Sequence Number                        |   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+   |                    Acknowledgment Number                      |   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+   |  Data |           |U|A|P|R|S|F|                               |   | Offset| Reserved  |R|C|S|S|Y|I|            Window             |   |       |           |G|K|H|T|N|N|                               |   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+   |           Checksum            |         Urgent Pointer        |   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+   |                    Options                    |    Padding    |   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+   |                             data                              |   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
复制代码

单单看这些头会比较枯燥,后面会根据一个简单的客户端和服务端的TCP/IP报文交互实例讲解这些报文头的格式和含义


简单的客户端和服务端

客户端和服务端是我们写的一个简单客户端程序,运行在Linux上。

客户端和服务端的功能如下:
客户端从标准输入读入一行,发送到服务端
服务端从网络读取一行,然后输出到客户端
客户端收到服务端的响应,输出这一行到标准输出

服务端代码如下:

复制代码
#include  <unistd.h>#include  <sys/types.h>       /* basic system data types */#include  <sys/socket.h>      /* basic socket definitions */#include  <netinet/in.h>      /* sockaddr_in{} and other Internet defns */#include  <arpa/inet.h>       /* inet(3) functions */#include <stdlib.h>#include <errno.h>#include <stdio.h>#include <string.h>#define MAXLINE 1024//typedef struct sockaddr  SA;void handle(int connfd);int  main(int argc, char **argv){    int     listenfd, connfd;    int  serverPort = 6888;    int listenq = 1024;    pid_t   childpid;    char buf[MAXLINE];    socklen_t socklen;    struct sockaddr_in cliaddr, servaddr;    socklen = sizeof(cliaddr);    bzero(&servaddr, sizeof(servaddr));    servaddr.sin_family = AF_INET;    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);    servaddr.sin_port = htons(serverPort);    listenfd = socket(AF_INET, SOCK_STREAM, 0);    if (listenfd < 0) {        perror("socket error");        return -1;    }    if (bind(listenfd, (struct sockaddr *) &servaddr, socklen) < 0) {        perror("bind error");        return -1;    }    if (listen(listenfd, listenq) < 0) {        perror("listen error");            return -1;    }    printf("echo server startup,listen on port:%d\n", serverPort);    for ( ; ; )  {        connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &socklen);        if (connfd < 0) {            perror("accept error");            continue;        }        sprintf(buf, "accept form %s:%d\n", inet_ntoa(cliaddr.sin_addr), cliaddr.sin_port);        printf(buf,"");        childpid = fork();        if (childpid == 0) { /* child process */            close(listenfd);    /* close listening socket */            handle(connfd);   /* process the request */            exit (0);        } else if (childpid > 0)  {            close(connfd);          /* parent closes connected socket */        } else {            perror("fork error");        }    }}void handle(int connfd){    size_t n;    char    buf[MAXLINE];    for(;;) {        n = read(connfd, buf, MAXLINE);        if (n < 0) {            if(errno != EINTR) {                perror("read error");                break;            }        }        if (n == 0) {            //connfd is closed by client            close(connfd);            printf("client exit\n");            break;        }        //client exit        if (strncmp("exit", buf, 4) == 0) {            close(connfd);            printf("client exit\n");            break;        }        write(connfd, buf, n); //write maybe fail,here don't process failed error    } } 
复制代码

 

客户端代码如下:

复制代码
#include  <unistd.h>#include  <sys/types.h>       /* basic system data types */#include  <sys/socket.h>      /* basic socket definitions */#include  <netinet/in.h>      /* sockaddr_in{} and other Internet defns */#include  <arpa/inet.h>       /* inet(3) functions */#include <netdb.h> /*gethostbyname function */#include <stdlib.h>#include <errno.h>#include <stdio.h>#include <string.h>#define MAXLINE 1024void handle(int connfd);int main(int argc, char **argv){    char * servInetAddr = "127.0.0.1";    int servPort = 6888;    char buf[MAXLINE];    int connfd;    struct sockaddr_in servaddr;    if (argc == 2) {        servInetAddr = argv[1];    }    if (argc == 3) {        servInetAddr = argv[1];        servPort = atoi(argv[2]);    }    if (argc > 3) {        printf("usage: echoclient <IPaddress> <Port>\n");        return -1;    }    connfd = socket(AF_INET, SOCK_STREAM, 0);    bzero(&servaddr, sizeof(servaddr));    servaddr.sin_family = AF_INET;    servaddr.sin_port = htons(servPort);    inet_pton(AF_INET, servInetAddr, &servaddr.sin_addr);    if (connect(connfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) {        perror("connect error");        return -1;    }    printf("welcome to echoclient\n");    handle(connfd);     /* do it all */    close(connfd);    printf("exit\n");    exit(0);}void handle(int sockfd){    char sendline[MAXLINE], recvline[MAXLINE];    int n;    for (;;) {        if (fgets(sendline, MAXLINE, stdin) == NULL) {            break;//read eof        }        /*        //也可以不用标准库的缓冲流,直接使用系统函数无缓存操作        if (read(STDIN_FILENO, sendline, MAXLINE) == 0) {            break;//read eof        }        */        n = write(sockfd, sendline, strlen(sendline));        n = read(sockfd, recvline, MAXLINE);        if (n == 0) {            printf("echoclient: server terminated prematurely\n");            break;        }        write(STDOUT_FILENO, recvline, n);        //如果用标准库的缓存流输出有时会出现问题        //fputs(recvline, stdout);    }}
复制代码

下载地址

编译服务器:

gcc echoserver.c -o echoserver

编译客户端

gcc echoclient.c -o echoclient

 

TCP/IP连接建立,交互,关闭

首先我们要启用tcpdump监控客户端和服务端的报文:

tcpdump -S -nn -vvv -i lo port 6888

-S 打印TCP 数据包的顺序号时, 使用绝对的顺序号, 而不是相对的顺序号

 -nn 表示不进行端口到名称的转换

-vvv 表示产生尽可能详细的协议输出

-i lo表示只监控网卡lo设备,默认是监控第一个网络设备。

port 6888表示只监控端口6888的相关监控数据,包括从6888端口接收和从6888端口发送的报文。

tcpdump详情可以参考本博客的"Linux tcpdump命令详解"的tcpdump的简单选项介绍。

接着我们要启动服务端:

./echoserver

再启动客户端:

./echoclient 

建立连接

客户端程序一启动,就会connect服务端,tcpdump对应的输出如下:

13:27:45.927137 IP (tos 0x0, ttl  64, id 304, offset 0, flags [DF], proto: TCP (6), length: 60) 127.0.0.1.60534 > 127.0.0.1.6888: S, cksum 0x5f32 (correct), 2584692379:2584692379(0) win 32792 <mss 16396,sackOK,timestamp 10962859 0,nop,wscale 6>13:27:45.927254 IP (tos 0x0, ttl  64, id 0, offset 0, flags [DF], proto: TCP (6), length: 60) 127.0.0.1.6888 > 127.0.0.1.60534: S, cksum 0x3648 (correct), 2589673026:2589673026(0) ack 2584692380 win 32768 <mss 16396,sackOK,timestamp 10962860 10962859,nop,wscale 6>13:27:45.927265 IP (tos 0x0, ttl  64, id 305, offset 0, flags [DF], proto: TCP (6), length: 52) 127.0.0.1.60534 > 127.0.0.1.6888: ., cksum 0x1d6a (correct), 2584692380:2584692380(0) ack 2589673027 win 513 <nop,nop,timestamp 10962860 10962860>

这里是TCP连接的三握手的报文交互,其中协议各个字段的含义如下:

tos表示服务类型,4bit的tos分别表示最小时延,最大吞吐量,最高可靠性,最小费用。这里都是0,表示一般服务,其余4bit废用,置0.

TTL(time - to - live)生存时间字段设置了数据报可以经过的最多路由器数。它指定了数据报的生存时间。TTL的初始值由源主机设置(通常为32或64),一旦经过一个处理它的路由器,它的值就减去1。当该字段的值为0时,数据报就被丢弃,并发送ICMP报文通知源主机。
id 对应IP报文头的Identification,用于IP分片重组。

offset 也用于IP分片重组,表示相对于原始未分片的报文的位置。

flags MF表示有更多分片,DF表示不分片,这里是DF,未使用分片,所以id和offset的值都可以忽略。

proto 表示协议,可以是TCP,UDP等,这里是TCP。

length 总长度字段,是指整个I P数据报的长度(至于首部长度这里没有给出,首部长度给出首部中32 bit字的数目。需要这个值是因为任选字段的长度是可变的。这个字段占4 bit,因此TCP最多有60字节的首部。然而没有任选字段,正常的长度是20字节)。

127.0.0.1.60534 > 127.0.0.1.6888表示数据是从IP为127.0.0.1端口为60534发送到IP为127.0.0.1端口为6888。分别对应的IP报文头的源地址和目的地址,以及TCP报文头的源端口和目的端口。

S 当建立一个新的连接时,SYN标志变1。序号字段包含由这个主机选择的该连接的初始序号ISN(Initial Sequence Number)。该主机要发送数据的第一个字节序号为这个ISN加1,因为SYN标志消耗了一个序号,这里客户端的ISN是2584692379,服务端的ISN是2589673026。

chksum 16位检验和,这里有IP首部检验和和TCP报文段(包括TCP首部和数据)检验和,具体是哪个检验和不详。

2584692379:2584692379(0)表示,第一个2584692379表示TCP报文段的序列号,(0)表示数据长度是0,即没有数据,第二个2584692379是第一个2584692379+数据长度  计算出来的。TCP是可靠连接,三握手的最大目的是为了初始化双方的ISN。假设客户端连接服务端,发送数据,刚好网络比较慢,在传输过程中,客户端和服务端已经都重启了,重新建立连接发送数据,发送过程中,服务端收到已经之前客户端的数据,发现ISN非法,就会抛弃这个包,不会对现有的服务造成影响。这个只是ISN的一方面的作用。

win TCP窗口大小,通知对方,发送方最多还可以接收的数据量,用于TCP的拥塞控制。第一个报文表示客户端通知服务端,客户端可以接受的数据的缓存区最大是32792个字节。服务端通知客户端,服务端最多可以接受的缓存区最大是32768,这个窗口大小在一方接受数据,却没有read的时候,窗口会逐渐减小,直至为0,最后对方不可以发送任何数据(如果要做该测试,需要发送的数据量大概接近65535,因为窗口的缓存区也会在剩余容量减小时,自动增加总共容量,直到总共容量接近65535,接下来就会看到win越来越小,直至0)。

ack TCP是可靠连接,所以收到发送方的数据,接受方就会发送ack确认,告诉发送方,接受方已经接收到数据,否则,发送方认为数据没有发送成功,重复发送数据。第二个包有ack 2584692380,其中2584692380是第一个报文包的2584692379:2584692379(0)的第二个2584692379+1的值。

<mss 16396,sackOK,timestamp 10962859 0,nop,wscale 6>这里表示IP报文头的可选字段,mss是最小最大分段大小,这里是16396,表示一个TCP报文段发送的数据最大可以是16396个字节,可能是lo设备的关系,这个mss很大,一般都是MTU 1500 个字节 - IP数据报文头20个字节- TCP报文头20个字节 = 1460个字节。wscale是TCP窗口扩大选项的窗口扩大因子,用于扩大TCP通告窗口,使TCP的窗口定义从16bit增加为32bit。这里的wscale是6,那么实际窗口是513左移6位,既513 X 64 = 32832,这个选项只在一个SYN报文中有意义。其他选项不详,具体参考RFC。

交互

建立连接之后,我们通过客户端分别发送a和123到服务端,客户端后台显示如下:

[root@localhost simpletcpip]# ./echoclient welcome to echoclientaa123123

tcpdump对应的输出是:

复制代码
13:27:48.248592 IP (tos 0x0, ttl  64, id 306, offset 0, flags [DF], proto: TCP (6), length: 54) 127.0.0.1.60534 > 127.0.0.1.6888: P, cksum 0xfe2a (incorrect (-> 0xb344), 2584692380:2584692382(2) ack 2589673027 win 513 <nop,nop,timestamp 10965181 10962860>13:27:48.248739 IP (tos 0x0, ttl  64, id 495, offset 0, flags [DF], proto: TCP (6), length: 52) 127.0.0.1.6888 > 127.0.0.1.60534: ., cksum 0x0b47 (correct), 2589673027:2589673027(0) ack 2584692382 win 512 <nop,nop,timestamp 10965181 10965181>13:27:48.249061 IP (tos 0x0, ttl  64, id 496, offset 0, flags [DF], proto: TCP (6), length: 54) 127.0.0.1.6888 > 127.0.0.1.60534: P, cksum 0xfe2a (incorrect (-> 0xaa32), 2589673027:2589673029(2) ack 2584692382 win 512 <nop,nop,timestamp 10965181 10965181>13:27:48.249085 IP (tos 0x0, ttl  64, id 307, offset 0, flags [DF], proto: TCP (6), length: 52) 127.0.0.1.60534 > 127.0.0.1.6888: ., cksum 0x0b43 (correct), 2584692382:2584692382(0) ack 2589673029 win 513 <nop,nop,timestamp 10965182 10965181>13:27:49.544830 IP (tos 0x0, ttl  64, id 308, offset 0, flags [DF], proto: TCP (6), length: 56) 127.0.0.1.60534 > 127.0.0.1.6888: P, cksum 0xfe2c (incorrect (-> 0xa1eb), 2584692382:2584692386(4) ack 2589673029 win 513 <nop,nop,timestamp 10966477 10965181>13:27:49.544987 IP (tos 0x0, ttl  64, id 497, offset 0, flags [DF], proto: TCP (6), length: 56) 127.0.0.1.6888 > 127.0.0.1.60534: P, cksum 0xfe2c (incorrect (-> 0x9cd8), 2589673029:2589673033(4) ack 2584692386 win 512 <nop,nop,timestamp 10966477 10966477>13:27:49.545010 IP (tos 0x0, ttl  64, id 309, offset 0, flags [DF], proto: TCP (6), length: 52) 127.0.0.1.60534 > 127.0.0.1.6888: ., cksum 0x011c (correct), 2584692386:2584692386(0) ack 2589673033 win 513 <nop,nop,timestamp 10966477 10966477>
复制代码

第一个报文是客户端给服务端发送了一个a数据,P表示push标志,发送方使用该标志通知接收方将所收到的数据全部提交给接收进程。这里的数据包括与PUSH一起传送的数据以及接收方TCP已经为接收进程收到的其他数据。长度为54是因为a数据后面还有一个\n(输入a然后回车导致的),所以是2个字节,比正常情况下没有数据的52个字节多了2个字节。

第二个报文是服务端接受了a数据后,发送给客户端的ack确认。

第三个报文和第四个报文分别是服务端发给客户端的回显a,以及客户端的ack确认。

第五个报文是客户端给服务端发送了123数据。

第六个报文,是服务端给客户端发送回显123,同时合并了对客户端的ack确认。

第七个报文是客户端对服务端的回显123数据的ack确认。

关闭连接

直接在客户端的控制终端执行Ctrl+C结束客户端程序,就可以关闭TCP连接,tcpdump输出如下:

13:38:10.081895 IP (tos 0x0, ttl  64, id 310, offset 0, flags [DF], proto: TCP (6), length: 52) 127.0.0.1.60534 > 127.0.0.1.6888: F, cksum 0x897d (correct), 2584692386:2584692386(0) ack 2589673033 win 513 <nop,nop,timestamp 11586913 10966477>13:38:10.081987 IP (tos 0x0, ttl  64, id 498, offset 0, flags [DF], proto: TCP (6), length: 52) 127.0.0.1.6888 > 127.0.0.1.60534: F, cksum 0x11e0 (correct), 2589673033:2589673033(0) ack 2584692387 win 512 <nop,nop,timestamp 11586913 11586913>13:38:10.081993 IP (tos 0x0, ttl  64, id 311, offset 0, flags [DF], proto: TCP (6), length: 52) 127.0.0.1.60534 > 127.0.0.1.6888: ., cksum 0x11df (correct), 2584692387:2584692387(0) ack 2589673034 win 513 <nop,nop,timestamp 11586913 11586913>

关闭的请求由客户端发出,第一个报文是客户端发给服务端,F表示Fin,发送关闭连接请求。这个关闭连接请求是由于客户端退出,操作系统回收客户端资源,自动发出的。当然,如果我们在客户端输入Ctrl+D,最后执行close(connfd),关闭连接请求的报文就会发送。

第二个报文是服务端发给客户端的关闭连接请求,当客户端关闭连接是,服务端处理客户端请求的handle函数中

if (n == 0) {            //connfd is closed by client            close(connfd);            printf("client exit\n");            break;}

执行close(connfd),对应的第二个报文就会发送。

第三个报文只是客户端对服务端的关闭连接请求报文的确认。

总结

本文基于tcpdump和一个简单的客户端和服务端,实例讲解了TCP/IP协议,不止有协议的含义,而且有TCP/IP连接的建立,交互,关闭的一些细节。由于篇幅和tcpdump的输出问题,未能将IP协议和TCP协议的报文头的每个字段含义都讲解一次,如果大家希望可以进一步了解,可以参考RFC 791 - Internet Protocol和RFC 793 - Transmission Control Protocol,还有TCP/IP协议卷一的相关内容。

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 吸毒犯戒毒所不收怎么办 信用卡到期人进拘留所怎么办? 人在拘留所信用卡逾期怎么办 探视权不让接走怎么办 医保过了缴费期怎么办 充电宝充手机慢怎么办 执行局执行不到钱怎么办 小孩挖耳朵疼了怎么办 挖耳朵挖破了怎么办 挖耳朵皮挖破了耳鸣怎么办 北京自考证丢啦怎么办 不服检察院的答复函该怎么办 检察院控申答复函不复怎么办 在看守所里疯了怎么办 第一次吸毒拘留五天第二次会怎么办 被派出所拘留15天怎么办 老公从拘留所出来聪明老婆怎么办 中信信用卡忘记还款了怎么办 监狱的犯人病了怎么办 判缓期间在行政拘留怎么办 法院拘留15天工作怎么办 高血压签定了无期限合同怎么办 法院司法拘留找不到人怎么办 c1骑摩托车要拘留怎么办 欠钱的找不到人怎么办 治安拘留人跑了怎么办 行政拘留拘留所不收应该怎么办 在看守所被打了怎么办 取保候审超过12个月怎么办 拘留20天还没有判刑怎么办 被打了没有证据怎么办 拘留37天后没有放人怎么办 强制执行执行的财产不够怎么办 收到公安局拘留通知家属应该怎么办 对方不出谅解书怎么办 寻衅滋事没抓的 怎么办 公务员政审父母拘留过怎么办 我参与了网赌怎么办 亲人出车祸去世家属怎么办 车牌换了原保险怎么办 北京车牌夫妻变更车险怎么办