赞
踩
hello !大家好呀! 欢迎大家来到我的网络编程系列之web服务器shttpd源码剖析——CGI支持实现,在这篇文章中,你将会学习到在Linux内核中如何创建一个自己的并发服务器shttpd,并且我会给出源码进行剖析,以及手绘UML图来帮助大家来理解,希望能让大家更能了解网络编程技术!!!
希望这篇文章能对你有所帮助
,大家要是觉得我写的不错的话,那就点点免费的小爱心吧!
![]()
目录
CGI(Common Gateway Interface,通用网关接口)是一种重要的互联网技术,它允许网页服务器运行外部程序来处理用户请求,并生成动态内容。CGI 是一种标准方法,用于 web 服务器传递用户请求给服务器上的应用程序,并接收应用程序的响应,然后将响应返回给用户。
在 CGI 工作流程中,通常发生以下步骤:
用户请求:用户通过浏览器发送请求给 web 服务器,请求可以是一个普通的页面请求,也可以是一个表单提交。
服务器响应:web 服务器接收到请求后,如果请求的是静态内容(如 HTML 文件),服务器直接返回内容。如果请求的是动态内容,服务器会根据请求的 URL 或其他信息确定需要运行哪个 CGI 程序。
运行 CGI 程序:服务器运行指定的 CGI 程序。这个程序可以是一个脚本(如 Perl、Python 或 Shell 脚本),也可以是一个编译过的程序(如 C 或 C++ 程序)。
CGI 程序执行:CGI 程序执行所需的操作,如数据库查询、计算等,并生成输出,通常是 HTML 格式的数据。
服务器返回响应:CGI 程序的输出被 web 服务器接收,然后服务器将这些输出作为响应返回给用户的浏览器。
要使用 CGI,你需要遵循以下步骤来设置你的 web 服务器和 CGI 程序:
编写 CGI 脚本:
配置 web 服务器:
设置正确的文件权限:
chmod
命令来设置权限。测试 CGI 脚本:
调试和错误处理:
安全和性能考虑:
CGI 脚本可能是一个脚本,或者一个二进制可执行程序,也就是说,它可能是一个编译好的程序、批命令文件或者其他可执行的东西。它的一个共同的特性是可以执行并将结果反馈回来。 CGI 脚本可以利用如下的两种方法使用:
1) 作为一个表单的 ACTION 的响应对象的URL。例如, 有一个脚本叫 Show _ Data, 它是一个指向 CGI 脚本的链接, 其 HTML 表示如下:
<A HREF=" http://192.168.1.100:8080/cgi-bin/showdate ">ShowtheDate</A>
一般情况下, CGI脚本都放在目录“/cgi-bin/”下,在许多 Web服务器中, 目录cgi-bin是仅能够放置CGI脚本的目录。当网络浏览器执行这个链接的时候,浏览器向客户端主机192.168.1.100发送请求,服务器接收到客户端的请求,然后执行CGI脚本,并将结果反馈回来。
2) 假设showdate 是服务器上的一个 CGI 脚本程序, 其代码如下:
- #!/bin/sh
- echo Content-type: text/plain
- echo/bin/date
第一行是个特殊的命令,告诉UNIX 系统这是个shell脚本;真实的情况是从这行开始的下一行, 这个脚本做两件事: 第一, 它输出行 Content-type:text/plain, 接着开始一个空行;第二,它调用 UNIX 系统时间 date 程序,输出日期和时间。脚本执行后输出如下:
Content-type: text/plainTue Dev 25 16:15:57 EDT 2008
Web服务器中的CGI是一段外部程序,它可以动态地生成代码,并可以接收输入的参数。支持CGI主要分为如下几个部分:
1)CGI运行程序和输入参数的分析;
2)一个进程运行CGI程序,将CGI程序的输出发给与客户端通信的进程;
3)与客户端通信的进程生成头部信息,并将CGI运行进程的输出发给客户端。
CGI程序及参数的分析用于得到CGI程序和CGI程序运行时的输入参数。例如对于一个请求htp:/localhost/add?a+b,在服务器端运行的CGI程序为 add, 参数为 a 和 b, 用于计算a、b之和。
一个完整的CGI程序执行过程如图18.所示:
在分析CGI程序和参数之后,需要建立进程间通信管道,便于执行CGI程序时接收CGI程序结果。然后进程分叉,主进程负责与客户端进行通信,先分析得到头部信息,然后与CGI执行程序进程通信,读取CGI执行的结果,最后关闭进程后退出。 执行CGI程序是一个相对来说比较复杂的设计,采用进程间的管道通信方式,来获得CGI程序的输出并发送到客户端。CGI执行程序的输出为标准输出,为了在主进程中能够获得CGI执行进程的输出,这里采用了进程间的管道通信方式并使用文件描述符的复制操作,将CGI执行进程中管道的一端与标准输出绑定起来,CGI程序的输出数据会进行管道,主程序可以在另外一端接收到CGI执行结果。
如图:
1)先建立管道
2)进程进行分叉,分为主进程和CGI进程,主进程负责与客户端通信,CGI进程负责执行
CGI程序。
在主进程中:
(s.1)关闭输入管道的写端,留下读端,这个管道另一端在CGI进程中与CGI 的标准输出绑定在一起。
(s.2)从管道中读取数据。
(s.3)将数据发送到客户端。
(s.4)如果数据结束等待CGI进程结束。
在CGI 进程中:
(c.1)关闭管道的读端,留下写端,这个管道与主进程中管道的读端相连,用于将CG执行结果发送给主进程。
(c.2)将此管道的写端与进程的标准输出绑定在一起。
(c.3) 关闭写管道
3)执行程序
管到构建过程如下:
CGI支持主要实现包括CGI命令的获取,CGI参数获取,进程管道间连接,主进程从CGI进程中读取数据和发送数据,CGI进程执行并发送结果给主进程。
我们需要初始化变量,然后找到?或者其他结束符,作为CGI命令的字符串,然后我们需要对字符串进行解析,包括参数的确定,以及查看CGI命令的属性,查看是否为目录且可以执行。
- #define CGISTR "/cgi-bin/"//CGI目录的字符串
- #define ARGNUM 16 //CGI程序变量的最大个数
- #define READIN 0 //读出管道
- #define WRITEOUT 1 //写入管道
- /******************************************************
- 函数名:cgiHandler(struct worker_ctl *wctl)
- 参数:
- 功能:
- *******************************************************/
- int cgiHandler(struct worker_ctl *wctl)
- {
- struct conn_request *req = &wctl->conn.con_req;
- struct conn_response *res = &wctl->conn.con_res;
- //strstr(str1,str2);str1:被查找目标 str2:要查找对象
- char *command = strstr(req->uri, CGISTR) + strlen(CGISTR);//获得匹配字符串/cgi-bin/
- char *arg[ARGNUM];
- int num = 0;
- char *rpath = wctl->conn.con_req.rpath;
- stat *fs = &wctl->conn.con_res.fsate;
- int retval = -1;
- char *pos = command;//查找CGI的命令
- for(;*pos != '?' && *pos !='\0';pos++);//找到命令尾
- {
- *pos = '\0';
- }
- sprintf(rpath, "%s%s",conf_para.CGIRoot,command);//构建全路径
- //查找CGI的参数
- pos++;
- for(;*pos != '\0' && num < ARGNUM;)
- { //CGI的参数为紧跟CGI命令后的?的字符串,多个变量之间用+连接起来,所以可以根据加号的个数确定参数的个数
- arg[num] = pos;//参数头
- for(;*pos != '+' && *pos!='\0';pos++);
- if(*pos == '+')
- {
- *pos = '\0';//参数尾
- pos++;
- num++;
- }
- }
- arg[num] = NULL;
- //命令的属性
- if(stat(rpath,fs)<0)
- {
- //错误
- res->status = 403;
- retval = -1;
- goto EXITcgiHandler;
- }
- else if((fs->st_mode & S_IFDIR) == S_IFDIR)
- {
- //是一个目录,列出目录下的文件
- GenerateDirFile(wctl);
- retval = 0;
- goto EXITcgiHandler;
- }
- else if((fs->st_mode & S_IXUSR) != S_IXUSR)
- {
- //所指文件不能执行
- res->status = 403;
- retval = -1;
- goto EXITcgiHandler;
- }

我们需要创建一个CGI进程来执行CGI程序,同时我们需要构建一个管道来进行主进程与CGI进程间通信,在CGI进程中,我们将客户端发送来的CGI脚本以及参数形成一个字符串,然后与管道写端绑定,然后主进程会接收到CGI进程发送的 标准数据,然后执行脚本。
代码如下:
- //进程分叉
- int pid = 0;
- pid = fork();
- if(pid < 0)//错误
- {
- res->status = 500;
- retval = -1;
- goto EXITcgiHandler;
- }
- else if(pid > 0)//父进程
- {
- close(pipe_out[WRITEOUT]);//关闭写端
- close(pipe_in[READIN]);//关闭读端
- //主进程从CGI的标准输出读取数据 ,并将数据发送到网络资源请求的客户端
- int size = 0;//这里初始化为0,怎么进入while循环?改为下面的情况
- int end = 0;
- //读取CGI进程数据
- size = read(pipe_out[READIN], res->res.ptr, sizeof(wctl->conn.dres));
- while(size > 0 && !end)
- {
- if(size > 0)
- {//将数据发送给客户端
- send(wctl->conn.cs, res->res.ptr, strlen(res->res.ptr));
- }
- else
- {
- end = 1;
- }
- size = read(pipe_out[READIN], res->res.ptr, sizeof(wctl->conn.dres));
- }
- wait(&end);//等待其子进程全部结束
- close(pipe_out[READIN]);//关闭管道
- close(pipe_in[WRITEOUT]);
- retval = 0;
- }
- else//子进程
- {
- char cmdarg[2048];
- char onearg[2048];
- char *pos = NULL;
- int i = 0;
- //形成执行命令
- memset(onearg, 0, 2048];
- for(i = 0;i<num;i++)
- sprintf(cmdarg,"%s %s", onearg, arg[i]);
- //将写入的管道绑定到标注输出
- close(pipe_out[READIN]); //关闭无用的读管道
- dup2(pipe_out[WRITEOUT], 1); //将写管道绑定到标准输出
- close(pipe_out[WRITEOUT]); //关闭写管道
-
- close(pipe_in[WRITEOUT]); // 关闭无用的写管道
- dup2(pipe_in[READIN], 0); // 将读管道绑定到标准输入
- close(pipe_in[READIN]); // 关闭写管道
- //execlp()会从PATH 环境变量所指的目录中查找符合参数file的文件名,
- //找到后便执行该文件,然后将第二个以后的参数当做该文件的argv[0]、
- //argv[1]……,最后一个参数必须用空指针(NULL)作结束
- execlp(rpath, arg);//执行命令,命令的输出需要为标准输出
- }
- EXITcgiHandler:
- return retval;
- }

好啦!到这里这篇文章就结束啦,关于实例代码中我写了很多注释,如果大家还有不懂得,可以评论区或者私信我都可以哦
!! 感谢大家的阅读,我还会持续创造网络编程相关内容的,记得点点小爱心和关注哟!
![]()
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。