39. 【C语言】经典 C 项目实战:迷你 HTTP 服务器

前面三十八篇文章,我们从变量、指针、数据结构一路学到系统调用、多线程、信号处理。现在我们终于有底气将它们全部串联起来,写一个真正能用的东西:一个迷你 HTTP 服务器

这个服务器程序会监听本地端口,接受浏览器请求,解析 GET 请求,从磁盘读取对应的文件(HTML、图片、CSS 等),并返回 HTTP 响应。我们还会加入多线程并发处理和信号优雅退出,让它像一个真正的服务器一样工作。

本文将带你从零开始,一步步构建它。


一、HTTP 服务器在做什么?

HTTP(超文本传输协议)是万维网的基础。它的工作模式很简单:

  1. 客户端(浏览器)连接服务器的某个端口(通常是 80 或 8080)。

  2. 客户端 发送一个 HTTP 请求 ,例如:

    复制代码
    GET /index.html HTTP/1.1
    Host: localhost:8080
  3. 服务器 解析这个请求,找到请求的资源(如 /index.html)。

  4. 服务器 构建一个 HTTP 响应

    复制代码
    HTTP/1.1 200 OK
    Content-Type: text/html
    Content-Length: 123
    
    <html>...</html>
  5. 服务器 发送响应,关闭连接(或保持连接)。

我们的服务器将使用 TCP 套接字(Socket) 来实现网络通信。如果你对系统调用有印象(第三十五篇),socket 本质上也是一种特殊的文件描述符------你可以对它进行 readwrite。这正是系统编程的魅力:文件、设备、网络都统一为"文件描述符"的抽象。


二、TCP Socket 编程速成

在开始写服务器之前,先快速过一下必需的 socket API。它们定义在 <sys/socket.h> 中。

1. 创建套接字:socket()

c 复制代码
int socket(int domain, int type, int protocol);
  • domain:协议族,IPv4 用 AF_INET
  • type:套接字类型,TCP 用 SOCK_STREAM
  • protocol:通常传 0 使用默认协议。
  • 返回值:文件描述符,失败返回 -1
c 复制代码
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) { perror("socket"); return 1; }

2. 绑定地址:bind()

c 复制代码
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

我们需要指定 IP 地址和端口。结构体 sockaddr_in(定义在 <netinet/in.h>)用于 IPv4:

c 复制代码
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;   // 监听所有网络接口
address.sin_port = htons(8080);         // 端口,htons 转换字节序

INADDR_ANY 表示接受来自任意网卡的连接。htons 将主机字节序转换为网络字节序(大端)。端口号我们选 8080(普通用户可不经 root 运行)。

3. 监听连接:listen()

c 复制代码
int listen(int sockfd, int backlog);

backlog 是等待连接队列的最大长度,我们设为 10

4. 接受连接:accept()

c 复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

它会阻塞 ,直到有客户端连接进来。返回一个新的文件描述符 ,用于与该客户端通信。原来的 server_fd 继续监听新连接。

5. 收发数据:send() / recv()

虽然可以用 read/write(socket 也是文件描述符),但 send/recv 提供更多控制选项。

c 复制代码
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

6. 关闭连接:close()


三、HTTP 请求解析

一个典型的 HTTP 请求长这样:

复制代码
GET /index.html HTTP/1.1\r\n
Host: localhost:8080\r\n
User-Agent: Mozilla/5.0\r\n
\r\n

我们需要从中提取方法 (GET)、路径(/index.html)、以及知道 HTTP 版本。后续可以忽略头部,只处理最简单的 GET 请求。

解析思路:用 recv 读入请求数据,找第一个空格和第二个空格之间的内容作为路径。如果路径是 /,默认映射到 /index.html


四、构建 HTTP 响应

根据请求路径,尝试打开对应的本地文件(如 ./www/index.html)。我们用系统调用 open/read 或标准 C 的 fopen/fread 都可以。为了与之前的知识一致,用标准 C 库会更方便处理文本和二进制。

构建响应头部的格式:

复制代码
HTTP/1.1 <状态码> <状态文本>\r\n
Content-Type: <MIME 类型>\r\n
Content-Length: <文件大小>\r\n
Connection: close\r\n
\r\n

后面紧跟文件内容。

MIME 类型我们只处理常见的几个扩展名:

扩展名 Content-Type
.html text/html
.css text/css
.js application/javascript
.png image/png
.jpg image/jpeg
.gif image/gif
.txt text/plain
其他 application/octet-stream

如果文件不存在,返回 404 页面;如果方法不是 GET,返回 405。


五、多线程并发

为了能同时处理多个客户端,每接受一个新连接,就创建一个线程来处理它。主线程继续等待下一个连接。这避免了串行处理导致浏览器排队超时。

我们将处理单个连接的逻辑封装到 handle_client 函数中,线程创建时传递客户端 socket 文件描述符的指针(动态分配内存,在线程中释放)。

c 复制代码
void *handle_client(void *arg) {
    int client_fd = *(int*)arg;
    free(arg);
    // 解析请求,发送响应
    close(client_fd);
    return NULL;
}

六、信号处理:优雅退出

当用户按 Ctrl+C 发送 SIGINT 信号时,我们应关闭服务器套接字,等待已连接的线程完成(或直接退出但释放资源)。一个简单做法是设置全局 volatile sig_atomic_t 标志,让主循环检查标志退出。退出前 close(server_fd),已创建的线程如果是分离的(pthread_detach)会自行结束,不会阻塞进程退出。为了简单,我们可以让主线程在退出时调用 close 并结束进程,线程会自动被终止(不优雅但可接受)。更完善的做法是跟踪所有线程并 pthread_join,但这里简化。


七、完整代码

文件结构

复制代码
project/
  server.c
  www/
    index.html
    style.css
    404.html

server.c

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>

#define PORT 8080
#define BUFFER_SIZE 4096
#define WWW_DIR "./www"

volatile sig_atomic_t running = 1;

void handle_signal(int sig) {
    (void)sig;
    running = 0;
}

const char *get_mime_type(const char *path) {
    const char *ext = strrchr(path, '.');
    if (!ext) return "application/octet-stream";
    if (strcmp(ext, ".html") == 0) return "text/html";
    if (strcmp(ext, ".css")  == 0) return "text/css";
    if (strcmp(ext, ".js")   == 0) return "application/javascript";
    if (strcmp(ext, ".png")  == 0) return "image/png";
    if (strcmp(ext, ".jpg")  == 0) return "image/jpeg";
    if (strcmp(ext, ".jpeg") == 0) return "image/jpeg";
    if (strcmp(ext, ".gif")  == 0) return "image/gif";
    if (strcmp(ext, ".txt")  == 0) return "text/plain";
    return "application/octet-stream";
}

/* 循环发送,确保所有数据都发送出去 */
ssize_t send_all(int fd, const void *buf, size_t len) {
    const char *p = buf;
    size_t total = 0;
    while (total < len) {
        ssize_t n = send(fd, p + total, len - total, 0);
        if (n == -1) {
            if (errno == EINTR) continue;
            return -1;
        }
        total += n;
    }
    return total;
}

void send_response(int client_fd, int status_code, const char *status_text,
                   const char *content_type, const void *body, size_t body_len) {
    char header[BUFFER_SIZE];
    int header_len = snprintf(header, sizeof(header),
        "HTTP/1.1 %d %s\r\n"
        "Content-Type: %s\r\n"
        "Content-Length: %zu\r\n"
        "Connection: close\r\n"
        "\r\n",
        status_code, status_text, content_type, body_len);

    if (header_len < 0 || (size_t)header_len >= sizeof(header)) {
        /* 响应头过长,发送简单错误 */
        const char *err = "HTTP/1.1 500 Internal Server Error\r\n\r\n";
        send(client_fd, err, strlen(err), 0);
        return;
    }

    if (send_all(client_fd, header, header_len) == -1) {
        perror("send header failed");
        return;
    }
    if (body && body_len > 0) {
        if (send_all(client_fd, body, body_len) == -1) {
            perror("send body failed");
        }
    }
}

/* 更严谨的目录遍历防护:规范化路径后检查 */
int is_path_safe(const char *path) {
    /* 拒绝包含 .. 的路径 */
    if (strstr(path, "..")) {
        return 0;
    }
    /* 拒绝空路径或不以 / 开头的路径 */
    if (!path || path[0] != '/') {
        return 0;
    }
    return 1;
}

/* 分块读取并发送文件,避免大文件一次性加载到内存 */
void serve_file_chunked(int client_fd, const char *filepath, const char *mime) {
    int fd = open(filepath, O_RDONLY);
    if (fd == -1) {
        const char *msg = "404 Not Found";
        send_response(client_fd, 404, "Not Found", "text/plain", msg, strlen(msg));
        return;
    }

    struct stat st;
    if (fstat(fd, &st) == -1) {
        close(fd);
        const char *msg = "500 Internal Server Error";
        send_response(client_fd, 500, "Internal Server Error", "text/plain", msg, strlen(msg));
        return;
    }

    off_t file_size = st.st_size;

    /* 发送响应头 */
    char header[BUFFER_SIZE];
    int header_len = snprintf(header, sizeof(header),
        "HTTP/1.1 200 OK\r\n"
        "Content-Type: %s\r\n"
        "Content-Length: %ld\r\n"
        "Connection: close\r\n"
        "\r\n",
        mime, (long)file_size);

    if (header_len < 0 || (size_t)header_len >= sizeof(header)) {
        close(fd);
        return;
    }

    if (send_all(client_fd, header, header_len) == -1) {
        close(fd);
        return;
    }

    /* 分块读取并发送 */
    char buf[BUFFER_SIZE];
    ssize_t n;
    while ((n = read(fd, buf, sizeof(buf))) > 0) {
        if (send_all(client_fd, buf, n) == -1) {
            break;
        }
    }

    close(fd);
}

void serve_file(int client_fd, const char *path) {
    if (!is_path_safe(path)) {
        const char *msg = "403 Forbidden";
        send_response(client_fd, 403, "Forbidden", "text/plain", msg, strlen(msg));
        return;
    }

    char filepath[512];
    int n = snprintf(filepath, sizeof(filepath), "%s%s", WWW_DIR, path);
    if (n < 0 || n >= (int)sizeof(filepath)) {
        const char *msg = "404 Not Found";
        send_response(client_fd, 404, "Not Found", "text/plain", msg, strlen(msg));
        return;
    }

    /* 检查是否是目录 */
    struct stat st;
    if (stat(filepath, &st) == 0 && S_ISDIR(st.st_mode)) {
        /* 目录请求,尝试 index.html */
        strncat(filepath, "/index.html", sizeof(filepath) - strlen(filepath) - 1);
    }

    /* 使用分块发送 */
    const char *mime = get_mime_type(filepath);
    serve_file_chunked(client_fd, filepath, mime);
}

void parse_request(const char *request, char *method, char *path) {
    method[0] = '\0';
    path[0] = '\0';

    /* 跳过前导空白 */
    const char *p = request;
    while (*p && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++;

    if (!*p) return;

    /* 解析 method */
    const char *m_start = p;
    while (*p && *p != ' ') p++;
    size_t method_len = p - m_start;
    if (method_len >= 16) method_len = 15;
    memcpy(method, m_start, method_len);
    method[method_len] = '\0';

    /* 跳过空格 */
    while (*p == ' ') p++;

    /* 解析 path */
    const char *path_start = p;
    while (*p && *p != ' ' && *p != '\r' && *p != '\n') p++;
    size_t path_len = p - path_start;
    if (path_len >= 256) path_len = 255;
    memcpy(path, path_start, path_len);
    path[path_len] = '\0';
}

void *handle_client(void *arg) {
    int client_fd = *(int*)arg;
    free(arg);

    char buffer[BUFFER_SIZE];
    ssize_t bytes = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
    if (bytes <= 0) {
        close(client_fd);
        return NULL;
    }
    buffer[bytes] = '\0';

    char method[16];
    char path[256];
    parse_request(buffer, method, path);

    /* 如果解析失败,返回 400 */
    if (method[0] == '\0' || path[0] == '\0') {
        const char *msg = "400 Bad Request";
        send_response(client_fd, 400, "Bad Request", "text/plain", msg, strlen(msg));
        close(client_fd);
        return NULL;
    }

    if (strcmp(method, "GET") != 0) {
        const char *msg = "405 Method Not Allowed";
        send_response(client_fd, 405, "Method Not Allowed", "text/plain", msg, strlen(msg));
    } else {
        if (strcmp(path, "/") == 0) {
            strcpy(path, "/index.html");
        }
        serve_file(client_fd, path);
    }

    close(client_fd);
    return NULL;
}

int main(void) {
    /* 使用 sigaction 替代 signal */
    struct sigaction sa;
    sa.sa_handler = handle_signal;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    if (sigaction(SIGINT, &sa, NULL) == -1 || sigaction(SIGTERM, &sa, NULL) == -1) {
        perror("sigaction");
        return 1;
    }

    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == -1) {
        perror("socket");
        return 1;
    }

    int opt = 1;
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {
        perror("setsockopt");
        close(server_fd);
        return 1;
    }

    struct sockaddr_in address;
    memset(&address, 0, sizeof(address));
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) == -1) {
        perror("bind");
        close(server_fd);
        return 1;
    }

    if (listen(server_fd, 10) == -1) {
        perror("listen");
        close(server_fd);
        return 1;
    }

    printf("服务器启动: http://localhost:%d\n", PORT);
    printf("按 Ctrl+C 退出\n");

    while (running) {
        struct sockaddr_in client_addr;
        socklen_t client_len = sizeof(client_addr);
        int *client_fd_ptr = malloc(sizeof(int));
        if (!client_fd_ptr) {
            perror("malloc");
            continue;
        }

        *client_fd_ptr = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
        if (*client_fd_ptr == -1) {
            free(client_fd_ptr);
            if (errno == EINTR) continue;
            perror("accept");
            continue;
        }

        /* 使用 inet_ntop 替代 inet_ntoa(线程安全) */
        char client_ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
        printf("新连接: %s:%d\n", client_ip, ntohs(client_addr.sin_port));

        pthread_t tid;
        if (pthread_create(&tid, NULL, handle_client, client_fd_ptr) != 0) {
            perror("pthread_create");
            close(*client_fd_ptr);
            free(client_fd_ptr);
            continue;
        }
        pthread_detach(tid);
    }

    printf("\n正在关闭服务器...\n");
    close(server_fd);
    return 0;
}

能在WIndows环境下运行的代码:

c 复制代码
/*
 * 迷你 HTTP 服务器 - Windows 兼容版本
 * 
 * 修改说明:
 * 1. 使用 Winsock2 API 替代 POSIX socket API
 * 2. 使用 Windows 线程 API 替代 pthread
 * 3. 使用 Windows 信号处理替代 POSIX sigaction
 * 4. 使用 _stat 替代 stat
 * 5. 使用 CreateFile/ReadFile 替代 open/read
 * 
 * 编译命令(MinGW/MSVC):
 *   gcc -o server.exe server_windows.c -lws2_32
 *   或
 *   cl server_windows.c /link ws2_32.lib
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#include <windows.h>
#include <ws2tcpip.h>
#include <sys/stat.h>

/* 链接 Winsock 库 */
#pragma comment(lib, "ws2_32.lib")

#define PORT 8080
#define BUFFER_SIZE 4096
#define WWW_DIR "./www"
#define MAX_THREADS 128

/* 全局状态 */
volatile LONG running = 1;
HANDLE server_mutex = NULL;

/* 客户端参数结构 */
typedef struct {
    SOCKET client_fd;
    struct sockaddr_in client_addr;
} client_info_t;

/* MIME 类型判断 */
const char *get_mime_type(const char *path) {
    const char *ext = strrchr(path, '.');
    if (!ext) return "application/octet-stream";
    if (_stricmp(ext, ".html") == 0) return "text/html";
    if (_stricmp(ext, ".css")  == 0) return "text/css";
    if (_stricmp(ext, ".js")   == 0) return "application/javascript";
    if (_stricmp(ext, ".png")  == 0) return "image/png";
    if (_stricmp(ext, ".jpg")  == 0) return "image/jpeg";
    if (_stricmp(ext, ".jpeg") == 0) return "image/jpeg";
    if (_stricmp(ext, ".gif")  == 0) return "image/gif";
    if (_stricmp(ext, ".txt")  == 0) return "text/plain";
    return "application/octet-stream";
}

/* 循环发送,确保所有数据都发送出去 */
int send_all(SOCKET fd, const void *buf, size_t len) {
    const char *p = buf;
    size_t total = 0;
    while (total < len) {
        int n = send(fd, p + total, (int)(len - total), 0);
        if (n == SOCKET_ERROR) {
            int err = WSAGetLastError();
            if (err == WSAEINTR) continue;
            return -1;
        }
        total += n;
    }
    return (int)total;
}

void send_response(SOCKET client_fd, int status_code, const char *status_text,
                   const char *content_type, const void *body, size_t body_len) {
    char header[BUFFER_SIZE];
    int header_len = snprintf(header, sizeof(header),
        "HTTP/1.1 %d %s\r\n"
        "Content-Type: %s\r\n"
        "Content-Length: %zu\r\n"
        "Connection: close\r\n"
        "\r\n",
        status_code, status_text, content_type, body_len);

    if (header_len < 0 || (size_t)header_len >= sizeof(header)) {
        const char *err = "HTTP/1.1 500 Internal Server Error\r\n\r\n";
        send(client_fd, err, (int)strlen(err), 0);
        return;
    }

    if (send_all(client_fd, header, header_len) == -1) {
        fprintf(stderr, "send header failed\n");
        return;
    }
    if (body && body_len > 0) {
        if (send_all(client_fd, body, body_len) == -1) {
            fprintf(stderr, "send body failed\n");
        }
    }
}

/* 目录遍历防护 */
int is_path_safe(const char *path) {
    if (strstr(path, "..")) {
        return 0;
    }
    if (!path || path[0] != '/') {
        return 0;
    }
    return 1;
}

/* 分块读取并发送文件 */
void serve_file_chunked(SOCKET client_fd, const char *filepath, const char *mime) {
    HANDLE hFile = CreateFileA(filepath, GENERIC_READ, FILE_SHARE_READ, NULL,
                                OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) {
        const char *msg = "404 Not Found";
        send_response(client_fd, 404, "Not Found", "text/plain", msg, strlen(msg));
        return;
    }

    LARGE_INTEGER file_size;
    if (!GetFileSizeEx(hFile, &file_size)) {
        CloseHandle(hFile);
        const char *msg = "500 Internal Server Error";
        send_response(client_fd, 500, "Internal Server Error", "text/plain", msg, strlen(msg));
        return;
    }

    /* 发送响应头 */
    char header[BUFFER_SIZE];
    int header_len = snprintf(header, sizeof(header),
        "HTTP/1.1 200 OK\r\n"
        "Content-Type: %s\r\n"
        "Content-Length: %lld\r\n"
        "Connection: close\r\n"
        "\r\n",
        mime, file_size.QuadPart);

    if (header_len < 0 || (size_t)header_len >= sizeof(header)) {
        CloseHandle(hFile);
        return;
    }

    if (send_all(client_fd, header, header_len) == -1) {
        CloseHandle(hFile);
        return;
    }

    /* 分块读取并发送 */
    char buf[BUFFER_SIZE];
    DWORD bytes_read;
    while (ReadFile(hFile, buf, sizeof(buf), &bytes_read, NULL) && bytes_read > 0) {
        if (send_all(client_fd, buf, bytes_read) == -1) {
            break;
        }
    }

    CloseHandle(hFile);
}

void serve_file(SOCKET client_fd, const char *path) {
    if (!is_path_safe(path)) {
        const char *msg = "403 Forbidden";
        send_response(client_fd, 403, "Forbidden", "text/plain", msg, strlen(msg));
        return;
    }

    char filepath[512];
    int n = snprintf(filepath, sizeof(filepath), "%s%s", WWW_DIR, path);
    if (n < 0 || n >= (int)sizeof(filepath)) {
        const char *msg = "404 Not Found";
        send_response(client_fd, 404, "Not Found", "text/plain", msg, strlen(msg));
        return;
    }

    /* 检查是否是目录 */
    struct _stat st;
    if (_stat(filepath, &st) == 0 && (st.st_mode & _S_IFDIR)) {
        strncat(filepath, "/index.html", sizeof(filepath) - strlen(filepath) - 1);
    }

    const char *mime = get_mime_type(filepath);
    serve_file_chunked(client_fd, filepath, mime);
}

void parse_request(const char *request, char *method, char *path) {
    method[0] = '\0';
    path[0] = '\0';

    const char *p = request;
    while (*p && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++;

    if (!*p) return;

    const char *m_start = p;
    while (*p && *p != ' ') p++;
    size_t method_len = p - m_start;
    if (method_len >= 16) method_len = 15;
    memcpy(method, m_start, method_len);
    method[method_len] = '\0';

    while (*p == ' ') p++;

    const char *path_start = p;
    while (*p && *p != ' ' && *p != '\r' && *p != '\n') p++;
    size_t path_len = p - path_start;
    if (path_len >= 256) path_len = 255;
    memcpy(path, path_start, path_len);
    path[path_len] = '\0';
}

/* 客户端处理函数(Windows 线程入口) */
DWORD WINAPI handle_client(LPVOID arg) {
    client_info_t *info = (client_info_t*)arg;
    SOCKET client_fd = info->client_fd;
    struct sockaddr_in client_addr = info->client_addr;
    free(info);

    char buffer[BUFFER_SIZE];
    int bytes = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
    if (bytes <= 0) {
        closesocket(client_fd);
        return 0;
    }
    buffer[bytes] = '\0';

    char method[16];
    char path[256];
    parse_request(buffer, method, path);

    if (method[0] == '\0' || path[0] == '\0') {
        const char *msg = "400 Bad Request";
        send_response(client_fd, 400, "Bad Request", "text/plain", msg, strlen(msg));
        closesocket(client_fd);
        return 0;
    }

    if (_stricmp(method, "GET") != 0) {
        const char *msg = "405 Method Not Allowed";
        send_response(client_fd, 405, "Method Not Allowed", "text/plain", msg, strlen(msg));
    } else {
        if (strcmp(path, "/") == 0) {
            strcpy(path, "/index.html");
        }
        serve_file(client_fd, path);
    }

    closesocket(client_fd);
    return 0;
}

/* Windows 控制台信号处理 */
BOOL WINAPI console_handler(DWORD signal) {
    if (signal == CTRL_C_EVENT || signal == CTRL_BREAK_EVENT) {
        InterlockedExchange(&running, 0);
        return TRUE;
    }
    return FALSE;
}

int main(void) {
    /* 设置控制台信号处理 */
    if (!SetConsoleCtrlHandler(console_handler, TRUE)) {
        fprintf(stderr, "无法设置信号处理程序\n");
        return 1;
    }

    /* 初始化 Winsock */
    WSADATA wsaData;
    int wsaResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (wsaResult != 0) {
        fprintf(stderr, "WSAStartup 失败: %d\n", wsaResult);
        return 1;
    }

    /* 创建 socket */
    SOCKET server_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (server_fd == INVALID_SOCKET) {
        fprintf(stderr, "socket 失败: %d\n", WSAGetLastError());
        WSACleanup();
        return 1;
    }

    /* 允许端口重用 */
    int opt = 1;
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, 
                   (const char*)&opt, sizeof(opt)) == SOCKET_ERROR) {
        fprintf(stderr, "setsockopt 失败: %d\n", WSAGetLastError());
        closesocket(server_fd);
        WSACleanup();
        return 1;
    }

    struct sockaddr_in address;
    memset(&address, 0, sizeof(address));
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(PORT);

    if (bind(server_fd, (struct sockaddr*)&address, sizeof(address)) == SOCKET_ERROR) {
        fprintf(stderr, "bind 失败: %d\n", WSAGetLastError());
        closesocket(server_fd);
        WSACleanup();
        return 1;
    }

    if (listen(server_fd, 10) == SOCKET_ERROR) {
        fprintf(stderr, "listen 失败: %d\n", WSAGetLastError());
        closesocket(server_fd);
        WSACleanup();
        return 1;
    }

    printf("服务器启动: http://localhost:%d\n", PORT);
    printf("按 Ctrl+C 退出\n");

    while (InterlockedCompareExchange(&running, 0, 0)) {
        fd_set readfds;
        FD_ZERO(&readfds);
        FD_SET(server_fd, &readfds);

        struct timeval tv;
        tv.tv_sec = 1;
        tv.tv_usec = 0;

        int ret = select(0, &readfds, NULL, NULL, &tv);
        if (ret == SOCKET_ERROR) {
            // select 出错,若 running 已为 0 则退出
            if (InterlockedCompareExchange(&running, 0, 0) == 0)
                break;
            fprintf(stderr, "select 失败: %d\n", WSAGetLastError());
            continue;
        } else if (ret == 0) {
            // 超时,重新检查 running 标志
            continue;
        }

        // 有连接到达
        struct sockaddr_in client_addr;
        int client_len = sizeof(client_addr);

        SOCKET client_socket = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
        if (client_socket == INVALID_SOCKET) {
            int err = WSAGetLastError();
            if (err == WSAEINTR)
                continue;
            if (InterlockedCompareExchange(&running, 0, 0) == 0)
                break;
            fprintf(stderr, "accept 失败: %d\n", err);
            continue;
        }

        // 获取客户端 IP 并打印
        char client_ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
        printf("新连接: %s:%d\n", client_ip, ntohs(client_addr.sin_port));

        client_info_t *info = malloc(sizeof(client_info_t));
        if (!info) {
            fprintf(stderr, "malloc 失败\n");
            closesocket(client_socket);
            continue;
        }
        info->client_fd = client_socket;
        info->client_addr = client_addr;

        HANDLE hThread = CreateThread(NULL, 0, handle_client, info, 0, NULL);
        if (hThread == NULL) {
            fprintf(stderr, "CreateThread 失败: %lu\n", GetLastError());
            closesocket(client_socket);
            free(info);
            continue;
        }
        CloseHandle(hThread);
    }

    printf("\n正在关闭服务器...\n");
    closesocket(server_fd);
    WSACleanup();
    return 0;
}

八、准备测试网页

www/ 目录下创建几个文件:

www/index.html

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>欢迎</title>
    <link rel="stylesheet" href="/style.css">
</head>
<body>
    <h1>你好,C 语言 HTTP 服务器!</h1>
    <p>这是一个用不到 200 行 C 代码写成的迷你服务器。</p>
</body>
</html>

www/style.css

css 复制代码
body {
    font-family: Arial, sans-serif;
    background-color: #f0f0f0;
    color: #333;
    max-width: 600px;
    margin: 50px auto;
    text-align: center;
}
h1 { color: #0066cc; }

www/404.html

html 复制代码
<!DOCTYPE html>
<html>
<head><title>404 Not Found</title></head>
<body>
    <h1>404 - 页面未找到</h1>
    <p>你请求的资源不存在。</p>
</body>
</html>

九、编译与测试

bash 复制代码
gcc -pthread -o server server.c
./server

打开浏览器访问 http://localhost:8080,你会看到漂亮的网页!再试一个不存在的路径 http://localhost:8080/nonexist,会显示 404 页面。

Ctrl+C,服务器打印关闭信息并退出。


十、可以继续完善的方向

这个迷你 HTTP 服务器已经具备了基本骨架,你可以在此基础上添加:

  • POST 请求支持 :解析 Content-Length 请求体。
  • CGI 动态页面:调用外部程序生成响应。
  • 日志记录:记录每个请求的时间和 IP。
  • 线程池:避免频繁创建销毁线程。
  • 配置解析:从配置文件读取端口、根目录等。

这个项目虽小,但已经融合了你学过的几乎全部核心技能:文件 I/O、字符串处理、内存管理、多线程、信号处理、网络系统调用。


十一、小结

今天,我们用 C 语言从零构建了一个能处理浏览器请求的 HTTP 服务器,把它变成了一个真正的网络服务。你综合运用了:

  • Socket 编程socket/bind/listen/accept/send/recv)作为网络通信基础。
  • 文件操作fopen/fread/fclose)读取静态资源。
  • HTTP 协议的解析与响应构建。
  • 多线程并发处理多个客户端。
  • 信号处理实现优雅退出。
  • 内存管理确保无泄漏。

这是你 C 语言学习路上的一座里程碑。它证明你不再只是"学语法",而是能够构建实际可用的系统。恭喜你!


十二、课后小练习

  1. 给服务器添加一个简单的访问日志:在控制台打印每个请求的方法、路径和客户端 IP。
  2. 实现一个简单的目录列表功能:如果请求路径是一个目录且没有默认 index.html,列出该目录下的文件和子目录(返回 HTML 格式)。
  3. 为服务器增加命令行参数支持:可以指定端口号和根目录(如 ./server -p 9090 -d /var/www)。
  4. (小挑战)将单线程中的 handle_client 改造成线程池版本:预先创建固定数量的工作线程,通过队列分发客户端 socket。这可以更好地控制并发量和资源使用。

我们下期见!

💡获取本系列示例代码请访问 GitCode 仓库

相关推荐
什巳1 小时前
JAVA练习277- 找到字符串中所有字母异位
java·开发语言·算法·leetcode
头茬韭菜1 小时前
3.6 依赖注入 — 四个依赖的精准边界
开发语言·ai·ecmascript
江华森2 小时前
Python神经网络编程(四):Python从零搭建神经网络
开发语言·python·神经网络
Helen_cai3 小时前
OpenHarmony 完整项目工程整合规范 + 模块化分层架构(API23+ 标准企业级结构)
开发语言·华为·php·harmonyos
大圣编程8 小时前
Java 多维数组详解
java·开发语言
殳翰11 小时前
下服务器端开发流程及相关工具介绍(C++)
开发语言·c++
落寞的星星11 小时前
这个对象就包含了已经转换好的DFA和各种词法分析器运转所需要的参数。下一步,我们就可以用ScannerInfo对象创建出Scanner对象,请看下面的代码:
开发语言·c#
nothing&nowhere11 小时前
用 Python 做问卷数据清洗:无效样本检测与处理实战
开发语言·python·数据清洗·数据处理·问卷星·问卷星脚本·刷问卷
2601_9615934212 小时前
Rust 开发环境配置繁琐?RustRover 开箱即用搞定编码调试
开发语言·后端·macos·rust