CGI程序开发和Cgicc库介绍

  CGI开发可真是个历史悠久的东西了,尤其在当前RPC越发流行的趋势下,就越感觉到CGI真是个古董级别的玩意儿了,所以如果不是当历史遗留项目的接盘侠的话,想必新项目开发也很难会接触到这个东西,其角色就像Perl一样大多作为维护状态使用。这东西在上个时代的互联网后台开发也算是风光无限过,现在寻找起来真是资料难觅,很多链接基本都打不开了,不过对其了解一下对后台开发也没有什么坏处,暂且不说有利于老项目的维护和开发,至少也可以在别人面前假装成一个具有时代开发印记的老码农了。
  作为CGI现在动态脚本语言用的比较多,比如Python、Perl、PHP等,而且为了降低启动进程的开销基本都是Fast-CGI作为实现。CGI的基本工作过程就是:首先HTTP Server将用户对CGI程序的请求信息设置在环境变量当中;然后对于POST、PUT类型的请求,HTTP Server将传递的消息体通过标准输入传递给CGI程序,同时消息体的长度保存在CONTENT_LENGTH环境变量当中;CGI程序利用这些信息进行业务逻辑处理,然后再将结果输出到标准输出中;HTTP Server再将这些结果回复给请求者,完成这次调用。因为这种机制是透明的,所以CGI用什么语言实现其实是可以互换,如果是要生成复杂的页面格式化输出,那么脚本语言有着其天然优势,但相比而言C/C++这类编译型强语言则注重那种performance-critical的高性能应用场景,同时C/C++和操作系统的亲密耦合,使得C/C++语言开发的CGI可以方便使用系统调用等更加底层的功能。
  前面说到,使用C/C++开发CGI程序势必会遇到很多繁杂的东西,C/C++标准和库对这种应用场景没有特别的支持,所以大家通常使用GNU Cgicc这个库来减轻CGI开发中的体力劳动。这个库还是比较mini的,其提供GET、POST请求的透明解析,支持对字符串、整形、浮点等数据的提取接口,CGI环境变量的加载和访问,HTML输出的辅助生成,HTTP文件上传等功能,基本涵盖了CGI程序开发的方方面面。
  话说,如果项目提供的CGI服务比较多,那么可以做出一些抽象以增加复用,形成一个服务调用框架的效果,比如统一收集输入环境变量、请求参数,统一格式化输出结果等,对业务部分也可以抽象出一个类似Transcation基类、或者采用模板类的方式来实现,当然这是C++本身的开发技能,和具体CGI项目就无关了。
  Cgicc库本身还是比较简单的,根据上面CGI的原理,可以将Cgicc的结构分为两部分:输入部分和响应生成部分。

一、输入部分

  Cgicc:是一个基本的类,通常在程序执行的开始位置就创建该类型的一个对象,其组要功能有:从HTML普通请求、form提交、文件上传等操作中提取信息,并操作环境变量相关的功能。
  CgiEnvironment:主要用于获取从HTTP Server传递过来的相关信息,这些环境变量信息都由CGI规范指明了的。如果不想使用这个类,可以直接调用库函数getenv()的方式取值,该函数返回指向其值的char指针。常用HTTP信息都可以通过响应的接口访问到,比如:getRemoteAddr()、getUserAgent()、getRequestMethod()等,还有URL后面的参数除了用迭代getElements()访问方式之外,也可以通过getQueryString()方式获取原始参数串自己分析处理。
  FormEntry:是一个不改变immutable的class,主要用于从提交表单中提取信息并以键值对的方式存储(比如form表单中action属性指向该cgi程序),该类提供了接口可以方便的访问string、interger、double等数据类型。当然,FormEntry不仅仅局限于表单提交,使用中可以直接格式化调用参数添加在URL后面,格式化的参数也可以通过FormEntry过来访问,所以CGI的客户端要向服务端请求一个CGI调用,就只需要使用预先设定的Callback URL,并根据本次调用上下文格式化成调用参数,再使用libcurl这类HTTP Client库进行调用请求就可以了。

1
2
3
4
cgicc::form_iterator name = cgi.getElement("name");
if(name != cgi.getElements().end()) {
cout << name->getValue() << end;
}

  FormFile:也是一个不可改变immutable的类,其代表了HTTP文件上传机制的上传文件,其和FormEntry机制类似,只不过无法使用键值对的方式访问内容。

二、输出部分

  C++ CGI的输出是写到C++标准输出流的,虽然可以直接使用std::cout方式来输出,但是借助Cgicc的HTML输出工具可以更方便快捷的HTML格式化输出(也有文献提到推荐Google开源的ctemplate库来进行HTML格式化输出)。需要说明的是,不是CGI程序就一定要进行HTML输出,比如还可以进行XML等其他输出格式都是可以的,因为我们基本不会用C/C++开发的CGI程序生成浏览器上显示的结果,而通常是会用libcurl这类轻量级的客户端进行CGI调用,而CGI程序执行后最终也只是封装一个调用结果信息。
  正常情况下HTTP请求的结果输出主要由一个或多个HTTP头和HTML文本组成,而像上面所说的,CGI程序可以产生任何响应输出。Cgicc的响应生成主要分为两个部分:

2.1 HTTP头

  HTTP头作为HTTP协议的组成,其含义是不言自明的。其主要的类包含:
  HTTPCookie:主要是保存cookie这类键值对信息的结构,用来标示识别客户端使用的。通过cgi.getEnvironment().getCookieList()可以返回vector类型对象的引用,而通过getName()和getValue()可以访问某个Cookie的键和值信息。
  HTTPHeader是一个基础头部类,通常不会直接使用它,而是他的派生类型:HTTPContentHeader用户表示返回给客户端的数据类型;HTTPRedirectHeader用于使客户端URL重定向;HTTPStatusHeader用于返回3位数的状态码和相关描述信息;HTTPHTMLHeader主要用于text/html类型的数据返回;HTTPPlainHeader主要用于text/plain类型的数据返回;HTTPResponseHeader功能更加强大的通用HTTP头部类,用于构建复杂的HTTP响应信息。

2.2 HTML生成类

  主要辅助生成HTML输出信息,Cgicc推荐HTML的输出要符合HTML 4.0标准,所以推荐你格式化的输出最好支持符合点规范。当然这部分内容写起来比较枯燥乏味,而且我觉得现实中用其大规模构建HTML输出的情况也不会多。
  下面直接摘录个例子吧,表明访问Environment和Elements的建议方法,其实还是挺简单的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include "cgicc/CgiDefs.h"
#include "cgicc/Cgicc.h"
#include "cgicc/HTTPHTMLHeader.h"
#include "cgicc/HTMLClasses.h"
#include <iostream>
#include <vector>
using namespace std;
using namespace cgicc;

int main(int argc, char ** argv) {
Cgicc cgi;
cout << HTTPHTMLHeader() << HTMLDoctype(HTMLDoctype::eStrict) << endl;
cout << html().set("lang","en").set("dir","ltr") << endl;
cout << head() << title("GNU Cgicc Test") << head() << endl;

const cgicc::CgiEnvironment& env = cgi.getEnvironment();
cout << cgi.getEnvironment().getRemoteHost() << endl;
cout << env.getScriptName() << endl;

for (cgicc::const_form_iterator iter = cgi.getElements().begin();
iter != cgi.getElements().end(); ++iter)
cout << iter->getName() << ": " << iter->getValue() << endl;
const std::vector<cgicc::HTTPCookie>& cks = env.getCookieList();
for (std::vector<cgicc::HTTPCookie>::size_type i = 0; i < cks.size(); i++)
cout << cks[i].getName() << ": " << cks[i].getValue() << endl;
return 0;
}

三、后言

  在传统公司CGI应该用的是很普通的,而新兴公司基本都不用这一套了。个人感觉CGI的主要缺陷是性能太慢,我司线上业务只能达到30 trans/sec的吞吐量(当然还有其他的业务在跑),虽然数据库也升级了,每个请求处理的时间也很短,但是性能就是上不去。想想每个请求都创建进程-处理任务-销毁进程,在对程序越来越极致化的今天,简直是不可忍受的……

本文完!

参考