刷到一个挺有意思的话题,结合自己之前的经验,整理了一下核心要点。
🔥草莓熊Lotso:个人主页
❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!
🎬 博主简介:
文章目录
- 前言:
- 一. TCP 服务器核心模型演进
- 二. 自研基础组件轮子深度解析
- 2.1 互斥锁与 RAII 锁守卫(Mutex.hpp)
- 2.2 条件变量封装(Cond.hpp)
- 2.3 线程封装(Thread.hpp)
- 2.4 策略模式日志系统(Logger.hpp)
- 2.5 网络地址封装(InetAddr.hpp)
- 3.1 需求背景与整体设计
- 3.2 业务层:命令执行模块深度解析(ExcuteCommand.hpp)
- 3.3 网络层:多线程 TCP 服务器深度解析
- 3.3.1 核心类型与成员定义
- 3.3.2 服务端完整代码解析(TcpServer.hpp)
- 3.3.3 客户端完整代码解析
- 3.3.4 服务端主函数代码(TcpServer.cc)
- 4.1 线程池核心设计思想
- 4.2 线程池模板类深度解析(ThreadPool.hpp)
- 4.3 基于线程池的 TCP Echo 服务器实现
- 4.3.1 服务器核心实现(TcpEchoServer.hpp)
- 4.3.2 服务器启动入口
前言:
在 Linux 后端开发中,TCP 服务器是网络编程的核心基石,从入门级的单进程 Echo 服务器,到生产环境支撑高并发的线程池服务器,其演进过程不仅是代码的优化,更是对操作系统进程 / 线程模型、网络 IO、并发编程的深度理解。而远程命令执行作为 TCP 服务器的经典业务场景,更是复刻了 SSH、运维管控平台的核心实现逻辑,同时也是后端开发面试中的高频手写题与深度问答考点。本文将从自研基础组件封装出发,完整拆解多线程远程命令执行服务器与线程池高并发 Echo 服务器的工业级实现,逐行解析核心源码,梳理 TCP 服务器模型的演进逻辑,同时覆盖并发编程的核心坑点与面试高频考点,让你不仅能写得出,更能懂底层、讲明白。
一. TCP 服务器核心模型演进
在正式进入代码实战前,大家先理清 TCP 服务器的四大核心模型的演进逻辑,明确每个模型的优劣与适用场景,这也是面试中最基础的必考题。
服务器模型核心实现核心优势核心劣势适用场景单进程模型accept 后串行处理客户端连接代码轻松、无并发安全麻烦一次只能处理一个连接,完全不支持并发入门学习、单客户端固定场景多进程模型每个客户端连接 fork 一个子进程处理进程地址空间隔离,稳定性极高进程创建/销毁开销大,并发上限低长连接、低并发、高稳定性要求场景多线程模型每个客户端连接创建一个线程处理线程开销远小于进程,共享地址空间通信便捷频繁创建销毁线程有开销,线程过多会导致系统调度压力剧增中等并发、中短连接场景线程池模型预创建固定数量线程,任务入队后线程池调度执行避免线程频繁创建销毁开销,限制最大线程数,控制系统调度压力不适合长连接场景(长连接会长期占用工作线程)短连接、高并发、突发流量场景
核心关键结论:线程池版本仅适合短服务 / 短连接场景。如果用线程池处理长连接,一个连接会长期占用一个工作线程,当线程池线程耗尽后,新的客户端将完全无法建立连接,这是新手最容易踩的坑。
二. 自研基础组件轮子深度解析
本文所有服务器实现,均基于自研的 Linux 系统编程组件封装,这些组件不仅屏蔽了原生 C 接口的繁琐细节,更是解决了并发安全、资源泄漏等经典麻烦。
2.1 互斥锁与 RAII 锁守卫(Mutex.hpp)
互斥锁是并发编程的基石,用于保护临界资源的原子访问;而 RAII 风格的锁守卫,是 C++ 中避免锁泄漏、死锁的最佳实践。
#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
源码核心解读:
- 接口极简设计:封装了原生pthread_mutex的核心操作,同时提供Origin()接口,方便与条件变量等原生 C 接口配合使用。
- RAII 机制保障:LockGuard在对象构造时自动加锁,生命周期结束时自动解锁,无论函数正常返回、异常抛出,都能保证锁被释放,彻底避免锁泄漏。
- 使用场景:所有临界资源的访问(如线程池任务队列、日志文件写入)都通过LockGuard保护,无需手动调用unlock,代码更健壮。
2.2 条件变量封装(Cond.hpp)
条件变量用于线程间的通知机制,配合互斥锁实现生产者 - 消费者模型,是线程池的核心同步组件。
#ifndef COND_HPP
#define COND_HPP
#include
#include
#include "Mutex.hpp"
/**
* @brief 条件变量封装类
* 核心逻辑:提供线程间的通知机制。
* 它允许线程在某些条件不满足时挂起,并在其他线程改变条件并发送信号时被唤醒。
*/
class Cond
{
public:
// 构造函数:初始化条件变量
Cond()
{
// nullptr 表示使用操作系统默认的条件变量属性
pthread_cond_init(&cond, nullptr);
}
/**
* @brief 等待条件满足
* @param mutex 必须是当前线程已经持有的互斥锁
* * 底层逻辑“三步跳”:
* 1. 自动释放传入的 mutex 锁(这样其他线程才能修改临界资源)。
* 2. 将当前线程挂起并加入到该条件变量的等待队列中。
* 3. 当被唤醒返回时,会自动尝试重新竞争并持有该 mutex 锁。
*/
void Wait(Mutex &mutex)
{
// 调用封装好的 Mutex 类的 Origin() 接口,配合底层 C 接口使用
pthread_cond_wait(&cond, mutex.Origin());
}
// 唤醒一个在此条件变量下等待的线程
void NotifyOne()
{
// 唤醒队列中的第一个线程(如果存在)
pthread_cond_signal(&cond);
}
// 唤醒所有在此条件变量下等待的线程
void NotifyAll()
{
// 广播通知,常用于多个消费者或复杂的资源变动场景
pthread_cond_broadcast(&cond);
}
// 析构函数:销毁条件变量资源
~Cond()
{
/**
* 注意事项:
* 销毁一个仍有线程在等待的条件变量是危险行为。
* 在线程池销毁前,通常需要先调用 NotifyAll 并回收所有线程。
*/
pthread_cond_destroy(&cond);
}
private:
pthread_cond_t cond; // POSIX 线程库提供的底层条件变量结构
};
#endif
源码核心解读:
- Wait 函数的底层 “三步跳”:这是条件变量最核心的考点
- 将当前线程挂起,加入条件变量的等待队列
- 被唤醒返回时,自动重新竞争并持有互斥锁
NotifyOne用于常规任务通知,NotifyAll用于线程池优雅退出等得唤醒所有线程的场景。使用规范:条件变量的等待必须配合while循环(而非 if),避免虚假唤醒,这一点在线程池实现中会重点体现。
2.3 线程封装(Thread.hpp)
对原生 POSIX 线程库进行 C++ 封装,解决了类成员函数作为线程入口的参数匹配麻烦,同时提供了线程命名、LWP 获取、状态管理等实用能力。
```
ifndef __THREAD_HPP
define __THREAD_HPP
include
include
include
include
include
include
include
// 定义线程执行的任务类型,使用包装器增强灵活性
using func_t = std::function;
// 线程状态枚举:用于构建轻松的状态机,确保护法操作
enum class TSTAYUS
{
THREAD_NEW, // 新建状态
THREAD_RUNNING, // 运行状态
THREAD_STOPPED, // 停止/退出状态
};
// 这个是有点bug的:全局静态变量在多线程并发创建对象时存在“竞态条件”
// 多个线程可能同时执行 gunm++,导致线程编号重复,生产环境下建议使用 std::atomic
static int gunm = 1;
class Thread
{
private:
// 获取所属进程的 PID
void get_pid()
{
_pid = getpid();
}
// 获取内核级线程 ID (LWP ID),这才是 Linux 系统监控(如 top -H)见到的真正 ID
void get_lwid()
{
// 原生 pthread 库没有直接获取 LWP 的接口,必须通过系统调用
_lwid = syscall(SYS_gettid);
}
/**
@brief 静态成员函数作为线程入口点
关键逻辑:pthread_create 要求回调函数必须是 void ()(void)
类的普通成员函数隐含 this 指针,参数不匹配,故必须设为 static。
通过传入 args (this 指针) 重新找回对象上下文。
/
static void routine(void args)
{
Thread* ts = static_cast(args);
ts->get_pid();
ts->get_lwid();
// 为线程设置名字,方便在调试器(如 gdb)中识别
pthread_setname_np(pthread_self(), ts->Name().c_str());
// 执行用户真正传入的任务
ts->_func();
return nullptr;
}
public:
// 构造函数:完成任务绑定与命名,此时线程尚未在内核中创建
Thread(func_t f) : _func(f), _joinable(true), _status(TSTAYUS::THREAD_NEW)
{
_name = "Worker-" + std::to_string(gunm++);
}
// 启动线程:正式调用底层接口
void start()
{
if(_status == TSTAYUS::THREAD_RUNNING)
{
std::cerr
就写这么多吧,内容比较基础,适合入门回顾。有补充的地方欢迎留言一起完善。
评论 (0)
暂无评论