Tinyhttpd源码剖析(二)

 

转载请注明出处

http://blog.csdn.net/pony_maggie/article/details/49838191

 

作者:小马

 

继续看execute_cgi函数,

 

if (strcasecmp(method, "GET") ==0)
 {
 
 while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
  numchars = get_line(client, buf, sizeof(buf));
 }
 else   /* 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);// 4xx:
  return;
  }
 }


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

 

 

下面一段建立两个管道,

 

if (pipe(cgi_output) < 0)
 {
 cannot_execute(client);
 return;
 }
 
 if(pipe(cgi_input) < 0)
 {
 cannot_execute(client);
 return;
 }


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

 

 

 

if ( (pid = fork()) < 0 ) //复制一个线程
 {
 cannot_execute(client);
 return;
 }


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

 

 

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

 

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

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

 

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

 

 

 

 

 

看看子进程,

 

if (pid == 0)  /* child: CGI script */
 {
 ….
 
 dup2(cgi_output[1], 1);///* 把 STDOUT 重定向到cgi_output 的写入端 */ 
 dup2(cgi_input[0], 0); ///* 把 STDIN 重定向到cgi_input 的读取端 */ 
 
  ///* 关闭 cgi_input 的写入端和 cgi_output 的读取端 */ 
 close(cgi_output[0]);
 close(cgi_input[1]);


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

 

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

 

继续看代码,

 

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


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

 

 

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

 

 execl(path, path, NULL);
 exit(0);
 }

 

 

 

 

 

再来看看父进程的代码,

 

close(cgi_output[1]);
 close(cgi_input[0]);
 
  if(strcasecmp(method, "POST") == 0)
  for (i = 0; i < content_length; i++)
   {
   recv(client, &c, 1, 0);
   write(cgi_input[1], &c, 1);/*把 POST 数据写入cgi_input,已经重定向到 STDIN */ 
   }
  
 while (read(cgi_output[0], &c, 1) > 0)/*读取 cgi_output的管道输出到客户端,该管道输入是 STDOUT */
  send(client, &c, 1, 0);
 
 close(cgi_output[0]);
 close(cgi_input[1]);
 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是基于无连接的,也就是完成一次请求后,马上断开与客户端的连接,所以一定要有这个关闭的动作。

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值