【Linux】三十五.网络基础(三)《从零实现一个C++ HTTP服务器:完整项目实战》----万字详解

一.前言

1.1.为什么要自己写HTTP服务器?

在浏览器地址栏敲下 http://localhost:8080 回车的瞬间,一整套网络流程就启动了:DNS 解析、TCP 三次握手、组装 HTTP 请求报文、服务端接收处理、构造响应报文回传。 对后端开发来说,吃透这套底层逻辑,远比直接用框架写接口更重要。下面我们只用 6 个核心文件,从零实现一个简易 HTTP 服务器,把网络编程核心原理落地。

1.2.技术栈

技术 用途
C++17 编程语言
POSIX Socket 网络通信
多进程 (fork) 并发处理
Makefile 项目构建

1.3.个人收获

知识点 说明
Socket 编程 socket/bind/listen/accept/recv/send 完整流程
HTTP 协议 请求行、头部、正文的解析与构建
多进程并发 fork、双重 fork、僵尸进程处理
设计模式 RAII、策略模式、模板方法模式
C++17 特性 智能指针、filesystem

二.项目概览

功能特性

特性 说明
HTTP/1.1 支持 GET 方法,解析请求行
静态文件服务 根据 URI 返回对应文件
多进程并发 每个客户端独立进程处理
日志系统 支持控制台/文件双输出
错误处理 返回 404 Not Found

三.整体架构设计

3.1.分层架构图


3.2. 文件依赖关系图


3.3.设计模式应用

设计模式 应用场景 核心代码位置 解决了什么问题 带来的好处
模板方法模式 Socket 基类定义 BuildTcpSocketMethod() 标准流程 Socket.hpp 每个协议(TCP/UDP)创建流程相同,但具体实现不同 标准化流程,子类只需实现具体步骤,新增协议无需修改流程代码
策略模式 日志输出支持控制台/文件两种方式 Log.hpp 日志输出方式可能变化(控制台、文件、网络等) 运行时动态切换策略,新增输出方式无需修改核心代码
RAII 模式 LockGuard 自动加解锁、LogMessage 自动输出日志 Mutex.hpp Log.hpp 手动加锁解锁容易忘记,导致死锁或资源泄漏 构造时获取资源,析构时自动释放,异常安全,永不忘记
工厂模式 std::make_unique<TcpSocket>() 创建 Socket 对象 TcpServer.hpp Socket.hpp 对象的创建逻辑分散在多处,更换实现需要修改多处代码 统一对象创建方式,更换实现只需修改工厂方法
观察者模式 TcpServer 通过回调函数处理客户端请求 TcpServer.hpp Http.hpp 服务器框架和业务逻辑强耦合,难以复用 解耦网络层和业务层,业务逻辑可插拔,易于测试

总结如下:

设计模式 一句话总结
模板方法模式 父类定流程,子类填细节
策略模式 同一件事,不同的做法
RAII 模式 构造拿资源,析构还资源
工厂模式 创建对象不自己 new
观察者模式 你只管通知,我负责处理

3.4.数据流转图


四.基础工具层

基础工具层提供了网络地址封装、互斥锁、日志系统和通用工具函数,它们是整个项目的基石。

4.1 网络地址封装(InetAddr.hpp)

网络编程中,地址转换(主机字节序 <--> 网络字节序)是基础且频繁的操作。InetAddr 类封装了 sockaddr_in,自动处理字节序转换。

cpp 复制代码
#pragma once
#include "Common.hpp"

// 类名: InetAddr
// 功能: 网络地址和主机地址之间进行转换的封装类
class InetAddr
{
public:
    // ==================== 构造函数 ====================
    
    // 1. 默认构造函数
    // 注意: 对象未初始化,需要后续调用 SetAddr() 设置
    InetAddr() {}
    
    // 2. 从 sockaddr_in 构造(网络字节序 → 主机字节序)
    // 用途: 接受客户端连接后,从 accept() 返回的地址构造
    InetAddr(struct sockaddr_in &addr)
    {
        SetAddr(addr);
    }
    
    // 3. 从 IP 和端口构造(主机格式 → 网络格式)
    // 用途: 客户端连接服务器时使用
    InetAddr(const std::string &ip, uint16_t port) : _ip(ip), _port(port)
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);
        _addr.sin_port = htons(_port);
    }
    
    // 4. 仅从端口构造(服务器监听用)
    // 用途: 服务器绑定端口,监听所有网卡
    InetAddr(uint16_t port) : _port(port), _ip()
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_addr.s_addr = INADDR_ANY;
        _addr.sin_port = htons(_port);
    }
    
    // ==================== 核心方法 ====================
    
    // SetAddr: 从网络地址解析出主机格式的 IP 和端口
    void SetAddr(struct sockaddr_in &addr)
    {
        _addr = addr;
        _port = ntohs(_addr.sin_port);
        char ipbuffer[64];
        inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(_addr));
        _ip = ipbuffer;
    }
    
    // ==================== Getter 方法 ====================
    
    uint16_t Port() { return _port; }
    std::string Ip() { return _ip; }
    const struct sockaddr_in &NetAddr() { return _addr; }
    
    // 获取网络地址指针 (转换为 sockaddr*)
    const struct sockaddr *NetAddrPtr()
    {
        return CONV(_addr);
    }
    
    socklen_t NetAddrLen()
    {
        return sizeof(_addr);
    }
    
    // ==================== 运算符重载 ====================
    
    bool operator==(const InetAddr &addr)
    {
        return addr._ip == _ip && addr._port == _port;
    }
    
    // ==================== 工具方法 ====================
    
    std::string StringAddr()
    {
        return _ip + ":" + std::to_string(_port);
    }
    
    ~InetAddr() {}

private:
    struct sockaddr_in _addr;   // 网络地址结构 (网络字节序)
    std::string _ip;            // IP地址 (主机格式,点分十进制)
    uint16_t _port;             // 端口号 (主机字节序)
};

这部分代码实现了一个轻量级的网络地址封装类 InetAddr,常见于各类 C++ 网络库中。它的核心作用是将底层的 C 风格 Socket 地址结构(struct sockaddr_in)面向对象化,简化 IP 和端口的转换、字节序处理以及地址信息的获取。

1.核心成员变量解析

cpp 复制代码
struct sockaddr_in _addr;    // IPv4 地址结构体(网络字节序)
std::string _ip;             // 主机格式的 IP 字符串(如 "127.0.0.1")
uint16_t _port;              // 主机字节序的端口号
  • _addr: 操作系统底层网络编程的标准结构体(定义在 <netinet/in.h> 中),包含了地址族、端口和 IP。所有传给系统调用(如 bind、connect)的数据最终都依赖于它。
  • _ip 与 _port: 缓存的主机端可读数据,方便应用层直接打印日志或进行逻辑处理,避免频繁调用转换函数。

2. 构造函数解析

a.客户端/指定 IP 构造函数

cpp 复制代码
InetAddr(const std::string &ip, uint16_t port) : _ip(ip), _port(port)
{
    memset(&_addr, 0, sizeof(_addr));
    _addr.sin_family = AF_INET;                             // 指定协议族为 IPv4
    inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);       // IP: 字符串 --> 网络字节序二进制
    _addr.sin_port = htons(_port);                          // 端口: 主机字节序 → 网络字节序
}
  • memset: 清空结构体内存,防止脏数据影响。
  • AF_INET: 声明使用 IPv4 互联网地址族。
  • inet_pton: Presentation to Network(点分十进制字符串转网络字节序二进制)。相比古老的 inet_addr,它支持 IPv6 且更加安全。
  • htons: Host to Network Short。计算机内部多采用小端序(Little-Endian),而网络传输规定使用大端序(Big-Endian/网络字节序),该函数将 16 位端口号转换为网络字节序。

b.服务器监听构造函数

cpp 复制代码
InetAddr(uint16_t port) : _port(port), _ip()
{
    memset(&_addr, 0, sizeof(_addr));
    _addr.sin_family = AF_INET;
    _addr.sin_addr.s_addr = INADDR_ANY;                     // 监听所有本地网卡
    _addr.sin_port = htons(_port);
}
步骤 代码 作用
_port(port), _ip() 保存端口号,IP初始化为空
memset(&_addr, 0, sizeof(_addr)) 清空结构体,防止垃圾数据
_addr.sin_family = AF_INET 设置IPv4协议族
_addr.sin_addr.s_addr = INADDR_ANY 监听所有网卡(0.0.0.0)
_addr.sin_port = htons(_port) 端口转网络字节序

INADDR_ANY: 宏定义(通常为 0x00000000),表示绑定到本机所有可用的网络接口(如有多个网卡或 IP,客户端连接任意一个均可被服务器接收)


3.成员函数解析

a. SetAddr(反向解析,通常用于 accept 接收新连接)

cpp 复制代码
void SetAddr(struct sockaddr_in &addr)
{
    _addr = addr;
    _port = ntohs(_addr.sin_port);                          // 端口: 网络 → 主机字节序
    char ipbuffer[64];
    inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(_addr)); // 网络字节序 → 字符串
    _ip = ipbuffer;
}
  • 当服务器调用 accept() 时,操作系统会填充客户端的 sockaddr_in。该函数将其存入内部,并通过 ntohs(Network to Host Short)将网络字节序的端口转回主机可读的格式。

  • inet_ntop : Network to Presentation,将二进制 IP 转为点分十进制字符串,是线程安全的(区别于废弃的 inet_ntoa)。

b.类型转换与辅助函数

cpp 复制代码
const struct sockaddr *NetAddrPtr() { return (struct sockaddr*)&_addr; }

强转指针 :诸如 bind()connect()accept() 等底层 C 统一 socket API 的参数类型都是 struct sockaddr*(通用地址结构),而内部用的是 struct sockaddr_in(IPv4 专用)。此处提供一个安全的向上造型接口,方便传参。

cpp 复制代码
socklen_t NetAddrLen() { return sizeof(_addr); }

返回地址结构体的大小,常用于 bind(&addr, len)accept(..., &len) 中的 addrlen 参数。

cpp 复制代码
std::string StringAddr() { return _ip + ":" + std::to_string(_port); }

返回形如 "192.168.1.100:8080" 的格式化字符串,极大地便利了服务器端打印连接日志(例如:"新连接来自 127.0.0.1:54321")。


4.2 互斥锁封装(Mutex.hpp)

这部分代码使用 C++ 封装了 POSIX 线程库(pthread)的互斥锁,并实现了现代 C++ 中极具代表性的 RAII(资源获取即初始化)锁管理机制。这在多线程编程中是非常标准且安全的写法(类似于 C++ 标准库中的 std::mutex 和 std::lock_guard)

cpp 复制代码
namespace MutexModule
{
    class Mutex
    {
    public:
        Mutex() { pthread_mutex_init(&_mutex, nullptr); }
        void Lock() { pthread_mutex_lock(&_mutex); }
        void Unlock() { pthread_mutex_unlock(&_mutex); }
        ~Mutex() { pthread_mutex_destroy(&_mutex); }
        pthread_mutex_t *Get() { return &_mutex; }
        
    private:
        pthread_mutex_t _mutex;
    };

    // RAII 锁管理:构造加锁,析构解锁
    class LockGuard
    {
    public:
        LockGuard(Mutex &mutex) : _mutex(mutex) { _mutex.Lock(); }
        ~LockGuard() { _mutex.Unlock(); }
        
    private:
        Mutex &_mutex;
    };
}

1.核心模块逐层解析

a.命名空间 MutexModule

cpp 复制代码
namespace MutexModule
{
    // ...
}

作用 :隔离作用域,防止类名(如 MutexLockGuard)与全局或其他第三方库发生命名冲突。

b.Mutex 类(底层互斥锁封装)

该类直接对 POSIX 的 pthread_mutex_t 进行了面向对象包装:

  • 构造函数 Mutex()
cpp 复制代码
Mutex() { pthread_mutex_init(&_mutex, nullptr); }

调用底层 API 初始化互斥锁。第二个参数传 nullptr 表示使用默认属性(通常是普通互斥锁)。

  • 加锁与解锁 Lock() / Unlock()
cpp 复制代码
void Lock() { pthread_mutex_lock(&_mutex); }
void Unlock() { pthread_mutex_unlock(&_mutex); }

包装了底层的阻塞式加锁和解锁操作。

  • 析构函数 ~Mutex()
cpp 复制代码
~Mutex() { pthread_mutex_destroy(&_mutex); }

对象销毁时自动销毁互斥锁,释放系统资源,防止内存泄漏。

  • 获取原生指针 Get()
cpp 复制代码
pthread_mutex_t *Get() { return &_mutex; }

提供一个"后门",当需要将此锁传给底层的依赖(例如条件变量 pthread_cond_wait)时,可以直接取出原生指针。


  • 防拷贝设计(隐患点提示)

这部分代码没有显式禁用拷贝构造和拷贝赋值。如果用户不小心复制了 Mutex 对象,会导致两个对象管理同一个 pthread_mutex_t,在析构时可能引发重复销毁或未定义行为。在工业级代码中,通常会使用 = delete 禁用拷贝:

cpp 复制代码
Mutex(const Mutex&) = delete;
Mutex& operator=(const Mutex&) = delete;

c. LockGuard 类(RAII 锁管理器)

这是并发编程防范死锁和忘记解锁的核心设计:

cs 复制代码
class LockGuard
{
public:
    LockGuard(Mutex &mutex) : _mutex(mutex) { _mutex.Lock(); }
    ~LockGuard() { _mutex.Unlock(); }
    
private:
    Mutex &_mutex;
};

RAII 机制:

  • 构造即加锁:当创建一个 LockGuard 局部对象并传入锁的引用时,它的构造函数会立刻执行 _mutex.Lock()。
  • 析构即解锁:当代码运行离开该局部变量的作用域(如函数执行完毕、遇到 return、或者抛出异常)时,LockGuard 的析构函数会被自动调用,从而安全地执行 _mutex.Unlock()。

**优势:**即使在临界区代码中发生了异常或者中途 return,也不用担心锁没有被释放而导致死锁(Deadlock)


4.3 日志系统(Log)

日志系统采用策略模式设计,支持控制台和文件两种输出策略,并可运行时切换。

cpp 复制代码
// 1. 策略基类
class LogStrategy
{
public:
    virtual void SyncLog(const std::string &message) = 0;
};

// 2. 控制台策略
class ConsoleLogStrategy : public LogStrategy
{
public:
    void SyncLog(const std::string &message) override
    {
        LockGuard guard(_mutex);
        std::cout << message << "\r\n";
    }
private:
    Mutex _mutex;
};

// 3. 文件策略
class FileLogStrategy : public LogStrategy
{
public:
    FileLogStrategy(const std::string &path = "/var/log/", 
                    const std::string &file = "my.log")
    {
        // C++17 文件系统自动创建目录
        if (!std::filesystem::exists(path))
            std::filesystem::create_directories(path);
    }
    
    void SyncLog(const std::string &message) override
    {
        LockGuard guard(_mutex);
        std::string filename = _path + "/" + _file;
        std::ofstream out(filename, std::ios::app);
        out << message << "\r\n";
    }
};

// 4. 日志等级
enum class LogLevel { DEBUG, INFO, WARNING, ERROR, FATAL };

// 5. 核心 Logger 类
class Logger
{
public:
    // LogMessage 内部类:RAII 自动输出
    class LogMessage
    {
    public:
        LogMessage(LogLevel level, std::string &file, int line, Logger &logger)
            : _logger(logger)
        {
            // 构建固定前缀
            std::stringstream ss;
            ss << "[" << GetTimeStamp() << "] "
               << "[" << Level2Str(level) << "] "
               << "[" << getpid() << "] "
               << "[" << file << ":" << line << "] - ";
            _loginfo = ss.str();
        }
        
        template <typename T>
        LogMessage &operator<<(const T &info)
        {
            std::stringstream ss;
            ss << info;
            _loginfo += ss.str();
            return *this;
        }
        
        ~LogMessage()
        {
            // 析构时自动输出!
            _logger._fflush_strategy->SyncLog(_loginfo);
        }
        
    private:
        std::string _loginfo;
        Logger &_logger;
    };
    
    // 创建临时 LogMessage 对象
    LogMessage operator()(LogLevel level, std::string file, int line)
    {
        return LogMessage(level, file, line, *this);
    }
    
    void EnableFileLogStrategy() { /* 切换策略 */ }
    void EnableConsoleLogStrategy() { /* 切换策略 */ }
    
private:
    std::unique_ptr<LogStrategy> _fflush_strategy;
};

// 全局对象和宏
Logger logger;
#define LOG(level) logger(level, __FILE__, __LINE__)
#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()

这部分代码实现了一个轻量级、现代且支持链式调用的 C++ 日志系统。它巧妙地结合了 策略模式(Strategy Pattern)RAII 流式日志机制(Stream-style & RAII),不仅支持多态输出(控制台、文件),还通过宏和临时对象完美模拟了类似于 std::cout 的连写体验,并能自动捕获代码行号和文件名。

1.架构与设计模式概述

整个日志系统主要分为两个核心维度:

  • 输出策略(Where to log):通过面向对象的多态,将日志的输出目的地(控制台、文件等)解耦。
  • 日志构造与触发(How to log):利用内部类 LogMessage 的生命周期,配合重载的 operator<< 和宏,实现"表达式结束即自动落盘"。

2.核心模块逐层解析

a.策略模式:日志输出端 (LogStrategy 及其子类)

cpp 复制代码
class LogStrategy
{
public:
    virtual void SyncLog(const std::string &message) = 0;
};
  • LogStrategy:抽象策略基类,定义统一接口 SyncLog。任何新的日志输出方式(如网络发送、写入数据库)只需继承并实现该接口即可。
  • ConsoleLogStrategy:控制台输出策略。内部使用前文提到的 Mutex 和 LockGuard 保证多线程并发向标准输出写日志时不会发生乱码(交叉输出)。
  • FileLogStrategy:文件输出策略。利用 C++17 的 <filesystem> 自动创建日志目录。

b.日志等级 (LogLevel)

cpp 复制代码
enum class LogLevel { DEBUG, INFO, WARNING, ERROR, FATAL };

采用 C++11 强类型枚举(enum class),避免命名冲突,用于区分日志的严重程度。

c.核心精髓:Logger::LogMessage(RAII 链式输出)

这是整个日志系统最精妙的部分,解决了"流式日志如何知道语句结束并触发写入"的问题。

  • 构造函数(收集元数据)
cpp 复制代码
LogMessage(LogLevel level, std::string &file, int line, Logger &logger)
    : _logger(logger)
{
    std::stringstream ss;
    ss << "[" << GetTimeStamp() << "] "
       << "[" << Level2Str(level) << "] "
       << "[" << getpid() << "] "
       << "[" << file << ":" << line << "] - ";
    _loginfo = ss.str();
}

当触发日志时,会立刻构造一个固定前缀(时间戳、等级、进程 PID、文件名、行号)。

  • 重载流操作符(链式拼接)
cpp 复制代码
template <typename T>
LogMessage &operator<<(const T &info)
{
    std::stringstream ss;
    ss << info;
    _loginfo += ss.str();
    return *this;
}

允许开发者像使用 std::cout 一样,用 << 拼接任意类型的数据(字符串、数字、自定义对象等),每次返回 *this 以支持连续调用。

  • 析构函数
cpp 复制代码
~LogMessage()
{
    _logger._fflush_strategy->SyncLog(_loginfo);
}

关键点 :当一行日志语句结束时(例如 LOG(INFO) << "hello" << 123;),表达式右侧创建的临时 LogMessage 对象生命周期结束,析构函数被自动调用。此时它把拼接好的完整字符串交由当前的策略对象(_fflush_strategy)进行实际输出。

d.宏与便捷调用

cpp 复制代码
Logger logger;
#define LOG(level) logger(level, __FILE__, __LINE__)
#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
  • __FILE____LINE__:编译期宏,分别代表当前代码所在的文件名和行号。

  • LOG(level) 宏展开

当你在代码中写下:

cpp 复制代码
LOG(INFO) << "Server init success, port: " << 8080;

它会被预处理器展开为:

cpp 复制代码
logger(LogLevel::INFO, "main.cpp", 25) << "Server init success, port: " << 8080;
  • logger(...) 调用 Logger::operator(),返回一个临时的 LogMessage 对象。
  • 连续调用 operator<< 拼接具体内容。
  • 语句结束(分号 ;),临时对象析构,自动调用 SyncLog 输出日志

4.4 工具函数(Util)

cpp 复制代码
class Util
{
public:
    // 读取文件全部内容
    static bool ReadFileContent(const std::string &filename, std::string *out)
    {
        std::ifstream in(filename);
        if (!in.is_open())
            return false;
        
        std::string line;
        while (std::getline(in, line))
        {
            *out += line;
            *out += "\n";  // 保留换行符
        }
        in.close();
        return true;
    }
    
    // 从大字符串中提取第一行(按分隔符)
    static bool ReadOneLine(std::string &bigstr, std::string *out, 
                           const std::string &sep = "\r\n")
    {
        auto pos = bigstr.find(sep);
        if (pos == std::string::npos)
            return false;
        
        *out = bigstr.substr(0, pos);
        bigstr.erase(0, pos + sep.size());
        return true;
    }
};

1.核心函数逐层解析

a.ReadFileContent ------ 读取整个文件内容

cpp 复制代码
static bool ReadFileContent(const std::string &filename, std::string *out)
{
    std::ifstream in(filename);
    if (!in.is_open())
        return false;
    
    std::string line;
    while (std::getline(in, line))
    {
        *out += line;
        *out += "\n";  // 保留换行符
    }
    in.close();
    return true;
}
  • 功能 :打开指定路径的文件,将文件所有内容逐行读取并拼接进输出参数 out 中,同时补上换行符。

  • 细节剖析

    • 参数设计out 使用指针(也可以用引用 std::string &out),代表这是一个输出型参数,带回读取结果。

    • 容错处理if (!in.is_open()) 能够有效防止因文件不存在、权限不足导致的崩溃或异常,直接返回 false

b. ReadOneLine ------ 提取并裁剪大字符串中的第一行

cpp 复制代码
static bool ReadOneLine(std::string &bigstr, std::string *out, 
                       const std::string &sep = "\r\n")
{
    auto pos = bigstr.find(sep);
    if (pos == std::string::npos)
        return false;
    
    *out = bigstr.substr(0, pos);
    bigstr.erase(0, pos + sep.size());
    return true;
}
  • 功能 :从一个大的字符串中(例如接收到的网络缓冲区数据),按指定的分隔符(默认是 HTTP 经典的 \r\n)提取第一行,并且会直接从原字符串 bigstr 中将这一行裁剪(擦除)掉

  • 经典应用场景 :网络编程中的 Protocol Parsing(协议解析)。比如解析 HTTP 报文时,需要一行行读取请求行和请求头:

cpp 复制代码
std::string buffer = "GET /index.html HTTP/1.1\r\nHost: localhost\r\n\r\n";
std::string line;
while (Util::ReadOneLine(buffer, &line)) {
    // 每次循环,buffer 都会自动变短,line 拿到当前行
}
  • 逻辑精妙处

    • 状态修改(Consume)bigstr.erase(0, pos + sep.size()) 具有"消费"数据的效果,非常适合配合 while 循环做流式解析。

    • 灵活性 :分隔符 sep 带有默认参数 "\r\n",但也支持传入 \n,适应不同的平台或协议标准。


五.网络层:Socket封装

Socket 层采用模板方法模式设计,定义统一的 Socket 接口,由子类实现具体协议(TCP/UDP)。

5.1 类图

5.2 核心代码

cpp 复制代码
// ==================== 抽象基类 ====================
class Socket
{
public:
    virtual ~Socket() {}
    
    // 纯虚函数
    virtual void SocketOrDie() = 0;
    virtual void BindOrDie(uint16_t port) = 0;
    virtual void ListenOrDie(int backlog) = 0;
    virtual std::shared_ptr<Socket> Accept(InetAddr *client) = 0;
    virtual void Close() = 0;
    virtual int Recv(std::string *out) = 0;
    virtual int Send(const std::string &message) = 0;
    virtual int Connect(const std::string &server_ip, uint16_t port) = 0;
    
    // ==================== 模板方法 ====================
    // 服务器创建流程:socket → bind → listen
    void BuildTcpSocketMethod(uint16_t port, int backlog = gbacklog)
    {
        SocketOrDie();      // 1. 创建 socket
        BindOrDie(port);    // 2. 绑定端口
        ListenOrDie(backlog); // 3. 开始监听
    }
    
    // 客户端创建流程:只需 socket
    void BuildTcpClientSocketMethod()
    {
        SocketOrDie();      // 只需创建 socket
    }
};

// ==================== TCP 实现 ====================
class TcpSocket : public Socket
{
public:
    TcpSocket() : _sockfd(defaultfd) {}
    TcpSocket(int fd) : _sockfd(fd) {}  // accept 返回的客户端 socket
    
    void SocketOrDie() override
    {
        _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0) {
            LOG(FATAL) << "socket error";
            exit(SOCKET_ERR);
        }
    }
    
    void BindOrDie(uint16_t port) override
    {
        InetAddr localaddr(port);
        int n = ::bind(_sockfd, localaddr.NetAddrPtr(), localaddr.NetAddrLen());
        if (n < 0) {
            LOG(FATAL) << "bind error";
            exit(BIND_ERR);
        }
    }
    
    void ListenOrDie(int backlog) override
    {
        int n = ::listen(_sockfd, backlog);
        if (n < 0) {
            LOG(FATAL) << "listen error";
            exit(LISTEN_ERR);
        }
    }
    
    std::shared_ptr<Socket> Accept(InetAddr *client) override
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int fd = ::accept(_sockfd, (struct sockaddr*)&peer, &len);
        if (fd < 0) {
            LOG(WARNING) << "accept warning ...";
            return nullptr;
        }
        client->SetAddr(peer);
        return std::make_shared<TcpSocket>(fd);
    }
    
    int Recv(std::string *out) override
    {
        char buffer[4096];
        ssize_t n = ::recv(_sockfd, buffer, sizeof(buffer) - 1, 0);
        if (n > 0) {
            buffer[n] = 0;
            *out += buffer;
        }
        return n;
    }
    
    int Send(const std::string &message) override
    {
        return ::send(_sockfd, message.c_str(), message.size(), 0);
    }
    
    void Close() override
    {
        if (_sockfd >= 0)
            ::close(_sockfd);
    }
    
    int Connect(const std::string &server_ip, uint16_t port) override
    {
        InetAddr server(server_ip, port);
        return ::connect(_sockfd, server.NetAddrPtr(), server.NetAddrLen());
    }
    
private:
    int _sockfd;
};

这部分代码实现了一个面向对象的网络套接字(Socket)抽象层,采用了多态(Polymorphism) 模板方法/流程封装(Template Method / Pipeline Pattern)设计。它将底层的 C 风格 Socket API 进行了优雅的面向对象包装,并且与前面分析过的 InetAddr(地址类)和 LOG(日志系统)实现了无缝联动。

1.核心设计思想与架构

整个体系分为两层:

  • Socket(抽象基类/接口):规定了网络通信的通用行为契约(如创建、绑定、监听、收发等),并提供了标准化的服务器/客户端构建流程。

  • TcpSocket(具体实现类):实现了针对 TCP 协议的具体细节,屏蔽了繁琐的系统调用。


2. 抽象基类 Socket 解析

cpp 复制代码
class Socket
{
public:
    virtual ~Socket() {}
    
    // 纯虚函数(接口定义)
    virtual void SocketOrDie() = 0;
    virtual void BindOrDie(uint16_t port) = 0;
    // ...
  • 虚析构函数 virtual ~Socket() :在多态编程中至关重要。当通过基类指针销毁子类对象时,能够确保正确调用子类(如 TcpSocket)的析构函数,防止内存泄漏。

  • OrDie 命名规范:带有该后缀的方法代表"如果操作失败,程序将无法继续运行",通常会直接记录日志并终止程序(Fail-fast 思想)。

流程封装方法

cpp 复制代码
// 服务器创建流程:socket → bind → listen
    void BuildTcpSocketMethod(uint16_t port, int backlog = gbacklog)
    {
        SocketOrDie();      // 1. 创建 socket
        BindOrDie(port);    // 2. 绑定端口
        ListenOrDie(backlog); // 3. 开始监听
    }
    
    // 客户端创建流程:只需 socket
    void BuildTcpClientSocketMethod()
    {
        SocketOrDie();      // 只需创建 socket
    }

这两段代码简化了上层调用者的逻辑。服务器初始化 TCP 套接字时不需要写三次零散的调用,只需要调一个 BuildTcpSocketMethod 即可串联起完整的初始化流程。


3.具体实现类 TcpSocket 解析

a.构造函数的设计

cpp 复制代码
TcpSocket() : _sockfd(defaultfd) {}
TcpSocket(int fd) : _sockfd(fd) {}  // accept 返回的客户端 socket

双构造函数设计

  • 无参构造用于主动创建套接字(如服务器监听套接字、客户端连接套接字)。

  • 带参数的构造用于 accept() 成功后,将操作系统内核返回的已连接套接字文件描述符(fd)直接包装成一个 TcpSocket 对象。

b.生命周期与核心系统调用包装

  • SocketOrDie : 封装了 ::socket(AF_INET, SOCK_STREAM, 0)。如果返回值小于 0,直接利用前面的 LOG(FATAL) 宏打印致命错误并退出。

  • BindOrDie : 巧妙地复用了前文的 InetAddr 类:

cpp 复制代码
InetAddr localaddr(port);
int n = ::bind(_sockfd, localaddr.NetAddrPtr(), localaddr.NetAddrLen());

这让底层的 bind 变得极其清爽,完全不需要手动去填写一堆 sockaddr_in 的强转逻辑。

  • Accept(多态工厂方法的雏形)
cpp 复制代码
std::shared_ptr<Socket> Accept(InetAddr *client) override
{
    struct sockaddr_in peer;
    socklen_t len = sizeof(peer);
    int fd = ::accept(_sockfd, (struct sockaddr*)&peer, &len);
    if (fd < 0) {
        LOG(WARNING) << "accept warning ...";
        return nullptr;
    }
    client->SetAddr(peer);          // 回填客户端的网络地址信息
    return std::make_shared<TcpSocket>(fd); // 返回智能指针管理的子类对象
}

返回类型是 std::shared_ptr<Socket>,实现了面向对象的多态返回。

自动通过 client->SetAddr(peer) 帮调用者解析出对端(客户端)的 IP 和端口。

c. 数据收发与连接

Recv

cpp 复制代码
char buffer[4096];
ssize_t n = ::recv(_sockfd, buffer, sizeof(buffer) - 1, 0);
if (n > 0) {
    buffer[n] = 0; // 保证字符串以 \0 结尾
    *out += buffer;
}
return n;

读取数据并安全地追加到 out 字符串中。

Connect

客户端调用,内部同样利用 InetAddr 快速构造服务端的地址并调用底层 ::connect


5.3 设计要点

要点 说明
模板方法模式 BuildTcpSocketMethod 定义标准流程,子类实现具体步骤
多态性 基类指针可以指向 TcpSocket,便于扩展 UDP
智能指针 Accept 返回 shared_ptr<Socket>,自动管理资源
封装 InetAddr 隐藏 sockaddr_in 细节,自动处理字节序

六.服务层:TCP服务器

TcpServer 是服务器的核心调度器,负责监听端口、接受连接、处理并发。

6.1 并发模型

采用多进程模型 ,使用双重 fork 技巧避免僵尸进程:

cpp 复制代码
父进程 (TcpServer)
    │
    ├── while 循环:
    │    ├── Accept() 接受客户端连接
    │    ├── fork() 创建子进程
    │    │
    │    ▼
    └── 子进程:
         ├── 关闭监听 socket
         ├── fork() 创建孙子进程
         ├── 立即 exit() ← 父进程 waitpid 回收
         │
         ▼
    孙子进程 (孤儿,由 init 收养):
         ├── callback(sock, client) 执行业务逻辑
         ├── 关闭客户端 socket
         └── exit() ← init 回收

6.2 核心代码

cpp 复制代码
using ioservice_t = std::function<void(std::shared_ptr<Socket> &sock, InetAddr &client)>;

class TcpServer
{
public:
    TcpServer(uint16_t port) : _port(port),
                               _listensockptr(std::make_unique<TcpSocket>()),
                               _isrunning(false)
    {
        _listensockptr->BuildTcpSocketMethod(_port);
    }
    
    void Start(ioservice_t callback)
    {
        _isrunning = true;
        while (_isrunning)
        {
            // 1. 接受客户端连接
            InetAddr client;
            auto sock = _listensockptr->Accept(&client);
            if (sock == nullptr)
                continue;
            
            LOG(DEBUG) << "accept success ..." << client.StringAddr();

            // 2. 双重 fork 处理请求
            pid_t id = fork();
            if (id == 0)
            {
                // === 子进程 ===
                _listensockptr->Close();  // 关闭监听 socket
                
                // 创建孙子进程
                if (fork() > 0)
                    exit(OK);  // 子进程退出,父进程回收
                
                // === 孙子进程(孤儿) ===
                callback(sock, client);   // 执行业务逻辑
                sock->Close();
                exit(OK);
            }
            else
            {
                // === 父进程 ===
                sock->Close();            // 关闭客户端 socket
                waitpid(id, nullptr, 0);  // 回收子进程
            }
        }
        _isrunning = false;
    }
    
private:
    uint16_t _port;
    std::unique_ptr<Socket> _listensockptr;
    bool _isrunning;
};

这部分代码实现了一个基于多进程并发模型(Multiprocessing Concurrency Model)的 TCP 服务器框架 (TcpServer)。它巧妙地结合了前面实现的 Socket 抽象层、InetAddr 地址类、LOG 日志系统,并通过现代 C++ 的 std::function 实现了解耦的业务回调机制。

1.核心类型定义:回调函数 (ioservice_t)

cpp 复制代码
using ioservice_t = std::function<void(std::shared_ptr<Socket> &sock, InetAddr &client)>;
  • 解耦业务与网络TcpServer 只负责底层的网络监听和连接接收(网络骨架),而具体的业务逻辑(如回显服务、HTTP 请求处理)交由外部通过回调函数传入。

  • 参数说明 :回调函数接收两个参数------客户端的套接字智能指针(sock)和客户端的网络地址对象(client),方便业务层知道"是谁发来的消息"并进行读写。


2.构造函数与服务器初始化

cpp 复制代码
TcpServer(uint16_t port) : _port(port),
                           _listensockptr(std::make_unique<TcpSocket>()),
                           _isrunning(false)
{
    _listensockptr->BuildTcpSocketMethod(_port);
}
  • 多态运用_listensockptr 是基类指针(std::unique_ptr<Socket>),但实际指向子类 TcpSocket

  • 一行代码完成初始化 :调用了前面在 Socket 中定义的模板方法 BuildTcpSocketMethod(_port),内部自动串联了 socket()bind()listen() 三步。


3.核心主循环与并发模型(重点:双重 fork

Start 方法是服务器的运行核心。为了能够同时服务多个客户端,这里采用了经典的多进程并发模型,并且使用了高级技巧------双重 fork(Double Fork / 孤儿进程回收法)来彻底解决僵尸进程问题。

a. 接受连接

cpp 复制代码
InetAddr client;
auto sock = _listensockptr->Accept(&client);
if (sock == nullptr)
    continue;

LOG(DEBUG) << "accept success ..." << client.StringAddr();

当有新客户端连接时,Accept 会返回一个专属于该客户端的 TcpSocket 智能指针,并通过 client 回填客户端的 IP 和端口。

b.双重 fork 机制解析

cpp 复制代码
pid_t id = fork();
if (id == 0)
{
    // === 子进程 ===
    _listensockptr->Close(); // 关闭继承自父进程的监听 socket
    
    // 创建孙子进程
    if (fork() > 0)
        exit(OK); // 子进程直接退出,由父进程立刻回收
    
    // === 孙子进程(孤儿进程) ===
    callback(sock, client);   // 执行具体的业务逻辑
    sock->Close();
    exit(OK);
}
else
{
    // === 父进程(服务器主进程) ===
    sock->Close();            // 关闭父进程中的客户端 socket(防止文件描述符泄漏)
    waitpid(id, nullptr, 0);  // 阻塞回收子进程
}

为什么要用"双重 fork"?

  1. 直观的单 fork 痛点 :如果只 fork 一次,子进程去处理业务,父进程(主进程)必须调用 waitpid 或异步回收子进程。如果不回收就会产生僵尸进程(Zombie Process)耗尽系统资源;如果用阻塞的 waitpid 回收,主进程就会被卡住,无法及时接收下一个客户端的连接。

  2. 双重 fork 的精妙解法

    • 第一步 :主进程 fork子进程

    • 第二步子进程 立刻再 fork孙子进程 ,然后子进程自己立刻退出(exit(OK)

    • 第三步 :主进程由于调用了 waitpid(id, nullptr, 0),会瞬间回收刚刚退出的子进程(因为子进程几乎不做任何事,瞬间就结束了,主进程几乎不卡顿)。

    • 第四步 :那个真正的"孙子进程"变成了孤儿进程 ,被系统的 init 进程(或 systemd)收养。它会在后台独立运行用户的业务逻辑 callback(sock, client),业务跑完后自动退出,由系统负责清理,完美实现了既并发、又不用主进程费心去异步回收的异步效果

c.文件描述符的关闭(极其重要)

  • 父进程中sock->Close();。因为 Accept 返回的 sock 会被拷贝给子进程,父进程不需要这个客户端的 fd,必须关闭,否则会导致文件描述符泄漏(File Descriptor Leak),文件描述符很快就会被耗尽。

  • 子进程中_listensockptr->Close();。子进程不需要负责监听新的连接,只负责服务当前客户端,因此必须关闭监听套接字。


七.协议层:HTTP处理

7.1 HTTP 请求格式

7.2 HttpRequest 类

cpp 复制代码
class HttpRequest
{
public:
    void ParseReqLine(std::string &reqline)
    {
        std::stringstream ss(reqline);
        ss >> _method >> _uri >> _version;
    }
    
    bool Deserialize(std::string &reqstr)
    {
        // 1. 提取请求行
        std::string reqline;
        Util::ReadOneLine(reqstr, &reqline, "\r\n");
        ParseReqLine(reqline);
        
        LOG(DEBUG) << "_method: " << _method;
        LOG(DEBUG) << "_uri: " << _uri;
        LOG(DEBUG) << "_version: " << _version;
        
        // 2. URI 映射到本地文件路径
        if (_uri == "/")
            _uri = webroot + _uri + homepage;      // ./wwwroot/index.html
        else
            _uri = webroot + _uri;                 // ./wwwroot/a/b/c.html
            
        return true;
    }
    
    std::string Uri() { return _uri; }

private:
    std::string _method;   // GET, POST, ...
    std::string _uri;      // /index.html → ./wwwroot/index.html
    std::string _version;  // HTTP/1.1
};

这部分代码实现了一个简易的 HTTP 请求解析与路由映射类 HttpRequest。有 HTTP 请求的最核心部分------请求行(Request Line),并将网上的 URL 路径安全或直接地映射为本地服务器的文件路径。

1. 核心成员变量解析

cpp 复制代码
std::string _method;   // 请求方法,如 GET, POST
std::string _uri;      // 资源路径,初始为 /index.html,随后被改写为本地文件路径 ./wwwroot/index.html
std::string _version;  // HTTP 协议版本,如 HTTP/1.1

这三个字段构成了 HTTP 请求行(Request Line)的标准三要素。例如对于请求 GET /index.html HTTP/1.1,它们分别会被解析为 GET/index.htmlHTTP/1.1


2.核心成员函数解析

a.ParseReqLine ------ 解析请求行

cpp 复制代码
void ParseReqLine(std::string &reqline)
{
    std::stringstream ss(reqline);
    ss >> _method >> _uri >> _version;
}
  • 工作原理 :利用 std::stringstream 默认以空白字符(空格、Tab等)为分隔符的特性,一气呵成地将一行请求字符串拆解并赋值给 _method_uri_version

  • 简洁高效:这是 C++ 中解析空格分隔文本最经典的写法。

b. Deserialize ------ 反序列化与路径映射

这是整个类的逻辑核心,负责把从网络中读到的原始 HTTP 文本进行"解码"和"本地化"。

cpp 复制代码
bool Deserialize(std::string &reqstr)
{
    // 1. 提取请求行
    std::string reqline;
    Util::ReadOneLine(reqstr, &reqline, "\r\n");
    ParseReqLine(reqline);
    
    LOG(DEBUG) << "_method: " << _method;
    LOG(DEBUG) << "_uri: " << _uri;
    LOG(DEBUG) << "_version: " << _version;
    
    // 2. URI 映射到本地文件路径
    if (_uri == "/")
        _uri = webroot + _uri + homepage;      // ./wwwroot/index.html
    else
        _uri = webroot + _uri;                 // ./wwwroot/a/b/c.html
        
    return true;
}
  • 步骤 1:剥离请求行 : 调用了前文分析过的 Util::ReadOneLine,从原始 HTTP 报文 reqstr 中切出第一行(以 \r\n 结尾的请求行),并交由 ParseReqLine 处理。此时,reqstr 内部的请求行已经被 ReadOneLine 自动从头部擦除了,留下了后续的请求头(Headers)和正文(Body)。

  • 步骤 2:Web 根目录映射(Web Root Mapping): 这是 Web 服务器实现静态资源访问的关键步骤:

    • 如果用户访问的是根路径 /,自动将其拼装为 webroot + "/" + homepage(例如 ./wwwroot/index.html)。

    • 如果用户访问的是具体文件(如 /a/b/c.html),则直接拼装为 webroot + "/a/b/c.html"(即 ./wwwroot/a/b/c.html)。

  • 经过这一步转换后,原本抽象的网络 _uri 变为了实实在在的本地磁盘文件路径 ,后续服务器就可以直接通过 open()std::ifstream 读取该文件并返回给客户端。


7.3 HttpResponse 类

cpp 复制代码
class HttpResponse
{
public:
    std::string Serialize()
    {
        // 1. 状态行: HTTP/1.1 200 OK\r\n
        std::string status_line = _version + " " + 
                                  std::to_string(_code) + " " + _desc + "\r\n";
        
        // 2. 头部
        std::string resp_header;
        for (auto &header : _headers)
        {
            resp_header += header.first + ": " + header.second + "\r\n";
        }
        
        // 3. 完整响应
        return status_line + resp_header + _blankline + _text;
    }
    
    bool MakeResponse()
    {
        bool res = Util::ReadFileContent(_targetfile, &_text);
        if (!res)
        {
            _code = 404;
            _desc = "Not Found";
        }
        return res;
    }
    
    void SetTargetFile(const std::string &target)
    {
        _targetfile = target;
    }

public:
    std::string _version;      // HTTP/1.1
    int _code;                 // 200, 404
    std::string _desc;         // OK, Not Found
    std::unordered_map<std::string, std::string> _headers;
    std::string _blankline;    // "\r\n"
    std::string _text;         // 响应正文
    std::string _targetfile;   // 目标文件路径
};

这段代码实现了一个简易的 HTTP 响应构建与序列化类 HttpResponse。它负责将服务器端准备好的状态信息、响应头、空行以及文件正文(Body),组装成符合 HTTP 协议标准的原始字节流,准备发送回客户端。

1.核心成员变量解析

cpp 复制代码
std::string _version;      // HTTP 协议版本(如 HTTP/1.1)
int _code;                 // 状态码(如 200, 404)
std::string _desc;         // 状态描述(如 OK, Not Found)
std::unordered_map<std::string, std::string> _headers; // 响应头部键值对(如 Content-Type, Content-Length)
std::string _blankline;    // 空行,固定为 "\r\n",用于分隔头部与正文
std::string _text;         // 响应正文(通常是读取到的网页 HTML 文件内容)
std::string _targetfile;   // 客户端请求的目标文件磁盘路径

2.核心成员函数解析

a. Serialize------ 序列化(组装 HTTP 响应报文)

HTTP 协议规定,一个标准的响应报文由状态行 + 响应报头 + 空行 + 响应正文四部分组成。该函数完美地遵循了这一规范:

cpp 复制代码
std::string Serialize()
{
    // 1. 状态行: HTTP/1.1 200 OK\r\n
    std::string status_line = _version + " " + 
                              std::to_string(_code) + " " + _desc + "\r\n";
    
    // 2. 头部
    std::string resp_header;
    for (auto &header : _headers)
    {
        resp_header += header.first + ": " + header.second + "\r\n";
    }
    
    // 3. 完整响应
    return status_line + resp_header + _blankline + _text;
}
  • 状态行 :格式为 版本 状态码 描述\r\n(例如:HTTP/1.1 200 OK\r\n)。

  • 响应头 :遍历哈希表 _headers,将所有头部字段(如 Content-Type: text/html)按 Key: Value\r\n 的格式拼接起来。

  • 最终组装 :通过 状态行 + 响应头 + 空行(_blankline) + 正文(_text) 拼成一个完整的 HTTP 报文字符串,可以直接通过 Socket::Send 发送给客户端。

b. MakeResponse ------ 业务核心:文件读取与状态决策

cpp 复制代码
bool MakeResponse()
{
    bool res = Util::ReadFileContent(_targetfile, &_text);
    if (!res)
    {
        _code = 404;
        _desc = "Not Found";
    }
    return res;
}
  • 文件加载 :尝试通过前面见过的工具类 Util::ReadFileContent_targetfile(如 ./wwwroot/index.html)的内容读取到正文 _text 中。

  • 异常降级处理(404 兜底)

    • 如果文件读取成功(返回 true),说明资源存在。

    • 如果读取失败(返回 false,例如用户访问了一个不存在的页面),则立刻将状态码修改为 404,描述改为 Not Found

c.SetTargetFile ------ 设置目标文件

cpp 复制代码
void SetTargetFile(const std::string &target)
{
    _targetfile = target;
}

这是一个简单的 Setter 函数,用于接收由 HttpRequest 解析并转换好的本地文件路径,为后续的 MakeResponse 做准备。


7.4 Http 主控类

这段代码实现了一个简易的 HTTP 服务器的主控类 Http。它扮演了 门面模式(Facade Pattern)控制器(Controller) 的角色,将前面实现的所有底层模块(TcpServerHttpRequestHttpResponse)有机地串联起来,构成了完整的 Web 服务器业务流。

1.核心架构与设计角色

Http 类内部持有一个 TcpServer 的智能指针,把网络通信和 HTTP 协议解析完美地粘合在了一起。当外部调用 Http::Start() 时,实际上是启动了底层服务器,并在有新连接到来时触发 HTTP 处理流程。


2.核心成员函数解析

a. HandlerHttpRquest ------ HTTP 请求处理流水线(Pipeline)

这是整个 Web 服务器最核心的业务逻辑函数,严格遵循了经典的 "接收 → 解析 → 构建 → 发送" 四步流水线:

cpp 复制代码
void HandlerHttpRquest(std::shared_ptr<Socket> &sock, InetAddr &client)
{
    // 1. 接收请求
    std::string httpreqstr;
    int n = sock->Recv(&httpreqstr);
    if (n <= 0) return;
    
    // 2. 解析请求
    HttpRequest req;
    req.Deserialize(httpreqstr);
    
    // 3. 构建响应
    HttpResponse resp;
    resp.SetTargetFile(req.Uri());
    resp.MakeResponse();
    
    // 4. 发送响应
    std::string response_str = resp.Serialize();
    sock->Send(response_str);
}
  • 步骤 1(Recv) :通过客户端套接字 sock 将浏览器发过来的原始 HTTP 报文读取到字符串 httpreqstr 中。

  • 步骤 2(Deserialize) :实例化 HttpRequest,调用 Deserialize 提取出请求行、计算出对应的本地文件路径(如 ./wwwroot/index.html)。

  • 步骤 3(MakeResponse) :实例化 HttpResponse,把目标文件路径传进去,尝试读取文件内容构建响应正文(如果文件不存在则自动变为 404)。

  • 步骤 4(Serialize & Send) :调用 resp.Serialize() 将状态行、头部、正文打包成符合 HTTP 规范的完整报文,通过 sock->Send() 一气呵成发回给客户端浏览器。

b.Start ------ 启动服务器与回调绑定

cpp 复制代码
void Start()
{
    tsvrp->Start([this](std::shared_ptr<Socket> &sock, InetAddr &client) {
        this->HandlerHttpRquest(sock, client);
    });
}
  • Lambda 表达式与闭包 :调用了 TcpServer::Start,并传入了一个 Lambda 表达式作为业务回调。

  • [this] 捕获 :由于 HandlerHttpRquestHttp 类的成员函数,必须捕获 this 指针才能在回调中调用它。

  • 高层解耦TcpServer 只管无脑地接收连接、调用回调,而"回调里面具体做什么(是处理 HTTP 协议,还是做简单的 Echo 回显)"完全由 Http 类接管,体现了极佳的模块分层。


3.总结:整套网络库的闭环

至此,通过这一系列代码亲手拼凑出了一个迷你的、面向对象的 C++ Web 服务器:

  1. Mutex / LockGuard:多线程并发安全。

  2. InetAddr:IP/端口与字节序的面向对象管理。

  3. Socket / TcpSocket:网络底层系统调用的多态封装。

  4. TcpServer :基于双重 fork 的多进程并发网络骨架。

  5. HttpRequest / HttpResponse:HTTP 协议的解析与序列化。

  6. Http:将网络骨架与 HTTP 协议粘合在一起的整体控制器。


7.5 URI 映射规则

请求 URI 映射文件 说明
/ ./wwwroot/index.html 默认首页
/about.html ./wwwroot/about.html 静态 HTML
/css/style.css ./wwwroot/css/style.css CSS 文件
/images/logo.png ./wwwroot/images/logo.png 图片文件

八.程序入口与构建

8.1 主程序(Main.cc

cpp 复制代码
#include "Http.hpp"

int main(int argc, char *argv[])
{
    // 参数检查
    if (argc != 2)
    {
        std::cout << "Usage: " << argv[0] << " port" << std::endl;
        exit(USAGE_ERR);
    }
    
    // 解析端口
    uint16_t port = std::stoi(argv[1]);
    
    // 创建并启动服务器
    std::unique_ptr<Http> httpsvr = std::make_unique<Http>(port);
    httpsvr->Start();
    
    return 0;
}

这段代码是整个轻量级 Web 服务器的程序入口(main 函数)。它负责接收命令行参数、初始化服务器实例并启动整个网络监听循环,为整个项目画上了完美的闭环。

1. 核心逻辑逐层解析

a. 命令行参数检查(防御性编程)

cpp 复制代码
if (argc != 2)
{
    std::cout << "Usage: " << argv[0] << " port" << std::endl;
    exit(USAGE_ERR);
}
  • 作用:确保用户在启动程序时正确输入了端口号。

  • 规则argc 必须等于 2(argv[0] 是程序自身的运行路径,argv[1] 是传入的端口号,例如 ./httpserver 8080)。如果参数不对,打印标准的使用提示(Usage)并以错误码退出,防止程序因缺少配置而崩溃。

b. 端口转换

cpp 复制代码
uint16_t port = std::stoi(argv[1]);

作用 :命令行传入的参数本质上是字符串,这里通过 std::stoi(String to Integer)将其转换为整型,并用 uint16_t(16位无符号整型,刚好对应端口号的最大范围 0~65535)接收。

c.创建并启动服务器

cpp 复制代码
std::unique_ptr<Http> httpsvr = std::make_unique<Http>(port);
httpsvr->Start();
  • 现代 C++ 内存管理 :使用 std::unique_ptr 搭配 std::make_unique 在堆上动态创建 Http 服务器对象。这不仅代码优雅,而且利用了 RAII 机制,当 main 函数意外退出时会自动释放内存,绝不发生内存泄漏。

  • 启动引擎 :调用 httpsvr->Start()。这一步会直接触发底层 TcpServer 的主循环,开始阻塞监听端口、处理来自浏览器的 HTTP 请求。


**2.总结:**从底层到顶层的整个 C++ Web 服务器的核心架构:

  • 基础设施Mutex(锁)、InetAddr(网络地址)、Util(文件/字符串工具)。

  • 网络层Socket / TcpSocket(多态套接字)、TcpServer(多进程并发骨架)。

  • 应用层HttpRequest / HttpResponse(HTTP 报文解析与序列化)。

  • 控制层与入口Http(业务粘合控制器)与当前的 main.cpp


8.2 构建配置(Makefile)

cpp 复制代码
myhttp: Main.cc
	g++ -o $@ $^ -std=c++17

.PHONY: clean
clean:
	rm -f myhttp

九.运行与验证

浏览器访问

cpp 复制代码
http://localhost:8080
cpp 复制代码
http://127.0.0.1:8080

localhost 是一个域名 ,操作系统在解析域名时,会先查 hosts 文件(Linux 下是 /etc/hosts,Windows 是 C:\Windows\System32\drivers\etc\hosts),里面有一行:

cpp 复制代码
127.0.0.1 localhost

所以 localhost 最终被解析成 127.0.0.1,两者指向同一个 IP 地址。

下一步:测试其他页面

测试其他页面

访问地址 预期结果
http://localhost:8080/Login.html 登录页面
http://localhost:8080/Register.html 注册页面
http://localhost:8080/test.html 测试页面("点我"链接)
http://localhost:8080/notexist.html 404 Not Found

不同地址的区别

访问方式 含义 谁能访问
localhost:8080 本机域名 只有你自己
127.0.0.1:8080 本机IP 只有你自己
192.168.1.100:8080 局域网IP 同一局域网的人
公网IP:8080 公网IP 互联网上所有人

十.总结

到这里,我们的 HTTP 服务器就全部完成了。回顾一下,我们做了这些事:

模块 文件 核心功能
工具层 InetAddr.hpp、Mutex.hpp、Log.hpp、Util.hpp 地址转换、互斥锁、日志、文件读取
网络层 Socket.hpp Socket 系统调用封装
服务层 TcpServer.hpp TCP 连接管理、多进程并发
协议层 Http.hpp HTTP 协议解析与响应
入口 Main.cc 程序启动

最终,我们用 6 个核心文件 ,实现了:你在浏览器输入 http://localhost:8080 并按下回车,背后发生了一系列复杂的事情

整个项目虽然只有几百行代码,但涵盖了 Socket 编程、HTTP 协议、多进程并发、设计模式等核心知识点。麻雀虽小,五脏俱全。

如果你能跟着文章一步步走到这里,那么,你已经对网络编程有了实实在在的理解。这不是纸上谈兵,而是真正能跑起来的代码。

相关推荐
吴声子夜歌1 小时前
Shell脚本——数组
linux·运维·shell
yuewell_ai1 小时前
具身机器人一多服务器就崩-告别轮询上事件驱动
运维·服务器·机器人
道尔柯南1 小时前
【Linux】进程信号----1
linux·运维·服务器
逐流人1 小时前
华为云服务全景指南:从基础架构到运维实践
运维·华为云·云计算·云服务·ecs
豆芽脚脚1 小时前
华为鲲鹏arm服务器部署DeepSeek-V4-Flash-0731-w8a8
运维·服务器·arm开发
Mortalbreeze2 小时前
深入理解 Linux IO 模型(四):多路复用 —— epoll
linux·运维·服务器·网络·tcp/ip
隐擎fox2 小时前
网络传输中的 DNS 泄漏成因剖析与 Python 自动化检测排查实战
python·网络协议·自动化·dns·ip/tcp
lucybean012 小时前
平板曝气器靠谱供应商甄别方法及选型策略解析
运维
阿里云云原生2 小时前
云原生可观测性落地:乐檬通过自然语言观测与自动巡检提升运维效能的最佳实践
运维·云原生·starops·云监控2.0