高级计算机网络实验——c++实现ping工具

高级计算机网络实验——c++实现ping工具

1.Prepare

若要实现Ping工具,需要提前做好以下工作:

  • 了解ICMP协议以及Ping包的协议结构
  • 实现Socket通信,能够对包进行封装和解析

首先让我们来看看Windows下的Ping长什么样子?

高级计算机网络实验——c++实现ping工具_第1张图片

具体关于Ping的介绍,可以阅读以下文章:

Ping 命令详解_hebbely的博客-CSDN博客_ping

1.1 ICMP 协议

RFC 792: Internet Control Message Protocol (rfc-editor.org)

详解RFC 792文档 - 知乎 (zhihu.com)

可以看到,通常ICMP协议用于源主机与目的主机通信过程中数据包处理出现错误的情况,它是IP协议的一个补充,每个ICMP报文必须由IP模块来实现。

ICMP将会在以下几种情况发送:

  • 当数据包无法到达目的主机时
  • 当网关没有能力转发数据包时
  • 当网关能够引导主机以一个更短路由发送数据时
ICMP消息格式

ICMP消息使用基本的IP报头进行发送,其中IP首部的Protocol字段值为1(标志了这是ICMP报文),随后IP数据包的数据部分由ICMP包组成。其第1个字段(8 bit)是ICMP类型,这个值决定了剩余数据的格式,任何标记为“未使用的”的字段都是为以后的扩展保留的 ,并且在发送时必须为零,但是接收方不应该使用这些字段(除了将它们包含在校验和中),除非在个别格式说明下另有说明。

我们这里不再描述各类消息的具体字段以及使用场景,具体内容在rfc文档中和网络上已经有较多说明。我们若想实现ping工具,就需要考虑多种返回包的情况。

通过自己抓包也可以观察、分析ICMP的格式

高级计算机网络实验——c++实现ping工具_第2张图片

1.2 Windows下Socket编程实现

windows下的socket编程(C++代码实现)_舒然—小广广的博客-CSDN博客_windows下socket编程

基本的实现上述文章已做了介绍,下面我们需要做的就是icmp包的封装以及解析得到回传信息。

2. Coding

/*
 * @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操作,可自主控制包数量与大小。

高级计算机网络实验——c++实现ping工具_第3张图片

参考链接

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

你可能感兴趣的:(计算机网络,c++)