TCP-IP报文结构分析

学习TCP/IP数据报的结构

推荐课程

实验楼:TCP/IP 网络协议基础


IP

IP简介

image

普通的 IP 数据报的报头长度 20 字节(除非有选项字段),各个部分的作用:

  • 版本号 :4 位,用于标明 IP 版本号,0100 表示 IPv4,0110 表示 IPv6。目前常见的是 IPv4。
  • 首部长度 :4 位,表示 IP 报头长度,标识头部有多少个4字节,包括选项字段。
  • 服务类型(TOS) :8位,分别有:最小时延、最大吞吐量、最高可靠性、最小花费 4 种服务,如下图所示。4 个标识位只能有一个被置为 1。
  • 总长度 :16 位,报头长度加上数据部分长度,便是数据报的总长度。IP 数据报最长可达 65535 字节。
  • 标识 :16 位,接收方根据分片中的标识字段相不相同来判断这些分片是不是同一个数据报的分片,从而进行分片的重组。通常每发送一份报文它的值就会加 1。
  • 标志 :3 位,用于标识数据报是否分片。其中的第 2 位是不分段(DF)位。当 DF 位被设置为 1 时,则不对数据报进行分段处理;第 3 位是分段(MF)位,除了最后一个分段的 MF 位被设置为 0 外,其他的分段的 MF 位均设置为 1。
  • 偏移 :13 位,在接收方进行数据报重组时用来标识分片的顺序。
  • 生存时间(TTL) :8 位,用于设置数据报可以经过的最多的路由器个数。TTL 的初始值由源主机设置(通常为 32 或 64),每经过一个处理它的路由器,TTL 值减 1。如果一个数据报的 TTL 值被减至 0,它将被丢弃。
  • 协议 :8 位,用来标识是哪个协议向 IP 传送数据。ICMP 为 1,IGMP 为 2,TCP 为 6,UDP 为 17,GRE 为 47,ESP 为 50。
  • 首部校验和 :16位 根据 IP 首部计算的校验和码。
  • 源 IP 和目的 IP :32位,数据报头还会包含该数据报的发送方 IP 和接收方 IP。
  • 选项 :是数据报中的一个可变长、可选的信息,不常用,多用于安全、军事等领域。

IP报文分析

1
sudo tcpdump -ntx -c 1
1
2
3
4
4500 0038 106d 4000 4006 2c51 7f00 0001
7f00 0001 1e61 c0ea 1c88 a9f9 d048 2bf6
8018 00e5 fe2c 0000 0101 080a 0017 c23c
8cbd 9fc0 3232 3333

使用十六进行表示,一个数字表示四位

  • 版本号:0x4 –> IPv4
  • 首部长度: 0x5 –> 20字节 –> 160位 –> 40个16进制 –> 10个小段(一直到0001,一段4个十六进制)
  • 服务类型:0x00 –> 一般服务
  • 总长度:0x0038 –> 56字节 –> 448位 –> 112个16进制 –> 28个小段(相符)
  • 标识:0x106d
  • 标志:010 不用做分段处理
  • 偏移:0 0000 0000 0000 0000
  • 生存时间:0x40 –> 64 –> 一般Linux为64
  • 协议:0x06 –> 为TCP协议
  • 首部校验和:0x2c51
  • 源IP:7f000001 –> 127.0.0.1
  • 目标IP: 7f000001 –> 127.0.0.1

TCP

TCP简介

image

  • 源端口和目的端口:各占2字节(16bit),分别写入源端口号和目的端口号。这和 UDP 报头有类似之处,因为都是传输层协议。
  • 序号:占4字节(32bit),序号范围[0,2^32-1],序号增加到 2^32-1 后,下个序号又回到 0。TCP 是面向字节流的,通过 TCP 传送的字节流中的每个字节都按顺序编号,而报头中的序号字段值则指的是本报文段数据的第一个字节的序号。
  • 确认序号:占4字节(32bit),期望收到对方下个报文段的第一个数据字节的序号。
  • 数据偏移:占4位,指 TCP 报文段的报头长度,包括固定的 20 字节和选项字段。
  • 保留:占6位,保留为今后使用,目前为 0。
  • 控制位:共有 6 个控制位,说明本报文的性质,意义如下:
    • URG 紧急:当 URG=1 时,它告诉系统此报文中有紧急数据,应优先传送(比如紧急关闭),这要与紧急指针字段配合使用。
    • ACK 确认:仅当 ACK=1 时确认号字段才有效。建立 TCP 连接后,所有报文段都必须把 ACK 字段置为 1。
    • PSH 推送:若 TCP 连接的一端希望另一端立即响应,PSH 字段便可以“催促”对方,不再等到缓存区填满才发送。
    • RST复位:若 TCP 连接出现严重差错,RST 置为 1,断开 TCP 连接,再重新建立连接。
    • SYN 同步:用于建立和释放连接,稍后会详细介绍。
    • FIN 终止:用于释放连接,当 FIN=1,表明发送方已经发送完毕,要求释放 TCP 连接。
  • 窗口:占2字节(16bit)。窗口值是指发送者自己的接收窗口大小,因为接收缓存的空间有限。
  • 检验和:2字节(16bit)。和 UDP 报文一样,有一个检验和,用于检查报文是否在传输过程中出差错。
  • 紧急指针:2字节(16bit)。当 URG=1 时才有效,指出本报文段紧急数据的字节数。
  • 选项:长度可变,最长可达 40 字节。具体的选项字段,需要时再做介绍。

TCP报文分析

端口7777 – 2233 –> 端口49386

1
2
3
4
4500 0038 106d 4000 4006 2c51 7f00 0001
7f00 0001 1e61 c0ea 1c88 a9f9 d048 2bf6
8018 00e5 fe2c 0000 0101 080a 0017 c23c
8cbd 9fc0 3232 3333

根据之前的分析,TCP部分从1e61开始

  • 源端口: 0x1e61 –> 7777
  • 目标端口: 0xc0ea –> 49386
  • Seq序号: 0x1c88a9f9 –> 478718457
  • Ack确认序号:0xd0482bf6 –> 3494390774
  • 数据偏移(报文头长度):0x8 –> 32字节 –> 16小段 –> 一直到倒数第三个为止为报头
  • 保留:0000 00
  • 控制位:011000
    • URG:0
    • ACK:1
    • PSH:1
    • RST:0
    • SYN:0
    • FIN:0
  • 窗口:0x00e5
  • 检验和:0xfe2c
  • 紧急指针:0x0000
  • Options:0101 080a 0017 c23c 8cbd 9fc0
  • Data: 3232 3333 –> ‘2’ ‘2’ ‘3’ ‘3’

握手和挥手

运行程序,程序见尾部,Linux环境,并执行如下命令

1
sudo tcpdump -vvv -X -i lo tcp port  7777

握手

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
4500 003c 3616 4000 4006 06a4 7f00 0001
7f00 0001 c0ec 1e61 3e47 f97a 0000 0000
a002 aaaa fe30 0000 0204 ffd7 0402 080a
8cd3 b598 0000 0000 0103 0307

4500 003c 0000 4000 4006 3cba 7f00 0001
7f00 0001 1e61 c0ec 9b10 13b9 3e47 f97b
a012 aaaa fe30 0000 0204 ffd7 0402 080a
002d cf57 8cd3 b598 0103 0307

4500 0034 3617 4000 4006 06ab 7f00 0001
7f00 0001 c0ec 1e61 3e47 f97b 9b10 13ba
8010 0156 fe28 0000 0101 080a 8cd3 b598
002d cf57
  • :49386 –> :7777 (a002 –> 1010 0000 0000 00 1(SYN) 0)
    • Seq:0x3e47f97a (X)
    • SYN:1
  • :7777 –> :49386 (a012 –> 1010 0000 0001(ACK) 00 1(SYN) 0)
    • Seq: 0x9b1013b9(Y)
    • Ack: 0x3e47f97b(X+1)
    • SYN:1
    • ACK:1
  • :49386 –> :7777 (8010 –> 1000 0000 0001(ACK) 0000)
    • Seq: 0x3e47f97b(X+1 == Ack)
    • Ack:0x9b1013ba(Y+1)
    • ACK:1

挥手

tcp/ip 四次挥手?no, 还有三次挥手

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
4500 0034 6fc9 4000 4006 ccf8 7f00 0001
7f00 0001 c0ee 1e61 39cf c943 6640 1ca7
8011 0156 fe28 0000 0101 080a 8cd9 9ce7
0033 b0cf

4500 0034 ba28 4000 4006 8299 7f00 0001
7f00 0001 1e61 c0ee 6640 1ca7 39cf c944
8011 00e5 fe28 0000 0101 080a 0033 b6bb
8cd9 9ce7

4500 0034 6fca 4000 4006 ccf7 7f00 0001
7f00 0001 c0ee 1e61 39cf c944 6640 1ca8
8010 0156 fe28 0000 0101 080a 8cd9 9ce7
0033 b6bb
  • :49386 –> :7777(8011 –> 1000 0000 000 1(ACK) 000 1(FIN)
    • Seq:0x39cfc943(U)
  • :7777 –> :49386(8011 –> 1000 0000 000 1(ACK) 000 1(FIN)
    • Seq:0x66401ca7(W)
    • Ack:0x39cfc944(U+1)
    • ACK:1
    • FIN:1
  • :49386 –> :7777(8010 –> 1000 0000 000 1(ACK) 0000)
    • Seq:0x39cfc944(U+1)
    • Ack:0x66401ca8(W+1)
    • ACK:1

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
// server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <unistd.h>

#define BUFLEN 10

int main(int argc, char *argv[])
{
int sockfd, newfd;
struct sockaddr_in s_addr, c_addr;
char buf[BUFLEN];
socklen_t len;
unsigned int port, listnum;

/*建立socket*/
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
perror("socket");
exit(errno);
}
/*设置服务器端口*/
if(argv[2])
port = atoi(argv[2]);
else
port = 7777;
/*设置侦听队列长度*/
if(argv[3])
listnum = atoi(argv[3]);
else
listnum = 3;
/*设置服务器ip*/
bzero(&s_addr, sizeof(s_addr));
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(port);
if(argv[1])
s_addr.sin_addr.s_addr = inet_addr(argv[1]);
else
s_addr.sin_addr.s_addr = INADDR_ANY;
/*把地址和端口帮定到套接字上*/
if((bind(sockfd, (struct sockaddr*) &s_addr,sizeof(struct sockaddr))) == -1){
perror("bind");
exit(errno);
}
/*侦听本地端口*/
if(listen(sockfd,listnum) == -1){
perror("listen");
exit(errno);
}
while(1){
printf("*****************server start***************\n");
len = sizeof(struct sockaddr);
if((newfd = accept(sockfd,(struct sockaddr*) &c_addr, &len)) == -1){
perror("accept");
exit(errno);
}
while(1){
_retry:
/******发送消息*******/
bzero(buf,BUFLEN);
printf("enter your words:");
/*fgets函数:从流中读取BUFLEN-1个字符*/
fgets(buf,BUFLEN,stdin);
/*打印发送的消息*/
//fputs(buf,stdout);
if(!strncasecmp(buf,"quit",4)){
printf("server stop\n");
break;
}
/*如果输入的字符串只有"\n",即回车,那么请重新输入*/
if(!strncmp(buf,"\n",1)){

goto _retry;
}
/*如果buf中含有'\n',那么要用strlen(buf)-1,去掉'\n'*/
if(strchr(buf,'\n'))
len = send(newfd,buf,strlen(buf)-1,0);
/*如果buf中没有'\n',则用buf的真正长度strlen(buf)*/
else
len = send(newfd,buf,strlen(buf),0);
if(len > 0)
printf("send successful\n");
else{
printf("send failed\n");
break;
}
/******接收消息*******/
bzero(buf,BUFLEN);
len = recv(newfd,buf,BUFLEN,0);
if(len > 0)
printf("receive massage:%s\n",buf);
else{
if(len < 0 )
printf("receive failed\n");
else
printf("client stop\n");
break;
}
}
/*关闭聊天的套接字*/
close(newfd);
/*是否退出服务器*/
printf("exit?:y->yes;n->no ");
bzero(buf, BUFLEN);
fgets(buf,BUFLEN, stdin);
if(!strncasecmp(buf,"y",1)){
printf("server stop\n");
break;
}
}
/*关闭服务器的套接字*/
close(sockfd);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <unistd.h>

#define BUFLEN 10

int main(int argc, char *argv[])
{
int sockfd;
struct sockaddr_in s_addr;
socklen_t len;
unsigned int port;
char buf[BUFLEN];

/*建立socket*/
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
perror("socket");
exit(errno);
}
/*设置服务器端口*/
if(argv[2])
port = atoi(argv[2]);
else
port = 7777;
/*设置服务器ip*/
bzero(&s_addr, sizeof(s_addr));
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(port);
if (inet_aton(argv[1], (struct in_addr *)&s_addr.sin_addr.s_addr) == 0) {
perror(argv[1]);
exit(errno);
}
/*开始连接服务器*/
if(connect(sockfd,(struct sockaddr*)&s_addr,sizeof(struct sockaddr)) == -1){
perror("connect");
exit(errno);
}else
printf("*****************client start***************\n");

while(1){
/******接收消息*******/
bzero(buf,BUFLEN);
len = recv(sockfd,buf,BUFLEN,0);
if(len > 0)
printf("receive massage:%s\n",buf);
else{
if(len < 0 )
printf("receive failed\n");
else
printf("server stop\n");
break;
}
_retry:
/******发送消息*******/
bzero(buf,BUFLEN);
printf("enter your words:");
/*fgets函数:从流中读取BUFLEN-1个字符*/
fgets(buf,BUFLEN,stdin);
/*打印发送的消息*/
//fputs(buf,stdout);
if(!strncasecmp(buf,"quit",4)){
printf("client stop\n");
break;
}
/*如果输入的字符串只有"\n",即回车,那么请重新输入*/
if(!strncmp(buf,"\n",1)){

goto _retry;
}
/*如果buf中含有'\n',那么要用strlen(buf)-1,去掉'\n'*/
if(strchr(buf,'\n')) {
if( buf[0] == 'q')
break;
len = send(sockfd,buf,strlen(buf)-1,0);
}
/*如果buf中没有'\n',则用buf的真正长度strlen(buf)*/
else
len = send(sockfd,buf,strlen(buf),0);
if(len > 0)
printf("send successful\n");
else{
printf("send failed\n");
break;
}
}
/*关闭连接*/
close(sockfd);

return 0;
}