MENU

Tracert 与 Ping程序设计与实现(一)

December 20, 2019 • 阅读: 2262 • 笔记&折腾



目录:

知识储备

此题为我计算机网络期末课程设计的第二题。由于我对GUI的掌握度不高,先暂时不做图形界面。

关于ICMP报文和IP报文的详解请阅读教材。

以下为需要强调学习的知识:

ICMP数据报包括ICMP数据头及ICMP数据部分,IP数据报包括IP数据头及IP数据部分,ICMP数据报将会作为IP数据报的数据部分。

ICMP接收的报文种类中其中有两种为:回显应答报文及超时差错报文。利用这两种报文可判断出路由途中的某主机是可达的还是超时的。

Tracert的重点是对接收到的数据进行解码,与发送的数据进行对比,判断是否为当前进程发送的数据,以及是否为目的主机返回的数据。

代码

#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <string>
#include <cstring>
#include <iostream>
#include <stdlib.h>
#include <stdio.h>
using namespace std;
#pragma comment(lib, "Ws2_32.lib")

//IP 报头
typedef struct
{
    unsigned char hdr_len:4; //4 位头部长度
    unsigned char version:4; //4 位版本号
    unsigned char tos; //8 位服务类型
    unsigned short total_len; //16 位总长度
    unsigned short identifier; //16 位标识符
    unsigned short frag_and_flags; //3 位标志加 13 位片偏移
    unsigned char ttl; //8 位生存时间
    unsigned char protocol; //8 位上层协议号
    unsigned short checksum; //16 位校验和
    unsigned long sourceIP; //32 位源 IP 地址
    unsigned long destIP; //32 位目的 IP 地址
} IP_HEADER;

//ICMP 报头
typedef struct
{
    BYTE type; //8 位类型字段
    BYTE code; //8 位代码字段
    USHORT cksum; //16 位校验和
    USHORT id; //16 位标识符
    USHORT seq; //16 位序列号
} ICMP_HEADER;

//报文解码结构
typedef struct
{
    USHORT usSeqNo; //序列号
    DWORD dwRoundTripTime; //往返时间
    in_addr dwIPaddr; //返回报文的 IP 地址
} DECODE_RESULT;

//计算网际校验和函数
USHORT checksum(USHORT *pBuf,int iSize)
{
    unsigned long cksum=0;
    while(iSize>1)
    {
        cksum+=*pBuf++;
        iSize-=sizeof(USHORT);
    }
    if(iSize)
    {
        cksum+=*(UCHAR *)pBuf;
    }
    cksum=(cksum>>16)+(cksum&0xffff);
    cksum+=(cksum>>16);
    return (USHORT)(~cksum);
}

//对数据包进行解码,结果封装到Decode当中
/*
buf:接收到的buf,字符串
ipacketsize:接收到的数据长度
解析结果封装到DecodeResult
ICMP类型, echo_reply是一个常量,放到全局也行
ICMP类型
*/
BOOL DecodeIcmpResponse(char * pBuf,int iPacketSize,DECODE_RESULT &DecodeResult,BYTE ICMP_ECHO_REPLY,BYTE ICMP_TIMEOUT)
{
    //检查数据报大小的合法性
    IP_HEADER* pIpHdr = (IP_HEADER*)pBuf; //将接收到的数据转换成ip抱头
    int iIpHdrLen = pIpHdr->hdr_len * 4;//计算报文长度
    //如果接收的报文长度小于IP报文长度,就退出
    if (iPacketSize < (int)(iIpHdrLen+sizeof(ICMP_HEADER)))
        return FALSE;
    //根据 ICMP 报文类型提取 ID 字段和序列号字段
    ICMP_HEADER *pIcmpHdr=(ICMP_HEADER *)(pBuf+iIpHdrLen); //根据字符串结构体指针自动匹配长度
    USHORT usID,usSquNo;
    //如果返回的是回显应答报文
    if(pIcmpHdr->type==ICMP_ECHO_REPLY) //ICMP 回显应答报文
    {
        usID=pIcmpHdr->id; //报文 ID
        usSquNo=pIcmpHdr->seq; //报文序列号
    }
    //如果返回的是超时差错报文
    else if(pIcmpHdr->type==ICMP_TIMEOUT)//ICMP 超时差错报文
    {
        char * pInnerIpHdr=pBuf+iIpHdrLen+sizeof(ICMP_HEADER); //载荷中的 IP 头
        int iInnerIPHdrLen=((IP_HEADER *)pInnerIpHdr)->hdr_len*4; //载荷中的 IP 头长
        ICMP_HEADER * pInnerIcmpHdr=(ICMP_HEADER *)(pInnerIpHdr+iInnerIPHdrLen);//载荷中的 ICMP 头
        usID=pInnerIcmpHdr->id; //报文 ID
        usSquNo=pInnerIcmpHdr->seq; //序列号
    }
    //如果都不是,则退出
    else
    {
        return false;
    }
    //检查 ID 和序列号以确定收到期待数据报
    //检查ID是否为当前进程ID,获取的序列号是否为指定序列号
    if(usID!=(USHORT)GetCurrentProcessId()||usSquNo!=DecodeResult.usSeqNo)
    {
        return false;
    }
    //记录 IP 地址并计算往返时间
    DecodeResult.dwIPaddr.s_addr=pIpHdr->sourceIP;
    DecodeResult.dwRoundTripTime=GetTickCount()-DecodeResult.dwRoundTripTime;
    //处理正确收到的 ICMP 数据报
    if (pIcmpHdr->type == ICMP_ECHO_REPLY ||pIcmpHdr->type == ICMP_TIMEOUT)
    {
        //输出往返时间信息
        if(DecodeResult.dwRoundTripTime)
            cout<<" "<<DecodeResult.dwRoundTripTime<<"ms"<<flush;
        else
            cout<<" "<<"<1ms"<<flush;
    }
    return true;
}

//跳转到下一个IP地址
char *toNextIp(char *tempIp)
{
    char temp[4][4];
    int ip1=0;
    int ip2=0;
    for(int i=0; i< strlen(tempIp); i++)
    {
        if(tempIp[i]=='.')
        {
            temp[ip1][ip2] = '\0';
            ip1++;
            ip2=0;
            continue;
        }
        temp[ip1][ip2] = tempIp[i];
        ip2++;
    }
    temp[ip1][ip2] ='\0';
    if(atoi(temp[3])==254)
    {
        strcpy(temp[3],"1");
    }
    else
    {
        int t1 = atoi(temp[3])+1;
        itoa(t1,temp[3],10);
    }
    char tip[17];
    strcpy(tip,temp[0]);
    for(int i=1; i<4; i++)
    {
        strcat(tip,".");
        strcat(tip,temp[i]);
    }
    //puts(tip);
    return tip;
}

int main()
{
    //初始化 Windows sockets 网络环境
    WSADATA wsa;
    WSAStartup(MAKEWORD(2,2),&wsa);
    char begin_ip[17];
    char IpAddress[17];
    char end_ip[17];
    cout<<"请输入起始IP地址 :";
    cin>>begin_ip;
    cout<<"请输入终止IP地址:";
    cin>>end_ip;
    strcpy(IpAddress,begin_ip);
    while(strcmp(IpAddress,end_ip)!=0)
    {
        bool is_reach = false;
        //得到 IP 地址
        u_long ulDestIP=inet_addr(IpAddress);
        //转换不成功时按域名解析
        if(ulDestIP==INADDR_NONE)
        {
            hostent * pHostent=gethostbyname(IpAddress);
            if(pHostent)
            {
                ulDestIP=(*(in_addr*)pHostent->h_addr).s_addr;
            }
            else
            {
                cout<<"输入的 IP 地址或域名无效!"<<endl;
                WSACleanup();
                return 0;
            }
        }
        cout<<"\nTracing route to "<<IpAddress<<" with a maximum of 30 hops.\n"<<endl;
        //填充目地端 socket 地址
        sockaddr_in destSockAddr;
        ZeroMemory(&destSockAddr,sizeof(sockaddr_in));
        destSockAddr.sin_family=AF_INET;//协议IPV4
        destSockAddr.sin_addr.s_addr=ulDestIP;//目的IP地址
        //创建原始套接字
        SOCKET sockRaw=WSASocket(AF_INET,SOCK_RAW,IPPROTO_ICMP,NULL,0,WSA_FLAG_OVERLAPPED);
        //超时时间
        int iTimeout=3000;
        //接收超时
        setsockopt(sockRaw,SOL_SOCKET,SO_RCVTIMEO,(char *)&iTimeout,sizeof(iTimeout));
        //发送超时
        setsockopt(sockRaw,SOL_SOCKET,SO_SNDTIMEO,(char *)&iTimeout,sizeof(iTimeout));

        //构造 ICMP 回显请求消息,并以 TTL 递增的顺序发送报文
        //ICMP 类型字段
        const BYTE ICMP_ECHO_REQUEST=8; //请求回显
        const BYTE ICMP_ECHO_REPLY=0; //回显应答
        const BYTE ICMP_TIMEOUT=11; //传输超时

        //其他常量定义
        const int DEF_ICMP_DATA_SIZE=32; //ICMP 报文默认数据字段长度
        const int MAX_ICMP_PACKET_SIZE=1024;//ICMP 报文最大长度(包括报头)
        const DWORD DEF_ICMP_TIMEOUT=3000; //回显应答超时时间
        const int DEF_MAX_HOP=30; //最大跳站数

        //填充 ICMP 报文中每次发送时不变的字段
        //发送缓冲区
        char IcmpSendBuf[sizeof(ICMP_HEADER)+DEF_ICMP_DATA_SIZE];//发送缓冲区
        memset(IcmpSendBuf, 0, sizeof(IcmpSendBuf)); //初始化发送缓冲区,用0填充
        //接收缓冲区
        char IcmpRecvBuf[MAX_ICMP_PACKET_SIZE]; //接收缓冲区
        memset(IcmpRecvBuf, 0, sizeof(IcmpRecvBuf)); //初始化接收缓冲区,用0填充

        ICMP_HEADER * pIcmpHeader=(ICMP_HEADER*)IcmpSendBuf; //发送报头
        pIcmpHeader->type=ICMP_ECHO_REQUEST; //类型为请求回显,8
        pIcmpHeader->code=0; //代码字段为 0
        pIcmpHeader->id=(USHORT)GetCurrentProcessId(); //ID 字段为当前进程号

        memset(IcmpSendBuf+sizeof(ICMP_HEADER),'E',DEF_ICMP_DATA_SIZE);//数据字段,用'E'填充
        USHORT usSeqNo=0; //ICMP 报文序列号

        int iTTL=1; //TTL 初始值为 1
        BOOL bReachDestHost=FALSE; //循环退出标志
        int iMaxHot=DEF_MAX_HOP; //循环的最大次数 30
        DECODE_RESULT DecodeResult; //传递给报文解码函数的结构化参数
        while(!bReachDestHost&&iMaxHot--)
        {
            //设置 IP 报头的 TTL 字段
            setsockopt(sockRaw,IPPROTO_IP,IP_TTL,(char *)&iTTL,sizeof(iTTL));
            cout<<iTTL<<flush; //输出当前序号
            //填充 ICMP 报文中每次发送变化的字段
            ((ICMP_HEADER *)IcmpSendBuf)->cksum=0; //校验和先置为 0
            ((ICMP_HEADER *)IcmpSendBuf)->seq=htons(usSeqNo++); //填充序列号
            ((ICMP_HEADER *)IcmpSendBuf)->cksum=checksum((USHORT *)IcmpSendBuf,sizeof(ICMP_HEADER)+DEF_ICMP_DATA_SIZE); //计算校验和
            //记录序列号和当前时间
            DecodeResult.usSeqNo=((ICMP_HEADER*)IcmpSendBuf)->seq; //当前序号
            DecodeResult.dwRoundTripTime=GetTickCount(); //当前时间
            //发送 TCP 回显请求信息
            sendto(sockRaw,IcmpSendBuf,sizeof(IcmpSendBuf),0,(sockaddr*)&destSockAddr,sizeof(destSockAddr));
            //接收 ICMP 差错报文并进行解析处理
            sockaddr_in from; //对端 socket 地址
            int iFromLen=sizeof(from); //地址结构大小
            int iReadDataLen; //接收数据长度
            while(1)
            {
                //接收数据
                iReadDataLen=recvfrom(sockRaw,IcmpRecvBuf,MAX_ICMP_PACKET_SIZE,0,(sockaddr*)&from,&iFromLen);
                if(iReadDataLen!=SOCKET_ERROR)//有数据到达
                {
                    //对数据包进行解码
                    if(DecodeIcmpResponse(IcmpRecvBuf,iReadDataLen,DecodeResult,ICMP_ECHO_REPLY,ICMP_TIMEOUT))
                    {
                        //到达目的地,退出循环
                        if(DecodeResult.dwIPaddr.s_addr==destSockAddr.sin_addr.s_addr)bReachDestHost=true;
                        //输出 IP 地址
                        cout<<'\t'<<inet_ntoa(DecodeResult.dwIPaddr)<<endl;
                        if(DecodeResult.dwIPaddr.s_addr == destSockAddr.sin_addr.s_addr)
                        {
                            //cout << inet_ntoa(destSockAddr.sin_addr)<<"可达!"<<endl;
                            is_reach = true;
                        }
                        break;
                    }
                }
                else if(WSAGetLastError()==WSAETIMEDOUT) //接收超时,输出*号
                {
                    cout<<" *"<<'\t'<<"Request timed out."<<endl;
                    break;
                }
                else
                {
                    break;
                }
            }
            iTTL++; //递增 TTL 值
        }
        cout << "主机号:"<<IpAddress << (is_reach? " 在线":" 不在线") <<endl;
        strcpy(IpAddress,toNextIp(IpAddress));
    }
}

运行演示

注意:范围测试只适用于局域网。输入ip范围为 *.*.*.1~*.*.*.254 。 255为广播地址。

返回结果为对每个地址的tracert路径,及目的IP地址是否在线。

Last Modified: February 6, 2021