TinyHTTPd--超轻量型Http Server源码分析

来源:互联网 发布:空调的选购 知乎 编辑:程序博客网 时间:2024/06/06 16:31

TinyHTTPd是一个超轻量型Http Server,使用C语言开发,全部代码不到600行,附带一个简单的Client,可以通过阅读这段代码理解一个Http Server的本质。源码下载链接http://sourceforge.net/projects/tinyhttpd/

分析这段源码前,需要对网络协议,Unix编程,以及HTTP有一定的了解,这里假设大家对http有一定的了解,如果有时间,会额外介绍下Http。

本文先全篇分析下该开源项目的源码,最后给出测试。

服务器端代码:httpd.c


建议源码阅读顺序:main —> startup —> accept_request —> excute_cgi

TinyHTTPd 项目流程图



先介绍几个中间辅助函数:(完整代码参见前面源码链接)

从客户端读取一行数据,以\r或\r\n为行结束符

/**********************************************************************//* 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) *//**********************************************************************//**********************************************************************//* 从socket读取一行数据。以\r或\r\n为行结束符* 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;//至多读取size-1个字符,最后一个字符置'\0'while ((i < size - 1) && (c != '\n')){n = recv(sock, &c, 1, 0);//单个字符接收if (n > 0){if (c == '\r')//如果是回车符,继续读取{/*使用 MSG_PEEK 标志使下一次读取依然可以得到这次读取的内容,可认为接收窗口不滑动*/n = recv(sock, &c, 1, MSG_PEEK);if ((n > 0) && (c == '\n'))//如果是回车换行符recv(sock, &c, 1, 0);//继续接收单个字符,实际上和上面那个标志位MSG_PEEK读取同样的字符,读完后删除输入队列的数据,即滑动窗口,c=='\n'elsec = '\n';//只是读取到回车符,则置为换行符,也终止了读取}buf[i] = c;//放入缓冲区i++;}else//没有读取到任何数据c = '\n';}buf[i] = '\0';return(i);//返回读到的字符个数(包括'\0')}
请求出错情况处理
/**********************************************************************//* 告知客户端该请求有错误 400* Parameters: client socket *//**********************************************************************/void bad_request(int client){char buf[1024];/*将字符串存入缓冲区,再通过send函数发送给客户端*/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);}/**********************************************************************//* 通知客户端CGI脚本不能被执行 500* Parameter: the client socket descriptor. *//**********************************************************************/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);}/**********************************************************************//* 打印出错信息,详见《Unix 环境高级编程》并终止*//**********************************************************************/void error_die(const char *sc){perror(sc);exit(1);}/**********************************************************************//* 返回客户端404错误信息 404(万恶的404) *//**********************************************************************/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);}

读取文件中的数据到client

/**********************************************************************//* 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 */ /*Unix shell 命令cat file 即打印文件file中的数据*//**********************************************************************/ /*将文件结构指针resource中的数据发送至client*/void cat(int client, FILE *resource){ char buf[1024]; fgets(buf, sizeof(buf), resource);//从文件结构指针resource中读取数据,保存至buf中 //处理文件流中剩下的字符 while (!feof(resource))//检测流上的文件结束符,文件结束返回非0值,结束返回0 {  send(client, buf, strlen(buf), 0);//文件流中的字符全部发送给client  fgets(buf, sizeof(buf), resource);/*从文件结构体指针resource中读取至多bufsize-1个数据                                    (第bufsize个字符赋'\0')每次读取一行,如果不足bufsize,                                     则读完该行结束。这里通过feof函数来判断fgets是否因出错而终止                                     另外,这里有文件偏移位置,下一轮读取会从上一轮读取完的位置继续*/ }}

返回文件信息给client

/**********************************************************************//* 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");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);}/**********************************************************************//* 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 *//*返回文件数据,用于静态页面返回*//**********************************************************************/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));resource = fopen(filename, "r");//只读方式打开文件if (resource == NULL)not_found(client);//如果文件不存在,返回404错误else{headers(client, filename);//先返回文件头部信息cat(client, resource);//将resource描述符指定文件中的数据发送给client}fclose(resource);//关闭}
下面就是tynyhttpd服务器端的核心代码部分。

为了更好地理解源码,这里提出http的请求报文格式

                                      

服务器端套接字初始化设置

/**********************************************************************//* 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;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);/*在以端口号0调用bind后,getsockname用于返回由内核赋予的本地端口号*/if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1)error_die("getsockname");*port = ntohs(name.sin_port);//网络字节顺序转换为主机字节顺序,返回主机字节顺序表达的数}if (listen(httpd, 5) < 0)//服务器监听客户端请求。套接字排队的最大连接个数5error_die("listen");return(httpd);}
接收客户端的请求报文
/**********************************************************************//* 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 *//**********************************************************************//**********************************************************************//* HTTP协议规定,请求从客户端发出,最后服务器端响应该请求并返回。 * 这是目前HTTP协议的规定,服务器不支持主动响应,所以目前的HTTP * 协议版本都是基于客户端请求,然后响应的这种模型。 */ /*accept_request函数解析客户端请求,判断是请求静态文件还是cgi代码 (通过请求类型以及参数来判定),如果是静态文件则将文件输出给前端, 如果是cgi则进入cgi处理函数*//**********************************************************************/ void accept_request(int client){ char buf[1024]; int numchars; char method[255];//请求方法GET or POST 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));//从client中读取指定大小数据到buf i = 0; j = 0; //解析客户端的http请求报文 /*接收字符处理:提取空格字符前的字符,至多254个*/ while (!ISspace(buf[j]) && (i < sizeof(method) - 1)) {  method[i] = buf[j];//根据http请求报文格式,这里得到的是请求方法  i++; j++; } method[i] = '\0'; //忽略大小写比较字符串,用于判断是哪种类型 if (strcasecmp(method, "GET") && strcasecmp(method, "POST")) {  unimplemented(client);//两种method都不是,告知客户端所请求的方法未能实现  return; } if (strcasecmp(method, "POST") == 0)//POST 类型  cgi = 1;//设置标志位 i = 0; while (ISspace(buf[j]) && (j < sizeof(buf)))//过滤空格字符,空格后面是URL  j++;/*将buf中的非空格字符转存进url缓冲区,遇空格字符或满退出*/ while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf))) {  url[i] = buf[j];//获取的是URL(互联网标准资源的地址)  i++; j++; } url[i] = '\0'; if (strcasecmp(method, "GET") == 0)//GET method {  query_string = url;//请求信息  while ((*query_string != '?') && (*query_string != '\0'))//截取'?'前的字符   query_string++;//问号前面是路径,后面是参数  if (*query_string == '?')//有'?',表明动态请求  {   cgi = 1;   *query_string = '\0';   query_string++;  } }//下面是TinyHTTPd项目htdocs文件下的文件 sprintf(path, "htdocs%s", url);//获取请求文件路径 if (path[strlen(path) - 1] == '/')//如果文件类型是目录(/),则加上index.html  strcat(path, "index.html");////根据路径找文件,并获取path文件信息保存到结构体st中 if (stat(path, &st) == -1) {//执行失败,文件未找到  /*丢弃所有 headers 的信息*/  while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */   numchars = get_line(client, buf, sizeof(buf));//从客户端读取数据进buf  not_found(client);//回应客户端找不到 } else//获取文件信息,执行成功 {  /*如果是个目录,则默认使用该目录下 index.html 文件*/  if ((st.st_mode & S_IFMT) == S_IFDIR)   strcat(path, "/index.html");  if ((st.st_mode & S_IXUSR) ||      (st.st_mode & S_IXGRP) ||      (st.st_mode & S_IXOTH)    )   cgi = 1;  if (!cgi)//静态页面请求   serve_file(client, path);//直接返回文件信息给客户端,静态页面返回  else//动态页面请求   execute_cgi(client, path, method, query_string);//执行cgi脚本 } close(client);//关闭客户端套接字}
执行CGI脚本,动态页面申请
/**********************************************************************//* 执行CGI(公共网卡接口)脚本,需要设定合适的环境变量* Parameters: client socket descriptor*             path to the CGI script *//*execute_cgi函数负责将请求传递给cgi程序处理,服务器与cgi之间通过管道pipe通信,首先初始化两个管道,并创建子进程去执行cgi函数*//*子进程执行cgi程序,获取cgi的标准输出通过管道传给父进程,由父进程发送给客户端*//**********************************************************************/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)//GET方法:一般用于获取/查询资源信息while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers读取并丢弃 HTTP 请求 */numchars = get_line(client, buf, sizeof(buf));//从客户端读取else    /* POST 一般用于更新资源信息*/{numchars = get_line(client, buf, sizeof(buf));//获取HTTP消息实体的传输长度while ((numchars > 0) && strcmp("\n", buf))//不为空且不为换行符{buf[15] = '\0';if (strcasecmp(buf, "Content-Length:") == 0)//是否为Content-Length字段content_length = atoi(&(buf[16]));//Content-Length用于描述HTTP消息实体的传输长度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);//建立管道,两个通道cgi_output[0]:读取端,cgi_output[1]:写入端if (pipe(cgi_output) < 0) {cannot_execute(client);//管道建立失败,打印出错信息return;}//管道只能具有公共祖先的进程间进行,这里是父子进程之间if (pipe(cgi_input) < 0) {cannot_execute(client);return;}//fork子进程,这样就创建了父子进程间的IPC通道if ((pid = fork()) < 0) {cannot_execute(client);return;}//实现进程间的管道通信机制/*子进程继承了父进程的pipe,然后通过关闭子进程output管道的输出端,input管道的写入端;关闭父进程output管道的写入端,input管道的输出端*///子进程,if (pid == 0)  /* child: CGI script */{char meth_env[255];char query_env[255];char length_env[255];//复制文件句柄,重定向进程的标准输入输出//dup2的第一个参数描述符关闭dup2(cgi_output[1], 1);//标准输出重定向到output管道的写入端dup2(cgi_input[0], 0);//标准输入重定向到input管道的读取端close(cgi_output[0]);//关闭output管道的写入端close(cgi_input[1]);//关闭输出端sprintf(meth_env, "REQUEST_METHOD=%s", method);putenv(meth_env);if (strcasecmp(method, "GET") == 0) {//GET/*设置 query_string 的环境变量*/sprintf(query_env, "QUERY_STRING=%s", query_string);putenv(query_env);}else {   /* POST *//*设置 content_length 的环境变量*/sprintf(length_env, "CONTENT_LENGTH=%d", content_length);putenv(length_env);}execl(path, path, NULL);//exec函数簇,执行CGI脚本,获取cgi的标准输出作为相应内容发送给客户端//通过dup2重定向,标准输出内容进入管道output的输入端exit(0);//子进程退出}else {    /* parent */close(cgi_output[1]);//关闭管道的一端,这样可以建立父子进程间的管道通信close(cgi_input[0]);/*通过关闭对应管道的通道,然后重定向子进程的管道某端,这样就在父子进程之间构建一条单双工通道如果不重定向,将是一条典型的全双工管道通信机制*/if (strcasecmp(method, "POST") == 0)//POST方式,将指定好的传输长度字符发送/*接收 POST 过来的数据*/for (i = 0; i < content_length; i++) {recv(client, &c, 1, 0);//从客户端接收单个字符write(cgi_input[1], &c, 1);//写入input,然后重定向到了标准输入//数据传送过程:input[1](父进程) ——> input[0](子进程)[执行cgi函数] ——> STDIN ——> STDOUT // ——> output[1](子进程) ——> output[0](父进程)[将结果发送给客户端]}while (read(cgi_output[0], &c, 1) > 0)//读取output的管道输出到客户端,output输出端为cgi脚本执行后的内容send(client, &c, 1, 0);//即将cgi执行结果发送给客户端,即send到浏览器,如果不是POST则只有这一处理close(cgi_output[0]);//关闭剩下的管道端,子进程在执行dup2之后,就已经关闭了管道一端通道close(cgi_input[1]);waitpid(pid, &status, 0);//等待子进程终止}}
上面父子进程间的管道通信可以用下图表示:父子进程各司其职,分工合作,通过管道建立通信通道。

                                   
上面最终完整状态是 POST 方式,如果不是 POST 方式,则只有 output[0] ——> 客户端。
上面即服务器端的程序:这里简单罗列一下:
#define ISspace(x) isspace((int)(x))//若x为空格字符,返回true#define SERVER_STRING "Server: jdbhttpd/0.1.0\r\n"void accept_request(int);//客户端向服务器端发送请求void bad_request(int);//告诉客户端请求出错,400void cat(int, FILE *);//读取文件并发送给客户端void cannot_execute(int);//通知客户端不能执行CGI脚本(perl)void error_die(const char *);//打印出错信息void execute_cgi(int, const char *, const char *, const char *);//执行CGI脚本,内部调用exec函数簇int get_line(int, char *, int);//从套接字读取数据,返回读取到的字符个数void headers(int, const char *);//返回HTTP头文件信息void not_found(int);//通知客户端页面未找到,404void serve_file(int, const char *);//发送消息给客户端,用于静态页面返回int startup(u_short *);//服务器端套接字设置,创建,绑定,监听(TCP协议)void unimplemented(int);//通知客户端所请求的网络方法没有实现(GET、POST)
下面这个就是服务器端的main.c
int main(void){ int server_sock = -1; u_short port = 0;//传入的端口为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)//创建工作线程,执行回调函数accept_request,参数client_sock   perror("pthread_create"); } close(server_sock);//关闭套接字,就协议栈而言,即关闭TCP连接 return(0);}

从上面我们可以的出Tinyhttp的工作流程:

  1. 服务器启动,指定端口或随机选取端口绑定httpd服务,监听客户端的连接请求。(startup 函数)
  2. 收到客户端的 HTTP 请求,派生一个线程去相应客户端请求(多线程服务器模型),即执行 accept_request 函数
  3. 服务器端解析客户端 HTTP 请求报文。判断是何种 method (GET or POST)以及获取 url。对于 GET 方法,如果携带参数,则 query_string 指针指向 url 中 ? 后面的 GET 参数(http 协议)
  4. 拷贝 url 数据到 path数组,表示浏览器请求的服务器文件路径,在 tinyhttpd 中服务器文件是在 htdocs 文件夹下,若 url 以 /  结尾,或 url 本身是个目录(stat 文件信息),则默认在 path 中加上 index.html,表示访问主页。
  5. 在文件路径合法的前提下,如果是静态页面访问 ,直接输出服务器文件到浏览器,即用 HTTP 格式写到客户端套接字上,然后跳到。如果是动态页面申请(带?的GET方式,POST方式,utl 为可执行文件),则转调用 excute_cgi 函数执行cgi脚本。
  6. 读取整个 HTTP 请求并丢弃,如果是 POST 则找出Content-Length。把"HTTP/1.0 200 OK\r\n" 状态码写到套接字。
  7. 建立两个管道,cgi_input 和 cgi_output ,并 fork 一个进程(必须 fork 子进程,pipe 管道才有意义)。建立父子进程间的通信机制。
  8. 在子进程中,对其进程下的管道进行重定向,并设置对应的环境变量(method、query_string、content_length),这些环境变量都是为了给 cgi 脚本调用,接着用 execl 运行 cgi 脚本,可以看出 cgi 脚本的执行在子进程中进行,然后结果通过管道以及重定向返回给父进程。
  9. 父进程中,关闭管道一端,如果是 POST 方式,则把 POST 数据写入 cgi_intput,已被重定向到 STDIN,读取 cgi_output 管道输出到客户端(浏览器输出),具体流程图参见上面的管道最终状态图。接着关闭所有管道,等待子进程结束。
  10. 关闭连接,完成一次 HTTP 请求与回应。
HTTP 是无连接的,在进行 Web 应用前无须建立专门的 HTTP 应用层会话连接,仅需要直接利用传输层已为它建立好的 TCP 传输连接即可。即虽然是不可靠的无连接协议,但使用可可靠的 TCP 传输层协议,所以从数据传输角度来讲,HTTP 的报文传输仍是可靠的。

值得说明的是,这个项目是不能直接在Linux环境下编译运行的,它本来是在Solaris上实现的,需要修改几处地方,由于篇幅问题,下一篇TinyHTTPd 在Linux 下编译 给出修改地方以及最后运行测试结果。

如果错误,欢迎指出,交流进步,谢谢。



0 0
原创粉丝点击