zoukankan      html  css  js  c++  java
  • 通信编程:基于 ICMP 编写 ping 程序

    ICMP 协议

    因特网控制报文协议

    ICMP,即因特网控制报文协议,在主机和路由器之间起到沟通网络层信息的作用。最典型的用途就是差错报告,它允许主机或路由器报告查错情况和提交有关异常情况的报告。例如网络通不通、主机是否可达、路由是否可用等网络本身的消息,这些控制消息虽对于数据的传递起着重要的作用。ICMP 报文作为 IP 有效载荷承载的,因此虽然 ICMP 被认为是 IP 的一部分,但在体系结构上 ICMP 位于 IP 之上。当主机接收到指明上层协议为 ICMP 的 IP 数据报时,该数据报分解的内容应当交给 ICMP。

    ICMP 报文格式

    ICMP 报文包括 IP 头部、ICMP 头部和 ICMP 报文 3 个部分,ICMP 报文是作为 IP 有效载荷承载的。

    字段 说明
    Type ICMP 的类型,标识生成的错误报文;
    Code 进一步划分 ICMP 的类型,该字段用来查找产生错误的原因;
    Checksum 校验码,字段包含有从 ICMP 报头和数据部分计算得来的,用于检查错误的数据;
    ID ID 值,在 Echo Reply 类型的消息中要返回这个字段;
    Sequence 这个字段包含一个序号,在 Echo Reply 类型的消息中要返回这个字段。

    ICMP 报文类型

    常用报文类型如下:

    对于 Ping 程序而言是会发送一个回显请求(类型 8 编码 0)报文给目的主机,目的主机收到之后就发送回显应答(类型 0 编码 0)报文进行回显。TTL 报文(类型 11 编码 0)是在 Traceroute 程序中,路由器检查到 Traceroute 发出的 IP 数据报中 TTL 正好过期,因此路由器就需要丢包并且发送该警告报文返回源主机。源主机就可以得到路由器的 IP 地址,以此达到路由追踪的目的。

    原始套接字

    原始套接字是允许访问底层传输协议的一种套接字类型,可以直接从应用层将数据送到网络层,在网络层对协议进行解析,起到一种“隔山打牛”的作用。原始套接字有两种类型,第一种类型是在 IP 头中使用预定义的协议,例如 ICMP,第二种类型是在 IP 头中使用自定义的协议。原始套接字提供管理下层传输的能力,它们可能会被恶意利用,因此为了保证安全性仅 Administrator 组的成员能够创建 SOCK_RAW 类型的套接字。
    创建原始套接字的函数也是 socket 或者WSASocket,要将套接字类型指定为SOCK_RAW,第 3 个参数 protocol 的值将成为 IP 头中协议域的值。也就是说如果创建原始套接字时,IPPROTO_ICMP 指定要使用 ICMP 协议。

    SOCKET sRaw = ::Socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    

    协议类型可以使用 ICMP,也可以使用 IGMP、UDP、IP,对应的宏定义分别是 IPPROTO_IGMP、IPPROTO_UDP、IPPROTO_IP或IPPROTO_RAW,其中协议标志 IPPROTO_UDP、IPPROTO_IP和IPPROTO_RAW 需要有效 IP_HDRINCL 选项。使用恰当的协议标志创建原始套接字之后,便可以在发送和接收调用中使用此套接字句柄了。

    ping 程序编写

    Ping 经常用来确定特定的主机是否存在,是否可以到达。通过产生一个 ICMP 回显请求发送给目的主机,根据应答的报文情况,便可以确定是否可以成功到达那个机器。

    ICMP 头结构

    初始化 ICMP 头时先初始化消息类型和代码域,之后回显请求头,然后编写校验和的计算方法。发送ICMP报文时,必须由程序自己计算校验和,将它填入 ICMP 头部对应的域中。校验和的计算方法是:将数据以字为单位累加到一个双字中,如果数据长度为奇数,最后一个字节将被扩展到字,累加的结果是一个双字,最后将这个双字的高 16 位和低 16 位相加后取反,便得到了校验和。

    #include "initsock.h"
    
    #include <iostream>
    using namespace std;
    
    CInitSock initSock;     // 初始化Winsock库
    
    typedef struct icmp_hdr
    {
        unsigned char   icmp_type;      // 消息类型
        unsigned char   icmp_code;      // 代码
        unsigned short  icmp_checksum;  // 校验和
        // 下面是回显头
        unsigned short  icmp_id;        // 用来惟一标识此请求的ID号,通常设置为进程ID
        unsigned short  icmp_sequence;  // 序列号
        unsigned long   icmp_timestamp; // 时间戳
    } ICMP_HDR, * PICMP_HDR;
    
    USHORT checksum(USHORT* buff, int size)
    {
        unsigned long cksum = 0;
        //将数据以字为单位累加到 cksum 中
        while (size > 1)
        {
            cksum += *buff++;
            size -= sizeof(USHORT);
        }
        // 如果为奇数,将最后一个字节扩展到双字,再累加到cksum中
        if (size)
        {
            cksum += *(UCHAR*)buff;
        }
        // 将 32 位的 chsum 高 16 位和低 16 位相加,然后取反
        cksum = (cksum >> 16) + (cksum & 0xffff);
        cksum += (cksum >> 16);    //进位处理
        return (USHORT)(~cksum);
    }
    

    initsock.h

    #include <winsock2.h>
    #pragma comment(lib, "WS2_32")  // 链接到 WS2_32.lib
    
    class CInitSock
    {
    public:
        /*CInitSock 的构造器*/
        CInitSock(BYTE minorVer = 2, BYTE majorVer = 2)
        {
            // 初始化WS2_32.dll
            WSADATA wsaData;
            WORD sockVersion = MAKEWORD(minorVer, majorVer);
            if (::WSAStartup(sockVersion, &wsaData) != 0)
            {
                exit(0);
            }
        }
    
        /*CInitSock 的析构器*/
        ~CInitSock()
        {
            ::WSACleanup();
        }
    };
    

    主函数

    Ping 的编写步骤如下:

    1. 创建协议类型为 IPPROTO_ICMP 的原始套接字,设置套接字的属性;
    2. 创建并初始化 ICMP 封包;
    3. 调用 sendto 函数向远程主机发送ICMP请求;
    4. 调用 recvfrom 函数接收ICMP响应。
    int main()
    {
        // 目的IP地址,即要Ping的IP地址
        char szDestIp[] = "36.152.44.96";
    
        // 创建原始套节字
        SOCKET sRaw = ::socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    
        // 设置目的地址
        SOCKADDR_IN dest;
        dest.sin_family = AF_INET;
        dest.sin_port = htons(0);
        dest.sin_addr.S_un.S_addr = inet_addr(szDestIp);
    
        // 创建ICMP封包
        char buff[sizeof(ICMP_HDR) + 32];
        ICMP_HDR* pIcmp = (ICMP_HDR*)buff;
        // 填写ICMP封包数据
        pIcmp->icmp_type = 8;   // 请求一个ICMP回显
        pIcmp->icmp_code = 0;
        pIcmp->icmp_id = (USHORT)::GetCurrentProcessId();
        pIcmp->icmp_checksum = 0;
        pIcmp->icmp_sequence = 0;
        // 填充数据部分,可以为任意
        memset(&buff[sizeof(ICMP_HDR)], 'E', 32);
    
        // 开始发送和接收ICMP封包
        USHORT  nSeq = 0;
        char recvBuf[1024];
        SOCKADDR_IN from;
        int nLen = sizeof(from);
        while (TRUE)
        {
            static int nCount = 0;
            int nRet;
            if (nCount++ == 4) {
                break;
            }
            pIcmp->icmp_checksum = 0;
            pIcmp->icmp_timestamp = ::GetTickCount();
            pIcmp->icmp_sequence = nSeq++;
            pIcmp->icmp_checksum = checksum((USHORT*)buff, sizeof(ICMP_HDR) + 32);
            nRet = ::sendto(sRaw, buff, sizeof(ICMP_HDR) + 32, 0, (SOCKADDR*)&dest, sizeof(dest));
            if (nRet == SOCKET_ERROR)
            {
                cout << " sendto() failed: " << ::WSAGetLastError() << endl;
                return -1;
            }
            nRet = ::recvfrom(sRaw, recvBuf, 1024, 0, (sockaddr*)&from, &nLen);
            if (nRet == SOCKET_ERROR)
            {
                if (::WSAGetLastError() == WSAETIMEDOUT)
                {
                    cout << " timed out" << endl;
                    continue;
                }
                cout << " recvfrom() failed: " << ::WSAGetLastError() << endl;
                return -1;
            }
    
            // 下面开始解析接收到的ICMP封包
            int nTick = ::GetTickCount();
            if (nRet < sizeof(IPHeader) + sizeof(ICMP_HDR))
            {
                cout << " Too few bytes from %s" << ::inet_ntoa(from.sin_addr) << endl;
            }
            // 接收到的数据中包含IP头,IP头大小为20个字节,所以加20得到ICMP头
            ICMP_HDR* pRecvIcmp = (ICMP_HDR*)(recvBuf + sizeof(IPHeader));
            if (pRecvIcmp->icmp_type != 0)   // 回显
            {
                cout << " nonecho type " << pRecvIcmp->icmp_type << " recvd" << endl;
                return -1;
            }
    
            if (pRecvIcmp->icmp_id != ::GetCurrentProcessId())
            {
                cout << " someone else's packet!" << endl;
                return -1;
            }
    
            cout << nRet << " bytes from " << inet_ntoa(from.sin_addr) 
                << " icmp_seq = " << pRecvIcmp->icmp_sequence << "." 
                << " time: " << nTick - pRecvIcmp->icmp_timestamp << "ms" << endl;
    
            ::Sleep(1000);
        }
    
        return 0;
    }
    

    运行效果

    参考资料

    《Windows 网络与通信编程》,陈香凝 王烨阳 陈婷婷 张铮 编著,人民邮电出版社

  • 相关阅读:
    前端攻城狮学习笔记九:让你彻底弄清offset
    JavaScript中Element与Node的区别,children与childNodes的区别
    JavaScript代码优化实战之一:缓存变量,关键字过滤
    【转】纯CSS画的基本图形(矩形、圆形、三角形、多边形、爱心、八卦等),NB么
    包装对象——JavaScript中原始类型拥有属性的原因
    关于两个容积不同的瓶子中装水可以得到哪些精确值的问题的算法
    JavaScript中判断鼠标按键(event.button)
    累了休息一会儿吧——分享一个JavaScript版扫雷游戏
    用CSS让未知高度内容垂直方向居中
    空间换时间,把递归的时间复杂度降低到O(2n)
  • 原文地址:https://www.cnblogs.com/linfangnan/p/15729663.html
Copyright © 2011-2022 走看看