【Linux】34.封装 UdpSocket(1)

news/2025/2/23 5:49:31

文章目录

  • 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.niftyadmin.cn/n/5863040.html

相关文章

二级公共基础之数据结构与算法篇(七)排序技术

目录 前言 一、交换类排序 1.冒泡排序法 1. 冒泡排序的思想 2. 冒泡排序的实现步骤 3. 示例 4. 冒泡排序的特点 2.快速排序 1. 快速排序的核心思想 2. 快速排序的实现步骤 3. 示例代码(C语言) 4. 快速排序的特点 二、插入类排序 1. 简单插入排序 1.简单插入排…

深入理解 Rust 中的智能指针

一、什么是智能指针&#xff1f; 智能指针是具有指针行为的数据结构&#xff0c;但它们与传统指针相比&#xff0c;提供了更多的功能。智能指针不仅拥有指向数据的能力&#xff0c;还可以管理内存&#xff0c;控制数据的所有权&#xff0c;并在不再需要时自动清理数据。Rust 通…

npm install 卡在“sill idealTree buildDeps“

问题 当你执行 npm install 时&#xff0c;你可能会遇到一个问题&#xff1a;命令卡在"sill idealTree buildDeps"这一步&#xff0c;没有任何反应 原因 这个问题的根源在于淘宝镜像源的域名过期&#xff0c;而实际上需要绑定新的镜像源。 解决方案 更换新的淘宝…

基于SpringBoot+vue+uniapp的智慧旅游小程序+LW示例参考

系列文章目录 1.基于SSM的洗衣房管理系统原生微信小程序LW参考示例 2.基于SpringBoot的宠物摄影网站管理系统LW参考示例 3.基于SpringBootVue的企业人事管理系统LW参考示例 4.基于SSM的高校实验室管理系统LW参考示例 5.基于SpringBoot的二手数码回收系统原生微信小程序LW参考示…

HAProxy介绍与编译安装

目录 1、HAProxy介绍 2、HAProxy编译安装 Centos 基础环境 Ubuntu 基础环境 编译安装HAProxy 验证HAProxy版本 HAProxy启动脚本 配置文件 启动haproxy 验证haproxy状态 查看haproxy的状态页面 1、HAProxy介绍 HAProxy是法国开发者 威利塔罗(Willy Tarreau) 在2000年…

智慧校园系统在学生学习与生活中的应用

随着科技的快速发展&#xff0c;智慧校园系统逐渐成为现代教育不可或缺的一部分。它整合了先进的信息技术、物联网技术以及人工智能等&#xff0c;旨在构建一个全面、智能、个性化的学习与生活环境。对于学生而言&#xff0c;这一系统不仅能够极大地提高学习效率&#xff0c;还…

node.js的常用指令

1. 基本指令 查看 Node.js 版本 node -v加粗样式该指令会输出当前系统中安装的 Node.js 版本号&#xff0c;有助于确认你使用的 Node.js 版本是否符合项目需求。 查看 npm 版本 npm -vnpm&#xff08;Node Package Manager&#xff09;是 Node.js 的包管理工具&#xff0c;…

uniapp小程序自定义日历(签到、补签功能)

1、切换月份根据当前月判断&#xff0c;只能切换当前月份之前的时间。 2、补卡功能&#xff0c;根据后台设置自己写上即可&#xff0c;可补签多少天。 3、点击签到是签到当前天的&#xff0c;不能指定签到时间。 备注&#xff1a;当前代码只构建了排版样式和切换月份功能&…