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
)安全構造對象。
既避免了手動內存管理的問題,又保證了代碼的簡潔性和安全性。