企業網絡常強制通過 HTTP 代理訪問外網。標準 `URLDownloadToFile` 或 `WinHTTP` 高層 API 雖支持代理,但底層 Socket 實現可控性最強,適合嵌入式、無依賴、需自定義協議細節 (如斷點續傳、自定義 Header) 的場景。
HTTPS 下載本質上分兩階段:
HTTP (非加密) 同理,但通常直接發送 `GET https://host/path HTTP/1.1` 給代理,無需 CONNECT。
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;
};
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;
}
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;
}
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;
}
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;
}
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;
}
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、精確進度控制、斷點續傳、限速、多線程分塊下載等。理解這套流程,對網絡協議分析、爬蟲開發、嵌入式網絡編程極有裨益。