网络协议 -- ICMP协议(2) Ping程序

一、Ping实现原理

大多数系统都已经在内核中内置了ping服务器的功能,所以不需要单独的其他进程来接收主机的ping请求。

windows系统下,输入ping /?命令查看ping的用法如下:

用法: ping [-t] [-a] [-n count] [-l size] [-f] [-i TTL] [-v TOS]
           [-r count] [-s count] [[-j host-list] | [-k host-list]]
           [-w timeout] [-R] [-S srcaddr] [-4] [-6] target_name

选项:
    -t             Ping 指定的主机,直到停止。
                   若要查看统计信息并继续操作 - 请键入 Control-Break;
                   若要停止 - 请键入 Control-C。
    -a             将地址解析成主机名。
    -n count       要发送的回显请求数。
    -l size        发送缓冲区大小。
    -f             在数据包中设置“不分段”标志(仅适用于 IPv4)。
    -i TTL         生存时间。
    -v TOS         服务类型(仅适用于 IPv4。该设置已不赞成使用,且
                   对 IP 标头中的服务字段类型没有任何影响)。
    -r count       记录计数跃点的路由(仅适用于 IPv4)。
    -s count       计数跃点的时间戳(仅适用于 IPv4)。
    -j host-list   与主机列表一起的松散源路由(仅适用于 IPv4)。
    -k host-list   与主机列表一起的严格源路由(仅适用于 IPv4)。
    -w timeout     等待每次回复的超时时间(毫秒)。
    -R             同样使用路由标头测试反向路由(仅适用于 IPv6)。
    -S srcaddr     要使用的源地址。
    -4             强制使用 IPv4    -6             强制使用 IPv6

其中-r参数用于记录跃点路由,类似tracert的功能,但他们的实现方式不一样,ping是通过在IP头部“选项”字段记录经过的每个路由的IP来实现记录路由功能的,这种实现有个限制,就是IP首部“选项”字段的最大字节数为40字节,所以最多只能记录10个IP。

tracert的实现方式见:http://blog.csdn.net/china_jeffery/article/details/79142031

ping功能通过网络协议 – ICMP协议(1) 报文格式中介绍的ICMP的回显请求和回显应答来实现,也就是说ping是基于ICMP协议实现的。

ICMP回显请求和回显应答的报文格式如下:
网络协议 -- ICMP协议(2) Ping程序_第1张图片

  • 标识符:在实现中,一般将该字段设置为当前进程ID。这样即使在同一台主机上同时运行了多个ping程序实例, ping程序也可以识别出返回的信息属于哪个进程。
  • 序号:序号一般从0开始(没有强制性,从任何数字开始都可以),每发送一次新的回显请求就加1。因为ICMP是在IP数据报内部被传输的,而IP协议又是不可靠、无连接的,所以ping程序打印出返回的每个分组的序列号,方便我们查看是否有分组丢失、失序或重复。
  • 选项:在“选项”字段中,我们一般放入发送时间戳,这样在收到回应的时候可以用来计算本次ping的耗时。我们经常会指定ping包的大小,所以也会在“选项”字段中填充一些废数据来让包达到一定大小,在下面的FillPingPacket函数就有这样的实现。

二、C++代码实现

完整代码见https://gitee.com/china_jeffery/webrtc项目中的webrtc/src/msvc/test/ping工程。

2.1 定义ICMP、ping首部

networkprotocolheader.h头文件中定义了IP协议、ICMP协议等协议的首部结构体。

#pragma pack(1)
#define __u8 unsigned char
#define __u16 unsigned short
#define __u32 unsigned long

// See: http://blog.csdn.net/china_jeffery/article/details/79045630
//
struct icmp_common_hdr {
    __u8 type;
    __u8 code;
    __u16 check;
    /*Other content start here. */
};

struct ping_header {
    icmp_common_hdr common_hdr;
    __u16 id;
    __u16 seq;
    __u32 timestamp;
};
#pragma pack()

2.2 程序执行参数

DECLARE_bool(h); // 帮助
DECLARE_bool(t); // ping指定的主机直到停止
DECLARE_int(w);  // 等待每次回复的超时时间(毫秒)
DECLARE_int(s);  // 发送ping包超时时间(毫秒)
DECLARE_int(l);  // 发送缓冲区大小
DECLARE_int(i);  // TTL

DEFINE_bool(h, false, "帮助");
DEFINE_bool(t, false, "ping指定的主机直到停止");
DEFINE_int(w, 3000, "等待每次回复的超时时间(毫秒)");
DEFINE_int(s, 3000, "发送ping包超时时间(毫秒)");
DEFINE_int(l, 32, "发送缓冲区大小");
DEFINE_int(i, 128, "TTL");

ping程序的执行参数的定义和解析由webrtc的"rtc_base/flags.h"支持,具体使用方法见:WebRTC-命令行参数解析。

2.3 完整代码

代码中的某些功能,如参数解析、断言、时间戳等基于webrtc的rtc_base实现,这些功能也可以很方便的自己实现。

rtc_base的使用参考:http://blog.csdn.net/china_jeffery/article/details/78887619

另外,使用原始套接字需要管理员权限,如果需要绕开管理员权限,可以使用windows提供的IcmpSendEcho系列函数。

在发送ping请求的时候,我们只封装了一个ICMP报文,并没有自己手动添加IP头,封装IP报文。因为内核会自动添加IP头,如果想自己添加IP头,可以调用setsockopt设置IP_HDRINCL选项,告诉内核由我们自己来封装IP头。

#include 
#include 
#include 
#include "rtc_base/networkprotocolheader.h"
#include "rtc_base/checks.h"
#include "rtc_base/flags.h"
#include "rtc_base/timeutils.h"

DECLARE_bool(h); // 帮助
DECLARE_bool(t); // ping指定的主机直到停止
DECLARE_int(w);  // 等待每次回复的超时时间(毫秒)
DECLARE_int(s);  // 发送ping包超时时间(毫秒)
DECLARE_int(l);  // 发送缓冲区大小
DECLARE_int(i);  // TTL

DEFINE_bool(h, false, "帮助");
DEFINE_bool(t, false, "ping指定的主机直到停止");
DEFINE_int(w, 3000, "等待每次回复的超时时间(毫秒)");
DEFINE_int(s, 3000, "发送ping包超时时间(毫秒)");
DEFINE_int(l, 32, "发送数据大小");
DEFINE_int(i, 128, "TTL");



void FillPingPacket(__u8* icmp_packet, __u16 seq, __u16 icmp_packet_size) {
    RTC_DCHECK(icmp_packet);
    ping_hdr* pping_hdr = reinterpret_cast(icmp_packet);
    pping_hdr->common_hdr.type = 8;
    pping_hdr->common_hdr.code = 0;
    pping_hdr->id = (__u16)GetCurrentProcessId();
    pping_hdr->seq = seq;
    __u32 now = rtc::Time32();

    memcpy((icmp_packet + sizeof(ping_hdr)), &now, sizeof(__u32));

    // fill some junk in the buffer.
    int junk_data_size = FLAG_l - sizeof(__u32); // timestamp
    int junk_offset = icmp_packet_size - junk_data_size;

    if(junk_data_size > 0)
        memset((icmp_packet + junk_offset), 'E', junk_data_size);

    pping_hdr->common_hdr.check = 0;
    pping_hdr->common_hdr.check = rtc::GetCheckSum(reinterpret_cast<__u16*>(icmp_packet), icmp_packet_size);
}

void DecodeIPPacket(__u8* ip_packet, __u16 packet_size) {
    iphdr* ip_hdr = reinterpret_cast(ip_packet);
    __u32 now = rtc::Time32();

    __u16 ip_hdr_len = ip_hdr->ihl * 4; // bytes

    ping_hdr *pping_hdr = reinterpret_cast(ip_packet + ip_hdr_len);
    if (pping_hdr->common_hdr.type != 0 || pping_hdr->common_hdr.code != 0) {
        printf("non-echo response, type=%d, code=%d\n", pping_hdr->common_hdr.type, pping_hdr->common_hdr.code);
        return;
    }

    if (pping_hdr->id != (__u16)GetCurrentProcessId()) {
        printf("other process ping response packet, pid=%d\n", GetCurrentProcessId());
        return;
    }

    __u32 timestamp = 0;
    memcpy(×tamp, reinterpret_cast<__u32*>((__u8*)pping_hdr + sizeof(ping_hdr)), sizeof(__u32));

    in_addr from;
    from.s_addr = ip_hdr->saddr;
    printf("%d bytes from %s, time < %d ms, icmp_seq = %d, TTL = %d \n", 
        packet_size - ip_hdr_len - sizeof(ping_hdr),
        inet_ntoa(from),
        now - timestamp,
        pping_hdr->seq,
        ip_hdr->ttl
    );
}

int main(int argc, char**argv)
{
    rtc::FlagList::SetFlagsFromCommandLine(&argc, argv, true);
    if (FLAG_h) {
        rtc::FlagList::Print(NULL, false);
        return 1;
    }

    char *hostname = argv[argc - 1];
    if (!hostname || strlen(hostname) == 0) {
        printf("Invalid host name\n");
        return 1;
    }

    if (FLAG_l <= 4) {
        return 1;
    }

    WSADATA wsaData;
    WORD wVersionRequested = MAKEWORD(2, 2);
    WSAStartup(wVersionRequested, &wsaData);


    sockaddr_in from;
    int from_len = sizeof(sockaddr_in);

    sockaddr_in dest;
    memset(&dest, 0, sizeof(sockaddr_in));
    dest.sin_family = AF_INET;
    dest.sin_addr.s_addr = inet_addr(hostname);

    // resolve host name
    if (dest.sin_addr.s_addr == INADDR_NONE) {
        unsigned long begin_time = rtc::Time32();
        struct addrinfo* result = nullptr;
        struct addrinfo hints = { 0 };
        hints.ai_family = AF_UNSPEC;

        hints.ai_flags = AI_ADDRCONFIG;
        int ret = getaddrinfo(hostname, nullptr, &hints, &result);
        if (ret != 0) {
            printf("Resolve host name failed, error code = %d\n", ret);
            return 1;
        }
        unsigned long end_time = rtc::Time32();
        struct addrinfo* cursor = result;
        printf("------------------------------\n");
        printf("Resolve [time < %d ms]: \n", end_time - begin_time);
        bool flag = false;
        for (; cursor; cursor = cursor->ai_next) {
            sockaddr_in *paddr_in = reinterpret_cast(cursor->ai_addr);
            printf("%s\n", inet_ntoa(paddr_in->sin_addr));

            if (!flag) {
                dest.sin_addr = paddr_in->sin_addr;
                flag = true;
            }
        }
        freeaddrinfo(result);
        printf("-------------------------------\n");
    }

    printf("Ping %s [TTL %d]: \n", inet_ntoa(dest.sin_addr), FLAG_i);

    // socket函数需要管理员权限
    // 需要绕开管理员权限,可以使用IcmpSendEcho系列函数
    //
    SOCKET s = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    if (s == INVALID_SOCKET) {
        printf("create socket failed, error code = %d\n", WSAGetLastError());
        WSACleanup();
        return 1;
    }

    int err = setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast<const char*>(&FLAG_s), sizeof(FLAG_s));
    RTC_DCHECK(err != SOCKET_ERROR);
    if (err == SOCKET_ERROR) {
        printf("setsockopt for SO_SNDTIMEO failed, error code = %d\n", WSAGetLastError());
        closesocket(s);
        WSACleanup();
        return 1;
    }

    err = setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<const char*>(&FLAG_w), sizeof(FLAG_w));
    RTC_DCHECK(err != SOCKET_ERROR);
    if (err == SOCKET_ERROR) {
        printf("setsockopt for SO_RCVTIMEO failed, error code = %d\n", WSAGetLastError());
        closesocket(s);
        WSACleanup();
        return 1;
    }

    err = setsockopt(s, IPPROTO_IP, IP_TTL, reinterpret_cast<const char*>(&FLAG_i), sizeof(FLAG_i));
    RTC_DCHECK(err != SOCKET_ERROR);
    if (err == SOCKET_ERROR) {
        printf("setsockopt for IP_TTL failed, error code = %d\n", WSAGetLastError());
        closesocket(s);
        WSACleanup();
        return 1;
    }

    // ping request
    int icmp_packet_size = sizeof(ping_hdr)
        + FLAG_l; // data

    __u8 *icmp_packet = new __u8[icmp_packet_size];
    RTC_DCHECK(icmp_packet);

    // ping response
    __u16 ip_packet_size = icmp_packet_size + 20; // 20 bytes ip header, no option.
    __u8 *ip_packet = new __u8[ip_packet_size];
    RTC_DCHECK(ip_packet);

    if (!icmp_packet || !ip_packet) {
        closesocket(s);
        WSACleanup();
        return 1;
    }

    __u16 i = 0;
    while (true) {
        if (i == 0xFFFF)
            i = 0;
        i++;

        if (!FLAG_t) {
            if(i > 4)
                break;
        }

        FillPingPacket(icmp_packet, i, icmp_packet_size);

        int sent = sendto(s, 
            reinterpret_cast<const char*>(icmp_packet), 
            icmp_packet_size,
            0, 
            reinterpret_cast<const sockaddr*>(&dest), 
            sizeof(sockaddr));

        if (sent == SOCKET_ERROR) {
            int gle = WSAGetLastError();
            if (gle == WSAETIMEDOUT) {
                printf("request timeout\n");
                continue;
            }
            else {
                printf("ping %s failed, error code = %d\n", inet_ntoa(dest.sin_addr), gle);
                break;
            }
        }

        if (sent < FLAG_l) {
            printf("warning, sent %d bytes\n", sent);
        }

        int bread = recvfrom(s, 
            reinterpret_cast<char*>(ip_packet),
            ip_packet_size, 
            0, 
            reinterpret_cast(&from), 
            &from_len);

        if (bread == SOCKET_ERROR) {
            int gle = WSAGetLastError();
            if (gle == WSAETIMEDOUT) {
                printf("receive timeout\n");
                continue;
            }
            else {
                printf("ping %s failed, error code = %d\n", inet_ntoa(dest.sin_addr), gle);
                break;
            }
        }

        if (bread < ip_packet_size) {
            printf("too few bytes from %s\n", inet_ntoa(from.sin_addr));
            continue;
        }

        DecodeIPPacket(reinterpret_cast<__u8*>(ip_packet), ip_packet_size);

        Sleep(1000);
    }

    delete [] icmp_packet;
    delete [] ip_packet;

    if (s != INVALID_SOCKET) {
        closesocket(s);
    }

    WSACleanup();
    return 0;
}

运行效果:
网络协议 -- ICMP协议(2) Ping程序_第2张图片

你可能感兴趣的:(☆,网络编程,网络协议)