自己写个ping程序玩玩---附带抓包

972 篇文章 329 订阅
147 篇文章 46 订阅

       ping功能很常用, 在Windows/Linux上, 我们经常用ping功能来探测对方是否在线。 那么, ping到底是怎么实现的呢? 在本文中, 我们自己来写一个ping, 当然, 功能肯定没有Windows/Linux自带的ping那么强大。 但是, 自己写写, 可以理解更深刻。

       说明, 为了简便起见, 程序中的众多异常, 我就不考虑了, 只聚焦主要功能。

 

       好, 直接上代码:

 

#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")
	

// IP数据包头结构(如果不清楚每个字段的含义, 请查看IP报文格式, 我就不对每个字段进行注释了)
typedef struct iphdr 
{
	unsigned int headLen:4;
	unsigned int version:4;
	unsigned char tos;
	unsigned short totalLen;
	unsigned short ident;
	unsigned short fragAndFlags;
	unsigned char ttl;
	unsigned char proto;
	unsigned short checkSum;
	unsigned int sourceIP;
	unsigned int destIP;
}IpHeader;


// ICMP数据头结构
typedef struct ihdr 
{
	unsigned char iType;
	unsigned char iCode;	
	unsigned short iCheckSum;							
	unsigned short iID;
	unsigned short iSeq;
	unsigned long  timeStamp;
}IcmpHeader;


// 计算ICMP包的校验和(发送前要用)
unsigned short checkSum(unsigned short *buffer, int size) 
{
	unsigned long ckSum = 0;

	while(size > 1)
	{
		ckSum += *buffer++;
		size -= sizeof(unsigned short);
	}

	if(size) 
	{
		ckSum += *(unsigned char*)buffer;
	}	

	ckSum = (ckSum >> 16) + (ckSum & 0xffff);
	ckSum += (ckSum >>16);

	return unsigned short(~ckSum);
}


// 填充ICMP请求包的具体参数
void fillIcmpData(char *icmpData, int dataSize)
{
	IcmpHeader *icmpHead = (IcmpHeader*)icmpData;
	icmpHead->iType = 8;  // 8表示请求包
	icmpHead->iCode = 0;
	icmpHead->iID = (unsigned short)GetCurrentThreadId();
	icmpHead->iSeq = 0;
	icmpHead->timeStamp = GetTickCount();
	char *datapart = icmpData + sizeof(IcmpHeader);
	memset(datapart, 'x', dataSize - sizeof(IcmpHeader)); // 数据部分为xxx..., 实际上有32个x
	icmpHead->iCheckSum = checkSum((unsigned short*)icmpData, dataSize); // 千万要注意, 这个一定要放到最后
}


// 对返回的IP数据包进行解析,定位到ICMP数据
int decodeResponse(char *buf, int bytes, struct sockaddr_in *from, int tid)
{
	IpHeader *ipHead = (IpHeader *)buf;
	unsigned short ipHeadLen = ipHead->headLen * 4 ; 
	if (bytes < ipHeadLen + 8) // ICMP数据不完整, 或者不包含ICMP数据
	{
		return -1; 
	}

	IcmpHeader *icmpHead = (IcmpHeader*)(buf + ipHeadLen);  // 定位到ICMP包头的起始位置
	if (icmpHead->iType != 0) 	// 0表示回应包
	{
		return -2; 
	}

	if (icmpHead->iID != (unsigned short)tid) // 理应相等
	{ 
		return -3; 
	}

	int time = GetTickCount() - (icmpHead->timeStamp); // 返回时间与发送时间的差值
	if(time >= 0)
	{
		return time;
	}

	return -4; // 时间错误
}


// ping操作
int ping(const char *ip, unsigned int timeout)
{
	// 网络初始化
	WSADATA wsaData;
	WSAStartup(MAKEWORD(1, 1), &wsaData);
	unsigned int sockRaw = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);	// 注意, 第三个参数非常重要, 指定了是icmp
	setsockopt(sockRaw, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(timeout));  // 设置套接字的接收超时选项
	setsockopt(sockRaw, SOL_SOCKET, SO_SNDTIMEO, (char*)&timeout, sizeof(timeout));  // 设置套接字的发送超时选项

	// 准备要发送的数据
	int  dataSize = sizeof(IcmpHeader) + 32; // 待会儿会有32个x
	char icmpData[1024] = {0};
	fillIcmpData(icmpData, dataSize);
	unsigned long startTime = ((IcmpHeader *)icmpData)->timeStamp;

	// 远程通信端	
	struct sockaddr_in dest;
	memset(&dest, 0, sizeof(dest));
	struct hostent *hp = gethostbyname(ip);
	memcpy(&(dest.sin_addr), hp->h_addr, hp->h_length);
	dest.sin_family = hp->h_addrtype;

	// 发送数据
	sendto(sockRaw, icmpData, dataSize, 0, (struct sockaddr*)&dest, sizeof(dest));

	int iRet = -1;
	struct sockaddr_in from;
	int fromLen = sizeof(from);	
	while(1)
	{
		// 接收数据
		char recvBuf[1024] = {0};
		int iRecv = recvfrom(sockRaw, recvBuf, 1024, 0, (struct sockaddr*)&from, &fromLen);
		int time  = decodeResponse(recvBuf, iRecv, &from, GetCurrentThreadId());
		if(time >= 0)
		{
			iRet = 0;   // ping ok
			break;
		}
		else if( GetTickCount() - startTime >= timeout || GetTickCount() < startTime)
		{
			iRet = -1;  // ping超时
			break;
		}
	}

	// 释放网络
	closesocket(sockRaw);
	WSACleanup();

	return iRet;
}


// 主函数
int main()
{
	// 请用合法的ip地址, 如果ip地址不合法(最好用程序校验一下), 可能产生程序错误。 
	char szIPs[][16] = 
	{
		"192.168.1.1",    // 我的网关
		"192.168.1.100",  // 我的PC   (wifi接入)
		"192.168.1.101"   // 我的手机 (wifi接入)
	};

	int i = 0;
	int size = sizeof(szIPs) / sizeof(szIPs[0]);
	for(i = 0; i < size; i++)
	{
		int iRet = ping(szIPs[i], 3000); // 超时时间为3000ms

		if(0 == iRet)
		{
			printf("%s 在线\n", szIPs[i]);
		}
		else if(-1 == iRet)
		{
			printf("%s 超时\n", szIPs[i]);
		}
		else
		{
			printf("未知错误");
		}
	}

	return 0;
}

 

      结果:
192.168.1.1 在线
192.168.1.100 在线
192.168.1.101 在线

     在上述过程中, wireshark抓包结果为:

       有一点值得注意, 当arp缓存表中有ip-mac映射值时, Windows/Linux和我上面的ping不再发arp.  否则需要发arp.

       然后, 我把我手机的wifi关掉, 程序运行的结果为:

192.168.1.1 在线
192.168.1.100 在线
192.168.1.101 超时

 

       折腾一下午, 总算是基本搞出来了。 吃饭去得意

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值