在上一章中我们一起看了如何实现静态的网页,在这里我们一起看Tinyhttpd最后的一部分,动态网页的实现:在这里首先声明下因为cgi脚本的支持问题,所以我会新建一个简单的cgi脚本然后将路径导向到这个脚本:
0.perl的配置:
sudo apt update
sudo apt install build-essential libssl-dev zlib1g-dev
sudo apt install perl
1.新增cgi脚本内容:
在color.cgi同级目录下新增一个temp.cgi的文件,然后内容如下:
#!/usr/bin/perl
use strict;
use warnings;print "Content-Type: text/html\n\n";
print "<html>\n";
print "<head><title>Simple CGI Script</title></head>\n";
print "<body>\n";
print "<h1>Hello, World!</h1>\n";
print "</body>\n";
print "</html>\n";
2.修改响应路径:
在index.html中将<FORM ACTION="color.cgi" METHOD="POST">替换成:
<FORM ACTION="temp.cgi" METHOD="POST">
3.运行httpd
然后再对话框内随便输入,点击submit即可看到替换后的效果:
4.execute_cgi函数:
这段代码是带有注释的execute_cgi函数,已经在实现cgi的重要逻辑中都加上了log。诸位可以自行选择直接看还是先看后面的简单解析。
void execute_cgi(int client, const char *path,const char *method, const char *query_string)
{char buf[1024];int cgi_output[2];int cgi_input[2];pid_t pid;int status;int i;char c;int numchars = 1;int content_length = -1;//确认请求方法buf[0] = 'A'; buf[1] = '\0';if (strcasecmp(method, "GET") == 0)while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */numchars = get_line(client, buf, sizeof(buf));else if (strcasecmp(method, "POST") == 0) /*POST*/{numchars = get_line(client, buf, sizeof(buf));while ((numchars > 0) && strcmp("\n", buf)){buf[15] = '\0';if (strcasecmp(buf, "Content-Length:") == 0)content_length = atoi(&(buf[16]));numchars = get_line(client, buf, sizeof(buf));}if (content_length == -1) {bad_request(client);return;}}else/*HEAD or other*/{}//pipe是创建一个匿名的双向管道。允许数据在父子进程或者相关联进程之前单向流动//cgi_output一般是一个整形数组,用来存放管道的两个文件描述符。如果调用成功,cgi_output[0]会存放管道的读段描述符,cgi_output[1]会存放管道的写段描述符//如果返回值小于0则表示pipe调用失败.if (pipe(cgi_output) < 0) {cannot_execute(client);return;}if (pipe(cgi_input) < 0) {cannot_execute(client);return;}//fork用于创建一个与调用进程几乎完全相同的子进程,如果调用成功,则返回子进程的进程ID,否则返回-1.if ( (pid = fork()) < 0 ) {cannot_execute(client);return;}sprintf(buf, "HTTP/1.0 200 OK\r\n");send(client, buf, strlen(buf), 0);//如果当前在子进程中则pid为0if (pid == 0) /* child: CGI script */{char meth_env[255];char query_env[255];char length_env[255];//dup2的作用是复制文件描述符。在这里他将将cgi_output[1]复制到标准输出,将cgi_input[0]复制到标准输入。//这意味着任何原本要输出到终端的输出现在都会重定向到管道中。这样就能在父子进程直接进行数据传递了dup2(cgi_output[1], STDOUT);dup2(cgi_input[0], STDIN);close(cgi_output[0]);close(cgi_input[1]);sprintf(meth_env, "REQUEST_METHOD=%s", 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);}//根据path执行脚本execl(path, NULL);exit(0);} else { /* parent *///在父进程显示cgi脚本close(cgi_output[1]);close(cgi_input[0]);//在post的情况下根据读取的客户端的输出将内容传递到子管道中if (strcasecmp(method, "POST") == 0)for (i = 0; i < content_length; i++) {recv(client, &c, 1, 0);write(cgi_input[1], &c, 1);}//将子进程的输出传递到客户端while (read(cgi_output[0], &c, 1) > 0)send(client, &c, 1, 0);close(cgi_output[0]);close(cgi_input[1]);waitpid(pid, &status, 0);}
}
5.execute_cgi函数简单解析:
主体的逻辑就是从pipe函数那里开始,创建一个子进程,父子进程的环境啥的都完全一样,然后再子进程里面打开cgi脚本,并且将脚本的输出通过dup2函数和cgi_output,cgi_input传递到父进程里面,然后由父进程传递到client(网页)中。
6.总结:
从精读1,2,3我们基本搞清楚了TinyHttpd这个项目的基本运行逻辑,以及静态网页动态网页显示的思路和逻辑。当然后面的cgi这个算是一个精简版讲解,受限于cgi的布置,我们也不太容易一睹原项目中cgi的风采,但是思路是一样的。其中对于异常情况的处理和思路也是值得我们去学习的,不过我并不是主修web服务这块的,所以目前这块我的理解不够深入,暂时也不打算这样深入。后面我将根据我阅读的TinyHttpd的心得,自己写一个简易版本的内容出来。各位也可以自己动手试试,相信各位自己动手写完后肯定是受益匪浅,收获满满。对于socket的理解和使用的逻辑也会更上一层楼,对于C++web服务感兴趣的也能自己手动开始由简入难慢慢丰富自己的功能。