一、前言
在浏览器中,我们输入网址点击回车,就可以打开网页。那么这个过程中到底发生了什么?其实背后就是浏览器作为HTTP 客户端 ,向服务器发起了一个 HTTP 请求。
本篇博客将手把手用纯 C 语言实现一个简洁版的"浏览器行为":
- 
输入域名和资源路径
 - 
使用 Socket 建立 TCP 连接
 - 
构造并发送 HTTP GET 请求
 - 
接收服务器响应内容(HTML 页面)
 - 
打印到终端
 
核心代码不足百行,帮助你彻底搞懂 HTTP 请求的底层流程。
二、程序运行效果
以访问百度首页 / 为例:
            
            
              bash
              
              
            
          
          ./http_client www.baidu.com /
        输出:
            
            
              bash
              
              
            
          
          response : HTTP/1.1 200 OK
Date: ...
Content-Type: text/html
...
<html>...</html>
        三、功能结构流程图(文字版)
下面是整个程序的结构流程图
            
            
              bash
              
              
            
          
          输入:主机名 和 资源路径
 │
 ├──▶ 1. 解析主机名为 IP 地址(host_to_ip)
 │       └─ 调用 DNS,获取如 "14.215.177.39"
 │
 ├──▶ 2. 创建 TCP Socket 并连接 80 端口(http_create_socket)
 │
 ├──▶ 3. 构造 HTTP 请求报文(GET + Host + Connection)
 │
 ├──▶ 4. 发送请求数据(send)
 │
 ├──▶ 5. 等待并接收服务器响应(select + recv)
 │
 └──▶ 6. 拼接为字符串返回并输出
        四、代码模块拆解
1. 解析主机名为 IP 地址
            
            
              cpp
              
              
            
          
          char *host_to_ip(const char *hostname) {
    struct hostent *host_entry = gethostbyname(hostname);
    if (host_entry) {
        return inet_ntoa(*(struct in_addr*)*host_entry->h_addr_list);
    }
    return NULL;
}
        作用 :使用 DNS 把域名转换为 IPv4 地址,例如:www.baidu.com → 14.215.177.39
2. 创建并连接 Socket
            
            
              cpp
              
              
            
          
          int http_create_socket(char *ip) {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in sin = {0};
    sin.sin_family = AF_INET;
    sin.sin_port = htons(80);
    sin.sin_addr.s_addr = inet_addr(ip);
    if (0 != connect(sockfd, (struct sockaddr*)&sin, sizeof(struct sockaddr_in))) {
        return -1;
    }
    fcntl(sockfd, F_SETFL, O_NONBLOCK);
    return sockfd;
}
        作用:基于目标 IP 地址创建 TCP socket,并连接到 80 端口(HTTP 服务端口)
3. 构造并发送 HTTP 请求
            
            
              cpp
              
              
            
          
          sprintf(buffer,
    "GET %s %s\r\n"
    "Host: %s\r\n"
    "%s\r\n"
    "\r\n",
    resource, HTTP_VERSION,
    hostname,
    CONNETION_TYPE);
send(sockfd, buffer, strlen(buffer), 0);
        作用:拼接出符合标准的 GET 请求报文,并通过 socket 发送出去。
请求示例:
            
            
              cpp
              
              
            
          
          GET / HTTP/1.1
Host: www.baidu.com
Connection:close
        4. 使用 select + recv 接收服务器响应
            
            
              cpp
              
              
            
          
          fd_set fdread;
FD_ZERO(&fdread);
FD_SET(sockfd, &fdread);
struct timeval tv = {5, 0};
char *result = malloc(1);
result[0] = '\0';
while (1) {
    int selection = select(sockfd + 1, &fdread, NULL, NULL, &tv);
    if (!selection || !FD_ISSET(sockfd, &fdread)) break;
    memset(buffer, 0, BUFFER_SIZE);
    int len = recv(sockfd, buffer, BUFFER_SIZE, 0);
    if (len == 0) break;
    result = realloc(result, strlen(result) + len + 1);
    strncat(result, buffer, len);
}
        作用:
- 
使用
select()等待 socket 变为可读 - 
调用
recv()读取响应内容 - 
使用动态字符串拼接响应文本
 
5. 主函数调用逻辑
            
            
              cpp
              
              
            
          
          int main(int argc, char *argv[]) {
    if (argc < 3) {
        printf("Usage: %s <hostname> <resource>\n", argv[0]);
        return -1;
    }
    char *response = http_send_request(argv[1], argv[2]);
    printf("response : %s\n", response);
    free(response);
}
        作用:从命令行读取目标域名和资源路径,调用 HTTP 请求函数并打印响应结果。
五、完整代码
            
            
              cpp
              
              
            
          
          #include <stdio.h>      // 标准输入输出函数,如 printf
#include <string.h>     // 字符串函数,如 strlen, memset, strncat
#include <stdlib.h>     // 内存分配函数,如 malloc, realloc
#include <sys/socket.h> // 套接字相关函数,如 socket, connect, send, recv
#include <netinet/in.h> // 地址结构 struct sockaddr_in
#include <arpa/inet.h>  // IP 地址转换函数,如 inet_addr, inet_ntoa
#include <unistd.h>     // close 函数等系统调用
#include <netdb.h>      // gethostbyname 用于域名解析
#include <fcntl.h>      // fcntl 函数设置非阻塞
// HTTP 协议版本
#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){
    // 通过 DNS 获取主机条目(IP 信息等)
    struct hostent *host_entry = gethostbyname(hostname); 
    // 将二进制 IP 地址转换为字符串形式返回
    if (host_entry)
    {
        return inet_ntoa(*(struct in_addr*)*host_entry->h_addr_list);
    }
    return NULL; // 解析失败返回 NULL
}
// 创建 TCP socket 并连接指定 IP 的 80 端口
int http_create_socket(char *ip){
    // 创建一个 TCP socket
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    // 设置服务器地址信息
    struct sockaddr_in sin = {0};
    sin.sin_family = AF_INET;             // 使用 IPv4
    sin.sin_port = htons(80);             // HTTP 默认端口(80),转换为网络字节序
    sin.sin_addr.s_addr = inet_addr(ip);  // 将字符串形式的 IP 转为网络字节序
    // 尝试连接服务器
    if (0 != connect(sockfd, (struct sockaddr*)&sin, sizeof(struct sockaddr_in)))
    {
        return -1; // 连接失败返回 -1
    }
    // 设置 socket 为非阻塞模式
    fcntl(sockfd, F_SETFL, O_NONBLOCK);
    return sockfd; // 返回 socket 文件描述符
}
// 发送 HTTP 请求并接收响应内容
char *http_send_request(const char *hostname, const char *resource){
    // 将主机名解析为 IP 地址
    char *ip = host_to_ip(hostname);
    // 通过 IP 创建并连接 socket
    int sockfd = http_create_socket(ip);
    // 准备发送的 HTTP 请求报文
    char buffer[BUFFER_SIZE] = {0};
    sprintf(buffer,
        "GET %s %s\r\n"
        "Host: %s\r\n"
        "%s\r\n"
        "\r\n",
        resource, HTTP_VERSION,
        hostname,
        CONNETION_TYPE
    );
    // 发送 HTTP 请求
    send(sockfd, buffer, strlen(buffer), 0);
    // 准备 select 用于等待 socket 可读
    fd_set fdread;
    FD_ZERO(&fdread);
    FD_SET(sockfd, &fdread);
    // 设置超时时间为 5 秒
    struct timeval tv;
    tv.tv_sec = 5;
    tv.tv_usec = 0;
    // 结果缓存,初始分配 4 个字节(但应该至少分配 1)
    char *result = malloc(sizeof(int));
    memset(result, 0, sizeof(int));  // 清空内存
    // 不断读取 socket 返回的数据
    while (1) {
        // 使用 select 等待可读事件
        int selection = select(sockfd + 1, &fdread, NULL, NULL, &tv);
        // 超时或 socket 不可读,退出循环
        if (!selection || !FD_ISSET(sockfd, &fdread)) {
            break;
        } else {
            // 清空接收缓冲区
            memset(buffer, 0, BUFFER_SIZE);
            // 从 socket 中接收数据
            int len = recv(sockfd, buffer, BUFFER_SIZE, 0);
            // 对方关闭连接
            if (len == 0) {
                break;
            }
            // 重新分配 result 空间,并拼接新接收到的数据
            result = realloc(result, (strlen(result) + len + 1) * sizeof(char));
            strncat(result, buffer, len);
        }
    }
    return result; // 返回完整的响应字符串
}
// 主函数,命令行调用格式:./程序名 www.xxx.com /path
int main(int argc, char *argv[]){
    // 参数不足,提示错误
    if (argc < 3) return -1;
    // 调用 HTTP 请求函数
    char *response = http_send_request(argv[1], argv[2]);
    // 打印响应内容
    printf("response : %s\n", response);
    // 释放申请的内存
    free(response);
}