文章目录
2. 实现一个简易的聊天室应用
2.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()
{
printMethod = Screen; // 默认输出到屏幕
path = "./log/"; // 默认日志目录
}
// 设置日志输出方式
void Enable(int method)
{
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;
// 打开文件:只写、如果不存在则创建、追加写入
int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
if (fd < 0)
return;
write(fd, logtxt.c_str(), logtxt.size()); // 写入日志内容
close(fd); // 关闭文件
}
// 根据日志级别写入不同文件
void printClassFile(int level, const std::string &logtxt)
{
std::string filename = LogFile;
filename += ".";
filename += levelToString(level); // 例如:log.txt.Debug
printOneFile(filename, logtxt);
}
// 重载函数调用运算符,实现格式化日志输出
void operator()(int level, const char *format, ...)
{
// 1. 获取时间信息
time_t t = time(nullptr);
struct tm *ctime = localtime(&t);
// 2. 格式化时间和级别信息
char leftbuffer[SIZE];
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]",
levelToString(level).c_str(),
ctime->tm_year + 1900, // 年(从1900年开始)
ctime->tm_mon + 1, // 月(0-11,需要+1)
ctime->tm_mday, // 日
ctime->tm_hour, // 时
ctime->tm_min, // 分
ctime->tm_sec); // 秒
// 3. 处理可变参数
va_list s; // 定义可变参数列表
va_start(s, format); // 初始化可变参数列表
char rightbuffer[SIZE]; // 存储格式化后的可变参数
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s); // 清理可变参数列表
// 4. 组合完整日志消息
char logtxt[SIZE * 2];
snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);
// 5. 输出日志
printLog(level, logtxt);
}
private:
int printMethod; // 日志输出方式
std::string path; // 日志文件路径
};
/* 可变参数示例
int sum(int n, ...)
{
va_list s; // 定义可变参数列表
va_start(s, n); // 初始化,n是最后一个固定参数
int sum = 0;
while(n)
{
sum += va_arg(s, int); // 依次获取int类型的参数
n--;
}
va_end(s); // 清理可变参数列表
return sum;
}
*/
2.2 Terminal.hpp - 终端重定向管理器
Terminal.hpp
// 包含必要的头文件
#include <iostream> // 标准输入输出流
#include <string> // 字符串类
#include <unistd.h> // UNIX标准函数,提供dup2等系统调用
#include <sys/types.h> // 基本系统数据类型
#include <sys/stat.h> // 文件状态信息
#include <fcntl.h> // 文件控制选项
// 指定终端设备路径
// /dev/pts/N 是伪终端(pseudo-terminal)的设备文件
// 数字N表示特定的终端编号
std::string terminal = "/dev/pts/6";
// 打开并重定向到指定终端的函数
int OpenTerminal()
{
// 以只写模式打开终端设备
// O_WRONLY: 以只写方式打开
int fd = open(terminal.c_str(), O_WRONLY);
// 错误处理:如果打开失败
if(fd < 0)
{
std::cerr << "open terminal error" << std::endl;
return 1;
}
// 重定向标准错误输出(stderr)到新打开的终端
// dup2(oldfd, newfd):将newfd重定向到oldfd
// 2代表标准错误输出(stderr)
dup2(fd, 2);
/* 测试代码(已注释)
// printf("hello world\n");
// close(fd);
*/
return 0;
}
这段代码的主要功能是:
- 打开一个指定的伪终端设备(/dev/pts/6)
- 将标准错误输出重定向到这个终端
- 这样所有的错误信息都会显示在指定的终端上
使用场景:
- 调试输出重定向
- 日志输出到特定终端
- 多终端输出管理
注意事项:
- 需要确保目标终端(/dev/pts/6)存在且有写权限
- 终端编号(6)可能需要根据实际情况调整
- 使用完后应该关闭文件描述符(当前未实现)
2.3 UdpClient.cc - 多线程聊天客户端
UdpClient.cc
// 必要的头文件
#include <iostream> // 标准输入输出
#include <cstdlib> // 标准库函数
#include <unistd.h> // UNIX标准函数
#include <strings.h> // bzero等字符串操作
#include <string.h> // 字符串操作
#include <sys/types.h> // 基本系统数据类型
#include <sys/socket.h> // 套接字接口
#include <netinet/in.h> // Internet地址族
#include <arpa/inet.h> // IP地址转换函数
#include <pthread.h> // POSIX线程
#include "Terminal.hpp" // 终端操作
using namespace std;
// 打印使用说明
void Usage(std::string proc)
{
std::cout << "\n\rUsage: " << proc << " serverip serverport\n"
<< std::endl;
}
// 线程间共享数据结构
struct ThreadData
{
struct sockaddr_in server; // 服务器地址信息
int sockfd; // 套接字描述符
std::string serverip; // 服务器IP地址
};
// 接收消息的线程函数
void *recv_message(void *args)
{
// OpenTerminal(); // 可选的终端重定向
ThreadData *td = static_cast<ThreadData *>(args); // 类型转换
char buffer[1024]; // 接收缓冲区
while (true)
{
memset(buffer, 0, sizeof(buffer)); // 清空缓冲区
struct sockaddr_in temp; // 临时存储发送方地址
socklen_t len = sizeof(temp);
// 接收数据
// sockfd: 套接字描述符
// buffer: 接收缓冲区
// 1023: 接收大小(留1字节给\0)
// flags: 0
// temp: 发送方地址
// len: 地址结构长度
ssize_t s = recvfrom(td->sockfd, buffer, 1023, 0,
(struct sockaddr *)&temp, &len);
if (s > 0)
{
buffer[s] = 0; // 添加字符串结束符
cerr << buffer << endl; // 输出接收到的消息
}
}
}
// 发送消息的线程函数
void *send_message(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
string message;
socklen_t len = sizeof(td->server);
// 发送欢迎消息
std::string welcome = td->serverip;
welcome += " comming...";
sendto(td->sockfd, message.c_str(), message.size(), 0,
(struct sockaddr *)&(td->server), len);
// 循环发送用户输入的消息
while (true)
{
cout << "Please Enter@ ";
getline(cin, message); // 获取用户输入
// 发送消息到服务器
sendto(td->sockfd, message.c_str(), message.size(), 0,
(struct sockaddr *)&(td->server), len);
}
}
// 主函数
// 使用方式:./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 ThreadData td;
bzero(&td.server, sizeof(td.server)); // 清零服务器地址结构
td.server.sin_family = AF_INET; // 使用IPv4
td.server.sin_port = htons(serverport); // 设置端口(转换为网络字节序)
td.server.sin_addr.s_addr = inet_addr(serverip.c_str()); // 设置IP地址
// 创建UDP套接字
td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (td.sockfd < 0)
{
cout << "socker error" << endl;
return 1;
}
td.serverip = serverip;
// 创建接收和发送线程
pthread_t recvr, sender;
pthread_create(&recvr, nullptr, recv_message, &td); // 创建接收线程
pthread_create(&sender, nullptr, send_message, &td); // 创建发送线程
// 等待线程结束
pthread_join(recvr, nullptr);
pthread_join(sender, nullptr);
// 清理资源
close(td.sockfd);
return 0;
}
工作流程:
初始化
↓
创建套接字
↓
创建线程 ─┬─→ 接收线程: 循环接收服务器消息
└─→ 发送线程: 循环发送用户输入
↓
等待线程结束
↓
清理资源
代码功能总结:
- 创建UDP套接字与服务器通信
- 使用多线程分别处理消息的发送和接收
- 发送线程负责获取用户输入并发送
- 接收线程负责显示来自服务器的消息
- 实现了简单的聊天室客户端功能
主要特点:
- 使用UDP协议进行通信
- 多线程设计,支持同时收发消息
- 支持命令行参数配置服务器地址
2.4 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 <unordered_map> // 哈希表
#include "Log.hpp" // 日志系统
// 定义回调函数类型:处理消息并返回响应
// 参数:消息内容、客户端IP、客户端端口
typedef std::function<std::string(const std::string &, const std::string &, uint16_t)> 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; // 缓冲区大小
// UDP服务器类
class UdpServer {
public:
// 构造函数:初始化服务器参数
UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip)
:sockfd_(0), port_(port), ip_(ip), isrunning_(false)
{}
// 初始化服务器
void Init()
{
// 创建UDP套接字
// AF_INET: IPv4协议族
// SOCK_DGRAM: UDP数据报套接字
sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd_ < 0)
{
lg(Fatal, "socket create error, sockfd: %d", sockfd_);
exit(SOCKET_ERR);
}
lg(Info, "socket create success, sockfd: %d", sockfd_);
// 绑定套接字到指定地址和端口
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 CheckUser(const struct sockaddr_in &client, const std::string clientip, uint16_t clientport)
{
auto iter = online_user_.find(clientip);
if(iter == online_user_.end()) // 如果是新用户
{
online_user_.insert({clientip, client}); // 添加到在线用户列表
std::cout << "[" << clientip << ":" << clientport << "] add to online user." << std::endl;
}
}
// 广播消息给所有在线用户
void Broadcast(const std::string &info, const std::string clientip, uint16_t clientport)
{
// 遍历所有在线用户
for(const auto &user : online_user_)
{
// 构造广播消息格式:[IP:PORT]# message
std::string message = "[";
message += clientip;
message += ":";
message += std::to_string(clientport);
message += "]# ";
message += info;
// 发送消息给每个用户
socklen_t len = sizeof(user.second);
sendto(sockfd_, message.c_str(), message.size(), 0,
(struct sockaddr*)(&user.second), len);
}
}
// 运行服务器主循环
void Run()
{
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;
}
// 获取客户端信息
uint16_t clientport = ntohs(client.sin_port); // 转换为主机字节序
std::string clientip = inet_ntoa(client.sin_addr); // 转换为点分十进制
// 检查用户并广播消息
CheckUser(client, clientip, clientport);
std::string info = inbuffer;
Broadcast(info, clientip, clientport);
}
}
// 析构函数:清理资源
~UdpServer()
{
if(sockfd_ > 0) close(sockfd_);
}
private:
int sockfd_; // 套接字文件描述符
std::string ip_; // 服务器IP地址
uint16_t port_; // 服务器端口号
bool isrunning_; // 服务器运行状态
std::unordered_map<std::string, struct sockaddr_in> online_user_; // 在线用户表
};
服务器工作流程:
- 初始化:创建套接字并绑定到指定地址和端口
- 主循环:
- 接收客户端消息
- 检查是否是新用户
- 广播消息给所有在线用户
- 关闭:清理资源
特点:
- 支持多客户端
- 消息广播功能
- 在线用户管理
- 集成日志系统
2.5 main.cc - 服务器启动程序
main.cc
#include "UdpServer.hpp" // UDP服务器类
#include <memory> // 智能指针
#include <cstdio> // 标准输入输出
#include <vector> // vector容器
// "120.78.126.148" 点分十进制字符串风格的IP地址
// 打印使用说明
void Usage(std::string proc)
{
std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}
// 主函数
// 使用方式:./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();
return 0;
}
代码说明:
- 这是一个UDP服务器的主程序
- 支持命令行参数指定端口号
- 使用智能指针管理服务器实例
- 原本包含远程命令执行功能,但已被注释掉以提高安全性
被注释掉的功能:
- Handler: 消息处理函数
- SafeCheck: 命令安全检查
- ExcuteCommand: 命令执行功能
安全考虑:
- 移除了远程命令执行功能
- 去掉了可能造成系统危险的操作
- 将服务器功能限制为纯消息转发
使用方式:
./udpserver 8080 // 在8080端口启动服务器