欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-07-13 23:47:24
...

一、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程序

  • 标识符:在实现中,一般将该字段设置为当前进程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 <WinSock2.h>
#include <ws2spi.h>
#include <ws2tcpip.h>
#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<ping_hdr*>(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<iphdr*>(ip_packet);
    __u32 now = rtc::Time32();

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

    ping_hdr *pping_hdr = reinterpret_cast<ping_hdr*>(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(&timestamp, 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<sockaddr_in *>(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<sockaddr*>(&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程序

相关标签: ping icmp