利用 C 語言 Socket 直接實現 HTTP 請求,是理解網絡協議棧、掌握底層通信細節的最佳練習。本文記錄從 URL 解析到完整接收響應的全流程實現邏輯與關鍵技術點。
URL 解析 → DNS 解析 → Socket 創建 → Connect → 發送 GET 請求 → 循環接收響應 → 內存拼接 → 釋放資源
輸入 www.baidu.com/1.html → Host: www.baidu.com, Path: /1.html, Port: 80 (默認)。需處理帶端口、帶協議頭、IP 直連等多種格式。
gethostbyname()struct hostent *host = gethostbyname(hostname); // hostname 如 "www.baidu.com"
if (!host) { /* 錯誤處理 */ }
struct in_addr *addr = (struct in_addr*)host->h_addr_list[0];
char *ip = inet_ntoa(*addr); // 獲取 IP 字符串
注意:gethostbyname 非線程安全,現代代碼推薦使用 getaddrinfo。
struct sockaddr_in server_addr = {0};
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
server_addr.sin_addr.s_addr = inet_addr(ip); // 或從 hostent 複製
connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
char request[1024];
snprintf(request, sizeof(request),
"GET %s HTTP/1.1\r\n"
"Host: %s\r\n"
"Connection: close\r\n"
"\r\n", path, host);
send(sockfd, request, strlen(request), 0);
關鍵點:必須以 \r\n\r\n 結束請求頭;Connection: close 避免長連接導致 recv 阻塞等待超時。
響應體大小未知,需動態擴容緩衝區:
char *buffer = NULL;
size_t buf_size = 0, buf_len = 0;
char tmp[4096];
int bytes;
while ((bytes = recv(sockfd, tmp, sizeof(tmp), 0)) > 0) {
if (buf_len + bytes >= buf_size) {
buf_size = buf_size ? buf_size * 2 : 8192;
buffer = realloc(buffer, buf_size);
}
memcpy(buffer + buf_len, tmp, bytes);
buf_len += bytes;
}
buffer[buf_len] = '\0'; // 字符串終止
realloc 指數增長策略(×2),均攤 O(1) 復制成本。setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) 防止無限阻塞。\r\n\r\n 分隔符,頭部通常為 ASCII/ISO-8859-1,體部編碼由 Content-Type: charset=... 指定。iconv 轉換為本地編碼。int main(int argc, char *argv[]) {
if (argc < 2) { printf("Usage: %s \n", argv[0]); return 1; }
char *url = argv[1];
char host[256], path[1024];
int port = 80;
parse_url(url, host, path, &port); // 自行實現
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// ... DNS, Connect, Send, Recv 邏輯 ...
printf("響應內容:\n%s\n", buffer);
free(buffer);
close(sockfd);
return 0;
}
手寫 HTTP 客戶端雖然「重複造輪子」,但能深度理解 TCP 三次握手、HTTP 協議文本格式、動態內存管理、網絡錯誤處理等核心底層技能。這是從「調用庫」進階為「理解原理」的關鍵一步。