Tracert 与Ping 程序设计与实现
参照附录 2,了解 Tracert 程序的实现原理,并调试通过。然后参考 Tracert 程序和教材 4.4.2 节,编写一个 Ping 程序,并能测试本局域网的所有机器是否在线。
首先我们要明白tracert是什么,Tracert(跟踪路由)是路由跟踪实用程序,用于确定 IP 数据包访问目标所采取的路径。Tracert 命令用 IP 生存时间 (TTL) 字段和 ICMP 错误消息来确定从一个主机到网络上其他主机的路由。
Tracert原理:
通过向目标发送不同IP生存时间 (TTL) 值的“Internet控制消息协议 (ICMP)”回应数据包,Tracert诊断程序确定到目标所采取的路由。要求路径上的每个路由器在转发数据包之前至少将数据包上的 TTL 递减 1。数据包上的 TTL 减为 0 时,路由器应该将“ICMP 已超时”的消息发回源系统。
Tracert 先发送 TTL 为 1 的回应数据包,并在随后的每次发送过程将TTL递增 1,直到目标响应或 TTL 达到最大值,从而确定路由。通过检查中间路由器发回的“ICMP 已超时”的消息确定路由。某些路由器不经询问直接丢弃 TTL 过期的数据包,这在 Tracert 实用程序中看不到。
了解了tracert原理后我们可以得知,ping命令实际上就是tracert简化版本。我们需要向目标ip发送icmp报文,然后计算时间差。由于我是使用的Linux下编程,所以不能直接使用附录的程序代码。为此,我从ICMP的编码和解码写起。
TCMP报文示意图:
ICMP封包
icmp->icmp_type = ICMP_ECHO; //设置类型
icmp->icmp_code = 0;
icmp->icmp_cksum = 0; //设置初始校验和
icmp->icmp_id = pid; //设置当前id
icmp->icmp_seq = seq++; //设置序号
tvstart = (struct timeval*)icmp->icmp_data; //设置icmp时间戳
icmp->icmp_cksum = checksum((unsigned char*)icmp, DEFAULT_LEN + 8);
ICMP解包
参照上述封包操作进行提取,同时我们需要注意校验和是否一致。如果不一致,则丢弃当前数据包。
校验和算法
将数据以字(16位)为单位累加到一个双字中,如果数据长度为奇数,最后一个字节将被扩展到字,累加的结果是一个双字,最后将这个双字的高16位和低16位相加后取反,便得到了校验和。
while (len > 1) //sum
{
sum += *pbuf++;
len -= 2;
} //累加求和
if (len) sum += (unsigned char)pbuf;
sum = (sum >> 16) + (sum & 0xffff);
sum += (sum >> 16);
return (unsigned short)(~sum); //取反
如何实现扫描局域网?
为了实现扫描局域网,我们首先可以使用gethostbyname获取得到当前ip。通过合理的字符串拼接算法,我们可以修改得到ip的末尾值,遍历0-255即可。
在这期间有一个问题就是速度问题,如果采用之前的版本,那么美发送一个icmp报文我们都将等待目标主机回复。程序会陷入阻塞状态,用户体验不佳。因此,本程序采用了多线程操作,同时向多个主机收发ICMP报文。
for(int i = 1;i<=255;i++)
pthread_create(&threads[i], NULL, pingIp, NULL);
pthread_join(threads[i],NULL);
实验结果中展示的就是tractert运行的结果,它所完成的功能是根据输入的起始IP地址到结束IP地址,查询这个范围内的所有IP地址所链接的主机是否在线,也就是是否接入互联网。判断的过程主要就是头部生存空间的使用。
初始时 TTL 等于 1,这样当该数据报抵达途中的第一个路由器时,TTL 的值就被减为 0,导致发生超时错误,因此该路由生成一份 ICMP 超时差错报文返回给源主机。随后,主机将数据报的 TTL 值递增 1,以便 IP 报能传送到下一个路由器。这个TTL的重复过程就是检测所IP所在的主机是否在线的过程。
实验二的学习可以说是对于网络上的报文信息在各个中转结点的应用的学习的好方法,它通过协议处理的方式展示了报文或者说数据从起始的IP地址的主机到达目的IP地址的过程。在整个过程,对于中转结点(也就是非目的结点)处的数据的处理等等。也就是利用TTL处理和检测每到一个结点之后对于信息的处理方式和对于是否是目的IP的判断方法。
同时通过运行解析程序学会了TTL的工作方法,TTL在到达每一个结点之后TTL的值就被减1,当发生了超时错误的时候生成一份ICMP报文返回给源主机,之后TTL的报文值再次加一。这样,源主机只需对返回的每一份 ICMP 报文进行解析处理,就可以掌握数据报从源主机到达目地主机途中所经过的路由信息。就是这样一个过程,使得报文从起始的IP地址传送至目的IP。
#include
#include
#include
#include
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;
//报文解码结构
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)
{
// 如果是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()
{
//char ip[18] = "192.168.254.254";
//findNextIp(ip);
//初始化 Windows sockets 网络环境
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
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)
{
// 执行,单线程执行,实现后改成多线程
u_long ulDestIP = inet_addr(nextIpAddress);
//转换不成功时按域名解析
if (ulDestIP == INADDR_NONE)
{
hostent * pHostent = gethostbyname(nextIpAddress);
if (pHostent)
{
ulDestIP = (*(in_addr*)pHostent->h_addr).s_addr;
}
else {
cout << "输入的 IP 地址或域名无效!" << endl;
WSACleanup();
return 0;
}
}
// 填充目的 sockaddr_in
sockaddr_in destSockAddr;
ZeroMemory(&destSockAddr, sizeof(sockaddr_in));
destSockAddr.sin_family = AF_INET;
destSockAddr.sin_addr.S_un.S_addr = ulDestIP;
// 创建原始套接字
SOCKET sockRaw = WSASocket(AF_INET, SOCK_RAW, IPPROTO_ICMP, NULL, 0,
WSA_FLAG_OVERLAPPED);
// 设置发送接收超时时间
int iTimeout = 400;
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 int DEF_ICMP_DATA_SIZE = 32; // ICMP 报文数据段长度
const int MAX_ICMP_PACKET_SIZE = 1024;//ICMP 报文最大长度(加上报头)
const DWORD DEF_ICMP_TIMEOUT = 300;// 回显超时时间
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;
pIcmpHeader->id = (USHORT)GetCurrentProcessId();// ID为进程PID
memset(IcmpSendBuf + sizeof(ICMP_HEADER), 'E', DEF_ICMP_DATA_SIZE);//数据字段
USHORT usSeqNo = 0; // ICMP 报文序列号
int iTTL = 1; // TTL初始化
BOOL bReachDestHost = FALSE; // 循环退出标志
int iMaxHot = DEF_MAX_HOP; // 最大循环数
DECODE_RESULT DecodeResult;// 传输数据的介质,封装成结构
int count11=0;
while (!bReachDestHost && iMaxHot--)
{
bReachDestHost = FALSE;
// 设置 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 回显请求信息
// 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)
{
//接收数据
iReadDataLen = recvfrom(sockRaw, IcmpRecvBuf, MAX_ICMP_PACKET_SIZE, 0, (sockaddr*)&from, &
iFromLen);
if (iReadDataLen != SOCKET_ERROR) // 有数据到达
{
//解析数据包
if (DecodeIcmpResponse2(IcmpRecvBuf, iReadDataLen, DecodeResult, ICMP_ECHO_REPLY, DEF_ICMP_TIMEOUT))
{
// 到达目的地,退出循环
if (DecodeResult.dwIPaddr.S_un.S_addr == destSockAddr.sin_addr.S_un.S_addr){
bReachDestHost = true;
count11++;
}
bReachDestHost = true;
// 输出 IP 地址
cout << '\t' << inet_ntoa(DecodeResult.dwIPaddr) << endl;
break;
}
}
else if (WSAGetLastError() == WSAETIMEDOUT) //接收超时,输出*号
{
cout << " *" << '\t' << "Request timed out." << endl;
break;
}
else
{
break;
}
}
iTTL++;
}
cout << "查找: " << nextIpAddress << "结果为 ->" << (bReachDestHost ? "在线" : "不在线") << endl;
//if nextIpAddress ==bReachDestHost;
count11++;
// 向下推
strcpy(nextIpAddress, findNextIp(nextIpAddress));
}
cout << count11<< endl;
cout << "在线总共:"+count11<< endl;
}
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 j = 3; j >= 0; j--)
{
if (strcmp("254", z[j]) == 0)
{
strcpy(z[j], "1"); // 这里让ip 1-254
}
else
{
int x;
x = atoi(z[j]) + 1;
itoa(x,z[j],10); // 第三个参数是 int的进制
break;
}
}
char retIp[ipAddressSize];
strcpy(retIp, z[0]);
char c[2] = ".";
for (int k = 1; k < 4; k++)
{
strcat(retIp, c);
strcat(retIp, z[k]);
}
/*cout << retIp << endl;*/
return retIp;
}