[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…...

抖音增长新引擎:品融电商,一站式全案代运营领跑者
抖音增长新引擎:品融电商,一站式全案代运营领跑者 在抖音这个日活超7亿的流量汪洋中,品牌如何破浪前行?自建团队成本高、效果难控;碎片化运营又难成合力——这正是许多企业面临的增长困局。品融电商以「抖音全案代运营…...

【快手拥抱开源】通过快手团队开源的 KwaiCoder-AutoThink-preview 解锁大语言模型的潜力
引言: 在人工智能快速发展的浪潮中,快手Kwaipilot团队推出的 KwaiCoder-AutoThink-preview 具有里程碑意义——这是首个公开的AutoThink大语言模型(LLM)。该模型代表着该领域的重大突破,通过独特方式融合思考与非思考…...

HBuilderX安装(uni-app和小程序开发)
下载HBuilderX 访问官方网站:https://www.dcloud.io/hbuilderx.html 根据您的操作系统选择合适版本: Windows版(推荐下载标准版) Windows系统安装步骤 运行安装程序: 双击下载的.exe安装文件 如果出现安全提示&…...

自然语言处理——Transformer
自然语言处理——Transformer 自注意力机制多头注意力机制Transformer 虽然循环神经网络可以对具有序列特性的数据非常有效,它能挖掘数据中的时序信息以及语义信息,但是它有一个很大的缺陷——很难并行化。 我们可以考虑用CNN来替代RNN,但是…...

vue3+vite项目中使用.env文件环境变量方法
vue3vite项目中使用.env文件环境变量方法 .env文件作用命名规则常用的配置项示例使用方法注意事项在vite.config.js文件中读取环境变量方法 .env文件作用 .env 文件用于定义环境变量,这些变量可以在项目中通过 import.meta.env 进行访问。Vite 会自动加载这些环境变…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...
Python+ZeroMQ实战:智能车辆状态监控与模拟模式自动切换
目录 关键点 技术实现1 技术实现2 摘要: 本文将介绍如何利用Python和ZeroMQ消息队列构建一个智能车辆状态监控系统。系统能够根据时间策略自动切换驾驶模式(自动驾驶、人工驾驶、远程驾驶、主动安全),并通过实时消息推送更新车…...
uniapp 集成腾讯云 IM 富媒体消息(地理位置/文件)
UniApp 集成腾讯云 IM 富媒体消息全攻略(地理位置/文件) 一、功能实现原理 腾讯云 IM 通过 消息扩展机制 支持富媒体类型,核心实现方式: 标准消息类型:直接使用 SDK 内置类型(文件、图片等)自…...

Matlab实现任意伪彩色图像可视化显示
Matlab实现任意伪彩色图像可视化显示 1、灰度原始图像2、RGB彩色原始图像 在科研研究中,如何展示好看的实验结果图像非常重要!!! 1、灰度原始图像 灰度图像每个像素点只有一个数值,代表该点的亮度(或…...
游戏开发中常见的战斗数值英文缩写对照表
游戏开发中常见的战斗数值英文缩写对照表 基础属性(Basic Attributes) 缩写英文全称中文释义常见使用场景HPHit Points / Health Points生命值角色生存状态MPMana Points / Magic Points魔法值技能释放资源SPStamina Points体力值动作消耗资源APAction…...