[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_lifetimeunion{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_inchar            *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为IPV4if(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_inmemcpy(&_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();     // 获取时间戳, 8bitmemcpy(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.cppPing.hpp
)

运行结果:

在这里插入图片描述

总结

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

学会python——读取大文本文件(python实例六)

目录 1、认识Python 2、环境与工具 2.1 python环境 2.2 Visual Studio Code编译 3、读取大文本文件 3.1 代码构思 3.2 代码示例 3.3 运行结果 4、总结 1、认识Python Python 是一个高层次的结合了解释性、编译性、互动性和面向对象的脚本语言。 Python 的设计具有很强…

电脑内存怎么看?5个秘诀,轻松查看内存!

“新买了一台电脑&#xff0c;想查看一下我电脑的内存&#xff0c;大家可以分享一下查看方法吗&#xff1f;” 当我们谈论电脑的性能时&#xff0c;内存无疑是一个不容忽视的关键组件。然而&#xff0c;对于许多普通用户来说&#xff0c;如何查看电脑内存的大小、类型以及使用情…

跳舞电动机器人单片机方案

这款机器人形状智能电子玩具是一款集娱乐、教育和互动于一身的高科技产品。它的主要功能包括&#xff1a; 1、智能对话&#xff1a;机器人可以进行简单的对话&#xff0c;回答用户的问题&#xff0c;提供有趣的互动体验。 2、前进、后退、左转、右转、滑行&#xff1a;机器人…

BERT报错记录

一、加载数据集下载失败 报错&#xff1a; TimeoutError: [WinError 10060] 由于连接方在一段时间后没有正确答复或连接的主机没有反应&#xff0c;连接尝试失败。urllib3.exceptions.NewConnectionError: <urllib3.connection.HTTPSConnection object at 0x00000241F9AD4…

Element UI 一键校验多表单(v-for循环表单,异步校验规则,v-for 中的 ref 属性,避坑 forEach 不支持异步 await )

需求描述 表单为数组 v-for 循环得到的多表单&#xff0c;如可自由增删的动态表单表单中存在异步校验规则&#xff0c;如姓名需访问接口校验是否已存在点击提交按钮&#xff0c;需一键校验所有表单&#xff0c;仅当所有表单都通过校验&#xff0c;才能最终提交到后台 效果预览 …

亚马逊新店如何实现高效流量转化?自养号测评深度解析与实用策略

在亚马逊平台上&#xff0c;自养号测评是一种通过卖家自行控制的海外买家账号对商品进行评价的方法&#xff0c;旨在提高商品的排名和流量。 亚马逊的自养号测评是指卖家通过使用在海外真实环境注册的买家账号&#xff0c;代替真实买家对商品进行测评。账号由卖家自己管理&…

电子传真怎么在国产系统上使用?一文看懂网络传真信创方案

国产化浪潮正在逐步深入&#xff0c;越来越多的企业开始关注如何在国产系统上高效、安全地使用办公软件&#xff0c;电子传真系统也不例外。 作为网络电子传真领域的重要品牌&#xff0c;EastFax也对原Windows电子传真系统进行了信创改造&#xff0c;全面支持国产化操作系统、…

串扰(一)

一、说明 串扰应该算比较常见的信号完整性问题了&#xff0c;一般是指由于走线较近&#xff0c;传输信号时在临线上产生耦合噪声的现象。串扰的原因是由于电场和磁场的耦合&#xff0c;我们经常用耦合电容和耦合电感模型进行问题分析。 本文是基于被攻击线阻抗匹配的情形下计…

Vatee万腾平台,让智能更懂你

在数字化浪潮席卷全球的今天&#xff0c;智能科技已经渗透到我们生活的方方面面。然而&#xff0c;真正的智能不仅仅是技术的堆砌&#xff0c;更是对人性需求的深刻理解和满足。Vatee万腾平台&#xff0c;正是这样一个让智能更懂你的平台&#xff0c;它以其独特的方式&#xff…

PyCharm配置教程,手把手教你如何配置

文章目录 引言1. 安装 PyCharm1.1 下载和安装1.2 初次启动 2. 基本配置2.1 设置界面2.2 常用配置项 3. 项目配置3.1 创建新项目3.2 配置解释器 4. 虚拟环境配置4.1 创建虚拟环境4.2 使用已有虚拟环境4.3 管理依赖 5. 插件和扩展5.1 安装插件5.2 推荐插件 6. 调试配置6.1 配置调…

【计算机毕业设计】234基于微信小程序的中国各地美食推荐平台

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

可穿戴设备:苹果“吃老底”、华为“忙复苏”、小米“再扩容”

配图来自Canva可画 随着产品功能的创新&#xff0c;可穿戴设备不再被简单地视为手机的延伸&#xff0c;而是被当成一种独立的、具有独特功能和优势的产品&#xff0c;受到了越来越多人的青睐。 一方面&#xff0c;技术的进步使得可穿戴设备在功能、性能和使用体验上得到显著提…

Golang | Leetcode Golang题解之第160题相交链表

题目&#xff1a; 题解&#xff1a; func getIntersectionNode(headA, headB *ListNode) *ListNode {if headA nil || headB nil {return nil}pa, pb : headA, headBfor pa ! pb {if pa nil {pa headB} else {pa pa.Next}if pb nil {pb headA} else {pb pb.Next}}retu…

IDEA快速入门03-代码头统一配置

三、代码规范配置 3.1 文件头和作者信息 配置入口&#xff1a;依次打开 File -> Settings -> Editor -> File and Code Templates。 Class /*** Copyright (C) 2020-${YEAR}, Glodon Digital Supplier & Purchaser BU.* * All Rights Reserved.*/ #if (${PACKA…

pdf只要其中一页,pdf只要其中几页怎么弄

在现代办公和学习环境中&#xff0c;pdf文件因其跨平台、保持原样等优点而被广泛使用。然而&#xff0c;有时我们需要一个pdf其中页或其中几页&#xff0c;以便更好地管理和使用其中的内容。本文将详细介绍几种拆分pdf文件的方法&#xff0c;帮助您轻松应对各种拆分需求。 打开…

开源项目推荐

这个资源列表集合了.NET开发领域的优秀工具、库、框架和软件等&#xff0c; 如果您目前研究开源大模型项目&#xff0c;请参考热门开源大模型项目推荐链接如下&#xff1a;https://blog.csdn.net/hefeng_aspnet/article/details/139669116 欢迎各位小伙伴收藏、点赞、留言、评论…

请不要自嗨,B端系统颜值不过关,功能再强大可能等于0

我们见过形形色色的系统&#xff0c;有不少老铁费劲开发出来的管理系统&#xff0c;输在了颜值上。商务人员觉得么有信心&#xff0c;就不敢推荐&#xff0c;客户中间人觉得拿不出手&#xff0c;就不会向上层重点推荐&#xff0c;有时候即便客户购买了&#xff0c;也是被客户的…

【Linux】使用 iptables 验证访问HDFS 所使用到的端口

目录 ​编辑 一、实操背景 二、iptables 简介 三、模拟操作 一、实操背景 背景&#xff1a; 在客户有外网的服务器需要访问内网大数据集群HDFS&#xff0c;使用iptable模拟测试需要开放的端口。 二、iptables 简介 具体介绍看文章&#xff1a; 【Linux】Iptables 详解与实战…

安全生产管理系统——特殊作业管控

特殊作业环节面临事故多发、频发、死亡率高&#xff0c;如何做到有效管理是一大考验&#xff0c;进行系统全面的规整很有必要。安全生产管理系统中特殊作业管理通过整合资源和采用信息化技术对动火、受限空间、盲板抽堵、高处、吊装、临时用电、动土、断路等特殊作业全过程管理…

让UI和前端相亲相爱的秘诀:与人方便,自己方便。

在实际项目开发中&#xff0c;UI设计师和前端开发人员经常互撕&#xff0c;这背后的原因是什么&#xff1f;有什么办法可以避么&#xff1f;贝格前端工场在这里为大家解读一下。 一、UI和前端互厮的原因有哪些&#xff1f; 导致UI和前端在写作过程中出现盲区导致互撕的原因可…