C++ Http 下载文件

55 阅读8分钟

C++ 面向对象实战:手写 HTTP 文件下载器

1. 项目简介与学习目标

本教程将带你使用标准 C++ (C++11) 编写一个基于命令行的 HTTP 文件下载器。不同于简单的脚本,我们将严格采用面向对象编程 (OOP) 的思想,将网络通信、协议构建和数据解析分离,构建一个类似于 curlwget 的核心雏形。

🎯 学习目标

  • 深入理解 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 提取方法保证二进制安全。
  • 数据流控制:循环发送与接收保证数据完整性。
  • 异常与错误处理:连接失败或发送接收异常均安全处理。

通过以上设计与实现,用户可以理解从协议构建到网络传输,再到数据解析的完整流程,掌握面向对象设计的实际应用。