socket实现Ping命令

实现的原理还是很简单的,主要还是要对ICMP协议有所了解。ICMP协议是在IP协议的数据部分实现的,普通的socket只能建立TCP或者UDP连接,实在传输层上做东西,只能控制要传输的数据,不能控制IP包的数据部分(即ICMP包实现的部分),所以我们需要一个原始套接字填充IP协议的数据部分。

#define WIN32_LEAN_AND_MEAN
#include "stdafx.h"
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#include <stdlib.h>

#pragma comment(lib,"ws2_32.lib")

#define IP_RECORD_ROUTE  0x7
#define ICMP_MIN 8
#define ICMP_ECHOREPLY 0
#define ICMP_ECHO 8

#define DEF_PACKET_SIZE  32        // Default packet size
#define MAX_PACKET       1024      // Max ICMP packet size
#define MAX_IP_HDR_SIZE  60        // Max IP header size w/options

typedef struct iphdr {
	unsigned int h_len : 4;						// 包头长度,冒号4表示强制只占四个位
	unsigned int version : 4;					// IP协议版本
	unsigned char tos;							// 服务类型(TOS)
	unsigned short total_len;				// 包的总长度
	unsigned short ident;						// 包的唯一标识
	unsigned short frag_and_flags;		// 标识
	unsigned char ttl;								// 生存时间(TTL)
	unsigned char proto;						// 传输协议 (TCP, UDP等)
	unsigned short checksum;				// IP校验和

	unsigned int sourceIP;
	unsigned int destIP;
}IpHeader;

USHORT checksum(USHORT *buffer, int size) {
	unsigned long cksum = 0;

	while (size > 1) {
		cksum += *buffer++;
		size -= sizeof(USHORT);
	}
	if (size) {
		cksum += *(UCHAR*)buffer;
	}
	cksum = (cksum >> 16) + (cksum & 0xffff);
	cksum += (cksum >> 16);
	return (USHORT)(~cksum);
}

typedef struct _ihdr {//icmp包头的长度不定,但icmp-echo和icmp-echoreply的包头一共12个字节
	BYTE i_type;										// 类型
	BYTE i_code;									// 编码
	USHORT i_cksum;								// 检验和,双字节无符号数,占两个字节
	USHORT i_id;									// 编号
	USHORT i_seq;									// 序列号
	ULONG timestamp;							// 时间戳,四个字节
}IcmpHeader;

void fill_icmp_data(char * icmp_data, int datasize){

	IcmpHeader *icmp_hdr;
	char *datapart;
	// 将缓冲区转换为icmp_hdr结构
	icmp_hdr = (IcmpHeader*)icmp_data;
	// 填充各字段的值
	icmp_hdr->i_type = ICMP_ECHO;									// 将类型设置为ICMP响应包
	icmp_hdr->i_code = 0;														// 将编码设置为0
	icmp_hdr->i_id = (USHORT)GetCurrentThreadId();			// 将编号设置为当前线程的编号
	icmp_hdr->i_cksum = 0;													// 将校验和设置为0
	icmp_hdr->i_seq = 0;														// 将序列号设置为0
	datapart = icmp_data + sizeof(IcmpHeader);					// 定义到数据部分
	// 在数据部分随便填充一些数据
	memset(datapart, 'E', datasize - sizeof(IcmpHeader));
}

int decode_icmp_resp(char *buf, int bytes, sockaddr_in *from, DWORD tid)
{
	IpHeader *iphdr;
	IcmpHeader *icmphdr;
	unsigned short iphdrlen;
	iphdr = (IpHeader*)buf;//先将收到的字符串转成ip报头部的格式
	iphdrlen = iphdr->h_len * 4;//首部长度的单位是四字节,数据报总长度的单位是字节,所以这里要乘以4
	if (bytes < iphdrlen + ICMP_MIN)//ICMP包头最小8个字节
	{
		return -1;
	}
	icmphdr = (IcmpHeader*)(buf + iphdrlen);//buf+iphdrlen的实质是buf+iphdrlen*sizeof(char),因为buf是指向char型的。
	if (icmphdr->i_type != ICMP_ECHOREPLY)
	{
		return -2;
	}
	if (icmphdr->i_id != (USHORT)tid)
	{
		return -3;
	}
	int time = GetTickCount() - (icmphdr->timestamp);
	if (time >= 0)
	{
		return time;
	}
	else
	{
		return -4;
	}
}

int ping(const char *ip, DWORD timeout)
{
	WSADATA wsa;
	SOCKET sockRaw = NULL;
	sockaddr_in dest, from;
	hostent *hp;
	int datasize;
	char *dest_ip;
	char *icmp_data = NULL;
	char *recvbuf = NULL;
	USHORT seq_no = 0;
	int retval;
	int ret;
	// 初始化SOCKET
	if (WSAStartup(MAKEWORD(2, 1), &wsa) != 0){
		ret = -1000;// WSAStartup 错误
		goto FIN;
	}
	// 创建原始套接字
	sockRaw = WSASocket(AF_INET,
		SOCK_RAW,
		IPPROTO_ICMP,
		NULL, 0, WSA_FLAG_OVERLAPPED);
	// 如果出现错误,则转到最后
	if (sockRaw == INVALID_SOCKET) {
		ret = -2;// WSASocket 错误
		goto FIN;
	}
	int bread = setsockopt(sockRaw, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout,
		sizeof(timeout));
	// 如果出现错误,则转到最后
	if (bread == SOCKET_ERROR) {
		ret = -3;// setsockopt 错误
		goto FIN;
	}
	// 设置套接字的发送超时选项
	bread = setsockopt(sockRaw, SOL_SOCKET, SO_SNDTIMEO, (char*)&timeout,
		sizeof(timeout));
	if (bread == SOCKET_ERROR) {
		ret = -4;// setsockopt 错误
		goto FIN;
	}
	memset(&dest, 0, sizeof(dest));

	unsigned int addr = 0;					// 将IP地址转换为网络字节序
	hp = gethostbyname(ip);				// 获取远程主机的名称
	if (!hp){
		addr = inet_addr(ip);   //如果使用gethostbyname取得网络地址失效的话就默认使用inet_addr产生的网络地址
	}
	if ((!hp) && (addr == INADDR_NONE)) {
		ret = -5; // 域名错误
		goto FIN;
	}
	// 配置远程通信地址
	if (hp != NULL)
		memcpy(&(dest.sin_addr), hp->h_addr, hp->h_length);
	else
		dest.sin_addr.s_addr = addr;

	if (hp)
		dest.sin_family = hp->h_addrtype;
	else
		dest.sin_family = AF_INET;
	dest_ip = inet_ntoa(dest.sin_addr);//获得gethostbyname返回的网络地址所对应的点分十进制地址,这才是真正要Ping的地址。

	datasize = DEF_PACKET_SIZE;
	datasize = datasize + sizeof(IcmpHeader);
	char icmp_dataStack[MAX_PACKET];
	char recvbufStack[MAX_PACKET];
	icmp_data = icmp_dataStack;
	recvbuf = recvbufStack;
	if (!icmp_data)//icmp包分配内存失败
	{
		ret = -6;
		goto FIN;
	}
	memset(icmp_data, 0, MAX_PACKET);
	fill_icmp_data(icmp_data, datasize);
	//((IcmpHeader*)icmp_data)->i_cksum = 0;
	DWORD startTime = GetTickCount();
	((IcmpHeader*)icmp_data)->timestamp = startTime;
	//((IcmpHeader*)icmp_data)->i_seq = seq_no++;
	((IcmpHeader*)icmp_data)->i_cksum = checksum((USHORT*)icmp_data, datasize);
	int bwrote;
	bwrote = sendto(sockRaw, icmp_data, datasize, 0, (struct sockaddr*)&dest, sizeof(dest));//发送数据部分(即ICMP部分),由RAWsocket负责封装成IP包
	if (bwrote == SOCKET_ERROR)
	{
		if (WSAGetLastError() != WSAETIMEDOUT)
		{
			ret = -7;
			goto FIN;
		}
	}
	if (bwrote < datasize)
	{
		ret = -8;
		goto FIN;
	}
	LARGE_INTEGER ticksPerSecond;
	LARGE_INTEGER start_tick;
	LARGE_INTEGER end_tick;
	double elapsed;
	QueryPerformanceFrequency(&ticksPerSecond);
	QueryPerformanceCounter(&start_tick);
	int fromlen = sizeof(from);
	while (1)
	{
		retval = recvfrom(sockRaw, recvbuf, MAX_PACKET, 0, (struct sockaddr*)&from, &fromlen);
		if (retval == SOCKET_ERROR)
		{
			if (WSAGetLastError() == WSAETIMEDOUT)
			{
				ret = -1;
				goto FIN;
			}
			ret = -9;
			goto FIN;
		}
		int time = decode_icmp_resp(recvbuf, retval, &from, GetCurrentThreadId());
		if (time >= 0)
		{
			QueryPerformanceCounter(&end_tick);
			elapsed = ((double)(end_tick.QuadPart - start_tick.QuadPart) / ticksPerSecond.QuadPart);
			ret = (int)(elapsed * 1000);
			goto FIN;
		}
		else if (GetTickCount() - startTime >= timeout || GetTickCount() < startTime)
		{
			ret = -1;
			goto FIN;
		}
	}
FIN:
	closesocket(sockRaw);
	WSACleanup();
	return ret;
}

int main(int argc, char **argv) {
	printf("ping %s\n", argv[1]);
	int ret = ping(argv[1], 5000);
	if (ret >= 0)
	{
		printf("%s在线,用时%dms\n", argv[1], ret);
	}
	else
	{
		printf("%d\n", ret);
	}
	system("pause");
	return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值