计算机网络——Tracert与Ping程序设计与实现

一、实验目的

了解Tracert程序的实现原理,并调试通过。

二、总体设计

1. 基本原理

tracert(跟踪路由)是路由跟踪实用程序,用于确定IP数据包访问目标所采取的路径。tracert 有一个固定的时间等待响应(ICMP TTL到期消息)。如果这个时间过了,它将打印出一系列的*号表明:在这个路径上,这个设备不能在给定的时间内发出ICMP TTL到期消息的响应。然后,Tracert给TTL记数器加1,继续进行。

2. 设计步骤

(1)加载套接字,创建套接字库;
使用Socket的程序在使用Socket之前必须调用WSAStartup函数,以后应用程序就可以调用所请求的Socket库中的其他Socket函数了。
(2)用inet_addr()将输入的点分十进制的IP地址转换为无符号长整型数,转换不成功时,按域名解析得到IP地址;
gethostbyname()是查找主机名最基本的函数,如果调用成功,就返回一个指向hosten结构的指针,该结构中含有对应于给定主机名的主机名字和地址信息,用来承接域名解析的结构。
(3)设置发送接收超时时间,即请求超时,设置接收、发送超时的套接字;
(4)构造ICMP回显请求消息,并以TTL递增顺序发送报文,填充ICMP报文中每次发送时不变的字段,构造ICMP头;
(5)设置IP报头的TTL字段,填充ICMP报文中每次发送变化的字段,记录序列号和当前时间;
(6)指定对方信息,发送TCP回显请求信息;
sendto()函数利用数据表的方式进行数据传输,指定哪个socket发送给对方
(7)接收ICMP差错报文并进行解析:如果有数据到达,解析数据包,如果到达目的地址,输出IP地址;如果没有数据到达,输出接收超时,递增TTL值,TTL增为最大时,若还没有到达目的地址,退出循环,输出目的地址不在线;
recvform()利用数据报方式进行数据传输,当recvfrom()返回时,(sockaddr*)&from包含实际存入from中的数据字节数。Recvfrom函数返回接收到的字节数或当出现错误时返回-1,并置相应的errno。
(8)重复(2)-(7),实现查找一个范围内的IP地址。

三、详细设计

1. 程序流程图

在这里插入图片描述

2. 实验代码

#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdlib.h>
#include <sstream>
using namespace std;
#pragma comment(lib, "Ws2_32.lib")
const int ipAddressSize = 14;
//int count11=0;
//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 位片偏移: 标志:MF 1是否还有分配 0 没有分片了
    //                         DF 0 可以分片
    // 片偏移:分片后的相对于原来的偏移
    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 报头,一共八个字节,前四个字节为:类型(1字节)、代码(1字节)和检验和(2字节)。后四个字节取决于类型
typedef struct
{
    BYTE type; //8 位类型字段:标识ICMP的作用
    BYTE code; //8 位代码字段
    USHORT cksum; //16 位校验和
    USHORT id; //16 位标识符
    USHORT seq; //16 位序列号
} ICMP_HEADER;

//报文解码结构
//接收到的数据缓存是字符数组char bufRev[],因此需要通过特定的解析(也就是拆成一段一段的)获取想要的信息
//把信息封装到结构体中,就比较方便的得到序列号、往返时间和目的IP了。
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);
}
//对数据包进行解码
// 1)接收到的Buf 2)接收到的数据长度 3)解析结果封装到Decode 4)ICMP回显类型 5)TIMEOUT时间
BOOL DecodeIcmpResponse2(char * pBuf, int iPacketSize, DECODE_RESULT &DecodeResult, BYTE
                         ICMP_ECHO_REPLY, BYTE ICMP_TIMEOUT)
{
    //查找数据报大小合法性
    //pBuf的首地址,就是IP报的首地址
    IP_HEADER *pIpHdr = (IP_HEADER*)pBuf;
    int iIpHdrLen = pIpHdr->hdr_len * 4;
    if(iPacketSize < (int)(iIpHdrLen + sizeof(ICMP_HEADER)))
        return FALSE;
    // 根据 ICMP 报文类型提取 ID 字段和序列号字段
    //ICMP字段包含在 IP数据段的起始位置,因此扣掉IP头,得到的就是ICMP头
    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超时差错报文
    {
        // 如果是TIMEOUT ,那么在ICMP数据包中,会夹带一个IP报(荷载IP)
        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 和序列号以确定收到期待数据报
    if (usID != (USHORT)GetCurrentProcessId() || usSquNo != DecodeResult.usSeqNo)
    {
        return false;
    }
    // 记录 IP 地址并计算往返时间
    DecodeResult.dwIPaddr.S_un.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;
}


char * findNextIp(char * nowIp);

int main()
{
    //初始化 Windows sockets 网络环境
    WSADATA wsa;//存储被WSAStartup函数调用后返回的Windows Sockets数据
    //使用Socket的程序在使用Socket之前必须调用WSAStartup函数,以后应用程序就可以调用所请求的Socket库中的其他Socket函数了
    WSAStartup(MAKEWORD(2, 2), &wsa);//进行相应的socket库绑定
    cout << "请输入你要查找的起始IP:" << endl;
    char IpAddressBeg[ipAddressSize]; // 255.255.255.255
    cin >> IpAddressBeg;
    cout << "请输入你要查找的终止IP:" << endl;
    char IpAddressEnd[ipAddressSize]; // 255.255.255.255
    cin >> IpAddressEnd;
    char nextIpAddress[17];
    strcpy(nextIpAddress, IpAddressBeg);
    while (strcmp(nextIpAddress, IpAddressEnd) != 0)
    {
        // 执行,单线程执行,实现后改成多线程
        //得到IP地址
        u_long ulDestIP = inet_addr(nextIpAddress);//inet_addr()的功能是将一个点分十进制的IP转换成一个无符号长整型数
        //转换不成功时按域名解析
        if (ulDestIP == INADDR_NONE)
        {
            //gethostbyname()是查找主机名最基本的函数
            //如果调用成功,就返回一个指向hosten结构的指针
            //该结构中含有对应于给定主机名的主机名字和地址信息,用来承接域名解析的结构
            hostent * pHostent = gethostbyname(nextIpAddress);
            if (pHostent)//调用成功
            {
                //得到IP地址
                //套了两层,IP和ICMP,ICMP是套在IP里面的
                //h_addr返回主机IP地址
                //in_addr返回报文的IP地址
                //sin_addr.s_addr指向IP地址
                ulDestIP = (*(in_addr*)pHostent->h_addr).s_addr;
            }
            else
            {
                cout << "输入的 IP 地址或域名无效!" << endl;
                WSACleanup();//解除与Socket库的绑定并且释放Socket库所占用的系统资源
                return 0;
            }
        }
        // 填充目的 sockaddr_in
        sockaddr_in destSockAddr;//sockaddr_in是Internet环境下套接字的地址形式
        //将指定的内存块清零,使用结构前清零,而不让结构体的成员数值具有不确定性,是一个好的编程习惯
        ZeroMemory(&destSockAddr, sizeof(sockaddr_in));
        destSockAddr.sin_family = AF_INET;//指代协议簇,在socket编程中只能是AF_INET
        destSockAddr.sin_addr.S_un.S_addr = ulDestIP;//按照网络字节顺序存储IP地址
        //创建原始套接字
        //WSASocket()的发送操作和接收操作都可以被重叠使用。接收函数可以被多次调用,发出接收缓冲区,准备接收到来的数据。发送函数也可以被多次调用,组成一个发送缓冲区队列
        //如无错误发生,返回新套接口的描述字,否则的话,返回INVALID_SOCKET
        //AF_INET为地址簇描述,SOCK_RAW为新套接口的类型描述,SOCK_RAW为原始套接字,可处理PING报文等
        //IPPROTO_ICMP为套接口使用的协议,为ICMP;NULL是一个指向PROTOCOL_INFO结构的指针,该结构定义所创建套接口的特性
        //0为套接口的描述字;WSA_FLAG_OVERLAPPED为套接口属性描述,WSA_FLAG_OVERLAPPED表示要使用重叠模型
        SOCKET sockRaw = WSASocket(AF_INET, SOCK_RAW, IPPROTO_ICMP, NULL, 0,
                                   WSA_FLAG_OVERLAPPED);
        // 设置发送接收超时时间,即请求超时
        //比如请求B站的一个视频,他超过一个时间没回我,我就认为超时了
        //超时时间是可能变化的,这个超时时间用来存储在不同的变量,它刚好在一个变量而已
        int iTimeout = 500;//如果没超过超时时间就会一直等着,超过超时时间就不等了
        //接收超时
        //sockRaw为将要被设置或者获取选项的套接字;SOL_SOCKET为在套接字级别上设置选项;SO_RCVTIMEO设置接收超时时间
        //(char*)&iTimeout指向存放选项值的缓冲区;sizeof(iTimeout)为缓冲区的长度
        setsockopt(sockRaw, SOL_SOCKET, SO_RCVTIMEO, (char *)&iTimeout, sizeof(iTimeout));
        //发送超时
        //sockRaw为将要被设置或者获取选项的套接字;SOL_SOCKET为在套接字级别上设置选项;SO_SNDTIMEO设置发送超时时间
        //(char*)&iTimeout指向存放选项值的缓冲区;sizeof(iTimeout)为缓冲区的长度
        setsockopt(sockRaw, SOL_SOCKET, SO_SNDTIMEO, (char *)&iTimeout, sizeof(iTimeout));
        // 构造 ICMP 回显请求消息, 并以TTL 递增顺序发送报文
        // ICMP 类型字段
        //采用const修饰变量,功能是对变量声明为只读特性,并保护变量值以防被修改
        const BYTE ICMP_ECHO_REQUEST = 8;//请求回显
        const BYTE ICMP_ECHO_REPLY = 0;//回显应答
        //其他常量定义
        const int DEF_ICMP_DATA_SIZE = 32; // ICMP 报文默认数据字段长度
        const int MAX_ICMP_PACKET_SIZE = 1024;//ICMP 报文最大长度(加上报头)
        const DWORD DEF_ICMP_TIMEOUT = 500;// 回显应答超时时间
        const int DEF_MAX_HOP = 20; // 最大跳站数
        // 填充 ICMP 报文中每次发送时不变的字段
        char IcmpSendBuf[sizeof(ICMP_HEADER) + DEF_ICMP_DATA_SIZE];// 发送缓冲区
        memset(IcmpSendBuf, 0, sizeof(IcmpSendBuf));//初始化发送缓冲区
        char IcmpRecvBuf[MAX_ICMP_PACKET_SIZE]; // 接收缓冲区
        memset(IcmpRecvBuf, 0, sizeof(IcmpRecvBuf)); //初始化接收缓冲区
        // 构造ICMP头
        ICMP_HEADER * pIcmpHeader = (ICMP_HEADER*)IcmpSendBuf;
        pIcmpHeader->type = ICMP_ECHO_REQUEST; // 类型为请求回显
        pIcmpHeader->code = 0;//代码字段为0
        pIcmpHeader->id = (USHORT)GetCurrentProcessId();// ID字段为当前进程号
        memset(IcmpSendBuf + sizeof(ICMP_HEADER), 'E', DEF_ICMP_DATA_SIZE);//数据字段
        USHORT usSeqNo = 0; // ICMP 报文序列号
        int iTTL = 1; // TTL初始化值为1
        BOOL bReachDestHost = FALSE; // 循环退出标志
        int iMaxHot = DEF_MAX_HOP; // 最大循环数
        DECODE_RESULT DecodeResult;// 传递给报文解码函数的结构化参数
        //int count11=0;
        while (!bReachDestHost && iMaxHot--)
        {
            bReachDestHost = FALSE;
            // 设置 IP 报头的 TTL 字段
            //sockRaw为将要被设置或者获取选项的套接字;IPPROTO_IP为套接口使用的协议,为IP;IP_TTL为设置IP报头的TTL字段
            //(char*)&iTTL指向存放选项值的缓冲区;sizeof(iTTL)为缓冲区的长度
            setsockopt(sockRaw, IPPROTO_IP, IP_TTL, (char *)&iTTL, sizeof(iTTL));
            cout << iTTL << flush; // 输出当前序号,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()利用数据报的方式进行数据传输
            // 1)指定哪个Socket发给对方 2)发送的数据 3)flag 4)目的地址  5)目的地址的sockaddr_in结构
            sendto(sockRaw, IcmpSendBuf, sizeof(IcmpSendBuf), 0, (sockaddr*)&destSockAddr, sizeof(destSockAddr));

            //接收 ICMP 差错报文并进行解析
            sockaddr_in from; // 对端 socket地址,对方的
            int iFromLen = sizeof(from);//地址结构大小
            int iReadDataLen;// 接收数据长度

            // 接收正常的话,这个循环只会执行一次
            while (true)
            {
                //接收数据
                //recvfrom()利用数据报方式进行数据传输
                //当recvfrom()返回时,(sockaddr*)&from包含实际存入from中的数据字节数。
                //Recvfrom()函数返回接收到的字节数或当出现错误时返回-1,并置相应的errno。
                iReadDataLen = recvfrom(sockRaw, IcmpRecvBuf, MAX_ICMP_PACKET_SIZE, 0, (sockaddr*)&from, &
                                        iFromLen);

                if (iReadDataLen != SOCKET_ERROR) // 有数据到达
                {
                    //解析数据包
                    // 1)接收到的Buf 2)接收到的数据长度 3)解析结果封装到Decode 4)ICMP回显类型 5)TIMEOUT时间
                    if (DecodeIcmpResponse2(IcmpRecvBuf, iReadDataLen, DecodeResult, ICMP_ECHO_REPLY, DEF_ICMP_TIMEOUT))
                    {
                        // 到达目的地,退出循环
                        //返回报文的IP地址等于输入的IP地址
                        if (DecodeResult.dwIPaddr.S_un.S_addr == destSockAddr.sin_addr.S_un.S_addr)
                        {
                            bReachDestHost = true;
                            // 输出 IP 地址
                            //inet_ntoa()功能是将网络地址转换成“.”点隔的字符串格式。
                            cout << '\t' << inet_ntoa(DecodeResult.dwIPaddr) << endl;
                            strcpy(nextIpAddress, inet_ntoa(DecodeResult.dwIPaddr));
                            break;
                        }

                    }

                }
                //WSAGetLastError()当一特定的Sockets API函数指出一个错误已经发生,该函数就应调用来获得对应的错误代码。
                //WSAETIMEDOUT在尝试连接超时,而不建立连接。
                else if (WSAGetLastError() == WSAETIMEDOUT) //接收超时,输出*号
                {
                    cout << " *" << '\t' << "Request timed out." << endl;
                    break;
                }
                else
                {
                    break;
                }
            }
            iTTL++;//递增TTL值
        }

        cout << "查找: " << nextIpAddress << "结果为 ->" << (bReachDestHost ? "在线" : "不在线") << endl;
        //if nextIpAddress ==bReachDestHost;
        // 向下推
        strcpy(nextIpAddress, findNextIp(nextIpAddress));
    }
    return 0;
}

char * findNextIp(char * nowIp)
{
    char nextIpAddress[ipAddressSize];
    char z[4][4];
    int idxIp = 0, idxj = 0;
    for (int i = 0; i < strlen(nowIp); i++)
    {
        if (nowIp[i] == '.')
        {
            z[idxIp][idxj] = '\0';

            idxIp++;
            idxj = 0;

            continue;

        }
        z[idxIp][idxj++] = nowIp[i];
    }
    z[idxIp][idxj] = '\0';

    //for (int i = 0; i < 4; i++)
    //{
    //	puts(z[i]);
    //}
    //cout << endl;

    for (int i = 3; i >= 0; i--)
    {
        if (strcmp("254", z[i]) == 0)
        {
            strcpy(z[i], "1"); // 这里让ip 1-254
        }
        else
        {
            int x;
            x = atoi(z[i]) + 1;
            //stringstream ss;
            //ss << x;
            //string z[i] = ss.str();
            itoa(x,z[i],10); // 第三个参数是 int的进制

            break;
        }
    }


    char retIp[ipAddressSize];
    strcpy(retIp, z[0]);
    char c[2] = ".";
    for (int i = 1; i < 4; i++)
    {
        strcat(retIp, c);
        strcat(retIp, z[i]);
    }
    /*cout << retIp << endl;*/

    return retIp;
}

四、实验结果

在这里插入图片描述

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值