【C语言网络编程】HTTP 客户端请求(基于 Socket 的完整实现)

一、前言

在浏览器中,我们输入网址点击回车,就可以打开网页。那么这个过程中到底发生了什么?其实背后就是浏览器作为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.com14.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);
}

https://github.com/0voice

相关推荐
sweet丶1 小时前
MQTT消息通道-基础篇
网络协议
yychen_java2 小时前
当算法成为武器:AI泛滥时代的多维危机透视与治理路径
网络·人工智能·ai
漫途科技2 小时前
精准盯防危房隐患,智守人居安全|MTB46-4-2A 4G数据采集终端专项应用方案
网络·安全
奥利奥夹心脆芙3 小时前
辅助排查 HTTP 接口代码报错,实操完整案例分享
http
Misnearch3 小时前
抓包Packet Capture
网络·抓包
zhangfeng11334 小时前
ps aux讲解,结合国家超算中心 hpc apptainer
linux·服务器·网络
吠品4 小时前
一次 Nginx 报错 unexpected end of file 的排查记录
网络协议·https·ssl
代码中介商4 小时前
TLS握手全解析:从1.2到1.3的加密演进
网络·网络协议·http
xlq223224 小时前
66.ip
网络·网络协议·tcp/ip
tudoSearcher4 小时前
手机、平板、电脑同时控制Claude Code / Codex ?:Paseo实战指南
网络·开源·开源软件·个人开发·ai编程