首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> VC >> VC++

VC++ 實現 HTTP 代理下載文件完整實戰教程

     編輯:VC++

場景:穿透代理服務器下載資源

企業網絡常強制通過 HTTP 代理訪問外網。標準 `URLDownloadToFile` 或 `WinHTTP` 高層 API 雖支持代理,但底層 Socket 實現可控性最強,適合嵌入式、無依賴、需自定義協議細節 (如斷點續傳、自定義 Header) 的場景。

核心協議:HTTP CONNECT 隧道技術

HTTPS 下載本質上分兩階段:

  1. 建立隧道:客戶端 → 代理 發送 `CONNECT target_host:443 HTTP/1.1`,代理返回 `200 Connection Established`,後續 TCP 連接直接透傳 TLS 流量。
  2. HTTP 請求:隧道建立後,客戶端在同一 Socket 上發送標準 `GET /path HTTP/1.1` 請求,代理轉發給目標服務器並回傳響應。

HTTP (非加密) 同理,但通常直接發送 `GET https://host/path HTTP/1.1` 給代理,無需 CONNECT。

完整實現架構

1. 關鍵數據結構

class CHttpProxyDownloader {
    SOCKET m_hSocket = INVALID_SOCKET;
    sockaddr_in m_ProxyAddr, m_TargetAddr;
    int m_nRecvTimeout = 30000, m_nSendTimeout = 30000;
    CString m_strProxyIP, m_strProxyPort, m_strTargetHost, m_strTargetPath;
    int m_nTargetPort = 80;
    bool m_bUseProxy = true;
    bool m_bHttps = false;
    // 斷點續傳
    long long m_llDownloaded = 0, m_llTotalSize = -1;
    HANDLE m_hFile = INVALID_HANDLE_VALUE;
};

2. Socket 初始化與超時設置

bool CHttpProxyDownloader::InitSocket() {
    WSADATA wsa; if (WSAStartup(MAKEWORD(2,2), &wsa)) return false;
    m_hSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (m_hSocket == INVALID_SOCKET) return false;

    // LINGER 設置:關閉時立即發送 RST,避免 TIME_WAIT
    linger lg = {1, 0};
    setsockopt(m_hSocket, SOL_SOCKET, SO_LINGER, (char*)&lg, sizeof(lg));
    // 收發超時
    setsockopt(m_hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&m_nRecvTimeout, sizeof(int));
    setsockopt(m_hSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)&m_nSendTimeout, sizeof(int));
    return true;
}

3. 連接代理服務器

bool CHttpProxyDownloader::ConnectProxy() {
    m_ProxyAddr.sin_family = AF_INET;
    m_ProxyAddr.sin_port = htons(atoi(m_strProxyPort));
    m_ProxyAddr.sin_addr.s_addr = inet_addr(m_strProxyIP);
    // 支持域名解析
    if (m_ProxyAddr.sin_addr.s_addr == INADDR_NONE) {
        hostent* ph = gethostbyname(m_strProxyIP);
        if (!ph) return false;
        m_ProxyAddr.sin_addr.s_addr = *(u_long*)ph->h_addr_list[0];
    }
    return connect(m_hSocket, (sockaddr*)&m_ProxyAddr, sizeof(m_ProxyAddr)) != SOCKET_ERROR;
}

4. 發送 HTTP 請求報文 (核心)

bool CHttpProxyDownloader::SendRequest(const CString& strRequest) {
    int len = strRequest.GetLength();
    return send(m_hSocket, strRequest, len, 0) != SOCKET_ERROR;
}

CString CHttpProxyDownloader::BuildConnectRequest() {
    CString req;
    req.Format(
        "CONNECT %s:%d HTTP/1.1\r\n"
        "Host: %s:%d\r\n"
        "User-Agent: VCProxyDownloader/1.0\r\n"
        "Proxy-Connection: Keep-Alive\r\n"
        "\r\n",
        m_strTargetHost, m_nTargetPort, m_strTargetHost, m_nTargetPort
    );
    return req;
}

CString CHttpProxyDownloader::BuildGetRequest() {
    CString req;
    CString rangeHeader;
    if (m_llDownloaded > 0) rangeHeader.Format("Range: bytes=%lld-\r\n", m_llDownloaded);

    req.Format(
        "GET %s HTTP/1.1\r\n"
        "Host: %s\r\n"
        "User-Agent: VCProxyDownloader/1.0\r\n"
        "Accept: */*\r\n"
        "Connection: Keep-Alive\r\n"
        "%s"
        "\r\n",
        m_strTargetPath, m_strTargetHost, rangeHeader
    );
    return req;
}

5. 接收並解析 HTTP 響應頭

bool CHttpProxyDownloader::RecvHttpHeader(CString& strHeader) {
    char c; int idx = 0; char buf[4096];
    while (idx < 4090) {
        int ret = recv(m_hSocket, &c, 1, 0);
        if (ret <= 0) return false;
        buf[idx++] = c;
        if (idx >= 4 && buf[idx-4]== '\r' && buf[idx-3]=='\n' && buf[idx-2]=='\r' && buf[idx-1]=='\n') break;
    }
    buf[idx] = '\0';
    strHeader = buf;
    return true;
}

bool CHttpProxyDownloader::ParseHeader(const CString& header, long long& contentLength, int& statusCode) {
    // 解析狀態碼: HTTP/1.1 200 OK
    int pos = header.Find(' ');
    if (pos != -1) statusCode = atoi(header.Mid(pos+1, 3));

    // 解析 Content-Length
    pos = header.Find("Content-Length:");
    if (pos != -1) {
        pos += 15;
        while (pos < header.GetLength() && isspace(header[pos])) pos++;
        int end = header.Find("\r\n", pos);
        contentLength = _atoi64(header.Mid(pos, end - pos));
    }
    // 解析 Transfer-Encoding: chunked (略)
    return true;
}

6. 下載主循環 (支持斷點續傳)

bool CHttpProxyDownloader::DownloadFile(const CString& localPath) {
    m_hFile = CreateFile(localPath, GENERIC_WRITE, FILE_SHARE_READ, NULL,
                         m_llDownloaded > 0 ? OPEN_ALWAYS : CREATE_ALWAYS,
                         FILE_ATTRIBUTE_NORMAL, NULL);
    if (m_hFile == INVALID_HANDLE_VALUE) return false;
    if (m_llDownloaded > 0) SetFilePointer(m_hFile, m_llDownloaded, NULL, FILE_BEGIN);

    char buffer[8192];
    DWORD written;
    long long totalRecv = 0;
    bool bHeaderParsed = false;

    while (true) {
        int nRecv = recv(m_hSocket, buffer, sizeof(buffer), 0);
        if (nRecv <= 0) break; // 0=關閉, <0=錯誤/超時

        if (!bHeaderParsed) {
            // 簡單處理:假設頭部在第一個包內 (健壯代碼需緩衝區拆分)
            CString header(buffer, nRecv);
            int headerEnd = header.Find("\r\n\r\n");
            if (headerEnd != -1) {
                ParseHeader(header, m_llTotalSize, m_nStatusCode);
                if (m_nStatusCode == 200 || m_nStatusCode == 206) {
                    // 數據開始位置
                    int dataStart = headerEnd + 4;
                    int dataLen = nRecv - dataStart;
                    if (dataLen > 0) WriteFile(m_hFile, buffer + dataStart, dataLen, &written, NULL);
                    totalRecv += dataLen;
                } else { return false; } // 404, 403 等
                bHeaderParsed = true;
            } else {
                // 頭部跨包,需累積緩衝區 (此處略)
            }
        } else {
            WriteFile(m_hFile, buffer, nRecv, &written, NULL);
            totalRecv += nRecv;
        }
        // 進度回調...
    }
    CloseHandle(m_hFile);
    return totalRecv > 0;
}

7. 完整流程協調

bool CHttpProxyDownloader::StartDownload() {
    if (!InitSocket()) return false;
    if (!ConnectProxy()) { Cleanup(); return false; }

    if (m_bHttps && m_bUseProxy) {
        // HTTPS: 先建立 CONNECT 隧道
        if (!SendRequest(BuildConnectRequest())) { Cleanup(); return false; }
        CString resp; RecvHttpHeader(resp);
        if (resp.Find("200") == -1) { Cleanup(); return false; }
        // 隧道建立成功,Socket 現在直通目標服務器
    } else if (m_bUseProxy) {
        // HTTP: 直接發送 GET 請求給代理 (請求行包含完整 URL)
        // BuildGetRequest 需修改為 "GET https://host/path HTTP/1.1"
    } else {
        // 直連目標服務器 (需先 ConnectTarget)
    }

    // 發送 GET 請求
    if (!SendRequest(BuildGetRequest())) { Cleanup(); return false; }

    // 下載數據
    return DownloadFile(m_strLocalFile);
}

關鍵技術難點總結

難點解決方案
代理認證 (407)請求頭添加 `Proxy-Authorization: Basic base64(user:pass)`
HTTPS 證書驗證Socket 層無法處理,需集成 OpenSSL/Schannel 在隧道建立後進行 TLS 握手
Chunked 編碼響應解析十六進制長度塊,去除塊頭尾 `\r\n` 還原實體
斷點續傳`Range: bytes=start-` + `206 Partial Content` + 文件指針定位
域名解析`gethostbyname` (舊) 或 `getaddrinfo` (現代, 支持 IPv6)
非阻塞/異步`ioctlsocket(FIONBIO)` + `select`/`WSAEventSelect`/`IOCP` 實現多任務並發

總結

基於 Socket 實現 HTTP/HTTPS 代理下載,核心在於掌握 `CONNECT` 隧道建立、HTTP 報文構建/解析、TCP 流式數據處理。雖代碼量較高層 API 大,但提供了極致的可控性:自定義 Header、精確進度控制、斷點續傳、限速、多線程分塊下載等。理解這套流程,對網絡協議分析、爬蟲開發、嵌入式網絡編程極有裨益。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved