用 C 语言实现一个支持 HTML 与视频的 HTTP(S) 服务器

1. 引言

HTTP 服务器是网络编程中最经典也最实用的入门项目之一。用 C 语言从零实现一个支持 HTML 页面与视频文件传输的 HTTP(S) 服务器,不仅能帮助你深入理解 TCP/IP、HTTP 协议与 TLS 加密通信的原理,还能为后续学习高性能服务器(如 epoll、多线程模型)打下坚实基础。

本文将带你从 socket 编程起步,逐步实现一个可运行、可扩展的 HTTP/1.1 服务器,并在此基础上接入 OpenSSL 库,使其支持 HTTPS。全文包含完整可编译的 C 代码、关键原理讲解与常见问题排查。

2. 准备工作与环境搭建

在开始编码之前,需要准备以下环境:

  • 操作系统:Linux(推荐 Ubuntu/Debian)或 macOS
  • 编译器 :GCC(gcc --version 确认已安装)
  • 构建工具:Make
  • SSL 库:OpenSSL 开发包(用于 HTTPS 支持)

安装依赖(Ubuntu/Debian):

bash 复制代码
sudo apt update
sudo apt install build-essential libssl-dev

macOS 用户可使用 Homebrew:

bash 复制代码
brew install openssl

3. 核心概念速览

在动手写代码前,先理清几个关键概念:

3.1 HTTP 协议基础

HTTP 请求由请求行、请求头和请求体组成。一个典型的 GET 请求如下:

http 复制代码
GET /index.html HTTP/1.1
Host: localhost:8080
User-Agent: curl/8.0
Connection: close

服务器需要解析请求行中的方法(GET/POST)、路径(/index.html)和协议版本,然后返回对应的响应。

3.2 HTTP 响应格式

http 复制代码
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 1234
Connection: close

<html>...</html>

关键响应头包括 Content-Type(告知浏览器内容类型)和 Content-Length(告知响应体长度,浏览器据此判断数据是否接收完整)。

3.3 MIME 类型

服务器需要根据文件扩展名返回正确的 Content-Type,否则浏览器可能无法正确渲染页面或播放视频。常见映射:

扩展名 MIME 类型
.html text/html
.css text/css
.js application/javascript
.jpg image/jpeg
.png image/png
.mp4 video/mp4
.webm video/webm

4. 服务器整体架构

我们的服务器采用单进程、单线程、阻塞式模型,逻辑清晰、易于理解。整体流程如下:
#mermaid-svg-RIkCgKiz9dWX9E8m{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RIkCgKiz9dWX9E8m .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RIkCgKiz9dWX9E8m .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RIkCgKiz9dWX9E8m .error-icon{fill:#552222;}#mermaid-svg-RIkCgKiz9dWX9E8m .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RIkCgKiz9dWX9E8m .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RIkCgKiz9dWX9E8m .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RIkCgKiz9dWX9E8m .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RIkCgKiz9dWX9E8m .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RIkCgKiz9dWX9E8m .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RIkCgKiz9dWX9E8m .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RIkCgKiz9dWX9E8m .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RIkCgKiz9dWX9E8m .marker.cross{stroke:#333333;}#mermaid-svg-RIkCgKiz9dWX9E8m svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RIkCgKiz9dWX9E8m p{margin:0;}#mermaid-svg-RIkCgKiz9dWX9E8m .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-RIkCgKiz9dWX9E8m .cluster-label text{fill:#333;}#mermaid-svg-RIkCgKiz9dWX9E8m .cluster-label span{color:#333;}#mermaid-svg-RIkCgKiz9dWX9E8m .cluster-label span p{background-color:transparent;}#mermaid-svg-RIkCgKiz9dWX9E8m .label text,#mermaid-svg-RIkCgKiz9dWX9E8m span{fill:#333;color:#333;}#mermaid-svg-RIkCgKiz9dWX9E8m .node rect,#mermaid-svg-RIkCgKiz9dWX9E8m .node circle,#mermaid-svg-RIkCgKiz9dWX9E8m .node ellipse,#mermaid-svg-RIkCgKiz9dWX9E8m .node polygon,#mermaid-svg-RIkCgKiz9dWX9E8m .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-RIkCgKiz9dWX9E8m .rough-node .label text,#mermaid-svg-RIkCgKiz9dWX9E8m .node .label text,#mermaid-svg-RIkCgKiz9dWX9E8m .image-shape .label,#mermaid-svg-RIkCgKiz9dWX9E8m .icon-shape .label{text-anchor:middle;}#mermaid-svg-RIkCgKiz9dWX9E8m .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-RIkCgKiz9dWX9E8m .rough-node .label,#mermaid-svg-RIkCgKiz9dWX9E8m .node .label,#mermaid-svg-RIkCgKiz9dWX9E8m .image-shape .label,#mermaid-svg-RIkCgKiz9dWX9E8m .icon-shape .label{text-align:center;}#mermaid-svg-RIkCgKiz9dWX9E8m .node.clickable{cursor:pointer;}#mermaid-svg-RIkCgKiz9dWX9E8m .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-RIkCgKiz9dWX9E8m .arrowheadPath{fill:#333333;}#mermaid-svg-RIkCgKiz9dWX9E8m .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-RIkCgKiz9dWX9E8m .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-RIkCgKiz9dWX9E8m .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RIkCgKiz9dWX9E8m .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-RIkCgKiz9dWX9E8m .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RIkCgKiz9dWX9E8m .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-RIkCgKiz9dWX9E8m .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-RIkCgKiz9dWX9E8m .cluster text{fill:#333;}#mermaid-svg-RIkCgKiz9dWX9E8m .cluster span{color:#333;}#mermaid-svg-RIkCgKiz9dWX9E8m div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-RIkCgKiz9dWX9E8m .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-RIkCgKiz9dWX9E8m rect.text{fill:none;stroke-width:0;}#mermaid-svg-RIkCgKiz9dWX9E8m .icon-shape,#mermaid-svg-RIkCgKiz9dWX9E8m .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RIkCgKiz9dWX9E8m .icon-shape p,#mermaid-svg-RIkCgKiz9dWX9E8m .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-RIkCgKiz9dWX9E8m .icon-shape .label rect,#mermaid-svg-RIkCgKiz9dWX9E8m .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RIkCgKiz9dWX9E8m .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-RIkCgKiz9dWX9E8m .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-RIkCgKiz9dWX9E8m :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 静态文件
404
创建 socket 并绑定端口
监听连接
accept 接受客户端连接
读取 HTTP 请求
解析请求行与路径
打开文件并发送响应
返回 404 页面
关闭连接

5. 实现 HTTP 服务器

5.1 主程序与 socket 初始化

首先实现服务器的入口与 socket 初始化逻辑:

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

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

int create_server_socket(int port) {
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

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

    if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        perror("bind");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    if (listen(server_fd, 10) < 0) {
        perror("listen");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    return server_fd;
}

5.2 解析 HTTP 请求

读取客户端请求后,需要解析出请求方法和请求路径:

c 复制代码
typedef struct {
    char method[16];
    char path[256];
} http_request;

int parse_request(const char *buffer, http_request *req) {
    // 请求行格式: METHOD PATH HTTP/1.1
    if (sscanf(buffer, "%15s %255s", req->method, req->path) != 2) {
        return -1;
    }
    return 0;
}

5.3 根据扩展名返回 MIME 类型

c 复制代码
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; charset=utf-8";
    if (strcmp(ext, ".css") == 0) return "text/css";
    if (strcmp(ext, ".js") == 0) return "application/javascript";
    if (strcmp(ext, ".jpg") == 0 || strcmp(ext, ".jpeg") == 0) return "image/jpeg";
    if (strcmp(ext, ".png") == 0) return "image/png";
    if (strcmp(ext, ".mp4") == 0) return "video/mp4";
    if (strcmp(ext, ".webm") == 0) return "video/webm";
    return "application/octet-stream";
}

5.4 发送文件响应

这是服务器最核心的部分。对于视频文件,需要以二进制方式读取并发送:

c 复制代码
void send_file_response(int client_fd, const char *filepath) {
    FILE *file = fopen(filepath, "rb");
    if (!file) {
        send_404(client_fd);
        return;
    }

    // 获取文件大小
    struct stat st;
    stat(filepath, &st);
    long file_size = st.st_size;

    // 发送响应头
    char header[512];
    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",
        get_mime_type(filepath), file_size);
    send(client_fd, header, strlen(header), 0);

    // 以二进制方式发送文件内容
    char buffer[BUFFER_SIZE];
    size_t bytes_read;
    while ((bytes_read = fread(buffer, 1, sizeof(buffer), file)) > 0) {
        send(client_fd, buffer, bytes_read, 0);
    }

    fclose(file);
}

5.5 处理客户端连接

c 复制代码
void handle_client(int client_fd) {
    char buffer[BUFFER_SIZE];
    int bytes_read = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
    if (bytes_read <= 0) {
        close(client_fd);
        return;
    }
    buffer[bytes_read] = '\0';

    http_request req;
    if (parse_request(buffer, &req) < 0) {
        send_404(client_fd);
        close(client_fd);
        return;
    }

    // 构造完整文件路径,防止目录穿越
    char filepath[512];
    snprintf(filepath, sizeof(filepath), "%s%s", WEB_ROOT, req.path);

    // 默认首页
    if (strcmp(req.path, "/") == 0) {
        snprintf(filepath, sizeof(filepath), "%s/index.html", WEB_ROOT);
    }

    send_file_response(client_fd, filepath);
    close(client_fd);
}

5.6 主循环

c 复制代码
int main() {
    int server_fd = create_server_socket(PORT);
    printf("HTTP server listening on port %d\n", PORT);

    while (1) {
        struct sockaddr_in client_addr;
        socklen_t client_len = sizeof(client_addr);
        int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
        if (client_fd < 0) {
            perror("accept");
            continue;
        }
        handle_client(client_fd);
    }

    close(server_fd);
    return 0;
}

6. 升级为 HTTPS 服务器

要让服务器支持 HTTPS,需要引入 OpenSSL 库,在 TCP 连接之上建立 TLS 加密层。

6.1 生成自签名证书

bash 复制代码
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes

6.2 初始化 SSL 上下文

c 复制代码
#include <openssl/ssl.h>
#include <openssl/err.h>

SSL_CTX *init_ssl_context() {
    SSL_CTX *ctx = SSL_CTX_new(TLS_server_method());
    if (!ctx) {
        ERR_print_errors_fp(stderr);
        exit(EXIT_FAILURE);
    }

    // 加载证书和私钥
    if (SSL_CTX_use_certificate_file(ctx, "cert.pem", SSL_FILETYPE_PEM) <= 0) {
        ERR_print_errors_fp(stderr);
        exit(EXIT_FAILURE);
    }
    if (SSL_CTX_use_PrivateKey_file(ctx, "key.pem", SSL_FILETYPE_PEM) <= 0) {
        ERR_print_errors_fp(stderr);
        exit(EXIT_FAILURE);
    }

    return ctx;
}

6.3 使用 SSL 读写数据

在 HTTPS 模式下,recv/send 需要替换为 SSL_read/SSL_write:

c 复制代码
void handle_client_ssl(SSL *ssl) {
    char buffer[BUFFER_SIZE];
    int bytes_read = SSL_read(ssl, buffer, sizeof(buffer) - 1);
    if (bytes_read <= 0) {
        SSL_free(ssl);
        return;
    }
    buffer[bytes_read] = '\0';

    http_request req;
    if (parse_request(buffer, &req) < 0) {
        SSL_free(ssl);
        return;
    }

    char filepath[512];
    snprintf(filepath, sizeof(filepath), "%s%s", WEB_ROOT, req.path);
    if (strcmp(req.path, "/") == 0) {
        snprintf(filepath, sizeof(filepath), "%s/index.html", WEB_ROOT);
    }

    send_file_response_ssl(ssl, filepath);
    SSL_free(ssl);
}

对应的 SSL 版本文件发送函数:

c 复制代码
void send_file_response_ssl(SSL *ssl, const char *filepath) {
    FILE *file = fopen(filepath, "rb");
    if (!file) {
        SSL_write(ssl, "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n", 44);
        return;
    }

    struct stat st;
    stat(filepath, &st);

    char header[512];
    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",
        get_mime_type(filepath), st.st_size);
    SSL_write(ssl, header, strlen(header));

    char buffer[BUFFER_SIZE];
    size_t bytes_read;
    while ((bytes_read = fread(buffer, 1, sizeof(buffer), file)) > 0) {
        SSL_write(ssl, buffer, bytes_read);
    }

    fclose(file);
}

6.4 HTTPS 主循环

c 复制代码
int main() {
    SSL_CTX *ctx = init_ssl_context();
    int server_fd = create_server_socket(8443);
    printf("HTTPS server listening on port 8443\n");

    while (1) {
        struct sockaddr_in client_addr;
        socklen_t client_len = sizeof(client_addr);
        int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
        if (client_fd < 0) continue;

        SSL *ssl = SSL_new(ctx);
        SSL_set_fd(ssl, client_fd);

        if (SSL_accept(ssl) == 1) {
            handle_client_ssl(ssl);
        } else {
            ERR_print_errors_fp(stderr);
            SSL_free(ssl);
        }
        close(client_fd);
    }

    SSL_CTX_free(ctx);
    close(server_fd);
    return 0;
}

7. 测试与验证

7.1 测试 HTTP

启动服务器后,在 www 目录下放置 index.html 和一个测试视频 demo.mp4:

bash 复制代码
mkdir -p www
echo "<h1>Hello C Server</h1>" > www/index.html

然后启动服务器并测试:

bash 复制代码
./server
curl -I http://localhost:8080/
curl -I http://localhost:8080/demo.mp4

浏览器访问 http://localhost:8080/ 即可看到页面,访问 http://localhost:8080/demo.mp4 可直接播放视频。

7.2 测试 HTTPS

bash 复制代码
./server_ssl
curl -k -I https://localhost:8443/

-k 参数用于忽略自签名证书的警告。浏览器访问时需手动信任证书。

8. 常见问题与优化方向

8.1 常见问题

  • 视频无法播放 :确认 Content-Type 为 video/mp4,且 Content-Length 正确。
  • 中文乱码 :HTML 响应头需包含 charset=utf-8。
  • 目录穿越漏洞 :务必校验请求路径,禁止包含 ..。
  • 大文件传输慢 :可增大发送缓冲区,或改用 sendfile 系统调用。

8.2 优化方向

  • 使用 sendfile 实现零拷贝文件传输,大幅提升性能。
  • 引入多线程或 epoll 模型,支持并发连接。
  • 增加 HTTP Range 支持,实现视频拖动进度条播放。
  • 增加日志系统,记录访问信息。

9. 总结

本文从零实现了一个支持 HTML 页面与视频文件传输的 HTTP 服务器,并在此基础上通过 OpenSSL 扩展了 HTTPS 支持。核心代码约 200 行,逻辑清晰、易于扩展。建议读者在此基础上继续完善 Range 支持、并发模型与安全校验,打造一个真正可用的生产级服务器。

相关推荐
Helix2501 小时前
2026编程入门指南:零基础如何科学选择第一门编程语言?
java·c语言·javascript·零基础·编程语言·python入门·编程就业
Escalating_xu1 小时前
【C 语言】字符函数和字符串函数:从 ctype 到 strtok、strerror 全面解析
java·c语言·开发语言
I Am a robert girl2 小时前
PhysStream 源码级拆解:流式视频生成如何做到“边生成边控制“
python·音视频·源码解析·视频生成·可控生成·流式生成·物理控制
Latchh2 小时前
视频封面为什么会黑屏或截到旧帧
图像处理·人工智能·计算机视觉·音视频
开开心心就好2 小时前
视频压缩工具推荐,画质效果很能打
智能手机·ffmpeg·ocr·word·排序算法·音视频·散列表
时间的拾荒人2 小时前
Qt 网络编程与音视频开发实战:从 UDP/TCP 到 HTTP 与多媒体
网络·qt·音视频
Eric.462 小时前
2025 深度实战:本地部署 AI 漫剧与 AI 视频全量产方案,彻底解决云端画质抖动、角色漂移、成本超标
大数据·人工智能·音视频·comfyui·ai漫剧
yyy(十一月限定版)11 小时前
224redfg
c语言
小师兄吃牛肉13 小时前
C语言中的nullptr有必要吗
c语言·c++·算法