当前位置: 首页 > article >正文

【Linux】34.封装 UdpSocket(1)

文章目录

  • 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)

    • 连接到指定IP和端口的服务器

    • 从用户获取输入并发送到服务器

    • 接收并显示服务器的响应

  4. 主程序 (main.c)

    • 初始化并启动UDP服务器

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

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

工作流程:

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

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

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


http://www.kler.cn/a/562191.html

相关文章:

  • LeetCode 热题 100 73. 矩阵置零
  • 【PyTorch][chapter 28][李宏毅深度学习][Diffusion Model-3]
  • Java使用EasyExcel实现异步导出
  • html - 手工添加上次阅读的位置, 方便下次阅读
  • 大语言模型安全测试:WDTA 标准下的全面解读与实践展望
  • 深入理解Redis:数据类型、事务机制及其应用场景
  • 前端项目配置 Nginx 全攻略
  • redis和mysqle辨析
  • 大语言模型的不足与研究热点
  • 2.部署kafka:9092
  • DeepSeek为云厂商带来新机遇,东吴证券看好AI带动百度智能云增长
  • 异常——及处理方式
  • 取消票证会把指定的票证从数据库中删除,同时也会把票证和航班 等相关表中的关联关系一起删除。但在删除之前,它会先检查当前用户是否拥有这张票
  • 【vLLM】【基准测试】:vLLM部署大模型的基准测试
  • RTSP协议
  • Starlink卫星动力学系统仿真建模第十讲-基于SMC和四元数的卫星姿态控制示例及Python实现
  • Cesium@1.126.0,创建3D瓦片,修改样式
  • 「软件设计模式」责任链模式(Chain of Responsibility)
  • 【Python爬虫(48)】分布式爬虫:解锁多领域数据宝藏的密码
  • Apache SeaTunnel 构建实时数据同步管道(最新版)