整理:【Linux网络】从零构建高性能UDP服务器:从Echo到英译汉业务级实现

今天翻到一篇不错的技术分享,看完之后自己也琢磨了一下,把思路梳理记录下来。

🔥个人主页:Cx330🌸

❄️个人专栏:《C语言》《LeetCode刷题集》《数据结构-初阶》《C++知识分享》

《优选算法指南-必刷经典100题》《Linux操作系统》:从入门到入魔

《Git深度解析》:版本管理实战全解 《Qt 极境架构》

🌟心向往之行必能


🎥Cx330🌸的简介:


目录

前言:

一. UDP 网络编程基础

1.1 UDP 协议三大核心特性

1.2 UDP Socket 核心工作流程图解

1.3 核心前置知识点

1.3.1 服务端必须显式 bind vs 客户端无需/不建议显式 bind 深度剖析

1.3.2 为什么推荐绑定 INADDR_ANY?

1.3.3 延伸硬核考点:INADDR_ANY 与网络字节序转换——为什么 0.0.0.0 不需要 htonl?

① 什么是字节序?

② 核心转换 API 系列

③ 为什么 0.0.0.0(INADDR_ANY)可以不调用 htonl?

1.4 核心 API 详解

二. 前置基础设施:工具类 实现

2.1 互斥锁封装 Mutex.hpp

2.2 线程安全日志系统 logger.hpp

三. V1 版本:UDP Echo 回显服务实现

3.1 服务端实现

3.1.1 服务端头文件 UdpEchoServer.hpp

3.1.2 服务端主函数 UdpEchoServer.cpp

3.2 客户端实现

3.3 代码编译与运行测试

3.3.1 编译 Makefile

3.3.2 运行测试

四. 解耦封装与业务实战:英译汉字典服务器 (V2)

4.1 需求分析

4.2 词典文件 Dict.txt

4.3 字典类实现 Dictionary.hpp

4.4 通用 UDP 服务端实现 UdpServer.hpp

4.5 字典服务端主函数 DictServer.cpp

4.6 字典客户端实现 DictClient.cpp

4.7 编译与运行测试在

五. 进阶:通用 UDP 服务端 / 客户端封装

5.1 基础套接字封装 udp_socket.hpp

5.2 通用服务端封装 udp_server.hpp

5.3 通用客户端封装 udp_client.hpp

5.4 基于封装的极简字典服务实现

六. UDP 编程核心考点与踩坑指南

第一部分:网络与 Socket API 核心面试真题

Q1:在 UDP 编程中,调用 sendto() 成功返回了一个大于 0 的整数,是否意味着对端客户端一定收到了该数据报?

Q2:在 TCP 编程中,读函数返回 0 代表连接断开(EOF);那么在 UDP 的 recvfrom() 中,返回 0 同样代表对端关闭了连接吗?

Q3:为什么服务端必须显式调用 bind() 绑定固定端口,而客户端绝不建议显式 bind()?

Q4:在绑定服务器网络地址属性时,为什么端口必须调用 htons() 转换字节序,而绑定的 INADDR_ANY 却可以直接赋值,不需要调用 htonl()?

Q5:云服务器(如阿里云、腾讯云等)在部署 UDP 服务端时,为什么绝对不能 bind 它的公网 IP?

第二部分:C++ 系统编程与 STL 生产环境踩坑指南

Q6:在写 recvfrom() 循环接收数据时,以下代码片段存在什么严重的隐藏 Bug?

Q7:为什么在多线程并发(如线程池架构)的 UDP 服务端开发中,绝对禁止用 inet_ntoa 函数?

Q8:在 UDP 服务端开发中,如果 recvfrom 指定的本地接收缓冲区 buf 只有 500 字节,但客户端发来了一个 1000 字节的数据报,会发生什么?

七. 结语


前言:

在网络编程的广阔天地中,UDP(用户数据报协议)因其无连接、不可靠但极具传输高效率的特性,在实时游戏、流媒体及高性能分布式系统中被广泛采用。
作为一名 C++ 领域的技术博主,今天我将结合 Linux 系统编程的核心接口,带大家一步步由浅入深,从零构建一个完整的 UDP 通讯框架。大家不仅会编写基础的 Echo 回显服务 (V1),还会实现优雅的字典翻译服务 (V2)
本文不仅涵盖了 Socket API、sockaddr 的深度绑定机制,还会探讨 inet_ntoa 这一类古老地址转换函数所带来的“多线程安全地雷”,以及如何利用 C++ 的 remove_if 算法完美擦除过期用户。这是一篇妥妥的干货,建议收藏后反复研读!

一. UDP 网络编程基础

在动手写代码之前,大家必须先搞懂 UDP 协议的本质特性,以及 UDP Socket 编程的完整流程,这是所有代码实现的理论基础。

1.1 UDP 协议三大核心特性

UDP(User Datagram Protocol,用户数据报协议)是传输层协议,和 TCP  同属网络分层模型的传输层,但其核心设计与 TCP 完全相反,三大核心特性如下:

特性详细说明与 TCP 的核心差异无连接通信前无需建立连接,知道对方的 IP 和端口即可直接发送数据,不存在三次握手、四次挥手的过程TCP 必须先通过三次握手建立连接,才能传输数据不可靠传输不给出确认应答、超时重传、序列号、乱序重排等机制,只保证把数据尽力发送出去,不保证数据一定到达、不重复、按序到达TCP 通过一系列机制保证数据可靠、不丢失、不重复、按序交付面向数据报数据以独立的报文为单位传输,收发次数严格匹配,报文之间有明确的边界,发送端一次发一个报文,接收端必须完整接收整个报文TCP 面向字节流,数据无边界,发送端发 1000 字节,接收端可以分多次读取,需要上层自行处理数据边界

重要提醒:不可靠是 UDP 的特性,而非缺点。UDP 舍弃了可靠性保障,换来了极致的低延迟和极小的头部开销(UDP 头部仅 8 字节,TCP 头部最少 20 字节),这也是实时场景选择 UDP 的核心原因。

1.2 UDP Socket 核心工作流程图解

UDP 是无连接的协议,所以其编程模型比 TCP 轻松很多,服务端和客户端的核心流程如下:

服务端核心流程

  • 创建 Socket 文件描述符:调用socket()函数,创建一个基于 IPv4、数据报类型的 UDP 套接字;
  • 绑定地址与端口:调用bind()函数,将套接字与固定的 IP 地址、端口号绑定,让客户端知道请求的目标地址;
  • 循环接收与发送数据:调用recvfrom()阻塞等待客户端数据,收到数据后执行业务处理,再调用sendto()将处理结果回发给客户端;
  • 关闭套接字:服务停止时,调用close()关闭套接字。
客户端核心流程
  • 创建 Socket 文件描述符:同服务端,调用socket()创建 UDP 套接字;
  • 填充服务端地址信息:定义sockaddr_in结构体,填充服务端的 IP、端口,作为数据发送的目标;
  • 循环发送与接收数据:调用sendto()向服务端发送数据,再调用recvfrom()等待服务端的响应
  • 关闭套接字:通信收尾时,调用close()关闭套接字。
【服务端 (Server)】                     【客户端 (Client)】
     +-------------------+                 +-------------------+
     |  1. socket()      |                 |  1. socket()      |
     |  创建UDP套接字    |                 |  创建UDP套接字    |
     +---------+---------+                 +---------+---------+
               |                                     |
     +---------v---------+                           |
     |  2. bind()        |                           | (客户端通常不需要显式bind)
     |  绑定固定的Port   |                           | (首次sendto时由OS随机分配)
     +---------+---------+                           |
               |                                     |
     +---------v---------+                           |
     |  3. recvfrom()    | +  6. recvfrom()
     |  将响应发送回     |      (发送响应数据)       |  接收服务端回包
     |  客户端的目标地址 |                           |
     +---------+---------+                 +---------v---------+
               |                           |  7. close()       |
          (回到步骤3)                       |  关闭套接字资源   |
               |                           +-------------------+
               v

1.3 核心前置知识点

在代码实现前,必须先吃透这几个高频踩坑的核心知识点:

1.3.1 服务端必须显式 bind vs 客户端无需/不建议显式 bind 深度剖析

在网络编程的学习中,几乎每个初学者都会发出一个疑问:为什么服务端非要调用 bind(),而客户端却从来不写?客户端确实没有绑定端口吗? 其实,客户端也必须绑定端口,但它是“隐式绑定”。这里面的设计逻辑关系到网络通信的底层哲学。

服务端为什么必须【显式 bind】?

服务端的基本角色是“被动给出服务者”,这就决定了它的行为特征:

1. 服务端的物理地址必须是“众所周知”(Well-known)的: 在网络通信中,永远是客户端主动发起请求。如果客户端想要发数据给服务端,它首先必须得知道服务端的具体 IP 和 Port。

  • 举个例子,这就好比大家要去“工商银行办事”,工商银行的地址(IP)和营业窗口(Port)必须是常年固定且对外公开的。
  • 如果服务端不进行显式 bind,它的端口在每次启动时都会由操作系统随机分配。今天启动是 52301,明天启动是 41908。由于客户端无法提前预知这个随机端口,根本无法向其发送请求,服务也就无从谈起了。
2. 端口代表特定的服务: 网络服务往往有默认的标准端口,比如 HTTP 是 80,HTTPS 是 443,SSH 是 22。服务端通过显式绑定这些固定端口,来向全网宣告自己的“服务入口”。

客户端为什么【无需/不建议显式 bind】?

客户端的角色是“服务的主动请求者”,对于客户端,核心考量是安全、防冲突与轻量化:

1. 防止端口冲突(Address already in use): 客户端是运行在普通用户电脑上的。用户在同一台设备上,可能会开多个相同的客户端(比如多开网页、多开游戏客户端),或者不同的应用程序可能恰好抢夺同一个固定端口。

  • 假设客户端在代码里显式绑定了 8888 端口。当用户在一台电脑上开启第一个客户端实例时运行正常。一旦用户试图打开第二个实例,或者此时电脑上正好有个后台程序已经占用了 8888 端口,第二个客户端进程在初始化调用 bind() 时,就会立刻因为 “Address already in use” (端口冲突) 报错而直接崩溃退出!
  • 这种脆弱的用户体验对客户端软件来说是不可接受的。
2. 隐式绑定(OS 动态指派): 客户端既然不需要被别人主动寻找,那么它的端口到底是多少根本无关紧要。所以,客户端不调用 bind。 操作系统底层设计了“隐式绑定”机制:当客户端首次调用 sendto()(或 TCP 的 connect())时,操作系统发现该套接字没有绑定端口,会自动从系统当前的 临时端口范围 (Ephemeral Ports,通常在 1024 ~ 65535) 中动态挑选一个当前无人占用的空闲端口,静默地为该套接字进行绑定。
  • 这样一来,不管你开了多少个客户端,或者电脑后台运行了多么麻烦的软件,操作系统都能确保每个客户端拿到一个唯一的、不发生冲突的端口。
3. 服务端通过 recvfrom 自动提取: 客户端发送数据时,操作系统会将“自动指派的临时端口”作为源端口(Source Port)封装进 UDP 数据报头部。服务端接收到该报文后,通过 recvfrom 的输出参数 peer 即可获取该客户端的临时端口,从而直接向其发回包,完全不需要客户端提前“死绑定”。

维度

服务端(Server)

客户端(Client)

角色定位

被动等待,主动给出服务

主动发起请求,被动接收响应

端口要求

必须固定、公开、众所周知(Well-known)

必须动态、随机、防冲突(Ephemeral)

绑定方式

显式绑定:程序员必须手动写代码进行 bind()

隐式绑定:由操作系统在首次发包时动态随机指派

冲突后果

端口被占则服务器无法正常启动(属于严重故障)

若显式 bind 固定端口,极易造成用户多进程冲突、程序闪退

寻址机制

客户端通过预先得知的服务地址直接寻址

服务端接收报文时通过 recvfrom() 底层提取,实现动态回包

1.3.2 为什么推荐绑定 INADDR_ANY?

在 V1 版本的服务器初始化代码中,我们见到了如下绑定设置:

local.sin_addr.s_addr = INADDR_ANY; // 其底层值定义为 0x00000000(即 IP 0.0.0.0)

在底层源码中,这个宏在 <netinet/in.h> 中定义为:

#define INADDR_ANY ((in_addr_t) 0x00000000)

别小看这行代码,这在 C++ 服务端开发面试中是一个极高频的考点。我们必须从以下三个核心维度深度剖析它的核心作用:

如果我们显式地将服务端 bind 了某一个确定的 IP,比如 192.168.1.100

  • 局限性:该套接字将只监听来自 eth0 网卡的数据。
  • 痛点:若客户端通过本地回环 127.0.0.1 或者外网网卡发送数据到服务端的相同端口,操作系统的 TCP/IP 协议栈会检测到目标 IP 与绑定的 192.168.1.100 不符,直接在底层将数据包丢弃
【客户端报文 1】 --- (发往 127.0.0.1:8888) -------> 【本地回环网卡】 ---> [协议栈检测 IP 匹配失败] ---> (丢弃 ❌)
【客户端报文 2】 --- (发往 192.168.1.100:8888) ----> 【内网网卡 eth0】 -> [协议栈检测 IP 匹配成功] ---> (接收并处理 ✔)

而当我们 bind INADDR_ANY 时:

  • 底层机制:我们是在告诉操作系统的网络内核:“只要是发往这个端口(比如 8888)的数据包,不管它是从本地回环网卡、内网网卡还是外网网卡流进来的,我的服务器统统要收下!
  • 这样做直接省去了繁琐的、判断数据到底是从服务器具体哪张网卡上面流入的过程,使服务器具备了完美的整体监听能力。
提高代码复用性与运维便利度

假设不用 INADDR_ANY,我们就必须在代码中硬编码(Hardcode)写死当前主机的特定 IP(如 "192.168.3.15"),或者通过解析繁琐的本地配置文件来读取。

  • 运维灾难:一旦这套代码需要迁移到另一台测试机、甚至是生产环境集群时,因为每台物理机/容器的私有 IP 都是不同的,你的程序一定会因为 IP 找不到而启动报错,运维必须逐个去修改部署配置文件。
  • 高可用迁移:采用 INADDR_ANY,同一套编译出来的服务器可执行二进制文件不需要任何修改,就可以无缝迁移、分发并运行在任意一台网络环境中,极大地提高了代码的灵活性和自动化部署能力。
1.3.3 延伸硬核考点:INADDR_ANY 与网络字节序转换——为什么 0.0.0.0 不需要 htonl?

在 C++ 套接字编程中,我们写过如下两行核心转换代码:

local.sin_port = htons(_port);      // 端口转换:host to network short
local.sin_addr.s_addr = INADDR_ANY; // IP 绑定:直接赋值

这里潜藏着另一个极具技术细节的面试问题:为什么端口需要调用 htons,而绑定的 INADDR_ANY 却可以直接赋值,不需要调用 htonl

① 什么是字节序?

在多字节整数(如 16 位短整型端口、32 位长整型 IP)存入内存时,由于 CPU 硬件架构设计不同,存在两种排序流派:

  • 小端字节序(Host Byte Order,主机序列):x86/x64 架构 CPU 默认用。低位字节存放在低内存地址。比如数值 0x1234 在小端内存中的布局为:[34] [12](地址由低到高)。
  • 大端字节序(Network Byte Order,网络序列):TCP/IP 协议栈统一的标准规范。高位字节存放在低内存地址。例如相同数值 0x1234 在大端内存中的布局为:[12] [34]
由于在网络通信中,发送方和接收方的主机架构可能完全不同(如小端的 x86 向大端的 MIPS 主机发送数据),所以数据必须在发送前统一转换为大端网络序列,接收后再转换为主机本地序列。
② 核心转换 API 系列

为了跨平台兼容性,C++ 网络编程提供了标准的系统级转换函数:

  • htons (Host to Network Short):将 16 位主机序短整型(如 Port)转为网络字节序。
  • htonl (Host to Network Long):将 32 位主机序长整型(如 IPv4 地址)转为网络字节序。
  • ntohs (Network to Host Short) 与 ntohl (Network to Host Long):网络字节序转主机本地字节序。
③ 为什么 0.0.0.0(INADDR_ANY)可以不调用 htonl?

由于 IP 地址是一个 32 位长整型数,常规 IP 在绑定时必须经过字节序转换(例如通过 inet_addr("192.168.1.100"),其内部已包含大端转换逻辑)。

然而对于 INADDR_ANY 来说:

1. 数值特殊性INADDR_ANY 的数值在十进制中是 0,底层 16 进制表现为 0x00000000(全 0)。
2. 大端与小端的物理对称

  • 小端系统上,其四个字节在内存中的存放顺序为:0x00, 0x00, 0x00, 0x00
  • 大端系统上,其四个字节在内存中的存放顺序依然为:0x00, 0x00, 0x00, 0x00
3. 完美等价:因为全零的对称性,htonl(0x00000000) 在任何机器上运算出来的物理结果,仍然是 0x00000000
延伸结论: 在整个 IPv4 地址空间中,除了 0.0.0.0 之外,还有一个特殊的广播 IP —— 255.255.255.255 (INADDR_NONE / 全 1) 同样不需要在乎主机字节序,因为其 16 进制形式为 0xffffffff,每个字节全为 0xff,在大端和小端中的表示也是绝对对称的。
最佳编码规范:出于严谨的代码语义化考量,部分团队的规范仍然推荐写 htonl(INADDR_ANY)。尽管它没有任何实际计算开销,但在代码可读性上明确了“这是一个需要转换为网络字节序的 IP 数据”。

1.4 核心 API 详解

我们先把 UDP 编程最核心的 4 个 API 的参数、返回值、注意事项讲清楚,后续代码实现会反复用到:

// 1. 创建套接字
#include
int socket(int domain, int type, int protocol);

  • domain:地址族,AF_INET表示 IPv4,AF_INET6表示 IPv6;
  • type:套接字类型,SOCK_DGRAM表示数据报套接字(UDP),SOCK_STREAM表示流式套接字(TCP);
  • protocol:协议编号,UDP 场景固定填 0,系统会自动匹配 UDP 协议;
  • 返回值:成功返回非负文件描述符,失败返回 - 1 并设置 errno。
// 2. 绑定地址与端口
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfdsocket () 返回的文件描述符;
  • addr:填充好的地址结构体指针,需要强转为通用struct sockaddr*类型;
  • addrlen:地址结构体的长度;
  • 返回值:成功返回 0,失败返回 - 1 并设置 errno。
// 3. 接收数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
  • sockfd:socket 文件描述符;
  • buf:接收数据的缓冲区;
  • len:缓冲区的最大长度;
  • flags:接收标志,常规场景填 0,代表阻塞接收;
  • src_addr:输出型参数,用于存储发送端(客户端)的地址信息;
  • addrlen:输入输出型参数,传入时是 src_addr 的长度,返回时是实际写入的地址长度;
  • 返回值:成功返回实际接收的字节数,失败返回 - 1 并设置 errno。
// 4. 发送数据
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
  • sockfd:socket 文件描述符;
  • buf:待发送的数据缓冲区;
  • len:待发送数据的长度;
  • flags:发送标志,常规场景填 0;
  • dest_addr:目标接收端的地址结构体;
  • addrlen:地址结构体的长度;
  • 返回值:成功返回实际发送的字节数,失败返回 - 1 并设置 errno。

二. 前置基础设施:工具类 实现

工业级的网络代码,不会把所有逻辑耦合在主流程中,我们先实现 2 个基础工具类,为后续的服务开发提供支撑:线程安全互斥锁、高性能日志系统。这两个代码我们之前写过了,这里就轻松介绍并且回顾一下,大家如果想的话这里其实还可以扩展一个禁止拷贝的基类,就跟我们上面那个图中的差不多。

2.1 互斥锁封装 Mutex.hpp

多线程环境下,日志打印、数据收发都涉及临界资源的访问,我们封装 POSIX 互斥锁,并通过 RAII 机制实现锁的自动管理,避免手动加解锁导致的死锁问题。

#ifndef MUTEX_HPP
#define MUTEX_HPP

#include
#include

// 互斥锁封装类:提供加锁/解锁及获取原始锁的接口
class Mutex
{
public:
    // 构造函数:初始化互斥锁
    Mutex()
    {
        pthread_mutex_init(&_lock, nullptr);
    }
    // 析构函数:销毁互斥锁
    ~Mutex()
    {
        pthread_mutex_destroy(&_lock);
    }
    // 加锁操作
    void Lock()
    {
        pthread_mutex_lock(&_lock);
    }
    // 解锁操作
    void UnLock()
    {
        pthread_mutex_unlock(&_lock);
    }
    // 获取原始互斥锁指针,用于需要原生 pthread_mutex_t 的接口
    pthread_mutex_t* Origin()
    {
        return &_lock;
    }
private:
    pthread_mutex_t _lock;  // POSIX 互斥锁
};

// RAII 风格的锁守卫类:构造时加锁,析构时解锁,自动管理锁的生命周期
class LockGuard
{
public:
    // 构造函数:接收一个 Mutex 指针,并立即加锁
    LockGuard(Mutex* lockptr) : _lockptr(lockptr)
    {
        _lockptr->Lock();
    }
    // 析构函数:自动解锁
    ~LockGuard()
    {
        _lockptr->UnLock();
    }
private:
    Mutex* _lockptr;  // 指向被管理的互斥锁
};

#endif

代码解析

  • Mutex类:封装了 POSIX 互斥锁的初始化、销毁、加锁、解锁操作,提供面向对象的接口;
  • LockGuard类:经典 RAII 实现,对象创建时自动加锁,离开作用域时析构自动解锁,即使代码抛出异常,也能保证锁被释放,彻底杜绝死锁风险;
  • 线程安全:后续日志系统、多线程服务都会基于这两个类保证临界资源的安全访问。

2.2 线程安全日志系统 logger.hpp

工业级服务必须有完善的日志系统,用于问题排查、运行状态监控。我们实现一个支持控制台 / 文件双输出、多等级日志、线程安全、自动格式化的日志系统,基于策略模式设计,方便后续扩展。

```

ifndef LOGGER_HPP


define LOGGER_HPP

include

include

include

include

include

include

include

include

include

include "Mutex.hpp"

namespace LogModule
{
// 1. 获取时间
std::string GetTimeStamp()
{
time_t currentTime = time(nullptr); // 默认获取当前时区的时间
// 我们希望把这个时间转换成年-月-日 时:分:秒
struct tm dataTime;

// 使用线程安全的版本 localtime_r,防止在多线程并发获取时间时
// 因为共享静态全局变量而导致的时间数据覆盖错乱。
localtime_r(&currentTime, &dataTime);

char dataTimeStr[128];
// 使用 snprintf 保证缓冲区不溢出,%02d 确保时间位宽不足时自动补0(如09秒)
snprintf(dataTimeStr, sizeof(dataTimeStr), "%4d-%02d-%02d %02d:%02d:%02d",
dataTime.tm_year + 1900, // tm_year 是从1900年开始计算的偏移量
dataTime.tm_mon + 1, // tm_mon 范围是 [0, 11],需加1修正
dataTime.tm_mday,
dataTime.tm_hour,
dataTime.tm_min,
dataTime.tm_sec
);

return dataTimeStr;
}

// 2. 日志等级 -- 枚举类型(整数)转换成字符串类型
// 使用 enum class 强类型枚举,避免命名污染,提高类型检查的严谨性
enum class LogLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};

/**
@brief 辅助函数:将枚举常量映射为可读字符串
解决强类型枚举无法直接通过 std::cout 打印的问题
*/
std::string LogLevel2String(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";
}
}

// 3. 刷新策略
// 基类: 策略模式
// 设计意图:将“日志消息的生成”与“日志消息的输出去向”解耦,方便后续扩展网络、数据库等输出端
class LogStrategy
{
public:
// 虚析构函数:确保通过基类指针释放子类对象时,子类的资源(如文件句柄)能被正确释放
virtual ~LogStrategy() = default; // 不在这里析构
// 纯虚函数:定义统一的刷新接口规范
virtual void SyncLog(const std::string &message) = 0; // 强制子类对其进行重写
};

// 策略1: 控制台日志策略
// 子类
class ConsoleLogStrategy: public LogStrategy
{
public:
ConsoleLogStrategy(){}
~ConsoleLogStrategy(){}
void SyncLog(const std::string &message) override // 检查重写的错误
{
// 显示器在多线程下是“临界资源”,加锁防止多线程输出字符交织(Interleaving)
LockGuard logGuard(&_mutex);
std::cout


暂时整理到这里。以上都是个人理解,可能有疏漏,欢迎指正。

评论 (0)

暂无评论