Tinyhttpd源码剖析(一)

一 介绍

Tinyhttpd是一个非常轻量级的http sever。代码不超过一千行。麻雀虽小,五脏俱全。反正我看完之后觉得很是畅快,收获很大。细心研究一下会对linux网络编程,http协议等概念有新的认识。


源码下载地址:

http://sourceforge.net/projects/tinyhttpd/

 

二 关于CGI

CGI要单独说一下,这是整个源码的核心,也是比较难理解的地方。

我们通过浏览器访问一个网站,会发送http请求给http 服务器,如果请求的是一个静态的页面或图片,服务器会直接返回结果给浏览器。但如果要完成一个动态的请求,比如需要查询数据库这样的操作,服务器会运行一个单独的程序来执行,这个程序处理完成后会把结果转化为服务器(或者浏览器)可以识别的格式输出。 


这样的程序就是CGI程序,因为它一般都是以脚本的形式存在的,所以也叫CGI脚本。


CGI现在很少使用了,目前主流的webserver技术大多基于JSP。CGI的主要问题一是效率比较低,毕竟它是作为一个独立的进程被调用的。另一个是CGI的实现是依赖于服务器所用的操作系统,移植性差。

 

三 源码分析

如果你对http的协议相关内容不了解,开始阅读之前,建议先自行学习一下。本文只关注代码,不会对协议做过多的介绍。

 

第一部分客户端

客户端程序比较简单,创建一个socket对象连接服务器,然后读写一个字符:

[cpp]  view plain  copy
  1. int main(int argc, char *argv[])  
  2. {  
  3.  intsockfd;  
  4.  intlen;  
  5.  struct sockaddr_in address;  
  6.  intresult;  
  7.  charch = 'A';  
  8.    
  9.  sockfd = socket(AF_INET, SOCK_STREAM, 0);  
  10.  address.sin_family = AF_INET;  
  11.  address.sin_addr.s_addr =inet_addr("127.0.0.1");  
  12.  address.sin_port = htons(49590);  
  13.  len= sizeof(address);  
  14.  result = connect(sockfd, (struct sockaddr*)&address, len);  
  15.    
  16.  if(result == -1)  
  17.  {  
  18.  perror("oops: client1");  
  19.  exit(1);  
  20.  }  
  21.  write(sockfd, &ch, 1);  
  22.  read(sockfd,&ch, 1);  
  23.  printf("char from server = %c\n",ch);  
  24.  close(sockfd);  
  25.  exit(0);  
  26. }  

第二部分服务器

Main函数,

[cpp]  view plain  copy
  1. int main(void)  
  2. {  
  3.  intserver_sock = -1;  
  4.  u_short port = 0;  
  5.  intclient_sock = -1;  
  6.  struct sockaddr_in client_name;  
  7.  intclient_name_len = sizeof(client_name);  
  8.  pthread_t newthread;  
  9.    
  10.  server_sock = startup(&port);  
  11.  printf("httpd running on port %d\n",port);  
  12.  printf("httpd server_sock: %d\n",server_sock);  
  13.    
  14.  while (1)  
  15.  {  
  16.  /*套接字收到客户端连接请求*/   
  17.  client_sock = accept(server_sock,  
  18.                        (struct sockaddr*)&client_name,  
  19.                        (socklen_t*)&client_name_len);  
  20.    
  21.    
  22.  printf("httpd client_sock: %d\n", client_sock);  
  23.    
  24.   if(client_sock == -1)  
  25.   error_die("accept");  
  26.    
  27.   /*派生新线程用accept_request 函数处理新请求*/   
  28.  /*accept_request(client_sock); */  
  29.  if(pthread_create(&newthread , NULL, (void *)accept_request, client_sock) !=0)  
  30.   perror("pthread_create");  
  31.  }  
  32.    
  33.  close(server_sock);  
  34.    
  35.  return(0);  
  36. }  

程序创建一个服务器socket对象,然后绑写(bind),并监听指定的端口(listen),这些动作都是在startup函数实现的,

[cpp]  view plain  copy
  1. int startup(u_short *port)  
  2. {  
  3.  inthttpd = 0;  
  4.  struct sockaddr_in name;  
  5.    
  6.  httpd = socket(PF_INET, SOCK_STREAM, 0);  
  7.  if(httpd == -1)  
  8.  error_die("socket");  
  9.  memset(&name, 0, sizeof(name));  
  10.  name.sin_family = AF_INET;  
  11.  name.sin_port = htons(*port);  
  12.  name.sin_addr.s_addr = htonl(INADDR_ANY);  
  13.  if(bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)  
  14.  error_die("bind");  
  15.    
  16.  if(*port == 0)  /* if dynamicallyallocating a port */  
  17.  {  
  18.   intnamelen = sizeof(name);  
  19.   if(getsockname(httpd, (struct sockaddr *)&name, (socklen_t *)&namelen) ==-1)  
  20.   error_die("getsockname");  
  21.  *port = ntohs(name.sin_port);  
  22.  }  
  23.  if(listen(httpd, 5) < 0)  
  24.  error_die("listen");  
  25.  return(httpd);  
  26. }  
有一个细节稍微注意一下,如果传来的端口是0,程序会随机分配一个监听的端口。并通过指向port变量的地址返回该值。

 

接下来进入循环,服务器通过调用accept等待客户端的连接,Accept会以阻塞的方式运行,直接有客户端连接才会返回。连接成功后,服务器启动一个新的线程来处理客户端的请求(accept_request),处理完成后,重新等待新的客户端请求。

核心函数是accept_request,它的实现如下,

[cpp]  view plain  copy
  1. numchars = get_line(client, buf,sizeof(buf));  
  2.    
  3.  i =0; j = 0;  
  4.  while (!ISspace(buf[j]) && (i <sizeof(method) - 1))  
  5.  {  
  6.  method[i] = buf[j];  
  7.  i++; j++;  
  8.  }  
  9.  method[i] = '\0';  
  10.    

一个HTTP请求报文由请求行(requestline)、请求头部(header)、空行和请求数据4个部分组成 ,请求行由请求方法字段(get或post)、URL字段和HTTP协议版本字段3个字段组成,它们用空格分隔。例如,

GET /index.html HTTP/1.1。

 上面这段代码就是解析请求行,把方法字段保存在method变量中。关于http协议的方法有不明白的可以自已查下。

 

继续看,

[cpp]  view plain  copy
  1. if (strcasecmp(method, "GET")&& strcasecmp(method, "POST"))  
  2.  {  
  3.  unimplemented(client);  
  4.  return NULL;  
  5.  }  
只能识别get或post

 

 

[cpp]  view plain  copy
  1. i =0;  
  2. while (ISspace(buf[j]) && (j <sizeof(buf)))  
  3. j++;  
  4. while (!ISspace(buf[j]) && (i <sizeof(url) - 1) && (j < sizeof(buf)))  
  5. {  
  6. url[i] = buf[j];  
  7. i++; j++;  
  8. }  
  9. url[i] = '\0'//保存url  

上面这一段代码解析并保存请求的URL(如有问号,也包括问号及之后的内容,后面会讲到)。

 

继续看,

[cpp]  view plain  copy
  1. if(strcasecmp(method, "GET") == 0)  
  2. {  
  3. query_string = url;  
  4. while ((*query_string != '?') && (*query_string != '\0'))  
  5.  query_string++;  
  6.  if(*query_string == '?')  
  7.  {  
  8. ///*开启 cgi */  
  9.  cgi = 1;  
  10.  *query_string = '\0';  
  11.  query_string++;  
  12.  }  
  13. }  
如果是get方法,请求参数和对应的值附加在URL后面, 利用一个问号(“?”)代表URL的结尾与请求参数的开始,传递参数长度受限制。例如,/index.jsp?10023

其中10023就是要传递的参数。这段代码把参数保存在query_string中。

 

下面这段代码保存有效的url地址并加上请求地址的主页索引。默认的根目录是在htdocs下。

[cpp]  view plain  copy
  1. sprintf(path, "htdocs%s", url);//如果有问号,取url问号前面的部分  
  2. if(path[strlen(path) - 1] == '/')  
  3. strcat(path, "index.html");  

接下来的代码,访问请求的文件,如果文件不存在直接返回,如果存在就调用CGI程序来处理。

[cpp]  view plain  copy
  1.  if(stat(path, &st) == -1)  
  2.  {  
  3.  while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */  
  4.   numchars = get_line(client, buf, sizeof(buf));  
  5.  not_found(client);  
  6.  }  
  7.  else  
  8.  {  
  9.   if((st.st_mode & S_IFMT) == S_IFDIR)//目录类型  
  10.   strcat(path, "/index.html");  
  11.    
  12.   if ((st.st_mode & S_IXUSR) ||  
  13.      (st.st_mode & S_IXGRP) ||  
  14.      (st.st_mode & S_IXOTH)    )  
  15.   cgi = 1;  
  16.    
  17.  printf("accept_request cgi:%d\n", cgi);  
  18.   if(!cgi)   serve_file(client, path);  
  19.  else  
  20.   execute_cgi(client, path, method, query_string);//query_string }  
  21.    
  22. close(client);  
  23.  return NULL;  
  24. }  


如果需要调用cgi(cgi标志位置1)在调用cgi之前有一段是对用户权限的判断,对应的含义如下:

[html]  view plain  copy
  1. S_IXUSR:用户可以执行  
  2. S_IXGRP:组可以执行  
  3. S_IXOTH:其它人可以执行  


先来看看serve_file函数,它返回一个静态文本,比较简单。

[cpp]  view plain  copy
  1. buf[0] = 'A'; buf[1] = '\0';  
  2.  while ((numchars > 0) &&strcmp("\n", buf))  /* read& discard headers */  
  3.  numchars = get_line(client, buf, sizeof(buf));  

这里是继续读完客户端发来的请求头(前面已经读完了请求行),最后一个请求头之后是一个空行,通过换行符可以读完所有的请求头。成功打开文件之后,先组织http响应报文的头部,headers函数处理,

[cpp]  view plain  copy
  1. void headers(int client, const char*filename)  
  2. {  
  3.  charbuf[1024];  
  4.  (void)filename;  /* could use filename to determine file type*/  
  5.    
  6.  strcpy(buf, "HTTP/1.0 200 OK\r\n");//status line  
  7.  send(client, buf, strlen(buf), 0);  
  8.    
  9.  strcpy(buf, SERVER_STRING);//server header  
  10.  send(client, buf, strlen(buf), 0);  
  11.    
  12.  sprintf(buf, "Content-Type:text/html\r\n");  
  13.  send(client, buf, strlen(buf), 0);  
  14.    
  15.  strcpy(buf, "\r\n");  
  16.  send(client, buf, strlen(buf), 0);  
  17. }  

接下来调用cat函数,把文件中读到的内容作为http响应报文的数据部分发送回客户端:

[cpp]  view plain  copy
  1. void cat(int client, FILE *resource)  
  2. {  
  3.  charbuf[1024] = {0};  
  4.    
  5.  fgets(buf, sizeof(buf), resource);  
  6.  while (!feof(resource))  
  7.  {  
  8.  send(client, buf, strlen(buf), 0);  
  9.  fgets(buf, sizeof(buf), resource);  
  10.  }  
  11. }  


未完待续。


猜你喜欢

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