tinyhttpd

源码分析

Posted by Liangjf on March 30, 2018

tinyhttpd源码分析

tinyhttpd是一个轻量级的http服务器,代码量只有500多行的源码,但麻雀虽小五脏俱全,通过对其源码分析,能够对http从请求到服务器响应,都有一个完整的了解。

在分析源码前,先对http有一定的了解,请看下面内容。

HTTP 消息结构

  • HTTP是基于客户端/服务端(C/S)的架构模型,通过一个可靠的链接来交换信息,是一个无状态的请求/响应协议。
  • 一个HTTP”客户端”是一个应用程序,通过连接到服务器达到向服务器发送一个或多个HTTP的请求的目的。
  • 一个HTTP”服务器”同样也是一个应用程序(通常是一个Web服务),通过接收客户端的请求并向客户端发送HTTP响应数据。

HTTP请求的方法

根据HTTP标准,HTTP请求可以使用多种请求方法。

  • HTTP1.0定义了三种请求方法: GET, POST 和 HEAD方法。
  • HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法。
  • 1.GET:请求指定的页面信息,并返回实体主体。
  • 2.HEAD:类似于get请求,只不过返回的响应中没有具体的内容,用于获取报头
  • 3.POST:向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改。
  • 4.PUT:从客户端向服务器传送的数据取代指定的文档的内容。
  • 5.DELETE:请求服务器删除指定的页面。
  • 6.CONNECT:HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。
  • 7.OPTIONS:允许客户端查看服务器的性能。
  • 8.TRACE:回显服务器收到的请求,主要用于测试或诊断。

常见的HTTP状态码:

  • 200 - 请求成功
  • 301 - 资源(网页等)被永久转移到其它URL
  • 404 - 请求的资源(网页等)不存在
  • 500 - 内部服务器错误

更详细的请自行查看,一般这种东西记住大概的200,300,400,500范围的是代表什么,其他仔细的用到再查,不然浪费大脑的容量了。

以上就是HTTP协议的简单介绍,下面是对tinyhttpd的个人分析。

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>
#include <strings.h>
#include <string.h>
#include <sys/stat.h>
#include <pthread.h>
#include <sys/wait.h>
#include <stdlib.h>

#define ISspace(x) isspace((int)(x))

#define SERVER_STRING "Server: jdbhttpd/0.1.0\r\n"

void accept_request(int);
void bad_request(int);
void cat(int, FILE *);
void cannot_execute(int);
void error_die(const char *);
void execute_cgi(int, const char *, const char *, const char *);
int get_line(int, char *, int);
void headers(int, const char *);
void not_found(int);
void serve_file(int, const char *);
int startup(u_short *);
void unimplemented(int);

void accept_request(int client)
{
 char buf[1024];
 int numchars;
 char method[255];
 char url[255];
 char path[512];
 size_t i, j;
 struct stat st;
 int cgi = 0;      /* becomes true if server decides this is a CGI
                    * program */
 char *query_string = NULL;

 numchars = get_line(client, buf, sizeof(buf));//获取一行数据
 i = 0; j = 0;
 while (!ISspace(buf[j]) && (i < sizeof(method) - 1))//循环判断直至遇到空格,和请求有关,GET XXX XXX XXX / POST xxx xxx xxx
 {
  method[i] = buf[j];//获取请求方法是GET还是POST
  i++; j++;
 }
 method[i] = '\0';

 //如果非GET/POST请求方法,返回以下信息给客户端
 if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
 {
  unimplemented(client);
  return;
 }

//如果是POST请求
 if (strcasecmp(method, "POST") == 0)
  cgi = 1;

 i = 0;
 while (ISspace(buf[j]) && (j < sizeof(buf)))
  j++; //POST后面的一个空格

  //获取到请求的url
 while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf)))
 {
  url[i] = buf[j];
  i++; j++;
 }
 url[i] = '\0';

 //如果是GET请求,url带有参数,?后的键值对形式
 if (strcasecmp(method, "GET") == 0)
 {
  query_string = url;
  while ((*query_string != '?') && (*query_string != '\0')) //获取到真正的url
   query_string++;
  if (*query_string == '?')
  {
   cgi = 1; //GET请求带参数的话,就置位cgi标志位
   *query_string = '\0';
   query_string++; //此时 query_string 指向?后面的键值对参数
  }
 }

 sprintf(path, "htdocs%s", url);
 if (path[strlen(path) - 1] == '/') //拼接生成主页
  strcat(path, "index.html");
 if (stat(path, &st) == -1) { //判断index.html,若不存在
  while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
   numchars = get_line(client, buf, sizeof(buf));//获取请求头
  not_found(client);	//客户端输入的url在服务器找不到,返回404错误
 }
 else
 {//若url正确在服务器存在
  if ((st.st_mode & S_IFMT) == S_IFDIR)
   strcat(path, "/index.html");

  //把现在的权限置为root权限才能执行后面的响应,cgi执行等
  if ((st.st_mode & S_IXUSR) ||	
      (st.st_mode & S_IXGRP) ||
      (st.st_mode & S_IXOTH)    )
   cgi = 1;
  if (!cgi)
   serve_file(client, path); //若不是请求cgi,直接返回一个文件
  else
   execute_cgi(client, path, method, query_string); //若请求cgi,先执行cgi程序,再返回结果
 }

 close(client);
}

void bad_request(int client)
{
 char buf[1024];

 sprintf(buf, "HTTP/1.0 400 BAD REQUEST\r\n");
 send(client, buf, sizeof(buf), 0);
 sprintf(buf, "Content-type: text/html\r\n");
 send(client, buf, sizeof(buf), 0);
 sprintf(buf, "\r\n");
 send(client, buf, sizeof(buf), 0);
 sprintf(buf, "<P>Your browser sent a bad request, ");
 send(client, buf, sizeof(buf), 0);
 sprintf(buf, "such as a POST without a Content-Length.\r\n");
 send(client, buf, sizeof(buf), 0);
}

void cat(int client, FILE *resource)
{
 char buf[1024];

 fgets(buf, sizeof(buf), resource);
 while (!feof(resource))
 {
  send(client, buf, strlen(buf), 0);
  fgets(buf, sizeof(buf), resource);
 }
}

void cannot_execute(int client)
{
 char buf[1024];

 sprintf(buf, "HTTP/1.0 500 Internal Server Error\r\n");
 send(client, buf, strlen(buf), 0);
 sprintf(buf, "Content-type: text/html\r\n");
 send(client, buf, strlen(buf), 0);
 sprintf(buf, "\r\n");
 send(client, buf, strlen(buf), 0);
 sprintf(buf, "<P>Error prohibited CGI execution.\r\n");
 send(client, buf, strlen(buf), 0);
}

void error_die(const char *sc)
{
 perror(sc);
 exit(1);
}

void execute_cgi(int client, const char *path,
                 const char *method, const char *query_string)
{
 char buf[1024];
 int cgi_output[2];
 int cgi_input[2];
 pid_t pid;
 int status;
 int i;
 char c;
 int numchars = 1;
 int content_length = -1;

 buf[0] = 'A'; buf[1] = '\0';
 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);
   return;
  }
 }

 sprintf(buf, "HTTP/1.0 200 OK\r\n");
 send(client, buf, strlen(buf), 0);//返回200表示请求成功

//打开两个管道,互斥关闭一端,达到父子进程通信的功能
 if (pipe(cgi_output) < 0) {
  cannot_execute(client);
  return;
 }
 if (pipe(cgi_input) < 0) {
  cannot_execute(client);
  return;
 }

//创建一个子进程来执行cgi程序
 if ( (pid = fork()) < 0 ) {
  cannot_execute(client);
  return;
 }
 if (pid == 0)  /* child: CGI script */
 {
  char meth_env[255];
  char query_env[255];
  char length_env[255];

  dup2(cgi_output[1], 1);//cgi_output[1]写端 重定向为标准输出
  dup2(cgi_input[0], 0);//cgi_input[0]读端 重定向为标准输入
  close(cgi_output[0]);//cgi_output[0]读端关闭,节省fd等资源
  close(cgi_input[1]);//cgi_input[1]写端关闭,节省fd等资源
  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);
  }
  execl(path, path, NULL);//执行cgi可执行程序
  exit(0);
 } else {    /* parent */
  close(cgi_output[1]);//cgi_output[1]写端关闭,节省fd等资源           父---> ---->子  out   
  close(cgi_input[0]);//cgi_input[0]读端关闭,节省fd等资源             父<--- <----子in
  if (strcasecmp(method, "POST") == 0)
   for (i = 0; i < content_length; i++) {
    recv(client, &c, 1, 0); //如果是POST请求,父进程读取客户端信息并通过管道传给子进程
    write(cgi_input[1], &c, 1);
   }
  while (read(cgi_output[0], &c, 1) > 0)//子进程收到信息执行cgi并把cgi结果返回给父进程,父进程再发送给客户端
   send(client, &c, 1, 0);

  close(cgi_output[0]);
  close(cgi_input[1]);
  waitpid(pid, &status, 0);
 }
}

int get_line(int sock, char *buf, int size)
{
 int i = 0;
 char c = '\0';
 int n;

 while ((i < size - 1) && (c != '\n'))
 {
  n = recv(sock, &c, 1, 0);
  /* DEBUG printf("%02X\n", c); */
  if (n > 0)
  {
   if (c == '\r')
   {
    n = recv(sock, &c, 1, MSG_PEEK);
    /* DEBUG printf("%02X\n", c); */
    if ((n > 0) && (c == '\n'))
     recv(sock, &c, 1, 0);
    else
     c = '\n';
   }
   buf[i] = c;
   i++;
  }
  else
   c = '\n';
 }
 buf[i] = '\0';

 return(i);
}

void headers(int client, const char *filename)
{
 char buf[1024];
 (void)filename;  /* could use filename to determine file type */

 strcpy(buf, "HTTP/1.0 200 OK\r\n");
 send(client, buf, strlen(buf), 0);
 strcpy(buf, SERVER_STRING);
 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);
}

void not_found(int client)
{
 char buf[1024];

 sprintf(buf, "HTTP/1.0 404 NOT FOUND\r\n");
 send(client, buf, strlen(buf), 0);
 sprintf(buf, SERVER_STRING);
 send(client, buf, strlen(buf), 0);
 sprintf(buf, "Content-Type: text/html\r\n");
 send(client, buf, strlen(buf), 0);
 sprintf(buf, "\r\n");
 send(client, buf, strlen(buf), 0);
 sprintf(buf, "<HTML><TITLE>Not Found</TITLE>\r\n");
 send(client, buf, strlen(buf), 0);
 sprintf(buf, "<BODY><P>The server could not fulfill\r\n");
 send(client, buf, strlen(buf), 0);
 sprintf(buf, "your request because the resource specified\r\n");
 send(client, buf, strlen(buf), 0);
 sprintf(buf, "is unavailable or nonexistent.\r\n");
 send(client, buf, strlen(buf), 0);
 sprintf(buf, "</BODY></HTML>\r\n");
 send(client, buf, strlen(buf), 0);
}

void serve_file(int client, const char *filename)
{
 FILE *resource = NULL;
 int numchars = 1;
 char buf[1024];

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

//打开 htdocs/index.html
 resource = fopen(filename, "r");
 if (resource == NULL)
  not_found(client);
 else
 {
  headers(client, filename);//先发响应头给客户端
  cat(client, resource); //读取index.html文件内容并发送给客户端
 }
 fclose(resource);
}

int startup(u_short *port)
{
 int httpd = 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 dynamically allocating a port */
 {
  int namelen = sizeof(name);
  if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1)
   error_die("getsockname");
  *port = ntohs(name.sin_port);
 }
 if (listen(httpd, 5) < 0)
  error_die("listen");
 return(httpd);
}

void unimplemented(int client)
{
 char buf[1024];

 sprintf(buf, "HTTP/1.0 501 Method Not Implemented\r\n");
 send(client, buf, strlen(buf), 0);
 sprintf(buf, SERVER_STRING);
 send(client, buf, strlen(buf), 0);
 sprintf(buf, "Content-Type: text/html\r\n");
 send(client, buf, strlen(buf), 0);
 sprintf(buf, "\r\n");
 send(client, buf, strlen(buf), 0);
 sprintf(buf, "<HTML><HEAD><TITLE>Method Not Implemented\r\n");
 send(client, buf, strlen(buf), 0);
 sprintf(buf, "</TITLE></HEAD>\r\n");
 send(client, buf, strlen(buf), 0);
 sprintf(buf, "<BODY><P>HTTP request method not supported.\r\n");
 send(client, buf, strlen(buf), 0);
 sprintf(buf, "</BODY></HTML>\r\n");
 send(client, buf, strlen(buf), 0);
}

int main(void)
{
     int server_sock = -1;
     u_short port = 0;
     int client_sock = -1;
     struct sockaddr_in client_name;
     int client_name_len = sizeof(client_name);
     pthread_t newthread;

     //创建一个被动的socket fd
     server_sock = startup(&port);
     printf("httpd running on port %d\n", port);

     while (1)
     {
        //一直监听连接
        client_sock = accept(server_sock, (struct sockaddr *)&client_name, &client_name_len);
        if (client_sock == -1)
            error_die("accept");
        /* accept_request(client_sock); */
        //每一个新连接创建一条新的线程负责
        if (pthread_create(&newthread , NULL, accept_request, client_sock) != 0)
            perror("pthread_create");
    }

    close(server_sock);

    return(0);
}

其中主要的架构是,一个服务器进程,在while(1)循环中一直sccept接受客户端的请求,每连接成功一个客户端就为其创造一条线程来处理器请求及回传结果响应,然后关闭线程。

其中需要注意的是execute_cgi函数,里面创建两个管道并创建一个子进程专门用于执行cgi可执行文件,并通过管道来回传结果给父进程然后父进程回传响应给客户端。

创建管道和重定向,并关闭不需要的一端,示意图如下:

对其简化:

总的来说说,tinyhttpd只是一个很轻量级的http服务器,实际应用架构肯定比它更复杂,比如增加线程池,工作队列,多路io复用技术(epoll等)等。而且在实际中还可以借用一些优秀的开源框架和库(libev/boots:asio/libevent/ACE库等)