TinyHttpd 是 J. David Blackstone 于 1999 年写的一个 500 行左右的超轻量级 http server,用来学习服务器的工作原理十分不错。
intro
本篇 blog 为自己学习 tinyhttpd 这个服务器小项目所写的学习笔记,内容不多,但很细致。另外,本文所使用的源码来自于Tiny HTTPd’s tiny homepage,按照源码中的注释,修改掉对应的部分就可以在 Linux 上运行了。实际上,按照注释修改的版本是个无线程版的 tinyhttpd,而现在 Linux 也有了pthread.h这个头文件,也就可以使用pthread_create函数(这个函数可以创建线程)了,但在源码基础上,仍然需要做一些小修改才可以运行。
如果要在 Linux 上使用线程函数pthread_create,在进行下面的改动之前,忽略掉源码本来的提示修改注释。
/**********************************************************************/ /* 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 */ /**********************************************************************/ intstartup(u_short *port) { int httpd = 0; structsockaddr_inname; // socket 函数会返回一个套接字 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); // IP 地址 // 绑定套接字 if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0) error_die("bind"); // 如果没有设置端口,就动态申请一个端口 if (*port == 0) /* if dynamically allocating a port */ { int namelen = sizeof(name); // 利用 getsockname 函数来动态申请一个端口,这个端口会保存在结构体中 if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1) error_die("getsockname"); // 通过结构体的成员赋值给解引用的指针,利用指针改变原变量的值 *port = ntohs(name.sin_port); } // 创建请求队列大小为 5 的监听队列 if (listen(httpd, 5) < 0) error_die("listen"); return httpd; }
从源码中可以发现,startup函数不仅创建好了套接字、端口,还开始进行监听了。虽然startup这个函数使用的系统 API 非常多,但结合注释也能大概知道功能是什么了。
/**********************************************************************/ /* 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) */ /**********************************************************************/ intget_line(int sock, char *buf, int size) { // CRLF == "\r\n" int i = 0; char c = '\0'; int n;
while ((i < size - 1) && (c != '\n')) { // 利用 recv 函数接受数据,长度为 1,用字符变量 c 来保存,recv 函数返回实际复制的字节数 n = recv(sock, &c, 1, 0); /* DEBUG printf("%02X\n", c); */ if (n > 0) { if (c == '\r') { // 读到换行符时 // 读取最后的回车符,但不删除输入流中的回车(MSG_PEEK 参数的意义) 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'; } // 如果 c 不是'\r'或'\n' ,就放到缓存字符串中 buf[i] = c; i++; } else c = '\n'; } // 完成一行的读取,最后加上 C 字符串结束的标志 buf[i] = '\0'; return(i); }
/**********************************************************************/ /* Give a client a 404 not found status message. */ /**********************************************************************/ voidnot_found(int client) { // 与 unimplemented 函数类似,向客户端发送 404 页面 char buf[1024];
/**********************************************************************/ /* Inform the client that a request it has made has a problem. * Parameters: client socket */ /**********************************************************************/ voidbad_request(int client) { // 错误请求,向客户端发送 400 页面 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); }
cannot_execute
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
/**********************************************************************/ /* Inform the client that a CGI script could not be executed. * Parameter: the client socket descriptor. */ /**********************************************************************/ voidcannot_execute(int client) { // CGI 脚本无法执行,返回 500 页面 char buf[1024];
/**********************************************************************/ /* Return the informational HTTP headers about a file. */ /* Parameters: the socket to print the headers on * the name of the file */ /**********************************************************************/ voidheaders(int client, constchar *filename) { // 向客户端发送响应报文头 char buf[1024]; (void)filename; /* could use filename to determine file type */
/**********************************************************************/ /* 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 */ /**********************************************************************/ voidserve_file(int client, constchar *filename) { FILE *resource = NULL; int numchars = 1; char buf[1024]; // 默认字符 buf[0] = 'A'; buf[1] = '\0'; // 与 accept_request 函数一样,丢弃剩余的报文信息 while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */ numchars = get_line(client, buf, sizeof(buf));
cat函数的功能类似 Linux 中的cat命令,都是读取逐行读取文件,不过这里的cat函数需要逐行读取文件并发送给客户端。
cat
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
/**********************************************************************/ /* 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 */ /**********************************************************************/ voidcat(int client, FILE *resource) { char buf[1024]; // 逐行读取 html 文件,并发送给客户端,读到 EOF 时,读取完毕,跳出循环 fgets(buf, sizeof(buf), resource); while (!feof(resource)) { send(client, buf, strlen(buf), 0); fgets(buf, sizeof(buf), resource); } }