当前位置: 首页 > news >正文

一个使用nginx转发的cgi程序示例

本文使用cgi库编写cgi服务,再使用nginx进行转发。两者结合,实现简单的get请求和post请求的web服务。

问题提出

近年笔者实现的web服务,基本是用golang、nodejs语言。C/C++的没有真正做过,为补充技术栈,一直想做。想了几年,现在因某些缘故,不能只想了,于是抽出时间研究一下。

设计思路

1、用cgi库实现简单的后端业务服务,提供get和post请求。端口端口为9000。

2、用spawn-fcgi启动服务。

3、使用nginx做转发,nginx为容器方式运行,对外端口为87,内部端口为9000。

4、上述服务运行在虚拟机中,在本地物理机使用curl测试。

实践

依赖库下载编译

本业务程序需使用spawn-fcgi和fcgi-2.4.1这2个库(工具)。

spawn-fcgi下载地址:http://download.lighttpd.net/spawn-fcgi/releases-1.6.x/spawn-fcgi-1.6.4.tar.gz

fcgi-2.4.1 下载地址:https://github.com/FastCGI-Archives/fcgi2/releases,具体为:https://github.com/FastCGI-Archives/FastCGI.com/blob/master/original_snapshot/fcgi-2.4.1-SNAP-0910052249.tar.gz

编译简要步骤:

tar xf spawn-fcgi-1.6.4.tar.gz
cd spawn-fcgi-1.6.4
./configure --prefix=/home/latelee/tools/cgi
make
make install

tar xf fcgi-2.4.1-SNAP-0910052249.tar.gz
cd fcgi-2.4.1-SNAP-0910052249
./configure --prefix=/home/latelee/tools/cgi
make
make install

注1:在fcgi-2.4.1-SNAP-0910052249/libfcgi/fcgio.cpp文件中添加#include <stdio.h>,否则编译失败,提示fcgio.cpp:70:72: error: 'EOF' was not declared in this scope

注2:如果系统上没有g++编译,则不会生成libfcgi++.so.0。库位于fcgi-2.4.1-SNAP-0910052249/libfcgi/.libs/

安装后得到文件:

$pwd
/home/latelee/tools/cgi

$ tree -L 2
.
├── bin
│   ├── cgi-fcgi
│   └── spawn-fcgi
├── include
│   ├── fastcgi.h
│   ├── fcgiapp.h
│   ├── fcgi_config.h
│   ├── fcgimisc.h
│   ├── fcgio.h
│   ├── fcgios.h
│   └── fcgi_stdio.h
├── lib
│   ├── libfcgi.a
│   ├── libfcgi++.a
│   ├── libfcgi.la
│   ├── libfcgi++.la
│   ├── libfcgi.so -> libfcgi.so.0.0.0
│   ├── libfcgi++.so -> libfcgi++.so.0.0.0
│   ├── libfcgi.so.0 -> libfcgi.so.0.0.0
│   ├── libfcgi++.so.0 -> libfcgi++.so.0.0.0
│   ├── libfcgi.so.0.0.0
│   └── libfcgi++.so.0.0.0
└── share
    └── man	

在Makefile中直接使用上述库路径,由于要使用spawn-fcgi,因此将其拷贝到系统PATH目录中。

业务程序代码

核心C++代码如下,由于是测试用,因此代码不做过多的设计。

#include <fcgi_stdio.h>
#include <fcgiapp.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <ctype.h>
#include <time.h>

#include "log.h"

#define THREAD_COUNT 4
#define VERSION "1.0.0"

// 获取编译时间字符串
const char* get_build_time() {
    static char build_time[64] = {0};
    if (build_time[0] == '\0') {
        // 使用 __DATE__ 和 __TIME__ 宏获取编译时间
        snprintf(build_time, sizeof(build_time), "%s %s", __DATE__, __TIME__);
    }
    return build_time;
}

char* get_request_path(FCGX_Request* request) {
    char* request_uri = FCGX_GetParam("REQUEST_URI", request->envp);
    if (!request_uri) return strdup("");
    
    // 去除查询字符串
    char* question_mark = strchr(request_uri, '?');
    if (question_mark) {
        return strndup(request_uri, question_mark - request_uri);
    } else {
        return strdup(request_uri);
    }
}

void send_response(FCGX_Request* request, const char* response) {
    FCGX_FPrintF(request->out, "%s", response);
}

void handle_get(FCGX_Request* request) {
    char* path = get_request_path(request);
    
    log_printf("GET request for path: %s\n", path);

    if (strcmp(path, "/info") == 0) {
        // 返回版本信息
        char response[512];
        snprintf(response, sizeof(response),
                 "Status: 200 OK\r\n"
                 "Content-Type: application/json\r\n\r\n"
                 "{\"service\": \"FastCGI Demo\", "
                 "\"version\": \"%s\", "
                 "\"build_time\": \"%s\", "
                 "\"current_time\": \"%ld\"}",
                 VERSION, get_build_time(), time(NULL));
        
        send_response(request, response);
    } else {
        send_response(request, "Status: 404 Not Found\r\n"
                              "Content-Type: text/plain\r\n\r\n"
                              "Endpoint not found. Try /info");
    }
    
    free(path);
}


char* php_url_decode(const char* str) {
    if (!str) return NULL;
    
    char* result = (char*)malloc(strlen(str) + 1);
    char* dst = result;
    const char* src = str;
    
    while (*src) {
        if (*src == '+') {
            *dst++ = ' ';
            src++;
        } else if (*src == '%' && isxdigit(src[1]) && isxdigit(src[2])) {
            char hex[3] = {src[1], src[2], '\0'};
            *dst++ = (char)strtol(hex, NULL, 16);
            src += 3;
        } else {
            *dst++ = *src++;
        }
    }
    
    *dst = '\0';
    return result;
}

char* parse_form_data(const char* data, const char* key) {
    if (!data || !key) return NULL;
    
    char* copy = strdup(data);
    char* token = strtok(copy, "&");
    char* result = NULL;
    
    while (token) {
        char* eq = strchr(token, '=');
        if (eq) {
            *eq = '\0';
            if (strcmp(token, key) == 0) {
                result = strdup(eq + 1);
                break;
            }
        }
        token = strtok(NULL, "&");
    }
    
    free(copy);
    return result;
}

void handle_post(FCGX_Request* request) {
    char* path = get_request_path(request);
    log_printf("POST request received,path: %s\n", path);
    free(path);

    char* content_length_str = FCGX_GetParam("CONTENT_LENGTH", request->envp);
    int content_length = content_length_str ? atoi(content_length_str) : 0;
    
    if (content_length <= 0) {
        send_response(request, "Status: 411 Length Required\r\n"
                              "Content-Type: text/plain\r\n\r\n"
                              "Content-Length required");
        return;
    }
    
    char* post_data = (char*)malloc(content_length + 1);
    FCGX_GetStr(post_data, content_length, request->in);
    post_data[content_length] = '\0';
    
    char* decoded_data = php_url_decode(post_data);
    const char* key = "name";
    char* value = parse_form_data(decoded_data, key);
    
    char response[1024];
    snprintf(response, sizeof(response), 
             "Status: 200 OK\r\n"
             "Content-Type: application/json\r\n\r\n"
             "{\"received\": \"%s\", \"%s\": \"%s\"}",
             post_data, key, value ? value : "not found");
    
    send_response(request, response);
    
    free(post_data);
    free(decoded_data);
    if (value) free(value);
}

void process_request(FCGX_Request* request) {
    char* request_method = FCGX_GetParam("REQUEST_METHOD", request->envp);
    
    log_printf("Received %s request\n", request_method ? request_method : "unknown");

    if (strcmp(request_method, "POST") == 0) {
        handle_post(request);
    } else if (strcmp(request_method, "GET") == 0) {
        handle_get(request);
    } else {
        send_response(request, "Status: 405 Method Not Allowed\r\n"
                              "Content-Type: text/plain\r\n\r\n"
                              "Only GET and POST methods are supported");
    }
}

void* handle_requests(void* arg) {
    FCGX_Request request;
    FCGX_InitRequest(&request, 0, 0);
    
    while (FCGX_Accept_r(&request) == 0) {
        process_request(&request);
        FCGX_Finish_r(&request);
    }
    
    return NULL;
}

int main()
{
    // 初始化日志系统
    if (log_init() != 0) {
        fprintf(stderr, "Failed to initialize logging system\n");
        return EXIT_FAILURE;
    }

    log_printf("Server started. Version: %s, Build time: %s\n", VERSION, get_build_time());

    FCGX_Init();
    
    pthread_t threads[THREAD_COUNT];
    for (int i = 0; i < THREAD_COUNT; i++) {
        pthread_create(&threads[i], NULL, handle_requests, NULL);
    }
    
    for (int i = 0; i < THREAD_COUNT; i++) {
        pthread_join(threads[i], NULL);
    }

    log_printf("Server shutting down\n");
    // 清理日志系统
    log_cleanup();

    return 0;
}

其中,log_printf等是日志模块函数,不是重点,因此不列出。要说明的是,本次代码绝大部分是使用AI经多次迭代后得到的,其风格与笔者的稍有不同,但也不修改了(以证明使用AI)。不过,像char* post_data = (char*)malloc(content_length + 1);这种代码就无法生成正确的版本,要借助编译器修正。

鉴于笔者不太喜欢动态库,因此用静态库编译,Makefile核心要点:

LIBS    += /home/latelee/tools/cgi/lib/libfcgi.a
LDFLAGS += $(LIBS) -lpthread -lrt 

INC = ./ ./inc /home/latelee/tools/cgi/include

运行:

spawn-fcgi -p 9000 -n ./a.out

nginx转发

本次使用nginx容器进行转发,docker-compose文件如下:

version: '3.8'

services:
  nginx-forward:
    image: registry.cn-shenzhen.aliyuncs.com/hxr/nginx:1.23.tb
    container_name: nginx-forward
    hostname: nginx-forward
    restart: always
    #command: "sleep 10000000"
    volumes:
      - ./log/nginx:/var/log/nginx
      - ./config/nginx.conf:/etc/nginx/nginx.conf
      - ./config/conf.d:/etc/nginx/conf.d
    environment:
      - TZ=Asia/Shanghai
      - AA=aaaadddaad
    ports:
      - 87:9000
    networks:
      - custom-net
networks:
  custom-net:
    driver: bridge

其中nginx.conf会加载nginx.conf目录下的文件,其下有http_cgi.conf配置文件,内容如下:

server {
    listen       9000;
    listen  [::]:9000;
    server_name  localhost;

    location / {
        fastcgi_pass 192.168.28.11:9000;
        include fastcgi_params;
        
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_script_name;
        
        # 允许POST请求
        fastcgi_param REQUEST_METHOD $request_method;
        fastcgi_param CONTENT_TYPE $content_type;
        fastcgi_param CONTENT_LENGTH $content_length;
    }

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

其中关键的地方是指定后端业务程序IP和端口,可以是本机,也可以是同网络其它主机,但一定要正确。

fastcgi_pass 192.168.28.11:9000;

测试

本次使用curl进行测试。

get请求示例:

$ curl http://192.168.28.11:87/info
{"service": "FastCGI Demo", "version": "1.0.0", "build_time": "Apr  4 2025 22:17:22", "current_time": "1743776337"}

post请求示例:

$ curl -X POST -d "name=latelee&age=38" http://192.168.28.11:87/api
{"received": "name=latelee&age=38", "name": "latelee"}

可以看到,已经能正确处理请求了。

扩展知识

后续考虑使用Tengine替换nginx来实验,再后续可能考虑使用商密的SSL证书。

小结

总体上,用C/C++实现感觉有些麻烦,涉及东西也多。不如用golang来得直接。也正是如此,笔者负责的和web有关的工程,都不用C/C++。即使需要和底层动态库交互,也有相应的方法实现(可参考笔者写的golang有关文章),维护起来相对简单。如果出于性能方面的考虑,的确应用稍底层的语言,不过,就笔者在生产上使用的情况看,还没有出现因语言本身导致的性能问题,如果一定要举例,那就是因为日志模块没有用异步写入,导致处理请求整体耗时较高,虽然在秒级以内,但也不能接受,后来修正,再有本地缓存加持,单次处理可在20毫秒内完成。

nginx无法转发问题

起初没留意http_cgi.conf的配置,nginx的error.log有如下错误:

*8 upstream sent unsupported FastCGI protocol version: 72 while reading response header from upstream, client: 192.168.28.11, server: localhost, request: "POST /api HTTP/1.1", upstream: "fastcgi://127.0.0.1:9000", host: "192.168.28.11:87"

信息里提到不支持FastCGI协议版本,或说nginx和自编的cgi程序协议版本不对。后来才发现是IP写错了。因为我的nginx使用容器启动,而配置文件的IP写成了127.0.0.1:9000,而恰好容器里监听的端口也是9000,就是说nginx的端口转给自己了。将IP改为主机IP即可解决问题。但当时调试时思路不够清晰,搞了好久才发现问题。

相关文章:

  • BEVFormer v2(CVPR2023)
  • Airflow量化入门系列:第一章 Airflow 基础与量化交易场景
  • K8S学习之基础七十二:Ingress基于Https代理pod
  • 【LLM】MCP(Python):实现 SSE 通信的 Server 和 Client
  • NO.66十六届蓝桥杯备战|基础算法-贪心-区间问题|凌乱的yyy|Rader Installation|Sunscreen|牛栏预定(C++)
  • 一键自动备份:数据安全的双重保障
  • Linux网络:数据链路层以太网
  • 6.第二阶段x64游戏实战-分析人物状态
  • MySQL:表的约束
  • Windows 10/11系统优化工具
  • 基于Spark的招聘数据预测分析推荐系统
  • Golang的Web框架比较与选择
  • 26.[MRCTF2020]Transform 1
  • 构建macOS命令速查手册:基于Flask的轻量级Web应用实践
  • 代码随想录算法训练营第三十八天 | 322.零钱兑换 279.完全平方数 139.单词拆分
  • 关于HikariDataSource (null)的误解,顺带提出一种mybaits-Plus mapper映射失败的容易被忽视的原因
  • 用swift playground写个ios应用和大模型或者网站交互
  • 日本汽车规模性经济计划失败,日产三大品牌的合并合作共赢,还是绝地求生?本田与日产合并确认失败,将成为世界第三大汽车集团愿景失败
  • 如何绕过myabtis-plus的逻辑删除条件
  • Unity URP管线与HDRP管线对比