一.前言
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 映射规则
| / | ./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 协议、多进程并发、设计模式等核心知识点。麻雀虽小,五脏俱全。
如果你能跟着文章一步步走到这里,那么,你已经对网络编程有了实实在在的理解。这不是纸上谈兵,而是真正能跑起来的代码。






