1.1 如何编写CGI程序

CGI的工作原理介绍:CGI(Common Gateway Interface)是一个WEB服务器提供信息服务的标准接口,通过这样一个接口,WEB服务器能够执行程序,并将程序输出的信息返回给浏览器。因为在WEB网上的数据都是静态的,通过CGI程序能够动态的处理浏览者的请求,如保存用户输入的信息,根据用户信息返回相关的资料等等。当客户端发送一个CGI请求给WEB服务器后,WEB服务器将根据CGI程序的类型决定数据向CGI程序的传送方式,一般来讲是通过标准输入/输出流和环境变量来与CGI程序间传递数据。


CGI输入输出原理

CGI的输入/输出方法:CGI程序通过标准输入(STDIN)和标准输出(STDOUT)来进行输入输出,STDIN和STDOUT是两个预先定义好的文件指针。你可以利用文件读写函数来对其进行操纵。

此外CGI程序还通过环境变量来得到输入,只不过环境变量中提供的是一些常用的信息,并且通常不包括用户在WEB页面中输入的信息(除使用下面讲的GET方法时,通过检查环境变量QUERY_STRING来得到输入数据),而STDIN通常用来传递用户输入的信息。在普通CGI程序开发中我们需要关心的环境变量有以下这些:

一部分是与WEB服务器有关的环境变量:

  • SERVER_NAME WEB服务器名称
  • SERVER_PORT WEB服务器监听地址
  • SERVER_PROTOCOL 用于发送请求的协议名称和版本
  • SERVER_SOFTWARE WEB服务器名称和版本
一部分是与运行CGI有关的:
  • REQUEST_METHOD 数据传送(信息传递)方法
  • CONTENT_LENGTH 数据长度
  • QUERY_STRING 所传送的数据
  • REMOTE_ADDR 客户方IP地址
  • REMOTE_HOST 客户方主机名程
一部分是与客户方有关的:
  • HTTP_USER_AGENT 客户浏览器名称
  • HTTP_ACCEPT 客户机所能支持的MIME类型列表
  • HTTP_REFERER 客户机中前一文档的URL

 

在输入时所使用的POST/GET方法:在WEB页面向CGI发送数据时通常采用两种方法:GET/POST,GET方法将数据附加在URL后发送,如:/cgi/a_cgi_test.exe?your_data,CGI程序通过检查环境变量QUERY_STRING来得到输入数据。而POST方法则会将数据送入CGI程序的STDIN输入流。在表单(FORM)中的各个变量都会成为name=value的形式向WEB服务器发送,多个数据间用&分隔,如:name=value&name2=value2。其中名字(name,name2)是Form中定义的INPUT、SELECT或TEXTAREA等标置(Tag)名字,值是用户输入或选择的标置值。

有了上面的知识我们就可以马上写一个简单的CGI程序了。代码如下:

void main(void)
{// 本程序将用户输入的数据打印出来
	fprintf(stdout,"content-type: text/plain/n/n");
	// 输出一个CGI标题,这行代码的意义后面会讲解

	char *pszMethod;
	pszMethod = getenv("REQUEST_METHOD");
	if(strncmp(pszMethod,"GET") == 0)
	{// GET method
	//读取环境变量来获取数据
		fprintf(stdout,"input data is :/n%s",getenv("QUERY_STRING"));
	}
	else
	{// POST method
	//读取STDIN来获取数据
		int iLength=atoi(getenv("CONTENT_LENGTH"));
		fprintf(stdout,"input data is :/n");
		for(int i=0;i<iLength;i++)
		{
			char cGet=fgetchar(stdin);
			fputchar(stdout,cGet);
		}
	}
}

 

如上面说讲,在CGI程序输出时必须先输出一个CGI标题,标题共有以下三类:

  • Location: 标题,指明输出另一个文档的URL,例如 fprintf(stdout,"Location: http://www.vchelp.net//n/n");
  • Content-Type: 标题,指明发送的数据的MIME类型,例如 fprintf(stdout,"Content-Type: text/html/n/n");
  • Status: 标题,指明HTTP状态码,例如 fprintf(stdout,"Status: 200/n/n");
注意每种标题后都必须跟一个换行和一个空行。

 

MIME类型以类型/子类型的形式来表示,下面是一些常用的类型/子类型的组合:

  • Text/plain 普通文本类型
  • Text/html HTML格式的文本类型
  • Audio/basic 八位声音文件格式,后缀为.au
  • Video/mpeg MPEG文件格式
  • Video/quicktime QuickTime文件格式
  • Image/gif GIF图形文件
  • Image/jpeg JPEG图形文件
  • Image/x-xbitmap X bitmap图形文件,后缀为.xbm

 

有了上面的知识我们就可以写出一些CGI程序,首先需要对输入数据进行分析,方法为:每当找到字符=,标志着一个Form变量名字的结束;每当找到字符& ,标志着一个Form变量值的结束。请注意输入数据的最后一个变量的值不以&结束。这样我们可以将输入数据分解为一组一组的指。

但随后会发现CGI的输入并不规则,例如有时会出现类似下面格式的输入字符号串:filename=hello&cmd=world+I%27,这是因为浏览器对一些上传的特殊字符进行了编码,所以在将数据分解开后需要进行解码,解码规则为:+:将+转换成空格符;%xx:用其十六进制ASCII码值表示的特殊字符(%做为转意符)。根据值xx将其转换成相应的ASCII字符。对Form变量名和变量值都要进行这种转换。下面是一个对Form数据进行分析并将结果回送给Web服务器的CGI程序。

  #include 
      
      
  #include 
      
      
  #include 
      
      
  int htoi(char *);
  main()
  {
   int i,n;
  char c;
  printf (″Contenttype: text/plain/n/n″);
  n=0;
  if (getenv(″CONTENT-LENGTH″))
   n=atoi(getenv(″CONTENT-LENGTH″));
  for (i=0; i<n;i++){
   int is-eq=0;
  c=getchar();
  switch (c){
   case ′&′:
    c=′/n′;
    break;
   case ′+′:
    c=′ ′;
    break;
   case ′%′:{
    char s[3];
    s[0]=getchar();
    s[1]=getchar();
    s[2]=0;
    c=htoi(s);
    i+=2;
   }
   break;
  case ′=′:
   c=′:′;
   is-eq=1;
   break;
  };
  putchar(c);
  if (is-eq) putchar(′ ′);
  }
  putchar (′/n′);
  fflush(stdout);
  }
  /* convert hex string to int */
  int htoi(char *s)
  {
   char *digits=″0123456789ABCDEF″;
  if (islower (s[0])) s[0]=toupper(s[0]);
  if (islower (s[1])) s[1]=toupper(s[1]);
  return 16 * (strchr(digits, s[0]) -strchr (digits,′0′))
  		+(strchr(digits,s[1])-strchr(digits,′0′));
  }
上面的程序首先输出一个MIME头信息给Web服务器,检查输入中的字符数,并循环检查每一个字符。当发现字符为&时,意味着一个名字/值对的结束,程序输出一个空行;当发现字符为+时,将它转换成空格; 当发现字符为%时,意味着一个两字符的十六进制值的开始,调用htoi()函数将随后的两个字符转换为相应的ASCII字符;当发现字符为=时,意味着一个名字/值对的名字部分的结束,并将它转换成字符:。最后将转换后的字符输出给Web服务器。

 

开发CGI程序可以按照下面的步骤进行:
1、判断数据输入方法为GET或是POST。
2、读取数据,根据分隔符号&分解每个接收的表单变量,并同时对数据进行解码。
3、处理数据。
4、输出CGI标题,输出HTML数据。
5、退出。

利用C语言开发CGI需要自己对输入的数据进行分析,但字符号串处理并非C语言的强项,所以我向大家推荐一套我认为比较不错的开发包,CGIC,(由http://www.boutell.com/boutell/免费提供)。我对开发包中所提供的文件进行了少量的修改,并用VC6编译成为LIB。下载后可以看看该开发包所提供的说明,该说明很详细不但给出例子代码而且对各个函数都有详细的解说。

返回

版权所有 闻怡洋 http://www.vchelp.net/


1.2 一种更亲切的CGI开发系统WinCGI

CGI的开发在某种角度来说过于复杂,主要是体现在获取环境变量和分解/解码数据上面。在Windows系统中可以使用WinCGI系统来进行开发。WinCGI的优点就在于减轻了开发者分解/解码数据的负担,而且提供统一的输入输出方式供使用。

在WinCGI程序执行时第一个参数会是一个文件名称,如:yourCGI.exe para.txt,在该文件中记录了所有的环境变量信息和参数信息,而且该文件的结构与INI文件结构相同,所以利用Windows提供的标准函数:GetPrivateProfileString就可以取出所有的参数。而且文件中的数据全部已经经过解码,你可以直接使用。

在参数文件中有以下几个区(Section)

  • [CGI]
  • [Accept]
  • [System]
  • [Extra Headers]
  • [Form Literal]
  • [Form External]
  • [Form File]
  • [Form Huge]
文件格式如同:
[CGI]
CGI Version=CGI/1.2 (Win)
Query String=XXXXXX
[Section]
key=value
key=value
...

在这个参数文件中对我们特别有用的是以下一些数据:

 

[CGI]区中的各个关键字记录了系统的参数和对方浏览器和对方IP地址等等信息。下面列出一些常用的关键字:

  • Request Method:传送数据的方式,如果传送方式为GET,你也可以通过检查Query String关键字的值来取得传送来的数据,否则就需要通过读取[Form Literal]区来取得输入。
  • Request Protocol:所使用的协议和版本,如HTTP/1.0。
  • From:浏览器使用者的邮件地址。
  • User Agent:浏览器名称,如Netscape,Internet Explorer。
  • Content Type:上传数据的类型,格式如type/subtype
  • Content Length:上传数据的长度
  • Content File:当数据过长时通过写如该关键字所指明的文件来保存数据。
  • Server Name:WEB服务器的网络地址名称。
  • Server Software:WEB服务器的软件名称和版本。
  • Server Port:WEB服务器监听的端口。
  • CGI Version:服务器上CGI系统的版本。
  • Remote Address:客户端的IP地址。

 

[CGI]区中的各个关键字记录了系统的参数和对方浏览器和对方IP地址等等信息。如果传送方式为GET,则需要检查Query String关键字的值。

[ACCEPT]区中的关键字记录了客户方浏览器可以接受的数据类型。所以可以看出[CGI]区和[ACCEPT]区中的信息和普通CGI中的环境变量的值没有区别。

[SYSTEM]区是一个很重要的区,GMT Offset关键字表示客户方的时区,Output File关键字为输出文件的文件名称,因为在WinCGI中你不需要直接向输出流中写数据,取而代之的是使用临时文件来保存数据,在CGI程序退出后WinCGI系统会将该文件的内容返回给客户并删除该文件(同时也删除调用过程中的所有临时文件)。

接下来的[Form Literal]区包含有经过解码的数据。原本格式为:key=value&key=value&...的数据经过解码后被存放于这个区,但是一些例外的情况就要使用[Form External]和[Form Huge]区。在[Form External]区中数据格式如同:key=pathname length,当上传的字符串长度大于254时,就会使用这个区,pathname指明了包含被解码后的字符存放的文件,length为字符串长度。当上传的数据长度大于64K时,数据会被存放入一个独立的文件,该文件的文件名称由[System]区中的Content File关键字指明。[Form Huge]区中数据格式如同:key=offset length其中offset为该数据在文件中的起始位置,length为数据的长度。

下面有一个例子:设输入为,a_cgi_sample.exe?name=your_name&email=your@mail.com&memo=length_gt_254
则文件中包含数据的几个区形式如下:

[Form Literal]
name=your_name
email=your@mail.com
[Form External]
;假设memo字段长度为500
memo=a_temp_file 500

 

最后给出一个WinCGI程序的伪代码轮廓:

void main(int argc,char** argv)
{
	if(argc == 1)
		error raise; //无法得到配置文件
	char szCfgFile[]=argv[1];
	Read [Form Literal] section for input data
	char szOutFile[]=Read "Output File" key from [System] section;
	fopen(szOutFile)
	{
		write result to outFile
		close outFile
	}
	return ;//退出
}

 

总的说来,WinCGI程序与普通CGI的程序的区别不大,只是在输入输出的方法上略有区别。

返回

版权所有 闻怡洋 http://www.vchelp.net/


1.3 利用ISAPI开发CGI程序

ISAPI(Internet Server API)最初是微软为IIS服务器所提供的一种CGI应用开发接口,其主要的目的也是为CGI开发提供好的开发接口,如果不负责的讲ISAPI也可以认为是类似WinCGI的一种开发模式,只不过ISAPI通过映射宏来取得用户表单传送的参数。这一点上和MFC消息映射宏很相似。

当然ISAPI的推出同时还具备了很多其他的特性:

  • ISAPI以DLL动态连接库的方式实现,所以加载较普通的EXE运行程序快,而且系统不会在使用完后马上清除掉DLL在内存中的空间,所以再次使用时会获得更快的速度。
  • ISAPI在调用者的内部以线程方式运行,所以和CGI进程相比较需要的运行空间也更小。
  • 在同一个DLL中可以集中多个处理函数,并且通过映射宏来指明不同的函数分别对什么样的请求来进行处理。
  • 由于与IIS的集成,所以可以利用ISAPI开发ISAPI过滤器。通过过滤器可以完成例如用户权限检测,数据加密,压缩,日志等功能,IIS服务器的功能也可以通过ISAPI过滤器来得到增强。
到目前为止,很多非M$的WEB服务器也都添加了对ISAPI的支持。

 

在同一个DLL中可以集中多个处理函数,所以执行ISAPI的请求和执行普通CGI的请求有一点区别,在URL中需要填写如下形式http://.../cgi-bin/test.dll?function_name&name=xxx&email=yyy,function_name表示的就是功能名称,为了能够正确处理你必须将其映射到DLL中的一个处理函数上。对与未映射的功能都将由一个默认函数处理。

VC4.2以上版本为创建一个ISAPI程序提供了向导程序,在新建工程时选择ISAPI Extension Wizard并在以后的对话框中选择创建服务器扩展和输入相关的信息就可以了。

一个最简单的ISAPI程序至少包含了一个由CHttpServer类所派生的新类,并且在该类中进行了基本的映射,重载了BOOL GetExtensionVersion(HSE_VERSION_INFO* pVer)函数和提供了形式如void Default(CHttpServerContext* pCtxt)的成员函数。幸运的是向导程序已经为我们做好了这一切并创建了最基本的代码。

Default函数用于处理没有带参数首先我们修改Default函数中的代码,首先我们该写其中的相关代码成为如下:

void CTestisaExtension::Default(CHttpServerContext* pCtxt)
{
	//Print the <HTML> <BODY> tags.
	StartContent(pCtxt);

	//Print the title.
	WriteTitle(pCtxt);

	*pCtxt << _T("<p>演示</p>");
	*pCtxt << _T("<p>目前是Default成员函数其作用</p>");

	//Print </HTML> </BODY> tags.
	EndContent(pCtxt);
}
然后用以下方式调用:http://.../cgi-bin/test.dll?Default或http://.../cgi-bin/test.dll?。你就会看到输出的HTML页面了。

 

 

<FORM ACTION="test.dll?Add" METHOD=POST>
<INPUT NAME="name">
<INPUT NAME="id">
<INPUT TYPE=SUBMIT>
</FORM>
上面表单要求用户输入了用户名和ID号,所以传送的数据形式形式应该如同:http://.../test.dll?Add&name=xxx&id=yyy,我们可以通过处理命令映射宏来指明处理命令的函数,然后通过参数映射宏对数据进行分解,分解后的数据将会存放入指定的变量。使用方法为:
ON_PARSE_COMMAND(Add, CTestExtension, ITS_PSTR ITS_I4)
ON_PARSE_COMMAND_PARAMS("name id")
ON_PARSE_COMMAND宏中需要指明函数名,类名和参数类型列表,ON_PARSE_COMMAND_PARAMS宏中需要根据前一个宏中所列出的参数列表来指明表单中的变量名称列表。最后一步就是定义一个名称与请求URL中?后命令名称相同的成员函数,参数的设置要与ON_PARSE_COMMAND宏中的参数列表定义相符合,在这个例子中为:
void CTestExtension::Add(CHttpServerContext* pCtxt,LPTSTR pszName,int iID)。参数类型列表中可以使用下面的类型后面为该类型的说明:
  • ITS_EMPTY 无数据
  • ITS_PSTR 字符串LPCSTR
  • ITS_I2 short
  • ITS_I4 long
  • ITS_R4 float
  • ITS_R8 double

 

下面我们看一个更复杂的表单,表单定义如下:

<FORM ACTION="test.dll?Delete" METHOD=POST>
<INPUT NAME="name">
<INPUT NAME="month">
<SELECT NAME="mode">
	<OPTION VALUE=1>All
	<OPTION VALUE=2>Before
	<OPTION VALUE=3>After
<INPUT TYPE=HIDDEN NAME=pwd VALUE=xxx>
<INPUT TYPE=SUBMIT>
</FORM>
定义映射和处理函数如下:
ON_PARSE_COMMAND(Add, CTestExtension, ITS_PSTR ITS_I4 ITS_I4 ITS_PSTR)
ON_PARSE_COMMAND_PARAMS("name month mode pwd")
void CTestExtension::Add(CHttpServerContext* pCtxt,LPTSTR pszName,int iMonth,int iMode,LPTSTR pszPwd)。

 

ISAPI程序的输出:在所有的命令处理函数中第一个参数都是CHttpServerContext*指针,我们所有的输出都要通过它进行,在CHttpServerContext类中定义了<<操作符,通过该操作符我们可以方便的输出字符串,数字,二进制数据。输出的方法为:

*pCtxt<<"this is a string";
*pCtxt<<'c';
*pCtxt<<10;
可以看得出输出是很简单的,一个完整的输出过程应该是如同下面的形式:
	StartContent(pCtxt);//开始输出
	WriteTitle(pCtxt);//输出头部信息,相当于输出<TITLE>
	*pCtxt << "your string";
	EndContent(pCtxt);//结束输出

 

我们所使用的ISAPI扩展类是由CHttpServer所派生,可以调用CHttpServer::AddHeader来指明返回数据的类型,例如下面的代码演示了输出纯文本:

void CTestisaExtension::Default(CHttpServerContext* pCtxt)
{
	AddHeader(pCtxt, "Content-type = text/plain/r/n");
	(*pCtxt) << "Hello world!/r/n"; 
}
我们可以重载某些函数来达到加强控制的目的,可重载的函数有以下这些:
  • virtual LPCTSTR CHttpServer::GetTitle( ) const;返回<TITLE>部分信息
  • virtual BOOL CHttpServer::OnParseError( CHttpServerContext* pCtxt, int nCause );错误处理
  • virtual void CHttpServer::StartContent( CHttpServerContext* pCtxt ) const;输出<HTML><BODY>部分信息
  • virtual void CHttpServer::EndContent( CHttpServerContext* pCtxt ) const;输出</BODY></HTML>部分信息
  • virtual void CHttpServer::WriteTitle( CHttpServerContext* pCtxt ) const;输出<TITLE></TITLE>部分信息

 

此外我们可以在命令处理函数中利用CHttpServerContext指针来得到CGI中的相关环境变量,在CHttpServerContext中有一个成员变量m_pECB为下面的结构指针。

typedef struct _EXTENSION_CONTROL_BLOCK {
    DWORD     cbSize;                              //IN 该结构长度
    DWORD     dwVersion                            //IN 版本
    HCONN     ConnID;                              //IN 连接上下文
    DWORD     dwHttpStatusCode;                   //OUT 状态码
    CHAR      lpszLogData[HSE_LOG_BUFFER_LEN];    //OUT
    LPSTR     lpszMethod;                          //IN 环境变量REQUEST_METHOD
    LPSTR     lpszQueryString;                     //IN QUERY_STRING
    LPSTR     lpszPathInfo;                        //IN PATH_INFO
    LPSTR     lpszPathTranslated;                  //IN PATH_TRANSLATED
    DWORD     cbTotalBytes;                        //IN CONTENT_LENGTH
    DWORD     cbAvailable;                         //IN
    LPBYTE    lpbData;                             //IN
    LPSTR     lpszContentType;                     //IN CONTENT_TYPE
...其他信息在此忽略
} EXTENSION_CONTROL_BLOCK, *LPEXTENSION_CONTROL_BLOCK;

 

返回

版权所有 闻怡洋 http://www.vchelp.net/


1.4 利用WinInet开发Internet程序

M$所提供的WinInet开发包是作为ActiveX技术中的一部分,后来MS对WinInet API进行了包装并提供了相应的MFC类。WinNet开发包的作用是提供了对HTTP FTP Gopher协议的支持,使开发者在开发Internet程序时避免编写底层协议相关的代码。此外WinInet很多功能都是与IE有联系的,比如说可以使用IE中的设置和使用IE缓存中的数据。本节主要将内容集中在HTTP协议的使用上。

WinInet中的CInternetSession是管理会话的类,一般来讲如果要使用WinInet功能都需要建立一个会话,然后在该会话的基础上使用数据存取功能。CInternetSession的构造函数中接收四个参数

CInternetSession( LPCTSTR pstrAgent = NULL, //应用程序名,可以自己定义
	DWORD dwContext = 1, //上下文标记,如果使用回调功能时这个值将给传送给回调函数
	DWORD dwAccessType = INTERNET_OPEN_TYPE_PRECONFIG, //
	LPCTSTR pstrProxyName = NULL, //CERN代理服务器地址,一般设置为NULL
	LPCTSTR pstrProxyBypass = NULL, //代理服务器地址
	DWORD dwFlags = 0 );//标记,一般设置为0

 

dwAccessType可以为:

  • INTERNET_OPEN_TYPE_PRECONFIG 使用IE中的连接设置
  • INTERNET_OPEN_TYPE_DIRECT 直接连接到服务器
  • INTERNET_OPEN_TYPE_PROXY 通过代理服务器进行连接

 

当dwAccessType为INTERNET_OPEN_TYPE_PROXY时指明代理服务器地址。

dwFlags可以为:

  • INTERNET_FLAG_DONT_CACHE 不在缓存中保存取得的内容
  • INTERNET_FLAG_OFFLINE 脱机方式,
  •  

 

在建立会话后就可以利用CInternetSession::OpenURL打开一个URL,并读取数据,函数原型为:

CStdioFile* OpenURL( LPCTSTR pstrURL, //文件URL地址
		DWORD dwContext = 1, //上下文ID
		DWORD dwFlags = INTERNET_FLAG_TRANSFER_ASCII, //标记
		LPCTSTR pstrHeaders = NULL, //发送到服务器的数据头
		DWORD dwHeadersLength = 0 );//发送到服务器的数据头长度
dwFlags可以为:
  • INTERNET_FLAG_RELOAD 强制重读数据
  • INTERNET_FLAG_DONT_CACHE 不保存到缓存
  • INTERNET_FLAG_TRANSFER_ASCII 使用文本数据
  • INTERNET_FLAG_TRANSFER_BINARY 使用二进制数据

 

OpenURL的返回值为CStdioFile文件指针,函数会根据使用的协议返回不同的文件类指针,但这些类都是CStdioFile的派生类。

URL typeReturns
file://CStdioFile*
http://CHttpFile*
gopher://CGopherFile*
ftp://CInternetFile*
然后通过CStdioFile::ReadString来读取文件内容。

 

本文给出的例子就是利用WinInet通过HTTP协议读取网上文件。

完成这一功能的代码非常简单,当然代码中少了各种错误检测。

void CSp_14Dlg::GetURL(void)
{
	UpdateData();
	CInternetSession sess;//建立会话
	CHttpFile* pF=(CHttpFile*)sess.OpenURL(m_szURL);//打开文件
	CString szData,szAllData;
	while(pF->ReadString(szData))
	{//读取文件
		szAllData+="/r/n";
		szAllData+=szData;
	}
	pF->Close();
	sess.Close();
	m_szHTML=szAllData;
	UpdateData(FALSE);
}
下载示范代码

 

这里我只讲了最简单的应用,如果你需要更强的控制性或是发送表单(FORM),则需要生成CHttpConnection对象和CHttpFile对象。

 

 

 

返回

版权所有 闻怡洋 http://www.vchelp.net/

 
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐