socket网络编程(1)
设计echo server进行接口使用
生成的Makefile文件如下
.PHONY:all
all:udpclient udpserverudpclient:UdpClient.ccg++ -o $@ $^ -std=c++17 -static
udpserver:UdpServer.ccg++ -o $@ $^ -std=c++17.PHONY:clean
clean:rm -f udpclient udpserver
这是一个简单的 Makefile 文件,用于编译和清理两个 UDP 网络程序(客户端和服务器)。我来逐部分解释:
-
.PHONY:all
- 声明
all
是一个伪目标,不代表实际文件
- 声明
-
all:udpclient udpserver
- 默认目标
all
依赖于udpclient
和udpserver
- 执行
make
时会自动构建这两个目标
- 默认目标
-
udpclient:UdpClient.cc
- 定义如何构建
udpclient
可执行文件 - 依赖源文件
UdpClient.cc
- 编译命令:
g++ -o $@ $^ -std=c++17 -static
$@
表示目标文件名(udpclient)$^
表示所有依赖文件(UdpClient.cc)-std=c++17
指定使用 C++17 标准-static
静态链接,生成的可执行文件不依赖动态库
- 定义如何构建
-
udpserver:UdpServer.cc
- 定义如何构建
udpserver
可执行文件 - 依赖源文件
UdpServer.cc
- 编译命令:
g++ -o $@ $^ -std=c++17
- 与客户端类似,但没有
-static
选项,会动态链接
- 与客户端类似,但没有
- 定义如何构建
-
.PHONY:clean
- 声明
clean
是一个伪目标
- 声明
-
clean:
- 清理目标
- 执行命令:
rm -f udpclient udpserver
- 强制删除(
-f
)生成的两个可执行文件
- 强制删除(
使用说明:
- 直接运行
make
会编译生成两个可执行文件 - 运行
make clean
会删除生成的可执行文件
注意:客户端使用了静态链接(-static
),而服务器没有,这可能是为了客户端能在更多环境中运行而不依赖系统库。
UdpSever.hpp
1.初始化:
1)创建套接字
//需要包含的头文件
#include <sys/types.h>
#include <sys/socket.h>
#include "Log.hpp"_sockfd=socket(AF_INET,SOCK_DGRAM,0);if(_sockfd<0){LOG(LogLevel::FATAL)<<"socket error";exit(1);}LOG(LogLevel::INFO)<<"socket success,sockfd:"<<_sockfd;
注意点:
<sys/types.h>作用为:
- 定义与系统调用相关的数据类型(如
pid_t
、off_t
)。 - 提高代码的可移植性,确保在不同架构和操作系统上正确运行。
- 兼容旧代码,尽管部分类型可能已被移到其他头文件,但许多系统仍然依赖它。
socket(AF_INET, SOCK_DGRAM, 0)
- 功能:调用
socket()
系统函数创建一个 UDP 套接字。 - 参数解析:
AF_INET
:表示使用 IPv4 协议(AF_INET6
表示 IPv6)。SOCK_DGRAM
:表示 无连接的、不可靠的 UDP 协议(区别于SOCK_STREAM
,即 TCP)。0
:表示使用默认协议(UDP 本身是确定的,所以这里填0
或IPPROTO_UDP
均可)。
- 返回值:
- 成功:返回一个 非负整数(即套接字描述符
_sockfd
)。 - 失败:返回
-1
,并设置errno
(错误码)。
- 成功:返回一个 非负整数(即套接字描述符
2)绑定套接字(socket,端口和ip)
需要用到一个库函数:bind
查询使用方法在XShell里面
man 2 bind
代码如下:
//2.绑定socket,端口号和ip//2.1填充socketaddr_in结构体struct sockaddr_in local;bzero(&local,sizeof(local));local.sin_family=AF_INET;// IP信息和端口信息,一定要发送到网络!// 本地格式->网络序列local.sin_port = htons(_port);// IP也是如此,1. IP转成4字节 2. 4字节转成网络序列 -> in_addr_t inet_addr(const char *cp);local.sin_addr.s_addr=inet_addr(_ip.c_str());int n=bind(_sockfd,(struct sockaddr*)&local,sizeof(local));if(n<0){LOG(LogLevel::FATAL)<<"bind error";exit(2);}LOG(LogLevel::INFO)<<"socket success,sockfd"<<_sockfd;
这段代码的作用是 将 UDP 套接字绑定到指定的 IP 地址和端口,使其能够接收发送到该地址的数据。以下是详细解析:
1. struct sockaddr_in local
- 作用:定义一个 IPv4 地址结构体,用于存储绑定的 IP 和端口信息。
- 成员解析:
sin_family
:地址族,AF_INET
表示 IPv4。sin_port
:端口号(需转换为网络字节序)。sin_addr.s_addr
:IP 地址(需转换为网络字节序)。
2. bzero(&local, sizeof(local))
- 作用:将
local
结构体清零,避免未初始化的内存影响绑定。 - 等价于
memset(&local, 0, sizeof(local))
。
3. local.sin_family = AF_INET
- 作用:指定地址族为 IPv4(
AF_INET
)。
如果是 IPv6,需使用AF_INET6
。
4. local.sin_port = htons(_port)
- 作用:设置端口号,并使用
htons()
将主机字节序转换为网络字节序。 - 为什么需要转换?
不同 CPU 架构的字节序可能不同(大端/小端),网络传输统一使用 大端序,因此需调用:htons()
:host to network short
(16 位端口号转换)。htonl()
:host to network long
(32 位 IP 地址转换)。
5. local.sin_addr.s_addr = inet_addr(_ip.c_str())
- 作用:将字符串格式的 IP(如
"192.168.1.1"
)转换为网络字节序的 32 位整数。 inet_addr()
函数:- 输入:点分十进制 IP 字符串(如
"127.0.0.1"
)。 - 输出:
in_addr_t
类型(网络字节序的 32 位 IP)。 - 如果
_ip
是空字符串或"0.0.0.0"
,可以改为:local.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定所有网卡
- 输入:点分十进制 IP 字符串(如
6. bind(_sockfd, (struct sockaddr*)&local, sizeof(local))
- 作用:将套接字绑定到指定的 IP 和端口。
- 参数解析:
_sockfd
:之前创建的套接字描述符。(struct sockaddr*)&local
:强制转换为通用地址结构体(sockaddr
是sockaddr_in
的基类)。sizeof(local)
:地址结构体的大小。
- 返回值:
- 成功:返回
0
。 - 失败:返回
-1
,并设置errno
(如EADDRINUSE
表示端口已被占用)。
- 成功:返回
7. 错误处理
if (n < 0) {LOG(LogLevel::FATAL) << "bind error"; // 记录致命错误exit(2); // 退出程序(错误码 2)
}
- 常见错误原因:
- 端口被占用(
EADDRINUSE
)。 - 无权限绑定特权端口(
<1024
需要 root 权限)。 - IP 地址无效。
- 端口被占用(
8. 成功日志
LOG(LogLevel::INFO) << "socket success, sockfd" << _sockfd;
- 记录绑定成功信息,通常包括套接字描述符
_sockfd
和绑定的 IP/端口。
完整代码逻辑
// 1. 初始化地址结构体
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 转网络字节序// 2. 绑定套接字
int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
if (n < 0) {LOG(LogLevel::FATAL) << "bind error"; // 绑定失败exit(2);
}
LOG(LogLevel::INFO) << "socket success, sockfd" << _sockfd; // 绑定成功
关键点总结
sockaddr_in
:存储 IPv4 地址和端口的结构体。- 字节序转换:
htons()
:端口号转网络字节序。inet_addr()
:IP 字符串转网络字节序。
bind()
:将套接字绑定到指定地址,使进程能监听该端口。- 错误处理:检查
bind()
返回值,失败时记录日志并退出。
初始化的代码:
void Init(){//1.创建套接字_sockfd=socket(AF_INET,SOCK_DGRAM,0);if(_sockfd<0){LOG(LogLevel::FATAL)<<"socket error";exit(1);}LOG(LogLevel::INFO)<<"socket success,sockfd:"<<_sockfd;//2.绑定socket,端口号和ip//2.1填充socketaddr_in结构体struct sockaddr_in local;bzero(&local,sizeof(local));local.sin_family=AF_INET;// IP信息和端口信息,一定要发送到网络!// 本地格式->网络序列local.sin_port = htons(_port);// IP也是如此,1. IP转成4字节 2. 4字节转成网络序列 -> in_addr_t inet_addr(const char *cp);local.sin_addr.s_addr=inet_addr(_ip.c_str());int n=bind(_sockfd,(struct sockaddr*)&local,sizeof(local));if(n<0){LOG(LogLevel::FATAL)<<"bind error";exit(2);}LOG(LogLevel::INFO)<<"socket success,sockfd"<<_sockfd;}
2.启动:
这段代码实现了一个 UDP 服务器的消息接收-回显(echo)逻辑。它的核心功能是:循环接收客户端发来的消息,并在每条消息前添加 "server echo@"
后返回给客户端。以下是详细解析:
1. _isrunning
控制循环
_isrunning = true;
while (_isrunning) { ... }
- 作用:通过
_isrunning
标志位控制服务端运行状态。 - 如果需要停止服务,可以在外部设置
_isrunning = false
终止循环。
2. 接收消息 recvfrom()
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
- 参数解析:
_sockfd
:绑定的 UDP 套接字描述符。buffer
:接收数据的缓冲区。sizeof(buffer)-1
:预留 1 字节用于添加字符串结束符\0
。peer
:输出参数,保存发送方的地址信息(IP + 端口)。len
:输入输出参数,传入peer
结构体大小,返回实际地址长度。
- 返回值:
s > 0
:接收到的字节数。s == -1
:出错(可通过errno
获取错误码)。
3. 处理接收到的消息
if (s > 0) {buffer[s] = 0; // 添加字符串结束符LOG(LogLevel::DEBUG) << "buffer:" << buffer;
}
buffer[s] = 0
:将接收到的数据转换为 C 风格字符串(方便日志输出或字符串操作)。- 日志记录:打印接收到的原始消息(调试级别日志)。
4. 构造回显消息并发送 sendto()
std::string echo_string = "server echo@";
echo_string += buffer;
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&peer, len);
- 回显逻辑:
- 在原始消息前拼接
"server echo@"
。 - 例如客户端发送
"hello"
,服务端返回"server echo@hello"
。
- 在原始消息前拼接
sendto()
参数:_sockfd
:套接字描述符。echo_string.c_str()
:待发送数据的指针。echo_string.size()
:数据长度。peer
:目标地址(即消息发送方的地址)。len
:地址结构体长度。
5. 关键点总结
- UDP 无连接特性:每次接收消息时通过
peer
获取客户端地址,发送时需显式指定目标地址。 - 缓冲区安全:
sizeof(buffer)-1
防止缓冲区溢出。buffer[s] = 0
确保字符串正确终止。
- 日志记录:记录收到的原始消息(调试用途)。
- 回显服务:简单修改收到的数据并返回,适用于测试或回声协议。
完整流程
- 启动循环,等待接收数据。
- 收到数据后,记录日志并保存客户端地址。
- 构造回显消息,发送回客户端。
- 循环继续,等待下一条消息。
扩展场景
- 多线程/异步处理:若需高性能,可将消息处理放到独立线程或使用
epoll
/kqueue
。 - 协议增强:可在回显消息中添加时间戳、序列号等信息。
- 错误处理:检查
sendto()
返回值,处理发送失败情况。
示例交互
- 客户端发送:
"hello"
- 服务端接收:
buffer = "hello"
- 服务端返回:
"server echo@hello"
代码如下:
void Start(){_isrunning=true;while(_isrunning){char buffer[1024];struct sockaddr_in peer;socklen_t len=sizeof(peer);//1.收消息ssize_t s=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);if(s>0){buffer[s]=0;LOG(LogLevel::DEBUG)<<"buffer:"<<buffer;//2.发消息std::string echo_string="server echo@";echo_string+=buffer;sendto(_sockfd,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&peer,len);}}}
完整代码如下:
udpserver.hpp
#pragma once#include <iostream>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include "Log.hpp"using namespace LogModule;
const int defaultfd=-1;class UdpServer
{public:UdpServer(const std::string &ip,uint16_t port): _sockfd(defaultfd),_ip(ip),_port(port){}void Init(){//1.创建套接字_sockfd=socket(AF_INET,SOCK_DGRAM,0);if(_sockfd<0){LOG(LogLevel::FATAL)<<"socket error";exit(1);}LOG(LogLevel::INFO)<<"socket success,sockfd:"<<_sockfd;//2.绑定socket,端口号和ip//2.1填充socketaddr_in结构体struct sockaddr_in local;bzero(&local,sizeof(local));local.sin_family=AF_INET;// IP信息和端口信息,一定要发送到网络!// 本地格式->网络序列local.sin_port = htons(_port);// IP也是如此,1. IP转成4字节 2. 4字节转成网络序列 -> in_addr_t inet_addr(const char *cp);local.sin_addr.s_addr=inet_addr(_ip.c_str());int n=bind(_sockfd,(struct sockaddr*)&local,sizeof(local));if(n<0){LOG(LogLevel::FATAL)<<"bind error";exit(2);}LOG(LogLevel::INFO)<<"socket success,sockfd"<<_sockfd;}void Start(){_isrunning=true;while(_isrunning){char buffer[1024];struct sockaddr_in peer;socklen_t len=sizeof(peer);//1.收消息ssize_t s=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);if(s>0){buffer[s]=0;LOG(LogLevel::DEBUG)<<"buffer:"<<buffer;//2.发消息std::string echo_string="server echo@";echo_string+=buffer;sendto(_sockfd,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&peer,len);}}}~UdpServer(){};private:int _sockfd;__uint16_t _port;std::string _ip;//用的是字符串风格,点分十进制bool _isrunning;
};
Mutex.hpp
#pragma once
#include <iostream>
#include <pthread.h>namespace MutexModule
{class Mutex{public:Mutex(){pthread_mutex_init(&_mutex, nullptr);}void Lock(){int n = pthread_mutex_lock(&_mutex);(void)n;}void Unlock(){int n = pthread_mutex_unlock(&_mutex);(void)n;}~Mutex(){pthread_mutex_destroy(&_mutex);}pthread_mutex_t *Get(){return &_mutex;}private:pthread_mutex_t _mutex;};class LockGuard{public:LockGuard(Mutex &mutex):_mutex(mutex){_mutex.Lock();}~LockGuard(){_mutex.Unlock();}private:Mutex &_mutex;};
}
这段代码实现了一个 基于 POSIX 线程(pthread)的互斥锁(Mutex)模块,包含 Mutex
类和 LockGuard
类,用于多线程环境下的资源同步。以下是详细解析:
1. 命名空间 MutexModule
namespace MutexModule { ... }
- 作用:将代码封装在命名空间中,避免与其他库的命名冲突。
2. Mutex
类(核心互斥锁)
成员变量
pthread_mutex_t _mutex; // POSIX 互斥锁对象
构造函数
Mutex() {pthread_mutex_init(&_mutex, nullptr); // 初始化互斥锁(默认属性)
}
pthread_mutex_init
:初始化互斥锁,nullptr
表示使用默认属性(非递归锁)。
加锁与解锁
void Lock() {int n = pthread_mutex_lock(&_mutex); // 阻塞直到获取锁(void)n; // 忽略返回值(实际工程中应检查错误)
}
void Unlock() {int n = pthread_mutex_unlock(&_mutex); // 释放锁(void)n;
}
pthread_mutex_lock
:如果锁已被其他线程持有,当前线程会阻塞。(void)n
:显式忽略返回值(实际项目中建议检查n != 0
的错误情况)。
析构函数
~Mutex() {pthread_mutex_destroy(&_mutex); // 销毁互斥锁
}
- 注意:必须在没有线程持有锁时调用,否则行为未定义。
获取原始锁指针
pthread_mutex_t* Get() {return &_mutex; // 返回底层 pthread_mutex_t 指针
}
- 用途:需要与原生 pthread 函数交互时使用(如
pthread_cond_wait
)。
3. LockGuard
类(RAII 锁守卫)
构造函数(加锁)
LockGuard(Mutex &mutex) : _mutex(mutex) {_mutex.Lock(); // 构造时自动加锁
}
- RAII 思想:利用构造函数获取资源(锁)。
析构函数(解锁)
~LockGuard() {_mutex.Unlock(); // 析构时自动释放锁
}
- 关键作用:即使代码块因异常退出,也能保证锁被释放,避免死锁。
成员变量
Mutex &_mutex; // 引用形式的 Mutex 对象
- 注意:使用引用避免拷贝问题(
pthread_mutex_t
不可拷贝)。
4. 核心设计思想
- 封装原生 pthread 锁:
- 提供更易用的 C++ 接口(如
Lock()
/Unlock()
)。 - 隐藏底层
pthread_mutex_t
的复杂性。
- 提供更易用的 C++ 接口(如
- RAII(资源获取即初始化):
LockGuard
在构造时加锁,析构时解锁,确保锁的安全释放。- 避免手动调用
Unlock()
的遗漏风险。
5. 使用示例
基本用法
MutexModule::Mutex mtx;void ThreadFunc() {MutexModule::LockGuard lock(mtx); // 自动加锁// 临界区代码// 离开作用域时自动解锁
}
对比原生 pthread 代码
// 原生 pthread 写法
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mutex);
// 临界区
pthread_mutex_unlock(&mutex);// 使用 LockGuard 后的写法
{MutexModule::LockGuard lock(mtx);// 临界区
} // 自动解锁
总结
Mutex
:封装pthread_mutex_t
,提供加锁/解锁接口。LockGuard
:RAII 工具类,自动管理锁的生命周期。- 用途:保护多线程环境下的共享资源,避免数据竞争。
- 优势:比手动调用
pthread_mutex_lock/unlock
更安全、更简洁。
Log.hpp
#ifndef __LOG_HPP__
#define __LOG_HPP__#include <iostream>
#include <cstdio>
#include <string>
#include <filesystem> //C++17
#include <sstream>
#include <fstream>
#include <memory>
#include <ctime>
#include <unistd.h>
#include "Mutex.hpp"namespace LogModule
{using namespace MutexModule;const std::string gsep = "\r\n";// 策略模式,C++多态特性// 2. 刷新策略 a: 显示器打印 b:向指定的文件写入// 刷新策略基类class LogStrategy{public:~LogStrategy() = default;virtual void SyncLog(const std::string &message) = 0;};// 显示器打印日志的策略 : 子类class ConsoleLogStrategy : public LogStrategy{public:ConsoleLogStrategy(){}void SyncLog(const std::string &message) override{LockGuard lockguard(_mutex);std::cout << message << gsep;}~ConsoleLogStrategy(){}private:Mutex _mutex;};// 文件打印日志的策略 : 子类const std::string defaultpath = "./log";const std::string defaultfile = "my.log";class FileLogStrategy : public LogStrategy{public:FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile): _path(path),_file(file){LockGuard lockguard(_mutex);if (std::filesystem::exists(_path)){return;}try{std::filesystem::create_directories(_path);}catch (const std::filesystem::filesystem_error &e){std::cerr << e.what() << '\n';}}void SyncLog(const std::string &message) override{LockGuard lockguard(_mutex);std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file; // "./log/" + "my.log"std::ofstream out(filename, std::ios::app); // 追加写入的 方式打开if (!out.is_open()){return;}out << message << gsep;out.close();}~FileLogStrategy(){}private:std::string _path; // 日志文件所在路径std::string _file; // 日志文件本身Mutex _mutex;};// 形成一条完整的日志&&根据上面的策略,选择不同的刷新方式// 1. 形成日志等级enum class LogLevel{DEBUG,INFO,WARNING,ERROR,FATAL};std::string Level2Str(LogLevel level){switch (level){case LogLevel::DEBUG:return "DEBUG";case LogLevel::INFO:return "INFO";case LogLevel::WARNING:return "WARNING";case LogLevel::ERROR:return "ERROR";case LogLevel::FATAL:return "FATAL";default:return "UNKNOWN";}}std::string GetTimeStamp(){time_t curr = time(nullptr);struct tm curr_tm;localtime_r(&curr, &curr_tm);char timebuffer[128];snprintf(timebuffer, sizeof(timebuffer),"%4d-%02d-%02d %02d:%02d:%02d",curr_tm.tm_year+1900,curr_tm.tm_mon+1,curr_tm.tm_mday,curr_tm.tm_hour,curr_tm.tm_min,curr_tm.tm_sec);return timebuffer;}// 1. 形成日志 && 2. 根据不同的策略,完成刷新class Logger{public:Logger(){EnableConsoleLogStrategy();}void EnableFileLogStrategy(){_fflush_strategy = std::make_unique<FileLogStrategy>();}void EnableConsoleLogStrategy(){_fflush_strategy = std::make_unique<ConsoleLogStrategy>();}// 表示的是未来的一条日志class LogMessage{public:LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger): _curr_time(GetTimeStamp()),_level(level),_pid(getpid()),_src_name(src_name),_line_number(line_number),_logger(logger){// 日志的左边部分,合并起来std::stringstream ss;ss << "[" << _curr_time << "] "<< "[" << Level2Str(_level) << "] "<< "[" << _pid << "] "<< "[" << _src_name << "] "<< "[" << _line_number << "] "<< "- ";_loginfo = ss.str();}// LogMessage() << "hell world" << "XXXX" << 3.14 << 1234template <typename T>LogMessage &operator<<(const T &info){// a = b = c =d;// 日志的右半部分,可变的std::stringstream ss;ss << info;_loginfo += ss.str();return *this;}~LogMessage(){if (_logger._fflush_strategy){_logger._fflush_strategy->SyncLog(_loginfo);}}private:std::string _curr_time;LogLevel _level;pid_t _pid;std::string _src_name;int _line_number;std::string _loginfo; // 合并之后,一条完整的信息Logger &_logger;};// 这里故意写成返回临时对象LogMessage operator()(LogLevel level, std::string name, int line){return LogMessage(level, name, line, *this);}~Logger(){}private:std::unique_ptr<LogStrategy> _fflush_strategy;};// 全局日志对象Logger logger;// 使用宏,简化用户操作,获取文件名和行号#define LOG(level) logger(level, __FILE__, __LINE__)#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
}#endif
1. 核心设计思想
- 策略模式:通过
LogStrategy
基类抽象日志输出方式,派生出ConsoleLogStrategy
(控制台输出)和FileLogStrategy
(文件输出)。 - RAII(资源获取即初始化):利用
LogMessage
类的构造和析构,自动组装日志内容并触发输出。 - 线程安全:使用
Mutex
类保护共享资源(如文件写入、控制台输出)。
2. 关键组件解析
(1) 日志级别 LogLevel
enum class LogLevel {DEBUG, // 调试信息INFO, // 普通信息WARNING, // 警告ERROR, // 错误FATAL // 致命错误
};
- 通过
Level2Str()
函数将枚举转换为字符串(如DEBUG
→"DEBUG"
)。
(2) 时间戳生成 GetTimeStamp()
std::string GetTimeStamp() {// 示例输出: "2023-08-20 14:30:45"time_t curr = time(nullptr);struct tm curr_tm;localtime_r(&curr, &curr_tm); // 线程安全的时间转换char buffer[128];snprintf(buffer, sizeof(buffer), "%04d-%02d-%02d %02d:%02d:%02d",curr_tm.tm_year + 1900, curr_tm.tm_mon + 1, curr_tm.tm_mday,curr_tm.tm_hour, curr_tm.tm_min, curr_tm.tm_sec);return buffer;
}
(3) 策略基类 LogStrategy
class LogStrategy {
public:virtual void SyncLog(const std::string &message) = 0;virtual ~LogStrategy() = default;
};
- 纯虚函数
SyncLog
:子类需实现具体的日志输出逻辑。
(4) 控制台输出策略 ConsoleLogStrategy
class ConsoleLogStrategy : public LogStrategy {
public:void SyncLog(const std::string &message) override {LockGuard lock(_mutex); // 线程安全std::cout << message << gsep; // gsep = "\r\n"}
private:Mutex _mutex;
};
(5) 文件输出策略 FileLogStrategy
class FileLogStrategy : public LogStrategy {
public:FileLogStrategy(const std::string &path = "./log", const std::string &file = "my.log") : _path(path), _file(file) {// 自动创建日志目录(如果不存在)std::filesystem::create_directories(_path);}void SyncLog(const std::string &message) override {LockGuard lock(_mutex);std::string filename = _path + "/" + _file;std::ofstream out(filename, std::ios::app); // 追加模式out << message << gsep;}
private:std::string _path, _file;Mutex _mutex;
};
(6) 日志组装与输出 Logger
和 LogMessage
class Logger {
public:// 切换输出策略void EnableFileLogStrategy() { _fflush_strategy = std::make_unique<FileLogStrategy>(); }void EnableConsoleLogStrategy() { _fflush_strategy = std::make_unique<ConsoleLogStrategy>(); }// 日志条目构建器class LogMessage {public:LogMessage(LogLevel level, const std::string &src_name, int line, Logger &logger) : _level(level), _src_name(src_name), _line_number(line), _logger(logger) {// 组装固定部分(时间、级别、PID、文件名、行号)_loginfo = "[" + GetTimeStamp() + "] [" + Level2Str(_level) + "] " +"[" + std::to_string(getpid()) + "] " +"[" + _src_name + ":" + std::to_string(_line_number) + "] - ";}// 支持链式追加日志内容(如 LOG(INFO) << "Error: " << errno;)template <typename T>LogMessage &operator<<(const T &data) {std::stringstream ss;ss << data;_loginfo += ss.str();return *this;}// 析构时触发日志输出~LogMessage() {if (_logger._fflush_strategy) {_logger._fflush_strategy->SyncLog(_loginfo);}}private:std::string _loginfo;// ... 其他字段省略};// 生成日志条目LogMessage operator()(LogLevel level, const std::string &file, int line) {return LogMessage(level, file, line, *this);}
private:std::unique_ptr<LogStrategy> _fflush_strategy;
};
(7) 全局日志对象与宏
// 全局单例日志对象
Logger logger;// 简化用户调用的宏
#define LOG(level) logger(level, __FILE__, __LINE__)
#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()
#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
LOG(level)
:自动填充文件名(__FILE__
)和行号(__LINE__
),例如:LOG(LogLevel::INFO) << "User login: " << username;
3. 使用示例
(1) 输出到控制台
Enable_Console_Log_Strategy();
LOG(LogLevel::DEBUG) << "Debug message: " << 42;
输出示例:
[2023-08-20 14:30:45] [DEBUG] [1234] [main.cpp:20] - Debug message: 42
(2) 输出到文件
Enable_File_Log_Strategy();
LOG(LogLevel::ERROR) << "Failed to open file: " << filename;
文件内容:
[2023-08-20 14:31:00] [ERROR] [1234] [server.cpp:45] - Failed to open file: config.ini
4. 关键优势
- 灵活的输出策略:可动态切换控制台/文件输出。
- 线程安全:所有输出操作受互斥锁保护。
- 易用性:通过宏和流式接口简化调用。
- 自动化:时间戳、PID、文件名等自动填充。
UdpServer.cpp
#include <iostream>
#include <memory>
#include "UdpServer.hpp"
int main(int argc,char *argv[])
{if(argc!=3){std::cerr<<"Usage:"<<argv[0]<<"ip port"<<std::endl;return 1;}std::string ip=argv[1];uint16_t port=std::stoi(argv[2]);Enable_Console_Log_Strategy();std::unique_ptr<UdpServer> usvr=std::make_unique<UdpServer>(ip,port);usvr->Init();usvr->Start();return 0;
}
std::make_unique<UdpServer>()
- 作用:在堆内存上动态分配一个
UdpServer
对象,并返回一个std::unique_ptr<UdpServer>
智能指针。 - 优点(对比
new
):- 更安全:避免直接使用
new
,防止内存泄漏。 - 更高效:
make_unique
会一次性分配内存并构造对象,比new
+unique_ptr
分开操作更优。 - 异常安全:如果构造过程中抛出异常,
make_unique
能保证内存不会泄漏。
- 更安全:避免直接使用
std::unique_ptr<UdpServer>
- 作用:独占所有权的智能指针,保证
UdpServer
对象的生命周期由它唯一管理。 - 关键特性:
- 独占所有权:同一时间只能有一个
unique_ptr
指向该对象。 - 自动释放:当
unique_ptr
离开作用域时,会自动调用delete
销毁UdpServer
对象。 - 不可复制:不能直接拷贝
unique_ptr
(可通过std::move
转移所有权)。
- 独占所有权:同一时间只能有一个
- 整行代码的语义
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>();
等价于:
std::unique_ptr<UdpServer> usvr(new UdpServer()); // 不推荐,优先用 make_unique
但更推荐使用 make_unique
,原因如上所述。
- 适用场景
- 当需要动态创建
UdpServer
对象,并希望其生命周期由智能指针管理时。 - 典型用例:
- 对象需要延迟初始化(如运行时决定是否创建)。
- 对象需要长生命周期(如跨多个函数作用域)。
- 避免手动
delete
,防止内存泄漏。
- 扩展说明
如果 UdpServer
构造函数需要参数,可以这样写:
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(arg1, arg2);
这行代码是现代 C++(C++11 及以上)中动态对象管理的推荐写法,结合了:
- 智能指针(
unique_ptr
)自动管理生命周期。 - 工厂函数(
make_unique
)安全构造对象。
既避免了手动内存管理的问题,又保证了代码的简洁性和安全性。