[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

相关文章:
[C++] 从零实现一个ping服务
💻文章目录 前言ICMP概念报文格式 Ping服务实现系统调用函数具体实现运行测试 总结 前言 ping命令,因为其简单、易用等特点,几乎所有的操作系统都内置了一个ping命令。如果你是一名C初学者,对网络编程、系统编程有所了解ÿ…...
2024网络安全学习路线 非常详细 推荐学习
关键词:网络安全入门、渗透测试学习、零基础学安全、网络安全学习路线 首先咱们聊聊,学习网络安全方向通常会有哪些问题 1、打基础时间太长 学基础花费很长时间,光语言都有几门,有些人会倒在学习 linux 系统及命令的路上&#…...
STM32F103ZET6_HAL_CAN
1定义时钟 2定义按键 按键上拉电阻 3开启串口 4打开CAN(具体什么意思上一篇讲了) 5生成代码 /* USER CODE BEGIN Header */ /********************************************************************************* file : main.c* brief …...
javaWeb项目-ssm+vue网上租车系统功能介绍
本项目源码:java-基于ssmvue的网上租车系统源码说明文档资料资源-CSDN文库 项目关键技术 开发工具:IDEA 、Eclipse 编程语言: Java 数据库: MySQL5.7 框架:ssm、Springboot 前端:Vue、ElementUI 关键技术:springboot、…...
Go模板页面浏览器显示HTML源码问题
<!--* Title: This is a file for ……* Author: JackieZheng* Date: 2024-06-09 17:00:01* LastEditTime: 2024-06-09 17:01:12* LastEditors: Please set LastEditors* Description:* FilePath: \\GoCode\\templates\\index.html --> <!DOCTYPE html> <html …...
弃用Docker Desktop:在WSL2中玩转Docker之Docker Engine 部署与WSL入门
Docker技术概论 在WSL2中玩转Docker之Docker Engine部署 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite:http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this article:https://bl…...
Mac下载了docker,在终端使用docker命令时用不了
问题:在mac使用docker的时候,拉取docker镜像失败 原因:docker是需要用app使用的 ,所以在使用的时候必须打开这个桌面端软件才可以在终端上使用docker命令!!!...
Spring Security——基于MyBatis
目录 项目总结 新建一个项目 pom.xml application.properties配置文件 User实体类 UserMapper映射接口 UserService访问数据库中的用户信息 WebSecurityConfig配置类 MyAuthenticationFailureHandler登录失败后 MyAuthenticationSuccessHandlerw登录成功后 WebSecur…...
Qt——升级系列(Level Four):控件概述、QWidget 核心属性、按钮类控件
目录 控件概述 QWidget 核心属性 核心属性概览 enabled geometry windowTitle windowIcon windowOpacity cursor font toolTip focusPolicy styleSheet 按钮类控件 Push Button Radio Buttion Check Box Tool Button 控件概述 Widget 是 Qt 中的核⼼概念. 英⽂原义是 "…...
品质卓越为你打造App UI 风格
品质卓越为你打造App UI 风格...
ei期刊和sci期刊的区别
ei期刊和sci期刊的区别 ei期刊和sci期刊的区别是什么?Sci和ei都属于国际期刊的一种,但是二者之间存在一些区别,选择期刊投稿时需要注意这些区别。EI期刊刊物的审查周期短,SCI学术期刊的审查期长。难度要求不同,SCI期刊比EI期刊对…...
从零手写实现 nginx-20-placeholder 占位符 $
前言 大家好,我是老马。很高兴遇到你。 我们为 java 开发者实现了 java 版本的 nginx https://github.com/houbb/nginx4j 如果你想知道 servlet 如何处理的,可以参考我的另一个项目: 手写从零实现简易版 tomcat minicat 手写 nginx 系列 …...
leetcode290:单词规律
题目链接:290. 单词规律 - 力扣(LeetCode) class Solution { public:bool wordPattern(string pattern, string s) {unordered_map<char, string> s2t;unordered_map<string, char> t2s;int len pattern.size();int CountSpace…...
IDEA 2022
介绍 【尚硅谷IDEA安装idea实战教程(百万播放,新版来袭)】 jetbrains 中文官网 IDEA 官网 IDEA 从 IDEA 2022.1 版本开始支持 JDK 17,也就是说如果想要使用 JDK 17,那么就要下载 IDEA 2022.1 或之后的版本。 公司…...
Vue TypeScript 实战:掌握静态类型编程
title: Vue TypeScript 实战:掌握静态类型编程 date: 2024/6/10 updated: 2024/6/10 excerpt: 这篇文章介绍了如何在TypeScript环境下为Vue.js应用搭建项目结构,包括初始化配置、创建Vue组件、实现状态管理利用Vuex、配置路由以及性能优化的方法&#x…...
Hudi extraMetadata 研究总结
前言 研究总结 Hudi extraMetadata ,记录研究过程。主要目的是通过 extraMetadata 保存 source 表的 commitTime (checkpoint), 来实现增量读Hudi表写Hudi表时,保存增量读状态的事务性,实现类似于流任务中的 exactly-once 背景需求 有个需求:增量读Hudi表关联其他Hudi…...
Vue31-自定义指令:总结
一、自定义函数的陷阱 1-1、自定义函数名 自定义函数名,不能用驼峰式!!! 示例1: 示例2: 1-2、指令回调函数的this 【回顾】: 所有由vue管理的函数,里面的this直接就是vm实例对象。…...
Windows环境如何使用Flutter Version Manager (fvm)
Windows环境如何使用Flutter Version Manager (fvm) Flutter Version Manager (fvm) 是一个用于管理多个 Flutter SDK 版本的命令行工具,它允许开发者在不同项目之间轻松切换 Flutter 版本。这对于需要维护多个使用不同 Flutter 版本的项目的开发人员来说非常有用。…...
优化Elasticsearch搜索性能:查询调优与索引设计
在构建基于 Elasticsearch 的搜索解决方案时,性能优化是关键。本文将深入探讨如何通过查询调优和索引设计来优化 Elasticsearch 的搜索性能,从而提高用户体验和系统效率。 查询调优 优化查询是提高 Elasticsearch 性能的重要方法。以下是一些有效的查询…...
STM32-17-DAC
STM32-01-认识单片机 STM32-02-基础知识 STM32-03-HAL库 STM32-04-时钟树 STM32-05-SYSTEM文件夹 STM32-06-GPIO STM32-07-外部中断 STM32-08-串口 STM32-09-IWDG和WWDG STM32-10-定时器 STM32-11-电容触摸按键 STM32-12-OLED模块 STM32-13-MPU STM32-14-FSMC_LCD STM32-15-DMA…...
docker详细操作--未完待续
docker介绍 docker官网: Docker:加速容器应用程序开发 harbor官网:Harbor - Harbor 中文 使用docker加速器: Docker镜像极速下载服务 - 毫秒镜像 是什么 Docker 是一种开源的容器化平台,用于将应用程序及其依赖项(如库、运行时环…...
Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以?
Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以? 在 Golang 的面试中,map 类型的使用是一个常见的考点,其中对 key 类型的合法性 是一道常被提及的基础却很容易被忽视的问题。本文将带你深入理解 Golang 中…...
MySQL 8.0 OCP 英文题库解析(十三)
Oracle 为庆祝 MySQL 30 周年,截止到 2025.07.31 之前。所有人均可以免费考取原价245美元的MySQL OCP 认证。 从今天开始,将英文题库免费公布出来,并进行解析,帮助大家在一个月之内轻松通过OCP认证。 本期公布试题111~120 试题1…...
Linux C语言网络编程详细入门教程:如何一步步实现TCP服务端与客户端通信
文章目录 Linux C语言网络编程详细入门教程:如何一步步实现TCP服务端与客户端通信前言一、网络通信基础概念二、服务端与客户端的完整流程图解三、每一步的详细讲解和代码示例1. 创建Socket(服务端和客户端都要)2. 绑定本地地址和端口&#x…...
七、数据库的完整性
七、数据库的完整性 主要内容 7.1 数据库的完整性概述 7.2 实体完整性 7.3 参照完整性 7.4 用户定义的完整性 7.5 触发器 7.6 SQL Server中数据库完整性的实现 7.7 小结 7.1 数据库的完整性概述 数据库完整性的含义 正确性 指数据的合法性 有效性 指数据是否属于所定…...
uniapp 开发ios, xcode 提交app store connect 和 testflight内测
uniapp 中配置 配置manifest 文档:manifest.json 应用配置 | uni-app官网 hbuilderx中本地打包 下载IOS最新SDK 开发环境 | uni小程序SDK hbulderx 版本号:4.66 对应的sdk版本 4.66 两者必须一致 本地打包的资源导入到SDK 导入资源 | uni小程序SDK …...
FFmpeg:Windows系统小白安装及其使用
一、安装 1.访问官网 Download FFmpeg 2.点击版本目录 3.选择版本点击安装 注意这里选择的是【release buids】,注意左上角标题 例如我安装在目录 F:\FFmpeg 4.解压 5.添加环境变量 把你解压后的bin目录(即exe所在文件夹)加入系统变量…...
毫米波雷达基础理论(3D+4D)
3D、4D毫米波雷达基础知识及厂商选型 PreView : https://mp.weixin.qq.com/s/bQkju4r6med7I3TBGJI_bQ 1. FMCW毫米波雷达基础知识 主要参考博文: 一文入门汽车毫米波雷达基本原理 :https://mp.weixin.qq.com/s/_EN7A5lKcz2Eh8dLnjE19w 毫米波雷达基础…...
wpf在image控件上快速显示内存图像
wpf在image控件上快速显示内存图像https://www.cnblogs.com/haodafeng/p/10431387.html 如果你在寻找能够快速在image控件刷新大图像(比如分辨率3000*3000的图像)的办法,尤其是想把内存中的裸数据(只有图像的数据,不包…...
基于开源AI智能名片链动2 + 1模式S2B2C商城小程序的沉浸式体验营销研究
摘要:在消费市场竞争日益激烈的当下,传统体验营销方式存在诸多局限。本文聚焦开源AI智能名片链动2 1模式S2B2C商城小程序,探讨其在沉浸式体验营销中的应用。通过对比传统品鉴、工厂参观等初级体验方式,分析沉浸式体验的优势与价值…...
