计算机网络课程设计之Tracert与Ping程序设计与实现

今天是课程设计的第二天,下午16:49,我在逸夫楼328写第三个实验。

设计题目

Tracert与Ping程序设计与实现

设计内容

设计一个Tracert与Ping程序。

设计步骤

原理分析

Tracert 程序关键是对 IP 头部生存时间(time to live)TTL 字段的使用,程序实现时是向目地主机发送一个 ICMP 回显请求消息,初始时 TTL 等于 1,这样当该数据报抵达途中的第一个路由器时,TTL 的值就被减为 0,导致发生超时错误,因此该路由生成一份 ICMP 超时差错报文返回给源主机。随后,主机将数据报的 TTL 值递增 1,以便 IP 报能传送到下一个路由器,并由下一个路由器生成 ICMP 超时差错报文返回给源主机。不断重复这个过程,直到数据报达到最终的目地主机,此时目地主机将返回 ICMP 回显应答消息。这样,源主机只需对返回的每一份 ICMP 报文进行解析处理,就可以掌握数据报从源主机到达目地主机途中所经过的路由信息。

思路步骤

(1)加载套接字,创建套接字库;使用Socket的程序在使用Socket之前必须调用WSAStartup函数,以后应用程序就可以调用所请求的Socket库中的其他Socket函数了。

(2)用inet_addr()将输入的点分十进制的IP地址转换为无符号长整型数,转换不成功时,按域名解析得到IP地址;

gethostbyname()是查找主机名最基本的函数,如果调用成功,就返回一个指向hosten结构的指针,该结构中含有对应于给定主机名的主机名字和地址信息,用来承接域名解析的结构。

(3)设置发送接收超时时间,即请求超时,设置接收、发送超时的套接字;

(4)构造ICMP回显请求消息,并以TTL递增顺序发送报文,填充ICMP报文中每次发送时不变的字段,构造ICMP头;

(5)设置IP报头的TTL字段,填充ICMP报文中每次发送变化的字段,记录序列号和当前时间;

(6)指定对方信息,发送TCP回显请求信息;sendto()函数利用数据表的方式进行数据传输,指定哪个socket发送给对方

(7)接收ICMP差错报文并进行解析:如果有数据到达,解析数据包,如果到达目的地址,输出IP地址;如果没有数据到达,输出接收超时,递增TTL值,TTL增为最大时,若还没有到达目的地址,退出循环,输出目的地址不在线;

recvform()利用数据报方式进行数据传输,当recvfrom()返回时,(sockaddr*)&from包含实际存入from中的数据字节数。Recvfrom函数返回接收到的字节数或当出现错误时返回-1,并置相应的errno。

(8)重复(2)-(7),实现查找一个范围内的IP地址。

流程图

在这里插入图片描述

关键代码

寻找下一个IP

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
char * findNextIp(char * nowIp)
{
char nextIpAddress[ipAddressSize];
char z[4][4];
int idxIp = 0, idxj = 0;
for (int i = 0; i < strlen(nowIp); i++)
{
if (nowIp[i] == '.')
{
z[idxIp][idxj] = '\0';

idxIp++;
idxj = 0;

continue;

}
z[idxIp][idxj++] = nowIp[i];
}
z[idxIp][idxj] = '\0';

for (int i = 3; i >= 0; i--)
{
if (strcmp("254", z[i]) == 0)
{
strcpy(z[i], "1"); // 这里让ip 1-254
}
else
{
int x;
x = atoi(z[i]) + 1;
itoa(x,z[i],10); // 第三个参数是 int的进制

break;
}
}


char retIp[ipAddressSize];
strcpy(retIp, z[0]);
char c[2] = ".";
for (int i = 1; i < 4; i++)
{
strcat(retIp, c);
strcat(retIp, z[i]);
}
/*cout << retIp << endl;*/

return retIp;
}


四、调试过程

五、设计结果及结果分析

结果

完整代码

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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdlib.h>
#include <sstream>
using namespace std;
#pragma comment(lib, "Ws2_32.lib")
const int ipAddressSize = 14;
//int count11=0;
//IP 报头
typedef struct
{
unsigned char hdr_len : 4; //4 位头部长度
unsigned char version : 4; //4 位版本号
unsigned char tos; //8 位服务类型
unsigned short total_len; //16 位总长度: 和头部长度一起就能区分 头 主体数据了
unsigned short identifier; //16 位标识符: 作用是分片后的重组
unsigned short frag_and_flags; //3 位标志加 13 位片偏移: 标志:MF 1是否还有分配 0 没有分片了
// DF 0 可以分片
// 片偏移:分片后的相对于原来的偏移
unsigned char ttl; //8 位生存时间
unsigned char protocol; //8 位上层协议号: 指出是何种协议
unsigned short checksum; //16 位校验和: 检验是否出错
unsigned long sourceIP; //32 位源 IP 地址
unsigned long destIP; //32 位目的 IP 地址
} IP_HEADER;
//ICMP 报头,一共八个字节,前四个字节为:类型(1字节)、代码(1字节)和检验和(2字节)。后四个字节取决于类型
typedef struct
{
BYTE type; //8 位类型字段:标识ICMP的作用
BYTE code; //8 位代码字段
USHORT cksum; //16 位校验和
USHORT id; //16 位标识符
USHORT seq; //16 位序列号
} ICMP_HEADER;

//报文解码结构
//接收到的数据缓存是字符数组char bufRev[],因此需要通过特定的解析(也就是拆成一段一段的)获取想要的信息
//把信息封装到结构体中,就比较方便的得到序列号、往返时间和目的IP了。
typedef struct
{
USHORT usSeqNo; //序列号
DWORD dwRoundTripTime; //往返时间
in_addr dwIPaddr; //返回报文的 IP 地址
} DECODE_RESULT;
//计算网际校验和函数
USHORT checksum(USHORT *pBuf, int iSize)
{
unsigned long cksum = 0;
while (iSize > 1)
{
cksum += *pBuf++;
iSize -= sizeof(USHORT);
}
if (iSize)
{
cksum += *(UCHAR *)pBuf;
}
cksum = (cksum >> 16) + (cksum & 0xffff);
cksum += (cksum >> 16);
return (USHORT)(~cksum);
}
//对数据包进行解码
// 1)接收到的Buf 2)接收到的数据长度 3)解析结果封装到Decode 4)ICMP回显类型 5)TIMEOUT时间
BOOL DecodeIcmpResponse2(char * pBuf, int iPacketSize, DECODE_RESULT &DecodeResult, BYTE
ICMP_ECHO_REPLY, BYTE ICMP_TIMEOUT)
{
//查找数据报大小合法性
//pBuf的首地址,就是IP报的首地址
IP_HEADER *pIpHdr = (IP_HEADER*)pBuf;
int iIpHdrLen = pIpHdr->hdr_len * 4;
if(iPacketSize < (int)(iIpHdrLen + sizeof(ICMP_HEADER)))
return FALSE;
// 根据 ICMP 报文类型提取 ID 字段和序列号字段
//ICMP字段包含在 IP数据段的起始位置,因此扣掉IP头,得到的就是ICMP头
ICMP_HEADER *pIcmpHdr = (ICMP_HEADER *)(pBuf + iIpHdrLen);

USHORT usID, usSquNo;
if (pIcmpHdr->type == ICMP_ECHO_REPLY) // ICMP 回显应答报文
{
usID = pIcmpHdr->id;//报文 ID
usSquNo = pIcmpHdr->seq;//报文序列号
}
else if (pIcmpHdr->type == ICMP_TIMEOUT)//ICMP超时差错报文
{
// 如果是TIMEOUT ,那么在ICMP数据包中,会夹带一个IP报(荷载IP)
char * pInnerIpHdr = pBuf + iIpHdrLen + sizeof(ICMP_HEADER); // 荷载中的 IP 的头
int iInnerIPHdrLen = ((IP_HEADER*)pInnerIpHdr)->hdr_len * 4;// 荷载中的IP 头长度
ICMP_HEADER * pInnerIcmpHdr = (ICMP_HEADER*)(pInnerIpHdr + iInnerIPHdrLen); //荷载中的ICMP头
usID = pInnerIcmpHdr->id;// 报文ID
usSquNo = pInnerIcmpHdr->seq; // 序列号
}
else
{
return false;
}

// 检查 ID 和序列号以确定收到期待数据报
if (usID != (USHORT)GetCurrentProcessId() || usSquNo != DecodeResult.usSeqNo)
{
return false;
}
// 记录 IP 地址并计算往返时间
DecodeResult.dwIPaddr.S_un.S_addr = pIpHdr->sourceIP;
DecodeResult.dwRoundTripTime = GetTickCount() - DecodeResult.dwRoundTripTime;
//处理正确收到的 ICMP 数据包
if (pIcmpHdr->type == ICMP_ECHO_REPLY || pIcmpHdr->type == ICMP_TIMEOUT)
{
// 输出往返时间信息
if (DecodeResult.dwRoundTripTime)
cout << " " << DecodeResult.dwRoundTripTime << "ms" << flush;
else
cout << " " << "<1ms" << flush;
}
return true;
}


char * findNextIp(char * nowIp);

int main()
{
//初始化 Windows sockets 网络环境
WSADATA wsa;//存储被WSAStartup函数调用后返回的Windows Sockets数据
//使用Socket的程序在使用Socket之前必须调用WSAStartup函数,以后应用程序就可以调用所请求的Socket库中的其他Socket函数了
WSAStartup(MAKEWORD(2, 2), &wsa);//进行相应的socket库绑定
cout << "请输入你要查找的起始IP:" << endl;
char IpAddressBeg[ipAddressSize]; // 255.255.255.255
cin >> IpAddressBeg;
cout << "请输入你要查找的终止IP:" << endl;
char IpAddressEnd[ipAddressSize]; // 255.255.255.255
cin >> IpAddressEnd;
char nextIpAddress[17];
strcpy(nextIpAddress, IpAddressBeg);
while (strcmp(nextIpAddress, IpAddressEnd) != 0)
{
// 执行,单线程执行,实现后改成多线程
//得到IP地址
u_long ulDestIP = inet_addr(nextIpAddress);//inet_addr()的功能是将一个点分十进制的IP转换成一个无符号长整型数
//转换不成功时按域名解析
if (ulDestIP == INADDR_NONE)
{
//gethostbyname()是查找主机名最基本的函数
//如果调用成功,就返回一个指向hosten结构的指针
//该结构中含有对应于给定主机名的主机名字和地址信息,用来承接域名解析的结构
hostent * pHostent = gethostbyname(nextIpAddress);
if (pHostent)//调用成功
{
//得到IP地址
//套了两层,IP和ICMP,ICMP是套在IP里面的
//h_addr返回主机IP地址
//in_addr返回报文的IP地址
//sin_addr.s_addr指向IP地址
ulDestIP = (*(in_addr*)pHostent->h_addr).s_addr;
}
else
{
cout << "输入的 IP 地址或域名无效!" << endl;
WSACleanup();//解除与Socket库的绑定并且释放Socket库所占用的系统资源
return 0;
}
}
// 填充目的 sockaddr_in
sockaddr_in destSockAddr;//sockaddr_in是Internet环境下套接字的地址形式
//将指定的内存块清零,使用结构前清零,而不让结构体的成员数值具有不确定性,是一个好的编程习惯
ZeroMemory(&destSockAddr, sizeof(sockaddr_in));
destSockAddr.sin_family = AF_INET;//指代协议簇,在socket编程中只能是AF_INET
destSockAddr.sin_addr.S_un.S_addr = ulDestIP;//按照网络字节顺序存储IP地址
//创建原始套接字
//WSASocket()的发送操作和接收操作都可以被重叠使用。接收函数可以被多次调用,发出接收缓冲区,准备接收到来的数据。发送函数也可以被多次调用,组成一个发送缓冲区队列
//如无错误发生,返回新套接口的描述字,否则的话,返回INVALID_SOCKET
//AF_INET为地址簇描述,SOCK_RAW为新套接口的类型描述,SOCK_RAW为原始套接字,可处理PING报文等
//IPPROTO_ICMP为套接口使用的协议,为ICMP;NULL是一个指向PROTOCOL_INFO结构的指针,该结构定义所创建套接口的特性
//0为套接口的描述字;WSA_FLAG_OVERLAPPED为套接口属性描述,WSA_FLAG_OVERLAPPED表示要使用重叠模型
SOCKET sockRaw = WSASocket(AF_INET, SOCK_RAW, IPPROTO_ICMP, NULL, 0,
WSA_FLAG_OVERLAPPED);
// 设置发送接收超时时间,即请求超时
//比如请求B站的一个视频,他超过一个时间没回我,我就认为超时了
//超时时间是可能变化的,这个超时时间用来存储在不同的变量,它刚好在一个变量而已
int iTimeout = 500;//如果没超过超时时间就会一直等着,超过超时时间就不等了
//接收超时
//sockRaw为将要被设置或者获取选项的套接字;SOL_SOCKET为在套接字级别上设置选项;SO_RCVTIMEO设置接收超时时间
//(char*)&iTimeout指向存放选项值的缓冲区;sizeof(iTimeout)为缓冲区的长度
setsockopt(sockRaw, SOL_SOCKET, SO_RCVTIMEO, (char *)&iTimeout, sizeof(iTimeout));
//发送超时
//sockRaw为将要被设置或者获取选项的套接字;SOL_SOCKET为在套接字级别上设置选项;SO_SNDTIMEO设置发送超时时间
//(char*)&iTimeout指向存放选项值的缓冲区;sizeof(iTimeout)为缓冲区的长度
setsockopt(sockRaw, SOL_SOCKET, SO_SNDTIMEO, (char *)&iTimeout, sizeof(iTimeout));
// 构造 ICMP 回显请求消息, 并以TTL 递增顺序发送报文
// ICMP 类型字段
//采用const修饰变量,功能是对变量声明为只读特性,并保护变量值以防被修改
const BYTE ICMP_ECHO_REQUEST = 8;//请求回显
const BYTE ICMP_ECHO_REPLY = 0;//回显应答
//其他常量定义
const int DEF_ICMP_DATA_SIZE = 32; // ICMP 报文默认数据字段长度
const int MAX_ICMP_PACKET_SIZE = 1024;//ICMP 报文最大长度(加上报头)
const DWORD DEF_ICMP_TIMEOUT = 500;// 回显应答超时时间
const int DEF_MAX_HOP = 20; // 最大跳站数
// 填充 ICMP 报文中每次发送时不变的字段
char IcmpSendBuf[sizeof(ICMP_HEADER) + DEF_ICMP_DATA_SIZE];// 发送缓冲区
memset(IcmpSendBuf, 0, sizeof(IcmpSendBuf));//初始化发送缓冲区
char IcmpRecvBuf[MAX_ICMP_PACKET_SIZE]; // 接收缓冲区
memset(IcmpRecvBuf, 0, sizeof(IcmpRecvBuf)); //初始化接收缓冲区
// 构造ICMP头
ICMP_HEADER * pIcmpHeader = (ICMP_HEADER*)IcmpSendBuf;
pIcmpHeader->type = ICMP_ECHO_REQUEST; // 类型为请求回显
pIcmpHeader->code = 0;//代码字段为0
pIcmpHeader->id = (USHORT)GetCurrentProcessId();// ID字段为当前进程号
memset(IcmpSendBuf + sizeof(ICMP_HEADER), 'E', DEF_ICMP_DATA_SIZE);//数据字段
USHORT usSeqNo = 0; // ICMP 报文序列号
int iTTL = 1; // TTL初始化值为1
BOOL bReachDestHost = FALSE; // 循环退出标志
int iMaxHot = DEF_MAX_HOP; // 最大循环数
DECODE_RESULT DecodeResult;// 传递给报文解码函数的结构化参数
//int count11=0;
while (!bReachDestHost && iMaxHot--)
{
bReachDestHost = FALSE;
// 设置 IP 报头的 TTL 字段
//sockRaw为将要被设置或者获取选项的套接字;IPPROTO_IP为套接口使用的协议,为IP;IP_TTL为设置IP报头的TTL字段
//(char*)&iTTL指向存放选项值的缓冲区;sizeof(iTTL)为缓冲区的长度
setsockopt(sockRaw, IPPROTO_IP, IP_TTL, (char *)&iTTL, sizeof(iTTL));
cout << iTTL << flush; // 输出当前序号,flush的作用是刷新缓冲区
// 填充 ICMP报文中每次发送变化的字段
((ICMP_HEADER *)IcmpSendBuf)->cksum = 0;//校验和为0
((ICMP_HEADER *)IcmpSendBuf)->seq = htons(usSeqNo++);// 填充序列号
((ICMP_HEADER *)IcmpSendBuf)->cksum = checksum((USHORT *)IcmpSendBuf,
sizeof(ICMP_HEADER) + DEF_ICMP_DATA_SIZE); //计算校验和

// 记录序列号和当前时间
DecodeResult.usSeqNo = ((ICMP_HEADER*)IcmpSendBuf)->seq;//当前序号
DecodeResult.dwRoundTripTime = GetTickCount();// 当前时间

// 指定对方信息
// 发送 TCP 回显请求信息
//sendto()利用数据报的方式进行数据传输
// 1)指定哪个Socket发给对方 2)发送的数据 3)flag 4)目的地址 5)目的地址的sockaddr_in结构
sendto(sockRaw, IcmpSendBuf, sizeof(IcmpSendBuf), 0, (sockaddr*)&destSockAddr, sizeof(destSockAddr));

//接收 ICMP 差错报文并进行解析
sockaddr_in from; // 对端 socket地址,对方的
int iFromLen = sizeof(from);//地址结构大小
int iReadDataLen;// 接收数据长度

// 接收正常的话,这个循环只会执行一次
while (true)
{
//接收数据
//recvfrom()利用数据报方式进行数据传输
//当recvfrom()返回时,(sockaddr*)&from包含实际存入from中的数据字节数。
//Recvfrom()函数返回接收到的字节数或当出现错误时返回-1,并置相应的errno。
iReadDataLen = recvfrom(sockRaw, IcmpRecvBuf, MAX_ICMP_PACKET_SIZE, 0, (sockaddr*)&from, &
iFromLen);

if (iReadDataLen != SOCKET_ERROR) // 有数据到达
{
//解析数据包
// 1)接收到的Buf 2)接收到的数据长度 3)解析结果封装到Decode 4)ICMP回显类型 5)TIMEOUT时间
if (DecodeIcmpResponse2(IcmpRecvBuf, iReadDataLen, DecodeResult, ICMP_ECHO_REPLY, DEF_ICMP_TIMEOUT))
{
// 到达目的地,退出循环
//返回报文的IP地址等于输入的IP地址
if (DecodeResult.dwIPaddr.S_un.S_addr == destSockAddr.sin_addr.S_un.S_addr)
{
bReachDestHost = true;
// 输出 IP 地址
//inet_ntoa()功能是将网络地址转换成“.”点隔的字符串格式。
cout << '\t' << inet_ntoa(DecodeResult.dwIPaddr) << endl;
strcpy(nextIpAddress, inet_ntoa(DecodeResult.dwIPaddr));
break;
}

}

}
//WSAGetLastError()当一特定的Sockets API函数指出一个错误已经发生,该函数就应调用来获得对应的错误代码。
//WSAETIMEDOUT在尝试连接超时,而不建立连接。
else if (WSAGetLastError() == WSAETIMEDOUT) //接收超时,输出*号
{
cout << " *" << '\t' << "Request timed out." << endl;
break;
}
else
{
break;
}
}
iTTL++;//递增TTL值
}

cout << "查找: " << nextIpAddress << "结果为 ->" << (bReachDestHost ? "在线" : "不在线") << endl;
//if nextIpAddress ==bReachDestHost;
// 向下推
strcpy(nextIpAddress, findNextIp(nextIpAddress));
}
return 0;
}

char * findNextIp(char * nowIp)
{
char nextIpAddress[ipAddressSize];
char z[4][4];
int idxIp = 0, idxj = 0;
for (int i = 0; i < strlen(nowIp); i++)
{
if (nowIp[i] == '.')
{
z[idxIp][idxj] = '\0';

idxIp++;
idxj = 0;

continue;

}
z[idxIp][idxj++] = nowIp[i];
}
z[idxIp][idxj] = '\0';

for (int i = 3; i >= 0; i--)
{
if (strcmp("254", z[i]) == 0)
{
strcpy(z[i], "1"); // 这里让ip 1-254
}
else
{
int x;
x = atoi(z[i]) + 1;
itoa(x,z[i],10); // 第三个参数是 int的进制

break;
}
}


char retIp[ipAddressSize];
strcpy(retIp, z[0]);
char c[2] = ".";
for (int i = 1; i < 4; i++)
{
strcat(retIp, c);
strcat(retIp, z[i]);
}
/*cout << retIp << endl;*/

return retIp;
}