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' else c = '\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)//服务器监听客户端请求。套接字排队的最大连接个数5 error_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);//等待子进程终止 } }上面父子进程间的管道通信可以用下图表示:父子进程各司其职,分工合作,通过管道建立通信通道。
#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);//告诉客户端请求出错,400 void 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);//通知客户端页面未找到,404 void 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的工作流程:
值得说明的是,这个项目是不能直接在Linux环境下编译运行的,它本来是在Solaris上实现的,需要修改几处地方,由于篇幅问题,下一篇TinyHTTPd 在Linux 下编译 给出修改地方以及最后运行测试结果。
如果错误,欢迎指出,交流进步,谢谢。