2019-12-28 18:11:08 mzc_love 阅读数 19
  • 第07章-网络通信协议(OSI、TCP、UDP、IP、ARP、ICMP...

    课程内容如下: 通信协议的概念 ISO、OSI七层参考模型详解 TCP、IP模型详解 网络接口层详解 IP协议详解 IP地址和MAC地址特征分析 有了IP地址为什么还要使用MAC地址 ARP工作过程及工作原理解析 ICMP知识解析 ping命令使用详解 tracert命令使用详解 TCP详解 UDP协议详解 网络通信协议(应用层)协议

    8102 人正在学习 去看看 深博

一.设计题目

Tracert 与Ping 程序设计与实现

二.设计内容

参照附录 2,了解 Tracert 程序的实现原理,并调试通过。然后参考 Tracert 程序和教材 4.4.2 节,编写一个 Ping 程序,并能测试本局域网的所有机器是否在线。

三.设计步骤

首先我们要明白tracert是什么,Tracert(跟踪路由)是路由跟踪实用程序,用于确定 IP 数据包访问目标所采取的路径。Tracert 命令用 IP 生存时间 (TTL) 字段和 ICMP 错误消息来确定从一个主机到网络上其他主机的路由。
Tracert原理:
通过向目标发送不同IP生存时间 (TTL) 值的“Internet控制消息协议 (ICMP)”回应数据包,Tracert诊断程序确定到目标所采取的路由。要求路径上的每个路由器在转发数据包之前至少将数据包上的 TTL 递减 1。数据包上的 TTL 减为 0 时,路由器应该将“ICMP 已超时”的消息发回源系统。
Tracert 先发送 TTL 为 1 的回应数据包,并在随后的每次发送过程将TTL递增 1,直到目标响应或 TTL 达到最大值,从而确定路由。通过检查中间路由器发回的“ICMP 已超时”的消息确定路由。某些路由器不经询问直接丢弃 TTL 过期的数据包,这在 Tracert 实用程序中看不到。
了解了tracert原理后我们可以得知,ping命令实际上就是tracert简化版本。我们需要向目标ip发送icmp报文,然后计算时间差。由于我是使用的Linux下编程,所以不能直接使用附录的程序代码。为此,我从ICMP的编码和解码写起。
TCMP报文示意图:
在这里插入图片描述

四.调试过程

ICMP封包
icmp->icmp_type = ICMP_ECHO; //设置类型
icmp->icmp_code = 0;
icmp->icmp_cksum = 0; //设置初始校验和
icmp->icmp_id = pid; //设置当前id
icmp->icmp_seq = seq++; //设置序号
tvstart = (struct timeval*)icmp->icmp_data; //设置icmp时间戳
icmp->icmp_cksum = checksum((unsigned char*)icmp, DEFAULT_LEN + 8);
ICMP解包
参照上述封包操作进行提取,同时我们需要注意校验和是否一致。如果不一致,则丢弃当前数据包。
校验和算法
将数据以字(16位)为单位累加到一个双字中,如果数据长度为奇数,最后一个字节将被扩展到字,累加的结果是一个双字,最后将这个双字的高16位和低16位相加后取反,便得到了校验和。
while (len > 1) //sum
{
sum += *pbuf++;
len -= 2;
} //累加求和
if (len) sum += (unsigned char)pbuf;
sum = (sum >> 16) + (sum & 0xffff);
sum += (sum >> 16);
return (unsigned short)(~sum); //取反
如何实现扫描局域网?
为了实现扫描局域网,我们首先可以使用gethostbyname获取得到当前ip。通过合理的字符串拼接算法,我们可以修改得到ip的末尾值,遍历0-255即可。
在这期间有一个问题就是速度问题,如果采用之前的版本,那么美发送一个icmp报文我们都将等待目标主机回复。程序会陷入阻塞状态,用户体验不佳。因此,本程序采用了多线程操作,同时向多个主机收发ICMP报文。
for(int i = 1;i<=255;i++)
pthread_create(&threads[i], NULL, pingIp, NULL);
pthread_join(threads[i],NULL);

五.设计结果及结果分析

1.实验结果展示

在这里插入图片描述
在这里插入图片描述

2.实验结果分析

实验结果中展示的就是tractert运行的结果,它所完成的功能是根据输入的起始IP地址到结束IP地址,查询这个范围内的所有IP地址所链接的主机是否在线,也就是是否接入互联网。判断的过程主要就是头部生存空间的使用。
初始时 TTL 等于 1,这样当该数据报抵达途中的第一个路由器时,TTL 的值就被减为 0,导致发生超时错误,因此该路由生成一份 ICMP 超时差错报文返回给源主机。随后,主机将数据报的 TTL 值递增 1,以便 IP 报能传送到下一个路由器。这个TTL的重复过程就是检测所IP所在的主机是否在线的过程。

六.心得体会

实验二的学习可以说是对于网络上的报文信息在各个中转结点的应用的学习的好方法,它通过协议处理的方式展示了报文或者说数据从起始的IP地址的主机到达目的IP地址的过程。在整个过程,对于中转结点(也就是非目的结点)处的数据的处理等等。也就是利用TTL处理和检测每到一个结点之后对于信息的处理方式和对于是否是目的IP的判断方法。
同时通过运行解析程序学会了TTL的工作方法,TTL在到达每一个结点之后TTL的值就被减1,当发生了超时错误的时候生成一份ICMP报文返回给源主机,之后TTL的报文值再次加一。这样,源主机只需对返回的每一份 ICMP 报文进行解析处理,就可以掌握数据报从源主机到达目地主机途中所经过的路由信息。就是这样一个过程,使得报文从起始的IP地址传送至目的IP。

七.源码

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

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;

//报文解码结构
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 = 400;
		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 int DEF_ICMP_DATA_SIZE = 32; // ICMP 报文数据段长度
		const int MAX_ICMP_PACKET_SIZE = 1024;//ICMP 报文最大长度(加上报头)
		const DWORD DEF_ICMP_TIMEOUT = 300;// 回显超时时间
		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;
		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;// 传输数据的介质,封装成结构
		int count11=0;
		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, DEF_ICMP_TIMEOUT))
					{
						// 到达目的地,退出循环
						if (DecodeResult.dwIPaddr.S_un.S_addr == destSockAddr.sin_addr.S_un.S_addr){
						bReachDestHost = true;
						count11++;
						}
							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;
		//if nextIpAddress ==bReachDestHost;
		count11++;
		// 向下推
		strcpy(nextIpAddress, findNextIp(nextIpAddress));
	}
	cout << count11<< endl;
	cout << "在线总共:"+count11<< endl;
}

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

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

	return retIp;
}

2019-07-16 16:27:00 qq_38474871 阅读数 124
  • 第07章-网络通信协议(OSI、TCP、UDP、IP、ARP、ICMP...

    课程内容如下: 通信协议的概念 ISO、OSI七层参考模型详解 TCP、IP模型详解 网络接口层详解 IP协议详解 IP地址和MAC地址特征分析 有了IP地址为什么还要使用MAC地址 ARP工作过程及工作原理解析 ICMP知识解析 ping命令使用详解 tracert命令使用详解 TCP详解 UDP协议详解 网络通信协议(应用层)协议

    8102 人正在学习 去看看 深博

一、设计目的

PING程序是我们使用的比较多的用于测试网络连通性的程序。PING程序基于ICMP,使用ICMP的回送请求和回送应答来工作。由计算机网络课程知道,ICMP是基于IP的一个协议,ICMP包通过IP的封装之后传递。

课程设计中选取PING程序的设计,其目的是通过PING程序的设计,能初步掌握TCP/IP网络协议的基本实现方法,对网络的实现机制有进一步的认识。

熟悉SOCKET的编程,包括基本的系统调用如SOCKET、BIND等。

二、设计内容

2.1 RAW模式的SOCKET编程

PING程序是面向用户的应用程序,该程序使用ICMP的封装机制,通过IP协议来工作。为了实现直接对IP和ICMP包进行操作,实验中使用RAW模式的SOCKET编程。

2.2 具体内容

2.2.1 定义数据结构

定义IP数据报、ICMP包等相关的数据结构。

18702784-c11001e2c97c4852.png

点击此处下载源码

2017-05-19 14:05:17 tengkonglieying 阅读数 859
  • 第07章-网络通信协议(OSI、TCP、UDP、IP、ARP、ICMP...

    课程内容如下: 通信协议的概念 ISO、OSI七层参考模型详解 TCP、IP模型详解 网络接口层详解 IP协议详解 IP地址和MAC地址特征分析 有了IP地址为什么还要使用MAC地址 ARP工作过程及工作原理解析 ICMP知识解析 ping命令使用详解 tracert命令使用详解 TCP详解 UDP协议详解 网络通信协议(应用层)协议

    8102 人正在学习 去看看 深博

ICMP是(Internet Control Message Protocol)Internet控制报文协议。它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。 要利用ICMP协议实现ping功能的程序,Ping程序将采取下列步骤:
(1)创建类型为SOCK_RAW的一个套接字,同时设定协议类型为IPPROTO_ICMP。
(2)创建并初始化ICMP头。
(3)调用sendto,将ICMP请求发送给远程主机。
(4)调用recvfrom,以接受任何ICMP响应。

#!/usr/bin/python
# -*- coding:UTF-8 -*-

#导入相关包
import os,socket,struct,select,time		
#struct包用于处理二进制数据,selcet包监听是否有可读、可写或异常事件产生

ICMP_ECHO_REQUEST = 8		# ICMP报文类型
DEFAULT_TIMEOUT = 2 		# 默认超时时间
DEFAULT_COUNT = 4 		# 默认发送报文的数量

class Pinger(object):		#定义Pinger类
	#定义初始化方法
	def __init__(self, target_host, count=DEFAULT_COUNT,timeout=DEFAULT_TIMEOUT):	
		self.target_host = target_host
		self.count = count
		self.timeout = timeout

	def do_checksum(self, source_string):		#计算校验和
		sum = 0		
		max_count = (len(source_string)/2)*2	# 取小于等于len(source_string)最大偶数
		count = 0
		while count < max_count:
			#ord返回单字符的ASCII码
			val = ord(source_string[count + 1])*256 + ord(source_string[count])	
			sum = sum + val	            #累加到sum
			sum = sum & 0xffffffff		#sum取后32位
			count = count + 2
		if max_count<len(source_string):	#若len(source_string)为奇数,加上source_string的最后一位	
			sum = sum + ord(source_string[len(source_string) - 1])
			sum = sum & 0xffffffff
		sum = (sum >> 16) + (sum & 0xffff)	     #将sum的高16位加上低16位
		sum = sum + (sum >> 16)		    #将sum加上sum的高16位
		answer = ~sum			    #将sum取反
		answer = answer & 0xffff	    # 取answer的低16位
		answer = answer >> 8 | (answer << 8 & 0xff00)   # 将answer右移8位与上answer左移8位
		return answer	 # 返回计算得到的校验和

	def send_ping(self, sock, ID):			#发送ICMP报文
		target_addr = socket.gethostbyname(self.target_host) 	# 获得目的 ip 地址
		my_checksum = 0		# 初始化校验和为0
		# 将数据按“bbHHh“ 格式打包 b:integer 1B H:unsigned short 2B h:short 2B
		header = struct.pack("bbHHh", ICMP_ECHO_REQUEST, 0,my_checksum, ID, 1)
		bytes_In_double = struct.calcsize("d")		 # 得到 ’d’ 格式的数据字节数
		data = (100 - bytes_In_double) * "Q"		 #data 一共 100 字节 发送一些列 ’Q...’
		data = struct.pack("d", time.time()) + data	 # data 前加上时间
		# 根据header和data计算校验和
		my_checksum = self.do_checksum(header + data)
		header = struct.pack("bbHHh", ICMP_ECHO_REQUEST, 0, socket.htons(my_checksum), ID, 1)
		# 打包header 并将校验和由主机字节顺序转为网络字节顺序
		packet = header + data		 # 将header和data组合成packet
		sock.sendto(packet, (target_addr, 1))		 # 向目标地址发送packet

	def receive_ping(self, sock, ID, timeout):		#接收服务器返回的报文
		time_remaining = timeout	# 设置超时时间
		while True:
			start_time = time.time()
			readable = select.select([sock], [], [], time_remaining)	# 监听sock是否有数据可读
			time_spent = (time.time() - start_time)
			if readable[0] == []:		 # 没有可读数据
				return
			time_received = time.time()		# 记录接收的时间
			recv_packet, addr = sock.recvfrom(1024)		 # 接收1024个字节数据
			icmp_header = recv_packet[20:28]	 # 前20字节为IP首部 icmp首部一共8个字节 ‘bbHHh’ 1+1+2+2+2=8
			# 从 icmp_header 中解包
			type, code, checksum, packet_ID, sequence = struct.unpack("bbHHh", icmp_header)		 
			if packet_ID == ID:		# ID匹配
				bytes_In_double = struct.calcsize("d")
				# 从 data 中得到发送的时间
				time_sent = struct.unpack("d", recv_packet[28:28 +bytes_In_double])[0] 		
				return time_received - time_sent	 # 返回所用的时间
			time_remaining = time_remaining - time_spent
			if time_remaining <= 0:		 # 超时返回
				return

	def ping_once(self):			#Ping一次服务器,返回延迟时间
		icmp = socket.getprotobyname("icmp")		 # 得到协议号
		try:
			# 创建原始套接字
			m_sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp)
		except socket.error, (errno, msg):
			if errno == 1:
				# 没有权限
				msg += "ICMP messages can only be sent from root user processes"
				print msg
			else:
				print errno
		except Exception, e:
			print "Exception: %s" %(e)
		my_ID = os.getpid() & 0xFFFF		# 进程id
		self.send_ping(m_sock, my_ID)		 # 发送报文
		delay = self.receive_ping(m_sock, my_ID, self.timeout)		 # 接收回应
		m_sock.close()
		return delay

	def ping(self):			#实现Ping count
		for i in xrange(self.count):
			print "Ping to %s..." % self.target_host,
			try:
				delay = self.ping_once()	 # 返回延迟时间
			except socket.error, e:
				print "Ping failed. (socket error: '%s')" % e[1]
				break
			if delay == None:
				print "Ping failed. (timeout within %ssec.)" % self.timeout
			else:
				delay = delay * 1000
				print "Get pong in %0.4fms" % delay

if __name__ == '__main__': 					 #Main函数
	target_host='www.runoob.com'				 # 目标主机
	pinger = Pinger(target_host=target_host)		 #创建Ping对象
	pinger.ping()						 # 调用ping方法

需要特别注意的是在Linux操作系统下,会出现下图这种情况!!!
Operation not permittedICMP messages can only be sent from root user processes

注意

现在加上sudo后就可以ping通了!运行结果如下:

这里写图片描述

2019-04-22 20:39:21 function_template 阅读数 132
  • 第07章-网络通信协议(OSI、TCP、UDP、IP、ARP、ICMP...

    课程内容如下: 通信协议的概念 ISO、OSI七层参考模型详解 TCP、IP模型详解 网络接口层详解 IP协议详解 IP地址和MAC地址特征分析 有了IP地址为什么还要使用MAC地址 ARP工作过程及工作原理解析 ICMP知识解析 ping命令使用详解 tracert命令使用详解 TCP详解 UDP协议详解 网络通信协议(应用层)协议

    8102 人正在学习 去看看 深博
//----------------------头文件------------------------
#include <stdio.h> 
#include <stdlib.h> 
#include <winsock.h> 
#include<conio.h> 
#pragma comment(lib, "ws2_32.lib")   //提供对以下网络相关API的支持
#define REPLY 0                      //ICMP 回送回应报文 
#define REQUEST 8                    //ICMP 回送请求报文
#define DATASIZE 32                  // 请求数据报的大小
#include <iostream>
using namespace std; 

//----------------------数据结构定义------------------------
//定义IP首部的数据结构
typedef struct IPHeader 
{ 
    unsigned char VerLenth;      //定义首部长度和版本,高4位为首部长度,低4位为版本
    unsigned char Tos;           //服务类型/区分服务
    unsigned short TotalLenth;   //总长度 
    unsigned short ID;           //标识号 
    unsigned short Frag_Flags;   //片偏移量 
    unsigned char TTL;           //生存时间 
    unsigned char Protocol;      //协议 
    unsigned short Checksum;     //首部校验和
    struct in_addr SrcIP;        //源 IP 地址 
    struct in_addr DestIP;       //目的地址
}IPHDR, *PIPHDR; 

//定义 ICMP 首部格式
typedef struct ICMPHeader 
{ 
    unsigned char Type;         //类型 
    unsigned char Code;         //代码 
    unsigned short Checksum;    //首部校验和 
    unsigned short ID;          //标识 
    unsigned short Seq;         //序列号
    char Data;                  //数据
}ICMPHDR, *PICMPHDR; 
//定义 ICMP 回应请求
typedef struct ECHOREQUEST 
{ 
    ICMPHDR icmpHdr; 
    DWORD dwTime; 
    char cData[DATASIZE]; 
}ECHOREQUEST, *PECHOREQUEST; 

//定义 ICMP 回应答复
typedef struct ECHOREPLY 
{ 
    IPHDR ipHdr; 
    ECHOREQUEST echoRequest; 
    char cFiller[256]; 
}ECHOREPLY,*PECHOREPLY;

//-----------------------------------------------------
//计算校验和
unsigned short CheckSum(unsigned short *buffer, int len) 
{ 
    register int nleft = len; 
    register unsigned short *w = buffer; 
    register unsigned short answer; 
    register int sum = 0; 
    //使用32的位累加器,进行16位的计算
    while ( nleft > 1 ) 
    { 
        sum += *w++; 
        nleft -= 2; 
    } 
    if ( nleft == 1 ) 
    { 
        unsigned short u = 0; 
        *(unsigned char *)(&u) = *(unsigned char*)w; 
        sum += u; 
    }
    //将反馈的 16 位从高位移到低位
    sum = (sum >> 16) + (sum & 0xffff); 
    sum += (sum >> 16); 
    answer = ~sum; 
    return (answer); 
} 


//发送回应请求函数
int SendRequest(SOCKET s, struct sockaddr_in *lpstToAddr) 
{ 
    static ECHOREQUEST echoReq; 
    static int nId = 1; 
    static int nSeq = 1; 
    int nRet; 
    //填充回应请求消息
    echoReq.icmpHdr.Type = REQUEST; 
    echoReq.icmpHdr.Code = 0; 
    echoReq.icmpHdr.Checksum = 0; 
    echoReq.icmpHdr.ID = nId++; 
    echoReq.icmpHdr.Seq = nSeq++; 
    //填充要发送的数据
    for (nRet = 0; nRet < DATASIZE; nRet++) 
    { 
        echoReq.cData[nRet] = '1' + nRet; 
    } 
    //存储发送的时间
    echoReq.dwTime = GetTickCount(); 
    //计算回应请求的校验和
    echoReq.icmpHdr.Checksum = CheckSum((unsigned short*)&echoReq, sizeof(ECHOREQUEST)); 
    //发送回应请求
    nRet = sendto(s,(LPSTR)&echoReq,sizeof(ECHOREQUEST),0,(struct sockaddr*)lpstToAddr,sizeof(SOCKADDR_IN)); 
    if (nRet == SOCKET_ERROR) 
    { 
        printf("发送失败\t error:%d\n", WSAGetLastError()); 
    } 
    return (nRet); 
} 

//接收应答回复并进行解析
DWORD RecvReply(SOCKET s, LPSOCKADDR_IN lpsaFrom, unsigned char *pTTL) 
{ 
    ECHOREPLY echoReply; 
    int nRet; 
    int nAddrLen = sizeof(struct sockaddr_in); 
    //接收应答回复
    nRet = recvfrom(s,(LPSTR)&echoReply,sizeof(ECHOREPLY),0,(LPSOCKADDR)lpsaFrom,&nAddrLen); 
    //检验接收结果
    if (nRet == SOCKET_ERROR) 
    { 
        printf("接收失败\t error:%d\n",WSAGetLastError()); 
    } 
    //记录返回的 TTL 
    *pTTL = echoReply.ipHdr.TTL; 
    //返回应答时间
    return(echoReply.echoRequest.dwTime); 
} 

//等待回应答复 ,使用select模型
int WaitForReply(SOCKET s) 
{
    struct timeval timeout;
    fd_set readfds;
    readfds.fd_count = 1;
    readfds.fd_array[0] = s;
    timeout.tv_sec = 1;
    timeout.tv_usec = 0;
    return(select(1, &readfds, NULL, NULL, &timeout));
} 

//PING
void Ping(char *pstrHost,bool logic) 
{ 
    char c; 
    SOCKET rawSocket; 
    LPHOSTENT lpHost; 
    struct sockaddr_in destIP; 
    struct sockaddr_in srcIP; 
    DWORD dwTimeSent; 
    DWORD dwElapsed; 
    unsigned char cTTL; 
    int nLoop,k=4; 
    int nRet,minimum=100000,maximum=0,average=0; 
    int sent=4,reveived=0,lost=0; 
    //创建原始套接字 ,ICMP 类型
	//原始套接字可以自行组装数据包(伪装本地 IP,本地 MAC),可以接收本机网卡上所有的数据帧(数据包)。另外,必须在管理员权限下才能使用原始套接字。
    rawSocket = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    // 第二个注释函数 socket 
    if (rawSocket == SOCKET_ERROR) 
    { 
        printf("原始套接字创建失败\t error:%d\n", WSAGetLastError()); 
        return; 
    } 
    //检测目标主机
    lpHost = gethostbyname(pstrHost); 
    if (lpHost==NULL) 
    { 
        printf("没有找到目标主机:%s\n", pstrHost); 
        return; 
    } 
    //设置目标机地址 
    destIP.sin_addr.s_addr = *((u_long FAR*)(lpHost->h_addr)); // 设置目标 IP 
    destIP.sin_family = AF_INET; //地址规格
    destIP.sin_port = 0; 
    //提示开始进行 PING 
    printf("\n正在Ping %s [%s] 具有%d字节的数据......\n",pstrHost,inet_ntoa(destIP.sin_addr),DATASIZE);
    //发起多次 PING 测试
    for (nLoop=0; nLoop<k; nLoop++){ 
        if (logic) k=k+1; 
        //发送 ICMP 回应请求 
        SendRequest(rawSocket, &destIP); 
        //等待回复的数据
        nRet = WaitForReply(rawSocket); 
        if(nRet == SOCKET_ERROR) 
        { 
            printf("select() error:%d\n", WSAGetLastError()); 
            break; 
        } 
        if (!nRet) 
        { 
            lost++; 
            printf("\n请求超时!"); 
            continue; 
        } 
        //接收回复
        dwTimeSent = RecvReply(rawSocket, &srcIP, &cTTL); 
        reveived++; 
        //计算花费的时间
        dwElapsed = GetTickCount() - dwTimeSent; 
        if(dwElapsed>maximum) maximum=dwElapsed; 
        if(dwElapsed<minimum) minimum=dwElapsed; 
        average+=dwElapsed; 
        printf("\n来自%s的回复-->\t字节:%d\t时间:%ldms\tTTL:%d", 
            inet_ntoa(srcIP.sin_addr),DATASIZE,dwElapsed,cTTL); 
        if(_kbhit()) // Use _getch to throw key away.  
        { 
            if ((c=_getch())==0x2) //crrl -b 
            break; 
        } else 
        Sleep(1000); 
    } 
    printf("\n\n"); 
	printf("%s的Ping统计信息如下:\n",inet_ntoa(srcIP.sin_addr)); 
    printf("数据包: 已发送=%d\t已接收=%d\t丢失=%d\n", sent,reveived,lost,(float)(lost*1.0/sent)*100); 
    if(lost==0) 
    { 
        printf("往返时间如下:\n"); 
        printf("最短时间=%dms\t最长时间=%dms\t平均时间=%dms\n",minimum,maximum,average/sent); 
    } 
    printf("****************************************************"); 
    nRet = closesocket(rawSocket); 
    if (nRet == SOCKET_ERROR) 
    { 
        printf("closesocket() error:%d\n", WSAGetLastError()); 
    } 
} 

//主程序
void main() 
{ 
	printf("*********************WelcometothePingTest*********************\n");
 printf("*************************Time:2018.11.15*************************\n");
    while(1) 
    { 
        WSADATA wsd;// 检测输入的参数 
        //初始化 Winsock 
        if(WSAStartup(MAKEWORD(1, 1), &wsd) != 0){// 第一个函数说明 WSAStartup() 
            printf(" 加载 Winsock 失败 !\n"); 
        } 
        char opt1[100]; 
        char *ptr=opt1; 
        bool log=false; 
        printf("\n\n请输入ip地址/域名:"); 
        cin.getline(opt1,100,'\n');//ping 的地址 字符串
        if(strstr(opt1, "-t")!=NULL) 
        { 
            log=true; 
            strncpy(ptr,opt1+0,strlen(opt1)-3);// 把原字符串的最后三位截取 
            ptr[strlen(opt1)-2]=0; 
            //printf("%s", ptr); 
        } 
        //开始 PING 
        Ping(ptr,log); 
        //程序释放 Winsock 资源
        WSACleanup(); 
    } 
}

 

2018-12-28 15:49:45 qq_37591656 阅读数 2383
  • 第07章-网络通信协议(OSI、TCP、UDP、IP、ARP、ICMP...

    课程内容如下: 通信协议的概念 ISO、OSI七层参考模型详解 TCP、IP模型详解 网络接口层详解 IP协议详解 IP地址和MAC地址特征分析 有了IP地址为什么还要使用MAC地址 ARP工作过程及工作原理解析 ICMP知识解析 ping命令使用详解 tracert命令使用详解 TCP详解 UDP协议详解 网络通信协议(应用层)协议

    8102 人正在学习 去看看 深博

一、预备知识

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;
}
没有更多推荐了,返回首页