网站公司怎么做业务推广运营怎么做
本文使用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 installtar 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.tbcontainer_name: nginx-forwardhostname: nginx-forwardrestart: always#command: "sleep 10000000"volumes:- ./log/nginx:/var/log/nginx- ./config/nginx.conf:/etc/nginx/nginx.conf- ./config/conf.d:/etc/nginx/conf.denvironment:- TZ=Asia/Shanghai- AA=aaaadddaadports:- 87:9000networks:- 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即可解决问题。但当时调试时思路不够清晰,搞了好久才发现问题。