一、预备知识
ICMP
ICMP的报文是封装在IP数据部分中的。按照我的理解,ICMP就是在网络层中,反馈一些转发、访问等操作时的附带信息。
ICMP分为两种,ICMP差错报告报文(IP传输时的反馈)和ICMP询问报文(主动发起检查)。具体类型值和作用如下:
- 3 终点不可达
- 11 时间超过
- 12 参数问题: IP首部数据有问题
- 5 改变路由: 规定发送到某目的的IP,经过某路由
- 8或0 回送请求或回答:向某台主机询问,主机必须给出某种回答
- 13或14 时间戳请求或回答:向某台主机询问时间。
ICMP的应用之tracert
用于测试到达某IP地址所需的TTL(跳数),往返时间。
原理:
源主机向目的主机发送一连串IP数据报,数据报封装的是无法交付的UDP(使用错误的端口号,好坏的)。
第一个数据包的生存时间TLL设置为1,当P1达到路径上的第一个路由时,路由器R1就收下,然后把TLL减1,这时TLL为0,R1就丢弃数据报,然后向源主机发送一个ICMP时间超过的差错报告报文。一直做下去,直到最后一个数据报到达目的主机,这是数据报的TTL是1。由于已经到达了目的地,那么主机收下数据报,且不做减一操作(TLL为1)。但是数据报的错误的,因此目的主机就会发一个ICMP终点不可达差错报告报文。
下图的三个时间是因为每一次都发送三个相同的数据报。
ICMP的应用之ping
向目的主机发送询问时间请求(ICMP中的13), 目的主机收到请求时,发回当前时间戳(ICMP中的14),因此利用时间戳可以计算出往返时间。
二、实验部分:Tracert 与 Ping 程序设计与实现
参照附录 2,了解 Tracert 程序的实现原理,并调试通过。然后参考 Tracert 程序和教材 4.4.2 节,编写一个 Ping 程序,并能测试本局域网的所有机器是否在线,运行界面如图 1 所示的 QuickPing 程序。
实现之前带着的疑问?
1) 报文的具体组成?如何将ICMP数据部分 + ICMP数据头组成ICMP数据报。再将ICMP数据报加入IP数据中,最后让IP数据部分加上IP数据头构成IP数据报?
2) IP报文通过什么方法解析,可以得到IP数据报的头和数据部分。然后数据部分如何解析出ICMP报文的头和数据部分?
3)包装好的IP报文通过什么通道传输。
疑问解答
完整的代码我放到最后,在visual stdio下,关闭sdk检查,完美运行,下面对于程序的个人理解。
问题一:报文的组成
这一步是为了将希望传递的信息封装成char sendRev[],数据缓冲区也就是字符串数组。不过socket帮我们封装了一个方法,让我们不用具体构造到字符数组。这里后面再说。
通过参数构造成ICMP头部结构体,然后再加上想要的ICMP数据部分。再构造出IP头部,把ICMP报文加到IP数据部分之前。这样完整的IP数据报就完成了。
通过结构体构造出ICMP数据头
//ICMP 报头,一共八个字节,前四个字节为:类型(1字节)、代码(1字节)和检验和(2字节)。后四个字节取决于类型
typedef struct
{
BYTE type; //8 位类型字段:标识ICMP的作用
BYTE code; //8 位代码字段
USHORT cksum; //16 位校验和
USHORT id; //16 位标识符
USHORT seq; //16 位序列号
} ICMP_HEADER;
通过结构体构造出IP数据头
//IP 报头,标准IPV4占20字节
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;
问题二:IP数据报的解析
接收到的数据缓存是字符数组 char bufRev[],因此需要通过特定的解析(也就是拆成一段一段的)获取想要的信息。
另外为了方便存取信息,这里又写了一种DECODE_RESULT,解码信息的结构体。把信息封装到结构体中,就比较方便的得到序列号、往返时间和目的IP了。
//报文解码结构
typedef struct
{
USHORT usSeqNo; //序列号
DWORD dwRoundTripTime; //往返时间
in_addr dwIPaddr; //返回报文的 IP 地址
}DECODE_RESULT;
这里还需知识储备,就是字符串转结构体指针这种写法。它会把字符数组中的内容按顺序赋值到结构体中。
char 占1个字节
int 占4个字节
unsigned char a[] = "0123456789abcdefghijk"; //无符号字符数组
struct A //结构体A,一个int 三个char 再接一个int
{
unsigned int a;
unsigned char b;
unsigned char c;
unsigned char d;
unsigned int e;
} *pp;
pp = (A*) a;
cout<< (*pp).a <<' '<<(*pp).b <<' '<<(*pp).c <<' '<<endl;
有了上面的知识储备,那么如何解析IP数据报(字符数组)就比较好理解了,通过特定的地址偏移,就能把字符数组赋值到IP、ICMP结构体中了
具体的解析函数,大部分都打上了注释
// 1)接收到的Buf 2)接收到的数据长度 3)解析结果封装到Decode 4)ICMP类型 ECHO_REPLY(是一个常量,放到全局也行) 5)ICMP类型 TIMEOUT
BOOL DecodeIcmpResponse(char * pBuf, int iPacketSize, DECODE_RESULT &DecodeResult, BYTE
ICMP_ECHO_REPLY, BYTE ICMP_TIMEOUT)
{
//查找数据报大小合法性
//pBuf的首地址,就是IP报的首地址,因此偏移0
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;
}
问题三:传输的通道
最后这个问题也是我比较困惑的,原因是Socket把底层封装好了,我们只需把参数(发送的IP结构、ICMP结构、目的主机地址结构体)填好,传递到sendto()函数里面,就能把IP数据报发送到目的主机。通过调用recv就能得到目的主机的反馈。目前我也没找到更加底层的分析,因此也只停留在会用而已。
说一下这个程序干了什么事,如何使用
效果
ping
首先ping的原理是基础,ping也就是发送一个ICMP类型为"时间戳请求"的数据报,当目的主机收到后就会反馈一个ICMP类型为"时间戳回答"的报文,然后发送方接收到反馈后,进行解析。发现收到的数据报包含了ICMP类型为“时间戳回答”的报文,因此计算出往返时间,结果就是ping通
tracert
在ping的基础上做一些修改,发送的ICMP类型是"请求"而不是"时间戳请求",每次都是发送到目的主机,但是TTL从1慢慢增加,这样就能获得路径上所经过的网络设备。
接收方
只要目标主机开启了ICMP的功能,那么它接收到携带ICMP报文的IP就会自动处理,因此接收方接收的事情在操作系统已经帮我们完成了。
发送方
根据ping 和 tracert业务不同改变ICMP类型即可,但是tracert要慢慢增加TTL,而ping是一下子把TTL开的足够大。
代码:实现ip区间内的tracert
#include "pch.h"
#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdlib.h>
using namespace std;
#pragma comment(lib, "Ws2_32.lib")
/******全局常量********/
const int ipAddressSize = 17;
//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 = 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));
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;// 传输数据的介质,封装成结构
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, ICMP_TIMEOUT))
{
// 到达目的地,退出循环
if (DecodeResult.dwIPaddr.S_un.S_addr == destSockAddr.sin_addr.S_un.S_addr)
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;
// 向下推
strcpy(nextIpAddress, findNextIp(nextIpAddress));
}
}
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;
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;
}