socket网络编程(1)

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 网络程序(客户端和服务器)。我来逐部分解释:

  1. .PHONY:all

    • 声明 all 是一个伪目标,不代表实际文件
  2. all:udpclient udpserver

    • 默认目标 all 依赖于 udpclientudpserver
    • 执行 make 时会自动构建这两个目标
  3. udpclient:UdpClient.cc

    • 定义如何构建 udpclient 可执行文件
    • 依赖源文件 UdpClient.cc
    • 编译命令:g++ -o $@ $^ -std=c++17 -static
      • $@ 表示目标文件名(udpclient)
      • $^ 表示所有依赖文件(UdpClient.cc)
      • -std=c++17 指定使用 C++17 标准
      • -static 静态链接,生成的可执行文件不依赖动态库
  4. udpserver:UdpServer.cc

    • 定义如何构建 udpserver 可执行文件
    • 依赖源文件 UdpServer.cc
    • 编译命令:g++ -o $@ $^ -std=c++17
      • 与客户端类似,但没有 -static 选项,会动态链接
  5. .PHONY:clean

    • 声明 clean 是一个伪目标
  6. 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>作用为:

  1. 定义与系统调用相关的数据类型(如 pid_toff_t)。
  2. 提高代码的可移植性,确保在不同架构和操作系统上正确运行。
  3. 兼容旧代码,尽管部分类型可能已被移到其他头文件,但许多系统仍然依赖它。

socket(AF_INET, SOCK_DGRAM, 0)

  • 功能:调用 socket() 系统函数创建一个 UDP 套接字
  • 参数解析
    • AF_INET:表示使用 IPv4 协议AF_INET6 表示 IPv6)。
    • SOCK_DGRAM:表示 无连接的、不可靠的 UDP 协议(区别于 SOCK_STREAM,即 TCP)。
    • 0:表示使用默认协议(UDP 本身是确定的,所以这里填 0IPPROTO_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);  // 绑定所有网卡
      

6. bind(_sockfd, (struct sockaddr*)&local, sizeof(local))

  • 作用:将套接字绑定到指定的 IP 和端口。
  • 参数解析
    • _sockfd:之前创建的套接字描述符。
    • (struct sockaddr*)&local:强制转换为通用地址结构体(sockaddrsockaddr_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;  // 绑定成功

关键点总结

  1. sockaddr_in:存储 IPv4 地址和端口的结构体。
  2. 字节序转换
    • htons():端口号转网络字节序。
    • inet_addr():IP 字符串转网络字节序。
  3. bind():将套接字绑定到指定地址,使进程能监听该端口。
  4. 错误处理:检查 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. 关键点总结

  1. UDP 无连接特性:每次接收消息时通过 peer 获取客户端地址,发送时需显式指定目标地址。
  2. 缓冲区安全
    • sizeof(buffer)-1 防止缓冲区溢出。
    • buffer[s] = 0 确保字符串正确终止。
  3. 日志记录:记录收到的原始消息(调试用途)。
  4. 回显服务:简单修改收到的数据并返回,适用于测试或回声协议。

完整流程

  1. 启动循环,等待接收数据。
  2. 收到数据后,记录日志并保存客户端地址。
  3. 构造回显消息,发送回客户端。
  4. 循环继续,等待下一条消息。

扩展场景

  • 多线程/异步处理:若需高性能,可将消息处理放到独立线程或使用 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. 核心设计思想

  1. 封装原生 pthread 锁
    • 提供更易用的 C++ 接口(如 Lock()/Unlock())。
    • 隐藏底层 pthread_mutex_t 的复杂性。
  2. 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) 日志组装与输出 LoggerLogMessage

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. 关键优势

  1. 灵活的输出策略:可动态切换控制台/文件输出。
  2. 线程安全:所有输出操作受互斥锁保护。
  3. 易用性:通过宏和流式接口简化调用。
  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;
}
  1. std::make_unique<UdpServer>()
  • 作用:在堆内存上动态分配一个 UdpServer 对象,并返回一个 std::unique_ptr<UdpServer> 智能指针。
  • 优点(对比 new):
    • 更安全:避免直接使用 new,防止内存泄漏。
    • 更高效make_unique 会一次性分配内存并构造对象,比 new + unique_ptr 分开操作更优。
    • 异常安全:如果构造过程中抛出异常,make_unique 能保证内存不会泄漏。
  1. std::unique_ptr<UdpServer>
  • 作用:独占所有权的智能指针,保证 UdpServer 对象的生命周期由它唯一管理。
  • 关键特性
    • 独占所有权:同一时间只能有一个 unique_ptr 指向该对象。
    • 自动释放:当 unique_ptr 离开作用域时,会自动调用 delete 销毁 UdpServer 对象。
    • 不可复制:不能直接拷贝 unique_ptr(可通过 std::move 转移所有权)。
  1. 整行代码的语义
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>();

等价于:

std::unique_ptr<UdpServer> usvr(new UdpServer());  // 不推荐,优先用 make_unique

但更推荐使用 make_unique,原因如上所述。

  1. 适用场景
  • 当需要动态创建 UdpServer 对象,并希望其生命周期由智能指针管理时。
  • 典型用例:
    • 对象需要延迟初始化(如运行时决定是否创建)。
    • 对象需要长生命周期(如跨多个函数作用域)。
    • 避免手动 delete,防止内存泄漏。
  1. 扩展说明

如果 UdpServer 构造函数需要参数,可以这样写:

std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(arg1, arg2);

这行代码是现代 C++(C++11 及以上)中动态对象管理的推荐写法,结合了:

  1. 智能指针unique_ptr)自动管理生命周期。
  2. 工厂函数make_unique)安全构造对象。

既避免了手动内存管理的问题,又保证了代码的简洁性和安全性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/916840.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/916840.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据集:机器学习的基石

三、数据集&#xff1a;机器学习的基石1. sklearn 玩具数据集&#xff1a;快速入门的理想选择1.1 玩具数据集的特点与价值sklearn 内置的玩具数据集&#xff08;Toy Datasets&#xff09;是机器学习入门的绝佳资源。这类数据集通常具有以下特点&#xff1a;数据量小&#xff1a…

SQL排查、分析海量数据以及锁机制

1. SQL排查 1.1 慢查询日志: mysql提供的一种日志记录, 用户记录MySQL中响应时间超过阈值的SQL语句(long_query_time, 默认10秒), 慢查询日志默认是关闭的, 建议开发调优时打开, 最终部署的时候关闭 1.1.1 检查是否开启了慢查询日志 show variables like %slow_query_log%;临…

conda 安装prokka教程

本章教程,记录如何在wsl2+ubuntu下载通过conda安装prokka软件包。 Prokka 是一个快速的、功能强大的基因组注释工具,特别适用于细菌基因组的注释。它能够自动化完成从基因组序列到功能注释的整个流程,包括基因的识别、功能预测和注释,并且支持多种文件格式输出,广泛应用于…

CSS3 圆角

CSS3 圆角 引言 CSS3圆角是现代网页设计中非常重要的一项功能&#xff0c;它使得网页元素的外观更加平滑、美观。本文将详细介绍CSS3圆角的概念、实现方法以及相关属性&#xff0c;帮助您更好地掌握这一技巧。 CSS3圆角概念 CSS3圆角指的是通过CSS3属性为元素&#xff08;如div…

牛顿-拉夫森法求解非线性方程组

牛顿-拉夫森法&#xff08;Newton-Raphson method&#xff09;是一种用于求解非线性方程组的迭代方法。该方法通过线性化非线性方程组&#xff0c;并逐步逼近方程组的解。以下是牛顿-拉夫森法求解非线性方程组的详细步骤和MATLAB实现。 1. 牛顿-拉夫森法的基本原理 对于非线性方…

Windows系统使用命令生成文件夹下项目目录树(文件结构树)的两种高效方法

Windows系统使用命令生成文件夹下项目目录树&#xff08;文件结构树&#xff09;的两种高效方法前言&#xff1a;**方法一&#xff1a;tree 命令 —— 快速生成经典目录树****方法二&#xff1a;PowerShell —— 可以精准过滤“降噪”的命令**这份列表非常精炼&#xff0c;只包…

react中暴露事件useImperativeHandle

注&#xff1a;本页面模块主要是使用 useImperativeHandle &#xff0c;一、概述1、要点hooks 中的暴露事情件方法useImperativeHandle&#xff0c;需要和forwardRef、ref 结合一起使用。1、外层校验的时候会校验里面所有需要校验的验证2、基础使用二、demo案例1、场景1、弹框打…

【论文阅读】-《RayS: A Ray Searching Method for Hard-label Adversarial Attack》

RayS&#xff1a;一种用于硬标签对抗攻击的光线搜索方法 Jinghui Chen University of California, Los Angeles jhchencs.ucla.edu Quanquan Gu University of California, Los Angeles qgucs.ucla.edu 原文链接&#xff1a;https://arxiv.org/pdf/2006.12792 摘要 深度神经…

15K的Go开发岗,坐标北京

好久没有分享最新的面经了&#xff0c;今天分享一下北京某公司Go开发岗的面经&#xff0c;薪资是15K左右&#xff0c;看看难度如何&#xff1a; 为什么要用分布式事务 分布式事务的核心作用是解决跨服务、跨数据源操作的数据一致性问题。在单体应用中&#xff0c;数据库本地事务…

Linux 文件管理高级操作:复制、移动与查找的深度探索

目录一、文件复制&#xff1a;从基础到企业级同步的全维度解析1. cp命令&#xff1a;基础工具的进阶密码&#xff08;1&#xff09;文件属性保留&#xff1a;从基础到极致&#xff08;2&#xff09;特殊文件处理&#xff1a;稀疏文件与设备文件&#xff08;3&#xff09;安全操…

Redis内存使用耗尽情况分析

目录 1、内存上限介绍 1.1、产生原因 1.2、Redis的maxmemory限额 1.3、影响的命令与场景 2. 内存用完后的策略 2.1、淘汰策略分类 2.2、淘汰策略介绍 2.3、不同策略对比 3、常见业务示例 3.1、影响 3.2、监控与自动告警 前言 在日常项目中&#xff0c;不知道你思考过…

Ubuntu 系统中配置 SSH 服务教程

一、什么是 SSH&#xff1f;SSH&#xff08;Secure Shell&#xff09;是一种加密的网络协议&#xff0c;用于在不安全的网络中安全地进行远程登录、远程命令执行和文件传输。它是 Telnet、FTP 等传统协议的安全替代品。二、确认系统环境在开始配置之前&#xff0c;请确认你的系…

基于springboot的编程训练系统设计与实现(源码+论文)

一、开发环境 技术/工具描述MYSQL数据库一个真正的多用户、多线程SQL数据库服务器&#xff0c;适用于Web站点或其他应用软件的数据库后端开发。B/S结构基于互联网系统的软件系统开发架构&#xff0c;利用浏览器进行访问&#xff0c;支持多平台使用。Spring Boot框架简化新Spri…

K8s集群两者不同的对外暴露服务的方式

在工作中&#xff0c;我们暴露集群内的服务通常有几种方式&#xff0c;对于普通的http或者https,我们通常使用​Ingress Nginx​ &#xff0c;对于原始的TCP或者UDP端口服务&#xff0c;可能需要选择 ​LoadBalancer​ &#xff0c;它们的核心区别在于工作层级、协议支持和流量…

实习日志111

第一天 加入内网和内网域&#xff0c;设置自己的操作系统 第二天 安装常用软件和平台 Notepad 是一款免费的源代码编辑器&#xff0c;支持多种编程语言&#xff0c;其功能强大且界面友好&#xff0c;适用于 Windows 操作系统。WinMerge 是一款开源的差异比较和合并工具&…

Redis 服务挂掉排查与解决

Redis 是一个高性能的键值对存储系统&#xff0c;广泛应用于缓存、会话存储、消息队列等场景。在使用 Redis 的过程中&#xff0c;偶尔会遇到 Redis 服务挂掉或无法连接的情况。本文将通过常见错误 RedisException in Redis.php line 63 Connection refused 来讲解如何排查并解…

DOM + HTML + HTTP

一、HTML5的新特性 1.语义化标签:其实就是可以让标签有自己的含义 html4之前都是有的,比如:<h1>、<ul>、<li> html5新增了很多语义化标签:<header>、<nav> html5的语义化标签的常用页面布局: 优点: 1.代码结构清晰,方便阅读,有利于团…

HTML 音频/视频

HTML 音频/视频 引言 HTML 音频和视频标签是网页设计中不可或缺的部分,它们为用户提供了一种将多媒体内容嵌入到网页中的方式。本文将详细介绍 HTML 音频/视频标签的用法、属性和注意事项,帮助开发者更好地在网页中嵌入音频和视频。 HTML 音频标签( ) 1. 标签基本用法 …

Apache Ignite Cluster Groups的介绍

以下这段内容是 Apache Ignite 官方文档中关于 Cluster Groups&#xff08;集群组&#xff09; 的介绍。我来用通俗易懂的方式帮你全面理解这个概念。&#x1f310; 什么是 Cluster Group&#xff1f; 简单来说&#xff1a;Cluster Group 就是一个“节点的子集”。想象一下你的…

github上传本地项目过程记录

最近有和别人进行unity项目协作的需求&#xff0c;需要把自己的本地代码上传到github已有的一个仓库里。记录一下上传过程&#xff0c;防止后续还需要用。 文章目录一、把自己的本地代码上传到github已有的一个仓库中二、常用功能一、把自己的本地代码上传到github已有的一个仓…