zoukankan      html  css  js  c++  java
  • Nginx + FastCGI 程序(C/C++) 搭建高性能web service的Demo及部署发布

      

    FastCGI编程包括四部分:初始化编码、接收请求循环、响应内容、响应结束循环。

     
    FCGX_Request request;
    FCGX_Init();
    int sock_fd = FCGX_OpenSocket("10.3.17.75:8003",100);
    FCGX_InitRequest(&request, sock_fd, 0);
    while (FCGX_Accept_r(&request) >= 0) {
      //get param 1
      map<string,string> param_map;
      for(int i = 0; request.envp[i]; ++i) {
        string s = request.envp[i];
        size_t pos = s.find_first_of('=');
        if (pos > 0 && pos < s.size() - 1) {
          param_map.insert(make_pair(s.substr(0,pos), s.substr(pos+1)));
        }
      }
      //or 2 
      char * clenstr = FCGX_GetParam("CONTENT_LENGTH", request.envp);
      //do something
      FCGX_Stream* fcgi_out = request.out;
      string output="test";
      FCGX_PutS(output.c_str(), fcgi_out);
      //finish
      FCGX_Finish_r(&request);
    }
     
     
     
     
     
     
     
    由于最近工作的需要,本人学习了一下利用高性能web server - Nginx,来发布C/C++编写的fastCGI程序,详细细节如下。
     
     
    1.介绍
        Nginx - 高性能web server,这个不用多说了,大家都知道。
        FastCGI程序 - 常驻型CGI程序,它是语言无关的、可伸缩架构的CGI开放扩展,其主要行为是将CGI解释器进程保持在内存中并因此获得较高的性能。
        Nginx要调用FastCGI程序,需要用到FastCGI进程管理程序(因为nginx不能直接执行外部的cgi程序,我们可使用lighttpd中的spawn-fastcgi来让nginx可支持外部cgi运行。也有其他方法安装nginx-fcgi来让nginx支持cgi,这里是使用spawn-fastcgi的方法),来达到调用FastCGI程序的目的。Nginx本身没有集成类似的模块,而Apache具备该功能模块,所以不需要额外安装FastCGI进程管理程序。
     
    2.工作原理
        Nginx不支持对外部程序的直接调用或者解析,所有的外部程序(包括PHP)必须通过FastCGI接口来调用。FastCGI接口在Linux下是socket(这个socket可以是文件socket,也可以是ip socket)。为了调用CGI程序,还需要一个FastCGI的wrapper(wrapper可以理解为用于启动另一个程序的程序),这个wrapper绑定在某个固定socket上,如端口或者文件socket。
        当Nginx将CGI请求发送给这个socket的时候,通过FastCGI接口,wrapper接收到请求,然后派生出一个新的线程,这个线程调用解释器或者外部程序处理脚本并读取返回数据;接着,wrapper再将返回的数据通过FastCGI接口,沿着固定的socket传递给Nginx;最后,Nginx将返回的数据发送给客户端。这就是Nginx+FastCGI的整个运作过程,如图1所示。
     
        
    ​图1 Nginx+FastCGI运行过程​
     
        FastCGI接口方式在脚本解析服务器(CGI应用程序服务器)上启动一个或者多个守护进程对动态脚本进行解析,这些进程就是FastCGI进程管理器,或者称为FastCGI引擎。 spawn-fcgi与PHP-FPM都是FastCGI进程管理器(支持PHP和C/C++​)。​
        
        介绍到这里,大家应该都对该模式有了一定的了解,下面开始进行实战!    
     
    3.环境部署
    3.1.Nginx的安装、部署与配置
        nginx下载目录 http://nginx.org/en/download.html
        这我们使用的是nginx-1.5.10
        
        [安装]
        下载以后解压并安装(请记得看README)
        ./configure (注意了类似checking for *** ... not found项,可能是依赖包没有,则需要安装依赖包)
        
        缺少pcre,则需要额外安装 http://www.pcre.org/ (或者采用apt-get或yum的安装方式)
        缺少zlib,则需要额外安装 http://www.zlib.net/ (或者采用apt-get或yum的安装方式)  
        缺少OpenSSL,则需要额外安装 http://www.openssl.org (或者采用apt-get或yum的安装方式)  
        如果需要配置安装额外的功能模块,可以参考这里 http://wiki.codemongers.com/NginxChsInstall
     
        make
        make install (默认安装到/usr/local/nginx
     
        [配置和管理]     
        1)执行选项
            -c </path/to/config> 为 Nginx 指定一个配置文件,来代替缺省的。不输入则使用默认的配置文件。
            -t 不运行,而仅仅测试配置文件。nginx 将检查配置文件的语法的正确性,并尝试打开配置文件中所引用到的文件。
            -v 显示 nginx 的版本。
            -V 显示 nginx 的版本,编译器版本和配置参数。
     
        2)检查配置文件
            sudo ./nginx -t
            nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
            nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful
       
         3)启动 - 默认和特殊
            /usr/local/nginx/sbin/nginx (默认启动方式)
            /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf (指定配置文件启动)
        
        4)查看nginx进程号(master是主进程)
            ps -ef | grep nginx 
     
        5)重新加载配置文件
            sudo kill -HUP [nginx主进程号]
            通过系统的信号控制 Nginx
            可以使用信号系统来控制主进程。默认,nginx 将其主进程的 pid 写入到 /usr/local/nginx/logs/nginx.pid 文件中。通过传递参数        给 ./configure 或使用 pid 指令,来改变该文件的位置。
            
            主进程可以处理以下的信号:
            命令 说明 备注
            TERM, INT 快速关闭  
            QUIT 从容关闭  
            HUP 重载配置 用新的配置开始新的工作进程    从容关闭旧的工作进程
            USR1 重新打开日志文件  
            USR2 平滑升级可执行程序  
            WINCH 从容关闭工作进程  
     
        6)默认目录结构
            主目录:/usr/local/nginx/
            配置目录:/usr/local/nginx/conf/
            root目录:/usr/local/nginx/html/
            可执行文件路径:/usr/local/nginx/sbin/
     
    3.2.spawn_fastcgi的安装、部署与配置
        spawn_fastcgi  https://github.com/lighttpd/spawn-fcgi 
        这里使用的是1.6.3的版本 https://github.com/lighttpd/spawn-fcgi/releases/tag/v1.6.3
        
        下载以后解压并安装(请记得看README)
        如果没有configure,请先执行./autogen.sh,生成configure
        
        ./configure
        make
     
        编译好以后,将可执行文件移动到nginx的sbin目录下
        cp ./src/spawn-fcgi /usr/local/nginx/sbin/ (cp到nginx的安装目录下)
     
     
    3.3.fastcgi库的安装(库绝对不是必须的,觉得技术好的大牛可以自己写)
        库地址 http://www.fastcgi.com/dist/fcgi.tar.gz
        下载以后,解压并安装 (默认安装)
        ./configure
        make
        make install
        
    4.Demo和web发布
    4.1.Demo程序
        [CGI程序]    
    [cpp] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1. #include <fcgi_stdio.h>  
    2. #include <stdlib.h>  
    3.   
    4. int main() {  
    5.     int count = 0;  
    6.     while (FCGI_Accept() >= 0) {  
    7.         printf("Content-type: text/html "  
    8.                 " "  
    9.                 ""  
    10.                 "FastCGI Hello!"  
    11.                 "Request number %d running on host%s "  
    12.                 "Process ID: %d ", ++count, getenv("SERVER_NAME"), getpid());  
    13.     }  
    14.     return 0;  
    15. }  
        [编译]
        g++ demo.cc -o demo  -lfcgi​  
        
        直接运行可执行文件,看看能否正常运行。如果出现缺少库libfcgi.so.0,则自己需要手动把/usr/local/lib/libfcgi.so.0库建立一个链接到/usr/lib/目录下:ln -s /usr/local/libfcgi.so.0 /usr/lib/(或者把so的库路径添加到/etc/ld.so.conf,并执行ldconfig更新一下)
     
    4.2.Web发布
        1)将CGI可执行程序移动到nginx的安装目录下 /usr/local/nginx/cgibin (文件夹不存在则自己创建) 
        
        2)启动spawn-fcgi管理进程,并绑定server IP和端口(不要跟nginx的监听端口重合)
         /usr/local/nginx/sbin/spawn-fcgi -a 127.0.0.1 -p 8088 -f /usr/local/nginx/cgibin/demo
        
        查看一下9002端口是否已成功:netstat -na | grep 8088
    ​   
        3)更改nginx.conf配置文件,让nginx转发请求
        
        在http节点的子节点-"server节"点中下添加配置
        location ~ .cgi$ {
            fastcgi_pass 127.0.0.1:8088;
            fastcgi_index index.cgi;
            fastcgi_param SCRIPT_FILENAME fcgi$fastcgi_script_name;

            include fastcgi_params;
        }
     
        4)重启nginx或者重新加载配置文件
        重新加载配置文件
        sudo kill -HUP [pid]
        或者
        重启nginx    
        killall nginx
    ​    ./nginx
     
        5)打开浏览器访问一下吧
        http://localhost/demo.cgi
     
        
     
        搞定收工,心里又小小的激动了一把!
     
    allenrlin
    2014/2/18
        
        参考文献与资料
        [1]Nginx+FastCGI运行原理​  http://book.51cto.com/art/201202/314840.htm
        ​[2]Nginx下配置FastCGI ​ http://www.cppblog.com/woaidongmao/archive/2011/06/21/149090.html
        [3]nginx+fastcgi+c/c++搭建高性能Web框架​  ​http://blog.csdn.net/marising/article/details/3932938​
        [4]什么是CGI、FastCGI、PHP-CGI、PHP-FPM、Spawn-FCGI  ​http://www.mike.org.cn/articles/what-is-cgi-fastcgi-php-fpm-spawn-fcgi/​
     
     
     

    接着上篇《Nginx安装与使用》,本篇介绍CGI/FASTCGI的原理、及如何使用C/C++编写简单的CGI/FastCGI,最后将CGI/FASTCGI部署到nginx。内容大纲如下:

    1.     CGI

    1.1.     环境变量

    1.2.     标准输入

    2.     FastCGI

    3. nginx cgi/fastcgi

    3.1. nginx + fastcgi

    3.1.1. spawn-fcgi

    3.1.2. 编写fastcgi应用程序

    3.1.3. nginx fastcgi配置

    3.2. nginx + cgi

    3.2.1 fastcgi-wrapper

    3.2.2. nginx fcgiwrap配置

    3.2.3. 编写cgi应用程序

    参考链接

    1.CGI

    通用网关接口Common Gateway Interface/CGI描述了客户端和服务器程序之间传输数据的一种标准,可以让一个客户端,从网页浏览器向执行在网络服务器上的程序请求数据。CGI 独立于任何语言的,CGI 程序可以用任何脚本语言或者是完全独立编程语言实现,只要这个语言可以在这个系统上运行。Unix shell script, PythonRubyPHP, perl, TclC/C++, 和 Visual Basic 都可以用来编写 CGI 程序。(http://www.dwz.cn/yFFgQ)

    最初,CGI 是在 1993 年由美国国家超级电脑应用中心(NCSA)为 NCSA HTTPd Web 服务器开发的。这个 Web 服务器使用了 UNIX shell 环境变量 来保存从 Web 服务器传递出去的参数,然后生成一个运行 CGI 的独立的进程。cgi的处理流程如下图所示:

     

    l   step1. web 服务器收到客户端(浏览器)的请求Http Request,启动CGI程序,并通过环境变量标准输入传递数据

    l   step2. cgi进程启动解析器、加载配置(如业务相关配置)、连接其它服务器(如数据库服务器)、逻辑处理等

    l   step3. cgi程将处理结果通过标准输出标准错误,传递给web 服务器

    l   step4. web 服务器收到cgi返回的结果,构建Http Response返回给客户端,并杀死cgi进程

    web服务器与cgi通过环境变量、标准输入、标准输出、标准错误互相传递数据。

    1.1.环境变量

    GET请求,它将数据打包放置在环境变量QUERY_STRING中,CGI从环境变量QUERY_STRING中获取数据。常见的环境变量如下表所示:

    环境变数

    内容

    AUTH_TYPE

    存取认证类型。

    CONTENT_LENGTH

    由标准输入传递给CGI程序的数据长度,以bytes或字元数来计算。

    CONTENT_TYPE

    请求的MIME类型。

    GATEWAY_INTERFACE

    服务器的CGI版本编号。

    HTTP_ACCEPT

    浏览器能直接接收的Content-types, 可以有HTTP Accept header定义.

    HTTP_USER_AGENT

    递交表单的浏览器的名称、版本 和其他平台性的附加信息。

    HTTP_REFERER

    递交表单的文本的 URL,不是所有的浏览器都发出这个信息,不要依赖它

    PATH_INFO

    传递给cgi程式的路径信息。

    QUERY_STRING

    传递给CGI程式的请求参数,也就是用"?"隔开,添加在URL后面的字串。

    REMOTE_ADDR

    client端的host名称。

    REMOTE_HOST

    client端的IP位址。

    REMOTE_USER

    client端送出来的使用者名称。

    REMOTE_METHOD

    client端发出请求的方法(如get、post)。

    SCRIPT_NAME

    CGI程式所在的虚拟路径,如/cgi-bin/echo。

    SERVER_NAME

    server的host名称或IP地址。

    SERVER_PORT

    收到request的server端口。

    SERVER_PROTOCOL

    所使用的通讯协定和版本编号。

    SERVER_SOFTWARE

    server程序的名称和版本。

    1.2.标准输入

    环境变量的大小是有一定的限制的,当需要传送的数据量大时,储存环境变量的空间可能会不足,造成数据接收不完全,甚至无法执行CGI程序。因此后来又发展出另外一种方法:POST,也就是利用I/O重新导向的技巧,让CGI程序可以由STDIN和STDOUT直接跟浏览器沟通。
    当我们指定用这种方法传递请求的数据时,web 服务器收到数据后会先放在一块输入缓冲区中,并且将数据的大小记录在CONTENT_LENGTH这个环境变数,然后调用CGI程式并将CGI程序的STDIN指向这块缓冲区,于是我们就可以很顺利的通过STDIN和环境变数CONTENT_LENGTH得到所有的资料,再没有资料大小的限制了。
     

    总结:CGI使外部程序与Web服务器之间交互成为可能。CGI程式运行在独立的进程中,并对每个Web请求建立一个进程,这种方法非常容易实现,但效率很差,难以扩展。面对大量请求,进程的大量建立和消亡使操作系统性能大大下降。此外,由于地址空间无法共享,也限制了资源重用。

    2.FastCGI

    快速通用网关接口(Fast Common Gateway Interface/FastCGI)是通用网关接口(CGI)的改进,描述了客户端和服务器程序之间传输数据的一种标准。FastCGI致力于减少Web服务器CGI程式之间互动的开销,从而使服务器可以同时处理更多的Web请求。与为每个请求创建一个新的进程不同,FastCGI使用持续的进程来处理一连串的请求。这些进程由FastCGI进程管理器管理,而不是web服务器。(http://www.dwz.cn/yFMap

     

    当进来一个请求时,Web 服务器把环境变量和这个页面请求通过一个unix domain socket(都位于同一物理服务器)或者一个IP Socket(FastCGI部署在其它物理服务器)传递给FastCGI进程。

     

    l  step1. Web 服务器启动时载入初始化FastCGI执行环境 。 例如IIS ISAPI、apache mod_fastcgi、nginx ngx_http_fastcgi_module、lighttpd mod_fastcgi

    l  step2. FastCGI进程管理器自身初始化,启动多个CGI解释器进程并等待来自Web 服务器的连接。启动FastCGI进程时,可以配置以ip和UNIX域socket两种方式启动。 

    l  step3. 当客户端请求到达Web 服务器时, Web 服务器将请求采用socket方式转发到 FastCGI主进程,FastCGI主进程选择并连接到一个CGI解释器。Web 服务器将CGI环境变量和标准输入发送到FastCGI子进程。

    l  step4. FastCGI子进程完成处理后将标准输出和错误信息从同一socket连接返回Web 服务器。当FastCGI子进程关闭连接时,请求便处理完成。

    l  step5. FastCGI子进程接着等待并处理来自Web 服务器的下一个连接。

    由于 FastCGI 程序并不需要不断的产生新进程,可以大大降低服务器的压力并且产生较高的应用效率。它的速度效率最少要比CGI 技术提高 5 倍以上。它还支持分布式的部署, 即 FastCGI 程序可以在web 服务器以外的主机上执行。

    总结:CGI 就是所谓的短生存期应用程序,FastCGI 就是所谓的长生存期应用程序。FastCGI像是一个常驻(long-live)型的CGI,它可以一直执行着,不会每次都要花费时间去fork一次(这是CGI最为人诟病的fork-and-execute 模式)。

    3.nginx cgi/fastcgi

    nginx 不能像apache那样直接执行外部可执行程序,但nginx可以作为代理服务器,将请求转发给后端服务器,这也是nginx的主要作用之一。其中nginx就支持FastCGI代理,接收客户端的请求,然后将请求转发给后端fastcgi进程。下面介绍如何使用C/C++编写cgi/fastcgi,并部署到nginx中。

    3.1. nginx + fastcgi

    通过前面的介绍知道,fastcgi进程由FastCGI进程管理器管理,而不是nginx。这样就需要一个FastCGI管理,管理我们编写fastcgi程序。本文使用spawn-fcgi作为FastCGI进程管理器。

    3.1.1. spawn-fcgi

    spawn-fcgi是一个通用的FastCGI进程管理器,简单小巧,原先是属于lighttpd的一部分,后来由于使用比较广泛,所以就迁移出来作为独立项目了。spawn-fcgi使用pre-fork 模型,功能主要是打开监听端口,绑定地址,然后fork-and-exec创建我们编写的fastcgi应用程序进程,退出完成工作。fastcgi应用程序初始化,然后进入死循环侦听socket的连接请求。

    安装spawn-fcgi:

    l  获取spawn-fcgi编译安装包,在http://redmine.lighttpd.net/projects/spawn-fcgi/wiki上可以获取当前最新的版本。

    l  解压缩spawn-fcgi-x.x.x.tar.gz包。

    l  进入解压缩目录,执行./configure。

    l  make & make install

    如果遇到以下错误:“ ./autogen.sh: x: autoreconf: not found”,因为没有安装automake 工具,ubuntu用下面的命令安装好就可以了:sudo apt-get install autoconf automake libtool 。

    spawn-fcgi的帮助信息可以通过man spawn-fcgi或spawn-fcgi –h获得,下面是部分常用spawn-fcgi参数信息:

    -f <fcgiapp> 指定调用FastCGI的进程的执行程序位置

    -a <addr> 绑定到地址addr。

    -p <port> 绑定到端口port。

    -s <path> 绑定到unix domain socket

    -C <childs> 指定产生的FastCGI的进程数,默认为5。(仅用于PHP)

    -P <path> 指定产生的进程的PID文件路径。

    -F <childs> 指定产生的FastCGI的进程数(C的CGI用这个)

    -u和-g FastCGI使用什么身份(-u 用户 -g 用户组)运行,CentOS下可以使用apache用户,其他的根据情况配置,如nobody、www-data等。

    3.1.2. 编写fastcgi应用程序

    使用C/C++编写fastcgi应用程序,可以使用FastCGI软件开发套件或者其它开发框架,如fastcgi++。

    本文使用FastCGI软件开发套件——fcgi(http://www.fastcgi.com/drupal/node/6?q=node/21),通过此套件可以轻松编写fastcgi应用程序,安装fcgi:

    l  获取fcgi编译安装包,在http://www.fastcgi.com/drupal/node/5上可以获取当前最新的版本。

    l  解压缩fcgi-x.x.x.tar.gz包。

    l  进入解压缩目录,执行./configure。

    l  make & make install

    如果编译提示一下错误:

    fcgio.cpp: In destructor 'virtual fcgi_streambuf::~fcgi_streambuf()':

    fcgio.cpp:50: error: 'EOF' was not declared in this scope
    fcgio.cpp: In member function 'virtual int fcgi_streambuf::overflow(int)':
    fcgio.cpp:70: error: 'EOF' was not declared in this scope
    fcgio.cpp:75: error: 'EOF' was not declared in this scope
    fcgio.cpp: In member function 'virtual int fcgi_streambuf::sync()':
    fcgio.cpp:86: error: 'EOF' was not declared in this scope
    fcgio.cpp:87: error: 'EOF' was not declared in this scope
    fcgio.cpp: In member function 'virtual int fcgi_streambuf::underflow()':
    fcgio.cpp:113: error: 'EOF' was not declared in this scope
    make[2]: *** [fcgio.lo] Error 1
    make[2]: Leaving directory `/root/downloads/fcgi-2.4.1-SNAP-0910052249/libfcgi'
    make[1]: *** [all-recursive] Error 1
    make[1]: Leaving directory `/root/downloads/fcgi-2.4.1-SNAP-0910052249'

    make: *** [all] Error 2

    解决办法:在/include/fcgio.h文件中加上 #include <cstdio>,然后再编译安装就通过了。

    如果提示找不到动态库,请在LD_LIBRARY_PATH或/etc/ld.so.conf中添加fcgi的安装路径,如/usr/local/lib,并执行ldconfig更新一下。

    #include "fcgi_stdio.h"

    #include <stdlib.h>

    int main(void)

    {

        int count = 0;

        while (FCGI_Accept() >= 0)

            printf("Content-type: text/html "

            " "

            "<title>FastCGI Hello!</title>"

            "<h1>FastCGI Hello!</h1>"

            "Request number %d running on host <i>%s</i> ",

            ++count, getenv("SERVER_NAME"));

        return 0;

    }
     

    编译g++ main.cpp -o demo –lfcgi,并将demo部署到/opt/nginx-1.7.7/cgi-bin/目录

    通过spawn-fcgi启动c/c++编写好的fastcgi程序:/opt/nginx-1.7.7/sbin/spawn-fcgi -a 127.0.0.1 -p 8081 -f /opt/nginx-1.7.7/cgi-bin/demo 

    3.1.3. nginx fastcgi配置

    关于nginx的几个配置文件解析,可以参阅《Nginx安装与使用http://www.cnblogs.com/skynet/p/4146083.html,在上篇的nginx.conf基础上增加下面的fastcgi配置。

     

    这样nginx收到http://localhost/demo.cgi请求时,会匹配到location = /demo.cgi块,将请求传到后端的fastcgi应用程序处理。如下如所示:(注意其中number为80,是因为我请求了80次)

     

    3.2. nginx + cgi

    nginx 不能直接执行外部可执行程序,并且cgi是接收到请求时才会启动cgi进程,不像fastcgi会在一开就启动好,这样nginx天生是不支持 cgi 的。nginx 虽然不支持cgi,但它支持 fastCGI。所以,我们可以考虑使用fastcgi包装来支持 cgi。原理大致如下图所示:pre-fork几个通用的代理fastcgi程序——fastcgi-wrapper,fastcgi-wrapper启动执行cgi然后将cgi的执行结果返回给nginx(fork-and-exec)。

     

    明白原理之后,编写一个fastcgi-warpper也比较简单。网上流传比较多的一个解决方案是,来自nginx wiki(http://wiki.nginx.org/SimpleCGI)上的使用perl的fastcgi包装脚本cgiwrap-fcgi.pl。但我对perl不是很感冒,下面给出一个C/C++写的fastcgi-wrapper。

    3.2.1. fastcgi-wrapper

    其实编写C/C++的fastcgi-wrapper,就是写一个C/C++的fastcgi,步骤和原理跟前面的小节(nginx+fastcgi)一样。github上已经有人开源了,C写的fastcgi-wrapper:https://github.com/gnosek/fcgiwrap

    安装fcgiwrap:

    l  下载(https://github.com/gnosek/fcgiwrap.git

    l  解压缩fcgiwrap,进入解压目录

    l  autoreconf -i

    l  ./configure

    l  make && make install

    启动fastcgi-wrapper:/opt/nginx-1.7.7/sbin/spawn-fcgi -f /usr/local/sbin/fcgiwrap -p 8081

    3.2.2. nginx fcgiwrap配置

    在nginx.conf中增加下面的loaction配置块,这样所有的xxx.cgi请求都会走到fcgiwrap,然后fcgiwrap会执行cgi-bin目录下的cgi程序。

     

    3.2.3. 编写cgi应用程序

    #include <stdio.h>

    #include <stdlib.h>

    int main(void)

    {

        int count = 0;

        printf("Content-type: text/html "

            " "

            "<title>CGI Hello!</title>"

            "<h1>CGI Hello!</h1>"

            "Request number %d running on host <i>%s</i> ",

            ++count, getenv("SERVER_NAME"));

        return 0;

    }

    tyler@ubuntu:~/ClionProjects/HelloFastCGI$ g++ cgi.cpp -o cgidemo -lfcgi

    tyler@ubuntu:~/ClionProjects/HelloFastCGI$ sudo cp cgidemo /opt/nginx-1.7.7/cgi-bin/

     

    注意图中的请求次数一直都是1,因为cgi的模式是fork-and-exec,每次都是一个新的进程。

    参考链接

    l  CGI, http://www.dwz.cn/yFFgQ

    l  fastcgi, http://www.dwz.cn/yFMap

    l  spawn-fcgi, http://redmine.lighttpd.net/projects/spawn-fcgi/wiki

    l  fcgi, http://www.fastcgi.com/drupal/node/6?q=node/21

    l  fcgiwrap, https://github.com/gnosek/fcgiwrap.git

    spawn-fcgi是一个小程序,作用是管理fast-cgi进程,功能和PHP-fpm类似,简单小巧,原先是属于lighttpd的一部分,后来由于使用比较广泛,所以就迁移出来作为独立项目了,本文介绍的是这个版本“spawn-fcgi-1.6.3”。不过从发布新版本到目前已经4年了,代码一直没有变动,需求少,基本满足了。另外php有php-fpm后,码农们再也不担心跑不起FCGI了。

    很久之前看的spawn-fcgi的代码,当时因为需要改一下里面的环境变量。今天翻代码看到了就顺手记录一下,就当沉淀.备忘吧。

    用spawn启动FCGI程序的方式为:./spawn-fcgi -a 127.0.0.1 -p 9003 -F ${count} -f ${webroot}/bin/demo.fcgi

    这样就会启动count个demo.fcgi程序,他们共同监听同一个listen端口9003,从而提供服务。

    spawn-fcgi代码不到600行,非常简短精炼,从main看起。其功能主要是打开监听端口,绑定地址,然后fork-exec创建FCGI进程,退出完成工作。

    老方法,main函数使用getopt解析命令行参数,从而设置全局变量。如果设置了-P参数,需要保存Pid文件,就用open系统调用打开文件。之后根据是否是root用户启动,如果是root,得做相关的权限设置,比如chroot, chdir, setuid, setgid, setgroups等。

    重要的是调用了bind_socket打开绑定本地监听地址,或者sock,再就是调用fcgi_spawn_connection创建FCGI进程,主要就是这2步。

    [cpp] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1. int main(int argc, char **argv)  
    2. {  
    3.     if (!sockbeforechroot && -1 == (fcgi_fd = bind_socket(addr, port, unixsocket, sockuid, sockgid, sockmode)))  
    4.         return -1;  
    5.     /* drop root privs */  
    6.     if (uid != 0)  
    7.     {  
    8.         setuid(uid);  
    9.     }  
    10.     else    //非root用户启动,打开监听端口,进入listen模式。  
    11.     {  
    12.         if (-1 == (fcgi_fd = bind_socket(addr, port, unixsocket, 0, 0, sockmode)))  
    13.             return -1;  
    14.     }  
    15.     if (fcgi_dir && -1 == chdir(fcgi_dir))  
    16.     {  
    17.         fprintf(stderr, "spawn-fcgi: chdir('%s') failed: %s ", fcgi_dir, strerror(errno));  
    18.         return -1;  
    19.     }  
    20.     //fork创建FCGI的进程  
    21.     return fcgi_spawn_connection(fcgi_app, fcgi_app_argv, fcgi_fd, fork_count, child_count, pid_fd, nofork);  
    22. }  


    bind_socket函数用来创建套接字,绑定监听端口,进入listen模式。其参数unixsocket表明需要使用unix sock文件,这里不多介绍。函数代码页挺简单,莫过于通用的sock程序步骤:socket()->setsockopt()->bind()->listen();

    [cpp] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1. static int bind_socket(const char *addr, unsigned short port, const char *unixsocket, uid_t uid, gid_t gid, int mode)  
    2. {  
    3.     //bind_socket函数用来创建套接字,绑定监听端口,进入listen模式  
    4.     if (-1 == (fcgi_fd = socket(socket_type, SOCK_STREAM, 0)))  
    5.     {  
    6.         fprintf(stderr, "spawn-fcgi: couldn't create socket: %s ", strerror(errno));  
    7.         return -1;  
    8.     }  
    9.     val = 1;  
    10.     if (setsockopt(fcgi_fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)) < 0)  
    11.     {  
    12.         fprintf(stderr, "spawn-fcgi: couldn't set SO_REUSEADDR: %s ", strerror(errno));  
    13.         return -1;  
    14.     }  
    15.     if (-1 == bind(fcgi_fd, fcgi_addr, servlen))  
    16.     {  
    17.         fprintf(stderr, "spawn-fcgi: bind failed: %s ", strerror(errno));  
    18.         return -1;  
    19.     }  
    20.     if (unixsocket)  
    21.     {  
    22.         if (0 != uid || 0 != gid)  
    23.         {  
    24.             if (0 == uid) uid = -1;  
    25.             if (0 == gid) gid = -1;  
    26.             if (-1 == chown(unixsocket, uid, gid))  
    27.             {  
    28.                 fprintf(stderr, "spawn-fcgi: couldn't chown socket: %s ", strerror(errno));  
    29.                 close(fcgi_fd);  
    30.                 unlink(unixsocket);  
    31.                 return -1;  
    32.             }  
    33.         }  
    34.         if (-1 != mode && -1 == chmod(unixsocket, mode))  
    35.         {  
    36.             fprintf(stderr, "spawn-fcgi: couldn't chmod socket: %s ", strerror(errno));  
    37.             close(fcgi_fd);  
    38.             unlink(unixsocket);  
    39.             return -1;  
    40.         }  
    41.     }  
    42.     if (-1 == listen(fcgi_fd, 1024))  
    43.     {  
    44.         fprintf(stderr, "spawn-fcgi: listen failed: %s ", strerror(errno));  
    45.         return -1;  
    46.     }  
    47.     return fcgi_fd;  
    48. }  

    fcgi_spawn_connection函数的工作是循环一次次创建子进程,然后立即调用execv(appArgv[0], appArgv);替换可执行程序,也就试运行demo.fcgi。

    [cpp] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1. static int fcgi_spawn_connection(char *appPath, char **appArgv, int fcgi_fd, int fork_count, int child_count, int pid_fd,  
    2.                                  int nofork)  
    3. {  
    4.     int status, rc = 0;  
    5.     struct timeval tv = { 0, 100 * 1000 };  
    6.     pid_t child;  
    7.     while (fork_count-- > 0)  
    8.     {  
    9.         if (!nofork)  //正常不会设置nofork的  
    10.         {  
    11.             child = fork();  
    12.         }  
    13.         else  
    14.         {  
    15.             child = 0;  
    16.         }  
    17.         switch (child)  
    18.         {  
    19.         case 0:  
    20.         {  
    21.             //子进程  
    22.             char cgi_childs[64];  
    23.             int max_fd = 0;  
    24.             int i = 0;  
    25.             if (child_count >= 0)  
    26.             {  
    27.                 snprintf(cgi_childs, sizeof(cgi_childs), "PHP_FCGI_CHILDREN=%d", child_count);  
    28.                 putenv(cgi_childs);  
    29.             }  
    30.             //wuhaiwen:add child id to thread  
    31.             char bd_children_id[32];  
    32.             snprintf(bd_children_id, sizeof(bd_children_id), "BD_CHILDREN_ID=%d", fork_count);  
    33.             putenv(bd_children_id);  
    34.             if (fcgi_fd != FCGI_LISTENSOCK_FILENO)  
    35.             {  
    36.                 close(FCGI_LISTENSOCK_FILENO);  
    37.                 dup2(fcgi_fd, FCGI_LISTENSOCK_FILENO);  
    38.                 close(fcgi_fd);  
    39.             }  
    40.             /* loose control terminal */  
    41.             if (!nofork)  
    42.             {  
    43.                 setsid();//执行setsid()之后,parent将重新获得一个新的会话session组id,child将仍持有原有的会话session组,  
    44.                 //这时parent退出之后,将不会影响到child了[luther.gliethttp].  
    45.                 max_fd = open("/dev/null", O_RDWR);  
    46.                 if (-1 != max_fd)  
    47.                 {  
    48.                     if (max_fd != STDOUT_FILENO) dup2(max_fd, STDOUT_FILENO);  
    49.                     if (max_fd != STDERR_FILENO) dup2(max_fd, STDERR_FILENO);  
    50.                     if (max_fd != STDOUT_FILENO && max_fd != STDERR_FILENO) close(max_fd);  
    51.                 }  
    52.                 else  
    53.                 {  
    54.                     fprintf(stderr, "spawn-fcgi: couldn't open and redirect stdout/stderr to '/dev/null': %s ", strerror  
    55.                             (errno));  
    56.                 }  
    57.             }  
    58.   
    59.             /* we don't need the client socket */  
    60.             for (i = 3; i < max_fd; i++)  
    61.             {  
    62.                 if (i != FCGI_LISTENSOCK_FILENO) close(i);  
    63.             }  
    64.   
    65.             /* fork and replace shell */  
    66.             if (appArgv)  //如果有外的参数,就用execv执行,否则直接用shell执行  
    67.             {  
    68.                 execv(appArgv[0], appArgv);  
    69.   
    70.             }  
    71.             else  
    72.             {  
    73.                 char *b = malloc((sizeof("exec ") - 1) + strlen(appPath) + 1);  
    74.                 strcpy(b, "exec ");  
    75.                 strcat(b, appPath);  
    76.   
    77.                 /* exec the cgi */  
    78.                 execl("/bin/sh", "sh", "-c", b, (char *)NULL);  
    79.             }  
    80.   
    81.             /* in nofork mode stderr is still open */  
    82.             fprintf(stderr, "spawn-fcgi: exec failed: %s ", strerror(errno));  
    83.             exit(errno);  
    84.   
    85.             break;  
    86.         }  
    87.     }  
    88. }  


    上面是创建子进程的部分代码,基本没啥可说明的。

    对于子进程:注意一下dup2函数,由子进程运行,将监听句柄设置为标准输入,输出句柄。比如FCGI_LISTENSOCK_FILENO 0 号在FCGI里面代表标准输入句柄。函数还会关闭其他不必要的socket句柄。
    然后调用execv替换可执行程序,运行新的二进制,也就是demo.fcgi的FCGI程序。这样子进程能够继承父进程的所有打开句柄,包括监听socket。这样所有子进程都能够在这个9002端口上进行监听新连接,谁拿到了谁就处理之。
    对于父进程: 主要需要用select等待一会,然后调用waitpid用WNOHANG参数获取一下子进程的状态而不等待子进程退出,如果失败就打印消息。否则将其PID写入文件。

    [cpp] view plain copy
     
     在CODE上查看代码片派生到我的代码片
    1. default:  
    2.     /* father */  
    3.   
    4.     /* wait */  
    5.     select(0, NULL, NULL, NULL, &tv);  
    6.   
    7.     switch (waitpid(child, &status, WNOHANG))  
    8.     {  
    9.     case 0:  
    10.         fprintf(stdout, "spawn-fcgi: child spawned successfully: PID: %d ", child);  
    11.         /* write pid file */  
    12.         if (pid_fd != -1)  
    13.         {  
    14.             /* assume a 32bit pid_t */  
    15.             char pidbuf[12];  
    16.             snprintf(pidbuf, sizeof(pidbuf) - 1, "%d", child);  
    17.             write(pid_fd, pidbuf, strlen(pidbuf));  
    18.             /* avoid eol for the last one */  
    19.             if (fork_count != 0)  
    20.             {  
    21.                 write(pid_fd, " ", 1);  
    22.             }  
    23.         }  
    24.         break;  



     

    基本就是上面的东西了,代码不多,但该有的都有,命令行解析,socket,fork,dup2等。很久之前看的在这里备忘一下。

    cgi进程可以写成单线程的,也可以写成多线程的。

    单线程就是main函数中有一个死循环,一直等待接受请求,有请求过来时,就处理请求,并返回结果,没有并发性。

    多线程也分两种模式:一种是main函数起多个线程,每个线程都独立接受请求。另一种是main函数起一个accpet线程接受请求,多个do_session线程处理请求,这种模式需要一个任务队列的支持。

    模式不同,采用的系统架构就不同。下面就这三种模型,给出编码架构设计。

    单线程模式:

    [cpp] view plain copy
     
    1. #include <fcgi_stdio.h>  
    2. #include <stdlib.h>  
    3. #include <string>  
    4.   
    5. int main()  
    6. {  
    7.     while(FCGI_Accept() >= 0)  
    8.     {  
    9.         string strGetData;  
    10.         int iContentLength;  
    11.         if (getenv("QUERY_STRING"))  
    12.         {  
    13.             strGetData = getenv("QUERY_STRING");  
    14.         }  
    15.         if (getenv("CONTENT_LENGTH"))  
    16.         {  
    17.             iContentLength = ::atoi(getenv("CONTENT_LENGTH"));  
    18.         }  
    19.         char* data = (char*)::malloc(iContentLength + 1);  
    20.         ::memset(data, 0, iContentLength + 1);  
    21.         FCGI_fgets(data, iContentLength + 1, FCGI_stdin);  
    22.         FCGI_printf("Content-type:text/html ");  
    23.         FCGI_printf(data);  
    24.     }  
    25.     return 0;  
    26. }  

    多线程模式,每个线程都独立接受请求:

    [cpp] view plain copy
     
    1. #include <fcgi_stdio.h>  
    2. #include <stdlib.h>  
    3. #include <pthread.h>  
    4. #include <string>  
    5.   
    6. void* pthread_func(void *arg);  
    7.   
    8. int main()  
    9. {  
    10.     int iThreadNum = 10;  
    11.     for (int index = 0; index != iThreadNum; ++ index)  
    12.     {  
    13.         pthread_t pthread_id;  
    14.         pthread_create(&pthread_id, NULL, pthread_func, NULL);  
    15.     }  
    16.   
    17.     pthread_join();  
    18.     return 0;  
    19. }  
    20.   
    21. void* pthread_func(void *arg)  
    22. {  
    23.     FCGX_Request *request = NULL;  
    24.     while (1)  
    25.     {  
    26.         int rc = FCGX_Accept_r(request);  
    27.         if (rc < 0)  
    28.         {  
    29.             continue;  
    30.         }  
    31.   
    32.         string strRequestMethod;  
    33.         string strGetData;  
    34.         string strPostData;  
    35.         int iPostDataLength;  
    36.   
    37.         if (FCGX_GetParam("QUERY_STRING", request->envp))  
    38.         {  
    39.             strGetData = FCGX_GetParam("QUERY_STRING", request->envp);  
    40.         }  
    41.         if (FCGX_GetParam("REQUEST_METHOD", request->envp))  
    42.         {  
    43.             iPostDataLength = ::atoi(FCGX_GetParam("CONTENT_LENGTH", _pRequest->envp));  
    44.             char* data = (char*)malloc(iPostDataLength + 1);  
    45.             ::memset(data, 0, iPostDataLength + 1);  
    46.             FCGX_GetStr(data, iPostDataLength, _pRequest->in);  
    47.             strPostData = data;  
    48.             free(data);  
    49.         }  
    50.   
    51.         FCGX_PutS("Content-type: text/html ", _pRequest->out);  
    52.         FCGX_PutS(strPostData.c_str(), _pRequest->out);  
    53.     }  
    54. }  

    多线程模式,一个accpet线程,多个do_session线程:

    [cpp] view plain copy
     
    1. #include <fcgi_stdio.h>  
    2. #include <stdlib.h>  
    3. #include <pthread.h>  
    4. #include <string>  
    5.   
    6. void* do_accept(void *arg);  
    7. void* do_session(void *arg);  
    8.   
    9. int main()  
    10. {  
    11.     pthread_t pthread_id;  
    12.     pthread_create(&pthread_id, NULL, do_accept, NULL);  
    13.     int iThreadNum = 10;  
    14.     for (int index = 0; index != iThreadNum; ++ index)  
    15.     {  
    16.         pthread_t pthread_id;  
    17.         pthread_create(&pthread_id, NULL, do_session, NULL);  
    18.     }  
    19.   
    20.     pthread_join();  
    21.     return 0;  
    22. }  
    23.   
    24. void* do_accept(void *arg)  
    25. {  
    26.     FCGX_Request *request = NULL;  
    27.     while (1)  
    28.     {  
    29.         int rc = FCGX_Accept_r(request);  
    30.         if (rc < 0)  
    31.         {  
    32.             continue;  
    33.         }  
    34.         httpRequest.put(request); //httpRequest 是一个生产者消费者模型,此处是放入任务  
    35.     }  
    36. }  
    37.   
    38. void* do_session(void *arg)  
    39. {  
    40.     while(1)      
    41.     {  
    42.         FCGX_Request *request = NULL;  
    43.         while (!request)  
    44.         {  
    45.             request = httpRequest.get(); //此处是取出任务  
    46.         }  
    47.   
    48.         string strRequestMethod;  
    49.         string strGetData;  
    50.         string strPostData;  
    51.         int iPostDataLength;  
    52.   
    53.         if (FCGX_GetParam("QUERY_STRING", request->envp))  
    54.         {  
    55.             strGetData = FCGX_GetParam("QUERY_STRING", request->envp);  
    56.         }  
    57.         if (FCGX_GetParam("REQUEST_METHOD", request->envp))  
    58.         {  
    59.             iPostDataLength = ::atoi(FCGX_GetParam("CONTENT_LENGTH", _pRequest->envp));  
    60.             char* data = (char*)malloc(iPostDataLength + 1);  
    61.             ::memset(data, 0, iPostDataLength + 1);  
    62.             FCGX_GetStr(data, iPostDataLength, _pRequest->in);  
    63.             strPostData = data;  
    64.             free(data);  
    65.         }  
    66.   
    67.         FCGX_PutS("Content-type: text/html ", _pRequest->out);  
    68.         FCGX_PutS(strPostData.c_str(), _pRequest->out);  
    69.     }  
    70. }  

    工作到目前为止,只见了这三种模型,如果哪位好友有其他的模型,欢迎指点一下,在下不胜感激~~~

  • 相关阅读:
    sql函数
    sql日期
    Windows下串口编程
    Libreoffice/Office:禁止首字母自动大写功能
    convert:图片转pdf失败
    LibreOffice/Calc:单元格设置下拉菜单
    Ubuntu:查询计算机软硬件信息
    tar:文件打包归档
    中科大自主招生2018年笔试数学之五
    文件分割与合并
  • 原文地址:https://www.cnblogs.com/diegodu/p/6030058.html
Copyright © 2011-2022 走看看