Web基础(二)CGI协议与实现

版权声明:本文为博主原创文章,转载请注明出处。http://blog.csdn.net/yingshukun https://blog.csdn.net/yingshukun/article/details/83957696

Web基础

1. CGI 协议

通用网关接口(Common Gateway Interface/CGI)是一种重要的互联网技术,可以让一个客户端,从网页浏览器向执行在网络服务器上的程序请求数据。CGI描述了服务器和请求处理程序之间传输数据的一种标准,即CGI是一种协议。

CGI是在1993年由美国国家超级计算机应用中心(NCSA)为NCSA HTTPd Web服务器开发的。这个Web服务器使用了UNIX shell 环境变量来保存从Web服务器传递出去的参数,然后生成一个运行CGI的独立的进程。
CGI服务器

CGI规范允许Web服务器执行外部程序,并将它们的输出发送给Web浏览器,CGI将Web的一组简单的静态超媒体文档变成一个完整的新的交互式媒体。通俗的讲CGI就像是一座桥,把网页和WEB服务器中的执行程序连接起来,它把HTML接收的指令传递给服务器的执行程序,再把服务器执行程序的结果返还给HTML页。CGI 的跨平台性能极佳,几乎可以在任何操作系统上实现。

实际上有多种方式可以执行CGI程序,但对http的请求方法来说,只有get和post两种方法允许执行CGI脚本(实际上post方法的内部本质还是get方法,只不过在发送http请求时,get和post方法对url中的参数处理方式不一样而已)。

常用于编写CGI的语言有perl、php、python等,实际上任何一种语言都能编写CGI,java也一样能写,但java的servlet完全能实现CGI的功能,且更优化、更利于开发。

1.1 特点

CGI方式在遇到连接请求(用户请求)时先要创建CGI的子进程,激活一个CGI进程,然后处理请求,处理完后结束这个子进程。所以用CGI方式的服务器有多少连接请求就会有多少CGI子进程,子进程反复加载是CGI性能低下的主要原因。当用户请求数量非常多时,会大量挤占系统的资源如内存,CPU时间等,造成效能低下。

1.2 CGI脚本工作流程

  1. 浏览器通过HTML表单或超链接请求指向一个CGI应用程序的URL
  2. 服务器收发到请求
  3. 服务器执行所指定的CGI应用程序
  4. CGI应用程序执行所需要的操作,通常是基于浏览者输入的内容
  5. CGI应用程序把结果格式化为网络服务器和浏览器能够理解的文档(通常为HTML网页)
  6. 网络服务器把结果返回到浏览器中

1.3 实现原理

一般情况下,服务器和CGI程序之间是通过标准输入输出来进行数据传递的,而这个过程需要环境变量的协作方可实现。每个CGI程序只能处理一个用户请求,所以在激活一个CGI程序进程时也创建了属于该进程的环境变量。

1.服务器将URL指向一个CGI应用程序
2.服务器为应用程序执行做准备
3.应用程序执行,读取标准输入和有关环境变量
4.应用程序进行标准输出

在这里插入图片描述

1.3.1 CGI 接口标准

接口标准 简述
标准输入 CGI程序像其他可执行程序一样,可通过标准输入(stdin)从Web服务器得到输入信息,如Form中的数据,这就是所谓的向CGI程序传递数据的POST方法。这意味着在操作系统命令行状态可执行CGI程序,对CGI程序进行调试。POST方法是常用的方法。
环境变量 操作系统提供了许多环境变量,它们定义了程序的执行环境,应用程序可以存取它们。Web服务器和CGI接口又另外设置了自己的一些环境变量,用来向CGI程序传递一些重要的参数。CGI的GET方法还通过环境变量QUERY_STRING向CGI程序传递Form中的数据。
标准输出 CGI程序通过标准输出(stdout)将输出信息传送给Web服务器。传送给Web服务器的信息可以用多种格式,通常是以纯文本或者HTML文本的形式,这样我们就可以在命令行状态调试CGI程序,并且得到它们的输出。

对于CGI程序来说,它继承了系统的环境变量。CGI的环境变量在CGI程序启动时初始化,在结束时销毁。当一个CGI程序不是被HTTP服务器调用时,它的环境变量几乎是系统环境变量的复制,而当这个CGI程序被HTTP服务器调用时,它的环境变量就会多出以下关于HTTP服务器、客户端、CGI传输过程等内容

与请求相关的环境变量
REQUEST_METHOD 服务器与CGI程序之间的信息传输方式。一般包括两种:POST和GET,但在写CGI程序时,最后还应考虑其他的情况
QUERY_STRING 采用GET时所传输的信息,包含URL中问号后面的参数
CONTENT_LENGTH 对于用POST递交的表单, 标准输入口的字节数
CONTENT_TYPE 指示所传来的信息的MIME类型。如表单是用POST提交为application/x-www-form-urlencoded,并且经过了URL编码;而在上传文件的表单中,则为 multipart/form-data
CONTENT_FILE 使用Windows HTTPd/WinCGI标准时,用来传送数据的文件名
PATH_INFO 路径信息。由浏览器通过GET方法发出
PATH_TRANSLATED CGI程序的完整路径名
SCRIPT_NAME 所调用的CGI程序的名字。它指向这个CGI脚本的路径, 是在URL中显示的(如, /cgi-bin/thescript)
与服务器相关的环境变量
GATEWAY_INTERFACE 服务器所实现的CGI版本。对于UNIX服务器, 是CGI/1.1.
SERVER_NAME CGI脚本运行时的主机名和IP地址
SERVER_PORT 服务器运行的TCP端口,通常Web服务器是80
SERVER_SOFTWARE 调用CGI程序的HTTP服务器的名称和版本号。如: CERN/3.0 或 NCSA/1.3.
与客户端相关的环境变量
REMOTE_ADDR 客户机的IP地址
REMOTE_HOST 客户机的主机名,该值不能被设置
ACCEPT 列出能被此请求接受的应答方式。即客户机所支持的MIME类型清单,内容如:“image/gif,image/jpeg”
ACCEPT_ENCODING 列出客户机支持的编码方式
ACCEPT_LANGUAGE 表明客户机可接受语言的ISO代码
AUTORIZATION 表明被证实了的用户
FORM 列出客户机的EMAIL地址
IF_MODIFIED_SINGCE 当用get方式请求并且只有当文档比指定日期更早时才返回数据
PRAGMA 设定将来要用到的服务器代理
REFFERER 指出连接到当前文档的文档的URL
USER_AGENT 客户端浏览器的信息

环境变量是一个保存用户信息的内存区。当客户端的用户通过浏览器发出CGI请求时,服务器就寻找本地的相应CGI程序并执行它。在执行CGI程序的同时,服务器把该用户的信息保存到环境变量里。接下来,CGI程序的执行流程是这样的:查询与该CGI程序进程相应的环境变量:第一步是request_method,如果是POST,就从环境变量的len,然后到该进程相应的标准输入取出len长的数据。如果是GET,则用户数据就在环境变量的QUERY_STRING里。

GET 通过在URL中嵌入的形式传递参数。对CGI程序而言,在GET请求中传递的参数要通过环境变量“QUERY_STRING”来接收。 1、参数的内容作为URL信息,用户可以看到;2、有大小的限制。
POST CGI程序从标准输入接收参数。与GET方法不同的是,参数的内容从URL信息中不能获得,对于大小也没有限制。 与GET方法问题1、2完全相反

1.POST
采用POST方法,那么来自客户端来的用户数据将存放在CGI进程的标准输入中,同时将用户数据的长度赋予环境变量中的CONTENT_LENGTH。客户端用POST方式发送数据有一个相应的MIME类型(通用Internet邮件扩充服务:Multi-purpose Internet Mail Extensions)。目前,MIME类型一般为:application/x-wwww-form-urlencoded,该类型表示数据来自HTML表单,记录在环境变量CONTENT_TYPE中,CGI程序应该检查该变量的值。

2.GET
在该方法下,CGI程序无法直接从服务器的标准输入中获取数据,因为服务器把它从标准输入接收到的数据编码到环境变量QUERY_STRING(或PATH_INFO)中。

1.3.2 CGI程序实现

进入我们上一篇博客的zjhttpstatic目录中,可以看到一个最简单的CGI程序sayhi.c。将该程序编译后,命名为sayhi.cgi,运行zjhttp服务器,在浏览器输入http://localhost:7749/sayhi.cgi 即可测试

//sayhi.c
#include <stdio.h>

int main(){
	printf("Content-Type: text/html\n");
	printf("\n");
    printf("<html>");
    printf("<head>");
    printf("<title>CGI</title>");
    printf("</head>");
    printf("<body>");
	printf("I am a CGI program!\n");
    printf("</body>");
    printf("</html>\n");
	
	return 0;
}

再看一下从服务器获取数据示例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 将用户输入的数据打印出来
void main(void) {
    //输出一个CGI标题
    fprintf(stdout,"content-type:text/plain\n\n"); 

    char *pszMethod; 
    pszMethod = getenv("REQUEST_METHOD"); 
    if(strcmp(pszMethod,"GET") == 0) {         //GET method 
        //读取环境变量来获取数据 
        printf("This is GETMETHOD!\n");
        printf("SERVER_NAME:%s\n",getenv("SERVER_NAME")); 
        printf("REMOTE_ADDR:%s\n",getenv("REMOTE_ADDR")); 
        fprintf(stdout,"input data is:%s\n",getenv("QUERY_STRING"));
    } else {                                                   // POST method 
        //读取STDIN来获取数据
        int iLength = atoi(getenv("CONTENT_LENGTH"));
        printf("This is POSTMETHOD!\n");
        fprintf(stdout,"input data is:\n");
        
        for(int i=0; i < iLength; i++) {
            char cGet = fgetc(stdin);
            fputc(cGet,stdout);
        } 
    } 
}

POST 请求中获取数据

void unencode(char *src, char *last, char *dest){
  // str = hello+there%21 此处跳过data=... 
  // last = ; 已到末尾.
  // dest= ; 空串.
   //解码原则
   //原则1: '+'变' ';
   //原则2: '%xx'变成对应的16进制ASCII码值;
   for(; src != last; src++, dest++){
         if(*src == '+'){
                *dest = ' ';
         }else if(*src == '%'){
                int code;
                if(sscanf(src+1, "%2x", &code) != 1){
                        code = '?';
                }
                *dest = code;
                src +=2;
         }else{
                *dest = *src;
         }
   }

   *dest = '\n';
   *++dest = '\0';
}

int main(void){
       char *lenstr;
       char input[MAXINPUT], data[MAXINPUT];
       long len;
       printf("%s%c%c\n","Content-Type:text/html;charset=iso-8859-1",13,10);
       printf("<TITLE>Response</TITLE>\n");

       lenstr =getenv("CONTENT_LENGTH");
       printf("CONTENT_LENGTH =%s\n",lenstr);

       if(lenstr == NULL ||sscanf(lenstr,"%ld",&len)!=1 || len > MAXLEN){
              printf("<P>Error ininvocation - wrong FORM probably.");
       } else {
              FILE *f;
              fgets(input, len+1, stdin);//add by ycy从输入流中获取字符串.
              unencode(input+EXTRA, input+len,data);

              f = fopen(DATAFILE,"a");
              if(f == NULL){
                     printf("<P>Sorry,cannot store your data.");
              }else{
                     fputs(data, f); //add byycy 将数据存储在对对应的文件中.
              }
              fclose(f);
              printf("<P>Thank you!Your contribution has been stored.");
       }

       return 0;
}

不管是POST还是GET方式,客户端发送给服务器的数据都不是原始的用户数据,而是经过URL编码的。此时,CGI的环境变量Content_type将被设置,如Content_type = application/x-www-form-urlencode就表示服务器收到的是经过URL编码的包含有HTML表单变量数据。

编码的基本规则是:
变量之间用“&”分开;
变量与其对应值用“=”连接;
空格用“+”代替;
保留的控制字符则用“%”连接对应的16禁止ASCII码代替;
某些具有特殊意义的字符也用“%”接对应的16进制ASCII码代替;
空格是非法字符;
任意不可打印的ASCII控制字符均为非法字符

CGI 数据输出

CGI程序如何将信息处理结果返回给客户端?这实际上是CGI格式化输出。在CGI程序中的标准输出stdout是经过重定义了的,它并没有在服务器上产生任何的输出内容,而是被重定向到客户浏览器,这与它是由C,还是Perl或Python实现无关。所以,我们可以用打印来实现客户端新的HTML页面的生成。比如,C的printf是向该进程的标准输出发送数据,Perl和Python用print向该进程的标准输出发送数据。

  • CGI标题
    CGI的格式输出内容必须组织成标题/内容的形式。CGI标准规定了CGI程序可以使用的三个HTTP标题。标题必须占据第一行输出!而且必须随后带有一个空行。
标题 描述
Content_type (内容类型) 设定随后输出数据所用的MIME类型
Location (地址) 设定随后输出数据所用的MIME类型
Status (状态) 指定HTTP状态码
  • MIME
    向标准输出发送网页内容时要遵守MIME格式规则。任意输出前面必须有一个用于定义MIME类型的输出内容(Content-type)行,而且随后还必须跟一个空行。如果遗漏了这一条,服务将会返回一个错误信息。(同样使用于其他标题)
类型/子类型 描述
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

1.3.3 注意事项

LibCGI 是一个易于使用且功能强大的库,从头开始编写,以帮助在C中制作CGI应用程序。它支持字符串操作,链接列表,cookie,会话,GET和POST方法以及更多内容。

CGI请求
  • 服务器根据 以 / 分隔的路径选择解释器
  • 如果有 AUTH 字段,需要先执行 AUTH,再执行解释器
  • 服务器确认 CONTENT-LENGTH 表示的是数据解析出来的长度,如果附带信息体,则必须将长度字段传送到解释器
  • 如果有 CONTENT-TYPE 字段,服务器必须将其传给解释器;若无此字段,但有信息体,则服务器判断此类型或抛弃信息体
  • 服务器必须设置 QUERY_STRING 字段,如果客户端没有设置,服务端要传一个空字符串“”
  • 服务器必须设置 REMOTE_ADDR,即客户端请求IP
  • REQUEST_METHOD 字段必须设置, GET 、POST 等,大小写敏感
  • SCRIPT_NAME 表示执行的解释器脚本名,必须设置
  • SERVER_NAMESERVER_PORT 代表着大小写敏感的服务器名和服务器受理时的TCP/IP端口
  • SERVER_PROTOCOL 字段指示着服务器与解释器协商的协议类型,不一定与客户端请求的SCHEMA 相同,如’https://’ 可能为HTTP
  • CONTENT-LENGTH 不为 NULL 时,服务器要提供信息体,此信息体要严格与长度相符,即使有更多的可读信息也不能多传
  • 服务器必须将数据压缩等编码解析出来
CGI响应
  • CGI解释器必须响应 至少一行头 + 换行 + 响应内容
  • 解释器在响应文档时,必须要有 CONTENT-TYPE
  • 在客户端重定向时,解释器除了 client-redir-response=绝对url地址,不能再有其他返回,然后服务器返回一个 302 状态码
  • 解释器响应 三位数字状态码,具体配置可自行搜索
  • 服务器必须将所有解释器返回的数据响应给客户端,除非需要压缩等编码,服务器不能修改响应数据

2. zjhttp 代码详解

充分学习了CGI协议,了解了CGI的相关知识,接下来则可以详细的学习我们上一篇博客的zjhttp代码了

看到zjHttp.c中的execute_cgi函数,结合上面的CGI相关知识与注释,很容易理解CGI的原理。

/* 执行cgi动态解析 */
void execute_cgi(Client client, char *path, const char *method, const char *query_string) {
    char buf[1024];
    int numchars = 1;
    int content_length = -1;
    buf[0] = 'A'; buf[1] = '\0';
    if (StrCaseCmp(method, "GET") == 0) {                    /* 是GET请求,读取并丢弃头信息 */
        while ((numchars > 0) && strcmp("\n", buf))
            numchars = get_line(client, buf, sizeof(buf));
    }else {                                                 /* POST请求 */
        numchars = get_line(client, buf, sizeof(buf));
        while ((numchars > 0) && strcmp("\n", buf)) {       /* 循环读取头信息找到Content-Length字段值 */
            buf[15] = '\0';                                 /* 截取Content-Length: */

            if (StrCaseCmp(buf, "Content-Length:") == 0) content_length = atoi(&(buf[16]));/* 获取Content-Length的值 */
            numchars = get_line(client, buf, sizeof(buf));
        }
        if (content_length == -1) {
            bad_request(client);
            return;
        }
    }
    sprintf(buf, "HTTP/1.0 200 OK\r\n");                    /* 返回正确响应码200 */
    send(client, buf, strlen(buf), 0);
#ifdef _ZJ_WIN32
    CGI_ENV env;
    memset(&env, 0, sizeof(env));
    env.len = sizeof(env.buf);
    add_env(&env, "SYSTEMROOT", getenv("SYSTEMROOT"));
    add_env(&env, "REQUEST_METHOD", method);

    if (StrCaseCmp(method, "GET") == 0) {
        add_env(&env, "QUERY_STRING", query_string);
    }else {                        /* POST */
        add_env(&env, "CONTENT_LENGTH", content_length);
    }
    char abspath[MAX_PATH];
    GetModuleFileName(NULL, abspath, MAX_PATH);

    char *p = NULL;
    for (p = abspath + strlen(abspath); *p != '\\'; p--);
    *(++p) = '\0';

    for (p = path; *p != '\0'; p++) {
        if (*p == '/') *p = '\\';
    }

    strcat(abspath, path);
    printf("abspath=%s\n", abspath);
    createCgiProcess(client, env.buf, abspath, method, content_length);
#else
    int cgi_output[2];
    int cgi_input[2];
    pid_t pid;
    int status,i;
    char c;

    /* 必须在fork()中调用pipe(),否则子进程不会继承文件描述符
       pipe(cgi_output)执行成功后,cgi_output[0]为读通道 cgi_output[1]为写通道 */
    if (pipe(cgi_output) < 0) {
        cannot_execute(client);
        return;
    }
    if (pipe(cgi_input) < 0) {
        cannot_execute(client);
        return;
    }

    if ((pid = fork()) < 0) {
        cannot_execute(client);
        return;
    }
    /* fork出一个子进程运行cgi脚本 */
    if (pid == 0)  /* 子进程 */{
        char meth_env[255];
        char query_env[255];
        char length_env[255];

        dup2(cgi_output[1], 1);                            /* 1代表着stdout,0代表着stdin,将系统标准输出重定向为cgi_output[1] */
        dup2(cgi_input[0], 0);                             /* 将系统标准输入重定向为cgi_input[0] */

        close(cgi_output[0]);                              /* 关闭了cgi_output中的读通道 */
        close(cgi_input[1]);                               /* 关闭了cgi_input中的写通道 */
                            
                            
        sprintf(meth_env, "REQUEST_METHOD=%s", method);    /* CGI标准需要将请求的方法存储环境变量存储REQUEST_METHOD */
        putenv(meth_env);
        if (strcasecmp(method, "GET") == 0) {
            sprintf(query_env, "QUERY_STRING=%s", query_string);
            putenv(query_env);
        }else {   /* POST */
            sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
            putenv(length_env);
        }

        execl(path, path, NULL);                           /* 执行CGI脚本 */
        exit(0);
    }else {    /* 父进程 */
        close(cgi_output[1]);                              /* 关闭了cgi_output中的写通道,此处是父进程中cgi_output变量*/
        close(cgi_input[0]);                               /* 关闭了cgi_input中的读通道 */
        if (strcasecmp(method, "POST") == 0)
            for (i = 0; i < content_length; i++) {
                recv(client, &c, 1, 0);                    /* 开始读取POST中的内容*/
                write(cgi_input[1], &c, 1);                /* 将数据发送给cgi脚本 */
            }
        
        while (read(cgi_output[0], &c, 1) > 0)             /* 读取cgi脚本返回数据 */
            send(client, &c, 1, 0);

        close(cgi_output[0]);
        close(cgi_input[1]);
        waitpid(pid, &status, 0);
    }
#endif /* _ZJ_WIN32 */
}

2.1 C 与Linux 函数

接下来详细说明一下zjhttp服务器里一些函数的使用情况,在zjHttp.c中,accept_request函数里使用了strtok函数,该函数是标准库中的函数

头文件<string.h>
函数原型char * strtok(char *s, const char *delim);
参数s 指向将要分割的字符串,参数delim 为分割符,即以之做为分割的标志。当函数在参数s 的字符串中发现参数delim 符时则会将该字符改为\0 字符。在第一次调用时,strtok必需给予参数s 字符串,往后的调用则将参数s 设置成NULL。每次调用成功则返回下一个分割后的字符串指针。
返回值:返回下一个分割后的字符串指针,如果已无则返回NULL

#include <stdio.h>
#include <string.h>

int main(){
    char str[] = "python Golang C++ Java JavaScript";
    /* 以空格作为分割符分割字符串 */
    char *p = strtok(str, " ");
    printf("%s\n", p);
    while((p = strtok(NULL, " ")))
        printf("%s\n",p);

    return 0;
}

打印结果如下

python
Golang
C++
Java
JavaScript

头文件<sys/stat.h>
函数原型int stat(const char *file_name, struct stat *buf);
通过文件名filename获取文件信息,并保存在buf所指的结构体stat中
返回值:执行成功则返回0,失败返回-1,错误代码存于errno(<errno.h>

struct stat{
    dev_t st_dev;               /* 设备编号(文件系统)*/
    ino_t st_ino;               /* 文件索引节点的编号 */
    mode_t st_mode;             /* 文件的类型和访问权限 */
    nlink_t st_nlink;           /* 硬链接计数*/
    uid_t st_uid;               /* 文件所有者的用户ID */
    gid_t st_gid;               /* 文件所有者的组ID*/
    dev_t st_rdev;              /* 设备编号(特殊文件) */
    off_t st_size;              /* 文件大小(B) */
    blksize_t st_blksize;       /* 块大小(文件系统的I/O 缓冲区大小,最佳I/O块大小)Windows中无此字段  */
    blkcnt_t st_blocks;         /* 文件的块数,Windows中无此字段*/
    time_t st_atime;            /* 最后访问时间*/
    time_t st_mtime;            /* 文件内容最后修改时间*/
    time_t st_ctime             /* 文件状态最后修改时间*/
};

在Linux中,st_mode字段的值比Windows要多不少

  • S_IXUSR:文件所有者具可执行权限
  • S_IXGRP:用户组具可执行权限
  • S_IXOTH:其他用户具可读取权限

头文件<unistd.h>
函数原型int pipe(int filedes[2]);
建立管道,并将文件描述符由参数filedes数组返回。
filedes[0]为管道里的读取端
filedes[1]则为管道的写入端。
返回值:若成功则返回零,否则返回-1,错误原因存于errno中
错误代码:
 - EMFILE 进程已用完文件描述符最大量
 - ENFILE 系统已无文件描述符可用。
 - EFAULT 参数 filedes 数组地址不合法

函数原型int execl(const char * path,const char * arg,....);
该函数的功能是加载一个新的程序替换掉当前的进程。它可以调用一个外部程序到当前的进程空间里,但不会产生一个新的进程。

参数path代表执行文件的路径,后面的不定参代表执行该文件时传递的argv(0)argv[1]……,最后一个参数必须用空指针(NULL)作结束。
返回值:如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno中。


头文件<sys/types.h><sys/wait.h>
函数原型pid_t waitpid(pid_t pid, int *status, int options);
使父进程等待,直到一个子进程结束或者该进程接收到了一个指定的信号为止。

pid:欲等待的子进程识别码

pid < -1 等待进程组号为pid绝对值的任何子进程
pid = -1 等待任何子进程,此时的waitpid()函数就退化成了普通的wait()函数
pid = 0 等待进程组号与目前进程相同的任何子进程,也就是说任何和调用waitpid()函数>的进程在同一个进程组的进程
pid > 0 等待进程号为pid的子进程

status:保存子进程的状态信息。如果不关心子进程为什么退出的话,可以传入空指针
waitpid:提供了一些另外的选项来控制waitpid()函数的行为。如果不想使用这些选项,则可以设为0。主要使用以下两个选项,参数可用“|”运算符连接

  • WNOHANG
    如果pid指定的子进程没有结束,则waitpid()函数立即返回0,而不是阻塞在这个函数上等待;如果结束了,则返回该子进程的进程号
  • WUNTRACED
    如果子进程进入暂停状态,则马上返回

返回值
如果执行成功,则返回子进程的进程号;如果有错误发生,则返回-1,失败原因存于errno中
错误代码:有三种

  • ECHILD:调用者没有等待子进程(wait),或是pid指定的进程或进程组不存在(waitpid)或者pid指定的进程组中没有那个成员是调用者的子进程
  • EINTR:函数被信号中断
  • EINVAL:waitpid的参数options是无效的

2.2 Windows API 编程

为了让我们的zjhttp能运行在WIndows平台,必然需要使用到Windows API,这里对用到的WIndows API做了详尽的说明,在条件编译中检测到_ZJ_WIN32宏的部分,包含了这些Windows API 的代码。

Windows API 在线手册

CreateProcess

Windows API 中用于创建进程的函数

BOOL CreateProcess (
LPCTSTR                lpApplicationName,
LPTSTR                 lpCommandLine,
LPSECURITY_ATTRIBUTES  lpProcessAttributes,
LPSECURITY_ATTRIBUTES  lpThreadAttributes,
BOOL                   bInheritHandles,
DWORD                  dwCreationFlags,
LPVOID                 lpEnvironment,
LPCTSTR                lpCurrentDirectory,
LPSTARTUPINFO          lpStartupInfo,
LPPROCESS_INFORMATION  lpProcessInformation
);
  • lpApplicationName
    指向一个NULL结尾的、用来指定可执行模块的字符串。
    这个字符串可以使可执行模块的绝对路径,也可以是相对路径,可以被设为NULL,在这种情况下,可执行模块的名字必须处于 lpCommandLine 参数的最前面并由空格符与后面的字符分开
  • lpCommandLine
    指向一个以NULL结尾的字符串,该字符串指定要执行的命令行。该参数可以为空,那么函数将使用lpApplicationName参数指定的字符串当做要运行的程序的命令行
  • lpProcessAttributes
    指向一个SECURITY_ATTRIBUTES结构体,这个结构体决定是否返回的句柄可以被子进程继承。如果lpProcessAttributes参数为空(NULL),那么句柄不能被继承
  • lpThreadAttributes
    同lpProcessAttribute,不过这个参数决定的是线程是否被继承.通常置为NULL
  • bInheritHandles
    指示新进程是否从调用进程处继承了句柄。如果参数的值为TRUE,调用进程中的每一个可继承的打开句柄都将被子进程继承。被继承的句柄与原进程拥有完全相同的值和访问权限
  • dwCreationFlags
    指定附加的、用来控制优先类和进程的创建的标志
  • lpEnvironment
    指向一个新进程的环境块。如果此参数为空,新进程使用调用进程的环境。一个环境块存在于一个由以NULL结尾的字符串组成的块中,这个块也是以NULL结尾的。每个字符串都是name=value的形式
  • lpCurrentDirectory
    指向一个以NULL结尾的字符串,这个字符串用来指定子进程的工作路径。这个字符串必须是一个绝对路径。如果该参数为空,新进程将使用与父进程的目录。这个选项是一个需要启动应用程序并指定它们的工作目录的shell程序的主要条件
  • lpStartupInfo
    指向一个用于决定新进程的主窗体如何显示的STARTUPINFO结构体
  • lpProcessInformation
    指向一个用来接收新进程的识别信息的PROCESS_INFORMATION结构体
  • 返回值
    如果函数执行成功,返回非零值。
    如果函数执行失败,返回零,可以使用GetLastError函数获得错误的附加信息
简单示例

hello.c

/* 编写hello.c作为子进程被调用 */

#include <windows.h>
#include <stdio.h>

int main(){
    long id = (long)GetCurrentProcessId();
    printf("hello,world! This is a child process. pid is %d\n",id);

    return 0;
}

parent.c

/* 编写parent.c作为父进程来创建子进程 */

#include <stdio.h>
#include <windows.h>

int main() {
    long id = (long)GetCurrentProcessId();
    STARTUPINFOW si;
    PROCESS_INFORMATION pi;

    memset(&si, 0, sizeof(si));
    memset(&pi, 0, sizeof(pi));

    si.cb = sizeof(si);
    CreateProcess(NULL, "hello.exe", NULL, NULL, FALSE, NULL,NULL, NULL, &si, &pi);
    
    printf("This is the parent process. pid is %d\n",id);
    return 0;
}

使用MinGW 编译源文件,生成可执行exe程序。运行时应当使hello.exe与parent.exe在同一目录下

gcc hello.c -o hello
gcc parent.c -o parent

CreateProcessCreateProcessACreateProcessW的区别
在Windows编程时,Windows提供了两套API,以便同时支持宽字符和窄字符。宽字符版本的函数/结构体等都以W结尾,窄字符的则以A结尾。

比如你写CreateProcess,则在宽字符版本中实际调用的是CreateProcessW,窄字符版本中调用的则是CreateProcessA。

实际上Windows的内部都采用宽字符,所以前者只是把字符串转换成宽字符格式,然后调用后者。

A为结尾的是 窄字符 即ASCII字符
W为结尾的是 宽字符 即Unicode

CreatePipe

管道(Pipe)是用于进程间通信的一段共享内存,创建管道的进程称为管道服务器,连接到一个管道的进程为管道客户机。一个进程在向管道写入数据后,另一进程就可以从管道的另一端将其读取出来。匿名管道(Anonymous Pipes)是在父进程和子进程间单向传输数据的一种未命名的管道,只能在本地计算机中使用,而不可用于网络间的通信。

在使用匿名管道通信时,父进程必须将其中的一个句柄传送给子进程。句柄的传递大多通过继承来完成,父进程也允许这些句柄为子进程所继承。

CreatePipe是Windows API 中用于创建匿名管道的函数。
匿名管道不允许异步操作,所以在一个管道中写入数据,且缓冲区已满,那么除非另一个进程从管道中读出数据,从而腾出了缓冲区的空间,否则写入函数会阻塞

BOOL WINAPI CreatePipe(
_Out_PHANDLE hReadPipe,
_Out_PHANDLE hWritePipe,
_In_opt_LPSECURITY_ATTRIBUTES lpPipeAttributes,
_In_DWORD nSize);
  • hReadPipe
    返回一个可用于读管道数据的文件句柄
  • hWritePipe
    返回一个可用于写管道数据的文件句柄
  • lpPipeAttributes
    传入一个SECURITY_ATTRIBUTES结构的指针,该结构用于决定该函数返回的句柄是否可被子进程继承。如果传NULL,则返回的句柄是不可继承的。
    该结构的lpSecurityDescriptor成员用于设定管道的安全属性,如果传NULL,那么该管道将获得一个默认的安全属性
  • nSize
    管道的缓冲区大小。只是一个建议值,系统会根据建议值计算出一个合适的值。如果nSize是0,使用缺省值
  • 返回值
    如果函数执行成功,返回值非0;如果失败,返回0。可以通过GetLastError获得更多的信息
typedef struct _SECURITY_ATTRIBUTES {
    DWORD nLength;                  /* 结构体的大小 */
    LPVOID lpSecurityDescriptor;    /* 安全描述符 */
    WINBOOL bInheritHandle;   /* 安全描述的对象能否被新创建的进程继承 */
  } SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;
匿名管道通信示例

在这里插入图片描述

parent.c

#include <stdio.h>
#include <windows.h>


int main() {
    char buf[1024];
    DWORD byteRead ,byteWrite;
    HANDLE readPipe1, writePipe1, readPipe2, writePipe2;
    SECURITY_ATTRIBUTES sat;
    STARTUPINFO si;
    PROCESS_INFORMATION pi;
    
    sat.nLength = sizeof(SECURITY_ATTRIBUTES);
    sat.bInheritHandle = TRUE;
    sat.lpSecurityDescriptor = NULL;
    
    /* 创建管道1。用于子进程的输出,父进程则读取 */
     if(!CreatePipe(&readPipe1, &writePipe1, &sat, NULL)){
         printf("Create Pipe Error!\n");
         return 1;
     }

     /* 设置子进程不继承管道1在父进程这一端的读取句柄 */
     if (!SetHandleInformation(readPipe1, HANDLE_FLAG_INHERIT, 0) ){
         printf("SetHandleInformation readPipe1 Error!\n");
         return 1;
     }
 
    /* 创建管道2。用于子进程的输入,父进程则写入 */
     if(!CreatePipe(&readPipe2, &writePipe2, &sat, NULL)){
        printf("Create Pipe Error!\n");
        return 1;
     }

     /* 设置子进程不继承管道2在父进程这一端的写入句柄 */
     if (!SetHandleInformation(writePipe2, HANDLE_FLAG_INHERIT, 0)){
         printf("SetHandleInformation readPipe2 Error!\n");
         return 1;
     }
       
    
    long id = (long)GetCurrentProcessId();
    printf("This is the parent process. pid is %d\n",id);
    
    memset(&si, 0, sizeof(si));
    memset(&pi, 0, sizeof(pi));

    si.cb = sizeof(si);                 /* 包含STARTUPINFO结构的字节数,应为结构体的大小 */
    si.wShowWindow = SW_HIDE;            /* 设定子应用程序初次调用的ShowWindow时,应用程序的第一个重叠窗口应该如何出现 */
    si.dwFlags |= STARTF_USESTDHANDLES; /* 包含该标志位,才能设置标准输入输出流 */
    si.hStdInput = readPipe2;            /* 将子进程的标准输入重定向到管道的读端 */
    si.hStdOutput = writePipe1;            /* 将子进程的标准输出重定向到管道的写端 */ 
    si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
    
    if(!CreateProcess(NULL, "hello.exe", NULL, NULL, TRUE, NULL, NULL, NULL, &si, &pi)){
        printf("create process error!\n");
        return 1;
    }
    
    char str[100] = "How are you? Greetings from father.";
    WriteFile(writePipe2, (LPCVOID)str, strlen(str) + 1, &byteWrite, NULL);

    memset(&buf, 0, sizeof(buf));
    if(ReadFile(readPipe1,buf,sizeof(buf),&byteRead,NULL)) {
        printf("The child told me:%s\n",buf);
    }

    CloseHandle(readPipe1);
    CloseHandle(readPipe2);
    CloseHandle(writePipe1);
    CloseHandle(writePipe2);
    return 0;
}

hello.c

#include <windows.h>
#include <stdio.h>

int main(){
    HANDLE hRead;
    DWORD dwRead;
    char buf[1024];
    
    /* 子进程通过调用GetStdHandle取得管道的读句柄 */
    hRead = GetStdHandle(STD_INPUT_HANDLE);
    /* hWrite = GetStdHandle(STD_OUTPUT_HANDLE); */
    memset(&buf, 0, sizeof(buf));
    if(ReadFile(hRead, (LPVOID)buf, sizeof(buf), &dwRead, NULL)){
        MessageBox(NULL,buf,"Tips",MB_OK);
    }
    
    long id = (long)GetCurrentProcessId();
    printf("hello,world! This is a child process. pid is %d\n",id);
    
    return 0;
}

*3. 拓展

3.1 FastCGI 协议

快速通用网关接口(Fast Common Gateway Interface/FastCGI)是一种让交互程序与Web服务器通信的协议。FastCGI是早期通用网关接口(CGI)的增强版本。
FastCGI致力于减少网页服务器与CGI程序之间交互的开销,从而使服务器可以同时处理更多的网页请求。

与为每个请求创建一个新的进程不同,FastCGI使用持续的进程来处理一连串的请求。这些进程由FastCGI服务器管理,而不是web服务器。 当进来一个请求时,web服务器把环境变量和这个页面请求通过一个socket比如FastCGI进程与web服务器(都位于本地)或者一个TCP connection(FastCGI进程在远端的server farm)传递给FastCGI进程。

FastCGI是从CGI发展改进而来的。传统CGI接口方式的主要缺点是性能很差,因为每次HTTP服务器遇到动态程序时都需要重新启动脚本解析器来执行解析,然后结果被返回给HTTP服务器。这在处理高并发访问时,几乎是不可用的。FastCGI像是一个常驻(long-live)型的CGI,它可以一直执行着,只要激活后,不会每次都要花费时间去fork一次(这是CGI最为人诟病的fork-and-execute 模式)。CGI 就是所谓的短生存期应用程序,FastCGI 就是所谓的长生存期应用程序。由于 FastCGI 程序并不需要不断的产生新进程,可以大大降低服务器的压力并且产生较高的应用效率。它的速度效率最少要比CGI 技术提高 5 倍以上。它还支持分布式的运算, 即 FastCGI 程序可以在网站服务器以外的主机上执行并且接受来自其它网站服务器来的请求。

FastCGI是语言无关的、可伸缩架构的CGI开放扩展,其主要行为是将CGI解释器进程保持在内存中并因此获得较高的性能。众所周知,CGI解释器的反复加载是CGI性能低下的主要原因,如果CGI解释器保持在内存中并接受FastCGI进程管理器调度,则可以提供良好的性能、伸缩性、Fail-Over特性等等。FastCGI接口方式采用C/S结构,可以将HTTP服务器和脚本解析服务器分开,同时在脚本解析服务器上启动一个或者多个脚本解析守护进程。当HTTP服务器每次遇到动态程序时,可以将其直接交付给FastCGI进程来执行,然后将得到的结果返回给浏览器。这种方式可以让HTTP服务器专一地处理静态请求或者将动态脚本服务器的结果返回给客户端,这在很大程度上提高了整个应用系统的性能。

在这里插入图片描述

3.1.1 特点

  1. 打破传统页面处理技术。传统的页面处理技术,程序必须与 Web 服务器或 Application 服务器处于同一台服务器中。这种历史已经早N年被FastCGI技术所打破,FastCGI技术的应用程序可以被安装在服务器群中的任何一台服务器,而通过 TCP/IP 协议与 Web 服务器通讯,这样做既适合开发大型分布式 Web 群,也适合高效数据库控制。
  2. 明确的请求模式。CGI 技术没有一个明确的角色,在 FastCGI 程序中,程序被赋予明确的角色(响应器角色、认证器角色、过滤器角色)。

3.1.2 FastCGI的工作流程

  1. Web Server启动时载入FastCGI进程管理器(以PHP-CGI或者spawn-cgi为例)
  2. FastCGI进程管理器自身初始化,启动多个CGI解释器进程(可见多个php-cgi)并等待来自Web Server的连接。
  3. 当客户端请求到达Web Server时,FastCGI进程管理器选择并连接到一个CGI解释器。Web server将CGI环境变量和标准输入发送到FastCGI子进程php-cgi。
  4. FastCGI子进程完成处理后将标准输出和错误信息从同一连接返回Web Server。当FastCGI子进程关闭连接时,请求便告处理完成。FastCGI子进程接着等待并处理来自FastCGI进程管理器(运行在Web Server中)的下一个连接。 在CGI模式中,php-cgi在此便退出。

3.1.3 实现原理

FastCGI规范 可以查看完整内容

FastCGI程序和Web服务器之间通过可靠的流式传输(Unix Domain Socket或TCP)来通信,相对于传统的CGI程序,有环境变量和标准输入输出,而FastCGI程序和Web服务器之间则只有一条socket连接来传输数据,所以它把数据分成以下多种消息类型

#define FCGI_BEGIN_REQUEST       1
#define FCGI_ABORT_REQUEST       2
#define FCGI_END_REQUEST         3
#define FCGI_PARAMS              4
#define FCGI_STDIN               5
#define FCGI_STDOUT              6
#define FCGI_STDERR              7
#define FCGI_DATA                8
#define FCGI_GET_VALUES          9
#define FCGI_GET_VALUES_RESULT  10
#define FCGI_UNKNOWN_TYPE       11
#define FCGI_MAXTYPE (FCGI_UNKNOWN_TYPE)
由web服务器向FastCGI程序传输的消息类型
FCGI_BEGIN_REQUEST 表示一个请求的开始
FCGI_ABORT_REQUEST 表示服务器希望终止一个请求
FCGI_PARAMS 是一种流记录类型,用于从Web服务器向应用程序发送键值对
FCGI_STDIN 对应CGI程序的标准输入,FastCGI程序从此消息获取 http请求的POST数据
FCGI_DATA 一种流记录类型,用于从Web服务器向应用程序发送任意数据。FCGI_DATA是第二个流记录类型,用于向应用程序发送其他数据
FCGI_GET_VALUES 旨在允许一组开放式变量
由FastCGI程序返回给web服务器的消息类型
FCGI_STDOUT 对应CGI程序的标准输出,web服务器会把此消息当作html返回给浏览器
FCGI_STDERR 对应CGI程序的标准错误输出, web服务器会把此消息记录到错误日志中
FCGI_END_REQUEST 表示该请求处理完毕
FCGI_UNKNOWN_TYPE 无法解析该消息类型

web服务器和FastCGI程序每传输一个消息,首先会传输一个8字节固定长度的消息头,这个消息头记录了随后要传输的这个消息的 类型,长度等等属性,消息头的结构体如下

struct FCGI_Header {
    unsigned char version;
    unsigned char type;
    unsigned char requestIdB1;
    unsigned char requestIdB0;
    unsigned char contentLengthB1;
    unsigned char contentLengthB0;
    unsigned char paddingLength;
    unsigned char reserved;
} ;

version 表示fastcgi协议版本
type 表示消息的类型,就是前面提到的多种消息类型之一,如 FCGI_BEGIN_REQUEST、FCGI_PARAMS 等等

requestIdB1requestIdB0 这两个字节组合来表示 requestId (对于每个请求web服务器会分配一个requestId), requestIdB1 是requestId的高八位,requestIdB0是低八位,所以最终的 requestId = (requestIdB1 << 8) + requestIdB0,因为是两个字节来表示,requestId最大取值为65535,
同理 contentLengthB1contentLengthB0 共同来表示消息体的长度,对于超过65535的消息体,可以切割成多个消息体来传输

paddingLength 为了使消息8字节对齐,提高传输效率,可以在消息上添加一些字节数来达到消息对齐的目的,paddingLength 为添加的字节数,这些字节是无用数据,读出来可以直接丢弃。

reserved 保留字段,暂时无用

对于 FCGI_BEGIN_REQUEST 和 FCGI_END_REQUEST 消息类型,fastcgi协议分别定义了一个结构体如下,而对于其他类型的消息体,没有专门结构体与之对应,消息体就是普通的二进制数据

struct FCGI_BeginRequestBody {
    unsigned char roleB1;
    unsigned char roleB0;
    unsigned char flags;
    unsigned char reserved[5];
} ;
 
struct  FCGI_EndRequestBody {
    unsigned char appStatusB3;
    unsigned char appStatusB2;
    unsigned char appStatusB1;
    unsigned char appStatusB0;
    unsigned char protocolStatus;
    unsigned char reserved[3];
};

FCGI_BeginRequestBody 的 roleB1 和 roleB0 两个字节组合指代 web服务器希望FastCGI程序充当的角色,目前FastCGI协议仅定义了三种角色

#define FCGI_RESPONDER  1
#define FCGI_AUTHORIZER 2
#define FCGI_FILTER     3

3.1.4 总结

在这里插入图片描述

web服务器和FastCGi程序之间消息发送流程

  1. web服务器向FastCGI程序发送一个 8 字节 type=FCGI_BEGIN_REQUEST的消息头和一个8字节 FCGI_BeginRequestBody 结构的 消息体,标志一个新请求的开始
  2. web服务器向FastCGI程序发送一个 8 字节 type=FCGI_PARAMS 的消息头 和一个消息头中指定长度的FCGI_PARAMS类型消息体
  3. 根据FCGI_PARAMS消息的长度可能重复步骤 2 多次,最终发送一个 8 字节 type=FCGI_PARAMS 并且 contentLengthB1 和 contentLengthB0 都为 0 的消息头 标志 FCGI_PARAMS 消息发送结束
  4. 以和步骤2、3相同的方式 发送 FCGI_STDIN 消息
  5. FastCGI程序处理完请求后 以和步骤2、3相同的方式 发送 FCGI_STDOUT消息 和 FCGI_STDERR 消息返回给服务器
  6. FastCGI程序 发送一个 type= FCGI_END_REQUEST 的消息头 和 一个8字节 FCGI_EndRequestBody 结构的消息体,标志此次请求结束

用c 编写一个简单的实现fastcgi协议的 Demo

官方提供的 Fastcgi Developer‘s kit

猜你喜欢

转载自blog.csdn.net/yingshukun/article/details/83957696