【Linux】34.封装 UdpSocket(1)

embedded/2025/2/27 2:35:07/

文章目录

  • 1. 实现一个简易的远程命令执行系统
    • 1.1 日志系统 (Log.hpp)
    • 1.2 UDP客户端 (UdpClient.cc)
    • 1.3 UDP服务器 (UdpServer.hpp)
    • 1.4 主程序 (main.c)


1. 实现一个简易的远程命令执行系统

1.1 日志系统 (Log.hpp)

Log.hpp

#pragma once  // 防止头文件重复包含// 必要的头文件包含
#include <iostream>     // 标准输入输出流
#include <time.h>      // 时间相关函数
#include <stdarg.h>    // 可变参数函数支持
#include <sys/types.h> // 系统调用相关类型
#include <sys/stat.h>  // 文件状态
#include <fcntl.h>     // 文件控制选项
#include <unistd.h>    // UNIX标准函数
#include <stdlib.h>    // 标准库函数#define SIZE 1024  // 缓冲区大小// 日志级别定义
#define Info 0     // 信息
#define Debug 1    // 调试
#define Warning 2  // 警告
#define Error 3    // 错误
#define Fatal 4    // 致命错误// 日志输出方式定义
#define Screen 1     // 输出到屏幕
#define Onefile 2    // 输出到单个文件
#define Classfile 3  // 按日志级别分类输出到不同文件#define LogFile "log.txt"  // 默认日志文件名class Log
{
public:// 构造函数:初始化输出方式为屏幕输出,设置默认日志路径Log(){// 初始化日志输出方式为屏幕输出(Screen=1)// Screen:直接输出到终端屏幕// Onefile:输出到单个日志文件// Classfile:根据日志级别输出到不同文件printMethod = Screen;  // 设置日志文件存放的默认路径为当前目录下的log子目录// 注意:使用前需要确保该目录存在,否则写入文件会失败path = "./log/";      }// 设置日志输出方式的方法void Enable(int method){// 通过传入不同的参数来修改日志的输出方式:// method可以是:// Screen(1) - 输出到屏幕// Onefile(2) - 输出到单个文件// Classfile(3) - 按日志级别分类输出到不同文件printMethod = method;}// 将日志级别转换为对应的字符串std::string levelToString(int level){switch (level){case Info:    return "Info";case Debug:   return "Debug";case Warning: return "Warning";case Error:   return "Error";case Fatal:   return "Fatal";default:      return "None";}}// 根据不同的输出方式打印日志void printLog(int level, const std::string &logtxt){switch (printMethod){case Screen:     // 输出到屏幕std::cout << logtxt << std::endl;break;case Onefile:    // 输出到单个文件printOneFile(LogFile, logtxt);break;case Classfile:  // 按级别输出到不同文件printClassFile(level, logtxt);break;default:break;}}// 将日志写入指定文件void printOneFile(const std::string &logname, const std::string &logtxt){std::string _logname = path + logname;// 打开文件,使用以下标志:// O_WRONLY: 只写模式// O_CREAT: 如果文件不存在则创建// O_APPEND: 追加写入,新内容添加到文件末尾// 0666: 文件权限(rw-rw-rw-)//fd用来标识一个打开的文件int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);if (fd < 0) // 如果打开文件失败(fd<0),直接返回return;// 将日志内容写入文件// logtxt.c_str(): 获取日志内容的C风格字符串// logtxt.size(): 获取日志内容的长度write(fd, logtxt.c_str(), logtxt.size());  // 使用fd写入文件close(fd);  // 使用fd关闭文件}// 根据日志级别将日志写入对应的文件void printClassFile(int level, const std::string &logtxt){std::string filename = LogFile;filename += ".";filename += levelToString(level);  // 例如: "log.txt.Debug"printOneFile(filename, logtxt);}~Log(){}// 重载operator()函数,实现日志打印功能// level: 日志级别// format: 格式化字符串// ...: 可变参数列表void operator()(int level, const char *format, ...){// 1. 构造日志的左半部分:时间戳和日志级别time_t t = time(nullptr);          // 获取当前时间戳struct tm *ctime = localtime(&t);  // 转换为本地时间char leftbuffer[SIZE];             // 存储左半部分的缓冲区// 格式化左半部分:[级别][年-月-日 时:分:秒]/*int snprintf(char *buffer, size_t size, const char *format, ...);参数说明:buffer:输出缓冲区,用于存储格式化后的字符串size:缓冲区大小(字节数),包括结尾的空字符'\0'format:格式化字符串...:可变参数列表*/snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),  // 日志级别转字符串ctime->tm_year + 1900,         // 年(需要加1900)ctime->tm_mon + 1,             // 月(需要加1)ctime->tm_mday,                // 日ctime->tm_hour,                // 时ctime->tm_min,                 // 分ctime->tm_sec);                // 秒// 2. 处理可变参数部分(日志内容)va_list s;                        // 定义可变参数列表/*va_start 是一个宏,用来初始化 va_list 类型的变量,使其指向可变参数列表的第一个参数。void va_start(va_list ap, last_arg);参数:ap: va_list类型的变量last_arg: 最后一个固定参数的名字*/va_start(s, format);              // 初始化可变参数列表char rightbuffer[SIZE];           // 存储右半部分的缓冲区/*vsnprintf用于格式化字符串int vsnprintf(char *buffer, size_t size, const char *format, va_list args);参数说明:buffer:输出缓冲区,存储格式化后的字符串size:缓冲区大小(字节数),包括结尾的'\0'format:格式化字符串args:va_list类型的可变参数列表*/vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);  // 格式化可变参数va_end(s);                        // 清理可变参数列表//vsnprintf 在执行时会将格式化后的结果存储在 rightbuffer 中,va_end(s) 只是清理 va_list 的状态,不会影响已经格式化好的字符串。// 3. 组合完整的日志信息char logtxt[SIZE * 2];            // 存储完整日志的缓冲区snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);  // 合并左右部分// 4. 调用printLog函数输出日志printLog(level, logtxt);}private:int printMethod;      // 日志输出方式std::string path;     // 日志文件路径
};/* 注释掉的可变参数示例函数
int sum(int n, ...)
{va_list s;           // 定义可变参数列表va_start(s, n);      // 初始化可变参数列表int sum = 0;while(n){sum += va_arg(s, int);  // 依次获取参数n--;}va_end(s);          // 清理可变参数列表return sum;
}
*/

在这段UDP客户端代码中,套接字的使用主要体现在以下几个步骤:

  1. 创建套接字:
// 创建UDP套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// AF_INET: IPv4协议族
// SOCK_DGRAM: UDP数据报套接字
// 0: 使用默认协议
  1. 使用套接字发送数据:
// 发送数据到服务器
sendto(sockfd,                           // 套接字描述符message.c_str(),                  // 要发送的数据message.size(),                   // 数据长度0,                               // 标志位(struct sockaddr *)&server,      // 目标服务器地址len);                            // 地址结构长度
  1. 使用套接字接收数据:
// 接收服务器响应
struct sockaddr_in temp;     // 存储发送方地址
socklen_t len = sizeof(temp);ssize_t s = recvfrom(sockfd,            // 套接字描述符buffer,            // 接收缓冲区1023,             // 缓冲区大小0,                // 标志位(struct sockaddr*)&temp,  // 发送方地址&len);            // 地址结构长度
  1. 完整的通信流程示例:
int main() {// 1. 创建套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0) {cerr << "socket creation failed" << endl;return 1;}// 2. 准备服务器地址struct sockaddr_in server_addr;server_addr.sin_family = AF_INET;server_addr.sin_port = htons(8080);server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");// 3. 发送数据string msg = "Hello Server";sendto(sockfd, msg.c_str(), msg.size(), 0,(struct sockaddr*)&server_addr, sizeof(server_addr));// 4. 接收响应char buffer[1024];struct sockaddr_in sender_addr;socklen_t sender_len = sizeof(sender_addr);ssize_t recv_len = recvfrom(sockfd, buffer, 1024, 0,(struct sockaddr*)&sender_addr, &sender_len);if (recv_len > 0) {buffer[recv_len] = '\0';cout << "Received: " << buffer << endl;}// 5. 关闭套接字close(sockfd);return 0;
}
  1. 错误处理示例:
// 创建套接字时的错误处理
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {cerr << "Socket creation failed: " << strerror(errno) << endl;return 1;
}// 发送数据时的错误处理
ssize_t sent = sendto(sockfd, msg.c_str(), msg.size(), 0,(struct sockaddr*)&server_addr, sizeof(server_addr));
if (sent < 0) {cerr << "Send failed: " << strerror(errno) << endl;return 1;
}// 接收数据时的错误处理
ssize_t recv_len = recvfrom(sockfd, buffer, 1024, 0,(struct sockaddr*)&sender_addr, &sender_len);
if (recv_len < 0) {cerr << "Receive failed: " << strerror(errno) << endl;return 1;
}

关键点:

  1. UDP是无连接的,不需要建立连接就可以直接发送数据
  2. 每次发送/接收都需要指定目标/来源地址
  3. UDP不保证数据的可靠传输
  4. 需要正确处理发送和接收可能出现的错误
  5. 记得在程序结束时关闭套接字

1.2 UDP客户端 (UdpClient.cc)

UdpClient.cc

// 必要的头文件包含
#include <iostream>      // 标准输入输出
#include <cstdlib>      // 标准库函数
#include <unistd.h>     // UNIX标准函数
#include <strings.h>    // 字符串操作函数
#include <sys/types.h>  // 基本系统数据类型
#include <sys/socket.h> // 套接字接口
#include <netinet/in.h> // Internet地址族
#include <arpa/inet.h>  // IP地址转换函数using namespace std;// 打印使用说明函数
void Usage(std::string proc)
{// 告诉用户正确的命令行参数格式:程序名 服务器IP 服务器端口std::cout << "\n\rUsage: " << proc << " serverip serverport\n"<< std::endl;
}// ./udpclient serverip serverport
int main(int argc, char *argv[])
{// 检查命令行参数数量是否正确if (argc != 3){Usage(argv[0]);exit(0);}// 获取服务器IP和端口信息std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);  // 字符串转整数// 配置服务器地址结构struct sockaddr_in server;bzero(&server, sizeof(server));            // 清零地址结构server.sin_family = AF_INET;               // 使用IPv4地址族server.sin_port = htons(serverport);       // 将端口转换为网络字节序server.sin_addr.s_addr = inet_addr(serverip.c_str());  // 将IP转换为网络字节序socklen_t len = sizeof(server);            // 地址结构长度// 创建UDP套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){cout << "socker error" << endl;return 1;}/* 关于客户端绑定的说明:// client 要bind吗?要!只不过不需要用户显示的bind!一般由OS自动随机选择!// 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!// 其实client的port是多少其实不重要,只要能保证主机上的唯一性就可以!// 系统会在首次发送数据的时候自动完成bind操作*/string message;        // 用户输入的消息char buffer[1024];    // 接收服务器响应的缓冲区// 主循环while (true){// 获取用户输入cout << "Please Enter@ ";getline(cin, message);// 发送数据到服务器// 参数:套接字、数据、数据长度、标志位、目标地址结构、地址结构长度sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, len);// 接收服务器响应struct sockaddr_in temp;     // 用于存储响应方的地址信息socklen_t len = sizeof(temp);// 接收数据// 参数:套接字、缓冲区、缓冲区大小、标志位、发送方地址结构、地址结构长度ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);if(s > 0)  // 如果成功接收到数据{buffer[s] = 0;  // 添加字符串结束符cout << buffer << endl;  // 打印服务器响应}}// 关闭套接字close(sockfd);return 0;
}

1.3 UDP服务器 (UdpServer.hpp)

UdpServer.hpp

#pragma once  // 防止头文件重复包含// 必要的头文件包含
#include <iostream>     // 标准输入输出
#include <string>       // 字符串类
#include <strings.h>    // bzero等字符串操作
#include <cstring>      // C风格字符串操作
#include <sys/types.h>  // 基本系统数据类型
#include <sys/socket.h> // 套接字接口
#include <netinet/in.h> // Internet地址族
#include <arpa/inet.h>  // IP地址转换函数
#include <functional>   // std::function
#include "Log.hpp"      // 日志类// 定义回调函数类型:接收一个string参数,返回一个string
// using func_t = std::function<std::string(const std::string&)>;
typedef std::function<std::string(const std::string&)> func_t;
//      |              |            |                    |                  
//      |              |            |                    └─ 新的类型名
//      |              |            └─ 函数参数类型         
//      |              └─ 函数返回值类型
//      └─ 函数包装器Log lg;  // 全局日志对象// 错误码枚举
enum{SOCKET_ERR=1,  // 套接字创建错误BIND_ERR       // 绑定错误
};// 默认配置
uint16_t defaultport = 8080;              // 默认端口号
std::string defaultip = "0.0.0.0";        // 默认IP地址(监听所有网卡)
const int size = 1024;                    // 缓冲区大小class UdpServer{
public:// 构造函数:初始化服务器参数UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip):sockfd_(0), port_(port), ip_(ip), isrunning_(false){}// 初始化服务器void Init(){// 1. 创建UDP套接字sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET: IPv4协议族, SOCK_DGRAM: UDP数据报套接字if(sockfd_ < 0){lg(Fatal, "socket create error, sockfd: %d", sockfd_); // 记录致命错误日志exit(SOCKET_ERR);}// 记录信息级别日志,显示创建成功的套接字描述符lg(Info, "socket create success, sockfd: %d", sockfd_);// 2. 绑定套接字到指定地址和端口//struct sockaddr_in 是用于IPv4地址的结构体struct sockaddr_in local;                    // 本地地址结构bzero(&local, sizeof(local));               // 清零地址结构local.sin_family = AF_INET;                 // 使用IPv4地址族local.sin_port = htons(port_);              // 将端口号转换为网络字节序local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 将IP地址转换为网络字节序// local.sin_addr.s_addr = htonl(INADDR_ANY);   // 替代方案:监听所有网卡// 绑定套接字if(bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) < 0){lg(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));exit(BIND_ERR);}lg(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno));}// 运行服务器主循环void Run(func_t func) // 使用回调函数处理请求{isrunning_ = true;char inbuffer[size];  // 接收数据的缓冲区while(isrunning_){struct sockaddr_in client;         // 客户端地址结构socklen_t len = sizeof(client);    // 地址结构长度// 接收数据ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);if(n < 0){lg(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));continue;}inbuffer[n] = 0;  // 字符串结束符// 处理请求并发送响应std::string info = inbuffer;std::string echo_string = func(info);  // 调用回调函数处理请求sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (const sockaddr*)&client, len); // 发送响应}}// 析构函数:清理资源~UdpServer(){if(sockfd_ > 0) close(sockfd_);}private:int sockfd_;     // 网络套接字文件描述符std::string ip_; // 服务器IP地址uint16_t port_;  // 服务器端口号bool isrunning_; // 服务器运行状态标志
};

1.4 主程序 (main.c)

main.c

#include "UdpServer.hpp"  // 包含UDP服务器类的头文件
#include <memory>         // 智能指针
#include <cstdio>         // 标准输入输出// 打印使用说明函数
void Usage(std::string proc)
{// 告诉用户如何正确使用程序,要求输入大于1024的端口号std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}// 消息处理函数,处理接收到的字符串
std::string Handler(const std::string &str)
{// 构建响应消息std::string res = "Server get a message: ";res += str;std::cout << res << std::endl;return res;
}// 执行shell命令并获取执行结果的函数
std::string ExcuteCommand(const std::string &cmd)
{// TODO: 添加安全检查机制// SafeCheck(cmd);// popen()创建管道,执行命令,并返回文件指针// "r"表示我们要读取命令的输出FILE *fp = popen(cmd.c_str(), "r");if(nullptr == fp){perror("popen");  // 如果popen失败,打印错误信息return "error";}// 读取命令执行结果std::string result;char buffer[4096];  // 临时缓冲区while(true){// 从管道读取数据到缓冲区char *ok = fgets(buffer, sizeof(buffer), fp);if(ok == nullptr) break;  // 如果读取完毕或出错,退出循环result += buffer;         // 将读取的数据追加到结果字符串}pclose(fp);  // 关闭管道return result;
}// 主函数
// ./udpserver port
int main(int argc, char *argv[])
{// 检查命令行参数数量是否正确if(argc != 2){Usage(argv[0]);  // 如果参数数量不对,打印使用说明exit(0);         // 退出程序}// 将命令行参数(端口号)转换为整数uint16_t port = std::stoi(argv[1]);// 创建UDP服务器对象,使用智能指针管理std::unique_ptr<UdpServer> svr(new UdpServer(port));// 初始化服务器svr->Init(/**/);// 运行服务器,传入命令执行函数作为回调svr->Run(ExcuteCommand);return 0;
}

这是一个基于UDP协议的远程命令执行系统,主要包含以下组件:

  1. 日志系统 (Log.hpp)

    • 支持多种日志级别(Info、Debug、Warning、Error、Fatal)

    • 可以选择日志输出方式(屏幕、单文件、分类文件)

    • 记录带时间戳的日志信息

  2. UDP服务器 (UdpServer.hpp)

    • 创建UDP套接字监听指定端口

    • 接收客户端请求

    • 通过回调函数处理请求并返回结果

  3. UDP客户端 (UdpClient.cc)

  4. 主程序 (main.c)

    • 初始化并启动UDP服务器

    • 实现命令执行功能(ExcuteCommand函数)

    • 将客户端发来的命令在服务器端执行,并将执行结果返回给客户端

工作流程:

  1. 客户端输入命令
  2. 通过UDP发送到服务器
  3. 服务器接收命令并在本地执行
  4. 将执行结果返回给客户端
  5. 客户端显示结果

也就是说main.c运行后创建服务器端,客户端运行可以和这个服务器端通信。

这实际上是一个简单的远程命令执行系统,允许客户端远程在服务器上执行命令并获取结果。不过需要注意,当前实现没有加入安全机制(如身份验证、命令过滤等),在实际使用中需要添加相应的安全措施。


http://www.ppmy.cn/embedded/167425.html

相关文章

存储产品和数据库产品之间有没有竞争关系

互联网各领域资料分享专区(不定期更新): Sheet 前言 存储产品通常指用于数据存储的硬件或软件解决方案,比如硬盘、NAS、SAN,或者云存储服务如Amazon S3、阿里云OSS。它们主要关注数据的持久化、可扩展性、可靠性和访问速度,但可能不提供复杂的数据处理功能。数据库产品则是…

功能测试-黑盒测试

黑盒测试是一种功能测试方法&#xff0c;它将软件视为一个“黑盒”&#xff0c;即测试人员不关心软件的内部结构和实现&#xff0c;细节只关注软件的输入和输出是否符合预期。以下是黑盒测试方法的详细解释&#xff1a; 1. 黑盒测试的核心理念 黑盒测试的核心在于验证软件的功…

HTML Application(hta)入门教程

简介 HTA是HTML Application的缩写&#xff0c;又称为HTML应用程序。 hta是一个可执行文件&#xff0c;双击可以直接运行 hta与html非常相似&#xff0c;可直接将文件后缀改为.hta来获得HTA格式的文件。 支持VBS和JavaScript html的权限被限制在网页浏览器内&#xff0c;只有操…

SOME/IP-SD -- 协议英文原文讲解5

前言 SOME/IP协议越来越多的用于汽车电子行业中&#xff0c;关于协议详细完全的中文资料却没有&#xff0c;所以我将结合工作经验并对照英文原版协议做一系列的文章。基本分三大块&#xff1a; 1. SOME/IP协议讲解 2. SOME/IP-SD协议讲解 3. python/C举例调试讲解 5.1.2.5 S…

抓包工具 wireshark

1.什么是抓包工具 抓包工具是什么&#xff1f;-CSDN博客 2.wireshark的安装 【抓包工具】win 10 / win 11&#xff1a;WireShark 下载、安装、使用_windows抓包工具-CSDN博客 3.wireshark的基础操作 Wireshark零基础使用教程&#xff08;超详细&#xff09; - 元宇宙-Meta…

安宝特科技 | Vuzix Z100智能眼镜+AugmentOS:重新定义AI可穿戴设备的未来——从操作系统到硬件生态,如何掀起无感智能革命?

一、AugmentOS&#xff1a;AI可穿戴的“操作系统革命” 2025年2月3日&#xff0c;Vuzix与AI人机交互团队Mentra联合推出的AugmentOS&#xff0c;被业内视为智能眼镜领域的“iOS时刻”。这款全球首个专为智能眼镜设计的通用操作系统&#xff0c;通过三大突破重新定义了AI可穿戴…

微信小程序地图map全方位解析

微信小程序地图map全方位解析 微信小程序的 <map> 组件是一个功能强大的工具&#xff0c;可以实现地图展示、定位、标注、路径规划等多种功能。以下是全方位解析微信小程序地图组件的知识点&#xff1a; 一、地图组件基础 1. 引入 <map> 组件 在页面的 .wxml 文…

AI(14)-prompt

1.BaseLLM 和Tuned LLM之间的区别 基本模型 指令微调模型 人类反强化学习 2.指南 下载包 导入包设置key 2个基本原则 写明确特定的指令 给模型时间思考 2.1.使用分割符清楚地指示输入的不同部分 示例&#xff1a;对这个段落进行总结&#xff0c;将用3个单引号…