C/C++ 实用封装:从零实现一个轻量级日志宏
在 C/C++ 开发中,直接按逻辑打印调试数据非常繁琐,且项目上线前难以统一清理。为了提高开发效率,我们需要封装一个独立的调试日志宏,实现输出格式化、自动附加源码信息,并通过日志等级控制打印内容的可见性。
1. 基础封装:定位文件与行号
初步的思路是利用编译器内置宏 __FILE__ 和 __LINE__,在输出日志时自动带上所在的文件名和行号,方便快速定位问题代码:
cpp
#define LOG(msg) fprintf(stdout, "[%s:%d] %s", __FILE__, __LINE__, msg)
int main() {
// 调用示例
LOG("Hello World");
}
局限性 :当前仅支持固定 %s 打印单个字符串。而在实际业务中,我们通常需要传入复杂的格式化字符串和不确定数量的参数。
2. 支持不定参数:__VA_ARGS__ 及其陷阱
为了接收不确定数量的参数,我们可以引入不定参符号 ...,并在展开时使用 __VA_ARGS__:
cpp
#define LOG(format, ...) fprintf(stdout, "[%s:%d] " format, __FILE__, __LINE__, __VA_ARGS__)
遇到问题 : 当只有格式化字符、没有不定参数时(例如 LOG("hello");),上述宏展开后,__LINE__ 后面会多出一个逗号,导致编译报错。
解决方案 : 引入 ##__VA_ARGS__。## 的作用是在不定参数为空时,自动消除前面多余的逗号。顺便在末尾加上换行符 \n:
cpp
#define LOG(format, ...) fprintf(stdout, "[%s:%d] " format "\n", __FILE__, __LINE__, ##__VA_ARGS__)
3. 引入时间戳与多行宏封装
在排查复杂的状态流转时,时间戳是必不可少的。因为获取时间涉及多行代码,我们需要使用 do { ... } while(0) 结构将宏包裹为一个独立的作用域,并使用 \ 进行行尾转义(续行):
cpp
#define LOG(format, ...) do{\
time_t t = time(NULL);\
struct tm *ltm = localtime(&t);\
char tmp[32] = {0};\
strftime(tmp, 31, "%H:%M:%S", ltm);\
fprintf(stdout, "[%s %s:%d] " format "\n", tmp, __FILE__, __LINE__, ##__VA_ARGS__);\
}while(0)
4. 终极形态:日志等级过滤
为了让日志系统在"调试"和"生产"环境下灵活切换,我们可以引入日志等级(Log Level)。通过比对当前调用等级与系统设定的全局等级,决定是否拦截打印。
利用 do { ... } while(0) 的特性,我们还可以直接用 break 语句实现优雅的条件拦截。
完整代码实现如下:
cpp
#include <iostream>
#include <time.h>
// 1. 定义日志等级枚举
#define INF 0
#define DBG 1
#define ERR 2
// 2. 设置全局日志等级阈值
#define LOG_LEVEL DBG
// 3. 核心宏逻辑(包含等级校验与时间戳生成)
#define LOG(level, format, ...) do{\
if (level < LOG_LEVEL) break;\
time_t t = time(NULL);\
struct tm *ltm = localtime(&t);\
char tmp[32] = {0};\
strftime(tmp, 31, "%H:%M:%S", ltm);\
fprintf(stdout, "[%s %s:%d] " format "\n", tmp, __FILE__, __LINE__, ##__VA_ARGS__);\
}while(0)
// 4. 封装上层快捷调用宏
#define INF_LOG(format, ...) LOG(INF, format, ##__VA_ARGS__)
#define DBG_LOG(format, ...) LOG(DBG, format, ##__VA_ARGS__)
#define ERR_LOG(format, ...) LOG(ERR, format, ##__VA_ARGS__)
int main() {
// 测试调用
INF_LOG("这条普通信息等级低于 DBG,将被拦截过滤");
DBG_LOG("模块初始化成功,当前分配端口:%d", 8080);
ERR_LOG("内存分配失败!");
return 0;
}
至此,我们就完成了一个轻量级、无第三方依赖且支持等级控制的 C/C++ 日志宏封装,能够充分满足日常底层模块和服务的调试需求。
🚀 深入浅出网络编程:Socket 套接字核心操作与进阶属性配置
在网络编程中,套接字(Socket)是我们进行网络通信的基石。无论是搭建高并发的服务器,还是编写轻量级的客户端,掌握套接字的核心操作与底层属性都至关重要。
本文将系统梳理 Socket 模块的基本操作流程,并重点解析两个在实际开发中必不可少的进阶属性:端口重用 与非阻塞模式。
一、 套接字核心生命周期与操作概览
根据我们在开发中最常用的步骤,套接字的操作可以清晰地划分为"建立连接"、"数据收发"和"高级配置"三大类。以下是套接字核心操作的整理:
| 操作类别 | 核心动作 | 功能说明 | 适用端 |
|---|---|---|---|
| 基础初始化 | 创建套接字 | 在内核中创建一个 Socket 对象,获取文件描述符 | 服务端 / 客户端 |
| 绑定地址信息 | 将套接字与指定的 IP 地址和端口(Port)进行绑定 | 服务端 | |
| 开始监听 | 使套接字进入被动监听状态,等待客户端连接请求 | 服务端 | |
| 连接管理 | 向服务器发起连接 | 根据 IP 和端口,主动向服务器发起 TCP 三次握手 | 客户端 |
| 获取新连接 | 从已完成连接的队列中取出一个新的客户端连接 (Accept) | 服务端 | |
| 创建一个服务端连接 | 封装上述服务端初始化流程的代码逻辑 | 服务端 | |
| 创建一个客户端连接 | 封装上述客户端初始化流程的代码逻辑 | 客户端 | |
| 数据交互 | 接收数据 | 从套接字缓冲区读取网络数据 | 服务端 / 客户端 |
| 发送数据 | 将本地数据写入套接字缓冲区并发送至网络 | 服务端 / 客户端 | |
| 资源释放 | 关闭套接字 | 结束通信,回收文件描述符和相关资源 | 服务端 / 客户端 |
| 进阶配置 | 开启地址端口重用 | 设置套接字选项,允许复用处于 TIME_WAIT 状态的端口 |
服务端为主 |
| 设置为非阻塞 | 设置套接字阻塞属性,避免 I/O 操作挂起整个进程 | 服务端 / 客户端 |
二、 进阶避坑指南:为什么我们需要设置套接字属性?
在基础的"收发数据"之外,实际生产环境中我们经常会遇到"端口被占用"或"进程卡死"的问题。这需要通过配置套接字选项(Socket Options)来解决。
1. 突破 TIME_WAIT 限制:开启地址端口重用
痛点场景:
当一个链接绑定了 IP 地址和端口后,主动断开连接的一方 会进入 TIME_WAIT 状态。这是一种 TCP 协议的保护机制,用来确保网络中残留的数据包消散。在这个状态下,套接字不会被立即释放,对应的 IP 和端口依然被系统占用。
这主要是为了保护网络通信的可靠性(如防止旧连接的数据包干扰新连接),但对于服务器而言却是致命的------如果服务器意外崩溃并尝试立即重启 ,会因为端口仍然被占用而报 Address already in use 错误,导致无法立即恢复服务。
解决方案:设置端口重用 (SO_REUSEADDR)
通过调用 setsockopt 设置套接字选项,开启"地址端口重用"。这允许服务器在重启时,直接绑定并复用处于 TIME_WAIT 状态的端口,保障服务的高可用性。
2. 拒绝进程假死:设置非阻塞属性 (Non-blocking)
痛点场景:
默认情况下,套接字的读写操作(如 recv、accept)是阻塞模式(Blocking)的。这意味着如果缓冲区里暂时没有数据,或者没有新的连接请求,程序就会一直停在原地等待。如果在单线程下运行,整个进程就会像"卡死"一样,什么都做不了。
解决方案:设置为非阻塞 (Non-blocking)
将套接字的阻塞属性修改为非阻塞。
设置后,如果尝试读取数据但数据尚未准备好,系统会立即返回一个错误(如 EAGAIN)而不是让进程挂起。这使得我们可以通过循环或结合 I/O 多路复用(如 epoll/select),实现循环读取直到读取完毕,大大提升了单进程处理多任务的并发效率。
总结
掌握基础的创建、绑定、监听和收发流程,能让你写出能跑的 Demo;而理解 TIME_WAIT 保护机制并合理使用端口重用 与非阻塞设置,才能让你写出健壮、工业级的网络程序。
大多数服务器绑定主机所有网卡的ip,所以缺省ip
accept第二个参数可以带出客户端信息,如果不需要传null