Tinyhttpd 是很早以前的一个 web 服务器程序,由 C 语言编写,整个程序十分小巧,源码只有几百行。它一般不适合用于生产环境,因为它很简单,只实现了读取 html 以及 Get / POST 两种方法,并且也只是简单支持了下,无法应对生产环境中的很多问题,生产环境还是要选拥有几十万行代码的成熟的 web服务器 :apache 和 nginx 。
不过 Tinyhttpd 因为过于小巧,所以对于初步了解服务器系统的基本运行原理很有帮助。
以下是我通过查阅相关资料后,对 tinyhttpd 的源码进行的一些注释解读。
运行环境是 mac os + gcc version 13.0.0
可以到 https://github.com/kohunglee/tinyhttpd 下载以下文件
httpd.c:
/* J. David\'s webserver *//* This is a simple webserver.* Created November 1999 by J. David Blackstone.* CSE 4344 (Network concepts), Prof. Zeigler* University of Texas at Arlington*//* This program compiles for Sparc Solaris 2.6.* To compile for Linux:* 1) Comment out the #include <pthread.h> line.* 2) Comment out the line that defines the variable newthread.* 3) Comment out the two lines that run pthread_create().* 4) Uncomment the line that runs accept_request().* 5) Remove -lsocket from the Makefile.*/#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>#include <stdint.h>#define ISspace(x) isspace((int)(x))#define SERVER_STRING "Server: jdbhttpd/0.1.0\\r\\n"#define STDIN 0#define STDOUT 1#define STDERR 2void accept_request(void *); // 处理链接,子线程void bad_request(int); // 400 错误void cat(int, FILE *); // 处理文件,读取文件内容,并发送到客户端void cannot_execute(int); // 500 错误处理函数void error_die(const char *); // 错误处理函数处理void execute_cgi(int, const char *, const char *, const char *); // 调用 CGIint get_line(int, char *, int); // 从缓存区读取一行void headers(int, const char *); // 服务器成功响应,返回200void not_found(int); // 请求的内容不存在 404void serve_file(int, const char *); // 处理文件请求int startup(u_short *); // 初始化服务器void unimplemented(int); // 501 仅实现了 get post 方法,其他方法处理函数/**********************************************************************//* A request has caused a call to accept() on the server port to* return. Process the request appropriately.* Parameters: the socket connected to the client *//**********************************************************************/// 处理链接,子线程void accept_request(void *arg){int client = (intptr_t)arg; // 建立 socket 描述符char buf[1024]; // 缓冲区size_t numchars;char method[255];char url[255]; // urlchar path[512]; // 路径的字符数组size_t i, j;struct stat st; // 文件状态信息,下面检查文件是否存在时会用到int cgi = 0; // 是否调用 cgi 程序/* becomes true if server decides this is a CGI* program */char *query_string = NULL;/* 添加 */pthread_detach(pthread_self()); // 子线程分离,在这个线程结束后,// 不需要其他的线程对他进行收尸// 开始对服务器进行读 第一行// get_line 就是解析 http 协议// http 协议第一行,请求方法、空格符、url、空格符、协议版本,这是第一行numchars = get_line(client, buf, sizeof(buf));i = 0; j = 0;// 这个循环就是在找空格符,判断第 i 个字符是不是空格// 并且,没有超过 method 缓冲的大小// 至于减去一,是因为要在最后面加一个 ’\\0‘ ,作为标识符while (!ISspace(buf[i]) && (i < sizeof(method) - 1)){method[i] = buf[i]; // 不是空格,就复制到 method 里面i++;}j=i;method[i] = \'\\0\';// 测试,打印方法printf("test:print the method-----%s\\n", method);// 仅实现了 GET 和 PUT 方法,别的方法还没有实现if (strcasecmp(method, "GET") && strcasecmp(method, "POST")){// 如果不是那两个方法,则调用 501 的错误处理函数unimplemented(client);return;}// 如果是 post 方法,将 cgi 设为1(下面会调用 cgi 来处理这些)if (strcasecmp(method, "POST") == 0)cgi = 1;// 下面该处理 url 了i = 0;while (ISspace(buf[j]) && (j < numchars))j++;while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < numchars)){// 如果不是空格的话,继续向 url 里进行复制,跟上面那个 method 方法一样url[i] = buf[j];i++; j++;}url[i] = \'\\0\'; // 读完后依然向最后加一个这个,以标识这是一个字符串// 如果是 GET 方法if (strcasecmp(method, "GET") == 0){query_string = url;// GET 方法,往往在 url 后面有 ?while ((*query_string != \'?\') && (*query_string != \'\\0\'))query_string++;// 逐个字符寻找 ’?‘ ,如果找到问号了,说明就是 get 提交的数据// 那么就需要 cgi 来处理数据,将 cgi 设置成 1// 并将 query_string 指向 ’?‘ 后的内容if (*query_string == \'?\'){cgi = 1;*query_string = \'\\0\';query_string++;}}// 如果 url 是一个目录// 那么就和 ’htdocs‘ 拼接,(也就是根目录)sprintf(path, "htdocs%s", url);if (path[strlen(path) - 1] == \'/\') // 如果最后一个字符是 ’/‘strcat(path, "index.html"); // 返回这个目录下的 html 文件,保证是个文件,而不是目录if (stat(path, &st) == -1) { // 检查拼接后的文件是否存在, -1 就是代表不存在while ((numchars > 0) && strcmp("\\n", buf)) /* read & discard headers */ // 读取,并丢弃 headersnumchars = get_line(client, buf, sizeof(buf));not_found(client); // 不存在,就返回 404_not_found}else // 如果文件存在{if ((st.st_mode & S_IFMT) == S_IFDIR) // 如果这个文件是一个目录的话strcat(path, "/index.html"); // 向下拼接 index.html (其实这两行不需要也行,上面已经拼接过了)if ((st.st_mode & S_IXUSR) ||(st.st_mode & S_IXGRP) ||(st.st_mode & S_IXOTH) ) // 检查权限,如果可执行的话,则 cgi = 1cgi = 1;if (!cgi) // 做最终判断serve_file(client, path); // cgi 等于 0 ,不需要调用 cgi ,相当于请求了个页面elseexecute_cgi(client, path, method, query_string); // 调用 cgi ,执行 cgi 程序// client:描述符、path:路径、method:请求的方法、query_string:判断是否有问号,以便使用 get 请求发送数据}close(client);}/**********************************************************************//* Inform the client that a request it has made has a problem.* Parameters: client socket *//**********************************************************************/// 400 错误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);}/**********************************************************************//* Put the entire contents of a file out on a socket. This function* is named after the UNIX "cat" command, because it might have been* easier just to do something like pipe, fork, and exec("cat").* Parameters: the client socket descriptor* FILE pointer for the file to cat *//**********************************************************************/// 处理文件,读取文件内容,并发送到客户端void cat(int client, FILE *resource){char buf[1024]; // 首先一个缓存区// 逐行读取,遇到换行符 eof 就停止fgets(buf, sizeof(buf), resource);while (!feof(resource)) // 是否已经读到了文件结尾,以确保读完整个文件{send(client, buf, strlen(buf), 0); // 到结尾后,send 发送到客户端fgets(buf, sizeof(buf), resource);}}/**********************************************************************//* Inform the client that a CGI script could not be executed.* Parameter: the client socket descriptor. *//**********************************************************************/// 500 错误处理函数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);}/**********************************************************************//* Print out an error message with perror() (for system errors; based* on value of errno, which indicates system call errors) and exit the* program indicating an error. *//**********************************************************************/// 错误处理函数处理void error_die(const char *sc){perror(sc);exit(1);}/**********************************************************************//* Execute a CGI script. Will need to set environment variables as* appropriate.* Parameters: client socket descriptor* path to the CGI script *//**********************************************************************/// 调用 CGIvoid 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) // 判断是不是 get 方法 ,如果是,则丢弃头部信息while ((numchars > 0) && strcmp("\\n", buf)) /* read & discard headers */numchars = get_line(client, buf, sizeof(buf));else if (strcasecmp(method, "POST") == 0) /*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])); // 比较前 15 个字符,如果等于 Content-Length: ,则转化为 intnumchars = get_line(client, buf, sizeof(buf));}if (content_length == -1) { // 如果不等于bad_request(client); // 错误的处理return;}}else/*HEAD or other*/{}sprintf(buf, "HTTP/1.0 200 OK\\r\\n"); // 上面成功执行,则向服务器发送成功的响应头部send(client, buf, strlen(buf), 0);// 初始化管道/*管道是为了在子线程里面的 cgi 和服务器调用 cgi 程序进程间通信用要创建两个管道1. 子线程向服务器端写的一个管道2. 子线程向服务器端读的一个管道*/if (pipe(cgi_output) < 0) {cannot_execute(client);return;}if (pipe(cgi_input) < 0) {cannot_execute(client);return;}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], STDOUT); // 0 文件( STDOUT )描述符重定向到管道读端dup2(cgi_input[0], STDIN); // 1 文件 ( STDIN )描述符重定向到管道写端close(cgi_output[0]); // 关闭不必要的读写端close(cgi_input[1]); // 子进程只需要从某一端读或某一端写,另一端是不需要的// 之后通过 cgi 写内容的话,是直接写到那个管道里而不是写到终端(或显示器)上sprintf(meth_env, "REQUEST_METHOD=%s", method); // 把 REQUEST_METHOD 写到环境变量里面( meth_env 这个变量可以写环境变量),是一种进行进程间通信的方式putenv(meth_env);if (strcasecmp(method, "GET") == 0) {// 如果是 get 方式,则向环境变量中写 QUERY_STRING ,以便让 cgi 程序知道 query_stringsprintf(query_env, "QUERY_STRING=%s", query_string);putenv(query_env);}else { /* POST */// 如果是 POST 方式。告诉 cgi 程序需要读多长的数据sprintf(length_env, "CONTENT_LENGTH=%d", content_length);putenv(length_env);}execl(path, NULL); // 处理完后,系统调用exit(0);} else { /* parent */// 父进程close(cgi_output[1]); // 先关闭不需要的管道( 读写端 )close(cgi_input[0]); // 父进程只需要管道的一端if (strcasecmp(method, "POST") == 0)for (i = 0; i < content_length; i++) {// 逐个字符读,然后写到管道里面recv(client, &c, 1, 0);fprintf(stderr,"%c\\n",c); // 测试write(cgi_input[1], &c, 1);}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); // 等待子进程结束}}/**********************************************************************//* Get a line from a socket, whether the line ends in a newline,* carriage return, or a CRLF combination. Terminates the string read* with a null character. If no newline indicator is found before the* end of the buffer, the string is terminated with a null. If any of* the above three line terminators is read, the last character of the* string will be a linefeed and the string will be terminated with a* null character.* Parameters: the socket descriptor* the buffer to save the data in* the size of the buffer* Returns: the number of bytes stored (excluding null) *//**********************************************************************/// 从缓存区读取一行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);elsec = \'\\n\';}buf[i] = c;i++;}elsec = \'\\n\';}buf[i] = \'\\0\';return(i);}/**********************************************************************//* Return the informational HTTP headers about a file. *//* Parameters: the socket to print the headers on* the name of the file *//**********************************************************************/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"); // 首http 的协议、状态码、oksend(client, buf, strlen(buf), 0);strcpy(buf, SERVER_STRING);send(client, buf, strlen(buf), 0);sprintf(buf, "Content-Type: text/html\\r\\n"); // 发送类型,htmlsend(client, buf, strlen(buf), 0);strcpy(buf, "\\r\\n"); // 结束后,空行send(client, buf, strlen(buf), 0);}/**********************************************************************//* Give a client a 404 not found status message. *//**********************************************************************/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);}/**********************************************************************//* Send a regular file to the client. Use headers, and report* errors to client if they occur.* Parameters: a pointer to a file structure produced from the socket* file descriptor* the name of the file to serve *//**********************************************************************/// client 是建立连接的 socket 标识符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 */ // 读取 http 头部信息numchars = get_line(client, buf, sizeof(buf));resource = fopen(filename, "r"); // 打开发送到客户端的文件,以 只读 的方式打开if (resource == NULL) // 错误处理函数not_found(client);else{headers(client, filename); // 如果成功,向客户端发送 200 的请求正确的头cat(client, resource); // 将文件内容,逐行的发送到客户端}fclose(resource); // 关闭文件}/**********************************************************************//* This function starts the process of listening for web connections* on a specified port. If the port is 0, then dynamically allocate a* port and modify the original port variable to reflect the actual* port.* Parameters: pointer to variable containing the port to connect on* Returns: the socket *//**********************************************************************/int startup(u_short *port){int httpd = 0; // 定义服务器的 Socket 描述符int on = 1; // ?struct sockaddr_in name; // 用那个结构体,绑定服务器端的 ip 地址httpd = socket(PF_INET, SOCK_STREAM, 0); // 创建服务器端的 socket// ip V4 、 SOCK_STREAM 建立安全 TCP 流的类型、0 是这个流默认的协议if (httpd == -1) // 做判断,如果是 -1 就是出错了error_die("socket"); // 出错就打印错误信息,并退出整个程序// 接下来就是绑定服务器端的地址和端口memset(&name, 0, sizeof(name)); // 把结构体初始化为 0// 下面是结构体的三个成员name.sin_family = AF_INET; // 指定地址类型 ipv4name.sin_port = htons(*port); // 传进来的端口,转化为网络字序节(就是大端储存的字节序)name.sin_addr.s_addr = htonl(INADDR_ANY); // INADDR_ANY 本机任意可用的 ip 地址// ?if ((setsockopt(httpd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))) < 0){error_die("setsockopt failed");}// 绑定到地址上if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)error_die("bind"); // 如果小于 0 ,就返回 绑定失败// 如果绑定的端口小于 0 ,则自动随机生成可用端口if (*port == 0) /* if dynamically allocating a port */{socklen_t namelen = sizeof(name);// 获取已经绑定后的套字节信息,主要是获取随机生成的端口号是多少if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1)error_die("getsockname");*port = ntohs(name.sin_port); // 转化成 ntohs 类型,就是把网络字节序转化为本地的字节序}if (listen(httpd, 5) < 0) // 这时开始监听error_die("listen");return(httpd); // 把生成的 socket 描述符传递回 main 函数,也就是 main 函数中的server_sock}/**********************************************************************//* Inform the client that the requested web method has not been* implemented.* Parameter: the client socket *//**********************************************************************/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; // 定义服务器 socket 的描述符u_short port = 4000; // 定义服务端监听端口int client_sock = -1; // 定义客户端 socket 的描述符struct sockaddr_in client_name; // 定义一个结构体,sockaddr_in 型socklen_t client_name_len = sizeof(client_name); // 获取客户端地址长度pthread_t newthread; // 定义线程的 idserver_sock = startup(&port); // 初始化服务器printf("httpd running on port %d\\n", port); // 在控制台打印出端口号// 循环创建链接和子线程(就是提供服务,等待与客户端建立链接)while (1){// 如果有客户过来,就从 listen() 里建立的队列里取一个建立连接的// 的链接,然后生成新的 socket 描述符。&client_name 就是客户端// 的地址信息client_sock = accept(server_sock,(struct sockaddr *)&client_name,&client_name_len); // 阻塞等待客户端建立链接if (client_sock == -1) // 如果函数出错的话,还是得错误处理error_die("accept");printf("%d\\n",ntohs(client_name.sin_port)); // 测试打印客户端端口/* accept_request(&client_sock); */// 创建子线程处理链接// 如果建立连接成功的话,还是创建一个子线程,处理服务器端与客户端的通信// &newthread 就是线程 ID 、NULL 是默认属性、accept_request就是子线程要执行的函数// 然后把 client_sock 强制转化成 void*if (pthread_create(&newthread , NULL, (void *)accept_request, (void *)(intptr_t)client_sock) != 0)perror("pthread_create"); // 如果线程创建失败,执行错误处理}close(server_sock);return(0);}
之后还有一个叫 htdocs 的文件夹,里面有两个文件, index.html 和 color.cpp
index.html:
<HTML><TITLE>Index</TITLE><BODY><P>Welcome to J. David\'s webserver.<H2>CGI demo<FORM ACTION="color.cgi" METHOD="POST">Enter a color: <INPUT TYPE="text" NAME="color"><INPUT TYPE="submit"></FORM></BODY></HTML>
color.php
#include <iostream>#include <unistd.h>#include <stdlib.h>int main(){char *data;char *length;char color[20];char c = 0;int flag = -1;std::cout << "Content-Type: text/html\\r\\n" << std ::endl; // 头部信息std::cout << "<HTML><TITLE>color world</TITLE>""<h1>hello</h1>""<BODY><P>the color is:"<< std ::endl;if((data = getenv("QUERY_STRING")) != NULL){while(*data != \'=\')data++;data++;sprintf(color,"%s",data);}if((length = getenv("CONTENT_LENGTH")) != NULL){int i;for(i = 0; i < atoi(length); i++){read(STDIN_FILENO,&c,1);if(c == \'=\'){flag = 0;continue;}if(flag > -1){color[flag++] = c;}}color[flag] = \'\\0\';}std::cout << color << std::endl;std::cout << "<body bgcolor = \\"" << color << "\\"/>" << std::endl;std::cout << "<BODY></HTML>" << std::endl;return 0;}
编译方式:
- gcc httpd.c
- cd htdocs
- g++ color.cpp -o color.cgi
- cd ..
- ./a.out
- 打开浏览器,http://127.0.0.1:4000 即可看到结果
- 不出意外,在表单中输入 red ,网页背景颜色会变红