三、 测试

本次测试

使用了两台机器。一台Ubuntu的浏览器作为客户端,一台Redhat作为服务器端,其中Redhat是Ubuntu上基于VirtualBox的一台虚拟机。

IP地址信息如下:Ubuntu的vboxnet0:

<img src="http://jbcdn2.b0.upaiyun.com/2016/11/73d81eee84e5fe07de8e076ee6b5d25a.jpg" alt="图片 1" />

RedHateth0:

图片 2

RedHat主机编译项目:

图片 3

由于我们同事监听了8000和4444,所以有两个进程启动。

HTTP的首页:

图片 4

目录显示功能:

图片 5

HTTP GET页面:

图片 6

HTTPGET响应:

图片 7

从HTTP GET响应中我们观察URL,参数的确是通过URL传送过去的。

其中getAuth.c如下:

#include "wrap.h"  
#include "parse.h"  

int main(void) {  
    char *buf, *p;  
    char name[MAXLINE], passwd[MAXLINE],content[MAXLINE];  

    /* Extract the two arguments */  
    if ((buf = getenv("QUERY_STRING")) != NULL) {  
    p = strchr(buf, '&');  
    *p = '\0';  
    strcpy(name, buf);  
    strcpy(passwd, p+1);  
    }  

    /* Make the response body */  
    sprintf(content, "Welcome to auth.com:%s and %s\r\n<p>",name,passwd);  
    sprintf(content, "%s\r\n", content);  

    sprintf(content, "%sThanks for visiting!\r\n", content);  

    /* Generate the HTTP response */  
    printf("Content-length: %d\r\n", strlen(content));  
    printf("Content-type: text/html\r\n\r\n");  
    printf("%s", content);  
    fflush(stdout);  
    exit(0);  
}

HTTPS的首页:由于我们的CA不可信,所以需要我们认可

图片 8

认可后HTTPS首页:

图片 9

HTTPS POST页面:

图片 10

HTTPS POST响应:

图片 11

从上我们可以看出,POST提交的参数的确不是通过URL传送的。

2、各模块功能介绍

头文件httpd.h,包含该项目代码所使用的全部函数的头文件以及宏定义,和函数声明;

 1 #ifndef _HTTPD_
 2 #define _HTTPD_
 3 
 4 #include <stdio.h>
 5 #include <stdlib.h>
 6 #include <sys/socket.h>
 7 #include <sys/types.h>
 8 #include <netinet/in.h>
 9 #include <arpa/inet.h>
10 #include <fcntl.h>
11 #include <errno.h>
12 #include <string.h>
13 #include <unistd.h>
14 #include <sys/stat.h>
15 #include <sys/wait.h>
16 
17 #define SUCCESS 0 
18 #define NOTICE  1
19 #define WARNING 2
20 #define ERROR   3
21 #define FATAL   4
22 
23 #define SIZE 1024
24 
25 void print_log(char *msg, int level); //打印日志
26 int startup(const char *ip, int  port); //创建监听套接字
27 void *handler_request(void *arg);  //处理请求
28 
29 #endif

main函数文件main.c实现主要通信逻辑,通过socket建立连接的,监听和接受套接字,然后创建新线程处理请求。

 1 #include <pthread.h>
 2 #include "httpd.h"
 3 
 4 static void usage(const char *proc)
 5 {
 6     printf("Usage: %s [local_ip] [local_port]\n", proc);
 7 }
 8 
 9 int main(int argc, char *argv[])
10 {
11     if(argc != 3){
12         usage(argv[0]);
13         return 1;
14     }
15 
16     int listen_sock = startup(argv[1], atoi(argv[2]));//监听套接字
17     //daemon(0, 0);
18     while(1){
19         struct sockaddr_in client;
20         socklen_t len = sizeof(client);
21         int new_sock = accept(listen_sock, (struct sockaddr*)&client, &len);//接收套接字
22         if(new_sock < 0){
23             print_log(strerror(errno), NOTICE);
24             continue;
25         }
26 
27         printf("get client [%s:%d]\n",\
28                 inet_ntoa(client.sin_addr),\
29                 ntohs(client.sin_port)); //链接到一个客户端之后打印其IP及端口号
30 
31         pthread_t id;
32         int ret = pthread_create(&id, NULL,\ //创建新线程
33                 handler_request, (void *)new_sock);
34         if(ret != 0){
35             print_log(strerror(errno), WARNING);
36             close(new_sock);
37         }else{
38             pthread_detach(id); //将子线程分离,该线程结束后会自动释放所有资源
39         }
40     }
41     close(listen_sock);
42     return 0;
43 }

模块功能函数在httpd.c文件

  1 #include "httpd.h"
  2 
  3 void print_log(char *msg, int level)
  4 {
  5 #ifdef _STDOUT_
  6     const char * const level_msg[]={
  7         "SUCCESS",
  8         "NOTICE",
  9         "WARNING",
 10         "ERROR",
 11         "FATAL",
 12     };
 13     printf("[%s][%s]\n", msg, level_msg[level%5]);
 14 #endif
 15 }
 16 
 17 int startup(const char *ip, int  port)  //
 18 {
 19     int sock = socket(AF_INET, SOCK_STREAM, 0);  //创建套接字
 20     if(sock < 0){
 21         print_log(strerror(errno), FATAL); //strerror()将错误码转换为对应的错误码描述
 22         exit(2);
 23     }
 24 
 25     int opt = 1;
 26     setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));  //将该套接字设置为地址复用状态,若服务器挂掉可实现立即重启
 27 
 28     struct sockaddr_in local;
 29     local.sin_family = AF_INET;
 30     local.sin_port = htons(port);  //端口号转换
 31     local.sin_addr.s_addr = inet_addr(ip);  //ip转换
 32     if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0){  //绑定
 33         print_log(strerror(errno), FATAL);
 34         exit(3);
 35     }
 36     if(listen(sock, 10) < 0){  //监听
 37         print_log(strerror(errno), FATAL);
 38         exit(4);
 39     }
 40     return sock;
 41 }
 42 
 43 //ret > 1, line != '\0'读成功,正常字符; ret=1&line='\n'  ret<=0&&line=='\0'
 44 static int get_line(int sock, char line[], int size)  //得到一行请求内容
 45 {
 46     // read 1 char , one by one
 47     char c = '\0'; 
 48     int len = 0;
 49     while( c != '\n' && len < size-1){  
 50         int r = recv(sock, &c, 1, 0);  
 51         if(r > 0){                      
 52            if(c == '\r'){
 53                //窥探,只把缓冲区的东西拿出来看看
 54                int ret = recv(sock, &c, 1, MSG_PEEK);
 55                if(ret > 0){
 56                    if(c == '\n'){
 57                        recv(sock, &c, 1, 0);
 58                    }else{
 59                        c = '\n'; 
 60                    }
 61                }
 62            }// \r->\n \r\n -> \n
 63            line[len++] = c;
 64         }else{
 65             c = '\n';
 66         }
 67     }
 68     line[len]='\0';
 69     return len;
 70 }
 71 //不同平台下\n、\r、\n+\r,意义不同,这里将其统一成\n
 72 
 73 static void echo_string(int sock)
 74 {}
 75 
 76 static int echo_www(int sock, char *path, int size)
 77 {
 78     int fd = open(path, O_RDONLY);
 79     if(fd < 0){
 80         echo_string(sock);
 81         print_log(strerror(errno), FATAL);
 82         return 8;
 83     }
 84 
 85     const char *echo_line="HTTP/1.0 200 OK\r\n";   //状态行
 86     send(sock, echo_line, strlen(echo_line), 0);
 87     const char *null_line="\r\n";
 88     send(sock, null_line, strlen(null_line), 0); //空行
 89 
 90     if(sendfile(sock, fd, NULL, size) < 0){//在内核区实现两个文件描述符的拷贝,不用定义临时变量,省略两次数据拷贝,效率提高
 91         echo_string(sock);
 92         print_log(strerror(errno), FATAL);
 93         return 9;
 94     }
 95 
 96     close(fd);
 97     return 0;
 98 }
 99 
100 static void drop_header(int sock)
101 {
102     char line[1024];
103     int ret = -1;
104     do{
105         ret = get_line(sock, line, sizeof(line));
106     }while(ret>0 && strcmp(line, "\n"));
107 }
108 
109 static int exe_cgi(int sock, char *method, \
110         char *path, char *query_string)
111 {
112     int content_len = -1;
113     char method_env[SIZE/10];
114     char query_string_env[SIZE];
115     char content_len_env[SIZE/10];
116 
117     if( strcasecmp(method, "GET") == 0 ){//忽略大小写的字符比较,此处为判断请求资源的方法是否为GET方法
118         drop_header(sock);//如果是GET方法则已从URL中知道用户请求资源所传参数
119     }else{//POST
120         char line[1024];
121         int ret = -1;
122         do{
123             ret = get_line(sock, line, sizeof(line));
124             if(ret > 0 &&\
125                strncasecmp(line,"Content-Length: ", 16)== 0){
126                 content_len = atoi(&line[16]);//消息正文字长描述
127             }
128         }while(ret>0 && strcmp(line, "\n"));
129         if(content_len == -1){
130             echo_string(sock);
131             return 10;
132         }
133     }
134     const char *echo_line="HTTP/1.0 200 OK\r\n";  //状态行
135     send(sock, echo_line, strlen(echo_line), 0);  
136     const char *type="Content-Type:text/html;charset=ISO-8859-1\r\n";
137     send(sock, type, strlen(type), 0);
138     const char *null_line="\r\n";  
139     send(sock, null_line, strlen(null_line), 0);  //空行
140 
141     printf("query_string: %s\n", query_string);
142     //path-> exe
143     int input[2];
144     int output[2];
145     if(pipe(input) < 0 || pipe(output) < 0){
146         echo_string(sock);
147         return 11;
148     }
149     pid_t id = fork();
150     if(id < 0){
151         echo_string(sock);
152         return 12;
153     }else if(id == 0){//child
154         close(input[1]);
155         close(output[0]);
156         sprintf(method_env, "METHOD=%s", method);
157         putenv(method_env);
158 
159         if(strcasecmp(method, "GET") == 0){
160             sprintf(query_string_env, "QUERY_STRING=%s", query_string);
161             putenv(query_string_env);
162         }else{ // POST
163             sprintf(content_len_env, "CONTENT_LENGTH=%d", content_len);
164             putenv(content_len_env);
165         }
166         dup2(input[0], 0);//重定向
167         dup2(output[1], 1);
168         execl(path, path, NULL);  //第一个参数:路径及名字,第二个参数:怎么执行,传什么参数
169         printf("execl error!\n");
170         exit(1);
171     }else{
172         close(input[0]);
173         close(output[1]);
174 
175         int i = 0;
176         char c = '\0';
177         if(strcasecmp(method, "POST") == 0){
178             for( ; i < content_len; i++ ){
179                 recv(sock, &c, 1, 0);
180                 write(input[1], &c, 1);
181             }
182         }
183 
184         c='\0';
185         while(read(output[0], &c, 1) > 0){
186             send(sock, &c, 1, 0);
187         }
188 
189         waitpid(id, NULL, 0);
190         close(input[1]);
191         close(output[0]);
192     }
193 }
194 
195 //thread 
196 void *handler_request(void *arg) 
197 {
198     int sock = (int)arg;
199 #ifdef _DEBUG_  //测试代码
200     char line[1024];  
201     do{
202         int ret = get_line(sock, line, sizeof(line));
203         if(ret > 0){
204             printf("%s", line);
205         }else{
206             printf("request ...... done!\n");
207             break;
208         }
209     }while(1);
210 #else
211     int ret = 0;
212     char buf[SIZE];  //读到的请求内容
213     char method[SIZE/10];  //请求资源的方法
214     char url[SIZE];  //统一资源标识符
215     char path[SIZE];  //有效资源路径
216     int i, j;
217     int cgi = 0; //设置CGI模式
218     char *query_string = NULL;  //请求资源字符串(URL中问号后的内容)
219     if(get_line(sock, buf, sizeof(buf)) <= 0){ //获得一行请求内容
220         echo_string(sock);
221         ret = 5;
222         goto end;
223     }
224     i=0;//method ->index
225     j=0;//buf -> index
226 
227     while( !isspace(buf[j]) &&\
228             j < sizeof(buf) &&\
229             i < sizeof(method)-1){
230         method[i]=buf[j];
231         i++, j++;
232     }
233     method[i] = 0;
234     if(strcasecmp(method, "GET") &&\  //忽略大小写的字符比较,此处为判断请求资源的方法是否为GET方法或POST方法
235             strcasecmp(method, "POST") ){
236         echo_string(sock);
237         ret = 6;
238         goto end;
239     }
240     if(strcasecmp(method, "POST") == 0){  //如果使用POST方法必定是CGI模式
241         cgi = 1;
242     }
243     //buf -> "GET          /      http/1.0"
244     while(isspace(buf[j]) && j < sizeof(buf)){
245         j++;
246     }
247     i=0;
248     while(!isspace(buf[j]) && j < sizeof(buf) && i < sizeof(url)-1){
249         url[i] = buf[j];
250         i++, j++;
251     }
252     url[i] = 0;
253     printf("method: %s, url: %s\n", method, url);
254     query_string = url;
255     while(*query_string != '\0'){
256         if(*query_string == '?'){//如果是GET方法且传参,必定是CGI模式
257             *query_string = '\0';
258             query_string++;
259             cgi = 1;
260             break;
261         }
262         query_string++;
263     }
264     sprintf(path, "wwwroot%s", url);
265     //method, url, query_string, cgi
266     if(path[strlen(path)-1] == '/'){ // '/'
267         strcat(path, "index.html");//如果是GET方法且无参,拼接上首页信息
268     }
269     struct stat st;
270     if(stat(path, &st) != 0){
271         echo_string(sock);
272         ret = 7;
273         goto end;
274     }else{
275         if(S_ISDIR(st.st_mode)){  //如果是目录,则拼接上首页信息,默认任何目录下都可以访问首页
276             strcat(path, "/index.html");
277         }else if( (st.st_mode & S_IXUSR) || \  //如果是二进制文件
278                   (st.st_mode & S_IXGRP) || \
279                   (st.st_mode & S_IXOTH) ){
280             cgi=1;
281         }else{
282         }
283         //ok->cgi=?, path, query_string, method
284         if(cgi){
285             printf("enter CGI\n");  //进入CGI模式处理
286             exe_cgi(sock, method, path, query_string);
287         }else{//非CGI处理
288             printf("method: %s, url: %s, path: %s, cgi: %d, query_string: %s\n", method, url, path, cgi, query_string);
289             drop_header(sock); //!!!!!!!!!!!!!!清除信息(不关心的内容)
290             echo_www(sock, path, st.st_size);//非CGI模式时的响应
291         }
292     }
293 
294 end:
295     printf("quit client...\n");  //出错退出
296     close(sock);
297     return (void*)ret;
298 #endif
299 }

使用 GET 方法传递信息

GET 方法发送已编码的用户信息追加到页面请求中。页面和已编码信息通过 ?
字符分隔开,如下所示:

http://www.test.com/cgi-bin/cpp.cgi?key1=value1&key2=value2

GET 方法是默认的从浏览器向 Web
服务器传信息的方法,它会在浏览器的地址栏中生成一串很长的字符串。当您向服务器传密码或其他一些敏感信息时,不要使用
GET 方法。GET 方法有大小限制,在一个请求字符串中最多可以传 1024 个字符。

当使用 GET 方法时,是使用 QUERY_STRING http 头来传递信息,在 CGI
程序中可使用 QUERY_STRING 环境变量来访问。

您可以通过在 URL 后跟上简单连接的键值对,也可以通过使用 HTML
<FORM> 标签的 GET 方法来传信息。

3.CGI数据输出

CGI程序如何将信息处理结果返回给客户端?这实际上是CGI格式化输出。

在CGI程序中的标准输出stdout是经过重定义了的,它并没有在服务器上产生任何的输出内容,而是被重定向到客户浏览器,这与它是由C,还是Perl或Python实现无关。

所以,我们可以用打印来实现客户端新的HTML页面的生成。比如,C的printf是向该进程的标准输出发送数据,Perl和Python用print向该进程的标准输出发送数据。

(1)    CGI标题

CGI的格式输出内容必须组织成标题/内容的形式。CGI标准规定了CGI程序可以使用

的三个HTTP标题。标题必须占据第一行输出!而且必须随后带有一个空行。

标题

描述

Content_type   (内容类型)

设定随后输出数据所用的MIME类型

Location    (地址)

设定输出为另外一个文档(URL)

Status      (状态)

指定HTTP状态码

 

MIME:

向标准输出发送网页内容时要遵守MIME格式规则:

任意输出前面必须有一个用于定义MIME类型的输出内容(Content-type)行,而且随后还必须跟一个空行。如果遗漏了这一条,服务将会返回一个错误信息。(同样使用于其他标题)

例如Perl和Python:

print “Content-type:text/html\n\n”;   //输出HTML格式的数据

print “<body>welcome<br>”

print “</body>”

C语言:

printf( “Content-type:text/html\n\n”);

printf(“Welcome\n”);

 

MIME类型以类型/子类型(type/subtype)的形式表示。

其中type表示一下几种典型文件格式的一种:

Text、Audio、Video、Image、Application、Mutipart、Message

Subtype则用来描述具体所用的数据格式。

Application/msword

微软的Word文件

Application/octet-stream

一种通用的二进制文件格式

Application/zip

Zip压缩文件

Application/pdf

Pdf文件

。。。。。。。。。。。。。。。。。。。。。。。。。。

。。。。。。。。。。。。。。。。。。。。。。。。。

 

Location:

使用Location标题,一个CGI可以使当前用户转而访问同一服务器上的另外一个程序,甚至可以访问另外一个URL,但服务器对他们的处理方式不一样。

使用Location的格式为:Location:Filename/URL,例如:

print “Location:/test.html\n\n”;

这与直接链接到test.html的效果是一样的。

 

print “Location:http://www.chinaunix.com/\n\n”

由于该URL并不指向当前服务器,用户浏览器并不会直接链接到指定的URL,而是给用户输出提示信息。

 

 

HTTP状态码:

      
表示了请求的结果状态,是CGI程序通过服务器用来通知用户其请求是否成功执行的信息码,本文不做研究。

这个主要是在CSAPP基础上做的,添加了POST,SSL,目录显示等功能。

2)GET方法

  在该方法下,CGI程序无法直接从服务器的标准输入(用户发送的消息正文)中获取数据,因为服务器把它从标准输入接收到得数据编码到环境变量QUERY_STRING(或PATH_INFO)。

  采用GET方法提交HTML表单数据的时候,客户机将把这些数据附加到由ACTION标记命名的URL的末尾,用一个包括把经过URL编码后的信息与CGI程序的名字分开:?name=hgq$id=1,QUERY_STRING的值为name=hgq&id=1(?左侧为要请求的资源,右侧为参数,参数形式一般为name=value形式,以“&”连接)。或者使用nomal形式的GET方法,无参数,不带正文,只有请求行+消息报头+空行。有些程序员不愿意采用GET方法,因为在他们看来,把动态信息附加在URL的末尾有违URL的出发点:URL作为一种标准用语,一般是用作网络资源的唯一定位标示。

简单的 URL 实例:Get 方法

下面是一个简单的 URL,使用 GET 方法传递两个值给 hello_get.py 程序。

/cgi-bin/cpp_get.cgi?first_name=ZARA&last_name=ALI

下面的实例生成 cpp_get.cgi CGI 程序,用于处理 Web
浏览器给出的输入。通过使用 C++ CGI 库,可以很容易地访问传递的信息:

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

#include <cgicc/CgiDefs.h> 
#include <cgicc/Cgicc.h> 
#include <cgicc/HTTPHTMLHeader.h> 
#include <cgicc/HTMLClasses.h>  

using namespace std;
using namespace cgicc;

int main ()
{
   Cgicc formData;

   cout << "Content-type:text/html\r\n\r\n";
   cout << "<html>\n";
   cout << "<head>\n";
   cout << "<title>使用 GET 和 POST 方法</title>\n";
   cout << "</head>\n";
   cout << "<body>\n";

   form_iterator fi = formData.getElement("first_name");  
   if( !fi->isEmpty() && fi != (*formData).end()) {  
      cout << "名:" << **fi << endl;  
   }else{
      cout << "No text entered for first name" << endl;  
   }
   cout << "<br/>\n";
   fi = formData.getElement("last_name");  
   if( !fi->isEmpty() &&fi != (*formData).end()) {  
      cout << "姓:" << **fi << endl;  
   }else{
      cout << "No text entered for last name" << endl;  
   }
   cout << "<br/>\n";

   cout << "</body>\n";
   cout << "</html>\n";

   return 0;
}

现在,编译上面的程序,如下所示:

$g++ -o cpp_get.cgi cpp_get.cpp -lcgicc

生成 cpp_get.cgi,并把它放在 CGI 目录中,并尝试使用下面的链接进行访问:

/cgi-bin/cpp_get.cgi?first_name=ZARA&last_name=ALI

这会产生以下结果:

名:ZARA 
姓:ALI 

2.GET方法

在该方法下,CGI程序无法直接从服务器的标准输入中获取数据,因为服务器把它从标

准输入接收到得数据编码到环境变量QUERY_STRING(或PATH_INFO)。

GET与POST的区别:采用GET方法提交HTML表单数据的时候,客户机将把这些数

据附加到由ACTION标记命名的URL的末尾,用一个包括把经过URL编码后的信息与CGI程序的名字分开:?name=hgq$id=1,QUERY_STRING的值为name=hgq&id=1

有些程序员不愿意采用GET方法,因为在他们看来,把动态信息附加在URL的末尾有

违URL的出发点:URL作为一种标准用语,一般是用作网络资源的唯一定位标示。

 

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

二、设计原理

首先介绍一些HTTP协议基本知识。

#1.GET/POST

本实现支持GET/POST方法,都是HTTP协议需要支持的标准方法。

GET方法主要是通过URL发送请求和传送数据,而POST方法在请求头空一格之后传送数据,所以POST方法比GET方法安全性高,因为GET方法可以直接看到传送的数据。另外一个区别就是GET方法传输的数据较小,而POST方法很大。所以一般表单,登陆页面等都是通过POST方法。

#2.MIME类型

当服务器获取客户端的请求的文件名,将分析文件的MIME类型,然后告诉浏览器改文件的MIME类型,浏览器通过MIME类型解析传送过来的数据。具体来说,浏览器请求一个主页面,该页面是一个HTML文件,那么服务器将”text/html”类型发给浏览器,浏览器通过HTML解析器识别发送过来的内容并显示。

下面将描述一个具体情景。

客户端使用浏览器通过URL发送请求,服务器获取请求。

如浏览器URL为:127.0.0.1/postAuth.html,

那么服务器获取到的请求为:GET  /postAuth.html  HTTP/1.1

意思是需要根目录下postAuth.html文件的内容,通过GET方法,使用HTTP/1.1协议(1.1是HTTP的版本号)。这是服务器将分析文件名,得知postAuth.html是一个HTML文件,所以将”text/html”发送给浏览器,然后读取postAuth.html内容发给浏览器。

实现简单的MIME类型识别代码如下:

主要就是通过文件后缀获取文件类型。

static void get_filetype(const char *filename, char *filetype)   
{  
    if (strstr(filename, ".html"))  
        strcpy(filetype, "text/html");  
    else if (strstr(filename, ".gif"))  
        strcpy(filetype, "image/gif");  
    else if (strstr(filename, ".jpg"))  
        strcpy(filetype, "image/jpeg");  
    else if (strstr(filename, ".png"))  
        strcpy(filetype, "image/png");  
    else  
    strcpy(filetype, "text/plain");  
}

如果支持HTTPS的话,那么我们就#define HTTPS,这主要通过gcc
的D选项实现的,具体细节可参考man手册。

静态内容显示实现如下:

static void serve_static(int fd, char *filename, int filesize) 
{
    int srcfd;
    char *srcp, filetype[MAXLINE], buf[MAXBUF];

    /* Send response headers to client */
    get_filetype(filename, filetype);
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    sprintf(buf, "%sServer: Tiny Web Server\r\n", buf);
    sprintf(buf, "%sContent-length: %d\r\n", buf, filesize);
    sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, filetype);

    /* Send response body to client */
    srcfd = Open(filename, O_RDONLY, 0);
    srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
    Close(srcfd);

    #ifdef HTTPS 
    if(ishttps)
    {
        SSL_write(ssl, buf, strlen(buf));
    SSL_write(ssl, srcp, filesize);
    }
    else
    #endif
    {
    Rio_writen(fd, buf, strlen(buf));
    Rio_writen(fd, srcp, filesize);
    }
    Munmap(srcp, filesize);
}

#3.CGI规范

如果只能显示页面那么无疑缺少动态交互能力,于是CGI产生了。CGI是公共网关接口(Common
Gateway
Interface),是在CGI程序和Web服务器之间传递信息的规则。CGI允许Web服务器执行外部程序,并将它们的输出发送给浏览器。这样就提供了动态交互能力。

那么服务器是如何分开处理静态页面和动态CGI程序的呢?这主要是通过解析URL的方式。我们可以定义CGI程序的目录,如cgi-bin,那么如果URL包含”cgi-bin”字符串则这是动态程序,且将URL的参数给cgiargs。如果是静态页面,parse_uri返回1,反正返回0。所以我们可以通过返回值区别不同的服务类型。

具体解析URL方式如下:

static void serve_static(int fd, char *filename, int filesize) 
{
    int srcfd;
    char *srcp, filetype[MAXLINE], buf[MAXBUF];

    /* Send response headers to client */
    get_filetype(filename, filetype);
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    sprintf(buf, "%sServer: Tiny Web Server\r\n", buf);
    sprintf(buf, "%sContent-length: %d\r\n", buf, filesize);
    sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, filetype);

    /* Send response body to client */
    srcfd = Open(filename, O_RDONLY, 0);
    srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
    Close(srcfd);

    #ifdef HTTPS 
    if(ishttps)
    {
        SSL_write(ssl, buf, strlen(buf));
    SSL_write(ssl, srcp, filesize);
    }
    else
    #endif
    {
    Rio_writen(fd, buf, strlen(buf));
    Rio_writen(fd, srcp, filesize);
    }
    Munmap(srcp, filesize);
}

GET方式的CGI规范实现原理:

服务器通过URL获取传给CGI程序的参数,设置环境变量QUERY_STRING,并将标准输出重定向到文件描述符,然后通过EXEC函数簇执行外部CGI程序。外部CGI程序获取QUERY_STRING并处理,处理完后输出结果。由于此时标准输出已重定向到文件描述符,即发送给了浏览器。

实现细节如下:由于涉及到HTTPS,所以稍微有点复杂。

static void serve_static(int fd, char *filename, int filesize) 
{
    int srcfd;
    char *srcp, filetype[MAXLINE], buf[MAXBUF];

    /* Send response headers to client */
    get_filetype(filename, filetype);
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    sprintf(buf, "%sServer: Tiny Web Server\r\n", buf);
    sprintf(buf, "%sContent-length: %d\r\n", buf, filesize);
    sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, filetype);

    /* Send response body to client */
    srcfd = Open(filename, O_RDONLY, 0);
    srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
    Close(srcfd);

    #ifdef HTTPS 
    if(ishttps)
    {
        SSL_write(ssl, buf, strlen(buf));
    SSL_write(ssl, srcp, filesize);
    }
    else
    #endif
    {
    Rio_writen(fd, buf, strlen(buf));
    Rio_writen(fd, srcp, filesize);
    }
    Munmap(srcp, filesize);
}

POST方式的CGI规范实现原理:

由于POST方式不是通过URL传递参数,所以实现方式与GET方式不一样。

POST方式获取浏览器发送过来的参数长度设置为环境变量CONTENT-LENGTH。并将参数重定向到CGI的标准输入,这主要通过pipe管道实现的。CGI程序从标准输入读取CONTENT-LENGTH个字符就获取了浏览器传送的参数,并将处理结果输出到标准输出,同理标准输出已重定向到文件描述符,所以浏览器就能收到处理的响应。

具体实现细节如下:

static void serve_static(int fd, char *filename, int filesize) 
{
    int srcfd;
    char *srcp, filetype[MAXLINE], buf[MAXBUF];

    /* Send response headers to client */
    get_filetype(filename, filetype);
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    sprintf(buf, "%sServer: Tiny Web Server\r\n", buf);
    sprintf(buf, "%sContent-length: %d\r\n", buf, filesize);
    sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, filetype);

    /* Send response body to client */
    srcfd = Open(filename, O_RDONLY, 0);
    srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
    Close(srcfd);

    #ifdef HTTPS 
    if(ishttps)
    {
        SSL_write(ssl, buf, strlen(buf));
    SSL_write(ssl, srcp, filesize);
    }
    else
    #endif
    {
    Rio_writen(fd, buf, strlen(buf));
    Rio_writen(fd, srcp, filesize);
    }
    Munmap(srcp, filesize);
}

目录显示功能原理:

主要是通过URL获取所需目录,然后获取该目录下所有文件,并发送相应信息,包括文件格式对应图片,文件名,文件大小,最后修改时间等。由于我们发送的文件名是通过超链接的形式,所以我们可以点击文件名继续浏览信息。

具体实现细节如下:

static void serve_static(int fd, char *filename, int filesize) 
{
    int srcfd;
    char *srcp, filetype[MAXLINE], buf[MAXBUF];

    /* Send response headers to client */
    get_filetype(filename, filetype);
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    sprintf(buf, "%sServer: Tiny Web Server\r\n", buf);
    sprintf(buf, "%sContent-length: %d\r\n", buf, filesize);
    sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, filetype);

    /* Send response body to client */
    srcfd = Open(filename, O_RDONLY, 0);
    srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
    Close(srcfd);

    #ifdef HTTPS 
    if(ishttps)
    {
        SSL_write(ssl, buf, strlen(buf));
    SSL_write(ssl, srcp, filesize);
    }
    else
    #endif
    {
    Rio_writen(fd, buf, strlen(buf));
    Rio_writen(fd, srcp, filesize);
    }
    Munmap(srcp, filesize);
}

HTTPS的实现:

HTTPS主要基于openssl的开源库实现。如果没有安装,那么我们就不#define
HTTPS。
HTTPS的功能主要就是提供安全的连接,服务器和浏览器之间传送的数据是通过加密的,加密方式可以自己选定。

开始连接时,服务器需要发送CA,由于我们的CA是自己签发的,所以需要我们自己添加为可信。

访问控制功能:

主要是通过获取客户端IP地址,并转换为整数,与上配置文件中定义的掩码,如果符合配置文件中允许的网段,那么可以访问,否则不可以。

具体实现如下。

static void serve_static(int fd, char *filename, int filesize) 
{
    int srcfd;
    char *srcp, filetype[MAXLINE], buf[MAXBUF];

    /* Send response headers to client */
    get_filetype(filename, filetype);
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    sprintf(buf, "%sServer: Tiny Web Server\r\n", buf);
    sprintf(buf, "%sContent-length: %d\r\n", buf, filesize);
    sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, filetype);

    /* Send response body to client */
    srcfd = Open(filename, O_RDONLY, 0);
    srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
    Close(srcfd);

    #ifdef HTTPS 
    if(ishttps)
    {
        SSL_write(ssl, buf, strlen(buf));
    SSL_write(ssl, srcp, filesize);
    }
    else
    #endif
    {
    Rio_writen(fd, buf, strlen(buf));
    Rio_writen(fd, srcp, filesize);
    }
    Munmap(srcp, filesize);
}

配置文件的读取:

主要选项信息都定义与配置文件中。

格式举例如下;

#HTTP PORT
PORT = 8888

所以读取配置文件函数具体如下:

static char* getconfig(char* name)  
{  
/* 
pointer meaning: 

...port...=...8000... 
   |  |   |   |  | 
  *fs |   |   |  *be    f->forward  b-> back 
      *fe |   *bs       s->start    e-> end 
          *equal 
*/  
    static char info[64];  
    int find=0;  
    char tmp[256],fore[64],back[64],tmpcwd[MAXLINE];  
    char *fs,*fe,*equal,*bs,*be,*start;  

    strcpy(tmpcwd,cwd);  
    strcat(tmpcwd,"/");  
    FILE *fp=getfp(strcat(tmpcwd,"config.ini"));  
    while(fgets(tmp,255,fp)!=NULL)  
    {  
        start=tmp;  
        equal=strchr(tmp,'=');  

        while(isblank(*start))  
            ++start;  
        fs=start;  

        if(*fs=='#')  
            continue;  
        while(isalpha(*start))  
            ++start;  
        fe=start-1;  

        strncpy(fore,fs,fe-fs+1);  
        fore[fe-fs+1]='\0';  
        if(strcmp(fore,name)!=0)  
            continue;  
        find=1;  

        start=equal+1;  
        while(isblank(*start))  
            ++start;  
        bs=start;  

        while(!isblank(*start)&&*start!='\n')  
            ++start;  
        be=start-1;  

        strncpy(back,bs,be-bs+1);  
        back[be-bs+1]='\0';  
        strcpy(info,back);  
        break;  
    }  
    if(find)  
        return info;  
    else  
        return NULL;  
}

5、遇到的一些问题:

1)本地环回测试ok,Linux下的浏览器测试也可以,但不能接外部的浏览器访问(没有设置桥接模式)嗯~要是在外部浏览器测试的话千万别忘记关闭防火墙。

解决:切换超级用户:$service iptables stop

2)服务器应答时,没有将html格式的页面发送,而是将底层的实现代码展示在浏览器,并且在调试时将本来要打印的调试信息会打印到网页上(在回应空行时将send期望发送的数值写的太大,本来只需要发送两个字节的内容)
解决:先检查代码,思路正确,在容易出现问题的地方加入调试信息,最后将问题定位在echo_www()函数内

3)不能显示图片(这个问题是没有将所有发送的情况考虑完全,只考虑到目录、可执行程序,但没有考虑到如果请求的是一个路径明确的普通文件)
解决:测试请求一个路径明确的test.html文件,加入调试信息
,将问题定位在:如果请求的资源存在,应该如何处理。对于普通文件,找到后并回显给浏览器;如果是目录,应答的是默认页面;如果是可执行程序,执行后返回结果

4)能显示图片后,但显示的不完整(原因:echo_www中,期望读取一行信息的line值太小,不能存下一张图片)

5)运行cgi模式时,每次提交数据并进行submit后都会自动出现提醒下载的页面
原因:在响应报头中,将Content-Type中的”text”写成”test”。而浏览器对于不能识别或解析的实体,都会提醒用户下载。

 

第一个 CGI 程序

请看下面的 C++ 程序:

#include <iostream>
using namespace std;

int main ()
{

   cout << "Content-type:text/html\r\n\r\n";
   cout << "<html>\n";
   cout << "<head>\n";
   cout << "<title>Hello World - 第一个 CGI 程序</title>\n";
   cout << "</head>\n";
   cout << "<body>\n";
   cout << "<h2>Hello World! 这是我的第一个 CGI 程序</h2>\n";
   cout << "</body>\n";
   cout << "</html>\n";

   return 0;
}

编译上面的代码,把可执行文件命名为 cplusplus.cgi,并把这个文件保存在
/var/www/cgi-bin 目录中。在运行 CGI 程序之前,请使用 chmod 755
cplusplus.cgi UNIX
命令来修改文件模式,确保文件可执行。访问可执行文件,您会看到下面的输出:

Hello World! 这是我的第一个 CGI 程序

上面的 C++ 程序是一个简单的程序,把它的输出写在 STDOUT
文件上,即显示在屏幕上。在这里,值得注意一点,第一行输出
Content-type:text/html\r\n\r\n。这一行发送回浏览器,并指定要显示在浏览器窗口上的内容类型。

您必须理解 CGI 的基本概念,这样才能进一步使用 Python 编写更多复杂的 CGI
程序。C++ CGI 程序可以与任何其他外部的系统(如 RDBMS)进行交互。

1.从服务器获取数据

C语言实现代码:

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

 

int get_inputs()

{

int length;

char *method;

char *inputstring;

 

method = getenv(“REQUEST_METHOD”); //将返回结果赋予指针

if(method == NULL)

    return 1;       //找不到环境变量REQUEST_METHOD

if(!strcmp(method, ”POST”))  // POST方法

{

    length = atoi(getenv(“CONTENT_LENGTH”)); //结果是字符,需要转换

    if(length != 0)

    {

        inputstring = malloc(sizeof(char)*length + 1) //必须申请缓存,因为stdin是不带缓存的。

        fread(inputstring, sizeof(char), length, stdin); //从标准输入读取一定数据

}

}

else if(!strcmp(method, “GET”))

{

    Inputstring = getenv(“QUERY_STRING”);   

    length = strlen(inputstring);

}

if(length == 0)

return 0;

}

Perl实现代码:

$method = $ENV{‘REQUEST_METHOD’};

if($method eq ‘POST’)

{

    Read(STDIN, $input, $ENV{‘CONTENT_LENGTH’});

}

if($method eq ‘GET’ || $method eq ‘HEAD’)

{

    $input = $ENV{‘QUERY_STRING’};

}

if($input eq “”)

{

&print_form;

exit;

}

       PYTHON代码实现

#!/usr/local/bin/python

import cgi

def main():

form = cgi.FieldStorage()

 

Python代码实现更简单,cgi.FieldStorage()返回一个字典,字典的每一个key就是变量名,key对应的值就是变量名的值,更本无需用户再去进行数据解码!

 

      
获取环境变量的时候,如果先判断“REQUEST_METHOD”是否存在,程序会更健壮,否则在某些情况下可能会造成程序崩溃。因为假若CGI程序不是由服务器调用的,那么环境变量集里就没有与CGI相关的环境变量(如REQUEST_METHOD,REMOTE_ADDR等)添加进来,也就是说“getenv(“REQUEST_METHOD”)”将返回NULL!

一、 实现功能:

1.支持GET/POST方法

2.支持SSL安全连接即HTTPS

3.支持CGI

4.基于IP地址和掩码的认证

5.目录显示

6.日志功能

7.错误提示页面

github地址:

源代码下载地址:点击打开链接

1)POST方法

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

GET 和 POST 方法

您可能有遇到过这样的情况,当您需要从浏览器传递一些信息到 Web
服务器,最后再传到 CGI 程序。通常浏览器会使用两种方法把这个信息传到 Web
服务器,分别是 GET 和 POST 方法。

2.URL编码

3、相关技术解释:

(1)CGI:通用网关接口

  基本原理:通用网关接口是一个Web服务器主机提供信息服务的标准接口。通过CGI接口,Web服务器根据客户端提交的资源请求信息,转交给服务器端对应的CGI程序进行处理,最后返回结果给客户端。简单来说就是HTTP服务器与客户端进行“交谈”的一种工具,其程序须运行在网络服务器上。

  组成CGI通信系统的是两部分:一部分是html页面,就是在用户端浏览器上显示的页面。另一部分则是运行在服务器上的Cgi程序。绝大多数的CGI程序被用来解释处理来自表单的输入信息,并在服务器产生相应的处理,或将相应的信息反馈给浏览器。CGI程序使网页具有交互功能。

  CGI在客户端与服务器通讯中的处理步骤:

  1)通过Internet把用户请求送到服务器;

  2)服务器接收用户请求并交给相应CGI程序处理;

  3)CGI程序把处理结果传送给服务器;

  4)服务器把结果返回给用户。

  前面已经介绍过服务器和客户端之间的通信,实际上是客户端的浏览器和服务器端的http服务器之间的HTTP通信,我们只需要知道浏览器请求执行服务器上哪个CGI程序就可以了,其他不必深究细节,因为这些过程不需要程序员去操作。服务器和CGI程序之间的通讯才是我们关注的。一般情况下,服务器和CGI程序之间是通过标准输入输出来进行数据传递的,而这个过程需要环境变量的协作方可实现。在服务器端执行步骤:1)服务器将URL指向一个应用程序 
2)服务器为应用程序执行做准备  3)应用程序执行,读取标准输入和有关环境变量 
4)应用程序进行标准输出。

(2)CGI关于环境变量

对于CGI程序来说,它继承了系统的环境变量。CGI环境变量在CGI程序启动时初始化,在结束时销毁。

       当一个CGI程序不是被HTTP服务器调用时,它的环境变量几乎是系统环境变量的复制。

当这个CGI程序被HTTP服务器调用时,它的环境变量就会多了以下关于HTTP服务器、客户端、CGI传输过程等项目。

图片 12

CONTENT_TYPE:如application/x-www-form-urlencoded,表示数据来自HTML表单,并且经过了URL编码。

ACCEPT:客户机所支持的MIME类型清单,内容如:”image/gif,image/jpeg”

REQUEST_METHOD:本项目涉及常见的两种方法:POST和GET,但我们写CGI程序时,最后还要考虑其他的情况。

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

 (3)POST/GET传输方式详解

简单的表单实例:GET 方法

下面是一个简单的实例,使用 HTML
表单和提交按钮传递两个值。我们将使用相同的 CGI 脚本 cpp_get.cgi
来处理输入。

<form action="/cgi-bin/cpp_get.cgi" method="get">
名:<input type="text" name="first_name">  <br />

姓:<input type="text" name="last_name" />
<input type="submit" value="提交" />
</form>

下面是上述表单的实际输出,请输入名和姓,然后点击提交按钮查看结果。

不管是POST还是GET方式,客户端浏览器发送给服务器的数据都不是原始的用户数据,而是经过URL编码的。此时,CGI的环境变量Content_type将被设置,如Content_type

application/x-www-form-urlencode就表示服务器收到的是经过URL编码的包含有HTML表单变量数据。

编码的基本规则是:

变量之间用“&”分开;

变量与其对应值用“=”连接;

空格用“+”代替;

保留的控制字符则用“%”连接对应的16禁止ASCII码代替;

某些具有特殊意义的字符也用“%”接对应的16进制ASCII码代替;

空格是非法字符;

任意不可打印的ASCII控制字符均为非法字符。

例如,假设3个HTML表单变量filename、e-mail和comments,它们的值对应分别为hello、mike@hotmail.com和I’ll
be there for you,则经过URL编码后应为:

 

filename=hello&e-mail=hello@hotmail.com&comments=I%27ll+be+there+for+you

 

 

所以,CGI程序从标准输入或环境变量中获取客户端数据后,还需要进行解码。解码的过程就是URL编码的逆变:根据“&”和“=”分离HTML表单变量,以及特殊字符的替换。

在解码方面,PYTHON代码实现是最理想的,cgi.FieldStorage()函数在获取数据的同时就已自动进行代码转换了,无需程序员再进行额外的代码编写。Perl其次,因为在一个现成的Perl库:cgi-lib.pl中提供了ReadParse函数,用它来进行URL解码很简单:

require ‘cgi-lib.pl’;

&ReadParse(*input);

 

1、项目介绍

  HTTP协议是应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。协议的详细内容,前面一篇HTTP协议详解已经详细介绍了,这里不再赘述。

   项目总体描述:HTTP支持客户端/服务器模式,终端用户可通过浏览器或网络爬虫与服务器建立连接,所以首先需要自主实现服务器Server端,具体由头文件httpd.h、main函数文件httpd.c、模块功能函数文件httpd.c组成,主要实现客户端与服务器通过socket建立通信机制。首先由用户主动发起一个到服务器上指定端口(默认端口为80)的请求,服务器则在那个端口监听客户端发送过来的请求。服务器一行一行读取请求,通过请求信息判断用户请求资源的方法和路径,若方法和路径没有问题,则方法和路径通过CGI模式或非CGI向用户提供不同的HTML网页信息。处理完请求客户端向用户发送响应,包括状态行如:“HTTP/1.1
200 OK”、响应报头、消息正文,消息体即为服务器上的资源。

实现功能一:静态首页展示(图片、文字文字信息);

实现二:支持表单提交,可以借助浏览器或telnet工具使用GET、POST方法访问服务器,实现数据的简单计算功能;

实现三:引入MYSQL,用户可通过页面表单进行数据操作,服务器拿到客户提交的数据后,会把数据存入到远端数据库,客户端也可请求查看数据库信息。

整个项目的文件目录:

图片 13

目录:
conf:配置文件,存放需要绑定的服务器的ip和port ;
log:shell的日志文件以及http错误处理的日志文件 ;
sql_client:mysql部分的API及CGI实现;
thread_pool:线程池实现;
wwwroot:web服务器工作的根目录,包含各种资源页面(例如默认的index.html页面,差错处理的404页面),以及执行cgi的可执行程序。下面还有一个 cgi-bin目录,是存放CGI脚本的地方。这些脚本使WWW服务器和浏览器能运行外部程序,而无需启动另一个程序。它是运行在Web服务器上的一个程序,并由来自于浏览者的输入触发。

整个项目的框架图:

 图片 14

向 CGI 程序传递复选框数据

当需要选择多个选项时,我们使用复选框。

下面的 HTML 代码实例是一个带有两个复选框的表单:

<form action="/cgi-bin/cpp_checkbox.cgi" 
         method="POST" 
         target="_blank">
<input type="checkbox" name="maths" value="on" /> 数学
<input type="checkbox" name="physics" value="on" /> 物理
<input type="submit" value="选择学科" />
</form>

下面的 C++ 程序会生成 cpp_checkbox.cgi 脚本,用于处理 Web
浏览器通过复选框给出的输入。

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

#include <cgicc/CgiDefs.h> 
#include <cgicc/Cgicc.h> 
#include <cgicc/HTTPHTMLHeader.h> 
#include <cgicc/HTMLClasses.h> 

using namespace std;
using namespace cgicc;

int main ()
{
   Cgicc formData;
   bool maths_flag, physics_flag;

   cout << "Content-type:text/html\r\n\r\n";
   cout << "<html>\n";
   cout << "<head>\n";
   cout << "<title>向 CGI 程序传递复选框数据</title>\n";
   cout << "</head>\n";
   cout << "<body>\n";

   maths_flag = formData.queryCheckbox("maths");
   if( maths_flag ) {  
      cout << "Maths Flag: ON " << endl;  
   }else{
      cout << "Maths Flag: OFF " << endl;  
   }
   cout << "<br/>\n";

   physics_flag = formData.queryCheckbox("physics");
   if( physics_flag ) {  
      cout << "Physics Flag: ON " << endl;  
   }else{
      cout << "Physics Flag: OFF " << endl;  
   }
   cout << "<br/>\n";
   cout << "</body>\n";
   cout << "</html>\n";

   return 0;
}

三.CGI程序实现步骤

Author

发表评论

电子邮件地址不会被公开。 必填项已用*标注