用c语言编写cgi脚本

Published On January 04, 2017

category server | tags CGI FastCGI nginx lighttpd


如今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;
}
上例使用printf将返回内容输出到标准输出,按照http协议的要求,包含http的Content-type头和简单的HTML文档作为正文,中间空一行,换行符必须是\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;
}
这段程序接受一个表单参数name,可以是GET方式也可以是enctype为application/x-www-form-urlencode(默认)的POST方式。

部署

大家最爱的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"  => ""
)
同时我修改了监听的端口为3000

配置文件中默认的document-root 是 /srv/http

上面的配置表示当请求的SCRIPT_NAME以.cgi为后缀或者是cgi-bin目录中的文件则直接执行该程序,将标准输出返回给浏览器。 我们将上面的hello.c代码编译后放到/srv/http里

gcc -o /srv/http/cgi-bin/hello.cgi hello.c
写一个简单的html测试文件test.html
<!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>
在浏览器里打开该文件,输入框中输入yxr 将会看到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;
        }
    }
}
上面共用了lighttpd的document root。

启动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
fcgiwrap.sock的权限是755,改成777后就好了。

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"));
}
我们使用了fcgi_stdio库,它可以让我们方便地将现有的CGI程序转换成FastCGI程序以及创建新的FastCGI程序。 一个FastCGI程序的代码由两部分组成:

  • 初始化部分,进程启动后只执行一次
  • 循环响应部分,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/.libs生成静态链接库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
spawn-fcgi会监听端口9001

通过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;
    }
}
nginx会将所有uri以.cgi结尾的请求转发到spawn-fcgi监听的端口上。

在浏览器里输入http://localhost:8009/a.cgi 会看到

FastCGI程序运行效果图

注意,uri与具体的文件没有对应关系。 刷新后次数会增加,杀掉进程重启,次数又从1开始计算,说明我们的程序确实是常驻内存的。

参考


qq email facebook github
© 2018 - Xurui Yan. All rights reserved
Built using pelican