文章目录
- 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客户端代码中,套接字的使用主要体现在以下几个步骤:
- 创建套接字:
// 创建UDP套接字 int sockfd = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET: IPv4协议族 // SOCK_DGRAM: UDP数据报套接字 // 0: 使用默认协议
- 使用套接字发送数据:
// 发送数据到服务器 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); // 地址结构长度
- 完整的通信流程示例:
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; }
- 错误处理示例:
// 创建套接字时的错误处理 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; }
关键点:
- UDP是无连接的,不需要建立连接就可以直接发送数据
- 每次发送/接收都需要指定目标/来源地址
- UDP不保证数据的可靠传输
- 需要正确处理发送和接收可能出现的错误
- 记得在程序结束时关闭套接字
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协议的远程命令执行系统,主要包含以下组件:
日志系统 (Log.hpp)
支持多种日志级别(Info、Debug、Warning、Error、Fatal)
可以选择日志输出方式(屏幕、单文件、分类文件)
记录带时间戳的日志信息
UDP服务器 (UdpServer.hpp)
创建UDP套接字监听指定端口
接收客户端请求
通过回调函数处理请求并返回结果
UDP客户端 (UdpClient.cc)
主程序 (main.c)
工作流程:
也就是说
main.c
运行后创建服务器端,客户端运行可以和这个服务器端通信。这实际上是一个简单的远程命令执行系统,允许客户端远程在服务器上执行命令并获取结果。不过需要注意,当前实现没有加入安全机制(如身份验证、命令过滤等),在实际使用中需要添加相应的安全措施。