Tinyhttpd源码剖析(二)

继续看execute_cgi函数,

[cpp]  view plain  copy
  1. if (strcasecmp(method, "GET") ==0)  
  2.  {  
  3.    
  4.  while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */  
  5.   numchars = get_line(client, buf, sizeof(buf));  
  6.  }  
  7.  else   /* POST */  
  8.  {  
  9.    
  10.  numchars = get_line(client, buf, sizeof(buf));  
  11.  while ((numchars > 0) && strcmp("\n", buf))  
  12.   {  
  13.   buf[15] = '\0';  
  14.    if(strcasecmp(buf, "Content-Length:") == 0)  
  15.    content_length = atoi(&(buf[16]));  
  16.   numchars = get_line(client, buf, sizeof(buf));  
  17.   }  
  18.    
  19.   if (content_length == -1) //   {  
  20.   bad_request(client);// 4xx:  
  21.   return;  
  22.   }  
  23.  }  

首先还是读取请求头并丢弃。   Content-Length描述HTTP消息实体的长度,对于POST方法,这个域是必须存在的,因为服务器需要这个长度来查找客户端请求的参数。

 

下面一段建立两个管道,

[cpp]  view plain  copy
  1. if (pipe(cgi_output) < 0)  
  2.  {  
  3.  cannot_execute(client);  
  4.  return;  
  5.  }  
  6.    
  7.  if(pipe(cgi_input) < 0)  
  8.  {  
  9.  cannot_execute(client);  
  10.  return;  
  11.  }  

pipe()会建立管道,并将文件描述词由参数filedes数组返回。filedes[0]为管道里的读取端,filedes[1]则为管道的写入端。管道一般是用来进程间通信的,好像目前为止只有一个进程?不急,马上就创建新的进程了。


[cpp]  view plain  copy
  1. if ( (pid = fork()) < 0 ) //复制一个线程  
  2.  {  
  3.  cannot_execute(client);  
  4.  return;  
  5.  }  

Linux的fork函数比较强大,但也不好理解。 它可以创建一个子线程,那么当前的进程就是父进程。子进程可以获取父进程的地址空间的拷贝以及相关的资源,比如打开的文件描述符。这个很关键,上面获取的管道的描述符正是基于此特点才变得有意义。

 

那么问题来了,如何区分当前执行的是父进程还是子进程呢?fork函数如果调用出错会返回小于0的值。如果成功了,它会返回两次,一次是父进程中返回子进程的PID,一次是在子进程中返回0。这两个进程并发的执行,系统内核层是以时间片切换来模拟多进程的执行,不过作为程序员我们认为有两条线在并行的运行就可以了。

 

所以,通过判断返回的PID,就可以分别对子进程和父进程进行操作。在开始讲子进程之前,还需要插播一个概念:

CGI程序的特点是通过标准输入(stdin)和环境变量(可以理解成有两个传递数据的途径,二者相辅相成,其实跟请求方法是get或post也相关)来得到服务器的信息,并通过标准输出(stdout)向服务器输出信息。环境变量就是指的系统环境变量,就是linux中可以用echo命令查询那些,比如HOME, PATH等。http协议中也定义了一些自己的环境变量,用于在CGI与服务器之间传输参数。下面给出几个相关的环境变量:

[html]  view plain  copy
  1. SERVER_NAME   
  2. CGI脚本运行时的主机名和IP地址.  
  3. SERVER_SOFTWARE   
  4. 你的服务器的类型如: CERN/3.0 或 NCSA/1.3.  
  5. GATEWAY_INTERFACE   
  6. 运行的CGI版本.对于UNIX服务器, 这是CGI/1.1.  
  7. SERVER_PROTOCOL   
  8. 服务器运行的HTTP协议. 这里当是HTTP/1.0.  
  9. SERVER_PORT   
  10. 服务器运行的TCP口,通常Web服务器是80.  
  11. REQUEST_METHOD   
  12. POST 或 GET, 取决于你的表单是怎样递交的.  
  13. HTTP_ACCEPT    
  14. 浏览器能直接接收的Content-types, 可以有HTTP Accept header定义.  
  15. HTTP_USER_AGENT   
  16. 递交表单的浏览器的名称、版本 和其他平台性的附加信息。  
  17. HTTP_REFERER   
  18. 递交表单的文本的 URL,不是所有的浏览器都发出这个信息,不要依赖它  
  19. PATH_INFO   
  20. 附加的路径信息, 由浏览器通过GET方法发出.  
  21. PATH_TRANSLATED   
  22. 在PATH_INFO中系统规定的路径信息.  
  23. SCRIPT_NAME   
  24. 指向这个CGI脚本的路径,是在URL中显示的(如, /cgi-bin/thescript).  
  25. QUERY_STRING   
  26. 脚本参数或者表单输入项(如果是用GET递交). QUERY_STRING 包含URL中问号后面的参数.  
  27. REMOTE_HOST   
  28. 递交脚本的主机名,这个值不能被设置.  
  29. REMOTE_ADDR   
  30. 递交脚本的主机IP地址.  
  31. REMOTE_USER   
  32. 递交脚本的用户名. 如果服务器的authentication被激活,这个值可以设置。  
  33. REMOTE_IDENT   
  34. 如果Web服务器是在ident(一种确认用户连接你的协议)运行,递交表单的系统也在运行ident, 这个变量就含有ident返回值.  
  35. CONTENT_TYPE   
  36. 如果表单是用POST递交, 这个值将是application/x-www-form-urlencoded. 在上载文件的表单中,content-type 是个multipart/form-data.  
  37. CONTENT_LENGTH   
  38. 对于用POST递交的表单, 标准输入口的字节数.  


看看子进程,

[cpp]  view plain  copy
  1. if (pid == 0)  /* child: CGI script */  
  2.  {  
  3.  ….  
  4.    
  5.  dup2(cgi_output[1], 1);///* 把 STDOUT 重定向到cgi_output 的写入端 */   
  6.  dup2(cgi_input[0], 0); ///* 把 STDIN 重定向到cgi_input 的读取端 */   
  7.    
  8.   ///* 关闭 cgi_input 的写入端和 cgi_output 的读取端 */   
  9.  close(cgi_output[0]);  
  10.  close(cgi_input[1]);  

前面讲到cgi默认输出是标准输出,所以子进程在运行cgi程序之前,会通过dup2函数把标准输出重定向到与客户端关联的描述符上,这样后面cgi写到标准输出的东西都会直接到客户端。

当然,重定向完成后要把不用的描述符关掉,注意这里的不用是仅子进程不用,前面说到fork出来的子进程会拥有一份和父进程几乎一样的资源拷贝,所以这里只是把子进程空间中的描述符资源关闭。

 

继续看代码,

[cpp]  view plain  copy
  1. sprintf(meth_env, "REQUEST_METHOD=%s", method);  
  2. putenv(meth_env);  
  3.   
  4.  if(strcasecmp(method, "GET") == 0)  
  5.  {  
  6.   
  7.  sprintf(query_env, "QUERY_STRING=%s", query_string);  
  8.  putenv(query_env);  
  9.  }  
  10. else  
  11. {   /* POST*/  
  12.   
  13.  sprintf(length_env, "CONTENT_LENGTH=%d", content_length);  
  14.  putenv(length_env);  
  15.  }  

前面已经提到环境变量是服务与CGI之间传递数据的一个途径,这一段就是服务器设置环境变量。

 

准备工作都作完了,可以调用cgi程序了,cgi所在的路径是前面保存的地址,还记得吧。

[cpp]  view plain  copy
  1. execl(path, path, NULL);  
  2. exit(0);  
  3. }  


再来看看父进程的代码,

[cpp]  view plain  copy
  1. close(cgi_output[1]);  
  2.  close(cgi_input[0]);  
  3.    
  4.   if(strcasecmp(method, "POST") == 0)  
  5.   for (i = 0; i < content_length; i++)  
  6.    {  
  7.    recv(client, &c, 1, 0);  
  8.    write(cgi_input[1], &c, 1);/*把 POST 数据写入cgi_input,已经重定向到 STDIN */   
  9.    }  
  10.     
  11.  while (read(cgi_output[0], &c, 1) > 0)/*读取 cgi_output的管道输出到客户端,该管道输入是 STDOUT */  
  12.   send(client, &c, 1, 0);  
  13.    
  14.  close(cgi_output[0]);  
  15.  close(cgi_input[1]);  
  16.  waitpid(pid, &status, 0);  

首先是关闭不用的文件描述符,避免资源泄露,然后父进程会把数据(post或get)写入cig_input[1], 因为前面已经重定向了,所以实际是写到stdin。然后从cgi_output[0]读取输出结果发送回客户端。

 

整个子进程和父进程的处理过程,可以用下面图解释(用的别人的图,出处是:http://blog.csdn.net/jcjc918/article/details/42129311)



 

 

 

最后回到accept_request函数,最后一行是

close(client);

这个有必要说一下,http是基于无连接的,也就是完成一次请求后,马上断开与客户端的连接,所以一定要有这个关闭的动作。

猜你喜欢

转载自blog.csdn.net/sunxiaopengsun/article/details/79627997
今日推荐