1、简介
tinyhttpd是一个开源的超轻量型Http Server,阅读其源码,可以对http协议,微型服务器有进一步的了解。
源码链接;
参考博客:tinyhttpd源码分析
2、笔记
------------------------------------------------------2016年5月7日21:31:35----------------------------------------------------
函数原型目录,列举出整个项目所要用到的所有函数原型
1 void accept_request(void *); 2 void bad_request(int); 3 void cat(int, FILE *); 4 void cannot_execute(int); 5 void error_die(const char *); 6 void execute_cgi(int, const char *, const char *, const char *); 7 int get_line(int, char *, int); 8 void headers(int, const char *); 9 void not_found(int); 10 void serve_file(int, const char *); 11 int startup(u_short *); 12 void unimplemented(int);
每个函数的作用在项目的readme文件中有简单的描述:
函数 | 描述 |
accept_request | 处理从套接字上监听到的一个 HTTP 请求,在这里可以很大一部分地体现服务器处理请求流程 |
bad_request | 返回给客户端这是个错误请求,HTTP 状态吗 400 BAD REQUEST |
cat | 读取服务器上某个文件写到 socket 套接字 |
cannot_execute | 主要处理发生在执行 cgi 程序时出现的错误 |
error_die | 把错误信息写到 perror 并退出 |
execute_cgi | 运行 cgi 程序的处理,也是个主要函数 |
get_line | 读取套接字的一行,把回车换行等情况都统一为换行符结束 |
headers | 把 HTTP 响应的头部写到套接字 |
not_found | 主要处理找不到请求的文件时的情况 |
sever_file | 调用 cat 把服务器文件返回给浏览器 |
startup | 初始化 httpd 服务,包括建立套接字,绑定端口,进行监听等 |
unimplemented | 返回给浏览器表明收到的 HTTP 请求所用的 method 不被支持 |
建议的源码阅读顺序:
main--->startup--->accept_request--->execute_cgi
按照这个顺序理清整个服务器工作流程。
main函数
1 int main(void) 2 { 3 int server_sock = -1; 4 u_short port = 4000; 5 int client_sock = -1; 6 struct sockaddr_in client_name; 7 socklen_t client_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 13 while (1) 14 { 15 client_sock = accept(server_sock, 16 (struct sockaddr *)&client_name, 17 &client_name_len); 18 if (client_sock == -1) 19 error_die("accept"); 20 /* accept_request(client_sock); */ 21 if (pthread_create(&newthread , NULL, (void *)accept_request, (void *)&client_sock) != 0) 22 perror("pthread_create"); 23 } 24 25 close(server_sock); 26 27 return(0); 28 }
第3-7行是socket相关变量定义,有关socket的详细介绍可以上网查阅相关资料或者查看书籍《Unix网络编程》;
第10行调用startup函数,有关该函数的内容后续简要解释;
第13-23行,监听客户端向服务端发来的请求连接,当接收一个连接之后,创建一个线程执行相应的请求,线程入口函数是accept_request,另外有关Linux中pthread_create的使用需要注意一些细节问题。
第25行关闭服务端的套接字。
startup函数
函数参数是一个端口号,用于创建对应端口号的tcp套接字
1 int startup(u_short *port) 2 { 3 int httpd = 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 if (*port == 0) /* if dynamically allocating a port */ 16 { 17 socklen_t namelen = sizeof(name); 18 if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1) 19 error_die("getsockname"); 20 *port = ntohs(name.sin_port); 21 } 22 if (listen(httpd, 5) < 0) 23 error_die("listen"); 24 return(httpd); 25 }
第6行创建一个socket套接字,由于其参数是SOCK_STREAM,因此创建的是tcp套接字;
第7-8行,如果套接字创建不成功,则调用封装好的错误处理函数error_die,该函数比较简单,就是打印错误信息并结束整个程序。
第13-23则是创建socket服务端的基本流程,先是bind操作,接下来listen操作;
注意第15行,如果port为0,表示端口号是通过系统自动分配的,需要通过getsockname获取,有关这方面的知识查看《Unix网络编程》书籍
Startup函数执行完成后,服务端的准备工作完成,将进入监听状态,等待客户端的连接,通过accept函数判断是否有客户端请求连接,如果有,服务端创建一个新的线程执行客户请求,线程的入口函数是accept_request;
accept_request函数
该函数的参数是个void*,实际传递给函数的参数是一个套接字描述符。
1 void accept_request(void *arg) 2 { 3 int client = *(int*)arg; 4 char buf[1024]; 5 size_t numchars; 6 char method[255]; 7 char url[255]; 8 char path[512]; 9 size_t i, j; 10 struct stat st; 11 int cgi = 0; /* becomes true if server decides this is a CGI 12 * program */ 13 char *query_string = NULL; 14 15 numchars = get_line(client, buf, sizeof(buf)); 16 i = 0; j = 0; 17 while (!ISspace(buf[i]) && (i < sizeof(method) - 1)) 18 { 19 method[i] = buf[i]; 20 i++; 21 } 22 j=i; 23 method[i] = '\0'; 24 25 if (strcasecmp(method, "GET") && strcasecmp(method, "POST")) 26 { 27 unimplemented(client); 28 return; 29 } 30 31 if (strcasecmp(method, "POST") == 0) 32 cgi = 1; 33 34 i = 0; 35 while (ISspace(buf[j]) && (j < numchars)) 36 j++; 37 while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < numchars)) 38 { 39 url[i] = buf[j]; 40 i++; j++; 41 } 42 url[i] = '\0'; 43 44 if (strcasecmp(method, "GET") == 0) 45 { 46 query_string = url; 47 while ((*query_string != '?') && (*query_string != '\0')) 48 query_string++; 49 if (*query_string == '?') 50 { 51 cgi = 1; 52 *query_string = '\0'; 53 query_string++; 54 } 55 } 56 57 sprintf(path, "htdocs%s", url); 58 if (path[strlen(path) - 1] == '/') 59 strcat(path, "index.html"); 60 if (stat(path, &st) == -1) { 61 while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */ 62 numchars = get_line(client, buf, sizeof(buf)); 63 not_found(client); 64 } 65 else 66 { 67 if ((st.st_mode & S_IFMT) == S_IFDIR) 68 strcat(path, "/index.html"); 69 if ((st.st_mode & S_IXUSR) || 70 (st.st_mode & S_IXGRP) || 71 (st.st_mode & S_IXOTH) ) 72 cgi = 1; 73 if (!cgi) 74 serve_file(client, path); 75 else 76 execute_cgi(client, path, method, query_string); 77 } 78 79 close(client); 80 }
第3-13行,变量说明部分;
第15行,通过get_line函数,从指定的套接字描述符的缓存中读取一行数据,数据长度为size,但是如果遇到'\n',函数直接返回,也就是这一行内容最多有size字节,函数返回实际读取到的字节数。
第17-23行,获取http请求头部;理解这一部分需要对http协议报文格式有所了解