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

一、预备知识

ICMP

ICMP的报文是封装在IP数据部分中的。按照我的理解,ICMP就是在网络层中,反馈一些转发、访问等操作时的附带信息。
在这里插入图片描述
ICMP分为两种,ICMP差错报告报文(IP传输时的反馈)和ICMP询问报文(主动发起检查)。具体类型值和作用如下:

  • 3 终点不可达
  • 11 时间超过
  • 12 参数问题: IP首部数据有问题
  • 5 改变路由: 规定发送到某目的的IP,经过某路由
  • 8或0 回送请求或回答:向某台主机询问,主机必须给出某种回答
  • 13或14 时间戳请求或回答:向某台主机询问时间。

ICMP的应用之tracert

用于测试到达某IP地址所需的TTL(跳数),往返时间。

原理:

  源主机向目的主机发送一连串IP数据报,数据报封装的是无法交付的UDP(使用错误的端口号,好坏的)。
  第一个数据包的生存时间TLL设置为1,当P1达到路径上的第一个路由时,路由器R1就收下,然后把TLL减1,这时TLL为0,R1就丢弃数据报,然后向源主机发送一个ICMP时间超过的差错报告报文。一直做下去,直到最后一个数据报到达目的主机,这是数据报的TTL是1。由于已经到达了目的地,那么主机收下数据报,且不做减一操作(TLL为1)。但是数据报的错误的,因此目的主机就会发一个ICMP终点不可达差错报告报文。

下图的三个时间是因为每一次都发送三个相同的数据报。

在这里插入图片描述

ICMP的应用之ping

向目的主机发送询问时间请求(ICMP中的13), 目的主机收到请求时,发回当前时间戳(ICMP中的14),因此利用时间戳可以计算出往返时间。

二、实验部分:Tracert 与 Ping 程序设计与实现

参照附录 2,了解 Tracert 程序的实现原理,并调试通过。然后参考 Tracert 程序和教材 4.4.2 节,编写一个 Ping 程序,并能测试本局域网的所有机器是否在线,运行界面如图 1 所示的 QuickPing 程序。
在这里插入图片描述

实现之前带着的疑问?

1) 报文的具体组成?如何将ICMP数据部分 + ICMP数据头组成ICMP数据报。再将ICMP数据报加入IP数据中,最后让IP数据部分加上IP数据头构成IP数据报?
2) IP报文通过什么方法解析,可以得到IP数据报的头和数据部分。然后数据部分如何解析出ICMP报文的头和数据部分?
3)包装好的IP报文通过什么通道传输。

疑问解答

完整的代码我放到最后,在visual stdio下,关闭sdk检查,完美运行,下面对于程序的个人理解。

问题一:报文的组成

这一步是为了将希望传递的信息封装成char sendRev[],数据缓冲区也就是字符串数组。不过socket帮我们封装了一个方法,让我们不用具体构造到字符数组。这里后面再说。

通过参数构造成ICMP头部结构体,然后再加上想要的ICMP数据部分。再构造出IP头部,把ICMP报文加到IP数据部分之前。这样完整的IP数据报就完成了。

通过结构体构造出ICMP数据头
//ICMP 报头,一共八个字节,前四个字节为:类型(1字节)、代码(1字节)和检验和(2字节)。后四个字节取决于类型
typedef struct
{
	BYTE type; //8 位类型字段:标识ICMP的作用
	BYTE code; //8 位代码字段
	USHORT cksum; //16 位校验和
	USHORT id; //16 位标识符
	USHORT seq; //16 位序列号
} ICMP_HEADER;

通过结构体构造出IP数据头
//IP 报头,标准IPV4占20字节
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;
问题二:IP数据报的解析

接收到的数据缓存是字符数组 char bufRev[],因此需要通过特定的解析(也就是拆成一段一段的)获取想要的信息。

另外为了方便存取信息,这里又写了一种DECODE_RESULT,解码信息的结构体。把信息封装到结构体中,就比较方便的得到序列号、往返时间和目的IP了。

//报文解码结构
typedef struct
{
	USHORT usSeqNo; //序列号
	DWORD dwRoundTripTime; //往返时间
	in_addr dwIPaddr; //返回报文的 IP 地址
}DECODE_RESULT;
这里还需知识储备,就是字符串转结构体指针这种写法。它会把字符数组中的内容按顺序赋值到结构体中。

char 占1个字节
int 占4个字节

 unsigned char a[] = "0123456789abcdefghijk";  //无符号字符数组
    struct A         //结构体A,一个int 三个char 再接一个int
    {
        unsigned int a;
        unsigned char b;
        unsigned char c;
        unsigned char d;
        unsigned int e;
    } *pp;

    pp = (A*) a;
    cout<< (*pp).a <<' '<<(*pp).b <<' '<<(*pp).c <<' '<<endl;

有了上面的知识储备,那么如何解析IP数据报(字符数组)就比较好理解了,通过特定的地址偏移,就能把字符数组赋值到IP、ICMP结构体中了

具体的解析函数,大部分都打上了注释
// 1)接收到的Buf 2)接收到的数据长度 3)解析结果封装到Decode 4)ICMP类型 ECHO_REPLY(是一个常量,放到全局也行) 5)ICMP类型 TIMEOUT
BOOL DecodeIcmpResponse(char * pBuf, int iPacketSize, DECODE_RESULT &DecodeResult, BYTE
	ICMP_ECHO_REPLY, BYTE ICMP_TIMEOUT)
{
	//查找数据报大小合法性
		//pBuf的首地址,就是IP报的首地址,因此偏移0
	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)
	{
		// 如果是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;
}
问题三:传输的通道

最后这个问题也是我比较困惑的,原因是Socket把底层封装好了,我们只需把参数(发送的IP结构、ICMP结构、目的主机地址结构体)填好,传递到sendto()函数里面,就能把IP数据报发送到目的主机。通过调用recv就能得到目的主机的反馈。目前我也没找到更加底层的分析,因此也只停留在会用而已。

说一下这个程序干了什么事,如何使用

效果

在这里插入图片描述

ping

首先ping的原理是基础,ping也就是发送一个ICMP类型为"时间戳请求"的数据报,当目的主机收到后就会反馈一个ICMP类型为"时间戳回答"的报文,然后发送方接收到反馈后,进行解析。发现收到的数据报包含了ICMP类型为“时间戳回答”的报文,因此计算出往返时间,结果就是ping通

tracert

在ping的基础上做一些修改,发送的ICMP类型是"请求"而不是"时间戳请求",每次都是发送到目的主机,但是TTL从1慢慢增加,这样就能获得路径上所经过的网络设备。

接收方

只要目标主机开启了ICMP的功能,那么它接收到携带ICMP报文的IP就会自动处理,因此接收方接收的事情在操作系统已经帮我们完成了。

发送方

根据ping 和 tracert业务不同改变ICMP类型即可,但是tracert要慢慢增加TTL,而ping是一下子把TTL开的足够大。

代码:实现ip区间内的tracert

#include "pch.h"
#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdlib.h> 

using namespace std;
#pragma comment(lib, "Ws2_32.lib")

/******全局常量********/

const int ipAddressSize = 17;

//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;

//报文解码结构
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)
	{
		// 如果是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()
{
	//char ip[18] = "192.168.254.254";
	//findNextIp(ip);

	//初始化 Windows sockets 网络环境
	WSADATA wsa;
	WSAStartup(MAKEWORD(2, 2), &wsa);

	 
	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)
	{
		// 执行,单线程执行,实现后改成多线程
		u_long ulDestIP = inet_addr(nextIpAddress);
		//转换不成功时按域名解析
		if (ulDestIP == INADDR_NONE)
		{
			hostent * pHostent = gethostbyname(nextIpAddress);
			if (pHostent)
			{
				ulDestIP = (*(in_addr*)pHostent->h_addr).s_addr;
			}
			else {
				cout << "输入的 IP 地址或域名无效!" << endl;
				WSACleanup();
				return 0;
			}
		}
		// 填充目的 sockaddr_in
		sockaddr_in destSockAddr;
		ZeroMemory(&destSockAddr, sizeof(sockaddr_in));
		destSockAddr.sin_family = AF_INET;
		destSockAddr.sin_addr.S_un.S_addr = ulDestIP;

		// 创建原始套接字
		SOCKET sockRaw = WSASocket(AF_INET, SOCK_RAW, IPPROTO_ICMP, NULL, 0,
			WSA_FLAG_OVERLAPPED);
		// 设置发送接收超时时间
		int iTimeout = 3000;
		setsockopt(sockRaw, SOL_SOCKET, SO_RCVTIMEO, (char *)&iTimeout, sizeof(iTimeout));
		setsockopt(sockRaw, SOL_SOCKET, SO_SNDTIMEO, (char *)&iTimeout, sizeof(iTimeout));
		// 构造 ICMP 回显请求消息, 并以TTL 递增顺序发送报文
		// ICMP 字段
		const BYTE ICMP_ECHO_REQUEST = 8;
		const BYTE ICMP_ECHO_REPLY = 0;
		const BYTE ICMP_TIMEOUT = 11;
		//其他常量
		const int DEF_ICMP_DATA_SIZE = 32; // ICMP 报文数据段长度
		const int MAX_ICMP_PACKET_SIZE = 1024;//ICMP 报文最大长度(加上报头)
		const DWORD DEF_ICMP_TIMEOUT = 3000;// 回显超时时间
		const int DEF_MAX_HOP = 30; // 最大跳
		// 填充 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;
		pIcmpHeader->id = (USHORT)GetCurrentProcessId();// ID为进程PID
		memset(IcmpSendBuf + sizeof(ICMP_HEADER), 'E', DEF_ICMP_DATA_SIZE);//数据字段
		USHORT usSeqNo = 0; // ICMP 报文序列号
		int iTTL = 1; // TTL初始化
		BOOL bReachDestHost = FALSE; // 循环退出标志
		int iMaxHot = DEF_MAX_HOP; // 最大循环数
		DECODE_RESULT DecodeResult;// 传输数据的介质,封装成结构
		while (!bReachDestHost && iMaxHot--)
		{
			bReachDestHost = FALSE;
			// 设置 IP 报头的 TTL 字段
			setsockopt(sockRaw, IPPROTO_IP, IP_TTL, (char *)&iTTL, sizeof(iTTL));
			cout << iTTL << 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 回显请求信息
			// 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)
			{
				//接收数据
				iReadDataLen = recvfrom(sockRaw, IcmpRecvBuf, MAX_ICMP_PACKET_SIZE, 0, (sockaddr*)&from, &
					iFromLen); 
				
				if (iReadDataLen != SOCKET_ERROR) // 有数据到达
				{
					//解析数据包
					if (DecodeIcmpResponse2(IcmpRecvBuf, iReadDataLen, DecodeResult, ICMP_ECHO_REPLY, ICMP_TIMEOUT))
					{
						// 到达目的地,退出循环
						if (DecodeResult.dwIPaddr.S_un.S_addr == destSockAddr.sin_addr.S_un.S_addr)
							bReachDestHost = true;
						// 输出 IP 地址
						cout << '\t' << inet_ntoa(DecodeResult.dwIPaddr) << endl;
						break;
					}

				}
				else if (WSAGetLastError() == WSAETIMEDOUT) //接收超时,输出*号
				{
					cout << " *" << '\t' << "Request timed out." << endl;
					break;
				}
				else
				{
					break;
				}
			}
			iTTL++;
		}
		cout << "查找: " << nextIpAddress << "结果为 ->" << (bReachDestHost ? "在线" : "不在线") << endl;
		// 向下推
		strcpy(nextIpAddress, findNextIp(nextIpAddress));
	}
}

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 = 0; i < 4; i++)
	//{
	//	puts(z[i]);
	//}
	//cout << endl;

	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;
}
  • 21
    点赞
  • 161
    收藏
    觉得还不错? 一键收藏
  • 13
    评论
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值