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

本文详细介绍了CGI的工作原理,探讨了shttpd服务器如何使用CGI处理用户请求,包括CGI脚本的执行流程、管道通信机制和源码中的关键步骤。通过实例代码解析,帮助读者掌握网络编程技术中的这一重要概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

#!/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

二.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命令的属性,查看是否为目录且可以执行。

#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;
    }

3.2 进程分叉以及数据处理

我们需要创建一个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;
}

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

评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值