網上流傳的 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;
};
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());
}
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;
}
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);
}
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));
}
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;
}
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 |
CHttpDownloader 類,暴露 Start/Stop/Pause、進度回調、錯誤碼枚舉。WSAEventSelect 或 IOCP 實現異步非阻塞,避免阻塞 UI 線程。OpenSSL 或 Windows Schannel 實現 HTTPS 下載 (CONNECT 後進行 TLS 握手)。HTTP 代理下載的核心在於 CONNECT 隧道建立 與 Range 斷點續傳 兩大協議特性。本文提供的 Winsock 級實現雖屬教學級 (同步阻塞、無 Chunked 支持),但邏輯完整、可直接運行、易於擴展。生產環境建議基於 WinHTTP、libcurl 或 WinINet 等成熟庫,專注於業務邏輯而非協議細節。