实现的原理还是很简单的,主要还是要对ICMP协议有所了解。ICMP协议是在IP协议的数据部分实现的,普通的socket只能建立TCP或者UDP连接,实在传输层上做东西,只能控制要传输的数据,不能控制IP包的数据部分(即ICMP包实现的部分),所以我们需要一个原始套接字填充IP协议的数据部分。
#define WIN32_LEAN_AND_MEAN
#include "stdafx.h"
#include
#include
#include
#include
#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;
}