当前位置:   article > 正文

【网络编程】Web服务器shttpd源码剖析——CGI支持实现

【网络编程】Web服务器shttpd源码剖析——CGI支持实现

 16b9d0dfc990426e968798e2f5a7628b.png

hello !大家好呀! 欢迎大家来到我的网络编程系列之web服务器shttpd源码剖析——CGI支持实现,在这篇文章中,你将会学习到在Linux内核中如何创建一个自己的并发服务器shttpd,并且我会给出源码进行剖析,以及手绘UML图来帮助大家来理解,希望能让大家更能了解网络编程技术!!!

希望这篇文章能对你有所帮助9fe07955741149f3aabeb4f503cab15a.png,大家要是觉得我写的不错的话,那就点点免费的小爱心吧!1a2b6b564fe64bee9090c1ca15a449e3.png

03d6d5d7168e4ccb946ff0532d6eb8b9.gif               

目录

一. CGI简介

1.1 什么是CGI?

1.2 CGI的使用方法

1.3  CGI脚本使用情况

二.shttpd中使用CGI详解

 2.1 使用管道进行进程通信

2.2 构建CGI执行程序的主要步骤

三.源码剖析

3.1 初始化以及参数属性确定

3.2 进程分叉以及数据处理


 

一. CGI简介

1.1 什么是CGI?

CGI(Common Gateway Interface,通用网关接口)是一种重要的互联网技术,它允许网页服务器运行外部程序来处理用户请求,并生成动态内容。CGI 是一种标准方法,用于 web 服务器传递用户请求给服务器上的应用程序,并接收应用程序的响应,然后将响应返回给用户。

在 CGI 工作流程中,通常发生以下步骤:

  1. 用户请求:用户通过浏览器发送请求给 web 服务器,请求可以是一个普通的页面请求,也可以是一个表单提交。

  2. 服务器响应:web 服务器接收到请求后,如果请求的是静态内容(如 HTML 文件),服务器直接返回内容。如果请求的是动态内容,服务器会根据请求的 URL 或其他信息确定需要运行哪个 CGI 程序。

  3. 运行 CGI 程序:服务器运行指定的 CGI 程序。这个程序可以是一个脚本(如 Perl、Python 或 Shell 脚本),也可以是一个编译过的程序(如 C 或 C++ 程序)。

  4. CGI 程序执行:CGI 程序执行所需的操作,如数据库查询、计算等,并生成输出,通常是 HTML 格式的数据。

  5. 服务器返回响应:CGI 程序的输出被 web 服务器接收,然后服务器将这些输出作为响应返回给用户的浏览器。

1.2 CGI的使用方法

要使用 CGI,你需要遵循以下步骤来设置你的 web 服务器和 CGI 程序:

  1. 编写 CGI 脚本

    • 使用 CGI 可用的编程语言(如 Perl、Python、Shell 脚本等)编写你的 CGI 脚本。
    • 脚本通常会处理来自 web 表单的数据,执行一些计算或数据库操作,并生成 HTML 输出。
  2. 配置 web 服务器

    • 在你的 web 服务器上配置 CGI 支持。不同的 web 服务器(如 Apache、Nginx)有不同的配置方法。
    • 设置好 CGI 执行目录,确保 web 服务器有权运行该目录下的脚本。
    • 配置服务器以识别哪些 URL 请求应该由 CGI 脚本处理。
  3. 设置正确的文件权限

    • 确保 CGI 脚本具有执行权限。在 Unix/Linux 系统中,你可以使用 chmod 命令来设置权限。
    • 如果脚本文件的所有者与运行 web 服务器的用户不同,你可能还需要调整文件的属主和属组。
  4. 测试 CGI 脚本

    • 将你的 CGI 脚本放置在配置好的 CGI 执行目录中。
    • 通过浏览器访问相应的 URL 来测试脚本是否正常工作。
  5. 调试和错误处理

    • 如果 CGI 脚本没有按预期工作,检查服务器的错误日志文件,这通常会提供有关错误原因的信息。
    • 使用浏览器开发者工具或网络分析工具来检查 HTTP 请求和响应,以确保数据正确传递。
  6. 安全和性能考虑

    • 考虑 CGI 脚本的安全性,确保输入数据得到验证和清洗,以防止 SQL 注入或其他安全漏洞。
    • 考虑性能优化,因为 CGI 通常比其他服务器端技术(如 FastCGI、WSGI、mod_php)慢。

1.3  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 脚本程序, 其代码如下:

  1. #!/bin/sh
  2. echo Content-type: text/plain
  3. echo/bin/date 

 第一行是个特殊的命令,告诉UNIX 系统这是个shell脚本;真实的情况是从这行开始的下一行, 这个脚本做两件事: 第一, 它输出行 Content-type:text/plain, 接着开始一个空行;第二,它调用 UNIX 系统时间 date 程序,输出日期和时间。脚本执行后输出如下:

Content-type: text/plainTue Dev 25 16:15:57 EDT 2008

二.shttpd中使用CGI详解

 2.1 使用管道进行进程通信

 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执行结果。

 

2.2 构建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进程执行并发送结果给主进程。

3.1 初始化以及参数属性确定

我们需要初始化变量,然后找到?或者其他结束符,作为CGI命令的字符串,然后我们需要对字符串进行解析,包括参数的确定,以及查看CGI命令的属性,查看是否为目录且可以执行。

  1. #define CGISTR "/cgi-bin/"//CGI目录的字符串
  2. #define ARGNUM 16 //CGI程序变量的最大个数
  3. #define READIN 0 //读出管道
  4. #define WRITEOUT 1 //写入管道
  5. /******************************************************
  6. 函数名:cgiHandler(struct worker_ctl *wctl)
  7. 参数:
  8. 功能:
  9. *******************************************************/
  10. int cgiHandler(struct worker_ctl *wctl)
  11. {
  12. struct conn_request *req = &wctl->conn.con_req;
  13. struct conn_response *res = &wctl->conn.con_res;
  14. //strstr(str1,str2);str1:被查找目标 str2:要查找对象 
  15. char *command = strstr(req->uri, CGISTR) + strlen(CGISTR);//获得匹配字符串/cgi-bin/
  16. char *arg[ARGNUM];
  17. int num = 0;
  18. char *rpath = wctl->conn.con_req.rpath;
  19. stat *fs = &wctl->conn.con_res.fsate;
  20. int retval = -1;
  21. char *pos = command;//查找CGI的命令
  22. for(;*pos != '?' && *pos !='\0';pos++);//找到命令尾
  23. {
  24. *pos = '\0';
  25. }
  26. sprintf(rpath, "%s%s",conf_para.CGIRoot,command);//构建全路径
  27. //查找CGI的参数
  28. pos++;
  29. for(;*pos != '\0' && num < ARGNUM;)
  30. { //CGI的参数为紧跟CGI命令后的?的字符串,多个变量之间用+连接起来,所以可以根据加号的个数确定参数的个数
  31. arg[num] = pos;//参数头
  32. for(;*pos != '+' && *pos!='\0';pos++);
  33. if(*pos == '+')
  34. {
  35. *pos = '\0';//参数尾
  36. pos++;
  37. num++;
  38. }
  39. }
  40. arg[num] = NULL;
  41. //命令的属性
  42. if(stat(rpath,fs)<0)
  43. {
  44. //错误
  45. res->status = 403;
  46. retval = -1;
  47. goto EXITcgiHandler;
  48. }
  49. else if((fs->st_mode & S_IFDIR) == S_IFDIR)
  50. {
  51. //是一个目录,列出目录下的文件
  52. GenerateDirFile(wctl);
  53. retval = 0;
  54. goto EXITcgiHandler;
  55. }
  56. else if((fs->st_mode & S_IXUSR) != S_IXUSR)
  57. {
  58. //所指文件不能执行
  59. res->status = 403;
  60. retval = -1;
  61. goto EXITcgiHandler;
  62. }

3.2 进程分叉以及数据处理

我们需要创建一个CGI进程来执行CGI程序,同时我们需要构建一个管道来进行主进程与CGI进程间通信,在CGI进程中,我们将客户端发送来的CGI脚本以及参数形成一个字符串,然后与管道写端绑定,然后主进程会接收到CGI进程发送的 标准数据,然后执行脚本。

代码如下:
 

  1. //进程分叉
  2. int pid = 0;
  3. pid = fork();
  4. if(pid < 0)//错误
  5. {
  6. res->status = 500;
  7. retval = -1;
  8. goto EXITcgiHandler;
  9. }
  10. else if(pid > 0)//父进程
  11. {
  12. close(pipe_out[WRITEOUT]);//关闭写端
  13. close(pipe_in[READIN]);//关闭读端
  14. //主进程从CGI的标准输出读取数据 ,并将数据发送到网络资源请求的客户端
  15. int size = 0;//这里初始化为0,怎么进入while循环?改为下面的情况
  16. int end = 0;
  17. //读取CGI进程数据
  18. size = read(pipe_out[READIN], res->res.ptr, sizeof(wctl->conn.dres));
  19. while(size > 0 && !end)
  20. {
  21. if(size > 0)
  22. {//将数据发送给客户端
  23. send(wctl->conn.cs, res->res.ptr, strlen(res->res.ptr));
  24. }
  25. else
  26. {
  27. end = 1;
  28. }
  29. size = read(pipe_out[READIN], res->res.ptr, sizeof(wctl->conn.dres));
  30. }
  31. wait(&end);//等待其子进程全部结束
  32. close(pipe_out[READIN]);//关闭管道
  33. close(pipe_in[WRITEOUT]);
  34. retval = 0;
  35. }
  36. else//子进程
  37. {
  38. char cmdarg[2048];
  39. char onearg[2048];
  40. char *pos = NULL;
  41. int i = 0;
  42. //形成执行命令
  43. memset(onearg, 0, 2048];
  44. for(i = 0;i<num;i++)
  45. sprintf(cmdarg,"%s %s", onearg, arg[i]);
  46. //将写入的管道绑定到标注输出
  47. close(pipe_out[READIN]); //关闭无用的读管道
  48. dup2(pipe_out[WRITEOUT], 1); //将写管道绑定到标准输出
  49. close(pipe_out[WRITEOUT]); //关闭写管道
  50. close(pipe_in[WRITEOUT]); // 关闭无用的写管道
  51. dup2(pipe_in[READIN], 0); // 将读管道绑定到标准输入
  52. close(pipe_in[READIN]); // 关闭写管道
  53. //execlp()会从PATH 环境变量所指的目录中查找符合参数file的文件名,
  54. //找到后便执行该文件,然后将第二个以后的参数当做该文件的argv[0]、
  55. //argv[1]……,最后一个参数必须用空指针(NULL)作结束
  56. execlp(rpath, arg);//执行命令,命令的输出需要为标准输出
  57. }
  58. EXITcgiHandler:
  59. return retval;
  60. }

   好啦!到这里这篇文章就结束啦,关于实例代码中我写了很多注释,如果大家还有不懂得,可以评论区或者私信我都可以哦4d7d9707063b4d9c90ac2bca034b5705.png!! 感谢大家的阅读,我还会持续创造网络编程相关内容的,记得点点小爱心和关注哟!2cd0d6ee4ef84605933ed7c04d71cfef.jpeg    

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/522908
推荐阅读
相关标签
  

闽ICP备14008679号