Tinyhttpd源码分析

Tinyhttpd是一个超轻量级的http服务器,使用C语言开发,代码只有500多行,不用于实际生产, 只是为了学习使用,通过阅读代码可以理解初步web服务器的本质,下面是学习Tinyhttpd的相关资料:

一、HTTP协议

在阅读源码之间, 我们先要初步了解HTTP协议。HTTP协议(HyperText Transfer Protocol,超文本传输协议)是因特网上应用最为广泛的一种网络传输协议,所有的WWW文件都必须遵守这个标准。简单地说HTTP协议就是规定了客户端和服务器的通信格式, 它建立在TCP协议的基础上, 默认使用80端口,也可以改为8080端口或者其他端口,并不涉及数据包的传输, 只规定了通信的规范。

HTTP有如下特点:

  • 支持客户/服务器模式
  • 客户向服务器请求服务时,只需传送请求方法和路径,请求方法常用的有GET、HEAD、POST
  • 允许传输任意类型的数据对象。
  • 无连接,无连接的含义是限制每次连接只处理一个请求,服务器处理完客户的请求,并收到客户的应答后,即断开连接。
  • 无状态,无状态是指协议对于事务处理没有记忆能力。

HTTP 1.0的协议通信过程如下,当连接建立后,浏览器发送一个请求,服务器回应一个消息,之后,连接就被关闭。当浏览器下次请求的时候,需要重新建立连接。

HTTP 1.1的协议通信过程如下,在HTTP1.1 版本中,给出了持续连接的机制,通过这种连接,浏览器可以在建立一个连接之后,发送请求并得到回应,然后继续发送请求并再次得到回应。这样比较节省时间,因为连接的建立是需要时间的。

根据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 回显服务器收到的请求,主要用于测试或诊断。

二、SOCKET

1、socket是什么

 Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。socket位置如图所示:

2、如何使用socket

先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。

3、基本函数

  • socket()函数
int socket(int domain, int type, int protocol);

socket对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。函数参数中,domain为协议域,又称为协议族(family),type为指定socket类型,protocol是指定协议

  • bind()函数
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。sockfd为socket描述字,它是通过socket()函数创建了,唯一标识一个socket。addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。addrlen为对应的是地址的长度。

  • listen()、connect()
int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。

  • accept()函数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。

  • read()、write()等函数
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1,并设置errno变量。在网络程序中,当我们向套接字文件描述符写时有俩种可能。write的返回值大于0,表示写了部分或者是全部的数据。返回的值小于0,此时出现了错误。

  • close()函数
int close(int fd);

在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。

 

三、Tinyhttpd源码分析

  • 主函数如下:
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;

 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);
}
  • 第一步:初始化httpd服务,包括建立套接字,绑定端口,进行监听。
server_sock = startup(&port);

startup函数如下:


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

首先创建一个socket,协议域为PF_INNET,使用 IPv4 网络协议,类型为SOCK_STREAM,即面向TCP的。然后调用bind函数把一个地址族中的特定地址赋给socket,返回绑定的端口,最后进行监听申请的连接。

  • 第二步:TCP服务器监听到请求,调用accept()函数取接收请求,建立连接。
client_sock = accept(server_sock,
                       (struct sockaddr *)&client_name,
                       &client_name_len);
  • 第三步:创建线程运行accept_request函数
if (pthread_create(&newthread, NULL, (void *)accept_request, (void *)(intptr_t)client_sock) != 0)
	  perror("pthread_create");

accept_request函数如下:

void accept_request(void *arg)
{
  //socket
 int client = (intptr_t)arg;
 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;
 //根据上面的Get请求,可以看到这边就是取第一行
 //这边都是在处理第一条http信息
 //"GET / HTTP/1.1\n"
 numchars = get_line(client, buf, sizeof(buf));
 i = 0; j = 0;

 //第一行字符串提取Get
 while (!ISspace(buf[j]) && (i < sizeof(method) - 1))
 {
  method[i] = buf[j];
  i++; j++;
 }
 //结束
 method[i] = '\0';

 //判断是Get还是Post
 if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
 {
  unimplemented(client);
  return;
 }

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

 i = 0;
 //跳过空格
 while (ISspace(buf[j]) && (j < sizeof(buf)))
  j++;

 //得到 "/"   注意:如果你的http的网址为http://192.168.0.23:47310/index.html
 //               那么你得到的第一条http信息为GET /index.html HTTP/1.1,那么
 //               解析得到的就是/index.html
 while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf)))
 {
  url[i] = buf[j];
  i++; j++;
 }
 url[i] = '\0';

 //判断Get请求
 if (strcasecmp(method, "GET") == 0)
 {
  query_string = url;
  while ((*query_string != '?') && (*query_string != '\0'))
   query_string++;
  if (*query_string == '?')
  {
   cgi = 1;
   *query_string = '\0';
   query_string++;
  }
 }

 //路径
 sprintf(path, "htdocs%s", url);

 //默认地址,解析到的路径如果为/,则自动加上index.html
 if (path[strlen(path) - 1] == '/')
  strcat(path, "index.html");

 //获得文件信息
 if (stat(path, &st) == -1) {
  //把所有http信息读出然后丢弃
  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");
  //如果你的文件默认是有执行权限的,自动解析成cgi程序,如果有执行权限但是不能执行,会接受到报错信号
  if ((st.st_mode & S_IXUSR) ||
      (st.st_mode & S_IXGRP) ||
      (st.st_mode & S_IXOTH)    )
   cgi = 1;
  if (!cgi)
   //接读取文件返回给请求的http客户端
   serve_file(client, path);
  else
   //执行cgi文件
   execute_cgi(client, path, method, query_string);
 }
 //执行完毕关闭socket
 close(client);
}

首先获取http请求的第一行,取出http请求中method(get或post)和url,对于get方法,如果有携带参数,则query_string指针指向url中后面的get参数。格式化url到path数组,表示浏览器请求的文件路径,在tinyhttpd中服务器文件是在htdocs文件夹下。当url以/结尾,或者url是个目录,则默认在path中加上index.thml,表示访问主页。如果文件路径合法,对于无参数的get请求,直接输出服务器文件到浏览器,即用http格式写到套接字上。其他情况(带参数get,post方法,url为可执行文件),则调用execute_cgi函数执行cgi脚本。最后关闭socket。

cgi脚本执行:

void execute_cgi(int client, const char *path,
                 const char *method, const char *query_string)
{
//缓冲区
 char buf[1024];

 //2根管道
 int cgi_output[2];
 int cgi_input[2];

 //进程pid和状态
 pid_t pid;
 int status;

 int i;
 char c;
 
 //读取的字符数
 int numchars = 1;

 //http的content_length
 int content_length = -1;

 //默认字符
 buf[0] = 'A'; buf[1] = '\0';

 //忽略大小写比较字符串
 if (strcasecmp(method, "GET") == 0)
 //读取数据,把整个header都读掉,以为Get写死了直接读取index.html,没有必要分析余下的http信息了
  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))
  {
   //如果是POST请求,就需要得到Content-Length,Content-Length:这个字符串一共长为15位,所以
   //取出头部一句后,将第16位设置结束符,进行比较
   //第16位置为结束
   buf[15] = '\0';
   if (strcasecmp(buf, "Content-Length:") == 0)
   //内存从第17位开始就是长度,将17位开始的所有字符串转成整数就是content_length
    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);
 //建立output管道
 if (pipe(cgi_output) < 0) {
  cannot_execute(client);
  return;
 }

 //建立input管道
 if (pipe(cgi_input) < 0) {
  cannot_execute(client);
  return;
 }
 //       fork后管道都复制了一份,都是一样的
 //       子进程关闭2个无用的端口,避免浪费             
 //       ×<------------------------->1    output
 //       0<-------------------------->×   input 

 //       父进程关闭2个无用的端口,避免浪费             
 //       0<-------------------------->×   output
 //       ×<------------------------->1    input
 //       此时父子进程已经可以通信


 //fork进程,子进程用于执行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];

  //子进程输出重定向到output管道的1端
  dup2(cgi_output[1], 1);
  //子进程输入重定向到input管道的0端
  dup2(cgi_input[0], 0);

  //关闭无用管道口
  close(cgi_output[0]);
  close(cgi_input[1]);

  //CGI环境变量
  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);
  }
  //替换执行path
  execl(path, path, NULL);
  //int m = execl(path, path, NULL);
  //如果path有问题,例如将html网页改成可执行的,但是执行后m为-1
  //退出子进程,管道被破坏,但是父进程还在往里面写东西,触发Program received signal SIGPIPE, Broken pipe.
  exit(0);
 } else {    /* parent */

  //关闭无用管道口
  close(cgi_output[1]);
  close(cgi_input[0]);
  if (strcasecmp(method, "POST") == 0)
   for (i = 0; i < content_length; i++) {
	//得到post请求数据,写到input管道中,供子进程使用
    recv(client, &c, 1, 0);
    write(cgi_input[1], &c, 1);
   }
  //从output管道读到子进程处理后的信息,然后send出去
  while (read(cgi_output[0], &c, 1) > 0)
   send(client, &c, 1, 0);

  //完成操作后关闭管道
  close(cgi_output[0]);
  close(cgi_input[1]);

  //等待子进程返回
  waitpid(pid, &status, 0);

 }
}

读取整个http请求并丢弃,如果是post则找出content-length,把http状态码200写到套接字里面。建立两个管道,cgi_input和cgi_output,并fork一个子进程。在子进程中,把stdout重定向到cgi_output的写入端,把stdin重定向到cgi_input的读取端,关闭cgi_input的写入端和cgi_output的读取端,是指request_method的环境变量,get的话设置query_string的环境变量,post的话设置content-length的环境变量,这些环境变量都是为了给cgi脚本调用,接着用execl运行cgi程序。在父进程中,关闭cgi_input的读取端和cgi_output的写入端,如果post的话,把post数据写入到cgo_input,已被重定向到stdin读取cgi_output的管道输出到客户端,等待子进程结束。

猜你喜欢

转载自blog.csdn.net/qq_15391889/article/details/84404071
今日推荐