欢迎光临
我们一直在努力

【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,自动处理字节序转换。

#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.核心成员变量解析

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 构造函数

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.服务器监听构造函数

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 接收新连接)

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.类型转换与辅助函数

const struct sockaddr *NetAddrPtr() { return (struct sockaddr*)&_addr; }

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

socklen_t NetAddrLen() { return sizeof(_addr); }

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

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)

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

namespace MutexModule
{
// …
}

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

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

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

  • 构造函数 Mutex():

Mutex() { pthread_mutex_init(&_mutex, nullptr); }

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

  • 加锁与解锁 Lock() / Unlock():

void Lock() { pthread_mutex_lock(&_mutex); }
void Unlock() { pthread_mutex_unlock(&_mutex); }

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

  • 析构函数 ~Mutex():

~Mutex() { pthread_mutex_destroy(&_mutex); }

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

  • 获取原生指针 Get()

pthread_mutex_t *Get() { return &_mutex; }

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


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

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

Mutex(const Mutex&) = delete;
Mutex& operator=(const Mutex&) = delete;

c. LockGuard 类(RAII 锁管理器)

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

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)

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

// 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 及其子类)

class LogStrategy
{
public:
virtual void SyncLog(const std::string &message) = 0;
};

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

b.日志等级 (LogLevel)

enum class LogLevel { DEBUG, INFO, WARNING, ERROR, FATAL };

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

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

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

  • 构造函数(收集元数据):

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、文件名、行号)。

  • 重载流操作符(链式拼接):

template <typename T>
LogMessage &operator<<(const T &info)
{
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}

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

  • 析构函数

~LogMessage()
{
_logger._fflush_strategy->SyncLog(_loginfo);
}

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

d.宏与便捷调用

Logger logger;
#define LOG(level) logger(level, __FILE__, __LINE__)
#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()

  • __FILE__ 与 __LINE__:编译期宏,分别代表当前代码所在的文件名和行号。

  • LOG(level) 宏展开:

当你在代码中写下:

LOG(INFO) << "Server init success, port: " << 8080;

它会被预处理器展开为:

logger(LogLevel::INFO, "main.cpp", 25) << "Server init success, port: " << 8080;

  • logger(…) 调用 Logger::operator(),返回一个临时的 LogMessage 对象。
  • 连续调用 operator<< 拼接具体内容。
  • 语句结束(分号 ;),临时对象析构,自动调用 SyncLog 输出日志

4.4 工具函数(Util)

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 —— 读取整个文件内容

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 —— 提取并裁剪大字符串中的第一行

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 报文时,需要一行行读取请求行和请求头:

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 核心代码

// ==================== 抽象基类 ====================
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 解析

class Socket
{
public:
virtual ~Socket() {}

// 纯虚函数(接口定义)
virtual void SocketOrDie() = 0;
virtual void BindOrDie(uint16_t port) = 0;
// …

  • 虚析构函数 virtual ~Socket():在多态编程中至关重要。当通过基类指针销毁子类对象时,能够确保正确调用子类(如 TcpSocket)的析构函数,防止内存泄漏。

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

流程封装方法

// 服务器创建流程: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.构造函数的设计

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 类:

InetAddr localaddr(port);
int n = ::bind(_sockfd, localaddr.NetAddrPtr(), localaddr.NetAddrLen());

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

  • Accept(多态工厂方法的雏形)

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:

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 技巧避免僵尸进程:

父进程 (TcpServer)

├── while 循环:
│ ├── Accept() 接受客户端连接
│ ├── fork() 创建子进程
│ │
│ ▼
└── 子进程:
├── 关闭监听 socket
├── fork() 创建孙子进程
├── 立即 exit() ← 父进程 waitpid 回收


孙子进程 (孤儿,由 init 收养):
├── callback(sock, client) 执行业务逻辑
├── 关闭客户端 socket
└── exit() ← init 回收


6.2 核心代码

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)

using ioservice_t = std::function<void(std::shared_ptr<Socket> &sock, InetAddr &client)>;

  • 解耦业务与网络:TcpServer 只负责底层的网络监听和连接接收(网络骨架),而具体的业务逻辑(如回显服务、HTTP 请求处理)交由外部通过回调函数传入。

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


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

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. 接受连接

InetAddr client;
auto sock = _listensockptr->Accept(&client);
if (sock == nullptr)
continue;

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

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

b.双重 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); // 阻塞回收子进程
}

为什么要用“双重 fork”?

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

  • 双重 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 类

    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. 核心成员变量解析

    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.html 和 HTTP/1.1。


    2.核心成员函数解析

    a.ParseReqLine —— 解析请求行

    void ParseReqLine(std::string &reqline)
    {
    std::stringstream ss(reqline);
    ss >> _method >> _uri >> _version;
    }

    • 工作原理:利用 std::stringstream 默认以空白字符(空格、Tab等)为分隔符的特性,一气呵成地将一行请求字符串拆解并赋值给 _method、_uri 和 _version。

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

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

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

    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 类

    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.核心成员变量解析

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

    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 —— 业务核心:文件读取与状态决策

    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 —— 设置目标文件

    void SetTargetFile(const std::string &target)
    {
    _targetfile = target;
    }

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


    7.4 Http 主控类

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

    1.核心架构与设计角色

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


    2.核心成员函数解析

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

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

    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 —— 启动服务器与回调绑定

    void Start()
    {
    tsvrp->Start([this](std::shared_ptr<Socket> &sock, InetAddr &client) {
    this->HandlerHttpRquest(sock, client);
    });
    }

    • Lambda 表达式与闭包:调用了 TcpServer::Start,并传入了一个 Lambda 表达式作为业务回调。

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

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


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

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

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

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

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

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

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

  • 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)

    #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. 命令行参数检查(防御性编程)

    if (argc != 2)
    {
    std::cout << "Usage: " << argv[0] << " port" << std::endl;
    exit(USAGE_ERR);
    }

    • 作用:确保用户在启动程序时正确输入了端口号。

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

    b. 端口转换

    uint16_t port = std::stoi(argv[1]);

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

    c.创建并启动服务器

    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)

    myhttp: Main.cc
    g++ -o $@ $^ -std=c++17

    .PHONY: clean
    clean:
    rm -f myhttp


    九.运行与验证

    浏览器访问

    http://localhost:8080
    http://127.0.0.1:8080

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

    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 协议、多进程并发、设计模式等核心知识点。麻雀虽小,五脏俱全。

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

    赞(0)
    未经允许不得转载:171主机测评 » 【Linux】三十五.网络基础(三)《从零实现一个C++ HTTP服务器:完整项目实战》----万字详解
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址