一、实验目的
了解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;
}