無第三方庫(如 libcurl)環境下,使用 Linux 原生 Socket API 實現 HTTP/1.1 客戶端,支持 Keep-Alive、非阻塞連接、超時控制。
m_iSocketFd 保持連接,下次請求先檢查可寫性復用。
// 提取 host、port、path
// https://host:port/path?query
// https 暫不支持(需 SSL/TLS 層)
int getPort(const string& url) { /* 默認 80 */ }
string getHost(const string& url) { /* 域名或 IP */ }
string getPath(const string& url) { /* /path?query */ }
string getIP(const string& url) {
// 域名 → gethostbyname → inet_ntoa
// IP 直通
}
string buildRequest(const string& method, const string& url, const string& body) {
string host = getHost(url);
string path = getPath(url);
string req = method + " " + path + " HTTP/1.1\r\n";
req += "Host: " + host + "\r\n";
req += "Connection: Keep-Alive\r\n";
req += "User-Agent: CustomCppClient/1.0\r\n";
req += "Accept: */*\r\n";
if (method == "POST") {
req += "Content-Type: application/x-www-form-urlencoded\r\n";
req += "Content-Length: " + to_string(body.size()) + "\r\n\r\n";
req += body;
} else {
req += "\r\n";
}
return req;
}
int sock = socket(AF_INET, SOCK_STREAM, 0);
fcntl(sock, F_SETFL, O_NONBLOCK); // 非阻塞
connect(sock, (sockaddr*)&addr, sizeof(addr));
// select 監聽寫就緒,超時 3.5s
fd_set wset; FD_ZERO(&wset); FD_SET(sock, &wset);
timeval tv = {3, 500000};
int ret = select(sock+1, nullptr, &wset, nullptr, &tv);
if (ret <= 0 || !FD_ISSET(sock, &wset)) { /* 超時/失敗 */ }
// 進一步 getsockopt SO_ERROR 確認連接真正成功
// 發送
send(sock, req.c_str(), req.size(), 0);
// 接收循環
string response;
char buf[4096];
while (true) {
int n = recv(sock, buf, sizeof(buf), 0);
if (n > 0) response.append(buf, n);
else if (n == 0) break; // 對端關閉
else if (errno == EINTR || errno == EAGAIN) continue;
else break; // 錯誤
}
// 成員變量
int m_iSocketFd = -1;
// 請求前檢查
if (m_iSocketFd != -1) {
fd_set wset; FD_ZERO(&wset); FD_SET(m_iSocketFd, &wset);
timeval tv = {0, 0}; // 非阻塞檢查
if (select(m_iSocketFd+1, nullptr, &wset, nullptr, &tv) > 0 && FD_ISSET(m_iSocketFd, &wset)) {
// 進一步檢查 SO_ERROR 為 0
// 復用發送
}
}
// 復用失敗或首次請求 → 重新建立連接
class HttpRequest {
public:
HttpRequest() : m_iSocketFd(-1) {}
~HttpRequest() { if (m_iSocketFd != -1) close(m_iSocketFd); }
bool Get(const string& url, string& response) {
return request("GET", url, "", response);
}
bool Post(const string& url, const string& data, string& response) {
return request("POST", url, data, response);
}
private:
int m_iSocketFd;
bool request(const string& method, const string& url, const string& data, string& response);
// 內部輔助:parseURL, buildRequest, connectWithTimeout, sendRecv, checkSocketReusable
};
int main() {
HttpRequest http;
string resp;
if (http.Get("https://www.baidu.com", resp)) {
cout << "GET 成功,長度: " << resp.size() << endl;
// cout << resp.substr(0, 500) << endl;
}
if (http.Post("https://httpbin.org/post", "key=value&foo=bar", resp)) {
cout << "POST 成功,長度: " << resp.size() << endl;
}
return 0;
}
| 局限 | 解決方案 |
|---|---|
| 不支持 HTTPS | 集成 OpenSSL(SSL_CTX, SSL_connect, SSL_read/write) |
| 不支持 Chunked Transfer | 解析 Transfer-Encoding: chunked 分塊讀取 |
| 不支持重定向 301/302 | 解析 Location 頭遞歸請求 |
| 無連接池 | 多 host 維護 unordered_map |
| 同步阻塞 | 改用 epoll/kqueue + 狀態機實現異步 |
基於 Socket 手寫 HTTP 客戶端是理解網絡編程基礎的最佳練習。掌握:
生產環境請直接使用 libcurl 或 cpp-httplib(單頭文件、支持 HTTPS/異步),本代碼僅供教學與特殊無依賴場景參考。