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

C C++最新Http 协议, 前后端交互理解_http交互(1),作为一个C C++程序员你还不会JetPack 既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新如果你需要这些资料,可以戳这里获取前端浏览器像后端服务器通过URL进行全局的资源定位请求网页内容, 后端服务器接收解析请求。响应内容,响应数据.后端服务器任务: 为前端提供数据支撑, 计算支撑. 服务支撑. 服务器还可能需要连接请求第三方服务器 阅读详情

前面三十八篇文章,我们从变量、指针、数据结构一路学到系统调用、多线程、信号处理。现在我们终于有底气将它们全部串联起来,写一个真正能用的东西:一个迷你 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 本质上也是一种特殊的文件描述符——你可以对它进行 read 和 write。这正是系统编程的魅力:文件、设备、网络都统一为“文件描述符”的抽象。


二、TCP Socket 编程速成

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

1. 创建套接字:socket()

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

2. 绑定地址:bind()

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

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

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()

int listen(int sockfd, int backlog);

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

4. 接受连接:accept()

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

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

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

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

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
.htmltext/html
.csstext/css
.jsapplication/javascript
.pngimage/png
.jpgimage/jpeg
.gifimage/gif
.txttext/plain
其他application/octet-stream

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


五、多线程并发

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

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

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

#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环境下运行的代码:

/*
 * 迷你 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

<!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

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

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

九、编译与测试

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 仓库。

miniweb一个小巧轻量级http服务器 一个小巧轻量级http服务器,可以用于局域网文件共享,绿色免安装,还有一个很好的用途就是为路由器刷机提供SFTP、FTP替代。对没有内置SFTP、FTP支持的路由器,你可以用电脑上的miniweb和路由器上的wget传输文件 立即下载

相关推荐

小型http服务器(EasyWebServer)

一款小型的Web服务器软件。它可以很快速地在您 的PC上创建一个站点。

迷你HTTP服务器+小型博客

Http请求 首先编写代码进行解析请求 请求包含以下部分 请求行 GET /aaa.html?id=1&password=ppp&name=10 HTTP/1.1 请求头 Host: www.aaa.com Accept:text/html等信息 请求体 请求体(get方法没有请求体) 特殊格式的请求 “GET /hello?target=%E4%B8%AD%E5%9B%BD HT...

qq_43220266的博客 2407

Nginx下ModSecurity模块安装详细教程

本文提供了在Nginx 1.28.0上安装ModSecurity v3的完整教程。首先需安装编译依赖,然后从源码编译安装ModSecurity库并设置优化参数。接着编译Nginx动态模块并配置加载。核心配置包括设置规则引擎、日志路径及性能优化参数。安装OWASP核心规则集后,在Nginx站点配置中启用ModSecurity。最后测试规则有效性并给出性能优化建议,如规则排除、精简规则集等。整个过程包含详细的命令和配置示例,确保安全防护功能正常运作。

深山技术宅的博客 1781

一个非常小的http服务器NanoHTTPD使用教程

NanoHTTPD 免费,轻量,开源 的特性是每个开发者的最爱,由于采用的是java,天生跨平台,所以还可以运行android手机上。

qyhua的专栏 5764

Windows10内核逆向-2-KiSystemCall64的调用流程剖析

本文深入剖析了Windows 10 x64系统中KiSystemCall64函数的完整调用流程。从用户态触发syscall指令开始,详细讲解了如何通过swapgs切换内核栈、构建KTRAP_FRAME陷阱帧、进行SMAP/CFG等安全检查,到最终通过SSDT寻址并执行目标系统服务的全过程,揭示了系统调用从Ring 3到Ring 0切换的核心机制与安全设计。

weixin_29209805的博客 82

WinFrom真入门(1)——Windows窗体应用概念

用Winform开发的桌面程序,是在Windows操作系统上运行的,这个不用多说。窗体(Form)的作用‌:窗体是用户交互的容器,承载按钮、文本框等控件,构成应用程序的界面‌。在Windows操作系统中,窗体(窗口)所处可见,例如: 一个标准的窗体其实就是一块矩形区域,客户区域指的是窗口内部区域。除去标题兰和边框外的区域可视为内部区域,称为客户区域。我们开发人员可以在窗口的客户区域中放置控件,也可以在其中绘制出咱们所需要的图形。WinForm程序的项目的结构,如下图: Main()方法是程序的入口,这几行代

ByteGeek 1633

史上最小的Http服务器

概述 TinyHttp基于CSAPP(深入理解计算机系统第11章网络编成)实现。 TinyHttp足够小,只有不到300行的代码,却可以完整的展示一个Web服务器的基本原理和功能。 TinyHttp实现了HTTP的GET、POST、HEAD方法处理过程。 TinyHttp可以动态的扩展静态文件类型。 TinyHttp可以动态的扩展cgi-bin下的可执行程序,完成各种不同功能的动态内容的生成。 TinyHttp是健壮的,实现对于常见异常的处理过程,比如,SIGPIPE信号等。 项目代码:https://

飞翔de刺猬 2596

OpenWrt南京电信PPPoE IPv6配置:从零搭建稳定双栈网络

本文详细指导如何在南京电信PPPoE宽带环境下,为OpenWrt路由器配置稳定的IPv6双栈网络。核心在于正确设置WAN6接口为DHCPv6客户端,并请求/60前缀,同时配置LAN口分发地址并放行防火墙关键流量,确保内网设备获得原生IPv6连接。

water的专栏 1339

【计算机网络】mini HTTP服务器框架与代码

注注注:本篇博文都是极简HTTP实现概述,但不会进行演示,由于tcp套接字部分本质都是套路,所以就不再进行赘述。

2301_78636079的博客 1307

银河麒麟v10-arm版离线安装Portainer

银河麒麟v10-arm版离线安装Portainer

q_hsolucky的博客 3050

手把手教你用vivado2018.3为复旦微JFMQL15T打IP补丁(含环境变量配置技巧)

本文详细介绍了在Vivado 2018.3环境中为复旦微JFMQL15T开发板打IP补丁的全流程,包括环境变量配置、IP缓存管理等关键步骤。通过实战验证的解决方案,帮助工程师快速搭建开发环境,解决国产芯片开发中的常见问题,提升开发效率。

weixin_30374009的博客 723

C语言写的 mini版的 http 服务器 , 很详细

基于 http 协议实现的 mini版服务器

qq_62989250的博客 3582

VMware Workstation Pro 17 最新破解版安装教程(附永久激活密钥)

本文详细介绍了VMware Workstation Pro 17专业版的安装与配置指南,包括系统要求检查、官方版本安装流程、高级配置优化及常见问题排查。通过合规使用官方版本,开发者可以充分利用其强大的虚拟化能力,提升多平台开发与测试效率。

weixin_29327977的博客 3531

libuhttpd:灵活高效的HTTP服务器C库

libuhttpd:灵活高效的HTTP服务器C库 在现代网络编程中,一个轻量级且高性能的HTTP服务器库对于开发者来说至关重要。libuhttpd正是这样一个项目,它以简洁的代码结构、卓越的性能和广泛的适用性赢得了开发者的青睐。 项目介绍 libuhttpd是一个基于libev和http-parser的HTTP服务器C库,专为嵌入式Linux系统设计。它具备轻量、高效的特点,同时支持HTTPS、I...

gitblog_00956的博客 361

端到端自动驾驶的精准测试场景开发

1. 背景介绍 自动驾驶技术的发展正在改变交通运输的未来,然而,要实现安全可靠的自动驾驶,需要进行大量的测试和验证。端到端自动驾驶系统的测试场景开发是实现这一目标的关键。本文将详细介绍端到端自动驾驶的精准测试场景开发,包括核心概念、算法原理、数学模型、项目实践,以及未来发展趋势。 2. 核心概念与联系

AGI×大数据,开启智能时代的认知跃迁;解码AGI,赋能数据驱动的智能革命。 1318

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

Linux套接子(c语言)模拟http请求、应答

有关套接子和http请求报文的博客在优快云有很多比如,点这里查看,这里我就不再做过多赘述了,下面我们直接实战,模拟http请求。 要求:浏览器访问本地的localhost,在浏览器页面打印出 Hello World 首先:ping 一下百度的网址得到一个百度的ip,我们可以利用这个ip来查看http应答报头 39.156.69.79这是我们得到的百度的ip,事实上我下面用到的代码是另...

专注于C/C++/Linux领域创作 1034

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

Linux 14:HTTP协议与web服务器

浏览器与web服务器在应用层通信使用的是HTTP协议,而HTTP协议在传输层使用的是TCP协议。那么浏览器需要和web服务器三次握手建立连接后,才可以发送HTTP请求报文,服务器收到请求报文后,向浏览器回复HTTP应答报文。浏览器服务器建立连接后,如果两次以上的请求复用同一个TCP连接,则称之为长连接。如果浏览器发送一次请求报文,服务器回复一次应答就断开连接,下次交互再重新进行三次握手建立连接,那么就被称作短连接。使用长连接更好,可以减少网络中的同步报文,也使得服务器的响应速度变快。......

小柒的秃头之路 801

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

c语言网页制作,C语言写网页

[TOC]* * * * *## 软件下载#### a.软件名称apache http server#### b.下载地址[官网下载](http://www.apache.org/)[百度云下载](http://pan.baidu.com/s/1dFNz2Fn)#### c.软件截图![](https://box.kancloud.cn/e8ba2b9ce71bc655ac109bfce8829...

weixin_42509815的博客 3163

自己实现Http服务器:一、Http协议解析

之前总结过一篇TCP/IP协议的文章,而Http就是建立在TCP的基础之上的。TCP、UDP属于传输层协议,而Http是属于应用层的协议。 Http是超文本传输协议的缩写。协议详细内容参考RFC2616。由于面向文本的,所以使用ASCII编码。 Http特点 1. 客户/服务器模式,支持基本认证和安全认证(Https) 2. 简单快速 3. 灵活:允许传输任意类型数据,通过C

janekeyzheng的专栏 5949

【C语言网络编程】HTTP 客户端请求(发送请求报文过程)

在 C 语言中,我们可以使用 socket 编程来手动实现一个简单的 HTTP 客户端,像浏览器一样请求网页数据。本文将结合实际代码,重点讲解如何通过 C 语言构造并发送一个 HTTP 请求报文,实现与服务器的基本通信。

qq_64148519的博客 663

libmicrohttpd 一个 C 编写的小型 HTTP 库

libmicrohttpd 一个 C 编写的小型 HTTP 库 原文地址:http://www.aqcoder.com/post/content?id=39 by ravenq libmicrohttpd 是 GUN 下开源的一个小型的 HTTP 库,能够方便的嵌入到系统中。支持 HTTP 1.1 可以同时侦听多个端口,具有 select, poll, pthread, thread poo ...

Ravenq的专栏 6512

Windows下C语言实现HTTP客户端

Windows下C语言实现HTTP客户端 1 #include "stdafx.h" //控制台应用自带头文件,能够加快编译的速度 2 #include<winsock2.h> //通过该头文件调用winsock的函数,如下文的WSAStar...

weixin_30425949的博客 816

【综合类型第 39 篇】HTTP 状态码详解

注:本篇博客只是在「阿里云开发者社区版 HTTP 状态码详解」中按自己的写作风格做了断句,归纳整理,方便查看和阅读。

“Allen Su”的博客 1515

C语言 HTTP中的chunked解码实现

C语言不像C#一样有很多很多高度的模块化的东西可以使用,在通讯过程中特别是与http相关的通讯过程中可能要对网站返回的数据做一定处理,而且有不少网站的回应是强制性的,例如向网站请求deflate有个能会返回的是gzip的数据。在这过程中与web特性有关的,在服务器构造消息之前可能并不知道或者不方便知道消息的长度,于是就会将消息分为一段段进行传送。 例如如下回应: 不难发现,c...

weixin_30686845的博客 1099

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

在浏览器中,我们输入网址点击回车,就可以打开网页。那么这个过程中到底发生了什么?其实背后就是浏览器作为,向服务器发起了一个。本篇博客将手把手用纯 C 语言实现一个简洁版的“浏览器行为”:输入域名和资源路径使用 Socket 建立 TCP 连接构造并发送 HTTP GET 请求接收服务器响应内容(HTML 页面)打印到终端。

qq_64148519的博客 1242

【C语言网络编程】HTTP 客户端请求(域名解析过程)

host_to_ip在做 C 语言网络编程或模拟 HTTP 客户端时,第一步就离不开“”这一步。很多人可能直接复制粘贴一段的代码,但未必真正理解它的原理。深入剖析,并进一步带你了解 HTTP 请求是如何基于 TCP 通信进行的。

qq_64148519的博客 512
上一篇: 38. 【C语言】时间、信号与程序控制
下一篇: 40. 【C语言】终点亦是起点:C 语言的现代进化与后续学习路线
gugucoding
博客等级 码龄140天 7粉丝 102原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值