示例项目

支持CGI赢博体育程序的web服务器

在计算机科学中广泛使用的设计模式是可扩展赢博体育程序,这种赢博体育程序提供了一些向赢博体育程序添加新特性的机制。使赢博体育程序具有可扩展性的最常见方法是提供插件机制,该机制使最终用户能够通过添加称为插件的特殊软件组件来定制赢博体育程序。

Unix长期以来一直有支持模块化体系结构的赢博体育程序的传统。Unix中模块化的主要机制是一个进程启动另一个进程作为辅助进程的能力。早期的web服务器利用这种能力,通过添加称为CGI赢博体育程序的特殊辅助赢博体育程序,使自己能够被最终用户扩展。CGI是“通用网关接口”的缩写。CGI是一组规则,用于指定在特定情况下,helper赢博体育程序如何从web服务器卸载工作。

在今天的示例中,我们将修改我们构建的web服务器,使其能够使用外部CGI赢博体育程序来帮助进行表单处理。具体来说,我们将修改web服务器,将某些POST请求交给外部程序处理。

需要处理的简单表单

在扩展我们的web服务器以允许它与CGI赢博体育程序一起工作之后,我们将把一个简单的测试示例放在一起。该示例包含一个简单的网页,其中包含需要处理的表单和将接管处理的CGI赢博体育程序。

以下是网页的HTML代码:

<!DOCTYPE html> <head> <title>欢迎来到miniweb</title> </head> <body> <h1>Register</h1> <p>填写下面的表格注册我们的通讯。</p> <form action="http://localhost:8888/cgi/register" method="post">您的姓名:<input type="text" name="name"><br>您的email: <input type="email" name="email"><br> <input type="submit"> </form> </body>

这个页面中表单的action属性指向一个URL,其中包含/cgi/作为URL的一部分。我们将修改web服务器,以便当它收到一个POST请求的URL包含/cgi/ web服务器将该请求传递到一个cgi赢博体育程序,其名称出现在/cgi/之后的URL。

我们将在存储miniweb服务器赢博体育程序的目录中添加一个cgi文件夹,并使用该文件夹存储服务器可以调用的cgi赢博体育程序。在这个例子中,表单请求中的URL想要使用注册CGI赢博体育程序。

CGI赢博体育程序

CGI是一种规范,它为CGI赢博体育程序应该如何工作设置了规则。CGI规范指出,一个赢博体育程序想要处理一个web服务器的POST应该做以下事情:

  1. 赢博体育程序应该从读取环境变量开始CONTENT_LENGTH。该变量指定POST请求中正文的大小。
  2. 然后,赢博体育程序应该从标准输入中读取POST请求的正文。
  3. 赢博体育程序应该构造一个HTTP响应,并将其响应打印到标准输出。通常,响应将由一组报头和一个包含响应的HTML代码的正文组成。

下面是我们示例的CGI赢博体育程序的代码。这个示例CGI赢博体育程序为用户将一些信息放入数据库,然后返回一个响应,表明操作成功。

#include <stdio.h> #include <stdlib.h> #include <stdlib.h> #include <unistd.h> int main(void) {char* lengthStr;if ((lengthStr = getenv("CONTENT_LENGTH")) != NULL){//获取内容长度int length;sscanf (lengthStr,“% d”,长度);//从stdin char buffer[256]读取查询;读(STDIN_FILENO、缓冲、长度);Buffer [length] = '\0';//从查询字符串中分离出名称。Char * p = strchr(buffer, '&');*p = '\0';Char * name = buffer+5;P = strchr(name,'+');if(p != NULL) *p = ';//使响应体的字符内容[1024];sprintf(内容、“< !DOCTYPE html > \ r \ n”);sprintf(内容、“% s <头> \ r \ n”,内容);sprintf(内容、“% s <标题> < /标题>注册成功\ r \ n”,内容);sprintf(内容、“% s < /头> \ r \ n”,内容);sprintf(内容、“% s <身体> \ r \ n”,内容);sprintf(内容、“% s < p >你好,% s。< p>\r\n",内容,名称);sprintf(内容、“% s < / >身体”,内容);//发送响应printf("HTTP/1.1 200 OK\r\n");printf("连接:关闭\ r \ n ");printf(" content -length: %d\r\n", (int)strlen(content));printf (" content - type: text / html \ r \ n \ r \ n”);printf(“% s”、内容);。fflush (stdout);} else{//返回一个错误响应printf("HTTP/1.1 500内部服务器错误\r\n");printf("连接:关闭\ r \ n ");printf("内容长度:21 \ r \ n ");printf("内容类型:文本/平原\ r \ n \ r \ n”);printf(“出错了”);。fflush (stdout);}返回0;}

为了读取CONTENT_LENGTH环境变量的内容,赢博体育程序使用getenv()函数,该函数返回一个指向存储该变量值的字符数组的指针。一旦我们知道了浏览器发送给我们的主体的长度,我们就可以使用read()函数从STDIN_FILENO中读取那么多字节,STDIN_FILENO从标准输入中读取字节。

同样,将响应写入标准输出。这里一个额外的特殊步骤是调用fflush()。当我们写入标准输出时,通常不需要这个调用。但是,当此赢博体育程序运行时,其标准输出将连接到连接到浏览器的套接字。为了确保我们所写的输出通过网络刷新到浏览器,我们需要添加对fflush()的调用。

调用CGI赢博体育程序

为了给我们的web服务器添加运行CGI赢博体育程序的能力,我们需要对服务器的serverrequest()函数做一些修改。下面是更新后的serverrequest()的代码:

无效serverrequest (int fd) {char lineBuffer[256];//读取请求的第一行readLine(fd,lineBuffer,255);//获取方法和URL char方法[16];char url [128];sscanf (lineBuffer、“% s % s”方法,url);if(strcmp(method,"POST") == 0) {if(strncmp(url,"/cgi/",5) == 0){//读取赢博体育内容直到空白行。//获取内容长度。char contentLength [16];while(1) {readLine(fd,lineBuffer,255);if(strncmp(lineBuffer,"Content-Length:",15) == 0) {strcpy(contentLength,lineBuffer+16);} else if(lineBuffer[0] == '\r') break;} if (fork() == 0) {char* emptylist[] = {NULL};setenv("CONTENT_LENGTH", contentLength, 1);dup2 (fd, STDIN_FILENO);dup2 (fd, STDOUT_FILENO);执行(url+1,空列表,环境);}等(空);} else {handle404(fd);}} else{//尝试获取用户想要的文件char fileName[128];strcpy(文件名,“www”);strcat(文件名、url);int file = open(fileName,O_RDONLY);If (file == -1) {handle404(fd);} else {const char* responseStatus = "HTTP/1.1 200 OK\n";const char* responseOther = “连接:关闭\nContent-Type: text/html\n”;//获取文件char len[64]的大小;Struct stat;fstat(提交、办法);sprintf(len,"Content-Length: %d\n\n",(int) st.st_size);//发送报头write(fd,responseStatus,strlen(responseStatus));写(fd, responseOther strlen (responseOther));写(fd, len strlen (len));//发送文件字符缓冲区[1024];int bytesRead;while(bytesRead = read(file,buffer,1023)) {write(fd,buffer,bytesRead);}关闭(提交);}} close(fd);}

这段代码的一个重要变化是我们从浏览器读取请求的方式。由于我们希望将某些请求传递给CGI赢博体育程序,因此我们必须小心,不要意外地读取从浏览器获得的任何请求的正文。相反,我们要做的是逐行读取浏览器的请求,以确定需要对请求做什么。通过一次一行地读取请求,我们可以在读取标记报头结束的空行后停止。这将使body保持未读状态,以便CGI赢博体育程序可以继续读取body。

为了方便一次一行地读取请求,我编写了一个简单的readLine()函数,可以从输入流中读取一行文本:

void readLine(int fd,char* buffer,int maxBytes) {char* ptr = buffer;int bytes = 0;while(bytesRead < maxBytes) {read(fd,ptr,1);If (*ptr == '\n') break;ptr + +;} *(++ptr) = '\0';}

此函数使用通常的read()函数从套接字中每次读取一个字节。我们每次读取一个字符,直到遇到标记一行结束的“\n”。

serverrequest()首先读取请求的第一行,其中包含请求的方法和URL。然后我们设置了一些if-else语句来处理两种简单的情况:

  1. 如果方法是POST并且URL以/cgi/开头,这是一个针对cgi赢博体育程序的POST请求。我们安排将请求传递给CGI赢博体育程序。
  2. 如果方法是GET,我们继续尝试以通常的方式处理请求。

下面是serverrequest()处理CGI POST请求的部分:

//读取赢博体育内容直到空行。//获取内容长度。char contentLength [16];while(1) {readLine(fd,lineBuffer,255);if(strncmp(lineBuffer,"Content-Length:",15) == 0) {strcpy(contentLength,lineBuffer+16);} else if(lineBuffer[0] == '\r') break;} if (fork() == 0) {char* emptylist[] = {NULL};setenv("CONTENT_LENGTH", contentLength, 1);dup2 (fd, STDIN_FILENO);dup2 (fd, STDOUT_FILENO);执行(url+1,空列表,环境);}等(空);

该代码以一个循环开始,该循环逐行读取标头,直到到达标记标头结束的空行。在此过程中,循环将忽略几乎赢博体育的头文件。它将读取的唯一标头是Content-Length标头:我们需要将该值传递给CGI赢博体育程序。

要启动CGI赢博体育程序,我们将首先执行fork()。调用fork()产生的子进程将启动CGI赢博体育程序。在启动赢博体育程序之前,我们使用setenv()函数编写CONTENT_LENGTH环境变量。然后使用dup2()函数将套接字从浏览器连接到标准输入和标准输出。我们这样做是因为CGI赢博体育程序总是尝试从标准输入读取其输入,并将其输出写入标准输出。

设置好一切之后,我们通过调用execute()来启动CGI赢博体育程序,这将启动赢博体育程序并使赢博体育程序接管当前正在运行的子进程。一旦CGI赢博体育程序完成它的工作并退出,子进程将停止。

回到父进程,我们对wait()函数发出调用,这将导致服务器等待,直到CGI赢博体育程序完成其工作。一旦发生这种情况,我们可以继续并关闭与浏览器的连接。

您可以在教材第五章“运行一个新进程”一节中阅读更多关于execve()函数的信息。您可以在“等待被终止的子进程”一节中了解更多关于wait()函数的信息。