若要实现Ping工具,需要提前做好以下工作:
首先让我们来看看Windows下的Ping长什么样子?
具体关于Ping的介绍,可以阅读以下文章:
Ping 命令详解_hebbely的博客-CSDN博客_ping
RFC 792: Internet Control Message Protocol (rfc-editor.org)
详解RFC 792文档 - 知乎 (zhihu.com)
可以看到,通常ICMP协议用于源主机与目的主机通信过程中数据包处理出现错误的情况,它是IP协议的一个补充,每个ICMP报文必须由IP模块来实现。
ICMP将会在以下几种情况发送:
ICMP消息使用基本的IP报头进行发送,其中IP首部的Protocol字段值为1(标志了这是ICMP报文),随后IP数据包的数据部分由ICMP包组成。其第1个字段(8 bit)是ICMP类型,这个值决定了剩余数据的格式,任何标记为“未使用的”的字段都是为以后的扩展保留的 ,并且在发送时必须为零,但是接收方不应该使用这些字段(除了将它们包含在校验和中),除非在个别格式说明下另有说明。
我们这里不再描述各类消息的具体字段以及使用场景,具体内容在rfc文档中和网络上已经有较多说明。我们若想实现ping工具,就需要考虑多种返回包的情况。
通过自己抓包也可以观察、分析ICMP的格式
windows下的socket编程(C++代码实现)_舒然—小广广的博客-CSDN博客_windows下socket编程
基本的实现上述文章已做了介绍,下面我们需要做的就是icmp包的封装以及解析得到回传信息。
/*
* @Author: De4thStr0ke_hai
* @Date: 2022-10-28 00:07:02
* @Last Modified by: De4thStr0ke_hai
* @Last Modified time: 2022-10-30 00:05:34
*/
#include "winsock2.h"
#include "stdlib.h"
#include "stdio.h"
#include
#include "string.h"
#pragma comment(lib,"WS2_32.lib")
using namespace std;
//IP数据包头结构 8位1字节 共20字节
typedef struct IPHeader
{
unsigned int m_HDlen:4; // 4位首部长度
unsigned int version:4; // 4位版本号
unsigned char m_byTOS; // 8服务类型
unsigned short m_byTotalLen; // 16总长度
unsigned short m_usID; // 16标识
unsigned short m_usFlagFragOffset; // 3位标识+13位片偏移
unsigned char m_byTTL; // 8TTL
unsigned char byProtocol; // 8位协议
unsigned short m_usHChecksum; // 16位首部检验和
unsigned int m_ulSrcIP; // 32源IP地址
unsigned int m_ulDestIP; // 32目的IP地址
}iphead;
//ICMP数据包头结构 8字节
typedef struct ICMPHeader
{
BYTE m_byType; // 类型
BYTE m_byCode; // 代码
USHORT m_usChecksum; // 检验和
USHORT m_usID; // 标识符
USHORT m_usSeq; // 序号
ULONG m_timestamp; // 时间戳
}icmphead;
#define ICMP_ECHO 8 // ICMP回显请求
#define ICMP_ECHOREPLY 0 // ICMP回显应答
#define ICMP_MIN 8 // ICMP数据包最短字节数
#define DEF_PACKET_SIZE 32 // 默认数据包长度
#define DEF_PACKET_NUMBER 4 // 默认发送ICMP请求的次数
#define MAX_PACKET 1024 // 数据包最大长度
int pad_icmp(char *icmp_data, int pack_size)
{
icmphead *icmp_header;
char *data;
icmp_header = (icmphead *)icmp_data; // 强制转换,填充
icmp_header->m_byType = ICMP_ECHO;
icmp_header->m_byCode = 0;
icmp_header->m_usID = (USHORT)GetCurrentProcessId();
icmp_header->m_usChecksum = 0;
icmp_header->m_usSeq = 0;
data = icmp_data + sizeof(icmphead); //地址后移
memset(data, 7, pack_size - sizeof(icmphead));
return 1;
}
USHORT checksum(USHORT *buffer, int size)
{
unsigned long cksum=0;
while(size >1) {
cksum+=*buffer++;
size -=sizeof(USHORT);
}
if(size) {
cksum += *(UCHAR*)buffer;
}
cksum = (cksum >> 16) + (cksum & 0xffff);
cksum += (cksum >>16);
return (USHORT)(~cksum);
}
int unpad_icmp(char *buf, int bytes,struct sockaddr_in *from)
{
iphead *iphdr;
icmphead *icmphdr;
unsigned short iphdrlen;
iphdr = (iphead *)buf;
iphdrlen = (iphdr->m_HDlen) * 4 ; //头部占几个节字节
if (bytes < iphdrlen + ICMP_MIN)
{
printf("数据包过小!");
}
//找到ICMP数据包开始的地方
icmphdr = (icmphead*)(buf + iphdrlen);
if (icmphdr->m_byType != ICMP_ECHOREPLY)
{
printf("未接收到 %d 的ECHO数据包\n",icmphdr->m_byType);
return 1;
}
//是不是发给本程序的数据包
if (icmphdr->m_usID != (USHORT)GetCurrentProcessId())
{
printf("接收到非本机的数据包\n");
return 1;
}
printf("%d bytes from %s:", bytes, inet_ntoa(from->sin_addr));
printf(" icmp_seq = %d. ",icmphdr->m_usSeq);
printf(" time: %d ms ", GetTickCount()-icmphdr->m_timestamp); //发送到接收过程的经历的时间
printf("\n");
return 0;
}
int main(int argc, char **argv) // argc:参数个数,argv:存储参数
{
WORD wVersionRequested;
WSADATA wsaData; // 初始化DLL
int err, Time_out, nums, pack_size;
int i, send_icmp, recv_icmp;
SOCKET sockPing; // 定义套接字
int timeout = 1000; // 定义超时时间1s
struct hostent *ip_addr; // 接收域名/IP信息
unsigned int dst_addr; // 若不是域名,接收ip
struct sockaddr_in src_ip, dst_ip; // 定义源ip和目的ip
char *cdst_ip; // 定义ip地址字符串
char *icmp_data; // ICMP数据包指针
char *recvbuf; // 缓冲区指针
unsigned short seq_init = 0; // 定义初始序列号
int ip_len =sizeof(src_ip);
int stat = 0; // 统计数据
if (argc < 2) // 参数输入错误
{
printf("请检查参数设置!\n");
return 0;
}
wVersionRequested = MAKEWORD( 2, 2 );
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 )
{
printf("DLL初始化失败!\n"); // 初始化失败
return 0;
}
sockPing = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); // 创建套接字
if (sockPing == INVALID_SOCKET) // 套接字创建失败
{
printf("套接字创建失败!\n");
return 0;
}
Time_out = setsockopt(sockPing, SOL_SOCKET, SO_SNDTIMEO, (const char*)&timeout, sizeof(timeout));
if (Time_out == SOCKET_ERROR) // 设置发送超时时间
{
printf("设置发送超时时间失败!\n");
return 0;
}
Time_out = setsockopt(sockPing, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout));
if (Time_out == SOCKET_ERROR) // 设置接收超时时间
{
printf("设置接收超时时间失败!\n");
return 0;
}
ip_addr = gethostbyname(argv[1]); // 通过域名获得ip地址
if (!ip_addr)
{
dst_addr = inet_addr(argv[1]); // 直接由ip获取地址
if (dst_addr == INADDR_NONE)
{
printf("不合法的网络地址!\n");
}
}
if (ip_addr != NULL) // 存储目标机的ip地址和地址类型
{
memcpy(&(dst_ip.sin_addr), ip_addr->h_addr_list[0], ip_addr->h_length);
dst_ip.sin_family = ip_addr->h_addrtype;
}
else
{
dst_ip.sin_addr.s_addr = dst_addr;
dst_ip.sin_family = AF_INET;
}
cdst_ip = inet_ntoa(dst_ip.sin_addr);
if (argc > 2) // Ping 次数
{
nums=atoi(argv[2]);
if(nums == 0)
nums = DEF_PACKET_NUMBER;
}
else
{
nums = DEF_PACKET_NUMBER;
}
if (argc == 3) // Ping 包大小
{
pack_size = atoi(argv[3]);
if(pack_size == 0)
pack_size = DEF_PACKET_SIZE;
else if (pack_size > 1024)
{
printf("数据大小过大!\n");
pack_size = DEF_PACKET_SIZE;
}
}
else
pack_size = DEF_PACKET_SIZE;
pack_size += sizeof(ICMPHeader); // 确定IP数据包数据字段大小(即ICMP包大小)
icmp_data = (char *)malloc(MAX_PACKET); // 申请地址
recvbuf = (char *)malloc(MAX_PACKET);
if (!icmp_data || !recvbuf)
{
printf("地址块申请失败!\n");
return 0;
}
memset(icmp_data, 1, MAX_PACKET); // 填充ICMP头
pad_icmp(icmp_data, pack_size); // 填充ICMP数据
printf("\n正在Ping %s ....\n\n", cdst_ip);
for (i=0; im_usChecksum = 0;
((icmphead *)icmp_data)->m_timestamp = GetTickCount();
((icmphead *)icmp_data)->m_usSeq = seq_init++;
((icmphead *)icmp_data)->m_usChecksum = checksum((USHORT*)icmp_data,pack_size);
// 发送数据包
send_icmp = sendto(sockPing, icmp_data, pack_size, 0, (struct sockaddr*)&dst_ip, sizeof(dst_ip));
if (send_icmp == SOCKET_ERROR)
{
if (WSAGetLastError() == WSAETIMEDOUT)
{
printf("发送超时!\n");
continue;
}
printf("发送失败: %d\n",WSAGetLastError());
break;
}
// 接收数据包
recv_icmp = recvfrom(sockPing, recvbuf, MAX_PACKET, 0, (struct sockaddr*)&src_ip, &ip_len);
//接收失败
if (recv_icmp == SOCKET_ERROR)
{
if (WSAGetLastError() == WSAETIMEDOUT)
{
printf("接收超时!\n");
continue;
}
printf("接收失败: %d\n",WSAGetLastError());
break;
}
if(!unpad_icmp(recvbuf,recv_icmp,&src_ip))
stat++; //记录成功接收响应数据包的次数
Sleep(1000);
}
printf("\nPing 统计: %s \n",cdst_ip);
printf(" 数据包: 发送 = %d,接收 = %d, 丢失 = %d (%2.0f%% loss)\n",nums,
stat,(nums-stat), (float)(nums-stat)/nums*100);
free(recvbuf);
free(icmp_data);
closesocket(sockPing);
WSACleanup();
return 0;
}
可以实现对ip/域名的ping操作,可自主控制包数量与大小。
recvfrom函数_My Lullaby的博客-CSDN博客_recvfrom
调试C/C++程序的常见方法_hesorchen的博客-CSDN博客_c++调试
C++实现基于ICMP协议的ping命令_Ning静致远的博客-CSDN博客_c++ icmp
WSADATA (winsock.h) - Win32 apps | Microsoft Learn
gethostbyname()函数:通过域名获取IP地址 (biancheng.net)
inet_ntoa()的用法_想看焰火吗的博客-CSDN博客
windows下ping程序使用C语言实现_Renaway的博客-CSDN博客
网络编程第一篇:IP地址结构sin_addr的定义解析。_gelao18sui的博客-CSDN博客_sin_addr