[C++] 从零实现一个ping服务

在这里插入图片描述

💻文章目录

  • 前言
  • ICMP
    • 概念
    • 报文格式
  • Ping服务实现
    • 系统调用函数
    • 具体实现
    • 运行测试
  • 总结


前言

ping命令,因为其简单、易用等特点,几乎所有的操作系统都内置了一个ping命令。如果你是一名C++初学者,对网络编程、系统编程有所了解,但又没有多少实操经验的话,不妨来尝试动手实现一个属于自己的ping命令。这样一来,也能提高你对系统编程、网络编程的能力。

ICMP

概念

ICMP是工作在网络层的一种不可靠的传输协议,意在辅助IP协议获取报文传输与网络连接的情况,被广泛运用于网络诊断工具(如:ping 和 traceroute)。

ICMP协议可以控制路由将报文错误原因返回给源主机,从而实现对网络状况的诊断。

在这里插入图片描述

报文格式

ICMP协议被封装在IP协议之中,以下为ICMP的报文固定格式:

在这里插入图片描述

  • 类型:用于标识报文的类型,ICMP报文类型分为两类:信息类报文、差错类报文。

  • 代码:用于标识差错类报文的具体错误信息。

  • 校验和:用于计算报文是否出现损坏(发送方填写,接收方校验)。

「ICMP常见消息类型」

ICMP 类型描述
0回显应答(Echo Reply):对回显请求的响应,通常用于ping操作。
3目的不可达(Destination Unreachable):目标地址无法到达时发送,包括网络不可达、主机不可达等子类型。
4源抑制(Source Quench):请求发送方降低发送速率,以防止网络拥塞(现已弃用)。
5重定向(Redirect):建议主机将数据包发送到不同的路由器,提供更优路径。
8回显请求(Echo Request):请求目标主机返回应答消息,通常用于ping操作。
11超时(Time Exceeded):数据包在网络中传输时间超过TTL值,或在分片重组过程中超时。
12参数问题(Parameter Problem):数据包的IP头部存在错误,导致无法处理。

「Linux中的实现」

Linux中ICMP报文格式有不少成员,但只是实现ping服务只需要以下成员:

  • icmp_type:icmp报文的类型。

  • icmp_cksum:校验和,用于计算数据是否损坏。

  • icmp_id:用于标识报文的唯一性。

  • icmp_seq:序列号字段,多用于echo、echoreply功能。

  • icmp_data:报文的内容,只有8bit大小

「Linux中ICMP报文的描述」

/*Linux中icmp的有较多成员变量,嫌麻烦可以看#define部分来认识主要成员变量*/
struct icmp
{
  uint8_t  icmp_type;	/* icmp类型; type of message, see below */
  uint8_t  icmp_code;	/* type sub code */
  uint16_t icmp_cksum;	/*校验和,用于确定报文是否完整无损*/
  union
  {
    unsigned char ih_pptr;	/* ICMP_PARAMPROB */
    struct in_addr ih_gwaddr;	/* gateway address */
    struct ih_idseq		/* echo datagram */
    {
      uint16_t icd_id;
      uint16_t icd_seq;
    } ih_idseq;
    uint32_t ih_void;

    /* ICMP_UNREACH_NEEDFRAG -- Path MTU Discovery (RFC1191) */
    struct ih_pmtu
    {
      uint16_t ipm_void;
      uint16_t ipm_nextmtu;
    } ih_pmtu;

    struct ih_rtradv
    {
      uint8_t irt_num_addrs;
      uint8_t irt_wpa;
      uint16_t irt_lifetime;
    } ih_rtradv;
  } icmp_hun;
#define	icmp_pptr	icmp_hun.ih_pptr
#define	icmp_gwaddr	icmp_hun.ih_gwaddr
#define	icmp_id		icmp_hun.ih_idseq.icd_id
#define	icmp_seq	icmp_hun.ih_idseq.icd_seq
#define	icmp_void	icmp_hun.ih_void
#define	icmp_pmvoid	icmp_hun.ih_pmtu.ipm_void
#define	icmp_nextmtu	icmp_hun.ih_pmtu.ipm_nextmtu
#define	icmp_num_addrs	icmp_hun.ih_rtradv.irt_num_addrs
#define	icmp_wpa	icmp_hun.ih_rtradv.irt_wpa
#define	icmp_lifetime	icmp_hun.ih_rtradv.irt_lifetime
  union
  {
    struct    //存储时间戳
    {
      uint32_t its_otime;        // 原始时间戳,发送时的时间
      uint32_t its_rtime;        // 接受时间戳,接受时的时间
      uint32_t its_ttime;        // 传输时间戳,传输所用时间
    } id_ts;
    struct
    {
      struct ip idi_ip;
      /* options and then 64 bits of data */
    } id_ip;
    struct icmp_ra_addr id_radv;
    uint32_t   id_mask;
    uint8_t    id_data[1];
  } icmp_dun;
#define	icmp_otime	icmp_dun.id_ts.its_otime
#define	icmp_rtime	icmp_dun.id_ts.its_rtime
#define	icmp_ttime	icmp_dun.id_ts.its_ttime
#define	icmp_ip		icmp_dun.id_ip.idi_ip
#define	icmp_radv	icmp_dun.id_radv
#define	icmp_mask	icmp_dun.id_mask
#define	icmp_data	icmp_dun.id_data
};

Ping服务实现

系统调用函数

原始套接字

要使用ICMP协议就必须绕过传输层(TCP/UDP),直接操作网络层,所以必须使用原始套接字,在Mac、Linux中使用原始套接字可能会需要root权限

//函数原型
int socket(int domain, int type, int protocol);

int _sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);  //使用原始套接字

信号转换

在Linux中的ping服务一般通过ctl+c来实现终止,所以得要将信号执行函数替换成自己的函数。

//函数原型
void (*signal(int sig, void (*func)(int)))(int);

//使用方式
signal(SIGINT, [](int sig)
{
    printf("sig:%d", sig);
} );

「域名转换为IP地址」

在Linux中将域名转成ip地址的函数有gethostbyname,但其在新版本的linux中已经被废弃,所以这里使用较新的getaddrinfo。

/*通过getaddrinfo获取的数据将存进该结构体*/
struct addrinfo {
   int              ai_flags;
   int              ai_family;    //协议族
   int              ai_socktype;
   int              ai_protocol;
   socklen_t        ai_addrlen;  // sockaddr 的长度
   struct sockaddr *ai_addr;     // 根据需求转换成sockaddr_in
   char            *ai_canonname;
   struct addrinfo *ai_next;     //下一个addrinfo,使用链表来连接匹配的IP。
};

int getaddrinfo(const char *restrict node,                  //需要转换的域名
                   const char *restrict service,            //DNS服务器地址,可为空
                   const struct addrinfo *restrict hints,   //用于限定获取的数据
                   struct addrinfo **restrict res);         //结果存放的指针

具体实现

ping服务的实现使用了类来进行封装,从而使得其更简洁易懂。

头文件声明

#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/ip_icmp.h>
#include <string>
#include <iostream>
#include <format>
#include <thread>


class PingServer
{
public:
    PingServer(const char* ip);   

    void Start();  

    static void TimeEnd();   // ping计算总结,ctrl+c调用。

private:
    void Init();     // 初始化类

    void SendData();  //发送数据

    void RecvData();  //接受数据

    unsigned short CheckSum(void* data, int len);   //计算校验和

private:
    static std::chrono::system_clock::time_point _oldTime;   //计算ping服务运行时间
    static int _sendSeq;  //发送数据次数
    static int _recvSeq;  //接受数据次数

    struct sockaddr_in _destAddr;  //远端地址信息
    const char* _ip;    //需要ping的ip/hostname;
    char _recvData[1024];   //接受数据缓冲区
    
    int _sockfd;   //套接字

    unsigned short _id;   //用于标识ip报文唯一性。
};

//初始化静态成员
std::chrono::system_clock::time_point PingServer::_oldTime = std::chrono::system_clock::now();
int PingServer::_sendSeq = 0;
int PingServer::_recvSeq = 0;

介绍完类的成员,也该到其实现了⬇️。

#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <netinet/ip_icmp.h>
#include <string>
#include <iostream>
#include <format>
#include <future>
#include <thread>


//TODO chrono时钟实现超时

class PingServer
{
public:
    PingServer(const char* ip)
        :_ip(ip), _id(htons(getpid()))
    {
        Init();
    }

    void Start()
    {
        std::thread(&PingServer::SendData, this).detach();
        RecvData();
    }

    static void TimeEnd()
    {
        auto now = std::chrono::system_clock::now();
        auto sum = std::chrono::duration_cast<std::chrono::milliseconds>(now-_oldTime).count();
        int loss = ((double)(_sendSeq - _recvSeq) / _sendSeq) * 100;

        std::cout << std::format("\n{} packets transimitted, {} received, {}% packet loss, time {}ms", _sendSeq, _recvSeq, loss, sum) << std::endl;
    }

private:
    void Init()
    {
        _sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);  //使用原始套接字
        if(_sockfd < 0) 
        {
            std::cerr << "socket error" << std::endl;  
            exit(-1);
        }

        struct addrinfo hints{}, *res{};   
        hints.ai_family = AF_INET;  //限定获取IP为IPV4

        if(getaddrinfo(_ip, nullptr, &hints, &res) != 0) //正确返回0
        {
            std::cerr << "hostname error" << std::endl;
            exit(EXIT_FAILURE);
        }

        sockaddr_in* ipv4 = (sockaddr_in*)res->ai_addr;  //转换成sockaddr_in结构 sockaddr->sockaddr_in
        memcpy(&_destAddr, ipv4, sizeof(sockaddr_in));
    }

    void SendData()
    {
        while (1)
        {
            //装包
            struct icmp icmphdr{};  //需要发送的ICMP报文
            icmphdr.icmp_seq = ++_sendSeq;  
            icmphdr.icmp_type = ICMP_ECHO;  //ICMP报文的类型
            // icmphdr.icmp_type = ICMP_TIMESTAMP;      
            icmphdr.icmp_id = _id;      

            auto now = std::chrono::system_clock::now();     // 获取时间戳, 8bit
            memcpy(icmphdr.icmp_data, &now, sizeof(now));    

            icmphdr.icmp_cksum = CheckSum(&icmphdr, sizeof(icmphdr));   // 计算校验和

            if(sendto(_sockfd, &icmphdr, sizeof(icmphdr), 0, (struct sockaddr*)&_destAddr, sizeof(_destAddr)) <= 0)
            {   //发送数据
                std::cout << "send data fail " << _ip << std::endl;
                exit(EXIT_FAILURE);
            }

            std::this_thread::sleep_for(std::chrono::seconds(1));   //每个一秒发送一次
        }
    }

    void RecvData()
    {
        while (1)
        {
            sockaddr_in addr{};
            socklen_t fromLen = sizeof(_destAddr);
            ssize_t n = recvfrom(_sockfd, _recvData, sizeof(_recvData), 0, (sockaddr*)&addr, &fromLen);
            if(n > 0)
            {   
                struct ip* ip_hdr = (struct ip*)_recvData;  
                // 获取ICMP报文位置,IP头部计算为首部字段长度*4;
                struct icmp* icmp_hdr = (struct icmp*)(_recvData + (ip_hdr->ip_hl << 2));   

                if (icmp_hdr->icmp_type == ICMP_ECHOREPLY && icmp_hdr->icmp_id == _id)  //筛选
                {
                    ++_recvSeq;
                    //计算耗时
                    auto now = std::chrono::system_clock::now();
                    auto data = (std::chrono::system_clock::time_point*)icmp_hdr->icmp_data;
                    auto sum = std::chrono::duration_cast<std::chrono::milliseconds>(now - *data).count();

                    std::cout << std::format("{} bytes from {}: icmp_seq={} ttl={} time={}ms",
                        n, inet_ntoa(_destAddr.sin_addr), icmp_hdr->icmp_seq, ip_hdr->ip_ttl, sum) << std::endl;
                }
                // else 
                // {
                //     std::cout << std::format("icmp_type: {}, icmp_ip: {}, icmp_code: {}", icmp_hdr->icmp_type, icmp_hdr->icmp_id, icmp_hdr->icmp_code) << std::endl;
                // }
            }
            else if(n <= 0)
            {
                std::cerr << "Recv fail" << std::endl;
                exit(EXIT_FAILURE);
            }
        }
        
    }

    unsigned short CheckSum(void* data, int len)
    {   
        unsigned short* buf = (unsigned short*)data;
        unsigned sum = 0;

        // 计算数据的和
        while(len > 1)
        {
            sum += *buf++;
            len -= 2;
        }
        if(len == 1)
        {
            sum += *(unsigned char*)buf;
        }

        // 把高16位和低16位相加
        sum = (sum >> 16) + (sum & 0xffff);
        sum += (sum >> 16);
        // 取反
        return (unsigned short)(~sum);
    }



private:
    static std::chrono::system_clock::time_point _oldTime;  
    static int _sendSeq;
    static int _recvSeq;

    unsigned short _id;
    int _sockfd;

    struct sockaddr_in _destAddr;
    const char* _ip;    //需要ping的ip;
    char _recvData[1024];
};

std::chrono::system_clock::time_point PingServer::_oldTime = std::chrono::system_clock::now();
int PingServer::_sendSeq = 0;
int PingServer::_recvSeq = 0;

main函数

#include "Ping.hpp"

//TOOD 初始化

void Usage()
{
    std::cout << "ping <ip/hostname>" << std::endl;
}

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Usage();
        return 1;
    }

    signal(SIGINT, [](int sig)  //当使用 ctl+c 时中断程序。
    {
        PingServer::TimeEnd();
        exit(0);
    });

    PingServer ping(argv[1]);

    ping.Start();

    return 0;
}

运行测试

CMakeList

cmake_minimum_required(VERSION 3.29)
project(PingServer)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

add_executable(test test.cpp
        Ping.hpp
)

运行结果:

在这里插入图片描述

总结

本篇文章实现了一个简易的ping指令,其对系统编程、网络编程都有所涉及,但真实的ping指令可远不止这么简单,感兴趣的读者可以通过访问Linux开源项目来了解真正的实现。

📜博客主页:主页
📫我的专栏:C++
📱我的github:github

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/705938.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Qt creator day1 练习

自由发挥登录窗口的应用场景&#xff0c;实现一个登录窗口界面&#xff0c;要求&#xff1a;第行代码都有注释 #include "mywidget.h"MyWidget::MyWidget(QWidget *parent): QWidget(parent) {this->setWindowTitle("贪玩蓝月——是兄弟就来砍我 登入&#…

[亲测好用]10个热门音频剪辑软件分享,快来看看你适合哪种

市面上的音频剪辑软件千千万&#xff0c;要想找到适合自己的音频剪辑软件的话&#xff0c;还是需要多多对比。今天小编总结了市面上比较热门的10款热门音频剪辑软件来进行对比测评&#xff0c;希望可以通过这篇文章可以帮助你找到适合自己的音频剪辑软件。 音频剪辑软件一&…

论文研读|以真实图像为参考依据的AIGC检测

前言&#xff1a;这篇文章介绍几篇AIGC检测的相关工作&#xff0c;其中前几篇文章是以真实图像的特征作为标准进行检测&#xff0c;最后一篇文章就当拓展一下知识边界吧&#xff5e; 目录 Detecting Generated Images by Real Images Only (202311 arXiv)Let Real Images be as…

高速直线导轨驱动与控制,精准稳定的运动核心元件

直线导轨在工业生产中&#xff0c;精度和稳定性是至关重要的。而在各种机械设备中&#xff0c;高精度直线导轨是提高设备运动控制精度和平稳性的核心部件&#xff0c;当我们考虑高速运动时&#xff0c;直线导轨的精度和稳定性是非常重要的因素。 直线导轨系统中如何确保高速运动…

美丽的拉萨,神奇的布达拉宫

原文链接&#xff1a;美丽的拉萨&#xff0c;神奇的布达拉宫 2022年11月30日&#xff0c;可能将成为一个改变人类历史的日子——美国人工智能开发机构OpenAI推出了聊天机器人ChatGPT-3.5&#xff0c;将人工智能的发展推向了一个新的高度。2023年11月7日&#xff0c;OpenAI首届…

Selenium - 启动后报org.openqa.selenium.InvalidArgumentException: invalid argument错

● 出现的异常&#xff1a; Build info: version: 3.141.59, revision: e82be7d358, time: 2018-11-14T08:25:48 System info: host: DESKTOP-H7TOMMO, ip: 192.168.64.1, os.name: Windows 10, os.arch: amd64, os.version: 10.0, java.version: 1.8.0_131 Driver info: dr…

git 常用操作指令

文章目录 git clonegit configgit addgit commitgit rmgit branch/checkoutgit pull/push git clone git clone 可以将一个远程 Git 仓库拷贝到本地&#xff0c;让自己能够查看该项目&#xff0c;或者进行修改。 拷贝项目命令格式如下&#xff1a;git clone [url] [url] 是你要…

解决IDEA报错Could not find resource mybatis-config.xml最全排错解决收录

解决IDEA报错:Could not find resource mybatis-config.xml最全排错解决收录 1.问题产生 迁移新项目的Java web开发测试数据库时IDEA爆Could not find resource mybatis-config.xml 这个错误表明Mybatis无法找到名为mybatis-config.xml的配置文件。 需要确保该文件存在于cla…

Python学习从0开始——Kaggle时间序列001

Python学习从0开始——Kaggle时间序列001 一、具有时间序列的线性回归1.时间序列2.时间序列线性回归1.时间步特征2.滞后特征 二、趋势1.介绍2.移动平均图3.设计趋向4.使用 三、季节性1.介绍2.季节图和季节指标季节性的指标 3.傅里叶特征和周期图用周期图选择傅里叶特征计算傅里…

等保三级怎么做,一文讲清楚

吉祥学安全知识星球&#x1f517;除了包含技术干货&#xff1a;《Java代码审计》《Web安全》《应急响应》《护网资料库》《网安面试指南》还包含了安全中常见的售前护网案例、售前方案、ppt等&#xff0c;同时也有面向学生的网络安全面试、护网面试等。 前面我们讲过等保二级方…

WARNING: pip is configured with locations that require TLS/SSL

在pycharm中运行pip下载软件包遇到该问题&#xff1a;WARNING: pip is configured with locations that require TLS/SSL, however the ssl module in Python is not available 原因&#xff1a;没有安装openssl&#xff1b; 到https://slproweb.com/products/Win32OpenSSL.ht…

数据结构之线性表(2)

顺序表中的动态存储 上文我们了解到了顺序表中的静态顺序表的相关操作&#xff0c;今天我们来学习动态顺序表的知识。 为什么会存在动态顺序表呢&#xff1f;&#xff1f; 原因&#xff1a;静态顺序表给定的数据容量固定&#xff0c;多了浪费&#xff0c;少了不够用。 首先我…

用【R语言】揭示大学生恋爱心理:【机器学习】与【深度学习】的案例深度解析

目录 第一部分&#xff1a;数据收集与预处理 1.1 数据来源 1.2 数据清洗 1.3 数据探索性分析 第二部分&#xff1a;特征工程与数据准备 2.1 特征选择 2.2 特征提取 第三部分&#xff1a;机器学习模型 3.1 逻辑回归模型 3.2 决策树模型 第四部分&#xff1a;深度学习…

~$开头的临时文件是什么?可以删除吗?

&#xff08;2023.12.4&#xff09; 在进行Word文档编辑的时候&#xff0c;都会产生一个以~$开头的临时文件&#xff0c;它会自动备份文档编辑内容&#xff0c;若是正常关闭程序&#xff0c;这个文档就会自动消失&#xff1b;而在非正常情况下关闭word文档&#xff0c;如断电&…

单片机使用循环来实现延时和定时器延时的区别是什么?

单片机&#xff0c;可以用循环延迟也可以用定时器。很多人说&#xff0c;唯一的区别浪费多少算力&#xff0c;话没毛病但不符合场景。只有追求实时性好&#xff0c;成本低的项目才会使用单片机。否则&#xff0c;我为什么不上一个资源更好&#xff0c;单位周期更快&#xff0c;…

被封号后,我终于明白免费代理的危害

在数字时代&#xff0c;网络已经成为人们日常生活和商业活动中不可或缺的一部分。为了实现更广阔的业务拓展和更畅通的网络体验&#xff0c;许多人开始考虑使用代理服务器。然而&#xff0c;虽然免费代理可能听起来像是个经济实惠的选择&#xff0c;但事实上&#xff0c;它可能…

考研计组chap2数据的表示和运算(补充)

一、进位计数制 1.r进制 第i位表示r进制的权为i 2.进制转换 &#xff08;1&#xff09;r->10 对应位置数*权值 &#xff08;2&#xff09;2 -> 16 or 8 每三位2进制数可表示1位16进制 每四位2进制数可表示1位16进制 so 分开之后转为16进制即可 eg&#xff1a;11…

qt仿制qq登录界面

#include "mainwindow.h"MainWindow::MainWindow(QWidget *parent): QMainWindow(parent) {// 设置窗口大小this->resize(window_width, window_heigth);// 固定窗口大小this->setFixedSize(window_width, window_heigth);// 设置窗口图标this->se…

Hot Sale | 澳鹏精品数据集火热来袭!

在人工智能项目需要快速启动时&#xff0c;成品数据集&#xff08;OTS / off-the-shelf datasets&#xff09;往往是许多AI团队的首选。 采用高质量、合规的成品数据集进行部署&#xff0c;不仅能够在速度至关重要的今天快人一步进入市场&#xff0c;更可以在预算有限的情况下…

【秋招突围】2024届秋招笔试-阿里系列笔试题-第一套-三语言题解(Java/Cpp/Python)

&#x1f36d; 大家好这里是清隆学长 &#xff0c;一枚热爱算法的程序员 ✨ 本系计划跟新各公司春秋招的笔试题 &#x1f4bb; ACM银牌&#x1f948;| 多次AK大厂笔试 &#xff5c; 编程一对一辅导 &#x1f44f; 感谢大家的订阅➕ 和 喜欢&#x1f497; &#x1f4e7; 清隆这边…