C++ 面向对象实战:手写 HTTP 文件下载器
1. 项目简介与学习目标
本教程将带你使用标准 C++ (C++11) 编写一个基于命令行的 HTTP 文件下载器。不同于简单的脚本,我们将严格采用面向对象编程 (OOP) 的思想,将网络通信、协议构建和数据解析分离,构建一个类似于 curl 或 wget 的核心雏形。
🎯 学习目标
- 深入理解 HTTP 协议:亲手构建请求头,解析响应头。
- 掌握 Socket 网络编程:理解 TCP 连接的三次握手在代码层面的实现。
- 提升架构设计能力:学习如何通过类的封装(Encapsulation)和单一职责原则(SRP)来管理代码。
💡 实现原理补充:
- 通过面向对象设计,将不同功能模块(请求构建、网络传输、响应解析)解耦,增强代码复用性和可维护性。
- 单一职责原则保证每个类只负责一个功能,降低耦合度。
2. 核心原理讲解
在开始写代码之前,我们需要理解两个核心概念:HTTP 协议格式 和 Socket 通信流程。
2.1 HTTP 协议交互图解
HTTP 本质上是基于文本的协议。下载文件就是客户端和服务器的一次“对话”。
客户端发送 (Request):
GET /index.html HTTP/1.1 <-- [请求行] 动作 + 路径 + 协议版本
Host: example.com <-- [头部] 目标主机
Connection: close <-- [头部] 告诉服务器发完就挂断
(空行) <-- [关键] 必须有空行,表示头部结束
服务器回复 (Response):
HTTP/1.1 200 OK <-- [状态行] 200表示成功
Content-Length: 1024 <-- [头部] 文件大小
Content-Type: text/html <-- [头部] 文件类型
(空行) <-- [关键] 分界线
<html>...</html> <-- [正文] 这就是我们要保存的文件内容
💡 实现原理补充:
- HTTP 协议通过文本方式传输信息,状态行指示请求结果,Header 提供元信息,Body 保存实际数据。
- 解析时可通过双换行符作为分界点,安全提取 Body。
2.2 TCP Socket 流程
HTTP 是应用层协议,它的底层跑在 TCP 上。流程如下:
- Socket(): 买个手机。
- GetHostByName(): 查电话本(域名转IP)。
- Connect(): 拨号。
- Send(): 说话(发送 HTTP 请求文本)。
- Recv(): 听话(接收数据)。
- Close(): 挂断。
💡 实现原理补充:
- TCP 提供可靠的字节流,保证数据顺序、完整性与错误重传。
- HTTP 在其上层组织请求和响应为可读文本,保证协议交互正常。
3. 面向对象设计 (The Big Three)
我们将项目拆分为三个独立的类,每个类只做一件事。
| 类名 | 职责 | 核心能力 |
|---|---|---|
| HttpRequest | 协议构建 | 像搭积木一样组装 HTTP 请求,管理 Method 和 Header。 |
| HttpResponse | 协议解析 | 像拆快递一样解析服务器返回的数据,提取状态码和文件内容。 |
| HttpConnection | 网络传输 | 负责底层的 Socket 操作,管理连接生命周期(RAII)。 |
💡 实现原理补充:
- 单一职责原则,每个类聚焦一件事,提升可维护性和可扩展性。
- 封装 Socket 细节,用户只需调用接口,无需关心底层实现。
4. 核心代码逻辑解析
4.1 HttpRequest 类
目标:不再硬编码字符串,而是动态管理 Header。
实现:使用 std::map<string, string> 存储头部。
void addHeader(string key, string value) {
_headers[key] = value;
}
string toString() {
// 遍历 map,拼接成 "Key: Value\r\n"
}
💡 实现原理补充:
- Header 使用 map 管理,动态增加、覆盖都很方便。
- 生成请求字符串时自动遍历 Header,提高灵活性和可扩展性。
4.2 HttpResponse 类
目标:精准分离 Header 和 Body,并解析状态码。
实现:
- 使用
stringstream按行读取,解析第一行获取状态码。 - 使用
find("\r\n\r\n")定位 Header 和 Body 的分界线。 - 分界线之后的所有字节都是文件内容。
💡 实现原理补充:
- Body 提取直接利用分界符位置,可兼容任意长度和格式的数据,保证二进制文件完整读取。
- Header 可通过 map 方便访问和查询。
4.3 HttpConnection 类
目标:资源自动管理(RAII)。
实现:
- 构造函数:初始化变量。
- 析构函数:调用 close(sockfd)。
- connectToServer() 执行 DNS 解析、创建 Socket 并建立连接。
- sendRequest() 确保完整发送请求数据。
- receiveResponse() 循环接收数据直到服务器断开。
💡 实现原理补充:
- RAII 模式保证资源安全释放。
- 循环发送和接收保证网络数据完整传输。
- 自动关闭 Socket 避免资源泄漏。
5. 完整源代码
/**
* Advanced Object-Oriented HTTP Downloader
* 文件名: http_downloader.cpp
* * 编译方法:
* g++ -o downloader http_downloader.cpp -std=c++11
* 运行方法:
* ./downloader
*/
#include <iostream>
#include <string>
#include <vector>
#include <map> // 用于存储 Header
#include <sstream> // 用于字符串流解析
#include <fstream> // 用于文件写入
#include <cstring> // 用于 memset, memcpy
#include <cstdlib> // 用于 atoi
#include <sys/socket.h> // Socket 核心
#include <netinet/in.h> // IP 地址结构
#include <netdb.h> // DNS 解析
#include <unistd.h> // close 函数
#include <arpa/inet.h>
// ==========================================
// 类 1: HttpRequest
// 职责: 构建 HTTP 请求协议包
// ==========================================
class HttpRequest {
public:
HttpRequest(const std::string& host, const std::string& path, const std::string& method = "GET")
: _host(host), _path(path), _method(method) {
// 设置默认 Header
addHeader("Host", host);
addHeader("User-Agent", "CppDownloader/2.0");
addHeader("Connection", "close"); // 短连接模式
addHeader("Accept", "*/*");
}
// 添加自定义 Header
void addHeader(const std::string& key, const std::string& value) {
_headers[key] = value;
}
// 生成最终发送给服务器的字符串
std::string toString() const {
std::stringstream ss;
// 1. 请求行
ss << _method << " " << _path << " HTTP/1.1\r\n";
// 2. 请求头
for (auto it = _headers.begin(); it != _headers.end(); ++it) {
ss << it->first << ": " << it->second << "\r\n";
}
// 3. 空行 (Header 结束标志)
ss << "\r\n";
return ss.str();
}
private:
std::string _host;
std::string _path;
std::string _method;
std::map<std::string, std::string> _headers;
};
// ==========================================
// 类 2: HttpResponse
// 职责: 解析服务器返回的原始数据
// ==========================================
class HttpResponse {
public:
HttpResponse(const std::string& rawData) : _statusCode(0) {
if (!rawData.empty()) {
parse(rawData);
}
}
int getStatusCode() const { return _statusCode; }
std::string getBody() const { return _body; }
// 获取特定 Header 的值
std::string getHeader(const std::string& key) const {
auto it = _headers.find(key);
if (it != _headers.end()) {
return it->second;
}
return "";
}
bool isSuccess() const { return _statusCode == 200; }
private:
int _statusCode;
std::map<std::string, std::string> _headers;
std::string _body;
void parse(const std::string& rawData) {
std::stringstream stream(rawData);
std::string line;
// --- 1. 解析状态行 (HTTP/1.1 200 OK) ---
if (std::getline(stream, line)) {
if (!line.empty() && line.back() == '\r') line.pop_back();
// 简单解析:找到第一个空格后的数字
size_t firstSpace = line.find(' ');
if (firstSpace != std::string::npos) {
size_t secondSpace = line.find(' ', firstSpace + 1);
std::string codeStr = line.substr(firstSpace + 1, secondSpace - firstSpace - 1);
_statusCode = std::atoi(codeStr.c_str());
}
}
// --- 2. 解析 Headers ---
while (std::getline(stream, line)) {
if (!line.empty() && line.back() == '\r') line.pop_back();
if (line.empty()) break; // 遇到空行停止
size_t colonPos = line.find(':');
if (colonPos != std::string::npos) {
std::string key = line.substr(0, colonPos);
std::string value = line.substr(colonPos + 1);
// 去除 Value 前导空格
if (!value.empty() && value[0] == ' ') value = value.substr(1);
_headers[key] = value;
}
}
// --- 3. 提取 Body ---
// 使用 find 查找双换行,比流读取更安全
std::string delimiter = "\r\n\r\n";
size_t splitPos = rawData.find(delimiter);
if (splitPos != std::string::npos) {
_body = rawData.substr(splitPos + delimiter.length());
}
}
};
// ==========================================
// 类 3: HttpConnection
// 职责: TCP 连接管理 (RAII)
// ==========================================
class HttpConnection {
public:
HttpConnection(const std::string& host, int port = 80)
: _host(host), _port(port), _sockfd(-1) {}
~HttpConnection() {
closeConnection(); // 析构时自动关闭
}
bool connectToServer() {
// 1. DNS 解析
struct hostent* server = gethostbyname(_host.c_str());
if (server == NULL) {
std::cerr << "DNS 解析失败: " << _host << std::endl;
return false;
}
// 2. 创建 Socket
_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0) return false;
// 3. 连接地址配置
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(_port);
memcpy(&serv_addr.sin_addr.s_addr, server->h_addr, server->h_length);
// 4. 发起连接
if (connect(_sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
return false;
}
return true;
}
bool sendRequest(const HttpRequest& request) {
if (_sockfd < 0) return false;
std::string reqStr = request.toString();
size_t totalSent = 0;
size_t len = reqStr.length();
while (totalSent < len) {
ssize_t n = send(_sockfd, reqStr.c_str() + totalSent, len - totalSent, 0);
if (n < 0) return false;
totalSent += n;
}
return true;
}
HttpResponse receiveResponse() {
std::string rawData;
char buffer[4096];
ssize_t n;
// 循环接收直到服务器断开
while ((n = recv(_sockfd, buffer, sizeof(buffer), 0)) > 0) {
rawData.append(buffer, n);
}
return HttpResponse(rawData);
}
void closeConnection() {
if (_sockfd >= 0) {
close(_sockfd);
_sockfd = -1;
}
}
private:
std::string _host;
int _port;
int _sockfd;
};
// ==========================================
// 主函数: 业务逻辑
// ==========================================
int main() {
// 目标配置
std::string host = "example.com";
std::string path = "/index.html";
std::string saveFile = "downloaded.html";
std::cout << ">>> 开始任务: http://" << host << path << std::endl;
// 1. 准备请求
HttpRequest req(host, path);
req.addHeader("Accept-Language", "en-US"); // 演示自定义 Header
// 2. 建立连接
HttpConnection conn(host);
if (!conn.connectToServer()) {
std::cerr << ">>> 连接失败!" << std::endl;
return 1;
}
// 3. 发送请求
if (!conn.sendRequest(req)) {
std::cerr << ">>> 请求发送失败!" << std::endl;
return 1;
}
// 4. 接收响应
std::cout << ">>> 正在等待响应..." << std::endl;
HttpResponse res = conn.receiveResponse();
// 5. 处理结果
std::cout << "--------------------------------" << std::endl;
std::cout << "状态码: " << res.getStatusCode() << std::endl;
std::cout << "文件大小: " << res.getHeader("Content-Length") << " bytes" << std::endl;
std::cout << "--------------------------------" << std::endl;
if (res.isSuccess()) {
std::ofstream out(saveFile, std::ios::binary);
if (out.is_open()) {
std::string body = res.getBody();
out.write(body.c_str(), body.size());
out.close();
std::cout << ">>> 下载成功!文件已保存为: " << saveFile << std::endl;
} else {
std::cerr << ">>> 无法写入文件!" << std::endl;
}
} else {
std::cerr << ">>> 服务器返回错误,下载失败。" << std::endl;
}
return 0;
}
6. 编译与运行指南
6.1 编译环境
本代码依赖 POSIX Socket API,适用于:
- Linux (Ubuntu, CentOS, etc.)
- macOS
- Windows Subsystem for Linux (WSL)
6.2 编译命令
g++ -o downloader http_downloader.cpp -std=c++11
6.3 运行命令
./downloader
6.4 预期结果
>>> 开始任务: http://example.com/index.html
>>> 正在等待响应...
--------------------------------
状态码: 200
文件大小: 1256 bytes
--------------------------------
>>> 下载成功!文件已保存为: downloaded.html
💡 实现原理补充:
- 输出状态码和 Content-Length 可用于验证请求成功与文件完整性。
- 文件以二进制方式保存,保证内容不被文本处理破坏。
总结实现原理:
- HTTP 请求和响应通过文本协议交互,Header 与 Body 清晰分离。
- TCP Socket 提供可靠传输,保证请求和响应完整。
- RAII 管理资源,防止泄漏。
- 面向对象设计提高可维护性,模块化降低耦合。
- Header 使用 map 管理,可扩展性高;Body 提取方法保证二进制安全。
- 数据流控制:循环发送与接收保证数据完整性。
- 异常与错误处理:连接失败或发送接收异常均安全处理。
通过以上设计与实现,用户可以理解从协议构建到网络传输,再到数据解析的完整流程,掌握面向对象设计的实际应用。