如何写一个简单的ping程序

今天翻硬盘时,发现自己很久之前写的ping程序。突然想不如把ping程序放上来做一下也算让之前自己写的东西重见天日,当时还是花了挺多时间来写,感触良多。

首先呢,ping用到的协议是网络层的ICMP协议,发送/接收的是ICMP报文,最终的形式还是以一个IP报文在网络中传送。
下面先定义一下IP头和ICMP协议的相关数据结构。

IP头

                       IP报文格式
0            8           16    19                   31
+------------+------------+-------------------------+
| ver + hlen |  服务类型  |          总长度         |
+------------+------------+----+--------------------+
|           标识位        |flag|    分片偏移(13位)  |
+------------+------------+----+--------------------+
|   生存时间 | 高层协议号 |        首部校验和       |
+------------+------------+-------------------------+
|                   源 IP 地址                      |
+---------------------------------------------------+
|                  目的 IP 地址                     |
+---------------------------------------------------+

数据结构,使用了位域来兼容协议格式,最终编译的时候使用1字节对齐

struct IPhead
{
    //这里使用了C语言的位域,也就是说像version变量它的大小在内存中是占4bit,而不是8bit
    uint8_t     version : 4; //IP协议版本
    uint8_t     headLength : 4;//首部长度
    uint8_t     serverce;//区分服务
    uint16_t    totalLength;//总长度
    uint16_t    flagbit;//标识
    uint16_t    flag : 3;//标志
    uint16_t    fragmentOffset : 13;//片偏移
    char        timetoLive;//生存时间(跳数)
    uint8_t     protocol;//使用协议
    uint16_t    headcheckSum;//首部校验和
    uint32_t    srcIPadd;//源IP
    uint32_t    dstIPadd;//目的IP
    //可选项和填充我就不定义了
};

ICMP协议相关:

ICMP 请求报文
+--------+--------+---------------+
|  类型  |  代码   |      描述    |
+--------+--------+---------------+
|    8   |    0   |    回显请求   |
+--------+--------+---------------+
|   10   |    0   |   路由器请求  |
+--------+--------+---------------+
|   13   |    0   |   时间戳请求  |
+--------+--------+---------------+
|   15   |    0   | 信息请求(废弃)|
+--------+--------+---------------+
|   17   |    0   | 地址掩码请求  |
+--------+--------+---------------+

ICMP 应答报文
+--------+--------+---------------+
|  类型  |  代码  |      描述     |
+--------+--------+---------------+
|    0   |    0   |    回显回答   |
+--------+--------+---------------+
|    9   |    0   |   路由器回答  |
+--------+--------+---------------+
|   14   |    0   |   时间戳回答  |
+--------+--------+---------------+
|   16   |    0   | 信息回答(废弃)|
+--------+--------+---------------+
|   18   |    0   | 地址掩码回答  |
+--------+--------+---------------+

ICMP协议请求/回答报文格式
0        8       16                32
+--------+--------+-----------------+
|  类型   |  代码   |     校验和    |
+--------+--------+-----------------+
|      标识符      |      序号      |
+-----------------+-----------------+
|             回显数据              |
+-----------------+-----------------+

ICMP协议我只定义了请求/回答功能的报文格式。因为不同的类型和代码报文的内容组成不一样,ICMP分请求报文,回答报文,差错报文。差错报文我没写出来,因为用不着

ICMP相关数据结构

//ICMP头
struct ICMPhead
{
    uint8_t type;//类型
    uint8_t code;//代码
    uint16_t checkSum;//校验和
    uint16_t ident;//进程标识符
    uint16_t seqNum;//序号
};
//ICMP回显请求报文(发送用)
struct ICMPrecvReq
{
    ICMPhead icmphead;//头部
    uint32_t timeStamp;//时间戳
    char     data[32];//数据
};
//ICMP应答报文(接收用)
struct ICMPansReply
{
    IPhead iphead;//IP头
    ICMPrecvReq icmpanswer;//ICMP报文
    char data[1024];//应答报文携带的数据缓冲区
};

需要实现的函数也不多,只需要6个就能够满足基本的ping程序用了

//计算校验和,参数1:协议头的指针,需要转换成空指针。参数2:计算的协议类型。返回校验和
uint16_t getCheckSum(void* protocol,char* type);
//ping程序,参数1:为目的IP地址。返回是否发送成功.参数2:只能写-t,或者不写
bool ping(const char* dstIPaddr,const char* param='\0');
//发送ICMP请求回答报文,参数1:套接字.参数2:目的地址.参数3:第num次发送
bool sendICMPReq(SOCKET &mysocket, sockaddr_in &dstAddr, unsigned int num);
//读取ICMP应答报文,参数1:套接字.参数2:接收方地址.参数3:跳数
uint32_t readICMPanswer(SOCKET &mysocket, sockaddr_in &srcAddr, char &TTL);
//等待socket是否由数据需要读取
int waitForSocket(SOCKET &mysocket);
//执行一次ping操作,参数1:套接字.参数2:源地址.参数3:目的地址。参数4序号ping报文序号
void doPing(SOCKET &mysocket,sockaddr_in &srcAddr,sockaddr_in &dstAddr,int num);
//判断参数是否为空,空的话返回一个NULL指针,否侧提取参数存入到param中,同时返回
char* isParamEmpty(char *buffer, char *param);
//捕获终止信号函数
void get_ctrl_stop(int signal);

本程序实在windows平台下实现的额,使用winsock的话需要添加一个预处理命令
#pragma comment(lib,"ws2_32.lib")来使能winsock的lib的使用
接下来就是正题了,ICMP报文不经过TCP或者UDP的封装,而我们有知道sock层的API是面向用户(程序员)的,一般的sock是当用户读取时,系统会帮我们摘除IP头的信息,这样的话,我们就拿不到传输层以下的协议的相关信息了,所以我们不能使用普通的sock来编写ping程序,而要用原始套接字SOCK_RAW,使用原始套接字的话,当我们想sock请求数据的时候,系统不会对sock做过多处理,我们最低可以拿到数据链路层的数据,因此在网络层的IP报头的数据也可以拿到了。

整个ping程序的基本流程如下:
创建RAW套接字->接收待ping地址和参数->提取参数->根据参数指向相应的ping(普通的ping4次,还是无限循环ping)->向待ping地址发送一个ICMP回显请求报文->接收回复报文->输出相应信息。当中其实还有一个处理无限ping中捕获Ctrl+C退出命令的信号函数,那个其实并不影响大流程就不在流程上说了。

因为也就8个API就可以实现ping程序了,所以我打算一个一个API的放出来,具体的工程代码就放在github上,有需要的人自己拿吧。

参数检查和提取

char *isParamEmpty(char *buffer, char *param)
{
    char *temp = NULL;
    temp = buffer;
    while (*temp != '\0')
    {
        if (*temp == ' ')
        {
            *temp = '\0';
            param = ++temp;
        }
        temp++;
    }
    return param;
}

计算校验和

uint16_t getCheckSum(void * protocol, char * type)
{
    uint32_t checkSum = 0;
    uint16_t* word = (uint16_t*)protocol;
    uint32_t size = 0;
    if (type == "ICMP") {//计算有多少个字节
        size = (sizeof(ICMPrecvReq));
    }
    while (size >1)//用32位变量来存是因为要存储16位数相加可能发生的溢出情况,将溢出的最高位最后加到16位的最低位上
    {
        checkSum += *word++;
        size -=2;
    }
    if (size == 1) 
    {
        checkSum += *(uint8_t*)word;
    }
    //二进制反码求和运算,先取反在相加和先相加在取反的结果是一样的,所以先全部相加在取反
    //计算加上溢出后的结果
    while (checkSum >> 16) checkSum = (checkSum >> 16) + (checkSum & 0xffff);
    //取反
    return (~checkSum);
}

ping流程选择

bool ping(const char * dstIPaddr,const char* param)
{
    SOCKET      rawSocket;//socket
    sockaddr_in srcAddr;//socket源地址
    sockaddr_in dstAddr;//socket目的地址
    int         Ret;//捕获状态值
    char        TTL = '0';//跳数

    //生成一个套接字
    //TCP/IP协议族,RAW模式,ICMP协议
    //RAW创建的是一个原始套接字,最低可以访问到数据链路层的数据,也就是说在网络层的IP头的数据也可以拿到了。
    rawSocket = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    //设置目标IP地址
    dstAddr.sin_addr.S_un.S_addr = inet_addr(dstIPaddr);
    //端口
    dstAddr.sin_port = htons(0);
    //协议族
    dstAddr.sin_family = AF_INET;

    //提示信息
    std::cout << "正在 Ping " << inet_ntoa(dstAddr.sin_addr) << " 具有 " << sizeof(ICMPrecvReq::data) << " 字节的数据:" << std::endl;
    if(!strcmp(param,"-t"))
    {
        uint32_t i=0;

        while (keepping)
        {
            //无限执行ping操作
            doPing(rawSocket, srcAddr, dstAddr,i);
            //睡眠1ms
            Sleep(1000);
            i++;
        }
        keepping = 1;
    }
    else if(!strcmp(param,"no_param"))
    {   //执行4次ping
        for (int i = 0; i < 4; i++)
        {
            doPing(rawSocket, srcAddr, dstAddr, i);
            Sleep(1000);
         }
    }

    Ret = closesocket(rawSocket);
    if (Ret == SOCKET_ERROR)
    {
        std::cerr << "socket关闭时发生错误:" << WSAGetLastError() << std::endl;
    }

    return true;
}

执行一次ping

void doPing(SOCKET & mysocket, sockaddr_in & srcAddr, sockaddr_in & dstAddr, int num)
{
    uint32_t    timeSent;//发送时的时间
    uint32_t    timeElapsed;//延迟时间
    char        TTL;//跳数
    //发送ICMP回显请求
    sendICMPReq(mysocket, dstAddr, num);
    //等待数据
    int Ret = waitForSocket(mysocket);
    if (Ret == SOCKET_ERROR)
    {
        std::cerr << "socket发生错误:" << WSAGetLastError() << std::endl;
        return;
    }
    if (!Ret)
    {
        std::cout << "请求超时:" << std::endl;
        return;
    }
    timeSent = readICMPanswer(mysocket, srcAddr, TTL);
    if (timeSent != -1) {
        timeElapsed = GetTickCount() - timeSent;
        //输出信息,注意TTL值是ASCII码,要进行转换
        std::cout << "来自 " << inet_ntoa(srcAddr.sin_addr) << " 的回复: 字节= " << sizeof(ICMPrecvReq::data) << " 时间= " << timeElapsed << "ms TTL= " << fabs((int)TTL) << std::endl;
    }
    else {
        std::cout << "请求超时" << std::endl;
    }
}

发送ICMP报文

bool sendICMPReq(SOCKET &mysocket,sockaddr_in &dstAddr,unsigned int num)
{
    //创建ICMP请求回显报文
    //设置回显请求
    ICMPrecvReq myIcmp;//ICMP请求报文
    myIcmp.icmphead.type = 8;
    myIcmp.icmphead.code = 0;
    //设置初始检验和为0
    myIcmp.icmphead.checkSum = 0;
    //获得一个进程标识
    myIcmp.icmphead.ident = (uint16_t)GetCurrentProcessId();
    //设置当前序号为0
    myIcmp.icmphead.seqNum = ++num;
    //保存发送时间
    myIcmp.timeStamp = GetTickCount();
    //计算并且保存校验和
    myIcmp.icmphead.checkSum = getCheckSum((void*)&myIcmp, "ICMP");
    //发送报文
    int Ret = sendto(mysocket, (char*)&myIcmp, sizeof(ICMPrecvReq), 0, (sockaddr*)&dstAddr, sizeof(sockaddr_in));

    if (Ret == SOCKET_ERROR)
    {
        std::cerr << "socket 发送错误:" <<WSAGetLastError()<< std::endl;
        return false;
    }
    return true;
}

等待ICMP报文的回复

int waitForSocket(SOCKET &mysocket)
{
    //5S 等待套接字是否由数据
    timeval timeOut;
    fd_set  readfd;
    readfd.fd_count = 1;
    readfd.fd_array[0] = mysocket;
    timeOut.tv_sec = 5;
    timeOut.tv_usec = 0;
    return (select(1, &readfd, NULL, NULL, &timeOut));
}

读取回复的ICMP报文

uint32_t readICMPanswer(SOCKET &mysocket, sockaddr_in &srcAddr, char &TTL)
{
    ICMPansReply icmpReply;//接收应答报文
    int addrLen = sizeof(sockaddr_in);
    //接收应答
    int Ret = recvfrom(mysocket, (char*)&icmpReply, sizeof(ICMPansReply), 0, (sockaddr*)&srcAddr, &addrLen);
    if (Ret == SOCKET_ERROR)
    {
        std::cerr << "socket 接收错误:" << WSAGetLastError() << std::endl;
        return false;
    }
    //读取校验并重新计算对比
    uint16_t checkSum = icmpReply.icmpanswer.icmphead.checkSum;
    //因为发出去的时候计算的校验和是0
    icmpReply.icmpanswer.icmphead.checkSum = 0;
    //重新计算
    if (checkSum== getCheckSum((void*)&icmpReply.icmpanswer, "ICMP")) {
        //获取TTL值
        TTL = icmpReply.iphead.timetoLive;
        return icmpReply.icmpanswer.timeStamp;
    }

    return -1;
}

处理退出信号函数

static volatile int keepping = 1;
//捕获终止信号函数,专门处理无限ping时的操作
void get_ctrl_stop(int signal)
{
    if (signal == SIGINT)
    {
        keepping = 0;
    }
}

如果要全部代码的话,可以在下面的链接里拿
ping程序完整工程

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值