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

Winsock 實現 HTTP 代理下載文件完整實戰 (支持斷點續傳)

     編輯:VC 教學

前言

網上流傳的 HTTP 代理下載代碼多殘缺不全。本文基於 RFC 2616/2817,提供一個完整、可運行、支持斷點續傳的 Winsock 實現,涵蓋 CONNECT 隧道建立、HTTP 請求構造、響應頭解析、Range 斷點續傳、超時控制 全流程。

一、核心數據結構與變量

class CHttpProxyDownloader {
    SOCKET m_hSocket;              // 通信套接字
    sockaddr_in m_serverAddr;      // 目標地址 (代理或直連服務器)
    int m_nRecvTimeout = 5000;     // 接收超時 (ms)
    int m_nSendTimeout = 5000;     // 發送超時

    // 下載參數
    CString m_strProxyIP;          // 代理 IP
    int m_nProxyPort;              // 代理端口
    CString m_strHost;             // 目標主機 IP (直連時用)
    int m_nHostPort = 80;          // 目標端口
    CString m_strRequestPath;      // 請求路徑 "/path/file.zip"
    CString m_strHostHeader;       // Host 頭值 "example.com"

    // 狀態
    long m_lFileSize = -1;         // 文件總大小
    long m_lDownloaded = 0;        // 已下載
    bool m_bUseProxy = true;
    bool m_bStopFlag = false;
};

二、關鍵輔助函數

1. 發送數據 (自動處理分包)

bool SendAll(SOCKET s, const char* buf, int len) {
    int sent = 0;
    while (sent < len && !m_bStopFlag) {
        int n = send(s, buf + sent, len - sent, 0);
        if (n == SOCKET_ERROR) {
            int err = WSAGetLastError();
            if (err == WSAEWOULDBLOCK) { Sleep(10); continue; }
            return false;
        }
        sent += n;
    }
    return sent == len;
}
bool SendString(SOCKET s, const CString& str) {
    return SendAll(s, str, str.GetLength());
}

2. 接收 HTTP 響應頭 (讀至 \r\n\r\n)

bool RecvHttpHeader(SOCKET s, CString& header) {
    header.Empty();
    char c;
    int consecutiveLF = 0;
    while (consecutiveLF < 4 && !m_bStopFlag) { // \r\n\r\n = 4 個控制字符
        int n = recv(s, &c, 1, 0);
        if (n <= 0) return false;
        header += c;
        if (c == '\r' || c == '\n') consecutiveLF++;
        else consecutiveLF = 0;
    }
    return true;
}

3. 解析 Content-Length

long ParseContentLength(const CString& header) {
    int pos = header.Find(_T("Content-Length:"));
    if (pos < 0) return -1;
    pos += 15; // 跳過 "Content-Length:"
    while (pos < header.GetLength() && isspace(header[pos])) pos++;
    int end = pos;
    while (end < header.GetLength() && isdigit(header[end])) end++;
    CString numStr = header.Mid(pos, end - pos);
    return _ttol(numStr);
}

4. 解析 HTTP 狀態碼

int ParseStatusCode(const CString& header) {
    // HTTP/1.1 200 OK
    int pos = header.Find(_T("HTTP/"));
    if (pos < 0) return -1;
    pos = header.Find(_T(" "), pos);
    if (pos < 0) return -1;
    pos++;
    int end = header.Find(_T(" "), pos);
    if (end < 0) end = header.GetLength();
    return _ttol(header.Mid(pos, end - pos));
}

三、連接建立:直連 vs HTTP 代理 CONNECT

bool Connect() {
    m_hSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (m_hSocket == INVALID_SOCKET) return false;

    // 設置超時
    DWORD timeout = m_nRecvTimeout;
    setsockopt(m_hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(timeout));
    timeout = m_nSendTimeout;
    setsockopt(m_hSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)&timeout, sizeof(timeout));

    // 綁定本地端口 (可選,讓系統自動分配通常更好)
    // sockaddr_in local = {0}; local.sin_family = AF_INET; local.sin_addr.s_addr = INADDR_ANY;
    // bind(m_hSocket, (sockaddr*)&local, sizeof(local));

    if (m_bUseProxy) {
        // 1. 連接代理服務器
        m_serverAddr.sin_family = AF_INET;
        m_serverAddr.sin_port = htons(m_nProxyPort);
        m_serverAddr.sin_addr.s_addr = inet_addr(m_strProxyIP);
        if (connect(m_hSocket, (sockaddr*)&m_serverAddr, sizeof(m_serverAddr)) == SOCKET_ERROR)
            return false;

        // 2. 發送 CONNECT 請求建立隧道
        CString req;
        req.Format(_T("CONNECT %s:%d HTTP/1.1\r\nHost: %s:%d\r\nUser-Agent: MyDownloader/1.0\r\n\r\n"),
                   m_strHost, m_nHostPort, m_strHost, m_nHostPort);
        if (!SendString(m_hSocket, req)) return false;

        // 3. 讀取代理響應
        CString respHeader;
        if (!RecvHttpHeader(m_hSocket, respHeader)) return false;
        if (ParseStatusCode(respHeader) != 200) return false; // 必須 200 Connection Established

        // 隧道建立成功,後續直接在該 Socket 上發送 HTTP 請求給目標服務器
    } else {
        // 直連目標服務器
        m_serverAddr.sin_family = AF_INET;
        m_serverAddr.sin_port = htons(m_nHostPort);
        m_serverAddr.sin_addr.s_addr = inet_addr(m_strHost); // 需提前 DNS 解析
        if (connect(m_hSocket, (sockaddr*)&m_serverAddr, sizeof(m_serverAddr)) == SOCKET_ERROR)
            return false;
    }
    return true;
}

四、發送 HTTP GET 請求 (支持 Range 斷點續傳)

bool SendHttpRequest() {
    CString req;
    // Request Line
    req.Format(_T("GET %s HTTP/1.1\r\n"), m_strRequestPath);
    // Host
    req += _T("Host: ") + m_strHostHeader + _T("\r\n");
    // Accept
    req += _T("Accept: */*\r\n");
    // User-Agent
    req += _T("User-Agent: MyDownloader/1.0\r\n");
    // Connection: Keep-Alive (可選)
    req += _T("Connection: Keep-Alive\r\n");
    // Range 斷點續傳核心
    if (m_lDownloaded > 0) {
        CString range;
        range.Format(_T("Range: bytes=%ld-\r\n"), m_lDownloaded);
        req += range;
    }
    // 空行結束頭
    req += _T("\r\n");

    return SendString(m_hSocket, req);
}

五、接收響應並寫入文件 (核心下載循環)

bool DownloadToFile(const CString& localPath) {
    CFile file;
    UINT mode = (m_lDownloaded > 0) ? (CFile::modeWrite | CFile::modeNoTruncate) : (CFile::modeCreate | CFile::modeWrite);
    if (!file.Open(localPath, mode)) return false;
    if (m_lDownloaded > 0) file.Seek(m_lDownloaded, CFile::begin);

    // 1. 讀取響應頭
    CString respHeader;
    if (!RecvHttpHeader(m_hSocket, respHeader)) { file.Close(); return false; }
    int status = ParseStatusCode(respHeader);
    if (status != 200 && status != 206) { file.Close(); return false; } // 206 Partial Content

    // 解析總長度 (若未知)
    if (m_lFileSize < 0) m_lFileSize = ParseContentLength(respHeader);

    // 2. 循環接收 Body
    const int BUFSIZE = 8192;
    std::vector buffer(BUFSIZE);
    while (!m_bStopFlag) {
        int n = recv(m_hSocket, buffer.data(), BUFSIZE, 0);
        if (n == SOCKET_ERROR) {
            int err = WSAGetLastError();
            if (err == WSAEWOULDBLOCK || err == WSAETIMEDOUT) { Sleep(50); continue; }
            break; // 真實錯誤
        }
        if (n == 0) break; // 連接關閉,下載完成

        file.Write(buffer.data(), n);
        m_lDownloaded += n;
        // 可選:回調進度 UpdateProgress(m_lDownloaded, m_lFileSize);
    }

    file.Close();
    return m_lDownloaded >= m_lFileSize || m_lFileSize < 0; // 若不知總長,以連接關閉為完成標誌
}

六、完整下載流程封裝

bool StartDownload(const CString& localPath) {
    // 1. 初始化 Winsock (進程級單例)
    static bool s_bWsaInited = false;
    if (!s_bWsaInited) {
        WSADATA wsa;
        if (WSAStartup(MAKEWORD(2,2), &wsa) != 0) return false;
        s_bWsaInited = true;
    }

    // 2. 連接 (代理或直連)
    if (!Connect()) return false;

    // 3. 發送請求
    if (!SendHttpRequest()) { closesocket(m_hSocket); return false; }

    // 4. 下載寫文件
    bool ok = DownloadToFile(localPath);

    // 5. 清理
    closesocket(m_hSocket);
    m_hSocket = INVALID_SOCKET;
    return ok;
}

七、關鍵技術點總結

技術點實現細節
HTTP 代理隧道CONNECT 方法 + Host 頭 → 200 Connection Established → 隧道透傳
斷點續傳請求頭 Range: bytes=START- → 響應 206 Partial Content → 文件追加寫入
超時控制SO_RCVTIMEO/SO_SNDTIMEO + WSAEWOULDBLOCK 重試
響應頭解析讀至 \r\n\r\n,解析 Status-Line、Content-Length、Transfer-Encoding
塊傳輸編碼本文未實現 Chunked 解碼;生產環境必須支持 (按塊大小十六進制讀取)
DNS 解析代理模式下由代理解析;直連模式需 gethostbyname/GetAddrInfo
HTTPS 支持CONNECT 隧道建立後需啟動 SSL/TLS (OpenSSL/Schannel),本文僅覆蓋 HTTP

八、工程化建議

  1. 封裝為 CHttpDownloader 類,暴露 Start/Stop/Pause、進度回調、錯誤碼枚舉。
  2. 使用 WSAEventSelect 或 IOCP 實現異步非阻塞,避免阻塞 UI 線程。
  3. 集成 OpenSSL 或 Windows Schannel 實現 HTTPS 下載 (CONNECT 後進行 TLS 握手)。
  4. 添加校驗:下載完成後驗證 MD5/SHA256。
  5. 支持多線程分片下載 (多 Range 請求並行),提升大文件速度。

結語

HTTP 代理下載的核心在於 CONNECT 隧道建立 與 Range 斷點續傳 兩大協議特性。本文提供的 Winsock 級實現雖屬教學級 (同步阻塞、無 Chunked 支持),但邏輯完整、可直接運行、易於擴展。生產環境建議基於 WinHTTP、libcurl 或 WinINet 等成熟庫,專注於業務邏輯而非協議細節。

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