Tinyhttpd源码剖析(一)

转载请注明出处

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


作者:小马


一 介绍

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


源码下载地址:

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

 

二 关于CGI

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

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


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


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

 

三 源码分析

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

 

第一部分客户端

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

int main(int argc, char *argv[])
{
 intsockfd;
 intlen;
 struct sockaddr_in address;
 intresult;
 charch = 'A';
 
 sockfd = socket(AF_INET, SOCK_STREAM, 0);
 address.sin_family = AF_INET;
 address.sin_addr.s_addr =inet_addr("127.0.0.1");
 address.sin_port = htons(49590);
 len= sizeof(address);
 result = connect(sockfd, (struct sockaddr*)&address, len);
 
 if(result == -1)
 {
 perror("oops: client1");
 exit(1);
 }
 write(sockfd, &ch, 1);
 read(sockfd,&ch, 1);
 printf("char from server = %c\n",ch);
 close(sockfd);
 exit(0);
}

第二部分服务器

Main函数,

int main(void)
{
 intserver_sock = -1;
 u_short port = 0;
 intclient_sock = -1;
 struct sockaddr_in client_name;
 intclient_name_len = sizeof(client_name);
 pthread_t newthread;
 
 server_sock = startup(&port);
 printf("httpd running on port %d\n",port);
 printf("httpd server_sock: %d\n",server_sock);
 
 while (1)
 {
 /*套接字收到客户端连接请求*/ 
 client_sock = accept(server_sock,
                       (struct sockaddr*)&client_name,
                       (socklen_t*)&client_name_len);
 
 
 printf("httpd client_sock: %d\n", client_sock);
 
  if(client_sock == -1)
  error_die("accept");
 
  /*派生新线程用accept_request 函数处理新请求*/ 
 /*accept_request(client_sock); */
 if(pthread_create(&newthread , NULL, (void *)accept_request, client_sock) !=0)
  perror("pthread_create");
 }
 
 close(server_sock);
 
 return(0);
}

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

int startup(u_short *port)
{
 inthttpd = 0;
 struct sockaddr_in name;
 
 httpd = socket(PF_INET, SOCK_STREAM, 0);
 if(httpd == -1)
 error_die("socket");
 memset(&name, 0, sizeof(name));
 name.sin_family = AF_INET;
 name.sin_port = htons(*port);
 name.sin_addr.s_addr = htonl(INADDR_ANY);
 if(bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
 error_die("bind");
 
 if(*port == 0)  /* if dynamicallyallocating a port */
 {
  intnamelen = sizeof(name);
  if(getsockname(httpd, (struct sockaddr *)&name, (socklen_t *)&namelen) ==-1)
  error_die("getsockname");
 *port = ntohs(name.sin_port);
 }
 if(listen(httpd, 5) < 0)
 error_die("listen");
 return(httpd);
}
有一个细节稍微注意一下,如果传来的端口是0,程序会随机分配一个监听的端口。并通过指向port变量的地址返回该值。

 

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

while (1)
 {
 client_sock = accept(server_sock,
                       (struct sockaddr*)&client_name,
                       (socklen_t*)&client_name_len);
 
 
 printf("httpd client_sock: %d\n", client_sock);
 
  if(client_sock == -1)
  error_die("accept");
 
 
 /*accept_request(client_sock); */
 if(pthread_create(&newthread , NULL, (void *)accept_request, client_sock) !=0)
  perror("pthread_create");
 }

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

numchars = get_line(client, buf,sizeof(buf));
 
 i =0; j = 0;
 while (!ISspace(buf[j]) && (i <sizeof(method) - 1))
 {
 method[i] = buf[j];
 i++; j++;
 }
 method[i] = '\0';
 

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

GET /index.html HTTP/1.1。

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

 

继续看,

if (strcasecmp(method, "GET")&& strcasecmp(method, "POST"))
 {
 unimplemented(client);
 return NULL;
 }
只能识别get或post

 

 

 i =0;
 while (ISspace(buf[j]) && (j <sizeof(buf)))
 j++;
 while (!ISspace(buf[j]) && (i <sizeof(url) - 1) && (j < sizeof(buf)))
 {
 url[i] = buf[j];
 i++; j++;
 }
 url[i] = '\0'; //保存url

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

 

继续看,

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

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

 

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

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

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

 if(stat(path, &st) == -1)
 {
 while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
  numchars = get_line(client, buf, sizeof(buf));
 not_found(client);
 }
 else
 {
  if((st.st_mode & S_IFMT) == S_IFDIR)//目录类型
  strcat(path, "/index.html");
 
  if ((st.st_mode & S_IXUSR) ||
     (st.st_mode & S_IXGRP) ||
     (st.st_mode & S_IXOTH)    )
  cgi = 1;
 
 printf("accept_request cgi:%d\n", cgi);
  if(!cgi)   serve_file(client, path);
 else
  execute_cgi(client, path, method, query_string);//query_string }
 
close(client);
 return NULL;
}


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

S_IXUSR:用户可以执行
S_IXGRP:组可以执行
S_IXOTH:其它人可以执行


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

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

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

void headers(int client, const char*filename)
{
 charbuf[1024];
 (void)filename;  /* could use filename to determine file type*/
 
 strcpy(buf, "HTTP/1.0 200 OK\r\n");//status line
 send(client, buf, strlen(buf), 0);
 
 strcpy(buf, SERVER_STRING);//server header
 send(client, buf, strlen(buf), 0);
 
 sprintf(buf, "Content-Type:text/html\r\n");
 send(client, buf, strlen(buf), 0);
 
 strcpy(buf, "\r\n");
 send(client, buf, strlen(buf), 0);
}

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

void cat(int client, FILE *resource)
{
 charbuf[1024] = {0};
 
 fgets(buf, sizeof(buf), resource);
 while (!feof(resource))
 {
 send(client, buf, strlen(buf), 0);
 fgets(buf, sizeof(buf), resource);
 }
}


未完待续。

下一篇地址:

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


你可能感兴趣的:(http协议,网络编程,环境变量,fork,tinyhttpd)