跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C

用C写Web:CGI、FastCGI与Nginx模块的实战笔记

用C语言开发Web应用,CGI、FastCGI和Nginx模块是三种基础方案。CGI简单但每请求新建进程,高并发下性能差;FastCGI常驻进程大幅提升吞吐,适合多数动态场景;Nginx模块直接嵌入服务器内核,性能极致但开发门槛高。文章给出了三种方式的代码示例、关键配置和常见坑点,并通过一个登录案例演示FastCGI与Nginx的配合,最后总结了适用场景和工程建议。

念念不忘发布于 2026/6/23更新于 2026/10/1667 浏览
用C写Web:CGI、FastCGI与Nginx模块的实战笔记

用C写Web:CGI、FastCGI与Nginx模块的实战笔记

Web架构示意图

为什么还要用C搞Web开发?

C语言在Web领域不算主流,但当你需要榨干服务器性能、写出极低延迟的服务时,它依然是个硬核选择。CGI、FastCGI和Nginx模块代表了从简单到极致的三个层次,理解它们不仅让你明白HTTP协议背后的机制,也能在特定场景下给出更靠谱的方案。

CGI:古老而直接的接口

CGI的工作模型很原始:每个请求到来,Web服务器拉起一个新进程执行你的CGI程序,程序跑完输出响应,进程就销毁了。这种隔离性天然防止了内存泄漏累积,但代价是每次请求都要走一遍进程创建与销毁,并发一上来系统负载就爆了。所以它只适合低频、隔离要求高的内部工具。

程序访问请求参数全靠环境变量。GET的参数在QUERY_STRING里,POST的数据从标准输入读取,长度由CONTENT_LENGTH指定。输出必须严格遵循HTTP格式:头部和主体之间用两个换行分隔。

一个最简的Hello World:

#include <stdio.h>
#include <stdlib.h>

int main() {
    printf("Content-Type: text/plain\n\n");
    printf("Hello from CGI!");
    return 0;
}

处理URL解码和参数解析的完整例子:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void decode_url(char *src, char *dst) {
    int i = 0, j = 0;
    while (src[i]) {
        if (src[i] == ) {
             value;
            (src + i + , , &value);
            dst[j++] = ()value;
            i += ;
        }   (src[i] == ) {
            dst[j++] = ;
            i++;
        }  {
            dst[j++] = src[i++];
        }
    }
    dst[j] = ;
}

  {
     *query_string = getenv();
     *content_type = getenv();
     *request_method = getenv();

    ();
    (, query_string ? query_string : );
    (, content_type ? content_type : );
    (, request_method ? request_method : );

     ((request_method, ) ==  && query_string) {
         *token = strtok(query_string, );
         (token) {
             *equals = (token, );
             (equals) {
                *equals = ;
                 *key = token;
                 *value = equals + ;
                 decoded_key[], decoded_value[];
                decode_url(key, decoded_key);
                decode_url(value, decoded_value);
                (, decoded_key, decoded_value);
            }
            token = strtok(, );
        }
    }
     ;
}
'%'
int
sscanf
1
"%2x"
char
3
else
if
'+'
' '
else
'\0'
int
main
()
char
"QUERY_STRING"
char
"CONTENT_TYPE"
char
"REQUEST_METHOD"
printf
"Content-Type: text/plain\n\n"
printf
"Query String: %s\n"
""
printf
"Content Type: %s\n"
""
printf
"Request Method: %s\n"
""
if
strcmp
"GET"
0
char
"&"
while
char
strchr
'='
if
'\0'
char
char
1
char
100
100
printf
"Parameter: %s = %s\n"
NULL
"&"
return
0

写CGI程序容易踩的坑:环境变量可能为NULL,一定要判空;输出格式必须严格,少了换行浏览器可能不认;虽然进程结束内存自动回收,但养成手动释放动态分配内存的习惯没坏处。

FastCGI:长驻进程解决性能痛点

为了修复CGI每次请求都起进程的毛病,FastCGI让一个或多个进程持续运行,Web服务器通过TCP或Unix Socket把请求转发过去。进程复用大幅削减了启动开销,成为动态Web服务的基础。许多语言(PHP、Python)的Web部署都用FastCGI或类似协议。

开发FastCGI程序一般用libfcgi库。核心是FCGI_Accept()循环:这个函数阻塞等待请求,返回非负值时就处理一次请求,处理完继续循环。进程不会退出,除非被信号终止。

Hello World变成了这样:

#include <fcgi_stdio.h>
#include <stdlib.h>

int main() {
    while (FCGI_Accept() >= 0) {
        printf("Content-Type: text/plain\n\n");
        printf("Hello from FastCGI!");
    }
    return 0;
}

获取参数的方式和CGI几乎一样,只是需要把逻辑包在循环里,注意每次循环结束时清理上一次的状态,避免数据串扰。

#include <fcgi_stdio.h>
#include <stdlib.h>
#include <string.h>

void decode_url(char *src, char *dst) {
    int i = 0, j = 0;
    while (src[i]) {
        if (src[i] == '%') {
            int value;
            sscanf(src + i + 1, "%2x", &value);
            dst[j++] = (char)value;
            i += 3;
        } else if (src[i] == '+') {
            dst[j++] = ' ';
            i++;
        } else {
            dst[j++] = src[i++];
        }
    }
    dst[j] = '\0';
}

int main() {
    while (FCGI_Accept() >= 0) {
        char *query_string = getenv("QUERY_STRING");
        char *content_type = getenv("CONTENT_TYPE");
        char *request_method = getenv("REQUEST_METHOD");

        printf("Content-Type: text/plain\n\n");
        printf("Query String: %s\n", query_string ? query_string : "");
        printf("Content Type: %s\n", content_type ? content_type : "");
        printf("Request Method: %s\n", request_method ? request_method : "");

        if (strcmp(request_method, "GET") == 0 && query_string) {
            char *token = strtok(query_string, "&");
            while (token) {
                char *equals = strchr(token, '=');
                if (equals) {
                    *equals = '\0';
                    char *key = token;
                    char *value = equals + 1;
                    char decoded_key[100], decoded_value[100];
                    decode_url(key, decoded_key);
                    decode_url(value, decoded_value);
                    printf("Parameter: %s = %s\n", decoded_key, decoded_value);
                }
                token = strtok(NULL, "&");
            }
        }
    }
    return 0;
}

这里要注意:全局变量不会在请求间自动归零,上次处理完一定要重置;FastCGI后端如果挂了,前端Nginx会报502,所以进程守护和健康检查很关键;通讯用的socket权限和端口冲突也得留神。

Nginx模块:把逻辑嵌进服务器核心里

Nginx的高性能来自事件驱动和多进程架构,用C写模块可以直接注册回调,让请求处理完全在Nginx内部完成,省掉了进程间通信,延迟极低。当然,难度也最大——你得和Nginx的内存池、事件模型深度绑定,稍有不慎就整个worker崩溃。

写一个模块需要引入ngx_config.h等头文件,定义命令、上下文和处理函数。下面是一个返回静态文本的简单模块:

#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>

static ngx_int_t ngx_http_hello_handler(ngx_http_request_t *r);
static ngx_command_t ngx_http_hello_commands[] = {
    {ngx_string("hello_world"), NGX_HTTP_LOC_CONF | NGX_CONF_NOARGS,
     ngx_conf_set_flag_slot, NGX_HTTP_LOC_CONF_OFFSET, 0, NULL},
    ngx_null_command
};

static ngx_http_module_t ngx_http_hello_module_ctx = {
    NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL
};

ngx_module_t ngx_http_hello_module = {
    NGX_MODULE_V1,
    &ngx_http_hello_module_ctx,
    ngx_http_hello_commands,
    NGX_HTTP_MODULE,
    NULL, NULL, NULL, NULL, NULL, NULL, NULL,
    NGX_MODULE_V1_PADDING
};

static ngx_int_t ngx_http_hello_handler(ngx_http_request_t *r) {
    ngx_int_t rc;
    ngx_buf_t *b;
    ngx_chain_t out;

    r->headers_out.content_type.len = sizeof("text/plain") - 1;
    r->headers_out.content_type.data = (u_char *)"text/plain";
    r->headers_out.status = NGX_HTTP_OK;
    r->headers_out.content_length_n = 13;

    rc = ngx_http_send_header(r);
    if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) {
        return rc;
    }

    b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
    if (b == NULL) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }
    out.buf = b;
    out.next = NULL;
    b->pos = (u_char *)"Hello from Nginx!";
    b->last = b->pos + 13;
    b->memory = 1;
    b->last_buf = 1;

    return ngx_http_output_filter(r, &out);
}

static ngx_int_t ngx_http_hello_init(ngx_conf_t *cf) {
    ngx_http_handler_pt *h;
    ngx_http_core_loc_conf_t *clcf;
    clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
    h = ngx_array_push(&clcf->handlers);
    if (h == NULL) {
        return NGX_ERROR;
    }
    *h = ngx_http_hello_handler;
    return NGX_OK;
}

static ngx_http_module_t ngx_http_hello_module_ctx = {
    NULL, ngx_http_hello_init, NULL, NULL, NULL, NULL, NULL, NULL
};

编写Nginx模块时几个至关重要的点:编译得链接Nginx源码,而且版本必须一致;内存管理必须用ngx_palloc等接口,直接用malloc会破坏内存池的一致性;Nginx是单线程事件循环,处理请求时如果阻塞整个worker都会卡住,需要非阻塞IO或者把耗时任务丢到线程池。

实战:用FastCGI搭一个登录服务

一个常见的场景:浏览器通过Nginx访问FastCGI后端,完成用户认证。这里用FastCGI实现登录程序,Nginx负责转发。

FastCGI程序会根据请求方法返回不同内容:GET时显示登录表单,POST时解析数据并验证。

#include <fcgi_stdio.h>
#include <stdlib.h>
#include <string.h>

void decode_url(char *src, char *dst) {
    int i = 0, j = 0;
    while (src[i]) {
        if (src[i] == '%') {
            int value;
            sscanf(src + i + 1, "%2x", &value);
            dst[j++] = (char)value;
            i += 3;
        } else if (src[i] == '+') {
            dst[j++] = ' ';
            i++;
        } else {
            dst[j++] = src[i++];
        }
    }
    dst[j] = '\0';
}

int main() {
    while (FCGI_Accept() >= 0) {
        char *content_type = getenv("CONTENT_TYPE");
        char *request_method = getenv("REQUEST_METHOD");

        if (strcmp(request_method, "POST") == 0) {
            char *content_length_str = getenv("CONTENT_LENGTH");
            int content_length = atoi(content_length_str);

            char *post_data = (char *)malloc(content_length + 1);
            if (post_data) {
                fread(post_data, 1, content_length, stdin);
                post_data[content_length] = '\0';

                char *username = NULL;
                char *password = NULL;
                char *token = strtok(post_data, "&");
                while (token) {
                    char *equals = strchr(token, '=');
                    if (equals) {
                        *equals = '\0';
                        char *key = token;
                        char *value = equals + 1;
                        char decoded_key[100], decoded_value[100];
                        decode_url(key, decoded_key);
                        decode_url(value, decoded_value);
                        if (strcmp(decoded_key, "username") == 0) {
                            username = strdup(decoded_value);
                        } else if (strcmp(decoded_key, "password") == 0) {
                            password = strdup(decoded_value);
                        }
                    }
                    token = strtok(NULL, "&");
                }

                printf("Content-Type: text/plain\n\n");
                if (username && password && strcmp(username, "admin") == 0 && strcmp(password, "123456") == 0) {
                    printf("登录成功!");
                } else {
                    printf("用户名或密码错误!");
                }
                free(username);
                free(password);
                free(post_data);
            }
        } else {
            printf("Content-Type: text/html\n\n");
            printf("<html>");
            printf("<head><title>登录页面</title></head>");
            printf("<body>");
            printf("<h1>用户登录</h1>");
            printf("<form method='post' action='/login'>");
            printf("用户名:<input type='text' name='username'><br>");
            printf("密码:<input type='password' name='password'><br>");
            printf("<input type='submit' value='登录'>");
            printf("</form>");
            printf("</body>");
            printf("</html>");
        }
    }
    return 0;
}

对应的Nginx配置很简单,把/login路径转发到FastCGI后端:

server {
    listen 80;
    server_name localhost;

    location / {
        root html;
        index index.html;
    }

    location /login {
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.cgi;
        include fastcgi_params;
    }
}

几点取舍

CGI足够简单,隔离安全但性能垫底;FastCGI在灵活性和效率之间找到平衡,是大多数动态内容的合理选择;Nginx模块能做到延迟、吞吐双巅峰,但开发维护成本太高,除非性能到了非优化不可的地步,否则不值得。实际项目中,先用FastCGI跑起来,确认瓶颈再考虑下沉到模块,是比较务实的路线。更深入的,可以继续玩玩Cookie解析、文件上传,把HTTP协议摸透。

目录

  1. 用C写Web:CGI、FastCGI与Nginx模块的实战笔记
  2. 为什么还要用C搞Web开发?
  3. CGI:古老而直接的接口
  4. FastCGI:长驻进程解决性能痛点
  5. Nginx模块:把逻辑嵌进服务器核心里
  6. 实战:用FastCGI搭一个登录服务
  7. 几点取舍

更多推荐文章

查看全部
  • Next AI Draw.io 简介、安装与使用指南
  • AI 时代,鸿蒙 App 还需要传统导航结构吗?
  • Python 环境安装与配置 Gurobi 求解器
  • Linux 环境下 libwebkit2gtk-4.1-0 库安装与原理详解
  • 用豆包打造专属 Python 智能管家解决编程难题
  • Python 基础语法与核心知识点速查手册
  • 《Agent Runtime 工程化》第五章 上下文工程:5.7 动手任务
  • 《Agent Runtime 工程化》第五章 上下文工程:5.4 历史滚动摘要
  • Meta Llama 3 中文微调模型评测:llama3-Chinese-chat 与 Llama3-8B-Chinese-Chat
  • 基于 Isaac Lab 训练机器人行走实战指南
  • Python Web 框架 Django 核心功能与实战
  • Dart File 类用法详解
  • Windows 11 下 WSL2 安装 Ubuntu 22.04 完整指南
  • SKResNet 架构详解:融合选择性卷积与残差结构
  • C++ 手搓 AVL 平衡二叉搜索树
  • 大模型微调实战:基于 LLaMA Factory 的部署与训练指南
  • 三种方法让老板秒懂大模型
  • Stable Diffusion 原理详解(附代码实现)
  • Pico 4XVR 1.10.13 安装与使用指南
  • 队列:数据结构与系统设计中的关键组件

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online