聊聊实现C++跨平台ping函数及ICMP请求回显数据包解析

版权声明:本文为博主原创文章,禁止转载。 https://blog.csdn.net/FlushHip/article/details/83993826

ping我们经常使用,大多数的时候是在命令行ping下IP地址,然后一堆输出。

程序中有时候也会用ping函数,那么ping是如何实现的呢。

计算机网络告诉我们,ping函数是基于ICMP协议实现的,而ICMP协议又是基于IP协议弄的(ICMP作为IP协议的数据部分传输)。

ping通过ICMP协议中的类型8代码0来搞的,这个类型和代码的组合在ICMP协议中表示请求回显。如果能正常回显,那么返回的ICMP协议包中的类型是类型0,表示回显成功了。

就是这么简单,那么在我们编程中,也是这么个流程,对ping的地址发送一个ICMP协议包,然后在接受,看看能不能正确接受到返回的ICMP协议包。

但是,在C++的网络编程中,面对ICMP这样底层的协议,就得使用原始套接字,原始套接字是允许访问底层传输协议的一种套接字类型,要用这种原始套接字编程,就必须对TCP/IP协议的协议格式有一定了解,因为,要自己组装IP协议包发送出去,因此就要清楚协议中各个子段的含义。

这里先科普下网络协议,这也便于后面ping程序代码的理解。

首先,IP协议的头部一般为20个字节,协议中会有一个字段叫协议域,填上一些值就表示一些特殊的协议,比如我们的ICMP协议;

其次,看下ICMP协议的结构:

在这里插入图片描述

我们可以通过Wireshark来抓抓包看看ICMP请求回显的包结构:

在命令行下执行ping 172.18.167.25,可以得到下面的网络包:

在这里插入图片描述

一个是请求,一个是回应,分别看看内容

request:
在这里插入图片描述

reply:

在这里插入图片描述

这就是pingrequestreply的数据包。

这里特意给出ping request的ICMP结构(从上面的图片中也可以看出来),因为待会代码中要用:

在这里插入图片描述

数据部分一定做对齐,比如1个字节的数据,你也要加31个字节的空数据进去,为了对齐。

上面的这些理论内容,就会体现在代码之中。

这里实现一个ping的API,函数签名如下:

bool Ping(const std::string & ip);

没有命令行的ping命令那么华丽的输出,就ping一下,能通就返回true

首先定义出上面的请求回显的ICMP头部结构:

struct IcmpHdr
{
    unsigned char icmpType;
    unsigned char icmpCode;
    unsigned short icmpChecksum;

    unsigned short icmpId;
    unsigned short icmpSequence;
};

然后就是创建ICMP包,并别填充好:

char buff[sizeof(IcmpHdr) + 32] = { 0 };
IcmpHdr *pIcmpHdr = (IcmpHdr *)(buff);

unsigned short id = std::rand() % (std::numeric_limits<unsigned short>::max)();

pIcmpHdr->icmpType = 8;
pIcmpHdr->icmpCode = 0;
pIcmpHdr->icmpId = id;
pIcmpHdr->icmpSequence = INDEX++;
std::memcpy(&buff[sizeof(IcmpHdr)], "FlushHip", sizeof("FlushHip"));
pIcmpHdr->icmpChecksum = [](unsigned short *buff, unsigned size) -> unsigned short
{
    unsigned long ret = 0;

    for (unsigned i = 0; i < size; i += sizeof(unsigned short), ret += *buff++) {}

    if (size & 1) ret += *(unsigned char *)buff;

    ret = (ret >> 16) + (ret & 0xFFFF);
    ret += ret >> 16;

    return (unsigned short)~ret;
}((unsigned short *)buff, sizeof(buff));

这里为了待会抓包的时候有辨识度,我在ICMP的数据部分填入了FlushHip字符串。这个校验和也要自己去算,算的方式在代码中有体现,不多说。

然后就是发送ICMP包

if (-1 == sendto(socketFd, buff, sizeof(buff), 0, (sockaddr *)&des, sizeof(des)))
    return false;

最后就是接受回应的ICMP包,并且解析包,看看是不是对应的回应包。

char recv[1 << 10];
int ret = recvfrom(socketFd, recv, sizeof(recv), 0, NULL, NULL);
if (-1 == ret || ret < 20 + sizeof(IcmpHdr))
    return false;

IcmpHdr *pRecv = (IcmpHdr *)(recv + 20);
return !(pRecv->icmpType != 0 || pRecv->icmpId != id);

由于接受到的是IP数据包,因此要自己从IP数据包中取出ICMP数据包。再看看ICMP数据包的类型是不是0,以及是不是对应request所返回的reply包。

最后给出完整跨平台的Ping函数代码,亲测在Windows和Linux(Linux下需要root用户权限,可以参见Windows/Linux中C++对于系统函数发生错误时的调试方法(调试Windows/Linux下创建原始socket失败返回-1))下都没有问题。

#ifdef _WIN32
#include <WinSock2.h>
#pragma comment(lib, "WS2_32")

struct WindowsSocketLibInit
{
    WindowsSocketLibInit()
    {
        WSADATA wsaData;
        WORD sockVersion = MAKEWORD(2, 2);
        WSAStartup(sockVersion, &wsaData);
    }
    ~WindowsSocketLibInit()
    {
        WSACleanup();
    }
} INITSOCKETGLOBALVARIABLE;
#else
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#endif

#include <string>
#include <limits>

bool Ping(std::string ip)
{
    static unsigned INDEX = 0;

    const unsigned IP_HEADER_LENGTH = 20;
    const unsigned FILL_LENGTH = 32;

    struct IcmpHdr
    {
        unsigned char icmpType;
        unsigned char icmpCode;
        unsigned short icmpChecksum;

        unsigned short icmpId;
        unsigned short icmpSequence;
    };

    int socketFd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);

    int timeoutTick = 1000;
    setsockopt(socketFd, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeoutTick, sizeof(timeoutTick));

    sockaddr_in des = { AF_INET, htons(0) };
    des.sin_addr.s_addr = inet_addr(ip.c_str());

    char buff[sizeof(IcmpHdr) + 32] = { 0 };
    IcmpHdr *pIcmpHdr = (IcmpHdr *)(buff);

    unsigned short id = std::rand() % (std::numeric_limits<unsigned short>::max)();

    pIcmpHdr->icmpType = 8;
    pIcmpHdr->icmpCode = 0;
    pIcmpHdr->icmpId = id;
    pIcmpHdr->icmpSequence = INDEX++;
    std::memcpy(&buff[sizeof(IcmpHdr)], "FlushHip", sizeof("FlushHip"));
    pIcmpHdr->icmpChecksum = [](unsigned short *buff, unsigned size) -> unsigned short
    {
        unsigned long ret = 0;

        for (unsigned i = 0; i < size; i += sizeof(unsigned short), ret += *buff++) {}

        if (size & 1) ret += *(unsigned char *)buff;

        ret = (ret >> 16) + (ret & 0xFFFF);
        ret += ret >> 16;

        return (unsigned short)~ret;
    }((unsigned short *)buff, sizeof(buff));

    if (-1 == sendto(socketFd, buff, sizeof(buff), 0, (sockaddr *)&des, sizeof(des)))
        return false;

    char recv[1 << 10];
    int ret = recvfrom(socketFd, recv, sizeof(recv), 0, NULL, NULL);
    if (-1 == ret || ret < 20 + sizeof(IcmpHdr))
        return false;

    IcmpHdr *pRecv = (IcmpHdr *)(recv + 20);
    return !(pRecv->icmpType != 0 || pRecv->icmpId != id);
}

由于套接字默认是阻塞套接字,因此设置了1s的超时。

这段代码是最基础的,你可以在ICMP的数据部分塞入时间戳,这样可以计算响应时间,这样就可以像命令行那样搞一些花里胡哨的输出了。

最后,运行程序,再次抓包,得到如下的东西:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

可以看到,数据部分有我们在程序中塞进去的"FlushHip"字符串,成功。

猜你喜欢

转载自blog.csdn.net/FlushHip/article/details/83993826