如今web应用开发只需要关注业务功能怎么实现,对于服务器底层的实现大部分人一无所知。本文并不打算介绍CGI以及FastCGI的概念,而是直接切入主题:如何使用c语言编写web应用。用C写CGI程序是php问世之前才会有人这么干,我只是怀旧而已。
CGI脚本的工作原理
CGI是一种协议,CGI脚本是web应用中由web服务器启动生成动态内容的外部程序。CGI规定了web服务器如何将请求的信息传递给CGI脚本,有了这种规范,任何语言都可以编写CGI脚本在支持CGI的web服务器上运行。可能是因为一开始的CGI程序都是perl或shell编写的,所以叫做CGI脚本。
输出
web服务器将CGI脚本的标准输出
返回给浏览器。
比如:
#include "stdio.h"
int main(void) {
printf( "Content-Type: text/plain\r\n\r\n" );
printf("Hello world !");
return 0;
}
\r\n
。
输入
http正文部分作为CGI脚本的标准输入,也就意味着POST请求的参数也是通过标准输入。请求的其他信息都是通过环境变量的方式传入CGI脚本,如果用C编写CGI程序则可以通过getenv函数获取。
环境变量列表
- 服务器相关的变量:
- SERVER_SOFTWARE: HTTP服务器的名称/版本
- SERVER_NAME: 服务器的主机名,可以是ip地址
- GATEWAY_INTERFACE: CGI/version
- 请求相关的变量:
- SERVER_PROTOCOL: HTTP/version
- SERVER_PORT: TCP端口号
- REQUEST_METHOD: HTTP方法名
- PATH_INFO: 路径后缀,URL中跟在CGI程序名和一个/之后的部分
- PATH_TRANSLATED: 如果出现PATH_INFO的话才有,对应的由服务器设置的完整路径
- SCRIPT_NAME: CGI程序的相对路径,比如 /cgi-bin/script.cgi
- QUERY_STRING: URL中跟在?后面的部分。查询字符串由&分隔的名称=值对组成(例如var1=val1&var2=val2...),通过GET方法提交表单数据时用到,以HTML application/x-www-form-urlencoded的形式
- REMOTE_HOST: 客户端的主机名,如果服务器没有查询则不会设置
- REMOTE_ADDR: 客户端的ip地址IP(点分十进制)
- AUTH_TYPE: 可用的认证方式
- REMOTE_USER: 在特定的认证方式下使用
- REMOTE_IDENT: 客户端的认证方式,只有服务器执行相应的查询才会有
- CONTENT_TYPE: 当使用PUT或POST方法时的网络媒体类型,作为HTTP头提供。
- CONTENT_LENGTH: 类似的,如果通过HTTP头提供的话表示输入数据的大小(十进制,单位是字节)
- 用户代理传递的变量(HTTP_ACCEPT, HTTP_ACCEPT_LANGUAGE, HTTP_USER_AGENT, HTTP_COOKIE 以及其他一些可能的变量),包含相关的HTTP头,并且具有相同的意义
hello.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void sayHello(char * input)
{
if (input) {
char * pos = strstr(input, "name");
if (pos) {
char * name = pos + 5;
if (*name) {
printf("Hello %s !\n", name);
return;
}
}
}
printf("Hello world !\n");
}
int main(void) {
printf( "Content-Type: text/plain\n\n" );
char * method = getenv("REQUEST_METHOD");
if (strcmp(method, "GET") == 0) {
char * query_str = getenv("QUERY_STRING");
sayHello(query_str);
} else if (strcmp(method, "POST") == 0) {
char * content_type;
content_type = getenv("CONTENT_TYPE");
if (strcmp(content_type, "application/x-www-form-urlencoded") == 0) {
int content_len = atoi(getenv("CONTENT_LENGTH"));
char * buffer = calloc(content_len + 1, sizeof(char));
fread(buffer, sizeof(char), content_len, stdin);
sayHello(buffer);
free(buffer);
} else {
printf("CONTENT_TYPE not supported now !");
}
} else {
sayHello(NULL);
}
return 0;
}
部署
大家最爱的nginx本身并不支持CGI,CGI已经被FastCGI替代了。 要想运行CGI程序有两种方式:
- 使用原生支持CGI的web服务器,比如Apache或lighttpd,这种方式是由web服务器的CGI模块启动CGI程序
- 如果一定要用nginx,就需要借助额外的工具,nginx提供了fcgiwrap模块用于启动CGI程序
下面分别介绍这两种运行方式。
在lighttpd中以CGI的方式运行
在lighttpd的配置文件中追加
server.modules = (
"mod_cgi",
)
$HTTP["url"] =~ "/cgi-bin/" {
cgi.assign = ( "" => "" )
}
cgi.assign = (
".cgi" => ""
)
配置文件中默认的document-root 是 /srv/http
上面的配置表示当请求的SCRIPT_NAME以.cgi为后缀或者是cgi-bin目录中的文件则直接执行该程序,将标准输出返回给浏览器。 我们将上面的hello.c代码编译后放到/srv/http里
gcc -o /srv/http/cgi-bin/hello.cgi hello.c
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello</title>
</head>
<body>
<form action="http://localhost:8008/cgi-bin/hello.cgi">
<input type="text" name="name" placeholder="please input your name here">
<input type="submit" formmethod="GET" value="Submit by GET">
<input type="submit" formmethod="POST" value="Submit by POST">
</form>
</body>
</html>
Hello yxr !
在nginx中借助fcgi-wrap以CGI的方式运行
fcgi-wrap顾名思义,是一种CGI wrapper,作为nginx的上游通过FastCGI协议与nginx交互,对于每个请求启动CGI程序执行,相当于把FastCGI转换成了CGI。
修改nginx的配置文件/etc/nginx/nginx.conf
http {
# 在http中添加以下内容
server {
listen 8008;
root /srv/http;
location ~ \.cgi$ {
include FastCGI_params;
FastCGI_pass unix:/var/run/fcgiwrap.sock;
try_files $uri =404;
}
}
}
启动fcgiwrap
fcgiwrap -s unix:/var/run/fcgiwrap.sock
访问的时候502 查看nginx的error.log
connect() to unix:/var/run/fcgiwrap.sock failed (13: Permission denied) while connecting to upstream
FastCGI编程
再次警告:这是20多年前的技术,没有足够的兴趣建议不要折腾了。
上面的方式依旧是古老的CGI编程,如何才能编写真正的FastCGI程序。
CGI是 fork-and-execute 的方式,每个请求都会新建一个进程处理,因为进程的创建和结束很低效,这种方式一直被人诟病。FastCGI以 long-live 的方式执行,是CGI的改进,起到了缓冲作用从而极大地提高了性能。fcgi进程管理器维护一个进程池,一个进程处理完请求后不退出而是可以继续处理后续的请求。现在实际项目中都是用FastCGI。
用C语言编写FastCGI程序
tiny-fcgi.c
#include <stdlib.h>
#include "fcgi_stdio.h"
void main(void)
{
int count = 0;
while(FCGI_Accept() >= 0)
printf("Content-type: text/html\r\n"
"\r\n"
"<title>FastCGI Hello!</title>"
"<h1>FastCGI Hello!</h1>"
"Request number %d running on host <i>%s</i>\n",
++count, getenv("SERVER_NAME"));
}
- 初始化部分,进程启动后只执行一次
- 循环响应部分,FastCGI程序每次被请求的时候都会执行
FCGI_Accept函数会一直阻塞到一个客户端请求进来,然后返回0。遇到错误会返回-1。 fcgi_stdio库是FastCGI Developer's Kit的一部分,下面介绍如何编译以上代码。
FastCGI Developer's Kit
http://www.fastcgi.com上已经找不到源代码了。 可以从github下载FastCGI Developer's Kit的镜像
编译
./configure
make
libfcgi.a
要编译上面的tiny-fcgi.c,只需要:
- 将包含fcgi_stdio.h的路径加入到gcc的头文件搜索路径中,可以使用-l选项,因为我的系统/usr/include中已经有该文件了,所以没有显式包含
- 链接的时候使用上面的静态库文件libfcgi.a,它包含了fcgi_stdio.h中定义的函数的具体实现
将libfcgi.a拷贝到tiny-fcgi.c的路径下,然后编译tiny-fcgi.c
gcc tiny-fcgi.c libfcgi.a -o tiny-fcgi.cgi
在nginx中以FastCGI的方式运行
与apache不同,nginx不会自动spawn FCGI进程,需要单独启动。
为了运行上面的FastCGI程序,我们需要spawn-fcgi来启动它,spawn-fcgi原本是lighttp内的FastCGI进程管理器。它将FastCGI进程与web server分离开,重启不会相互影响,并且可以在不同的机器上部署。 安装spawn-fcgi后执行
spawn-fcgi -p 9001 tiny-fcgi.cgi
通过netstat -tunlp|grep 9001
可以看到tiny-fcgi.cgi正在运行
配置nginx
server {
listen 8009;
root /srv/http;
location ~ \.cgi$ {
include FastCGI_params;
FastCGI_pass 127.0.0.1:9001;
}
}
在浏览器里输入http://localhost:8009/a.cgi 会看到
注意,uri与具体的文件没有对应关系。 刷新后次数会增加,杀掉进程重启,次数又从1开始计算,说明我们的程序确实是常驻内存的。