从C++实现Ping开始说起


在C++中实现ping功能,并不难。但真正了解ping是需要花费一番功夫的。

 Ping功能是在ICMP基础上实现的。IP协议并不是一个可靠的协议,它不保证数据被送达,那么,保证数据送达的工作应该由其他的模块来完成。其中一个重要的模块就是ICMP(网络控制报文)协议。ICMP主要是用来实现IP系统间传递差错和管理报文,是任何IP实现必须和要求的组成部分。它是TCP/IP协议族的一个子协议,属于网络层协议。ICMP提供一致易懂的出错报告信息。发送的出错报文返回到发送原数据的设备,因为只有发送设备才是出错报文的逻辑接受者。发送设备随后可根据ICMP报文确定发生错误的类型,并确定如何才能更好地重发失败的数据包。当传送IP数据包发生错误--比如主机不可达,路由不可达等等,ICMP协议将会把错误信息封包,然后传送回给主机。给主机一个处理错误的机会,这也就是为什么说建立在IP层以上的协议是可能做到安全的原因。

。在程序中实现Ping功能时,常用的方法有三:

一、调用system实现

#include 
#include 
#include 
char YN(int k) {
    FILE *f;
    char fn[40];
    char ln[80];
    char yn;
    int n;

    yn='N';
    sprintf(fn,"d:\\ping%d.txt",k);
    f=fopen(fn,"r");
    if (NULL!=f) {
        n=0;
        while (1) {
            if (NULL==fgets(ln,80,f)) break;//
            if (strstr(ln,"ms ")) {
                yn='Y';
                break;//
            }
            n++;
            if (n>=4) break;//
        }
        fclose(f);
    }
    return yn;
}
void main(int argc,char **argv) {
    char cmdstr[256];
    int i;
    int IP[3];
    char c;

    if (argc<2) {
    USAGE:
        printf("Usage example:\n    %s 192.168.60.\nto test 192.168.60.1-254\n",argv[0]);
        return;
    }
    if (4==sscanf(argv[1],"%d.%d.%d%c",&IP[0],&IP[1],&IP[2],&c)) {
        if (0<=IP[0] && IP[0]<=255
         && 0<=IP[1] && IP[1]<=255
         && 0<=IP[2] && IP[2]<=255
         && '.'==c) {
            for (i=1;i<255;i++) {
                sprintf(cmdstr,"cmd /c ping %s%d -n 1 -w 1000 >d:\\ping%d.txt",argv[1],i,i);
                WinExec(cmdstr,SW_HIDE);
            }
            Sleep(3000);
            for (i=1;i<255;i++) {
                printf("%c %s%d\n",YN(i),argv[1],i);
            }
            Sleep(3000);
            WinExec("cmd /c del /q d:\\ping*.txt",SW_HIDE);
        } else goto USAGE;
    } else goto USAGE;
}

二、使用socket协议实现

// MyPing.cpp : Defines the entry point for the console application.
 
//
#include "stdafx.h"
 
#include 
 
#include 
 
#include 
 
#include 
 
 
#define ICMP_ECHO 8
 
#define ICMP_ECHOREPLY 0
 
#define ICMP_MIN 8 //Minimum 8-byte ICMP packet (header)
 
  
 
#define DEF_PACKET_SIZE 32
 
#define MAX_PACKET 1024
 
#define MAX_IP_HDR_SIZE 60
 
  
 
//IP header structure
 
typedef struct _iphdr
 
{
 
    unsigned int h_len:4;//Length of the header
 
    unsigned int version:4;//Version of IP
 
    unsigned char tos;//Type of service
 
    unsigned short total_len;//Total length of the packet
 
    unsigned short ident;//Unique identifier
 
    unsigned short frag_and_flags;//Flags
 
    unsigned char ttl;//Time to live
 
    unsigned char proto;//Protocol (TCP,UDP,etc.)
 
    unsigned short checksum;//IP checksum
 
  
 
    unsigned int sourceIP;
 
    unsigned int destIP;
 
} IpHeader;
 
  
 
//ICMP header structure
 
typedef struct _icmphdr
 
{
 
    BYTE i_type;
 
    BYTE i_code;//Type sub code
 
    USHORT i_cksum;
 
    USHORT i_id;
 
    USHORT i_seq;
 
  
 
    //This is not the standard header, but we reserve space for time
 
    ULONG timestamp;
 
} IcmpHeader;
 
  
 
//IP option header--use with socket option IP_OPTIONS
 
typedef struct _ipoptionhdr
 
{
 
    unsigned char code;//Option type
 
    unsigned char len;//Length of option hdr
 
    unsigned char ptr;//Offset into optons
 
    unsigned long addr[9];//List of IP addrs
 
} IpOptionHeader;
 
  
 
int datasize;
 
char* lpdest;
 
  
 
//Print usage information
 
void usage()
 
{
 
    printf("usage:MyPing -i:IP [data size]\n");
 
    printf("    -i:IP           remote machine to Ping\n");
 
    printf("    datasize     can be up to 1 KB\n");
 
  
 
    ExitProcess(1);
 
}
 
  
 
//Helper function to fill in various fields for our ICMP request
 
void FillICMPData(char* icmp_data, int datasize)
 
{
 
    IcmpHeader* icmp_hdr = (IcmpHeader*)icmp_data;
 
    icmp_hdr->i_type = ICMP_ECHO;//Request an ICMP echo
 
    icmp_hdr->i_code = 0;
 
    icmp_hdr->i_id = (USHORT)GetCurrentProcessId();
 
    icmp_hdr->i_cksum = 0;
 
    icmp_hdr->i_seq = 0;
 
  
 
    char* datapart = icmp_data + sizeof(IcmpHeader);
 
  
 
    //Place some junk in the buffer
 
    memset(datapart, 'E', datasize - sizeof(IcmpHeader));
 
}
 
  
 
//This function calculates the 16-bit one's complement sum
 
//of the supplied buffer (ICMP) header
 
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);
 
}
 
  
 
//If the IP option header is present, find the IP options
 
//within the IP header and print the record route option values
 
void DecodeIPOptions(char* buf, int bytes)
 
{
 
    IpOptionHeader* ipopt = (IpOptionHeader*)(buf + 20);
 
  
 
    printf("RR:    ");
 
    for (int i = 0; i < (ipopt->ptr / 4) - 1; i++)
 
    {
 
        IN_ADDR inaddr;
 
        inaddr.S_un.S_addr = ipopt->addr[i];
 
  
 
        if (i != 0)
 
        {
 
            printf("        ");
 
        }
 
  
 
        HOSTENT* host = gethostbyaddr((char*)&inaddr.S_un.S_addr,
 
            sizeof(inaddr.S_un.S_addr), AF_INET);
 
        if (host)
 
        {
 
            printf("(%-15s) %s\n", inet_ntoa(inaddr), host->h_name);
 
        }
 
        else
 
        {
 
            printf("(%-15s)\n", inet_ntoa(inaddr));
 
        }
 
    }
 
  
 
    return;
 
}
 
  
 
//The response is an IP packet. We must decode the IP header to
 
//locate the ICMP data.
 
void DecodeICMPHeader(char* buf, int bytes, struct sockaddr_in* from)
 
{
 
    static int icmpcount = 0;
 
    IpHeader* iphdr = (IpHeader*)buf;
 
  
 
    //Number of 32-bit words * 4 = bytes
 
    unsigned short iphdrlen = iphdr->h_len * 4;
 
    DWORD tick = GetTickCount();
 
  
 
    if ((iphdrlen == MAX_IP_HDR_SIZE) && (!icmpcount))
 
    {
 
        DecodeIPOptions(buf, bytes);
 
    }
 
  
 
    if (bytes < iphdrlen + ICMP_MIN)
 
    {
 
        printf("Too few bytes from %s\n", inet_ntoa(from->sin_addr));
 
    }
 
  
 
    IcmpHeader* icmphdr = (IcmpHeader*)(buf + iphdrlen);
 
    if (icmphdr->i_type != ICMP_ECHOREPLY)
 
    {
 
        printf("nonecho type %d recvd\n", icmphdr->i_type);
 
        return;
 
    }
 
  
 
    //Make sure this is an ICMP reply to something we sent!
 
    if (icmphdr->i_id != (USHORT)GetCurrentProcessId())
 
    {
 
        printf("someone else's packet!\n");
 
        return;
 
    }
 
  
 
    printf("%d bytes from %s:", bytes, inet_ntoa(from->sin_addr));
 
    printf("  icmp_seq = %d. ", icmphdr->i_seq);
 
    printf("  time:%d ms", tick - icmphdr->timestamp);
 
    printf("\n");
 
  
 
    icmpcount++;
 
    return;
 
}
 
  
 
void ValidateArgs(int argc, _TCHAR** argv)
 
{
 
    lpdest = NULL;
 
    datasize = DEF_PACKET_SIZE;
 
  
 
    for (int i = 1; i < argc; i++)
 
    {
 
        if ((argv[i][0] == '-') || (argv[i][0] == '/'))
 
        {
 
            switch (tolower(argv[i][1]))
 
            {
 
            case 'i':
 
                lpdest = argv[i] + 3;
 
                break;
 
            default:
 
                usage();
 
                break;
 
            }
 
        }
 
        else if (isdigit(argv[i][0]))
 
        {
 
            datasize = atoi(argv[i]);
 
        }
 
    }
 
}
 
  
 
int _tmain(int argc, _TCHAR* argv[])
 
{
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
 
    {
        printf("WSAStartup() failed:%d\n", GetLastError());
        return -1;
    }
 
 
    ValidateArgs(argc, argv);
 
  
 
    SOCKET sockRaw = WSASocket(AF_INET, SOCK_RAW, IPPROTO_ICMP,
 
        NULL, 0, WSA_FLAG_OVERLAPPED);
 
    if (sockRaw == INVALID_SOCKET)
 
    {
 
        printf("WSASocket() failed:%d\n", WSAGetLastError());
 
        return -1;
 
    }
 
  
 
    //Set the send/recv timeout values
 
    struct sockaddr_in from;
    int fromlen = sizeof(from);
    int timeout = 1000;
 

    int bread = setsockopt(sockRaw, SOL_SOCKET, SO_RCVTIMEO,
 
        (char*)&timeout, sizeof(timeout));
 
    if (bread == SOCKET_ERROR)
 
    {
 
        printf("setsockopt(SO_RCVTIMEO) failed:%d\n", WSAGetLastError());
 
        return -1;
 
    }
 
    timeout = 1000;

    bread = setsockopt(sockRaw, SOL_SOCKET, SO_SNDTIMEO,(char*)&timeout, sizeof(timeout));
    if (bread == SOCKET_ERROR)
    {
        printf("setsockopt(SO_SNDTIMEO) failed:%d\n", WSAGetLastError());
        return -1;
 
    }

    struct sockaddr_in dest;
    memset(&dest, 0, sizeof(dest));
 
    //Resolve the endpoint's name if necessary
    dest.sin_family = AF_INET;
 
  
 
    if ((lpdest != NULL) && strlen(lpdest) != 0)
    {
        dest.sin_addr.s_addr = inet_addr(lpdest);
    }
 
    else
 
    {
        struct hostent* hp = gethostbyname(lpdest);
        if (hp != NULL)
 
        {
            memcpy(&(dest.sin_addr), hp->h_addr, hp->h_length);
            dest.sin_family = hp->h_addrtype;
            printf("dest.sin_addr = %s\n", inet_ntoa(dest.sin_addr));
 
        }
 
        else
 
        {
            printf("gethostbyname() failed:%d\n", WSAGetLastError());
            return -1;
        }
 
    }
 
 
    //Create the ICMP packet
 
    datasize += sizeof(IcmpHeader);
 
  
 
    char* icmp_data = (char*)HeapAlloc(GetProcessHeap(),
 
        HEAP_ZERO_MEMORY, MAX_PACKET);
 
    if (!icmp_data)
 
    {
 
        printf("HeapAlloc() failed:%d\n", GetLastError());
 
        return -1;
 
    }
 
    memset(icmp_data, 0, MAX_PACKET);
    FillICMPData(icmp_data, datasize);
    char* recvbuf = (char*)HeapAlloc(GetProcessHeap(),   HEAP_ZERO_MEMORY, MAX_PACKET);
 
  
 
    //Start sending/receiving ICMP packets
 
    USHORT seq_no = 0;
 
    while (true)
    {
        static int nCount = 0;
        int bwrote;
 
        if (nCount++ == 4)
        {
            break;
        }
 
 
        ((IcmpHeader*)icmp_data)->i_cksum = 0;
        ((IcmpHeader*)icmp_data)->timestamp = GetTickCount();
        ((IcmpHeader*)icmp_data)->i_seq = seq_no++;
        ((IcmpHeader*)icmp_data)->i_cksum = checksum((USHORT*)icmp_data, datasize);
 
 
        bwrote = sendto(sockRaw, icmp_data, datasize, 0,(struct sockaddr*)&dest, sizeof(dest));
        if (bwrote == SOCKET_ERROR)
 
        {
            if (WSAGetLastError() == WSAETIMEDOUT)
            {
                printf("timed out\n");
                continue;
            }
 
            printf("sendto() failed:%d\n", WSAGetLastError());
            return -1;
        }
 
        if (bwrote < datasize)
        {
            printf("Wrote %d bytes\n", bwrote);
        }
 
        bread = recvfrom(sockRaw, recvbuf, MAX_PACKET, 0,
            (struct sockaddr*)&from, &fromlen);
        if (bread == SOCKET_ERROR)
        {
            if (WSAGetLastError() == WSAETIMEDOUT)
 
            {
                printf("timed out\n");
 
                continue;
 
            }
 
            printf("revefrom() failed:%d\n", WSAGetLastError());
            return -1;
        }
 
 
        DecodeICMPHeader(recvbuf, bread, &from);
 
        Sleep(1000);
 
    }
 
 
    //Cleanup
 
    if (sockRaw != INVALID_SOCKET)
    {
        closesocket(sockRaw);
    }
 
    HeapFree(GetProcessHeap(), 0, recvbuf);
    HeapFree(GetProcessHeap(), 0, icmp_data);
    WSACleanup();
 
    return 0;
 
}

三、使用Win API调用ICMP.DLL实现、

在这里需要注意的一点是,Windows平台实现ping功能,如果需要使用到IcmpParseReplies函数,需要涉及到两个动态库Icmp.dll和Iphlpapi.dll。IcmpParseReplies函数在Windows2000上面由Icmp.dll导出,而在 Windows XP及以后的版本是由Iphlpapi.dll导出的。通过调用LoadLibrary和GetProcAddress来检测IcmpParseReplies由哪个动态库导出。优先判断Iphlpapi.dll是否存在且有导出此函数,如果存在就使用此动态库,否则再判断Icmp.dll。

另外随着IP6的使用,如果业务上涉及到这一块,也应该要留意。

常用的函数有三个:IcmpCreateFile、IcmpSendEcho、IcmpCloseHandle。具体使用见下实例。

void Ping(char *pIPAddr)
{
HANDLE iHwnd;
iHwnd=IcmpCreateFile();
IPAddr pAddr;
pAddr=(IPAddr)inet_addr (pIPAddr);
icmp_echo_reply pData;
for(int i=1;i<=LoopSend;i++)
{
IcmpSendEcho(iHwnd,pAddr,NULL,0,NULL,(LPVOID)&pData,sizeof(icmp_echo_reply),0);
if (pData.Status==0)
{
printf("Ping测试返回的结果: Time=%dms TTL=%d \n",(int)pData.RoundTripTime,(int)pData.Options.Ttl);
}
else
{
printf("Ping测试失败...\n");
}
}
if (!IcmpCloseHandle(iHwnd)) printf("Close handle has Error!\n");
}

你可能会发现,用IcmpSendEcho 测试 127.0.0.1的时候,ICMP_ECHO_REPLY.RoundTripTime 会等于0

其实,这个函数是没有出错的,即使用Ping 127.0.0.1也是可以的

那么应该怎么去判断这个函数出错呢?

用 ICMP_ECHO_REPLY.Status 来获得测试状态(记住,当Status为0的时候,函数是正确运行的)

常量名

含义

IP_SUCCESS

状态是成功。

IP_BUF_TOO_SMALL

11001

答复缓冲区太小。

IP_DEST_NET_UNREACHABLE

11002

目标网络不可达。

IP_DEST_HOST_UNREACHABLE

11003

目标主机不可达。

IP_DEST_PROT_UNREACHABLE

11004

目的地的协议是遥不可及。

IP_DEST_PORT_UNREACHABLE

11005

目标端口不可达。

IP_NO_RESOURCES

11006

IP资源不足是可用的。

IP_BAD_OPTION

11007

指定了错误的IP选项。

IP_HW_ERROR

11008

一个硬件错误。

IP_PACKET_TOO_BIG

11009

包太大。

IP_REQ_TIMED_OUT

11010

请求超时。

IP_BAD_REQ

11011

一个坏的请求。

IP_BAD_ROUTE

11012

一个糟糕的路线。

IP_TTL_EXPIRED_TRANSIT

11013

在传输过程中的生存时间(TTL)的过期。

IP_TTL_EXPIRED_REASSEM

11014

在碎片重组过程中的生存时间过期。

IP_PARAM_PROBLEM

11015

一个参数的问题。

IP_SOURCE_QUENCH

11016

数据报到达太快,处理和数据报可能被丢弃。

IP_OPTION_TOO_BIG

11017

一个IP选项是太大了。

IP_BAD_DESTINATION

11018

一个坏的目的地。

IP_GENERAL_FAILURE

11050

一般故障。这个错误可以返回一些畸形的ICMP数据包

要了解Ping的原理,我们先来了解下ping命令的使用

通过发送“网际消息控制协议 (ICMP)”回响请求消息来验证与另一台 TCP/IP 计算机的 IP 级连接。回响应答消息的接收情况将和往返过程的次数一起显示出来。Ping 是用于检测网络连接性、可到达性和名称解析的疑难问题的主要 TCP/IP 命令。

语法

ping [-t] [-a] [-n Count] [-l Size] [-f] [-i TTL] [-v TOS] [-r Count] [-s Count] [{-j HostList | -k HostList}] [-w Timeout] [TargetName]

-t

指定在中断前 ping 可以持续发送回响请求信息到目的地。要中断并显示统计信息,请按 CTRL-BREAK。要中断并退出 ping,请按 CTRL-C。

-a

指定对目的地 IP 地址进行反向名称解析。如果解析成功,ping 将显示相应的主机名。

-n Count

指定发送回响请求消息的次数。默认值为 4。

-lSize

指定发送的回响请求消息中“数据”字段的长度(以字节表示)。默认值为 32。size 的最大值是 65,527。

-f

指定发送的回响请求消息带有“不要拆分”标志(所在的 IP 标题设为 1)。回响请求消息不能由目的地路径上的路由器进行拆分。该参数可用于检测并解决“路径最大传输单位 (PMTU)”的故障。

-i TTL

指定发送回响请求消息的 IP 标题中的 TTL 字段值。其默认值是是主机的默认 TTL 值。对于 Windows XP 主机,该值一般是 128。TTL 的最大值是 255。

-v TOS

指定发送回响请求消息的 IP 标题中的“服务类型 (TOS)”字段值。默认值是 0。TOS 被指定为 0 到 255 的十进制数。

-r Count

指定 IP 标题中的“记录路由”选项用于记录由回响请求消息和相应的回响应答消息使用的路径。路径中的每个跃点都使用“记录路由”选项中的一个值。如果可能,可以指定一个等于或大于来源和目的地之间跃点数的 Count。Count 的最小值必须为 1,最大值为 9。

-s Count

指定 IP 标题中的“Internet 时间戳”选项用于记录每个跃点的回响请求消息和相应的回响应答消息的到达时间。Count 的最小值必须为 1,最大值为 4。

-jPath

指定回响请求消息使用带有 HostList 指定的中间目的地集的 IP 标题中的“稀疏资源路由”选项。可以由一个或多个具有松散源路由的路由器分隔连续中间的目的地。主机列表中的地址或名称的最大数为 9,主机列表是一系列由空格分开的 IP 地址(带点的十进制符号)。

-k HostList

指定回响请求消息使用带有 HostList 指定的中间目的地集的 IP 标题中的“严格来源路由”选项。使用严格来源路由,下一个中间目的地必须是直接可达的(必须是路由器接口上的邻居)。主机列表中的地址或名称的最大数为 9,主机列表是一系列由空格分开的 IP 地址(带点的十进制符号)。

-w Timeout

指定等待回响应答消息响应的时间(以微妙计),该回响应答消息响应接收到的指定回响请求消息。如果在超时时间内未接收到回响应答消息,将会显示“请求超时”的错误消息。默认的超时时间为 4000(4 秒 )。

TargetName

指定目的端,它既可以是 IP 地址,也可以是主机名。

/?

在命令提示符显示帮助。

 每个ICMP报文都有自己的格式,但它们开始的三个字段都是一样的:一个8位的报文类型(type)用来标识报文,一个8位的代码(code)用来 提供有关类型的进一步信息,一个16位的校验和(checksum)。(ICMP采用和IP相同的校验和算法,但ICMP校验和只覆盖ICMP报文)。这 里我们给出ICMP报文首部的数据结构:

struct ICMPHEADER

    BYTE   i_type;               // 类型

    BYTE   i_code;               // 代码

    USHORT i_cksum;              // 首部校验和

    USHORT i_id;                 // 标识

    USHORT i_seq;                // 序列号

    ULONG timestamp;             // 时间戳(选用)

};

下表表示了ICMP的报文类型及其含义:

TYPE

CODE

Description

Query

Error

Echo Reply——回显应答(Ping应答)

Network Unreachable——网络不可达

Host Unreachable——主机不可达

Protocol Unreachable——协议不可达

Port Unreachable——端口不可达

Fragmentation needed but no frag. bit set——需要进行分片但设置不分片比特

Source routing failed——源站选路失败

Destination network unknown——目的网络未知

Destination host unknown——目的主机未知

Source host isolated (obsolete)——源主机被隔离(作废不用)

Destination network administratively prohibited——目的网络被强制禁止

10

Destination host administratively prohibited——目的主机被强制禁止

11

Network unreachable for TOS——由于服务类型TOS,网络不可达

12

Host unreachable for TOS——由于服务类型TOS,主机不可达

13

Communication administratively prohibited by filtering——由于过滤,通信被强制禁止

14

Host precedence violation——主机越权

15

Precedence cutoff in effect——优先中止生效

Source quench——源端被关闭(基本流控制)

Redirect for network——对网络重定向

Redirect for host——对主机重定向

Redirect for TOS and network——对服务类型和网络重定向

Redirect for TOS and host——对服务类型和主机重定向

Echo request——回显请求(Ping请求)

Router advertisement——路由器通告

10

Route solicitation——路由器请求

11

TTL equals 0 during transit——传输期间生存时间为0

11

TTL equals 0 during reassembly——在数据报组装期间生存时间为0

12

IP header bad (catchall error)——坏的IP首部(包括各种差错)

12

Required options missing——缺少必需的选项

13

Timestamp request (obsolete)——时间戳请求(作废不用)

14

Timestamp reply (obsolete)——时间戳应答(作废不用)

15

Information request (obsolete)——信息请求(作废不用)

16

Information reply (obsolete)——信息应答(作废不用)

17

Address mask request——地址掩码请求

18

Address mask reply——地址掩码应答