HTTP 服务端实现

本文通过 C 语言从零开始实现一个简单的 HTTP 服务端。这对后续在恶意软件的网络通信分析及在 IOT 漏洞挖掘的学习中都有所帮助。

新手的话,建议在 IDE 环境(如 CLion)下完成后续代码的编写。这样可以在使用一些库函数时,直接查看函数的传参及返回值,方便理解函数及代码编写。

本文在作者原文的基础上,增加了函数说明部分。同时,修改了代码中的一些变量命名等。原文地址:https://mp.weixin.qq.com/s/tjYWhgLf0lLiF9aT623YcQ。

1、HTTP 协议基础

HTTP(HyperText Transfer Protocol)是应用层协议,用于在 WEB 浏览器和服务端之间传输数据。它基于请求-响应模型,客户端发送请求,服务端返回响应。 一个典型的 HTTP 请求如下:

1
2
3
4
GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0
Accept: text/html

对应的 HTTP 响应如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1234

<!DOCTYPE html>
<html>
<head>
    <title>示例页面</title>
</head>
<body>
    <h1>Hello, World!</h1>
</body>
</html>

2、网络编程基础

在实现 HTTP 服务端之前,我们需要了解一些网络编程的基础知识。

2-1、Socket编程

Socket 是网络编程的基础,它提供了一种进程间通信的机制。在 Linux 系统中,通常需要包含以下头文件:

1
2
3
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

2-2、TCP/IP协议栈

HTTP 通常基于 TCP 协议,它保证了数据传输的可靠性。我们需要创建 TCP 服务端套接字来监听客户端连接。

2-3、服务端工作流程

(1)创建套接字

(2)绑定IP地址和端口

(3)监听连接请求

(4)接受客户端连接

(5)接收和解析HTTP请求

(6)构造并发送HTTP响应

(7)关闭连接

3、简单的服务端

让我们从一个最简单的 HTTP 服务端开始:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// httpd_v1.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 8088
#define BUFFER_SIZE 1024

int main() {

    int server_fd, new_socket;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};
    char *hello = "HTTP/1.1 200 OK\nContent-Type: text/plain\nContent-Length: 12\n\nHello World!";

    // 1、创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("[!] Socket Failed!\n");
        exit(EXIT_FAILURE);
    }

    // 2、配置服务器地址
    address.sin_family = AF_INET;
    address.sin_port = htons(PORT);
    address.sin_addr.s_addr = INADDR_ANY;

    // 3、绑定IP地址和端口
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("[!] Bind Failed!\n");
        exit(EXIT_FAILURE);
    }

    // 4、监听连接
    if (listen(server_fd, 3) < 0) {
        perror("[!] Listen Failed\n");
        exit(EXIT_FAILURE);
    }

    printf("[+] 服务器启动,监听端口 %d\n", PORT);

    // 5、接受并处理连接
    while (1) {
        if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) < 0) {
            perror("[!] Accept Failed\n");
           continue;
        }

        // 读取客户端请求
        read(new_socket, buffer, BUFFER_SIZE);
        printf("[+] 收到请求内容:%s\n", buffer);

        // 向客户端发送响应
        write(new_socket, hello, strlen(hello));
        printf("[+] 响应已发送\n");

        // 关闭当前连接
        close(new_socket);
    }


    return 0;
}

3-1、变量声明

(1)server_fd

1
 int server_fd;

server_fd 是一个非常关键的角色。简单来说,它是服务端监听套接字的文件描述符(File Descriptor)

在 Linux/Unix 操作系统中,有一个核心理念叫“一切皆文件”。无论是普通的文本文件、键盘输入,还是网络连接,系统都把它们看作“文件”。当程序打开一个网络连接时,内核会返回一个整数(比如 3, 4, 5…)。这个整数就是 File Descriptor (fd)

(2)new_socket

1
int new_socket;

server_fd 只是负责“监听”门口有没有人来,而 new_socket 才是真正负责和这个特定客人聊天的。后续的 read()(读客户端请求)和 send()(发回数据)都会用到这个变量。

这是当服务端成功接受一个客户端连接后,由 accept() 函数返回的新文件描述符,这个描述符专门用于与当前这个特定的客户端通信。

(3)address

1
struct sockaddr_in address;

这是一个结构体,用来存储服务端的网络地址信息

1
2
3
4
5
6
struct sockaddr_in {
	sa_family_t     sin_family;  // 地址族
	in_port_t       sin_port;    // 端口号
	struct  in_addr sin_addr;    // IP 地址结构体
	char            sin_zero[8]; // 填充字节(保证与 sockaddr 长度一致)
};
  • sin_family: 协议族(通常是 AF_INET,代表 IPv4)。

  • sin_port: 端口号(比如 8088)。

  • sin_addr.s_addr: IP 地址(比如 INADDR_ANY 表示监听所有网卡)。

(4)htons(PORT)

1
2
3
address.sin_family = AF_INET;
address.sin_port = htons(PORT);
address.sin_addr.s_addr = INADDR_ANY;

htons( ) 函数(host-to-network-short),将“主机字节序”(通常是小端序)转换为“网络字节序”(大端序)。

需将端口号和IP地址都转换为大端序,例如 htons(PORT)、INADDR_ANY(系统定义的无需转换)。

(5)addrlen

1
int addrlen = sizeof(address);

address 结构体的大小(以字节为单位)。

在调用 bind()accept() 等底层系统函数时,内核需要知道你传进去的地址结构体到底有多大,以防止内存溢出。通常用 sizeof(address) 来初始化。

(6)buffer (数据缓冲区)

1
char buffer[BUFFER_SIZE] = {0};

一个字符数组,用来暂存从网络上收到的数据。

当客户端发送 HTTP 请求(比如 “GET / HTTP/1.1”)过来时,程序会把这些原始字节存进这个 buffer 里,方便后续分析和处理。{0}:这是初始化操作,把数组里的所有字节都先清零,防止读取到之前的脏数据。

(7)hello (响应字符串)

1
char *hello = "HTTP/1.1 200 OK\nContent-Type: text/plain\nContent-Length: 12\n\nHello World!";

这是一个指向字符串常量的指针,内容是符合 HTTP 协议格式 的回复。

  • HTTP/1.1 200 OK:告诉浏览器请求成功。
  • Content-Type: text/plain:告诉浏览器我发给你的是纯文本。
  • Content-Length: 12:告诉浏览器正文(Hello World!)一共有 12 个字节。
  • \n\n:两个换行符是 HTTP 协议要求的,用来分隔“报头”和“正文”。
  • Hello World!:最终显示在网页上的内容。

3-2、socket()

1
int socket(int domain, int type, int protocol);

(1)domain (协议族/域):决定了通信的范围和地址格式。

  • AF_INET:代表 IPv4 网络协议(最常用)。
  • AF_INET6:代表 IPv6 网络协议。
  • AF_UNIX:用于同一台机器上的进程间通信(本地通信)。

(2)type (套接字类型):决定了通信的可靠性。

  • SOCK_STREAM:面向连接的、可靠的字节流传输(即 TCP)。数据保证不丢失、不重复、按序到达。
  • SOCK_DGRAM:无连接的数据报传输(即 UDP)。速度快但不保证可靠。

(3)protocol (具体协议):通常设为 0

  • domaintype 确定后,系统通常只有一种协议符合条件(比如 IPv4 + 流传输 = TCP),传 0 表示让系统自动选择该默认协议。

返回值

  • 成功:返回一个非负整数。它是该套接字的文件描述符(File Descriptor)。在 Linux 中,你可以像读写文件一样通过这个整数来操作网络连接。
  • 失败:返回 -1。同时全局变量 errno 会被设置,指明具体错误原因(如内存不足、权限不够等)。

3-3、bind()

1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

(1)sockfd (套接字文件描述符):由 socket() 函数返回的那个整数(即 server_fd)。

  • 它代表了刚才申请到的那个“通信端点”资源。

(2)*addr (指向地址结构体的指针):传入的是刚才配置好的 address 结构体的地址。

  • 注意强制类型转换:由于 bind 是一个通用函数,它可以接受多种协议的地址(IPv4, IPv6, Unix 域等),因此需要将特定的 struct sockaddr_in * 强制转换为通用的 struct sockaddr *

(3)addrlen (地址结构体长度):通常使用 sizeof(address)

  • 它告诉内核:这个地址结构体在内存中占用了多少字节,防止内核读取越界。

返回值

  • 成功:返回 0
  • 失败:返回 -1。常见的失败原因包括:端口已被占用(Address already in use)、权限不足(如监听 1024 以下的端口需要 root 权限)。

3-4、listen()

1
int listen(int sockfd, int backlog);

(1)sockfd (套接字文件描述符):即之前通过 socket() 创建并经过 bind() 绑定了地址和端口的 server_fd

  • 只有在此刻,这个套接字才正式从“主动通信”模式转变为“被动监听”模式。

(2)backlog (积压队列的最大长度):代码中设为了 3

  • 含义:它定义了内核中未处理连接请求队列的最大长度。
  • 作用:当多个客户端同时发起连接(三次握手)而服务端还没来得及调用 accept() 处理它们时,这些请求会排成一个队列。如果队列满了,新的连接请求可能会被拒绝(收到 ECONNREFUSED 错误)。
  • 注意:在高并发服务端中,这个值通常会设置得更大(如 128 或 512)。

返回值

  • 成功:返回 0
  • 失败:返回 -1。同时 errno 会记录失败原因(例如 EADDRINUSE 端口冲突或 EBADF 无效描述符)。

3-5、accept()

1
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

(1)sockfd (监听套接字)

传入 server_fd。它是服务端的“总机”,负责监听所有进入的连接请求。

(2)*addr (客户端地址结构体指针)

传入 &address。当连接建立时,内核会将客户端的 IP 和端口信息填充到这个结构体中。

(3)*addrlen (地址长度指针)

传入 &addrlen。必须是一个指向长度变量的指针,内核会更新它以反映实际收到的地址大小。

返回值

  • 成功:返回一个全新的非负整数(即代码中的 new_socket)。这个描述符专门用于与当前这个特定的客户端通信。
  • 失败:返回 -1

3-6、read()/write()

1
2
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

(1)fd (文件描述符)

使用 accept 返回的 new_socket。注意不能用 server_fd,因为总机不负责谈话。

(2)*buf (缓冲区指针)

  • read 时:传入 buffer,将网络数据存入内存。
  • write 时:传入 hello 字符串,将内存数据发往网络。

(3)count (字节数)

期望读取或写入的最大字节长度。

返回值

  • 成功:返回实际读取或写入的字节数
  • 失败:返回 -1。如果 read 返回 0,通常表示客户端已断开连接。

3-7、close()

1
int close(int fd);

(1)fd (文件描述符)

这里关闭的是 new_socket

返回值

  • 成功:返回 0
  • 失败:返回 -1

编译并运行这个程序:

gcc -o httpd_v1 httpd_v1.c
./httpd_v1

在浏览器中访问 http://localhost:8088,你将看到"Hello World!“的响应。

4、支持解析请求的服务端

为了构建一个更实用的 HTTP 服务端,我们需要解析客户端发送的 HTTP 请求:

在标准 HTTP/1.1 中,每一行必须以 CRLF\r\n)结尾。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
// httpd_v2.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 8088
#define BUFFER_SIZE 4096

// 解析 HTTP 请求方法和路径
void parse_request(char *request, char *method, char *path) {
    sscanf(request, "%s %s", method, path);
}

// 构造 HTTP 200 响应
void construct_200_response(char *response, const char *content) {
    sprintf(response,
        "HTTP/1.1 200 OK\r\n"
        "Content-Type: text/html; charset=utf-8\r\n"
        "Content-Length: %ld\r\n"
        "Connection: close\r\n\r\n"
        "%s",
        strlen(content),
        content);
}

// 构造 HTTP 404 响应
void construct_404_response(char *response, const char *content) {
    sprintf(response,
        "HTTP/1.1 404 Not Found\r\n"
        "Content-Type: text/html; charset=utf-8\r\n"
        "Content-Length: %ld\r\n"
        "Connection: close\r\n\r\n"
        "%s",
        strlen(content),
        content);
}

int main() {

    int server_fd, new_socket;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};
    char method[16], path[256];

    // HTML 页面内容(Home Page, About Page, Contact Page, 404 Page)
    const char *home_page =
        "<!DOCTYPE html>"
        "<html>"
        "<head><title>C语言HTTP服务器</title></head>"
        "<body>"
        "<h1>欢迎访问C语言HTTP服务器</h1>"
        "<p>这是一个用C语言实现的简单HTTP服务器</p>"
        "<ul>"
        "<li><a href='/about'>关于我们</a></li>"
        "<li><a href='/contact'>联系我们</a></li>"
        "</ul>"
        "</body>"
        "</html>";

    const char *about_page =
        "<!DOCTYPE html>"
        "<html>"
        "<head><title>关于我们</title></head>"
        "<body>"
        "<h1>关于我们</h1>"
        "<p>这是一个学习网络编程的示例项目</p>"
        "<a href='/'>返回首页</a>"
        "</body>"
        "</html>";

    const char *contact_page =
        "<!DOCTYPE html>"
        "<html>"
        "<head><title>联系我们</title></head>"
        "<body>"
        "<h1>联系我们</h1>"
        "<p>邮箱: [email protected]</p>"
        "<a href='/'>返回首页</a>"
        "</body>"
        "</html>";

    const char *not_found_page =
        "<!DOCTYPE html>"
        "<html>"
        "<head><title>404 Not Found</title></head>"
        "<body>"
        "<h1>404 Not Found</h1>"
        "<p>请求的页面不存在</p>"
        "<a href='/'>返回首页</a>"
        "</body>"
        "</html>";



    // 1、创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("[!] Socket Failed!\n");
        exit(EXIT_FAILURE);
    }

    // 2、配置服务器地址
    address.sin_family = AF_INET;
    address.sin_port = htons(PORT);
    address.sin_addr.s_addr = INADDR_ANY;

    // 3、绑定IP地址和端口
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("[!] Bind Failed!\n");
        exit(EXIT_FAILURE);
    }

    // 4、监听连接
    if (listen(server_fd, 10) < 0) {
        perror("[!] Listen Failed\n");
        exit(EXIT_FAILURE);
    }

    printf("[+] 服务器启动,监听端口 %d\n", PORT);

    // 5、接受并处理连接
    while (1) {
        if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) < 0) {
            perror("[!] Accept Failed\n");
            continue;
        }

        // 读取请求数据
        ssize_t read_value = read(new_socket, buffer, BUFFER_SIZE);

        if (read_value > 0) {
            // 解析请求数据
            parse_request(buffer, method, path);
            printf("[+] 请求内容: %s %s\n", method, path);

            // 构造响应数据
            char response[BUFFER_SIZE * 2];
            if (strcmp(path, "/") == 0) {
                construct_200_response(response, home_page);
            }else if (strcmp(path, "/about") == 0) {
                construct_200_response(response, about_page);
            }else if (strcmp(path, "/contact") == 0) {
                construct_200_response(response, contact_page);
            }else {
                construct_404_response(response, not_found_page);
            }

            // 发送响应数据
            write(new_socket, response, strlen(response));
        }

        // 关闭连接
        close(new_socket);
    }


    return 0;

}

5、支持解析静态文件的服务端

为了使我们的 HTTP 服务端更加实用,我们需要支持静态文件服务。

在开始之前,需要先理解一下函数 fseek、ftell、fread、fclose、strrchr。

5-1、fseek()

1
int fseek(FILE *stream, long offset, int whence);

(1)stream (文件指针)

指向由 fopen() 打开的文件流。它代表你要操作的具体文件。

(2)offset (偏移量)

这是一个长整型(long),表示相对于参照点移动的字节数。正数表示向文件末尾方向移动,负数表示向文件开头方向移动。

(3)whence (参照点/起始位置)

决定了 offset 从哪里开始计算。C 语言定义了三个宏:

  • SEEK_SET:从文件开头开始偏移。
  • SEEK_CUR:从当前光标位置开始偏移。
  • SEEK_END:从文件末尾开始偏移。

返回值

  • 成功:返回 0
  • 失败:返回 非零值(通常是 -1)。如果移动的位置超出了文件合法范围,或者文件流不支持随机访问,则会报错。

5-2、ftell()

1
long ftell(FILE *stream);

(1)stream (文件指针)

指向由 fopen() 打开并正在进行读写操作的文件流。

返回值

  • 成功:返回一个 long 类型的整数,代表当前文件位置指针相对于文件起始位置的字节偏移量
  • 失败:返回 -1L,并设置全局变量 errno 以指示错误(例如文件流不支持定位)。

5-3、fread()

1
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

(1)ptr (内存缓冲区指针)

指向一块已经分配好的内存区域(在程序代码里是 *content)。它是数据的“目的地”,即文件内容读出来后存放在哪。

(2)size (单元大小)

指定每个要读取的元素的大小,以字节为单位。通常读取文本或二进制流时设为 1(代表 1 字节)。

(3)nmemb (元素个数)

指定要读取多少个单元。在程序代码里是 *length(即文件总字节数)。

(4)stream (文件指针)

指向要读取的源文件。

返回值

  • 成功:返回实际成功读取的元素个数(即 nmemb)。

  • 异常:如果返回值小于 nmemb,说明发生了以下两种情况之一:

    • 读到文件末尾 (EOF):文件剩下的内容不足 nmemb

    • 读取错误:发生了硬件故障或权限问题。

    • 异常时,需要用 feof()ferror() 来进一步判断。

5-4、fclose()

1
int fclose(FILE *stream);

(1)stream (文件指针)

指向由 fopen() 成功打开的 FILE 结构体。这代表你想要关闭的那个具体文件流。

返回值

  • 成功:返回 0
  • 失败:返回 EOF (通常是 -1)。失败的原因通常包括:磁盘空间不足导致缓冲区刷新失败、文件流已经关闭、或者发生硬件错误。

5-5、strrchr()

函数名可以拆解为:str (string) + r (reverse/right) + chr (character)。

  • strchr:从往右找第一个。
  • strrchr:从往左找第一个(即找最后一次出现的位置)。
1
char *strrchr(const char *s, int c);

(1)\*s (待搜索的字符串)

在程序代码中是 filepath。它包含了文件的完整路径或文件名(如 ./www/index.html./www/xxx(.xx))。

(2)c (要查找的字符)

这里传入的是 '.'。虽然参数类型是 int,但通常传入一个字符常量。

返回值

  • 成功:返回一个指针,指向字符 c 在字符串 s最后一次出现的位置。
  • 失败:如果字符串中不存在该字符,返回 NULL

支持解析静态文件的服务端代码:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
// httpd_v3.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/stat.h>


#define PORT 8088
#define BUFFER_SIZE 4096
#define WEB_ROOT "./www"


// 读取文件内容
int read_file(const char *filepath, char **content, long *length) {
    // char*:是一个指向字符的指针,通常用来指向一个字符串或内存块。
    // char**:是一个指向“字符指针”的指针。它存储的是另一个指针的地址。
    // 如果传入 char *content,函数内部得到的只是原指针的一个副本。在 read_file 函数内部用 malloc 给这个副本分配内存,函数执行完后,外部原来的指针依然是 NULL,从而导致内存泄漏和程序崩溃。
    // 如果传入 char **content(即传入指针的地址),函数就可以通过这个地址,直接把 malloc 申请到的内存地址“写”回到外部的那个指针变量里。

    FILE *file = fopen(filepath, "rb");
    if (!file) {
        return -1;
    }

    // 获取文件大小
    fseek(file, 0, SEEK_END);  // 把文件的“光标”直接拉到文件末尾。
    *length = ftell(file);     // 询问系统此时光标距离文件开头有多少个字节。这个数字就是文件大小。
    fseek(file, 0, SEEK_SET);  // 测量完后必须把光标移回开头,否则后面的 fread 会从末尾开始读,读出一片空白。

    // 分配内存并读取文件
    *content = malloc(*length);
    if (!*content) {
        fclose(file);
        return -1;
    }

    fread(*content, 1, *length, file);
    fclose(file);

    return 0;
}


const char* get_mime_type(const char *filepath) {

    const char *dot = strrchr(filepath, '.');

    if (!dot) return "application/octet-stream";

    if (strcmp(dot, ".html") == 0) return "text/html";
    if (strcmp(dot, ".css") == 0) return "text/css";
    if (strcmp(dot, ".js") == 0) return "application/javascript";
    if (strcmp(dot, ".jpg") == 0 || strcmp(dot, ".jpeg") == 0) return "image/jpeg";
    if (strcmp(dot, ".png") == 0) return "image/png";
    if (strcmp(dot, ".gif") == 0) return "image/gif";

    return "application/octet-stream";
}


void handle_static_file(int client_socket, const char *path) {

    char filepath[512];

    // 处理根路径
    if (strcmp(path, "/") == 0) {
        snprintf(filepath, sizeof(filepath), "%s/index.html", WEB_ROOT);  // filepath: ./www/index.html
    }else {
        snprintf(filepath, sizeof(filepath), "%s%s", WEB_ROOT, path);     // filepath: ./www/xxx(.xx)
    }

    // 检查文件是否存在
    struct stat file_stat;
    if (stat(filepath, &file_stat) < 0) {
        // 404 错误(页面不存在)
        const char *not_found_page =
            "HTTP/1.1 404 Not Found\r\n"
            "Content-Type: text/html; charset=utf-8\r\n"
            "Content-Length: 88\r\n"
            "Connection: close\r\n\r\n"
            "<html><body><h1>404 Not Found</h1><p>The requested file was not found.</p></body></html>";
        write(client_socket, not_found_page, strlen(not_found_page));
        return;
    }

    // 读取文件内容
    char *file_content;
    long file_length;
    if (read_file(filepath, &file_content, &file_length) < 0) {
        // 500 错误(服务器错误)
        const char *server_error_page =
                "HTTP/1.1 500 Internal Server Error\r\n"
                "Content-Type: text/html; charset=utf-8\r\n"
                "Content-Length: 101\r\n"
                "Connection: close\r\n\r\n"
                "<html><body><h1>500 Internal Server Error</h1><p>Failed to read the requested file.</p></body></html>";
        write(client_socket, server_error_page, strlen(server_error_page));
        return;
    }

    // 获取文件 MIME 类型
    const char *mime_type = get_mime_type(filepath);

    // 构造并发送响应
    char response_header[BUFFER_SIZE];
    snprintf(response_header, sizeof(response_header),
        "HTTP/1.1 200 OK\r\n"
        "Content-Type: %s; charset=utf-8\r\n"
        "Content-Length: %ld\r\n"
        "Connection: close\r\n\r\n",
        mime_type,
        file_length);

    write(client_socket, response_header, strlen(response_header));
    write(client_socket, file_content, file_length);

    free(file_content);
}


int main() {

    int server_fd, new_socket;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};
    char method[16], path[256];

    // 创建 WEB 目录
    mkdir(WEB_ROOT, 0755);

    // 1、创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("[!] Socket Failed!\n");
        exit(EXIT_FAILURE);
    }

    // 2、配置服务器地址
    address.sin_family = AF_INET;
    address.sin_port = htons(PORT);
    address.sin_addr.s_addr = INADDR_ANY;

    // 3、绑定套接字
    if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) < 0) {
        perror("[!] Bind Failed!\n");
        exit(EXIT_FAILURE);
    }

    // 4、监听连接
    if (listen(server_fd, 10) < 0) {
        perror("[!] Listen Failed!\n");
        exit(EXIT_FAILURE);
    }

    printf("[+] 服务器启动,监听端口:%d\n", PORT);
    printf("[+] WEB根目录:%s\n", WEB_ROOT);

    // 5、接受并处理连接
    while (1) {
        if ((new_socket = accept(server_fd, (struct sockaddr*)&address, (socklen_t *)&addrlen)) < 0) {
            perror("[!] Accept Failed!\n");
            continue;
        }

        // 读取请求数据
        ssize_t read_value = read(new_socket, buffer, BUFFER_SIZE);
        if (read_value > 0) {
            // 解析请求数据
            sscanf(buffer, "%s %s", method, path);
            printf("[+] 请求内容:%s %s\n", method, path);

            // 处理 GET 请求
            if (strcmp(method, "GET") == 0) {
                handle_static_file(new_socket, path);
            }else {
                const char *not_allowed =
                    "HTTP/1.1 405 Method Not Allowed\r\n"
                    "Content-Type: text/html; charset=utf-8\r\n"
                    "Content-Length: 93\r\n"
                    "Connection: close\r\n\r\n"
                    "<html><body><h1>405 Method Not Allowed</h1><p>Only GET method is supported.</p></body></html>";
                // 不支持的请求方法
                write(new_socket, not_allowed, strlen(not_allowed));
            }
        }

        // 关闭连接
        close(new_socket);
    }


    return 0;

}

6、支持多线程的服务端

为了提高服务端的并发处理能力,我们可以使用多线程。

在开始之前,需要先理解一下函数 pthread_create、pthread_detach。

6-1、pthread_create()

1
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

(1)thread (线程标识符指针)

传入 &thread_id。这是一个输出参数,函数成功后会将新线程的 ID 写入这个变量,方便后续管理(如等待或分离)。

(2)attr (线程属性)

传入 NULL。表示使用默认属性(包括默认的栈大小、调度策略等)。

(3)start_routine (函数指针)

传入 handle_client。这是线程启动后要执行的入口函数。需注意的是,该函数必须符合 void* func(void*) 的格式,即接受一个万能指针并返回一个万能指针。

(4)arg (传递给线程函数的参数)

传入 (void*)client。这是主线程传递给子线程的“情报包”。由于子线程需要知道具体的 socket 描述符,我们将整个 struct client_info 的地址传过去。

返回值

  • 成功:返回 0
  • 失败:返回 错误码(注意:它不设置 errno,而是直接返回错误码,如 EAGAIN 资源不足)。

6-2、pthread_detach()

1
int pthread_detach(pthread_t thread);

(1)thread (线程标识符)

程序传入刚刚通过 pthread_create 获取到的 thread_id

核心作用

  • 状态转变:在 Linux 中,线程默认是 joinable(可结合)状态。在这种状态下,线程退出后其占用的内存(如线程栈、内核数据结构)不会被立即释放,直到主线程调用 pthread_join
  • 资源回收:调用 pthread_detach 后,线程进入 detached(分离)状态。这意味着该线程一结束,操作系统就会立即自动回收它占用的所有资源,无需主线程干预。

返回值

  • 成功:返回 0
  • 失败:返回错误码。

支持多线程的服务端代码:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
// httpd_v4.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/stat.h>
#include <pthread.h>


#define PORT 8088
#define BUFFER_SIZE 4096
#define WEB_ROOT "./www"


// 读取文件内容
int read_file(const char *filepath, char **content, long *length) {
    // char*:是一个指向字符的指针,通常用来指向一个字符串或内存块。
    // char**:是一个指向“字符指针”的指针。它存储的是另一个指针的地址。
    // 如果传入 char *content,函数内部得到的只是原指针的一个副本。在 read_file 函数内部用 malloc 给这个副本分配内存,函数执行完后,外部原来的指针依然是 NULL,从而导致内存泄漏和程序崩溃。
    // 如果传入 char **content(即传入指针的地址),函数就可以通过这个地址,直接把 malloc 申请到的内存地址“写”回到外部的那个指针变量里。

    FILE *file = fopen(filepath, "rb");
    if (!file) {
        return -1;
    }

    // 获取文件大小
    fseek(file, 0, SEEK_END);  // 把文件的“光标”直接拉到文件末尾。
    *length = ftell(file);     // 询问系统此时光标距离文件开头有多少个字节。这个数字就是文件大小。
    fseek(file, 0, SEEK_SET);  // 测量完后必须把光标移回开头,否则后面的 fread 会从末尾开始读,读出一片空白。

    // 分配内存并读取文件
    *content = malloc(*length);
    if (!*content) {
        fclose(file);
        return -1;
    }

    fread(*content, 1, *length, file);
    fclose(file);

    return 0;
}


const char* get_mime_type(const char *filepath) {

    const char *dot = strrchr(filepath, '.');

    if (!dot) return "application/octet-stream";

    if (strcmp(dot, ".html") == 0) return "text/html";
    if (strcmp(dot, ".css") == 0) return "text/css";
    if (strcmp(dot, ".js") == 0) return "application/javascript";
    if (strcmp(dot, ".jpg") == 0 || strcmp(dot, ".jpeg") == 0) return "image/jpeg";
    if (strcmp(dot, ".png") == 0) return "image/png";
    if (strcmp(dot, ".gif") == 0) return "image/gif";

    return "application/octet-stream";
}


void handle_static_file(int client_socket, const char *path) {

    char filepath[512];

    // 处理根路径
    if (strcmp(path, "/") == 0) {
        snprintf(filepath, sizeof(filepath), "%s/index.html", WEB_ROOT);  // filepath: ./www/index.html
    }else {
        snprintf(filepath, sizeof(filepath), "%s%s", WEB_ROOT, path);     // filepath: ./www/xxx(.xx)
    }

    // 检查文件是否存在
    struct stat file_stat;
    if (stat(filepath, &file_stat) < 0) {
        // 404 错误(页面不存在)
        const char *not_found_page =
            "HTTP/1.1 404 Not Found\r\n"
            "Content-Type: text/html; charset=utf-8\r\n"
            "Content-Length: 88\r\n"
            "Connection: close\r\n\r\n"
            "<html><body><h1>404 Not Found</h1><p>The requested file was not found.</p></body></html>";
        write(client_socket, not_found_page, strlen(not_found_page));
        return;
    }

    // 读取文件内容
    char *file_content;
    long file_length;
    if (read_file(filepath, &file_content, &file_length) < 0) {
        // 500 错误(服务器错误)
        const char *server_error_page =
                "HTTP/1.1 500 Internal Server Error\r\n"
                "Content-Type: text/html; charset=utf-8\r\n"
                "Content-Length: 101\r\n"
                "Connection: close\r\n\r\n"
                "<html><body><h1>500 Internal Server Error</h1><p>Failed to read the requested file.</p></body></html>";
        write(client_socket, server_error_page, strlen(server_error_page));
        return;
    }

    // 获取文件 MIME 类型
    const char *mime_type = get_mime_type(filepath);

    // 构造并发送响应
    char response_header[BUFFER_SIZE];
    snprintf(response_header, sizeof(response_header),
        "HTTP/1.1 200 OK\r\n"
        "Content-Type: %s; charset=utf-8\r\n"
        "Content-Length: %ld\r\n"
        "Connection: close\r\n\r\n",
        mime_type,
        file_length);

    write(client_socket, response_header, strlen(response_header));
    write(client_socket, file_content, file_length);

    free(file_content);
}


// 客户端处理线程参数
struct client_info {
    int socket;
    struct sockaddr_in address;
};

// 客户端处理线程函数
void* handle_client(void *arg) {

    char buffer[BUFFER_SIZE] = {0};
    char method[16], path[256];
    struct client_info *client = (struct client_info *)arg;

    ssize_t read_value = read(client->socket, buffer, BUFFER_SIZE);
    if (read_value > 0) {
        sscanf(buffer, "%s %s", method, path);
        printf("[+] 请求内容:%s %s\n", method, path);

        // 处理 GET 请求
        if (strcmp(method, "GET") == 0) {
            handle_static_file(client->socket, path);
        }else {
            const char *not_allowed =
                "HTTP/1.1 405 Method Not Allowed\r\n"
                "Content-Type: text/html; charset=utf-8\r\n"
                "Content-Length: 93\r\n"
                "Connection: close\r\n\r\n"
                "<html><body><h1>405 Method Not Allowed</h1><p>Only GET method is supported.</p></body></html>";
            // 不支持的请求方法
            write(client->socket, not_allowed, strlen(not_allowed));
        }
    }

    // 关闭连接
    close(client->socket);
    free(client);

    return NULL;
}



int main() {

    int server_fd, new_socket;
    struct sockaddr_in address;
    int addrlen = sizeof(address);

    // 创建 WEB 目录
    mkdir(WEB_ROOT, 0755);

    // 1、创建套接字
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
        perror("[!] Socket Failed!\n");
        exit(EXIT_FAILURE);
    }

    // 2、配置服务器地址
    address.sin_family = AF_INET;
    address.sin_port = htons(PORT);
    address.sin_addr.s_addr = INADDR_ANY;

    // 3、绑定套接字
    if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) < 0) {
        perror("[!] Bind Failed!\n");
        exit(EXIT_FAILURE);
    }

    // 4、监听连接
    if (listen(server_fd, 10) < 0) {
        perror("[!] Listen Failed!\n");
        exit(EXIT_FAILURE);
    }

    printf("[+] 多线程服务器启动,监听端口:%d\n", PORT);
    printf("[+] WEB根目录:%s\n", WEB_ROOT);

    // 5、接受并处理连接
    while (1) {

        struct client_info *client = malloc(sizeof(struct client_info));

        if (!client) {
            perror("[!] malloc client_info Failed!\n");
            continue;
        }

        // 接受并存储 socket 连接到 client 结构体
        if ((client->socket = accept(server_fd, (struct sockaddr*)&address, (socklen_t*)&addrlen)) < 0) {
            perror("[!] Accept Failed!\n");
            free(client);
            continue;
        }

        // 创建线程处理客户端请求
        pthread_t thread_id;
        if (pthread_create(&thread_id, NULL, handle_client, (void*)client) != 0) {
            perror("[!] pthread_create Failed!\n");
            close(client->socket);
            free(client);
            continue;
        }

        // 分离线程,避免僵尸线程
        pthread_detach(thread_id);
    }


    return 0;

}

编译多线程版本需要链接 pthread 库:

gcc -o httpd_v4 httpd_v4.c -lpthread

7、性能优化建议

7-1、使用线程池

创建和销毁线程的开销较大,使用线程池可以提高性能:

1
2
3
4
5
6
7
8
9
// 线程池示例结构
typedef struct {
    pthread_t *threads;           // 线程数组
    int thread_count;             // 线程数量
    pthread_mutex_t queue_mutex;  // 队列互斥锁
    pthread_cond_t queue_cond;    // 队列条件变量
    task_queue_t *task_queue;     // 任务队列
    int shutdown;                 // 关闭标志
} thread_pool_t;

7-2、使用 epoll(Linux)

对于高并发场景,可以使用epoll代替传统的阻塞I/O:

1
2
3
4
5
6
7
8
9
#include <sys/epoll.h>

int epoll_fd = epoll_create1(0);
struct epoll_event ev, events[MAX_EVENTS];

// 添加监听套接字到epoll
ev.events = EPOLLIN;
ev.data.fd = server_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev);

7-3、缓存静态文件

对于频繁访问的静态文件,可以将其内容缓存在内存中:

1
2
3
4
5
6
typedef struct {
    char *filepath;
    char *content;
    long length;
    time_t last_modified;
} file_cache_t;

8、安全考虑

8-1、防止路径遍历攻击

1
2
3
4
5
6
7
// 检查路径是否包含危险字符
int is_safe_path(const char* path) {
    if (strstr(path, "..") != NULL) {
        return 0; // 不安全
    }
    return 1; // 安全
}

8-2、限制请求大小

1
2
3
4
5
// 限制请求行长度
if (strlen(request_line) > MAX_REQUEST_LINE_LENGTH) {
    send_error_response(client_socket, 414, "Request-URI Too Long");
    return;
}

8-3、设置超时机制

1
2
3
4
5
// 设置套接字超时
struct timeval timeout;
timeout.tv_sec = 30;
timeout.tv_usec = 0;
setsockopt(client_socket, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));

9、测试与部署

9-1、基本功能测试

使用 curl 工具测试服务端:

# 测试首页
curl http://localhost:8088/

# 测试不存在的页面
curl http://localhost:8088/notfound

# 测试不同文件类型
curl -v http://localhost:8088/style.css

9-2、压力测试

使用ab(Apache Bench)进行压力测试:

# 1000 个请求,10 个并发
ab -n 1000 -c 10 http://localhost:8088/

9-3、部署建议

(1)使用 systemd 管理服务

(2)配置防火墙规则

(3)设置日志记录

(4)使用反向代理(如Nginx)

10、结语

通过这篇文章,我们从零开始用 C 语言实现了一个功能相对完整的 HTTP 服务端。虽然它还远未达到生产环境的要求,但已经具备了 HTTP 服务端的核心功能:

(1)解析HTTP请求

(2)处理静态文件

(3)支持多线程并发

(4)基本的安全防护

网络编程是一个复杂而有趣的领域,实现 HTTP 服务端只是其中的一个小部分。通过这个实践项目,你不仅能深入理解 HTTP 协议,还能掌握网络编程的核心技术。

updatedupdated2026-02-262026-02-26