在 C 語言中,我們可以使用 socket 編程來手動實現一個簡單的 HTTP 客戶端,像瀏覽器一樣請求網頁數據。本文將結合實際代碼,重點講解如何通過 C 語言構造并發送一個 HTTP 請求報文,實現與服務器的基本通信。
文章目標
通過一個簡單的 http_send_request()
函數,我們將實現以下流程:
-
將域名(如
"www.baidu.com"
)解析成 IP 地址 -
與目標服務器建立 TCP 連接(80 端口)
-
構造 HTTP 請求報文并發送給服務器
一、代碼結構總覽
#define HTTP_VERSION "HTTP/1.1"
#define CONNETION_TYPE "Connection:close\r\n"
#define BUFFER_SIZE 4096
我們使用 HTTP/1.1 協議,連接類型為短連接(發送請求后關閉)。
二、域名解析函數:host_to_ip
char *host_to_ip(const char *hostname) {struct hostent *host_entry = gethostbyname(hostname); // DNS 查詢if (host_entry) {return inet_ntoa(*(struct in_addr*)host_entry->h_addr_list[0]); // 返回IP字符串}return NULL;
}
-
gethostbyname()
負責 DNS 解析 -
inet_ntoa()
將原始 IP 地址(二進制)轉換為點分十進制字符串,如 "14.215.177.39"
三、創建并連接 Socket:http_create_socket
int http_create_socket(char *ip) {int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 創建 TCP socketstruct sockaddr_in sin = {0};sin.sin_family = AF_INET;sin.sin_port = htons(80); // 設置端口:HTTP 默認 80sin.sin_addr.s_addr = inet_addr(ip); // 將 IP 字符串轉換為網絡地址if (0 != connect(sockfd, (struct sockaddr*)&sin, sizeof(sin))) {return -1; // 連接失敗}fcntl(sockfd, F_SETFL, O_NONBLOCK); // 設置非阻塞模式(可選)return sockfd;
}
四、發送 HTTP 請求:http_send_request
這是本文的重點,完整代碼如下:
char * http_send_request(const char *hostname, const char *resource) {char *ip = host_to_ip(hostname); // 1. 域名轉 IPint sockfd = http_create_socket(ip); // 2. 創建 TCP 連接char buffer[BUFFER_SIZE] = {0}; // 3. 準備請求報文緩沖區// 4. 構造 HTTP GET 請求報文sprintf(buffer,"GET %s %s\r\n""Host: %s\r\n""%s\r\n",resource, HTTP_VERSION, hostname, CONNETION_TYPE);// 5. 發送請求數據send(sockfd, buffer, strlen(buffer), 0);return NULL; // 當前版本未實現接收部分
}
五、HTTP 報文解析說明
通過 sprintf()
構造的請求報文如下所示(舉例):
GET /index.html HTTP/1.1
Host: www.baidu.com
Connection: close
它由以下部分組成:
行數 | 內容 | 說明 |
---|---|---|
第1行 | 請求行 | 指定方法、資源路徑、協議版本 |
第2行 | Host 頭 | 告訴服務器你訪問的是哪個域名 |
第3行 | Connection 頭 | 表示用完連接后立即關閉 |
空行 | 必須 | 表示請求頭結束,開始正文(此處沒有正文) |
\r\n
是 HTTP 標準要求的換行符,不能用 \n
替代。
六、http_send_request()
函數流程圖
開始││ 輸入參數:hostname 和 resource│├─? 1. 通過 host_to_ip(hostname)│ └─ DNS 查詢 → 獲取 IP 地址(如 "14.215.177.39")│├─? 2. 調用 http_create_socket(ip)│ └─ 創建 TCP socket 并連接服務器 80 端口│├─? 3. 構造 HTTP 請求報文│ └─ 格式如下:│ GET /resource HTTP/1.1│ Host: hostname│ Connection: close│├─? 4. 使用 send() 發送請求數據到 socket│└─? 5. 當前版本未實現 recv(),結束函數
域名 → IP → TCP連接 → 構造請求 → 發送數據
七、完整代碼
#define HTTP_VERSION "HTTP/1.1" // 指定使用的 HTTP 協議版本
#define CONNETION_TYPE "Connection:close\r\n" // 設置連接類型為關閉連接(短連接)#define BUFFER_SIZE 4096 // 定義請求緩沖區大小// 將主機名(域名)轉換為 IP 地址字符串
char *host_to_ip(const char *hostname) {struct hostent *host_entry = gethostbyname(hostname); // 調用 DNS 查詢函數// 如果查詢成功,返回對應 IP 地址(點分十進制字符串)// h_addr_list 是 IP 地址列表,取第一個并轉換為字符串if (host_entry) {return inet_ntoa((struct in_addr*)*host_entry->h_addr_list);}// 查詢失敗返回 NULLreturn NULL;
}// 創建一個 TCP socket 并連接到指定 IP 地址的 80 端口
int http_create_socket(char *ip) {int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 創建 TCP socketstruct sockaddr_in sin = {0}; // 初始化服務器地址結構sin.sin_family = AF_INET; // 使用 IPv4 協議sin.sin_port = htons(80); // 設置端口為 80,使用 htons 轉換為網絡字節序sin.sin_addr.s_addr = inet_addr(ip); // 將 IP 字符串轉換為網絡字節序// 嘗試連接服務器if (0 != connect(sockfd, (struct sockaddr*)&sin, sizeof(struct sockaddr_in))) {return -1; // 連接失敗則返回 -1}fcntl(sockfd, F_SETFL, O_NONBLOCK); // 設置 socket 為非阻塞模式(可選)return sockfd; // 返回連接成功的 socket 文件描述符
}// 構造并發送一個 HTTP GET 請求
char * http_send_request(const char *hostname, const char *resource) {char *ip = host_to_ip(hostname); // 第一步:通過域名獲取 IP 地址int sockfd = http_create_socket(ip); // 第二步:創建并連接 socket 到服務器char buffer[BUFFER_SIZE] = {0}; // 初始化發送緩沖區// 第三步:構造 HTTP 請求報文// 組成部分包括請求行、Host 頭部、Connection 頭部sprintf(buffer,"GET %s %s\r\n" // 請求行:GET /path HTTP/1.1"Host: %s\r\n" // Host 頭:指定服務器域名"%s\r\n", // Connection: close(關閉連接)resource, HTTP_VERSION,hostname,CONNETION_TYPE);// 第四步:通過 socket 發送請求報文send(sockfd, buffer, strlen(buffer), 0);return NULL; // 當前函數版本沒有實現響應接收,暫時返回 NULL
}
https://github.com/0voice