Ping命令的实现
Ping (Packet Internet Groper)是一种因特网包探索器,用于测试网络连接量的程序。本文将基于 Socket 编程,实现一个基本的 Ping 命令程序。
ICMP 报文分析
ICMP 报文捕获
在控制台输入 ping 202.195.147.248
,对该目的主机发起请求,可以看到控制台输出了一系列统计信息:4 个数据包全部接收并且往返时间为 5 ms(较短),表明与该主机之间的连接畅通。
使用 Wireshark 工具捕获 icmp 数据包,为了避免无关数据包的干扰,可以使用 filter 对数据包进行过滤,在上部栏输入 ip.src == 202.195.147.248 or ip.dst == 202.195.147.248
,表明只筛选源地址或目的地址为 202.195.147.248 的数据包,最终可以得到数据包的内容。
Wireshark 数据包分析
根据 ICMP 报文的格式进行分析:
- Type:数据包类型,占 1 Byte,为 0x00,代表回送报文。
- Code:代码部分,占 1 Byte,为 0x00.
- Checksum:检验和,占 2 Bytes,为 0x554c.
- Identifier(IE):占 2 Bytes,为 0x0001.
- Identifier(LE):占 2 Bytes,为 0x0100.
- Sequence Number(BE):占 2 Bytes,为 0x000f.
- Sequence Number(LE):占 2 Bytes,为 0x0f00.
- Data:占 32 Bytes,为6162636465666768696a6b6c6d6e6f7071727374757677616263646566676869.
实现思路
构造 ICMP 报文
自定义数据结构 icmpHeader 表示 ICMP 报文头部,包含类型、代码、检验和、标识符和序列号。
// ICMP 报文头
struct icmpHeader {
unsigned char type; // 类型
unsigned char code; // 代码
unsigned short checkSum; // 检验和
unsigned short id; // 标识符
unsigned short sequence; // 序列号
};
填充该报文,类型为 8 表示请求报文。检验和使用特定的算法计算,关于算法的具体内容可以自行查看相关文档,在此不过多赘述。标识符使用进程 id 填充。最后在 ICMP 报文头的尾部,添加 32 字节的数据作为 ICMP 报文的数据部分。
// 构造 ICMP 报文
char sendBuf[8 + 32] = { 0 };
icmpHeader* pIcmp = (icmpHeader*)sendBuf;
pIcmp->type = 8;
pIcmp->code = 0;
pIcmp->checkSum = 0;
pIcmp->id = (USHORT)::GetCurrentProcessId();
pIcmp->sequence = 0;
// 填充数据部分
memcpy(sendBuf + 8, "abcdelmnopqrstuvwiammekakuactor", 32);
// 计算检验和
pIcmp->checkSum = computeCks((icmpHeader*)sendBuf, sizeof(sendBuf));
发送请求报文
该部分使用 Socket 编程向指定 IP 地址发送 ICMP 请求报文。需要注意的是,在创建套接字时,需要使用原始套接字,且 protocol 参数为 IPPROTO_ICMP
,表明使用 ICMP 协议。SOCKET s = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
报文发送成功后,接收从客户端发送的回送报文信息。
// 初始化套接字库
WORD wReq = MAKEWORD(2, 2);
WSADATA wsadata;
WSAStartup(wReq, &wsadata);
// 填充服务端地址
SOCKADDR_IN serverAddr;
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.S_un.S_addr = inet_addr(targetIP.c_str());
// 创建套接字
SOCKET s = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
为了得到报文往返的时间,可以在发送前和接收后使用 GetTickCount64()
获取从操作系统启动到现在所经历的的时间 start 和 end,两时间相减得到时间差。
此外,由于 recvfrom()
在未收到报文时将会阻塞,因此可以使用 setsockopt()
设定一个接收超时时间,在超过指定时间未受到数据时返回 -1,表示接收异常。
setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeOut, sizeof(timeOut)); // 设置接收超时
解析回送报文
根据回送的 IP 数据包的指定格式对信息进行解析,IPv4 头部的 4 到 8 位为 IP 报文头部长度,第 9 个字节是 TTL 的值。
完整代码
#include<iostream>
#include<string>
#include<winsock.h>
#include<thread>
#pragma comment(lib,"ws2_32.lib")
// ICMP 报文头
struct icmpHeader {
unsigned char type; // 类型
unsigned char code; // 代码
unsigned short checkSum; // 检验和
unsigned short id; // 标识符
unsigned short sequence; // 序列号
};
// 计算检验和
unsigned short computeCks(icmpHeader* picmp, int len) {
long sum = 0;
unsigned short* pusicmp = (unsigned short*)picmp;
while (len > 1) {
sum += *(pusicmp++);
if (sum & 0x80000000)
sum = (sum & 0xffff) + (sum >> 16);
len -= 2;
}
if (len)
sum += (unsigned short)*(unsigned char*)pusicmp;
while (sum >> 16)
sum = (sum & 0xffff) + (sum >> 16);
return (unsigned short)~sum;
}
int ping(const std::string& targetIP) {
// 初始化套接字库
WORD wReq = MAKEWORD(2, 2);
WSADATA wsadata;
WSAStartup(wReq, &wsadata);
// 填充服务端地址
SOCKADDR_IN serverAddr;
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.S_un.S_addr = inet_addr(targetIP.c_str());
// 创建套接字
SOCKET s = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
// 构造 ICMP 报文
char sendBuf[8 + 32] = { 0 };
icmpHeader* pIcmp = (icmpHeader*)sendBuf;
pIcmp->type = 8;
pIcmp->code = 0;
pIcmp->checkSum = 0;
pIcmp->id = (USHORT)::GetCurrentProcessId();
pIcmp->sequence = 0;
// 填充数据部分
memcpy(sendBuf + 8, "abcdelmnopqrstuvwiammekakuactor", 32);
// 计算检验和
pIcmp->checkSum = computeCks((icmpHeader*)sendBuf, sizeof(sendBuf));
// 发送报文
DWORD start = GetTickCount64();
int sendLen = sendto(s, sendBuf, sizeof(sendBuf), 0, (SOCKADDR*)&serverAddr, sizeof(SOCKADDR));
if (sendLen < 0) printf("errno = %d\n", GetLastError());
// 接收报文
char recvBuf[1024];
SOCKADDR_IN fromAddr;
int fLen = sizeof(fromAddr);
unsigned timeOut = 1000; // 超时时间
setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeOut, sizeof(timeOut)); // 设置接收超时
while (true) {
int len = recvfrom(s, recvBuf, 1024, 0, (SOCKADDR*)&fromAddr, &fLen);
if (len < 0) {
std::cout << "请求超时" << std::endl;
return INT32_MAX;
}
else break;
}
DWORD end = GetTickCount64();
DWORD timeSpan = end - start;
// 回送报文解析
char ipInfo = recvBuf[0];
// ipv4 头部的第 9 个字节为 TTL 的值
unsigned char ttl = recvBuf[8];
int ipHeadLen = ((char)(ipInfo << 4) >> 4) * 4; // IP报文头部长度
icmpHeader* icmpResp = (icmpHeader*)(recvBuf + ipHeadLen);
if (icmpResp->type == 0) { //回显应答报文
printf("来自 %s 的回复:字节=32 时间=%2dms TTL=%d\n",
targetIP.c_str(), timeSpan, ttl);
return timeSpan;
}
else {
printf("请求超时。type = %d\n", icmpResp->type);
return INT32_MAX;
}
}
int main() {
std::cout << "请输入目的IP地址:";
std::string IP;
std::cin >> IP;
int maxTime = INT32_MIN, minTime = INT32_MAX, timeSum = 0, acpkgCnt = 0;
printf("\n正在 Ping %s 具有 32 字节的数据:\n", IP.c_str());
for (int i = 0; i < 4; ++i) {
std::this_thread::sleep_for(std::chrono::seconds(1));
int timeSpan = ping(IP);
acpkgCnt += timeSpan != INT32_MAX;
maxTime = max(maxTime, timeSpan);
minTime = min(minTime, timeSpan);
timeSum += timeSpan;
}
printf("\n%s 的 Ping 统计信息:\n", IP.c_str());
printf(" 数据包: 已发送 = 4,已接收 = %d,丢失 = %d (%d%% 丢失),\n",
acpkgCnt, 4 - acpkgCnt, (4 - acpkgCnt) * 100 / 4);
if (!acpkgCnt) return 0;
printf("往返行程的估计时间(以毫秒为单位):\n");
printf(" 最短 = %dms,最长 = %dms,平均 = %dms\n", minTime, maxTime, timeSum / acpkgCnt);
}
运行结果
可见,本地与该目的主机的连通性较好。
可见,本地与该目的主机无法连通。