1 /**************************************************************************** 2 * 3 * tinyhttpd-0.1.0_hacking 4 * 5 * 1.这是tinyhttpd-0.1.0版本中httpd.c(主程序)的源码,源码不到500行(除去注释). 6 * 2.通过分析、阅读该源码,可以一窥web服务器的大致工作机制. 7 * 3.知识量: 8 * 1.C语言; 9 * 2.Unix或类Unix系统编程; 10 * 3.微量的http协议(请求行、消息头、实体内容); 11 * 4.如何阅读别人的代码( 从main函数开始 :) ); 12 * 4.tinyhttpd-0.1.0 文件结构如下: 13 * . 14 * |-- Makefile -------->makefile 文件 15 * |-- README -------->说明文档 16 * |-- htdocs -------->程序会到该文件夹下找对应html、cgi文件 17 * | |-- README -------->说明文档 18 * | |-- check.cgi -------->cgi 程序 19 * | |-- color.cgi ----^ 20 * | `-- index.html -------->默认的 web 首页文件 21 * |-- httpd.c -------->你接下来要阅读的文件 22 * `-- simpleclient.c -------->没发现该文件有任何用处 @_@ 23 * 5.如何阅读该文档: 24 * 1.linux下使用vi/vim配和ctags,windows下使用Source Insight,当然你也 25 * 可以用其他文本编辑器看. 26 * 2.先找到main函数,然后就可以开始阅读了,遇到对应的函数,就去看对应的 27 * 函数. 28 * 3.对于有些函数,本人没有添加注释,或者说本人觉得没必要. 29 * 4.祝您好运. :) 30 * 31 * 6.tinyhttpd-0.1.0版本下载url: http://sourceforge.net/projects/tinyhttpd/ 32 * 33 * 如果您对本文有任何意见、提议,可以发邮件至zengjf42@163.com,会尽快回复. 34 * 本文的最终解释权归本人(曾剑锋)所有,仅供学习、讨论. 35 * 36 * 2015-3-1 阴 深圳 尚观 Var 37 * 38 ***************************************************************************/ 39 40 41 /* J. David's webserver */ 42 /* This is a simple webserver. 43 * Created November 1999 by J. David Blackstone. 44 * CSE 4344 (Network concepts), Prof. Zeigler 45 * University of Texas at Arlington 46 */ 47 /* This program compiles for Sparc Solaris 2.6. 48 * To compile for Linux: 49 * 1) Comment out the #includeline. 50 * 2) Comment out the line that defines the variable newthread. 51 * 3) Comment out the two lines that run pthread_create(). 52 * 4) Uncomment the line that runs accept_request(). 53 * 5) Remove -lsocket from the Makefile. 54 */ 55 #include 56 #include 57 #include 58 #include 59 #include 60 #include 61 #include 62 #include 63 #include 64 #include 65 #include 66 #include 67 #include 68 69 #define ISspace(x) isspace((int)(x)) 70 71 #define SERVER_STRING "Server: jdbhttpd/0.1.0\r\n" 72 73 void accept_request(int); 74 void bad_request(int); 75 void cat(int, FILE *); 76 void cannot_execute(int); 77 void error_die(const char *); 78 void execute_cgi(int, const char *, const char *, const char *); 79 int get_line(int, char *, int); 80 void headers(int, const char *); 81 void not_found(int); 82 void serve_file(int, const char *); 83 int startup(u_short *); 84 void unimplemented(int); 85 86 /** 87 * accept_request 函数说明: 88 * 1.获取请求方式,目前只支持GET、POST请求; 89 * 2.在本程序中所有的POST请求、带参数的GET请求都都被定义为访问cgi程序; 90 * 3.从带参数的GET请求中分离出请求参数; 91 * 4.如果没有指定需要访问的文件,使用index.html文件作为默认访问文件; 92 * 5.检查需要访问的文件是否存在,以及其是否具有对应的权限; 93 * 6.根据是否是cgi程序访问,来执行对应的任务. 94 */ 95 void accept_request(int client) 96 { 97 /** 98 * 局部变量说明: 99 * 1.buf : buffer缩写,主要用于暂存从socket中读出来的数据;100 * 2.numchars : 用于保存每次从socket中读到的字符的个数;101 * 3.method : 用于保存请求方式,目前该软件只支持GET、POST这两种方式;102 * 4.url : 用于保存访问文件信息,有些地方叫uri;103 * 5.path : 用于保存文件路径;104 * 6.i, j : 处理数据时的下标;105 * 7.st : 在判断文件类型、是否存在的时候用到;106 * 8.cgi : 是否调用cgi程序的标志.107 */108 char buf[1024];109 int numchars; 110 char method[255]; 111 char url[255]; 112 char path[512];113 size_t i, j;114 struct stat st;115 int cgi = 0; /* becomes true if server decides this is a CGI116 * program */117 char *query_string = NULL;118 119 /**120 * 判断程序是否是GET、POST请求两种的其中一种,如果不是则报错.121 */122 numchars = get_line(client, buf, sizeof(buf));123 i = 0; j = 0;124 while (!ISspace(buf[j]) && (i < sizeof(method) - 1))125 {126 method[i] = buf[j];127 i++; j++;128 }129 method[i] = '\0';130 131 if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))132 {133 unimplemented(client);134 return;135 }136 137 /**138 * 该程序把POST请求定义为cgi请求.139 */140 if (strcasecmp(method, "POST") == 0)141 cgi = 1;142 143 /**144 * 获取当前url,这里的url不过括网址,而是除去网址之后的东西,145 * 如浏览器中输入:http://127.0.0.1:8080/example/index.html146 * 得到的url:/example/index.html147 * 在有些地方不称这个为url,称之为uri148 */149 i = 0;150 while (ISspace(buf[j]) && (j < sizeof(buf)))151 j++;152 while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf)))153 {154 url[i] = buf[j];155 i++; j++;156 }157 url[i] = '\0';158 159 /**160 * 每次运行的时候都会出现2次这个,目前还不知道是什么原因导致的原因,161 * 这是本人在源代码的基础上添加的调试输出.162 * url: /favicon.ico163 * url: /favicon.ico164 */165 printf("url: %s\n", url);166 167 /**168 * 如果是GET请求,如果带了请求参数,那么也是cgi请求,并且从url中分离出请求参数169 */170 if (strcasecmp(method, "GET") == 0)171 {172 query_string = url;173 while ((*query_string != '?') && (*query_string != '\0'))174 query_string++;175 if (*query_string == '?')176 {177 cgi = 1;178 *query_string = '\0';179 query_string++;180 }181 }182 183 /**184 * 所有的需要的html文件、cgi程序都在htdocs文件夹中,185 * 如果没有指定html文件,或者cgi程序,那么使用默认的index.html文件186 * 作为目标输出文件.187 */188 sprintf(path, "htdocs%s", url);189 if (path[strlen(path) - 1] == '/')190 strcat(path, "index.html");191 192 /**193 * 检查要访问的文件的状态,如:194 * 1.是否存在;195 * 2.是否是一个文件夹;196 * 3.如果是cgi程序,是否用于对应的权限.197 * 当然如果执行stat时就出错了,那么,直接将socket中的数据读完,198 * 然后返回没有找到相关内容的信息提示.199 */200 if (stat(path, &st) == -1) {201 while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */202 numchars = get_line(client, buf, sizeof(buf));203 not_found(client);204 }205 else206 {207 if ((st.st_mode & S_IFMT) == S_IFDIR) /* 如果是一个文件夹 */208 strcat(path, "/index.html");209 if ((st.st_mode & S_IXUSR) ||210 (st.st_mode & S_IXGRP) ||211 (st.st_mode & S_IXOTH) ) /* 权限问题 */212 cgi = 1;213 214 /**215 * 通过cgi变量来判断是执行cgi程序,还是仅仅是返回一个html页面.216 */217 if (!cgi)218 serve_file(client, path); /* 向客户端返回一个html文件 */219 else220 execute_cgi(client, path, method, query_string); /* 执行一个cgi程序 */221 }222 223 close(client);224 }225 226 void bad_request(int client)227 {228 char buf[1024];229 230 sprintf(buf, "HTTP/1.0 400 BAD REQUEST\r\n");231 send(client, buf, sizeof(buf), 0);232 sprintf(buf, "Content-type: text/html\r\n");233 send(client, buf, sizeof(buf), 0);234 sprintf(buf, "\r\n");235 send(client, buf, sizeof(buf), 0);236 sprintf(buf, " Your browser sent a bad request, ");237 send(client, buf, sizeof(buf), 0);238 sprintf(buf, "such as a POST without a Content-Length.\r\n");239 send(client, buf, sizeof(buf), 0);240 }241 242 /**243 * 主要完成将resource指向的文件内容拷贝输出到客户端浏览器中244 */245 void cat(int client, FILE *resource)246 {247 char buf[1024];248 249 fgets(buf, sizeof(buf), resource);250 while (!feof(resource))251 {252 send(client, buf, strlen(buf), 0);253 fgets(buf, sizeof(buf), resource);254 }255 }256 257 void cannot_execute(int client)258 {259 char buf[1024];260 261 sprintf(buf, "HTTP/1.0 500 Internal Server Error\r\n");262 send(client, buf, strlen(buf), 0);263 sprintf(buf, "Content-type: text/html\r\n");264 send(client, buf, strlen(buf), 0);265 sprintf(buf, "\r\n");266 send(client, buf, strlen(buf), 0);267 sprintf(buf, "
Error prohibited CGI execution.\r\n");268 send(client, buf, strlen(buf), 0);269 }270 271 void error_die(const char *sc)272 {273 perror(sc);274 exit(1);275 }276 277 void execute_cgi(int client, const char *path,278 const char *method, const char *query_string)279 {280 /**281 * 局部变量说明:282 * 1.buf : buffer缩写;283 * 2.cgi_output : 用于保存输出管道的文件描述符;284 * 3.cgi_input : 用于保存输入管道的文件描述符;285 * 4.pid : 进程pid,最后父进程退出之前,等待子进程先退出,286 * 并回收相关的资源,这部分工作主要由waitpid()来完成;287 * 5.status : 在waitpid()中用于保存子进程的退出状态,本程序没有具体使用;288 * 6.i : 计数器;289 * 7.c : POST读取请求参数时,读取到的字符保存在这里;290 * 8.numchars : 读取的字符个数;291 * 9.conten_length : 内容实体的字符数;292 */293 char buf[1024];294 int cgi_output[2];295 int cgi_input[2];296 pid_t pid;297 int status;298 int i;299 char c;300 int numchars = 1;301 int content_length = -1;302 303 /**304 * 在本程序中,GET请求的消息头没有任何用处,直接处理掉就行了,305 * 而如果是POST请求,需要的消息头中的获取实体的大小,也就是Content-Length:后面跟的数字306 */307 buf[0] = 'A'; buf[1] = '\0';308 if (strcasecmp(method, "GET") == 0)309 while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */310 numchars = get_line(client, buf, sizeof(buf));311 else /* POST */312 {313 numchars = get_line(client, buf, sizeof(buf));314 while ((numchars > 0) && strcmp("\n", buf))315 {316 buf[15] = '\0';317 if (strcasecmp(buf, "Content-Length:") == 0)318 content_length = atoi(&(buf[16]));319 numchars = get_line(client, buf, sizeof(buf));320 }321 if (content_length == -1) {322 bad_request(client);323 return;324 }325 }326 327 /**328 * 返回返回行信息.329 */330 sprintf(buf, "HTTP/1.0 200 OK\r\n");331 send(client, buf, strlen(buf), 0);332 333 /**334 * 父子进程通过管道通信.335 */336 if (pipe(cgi_output) < 0) {337 cannot_execute(client);338 return;339 }340 if (pipe(cgi_input) < 0) {341 cannot_execute(client);342 return;343 }344 345 /**346 * 创建子进程,用于执行cgi程序,父进程接受子进程的结果,并返回给浏览器347 */348 if ( (pid = fork()) < 0 ) {349 cannot_execute(client);350 return;351 }352 if (pid == 0) /* child: CGI script */353 {354 char meth_env[255]; //cgi 请求方式环境变量355 char query_env[255]; //cgi GET请求参数环境变量356 char length_env[255]; //cgi POST请求参数内容大小环境变量357 358 /**359 * 重定向标准输入输出,并设置好对应的环境变量.360 */361 dup2(cgi_output[1], 1);362 dup2(cgi_input[0], 0);363 close(cgi_output[0]);364 close(cgi_input[1]);365 sprintf(meth_env, "REQUEST_METHOD=%s", method);366 putenv(meth_env);367 if (strcasecmp(method, "GET") == 0) {368 sprintf(query_env, "QUERY_STRING=%s", query_string);369 putenv(query_env);370 }371 else { /* POST */372 sprintf(length_env, "CONTENT_LENGTH=%d", content_length);373 putenv(length_env);374 }375 /* 执行对应的程序 */376 execl(path, path, NULL);377 exit(0);378 } else { /* parent */379 close(cgi_output[1]);380 close(cgi_input[0]);381 /**382 * 对于POST请求,将实体中的请求参数通过管道传送到cgi程序中383 */384 if (strcasecmp(method, "POST") == 0)385 for (i = 0; i < content_length; i++) {386 recv(client, &c, 1, 0);387 write(cgi_input[1], &c, 1);388 }389 /**390 * 读取cgi程序的执行结果,返回给浏览器391 */392 while (read(cgi_output[0], &c, 1) > 0)393 send(client, &c, 1, 0);394 395 close(cgi_output[0]);396 close(cgi_input[1]);397 /**398 * 等待子进程运行结束,并回收子进程的资源,399 * 防止出现孤儿进程400 */401 waitpid(pid, &status, 0);402 }403 }404 405 int get_line(int sock, char *buf, int size)406 {407 /**408 * 局部变量说明:409 * 1.i : 数组下标计数,不能大于size;410 * 2.c : 每次读到的字符保存在这里面;411 * 3.n : 每次读到的字符个数.412 */413 int i = 0;414 char c = '\0';415 int n;416 417 /**418 * 一直读到buf满了,或者遇到了'\n'为止.419 */420 while ((i < size - 1) && (c != '\n'))421 {422 n = recv(sock, &c, 1, 0);423 /* DEBUG printf("%02X\n", c); */424 if (n > 0)425 {426 /**427 * 读到'\r'也算是结束,通过判断后面有没有跟'\n'来判断是否要将下428 * 一个字符取出来,并且无论'\r'后面跟不跟'\n',都将'\r'换成'\n'.429 */430 if (c == '\r')431 {432 n = recv(sock, &c, 1, MSG_PEEK);433 /* DEBUG printf("%02X\n", c); */434 if ((n > 0) && (c == '\n'))435 recv(sock, &c, 1, 0);436 else437 c = '\n';438 }439 buf[i] = c;440 i++;441 }442 else443 c = '\n';444 }445 buf[i] = '\0'; /* 字符串结尾 */446 447 return(i);448 }449 450 void headers(int client, const char *filename)451 {452 char buf[1024];453 (void)filename; /* could use filename to determine file type */454 455 strcpy(buf, "HTTP/1.0 200 OK\r\n");456 send(client, buf, strlen(buf), 0);457 strcpy(buf, SERVER_STRING);458 send(client, buf, strlen(buf), 0);459 sprintf(buf, "Content-Type: text/html\r\n");460 send(client, buf, strlen(buf), 0);461 strcpy(buf, "\r\n");462 send(client, buf, strlen(buf), 0);463 }464 465 void not_found(int client)466 {467 char buf[1024];468 469 sprintf(buf, "HTTP/1.0 404 NOT FOUND\r\n");470 send(client, buf, strlen(buf), 0);471 sprintf(buf, SERVER_STRING);472 send(client, buf, strlen(buf), 0);473 sprintf(buf, "Content-Type: text/html\r\n");474 send(client, buf, strlen(buf), 0);475 sprintf(buf, "\r\n");476 send(client, buf, strlen(buf), 0);477 sprintf(buf, "
Not Found \r\n");478 send(client, buf, strlen(buf), 0);479 sprintf(buf, "The server could not fulfill\r\n");480 send(client, buf, strlen(buf), 0);481 sprintf(buf, "your request because the resource specified\r\n");482 send(client, buf, strlen(buf), 0);483 sprintf(buf, "is unavailable or nonexistent.\r\n");484 send(client, buf, strlen(buf), 0);485 sprintf(buf, "\r\n");486 send(client, buf, strlen(buf), 0);487 }488 489 void serve_file(int client, const char *filename)490 {491 /**492 * 局部变量说明:493 * 1.resource : 打开的文件的文件指针;494 * 2.numchars : 每次读到的字符个数;495 * 3.buf : buffer的缩写.496 */497 FILE *resource = NULL;498 int numchars = 1;499 char buf[1024];500 501 /**502 * 在本程序中消息头对于纯GET请求没有什么用,直接读取丢掉.503 */504 buf[0] = 'A'; buf[1] = '\0';505 while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */506 numchars = get_line(client, buf, sizeof(buf));507 508 resource = fopen(filename, "r");509 if (resource == NULL)510 not_found(client);511 else512 {513 /* 发送消息头 */514 headers(client, filename);515 /* 发送内容实体 */516 cat(client, resource);517 }518 fclose(resource);519 }520 521 /**522 * startup 函数完成内容:523 * 1.获取一个作为服务器的socket;524 * 2.绑定服务器端的socket;525 * 3.通过判断参数port的值,确定是否需要动态分配端口号;526 * 4.服务器开启监听;527 * 5.返回服务器段的socket文件描述符.528 */529 int startup(u_short *port)530 {531 /**532 * 局部变量说明:533 * 1.httpd : 保存服务器socket描述符,并作为返回值返回;534 * 2.name : 用于保存服务器本身的socket信息,创建服务器.535 */536 int httpd = 0;537 struct sockaddr_in name;538 539 httpd = socket(PF_INET, SOCK_STREAM, 0); 540 if (httpd == -1)541 error_die("socket");542 543 memset(&name, 0, sizeof(name));544 name.sin_family = AF_INET;545 name.sin_port = htons(*port);546 name.sin_addr.s_addr = htonl(INADDR_ANY);547 548 if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)549 error_die("bind");550 551 if (*port == 0) /* if dynamically allocating a port */552 {553 int namelen = sizeof(name);554 if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1)555 error_die("getsockname");556 *port = ntohs(name.sin_port);557 }558 559 if (listen(httpd, 5) < 0)560 error_die("listen");561 return(httpd);562 }563 564 void unimplemented(int client)565 {566 char buf[1024];567 568 sprintf(buf, "HTTP/1.0 501 Method Not Implemented\r\n");569 send(client, buf, strlen(buf), 0);570 sprintf(buf, SERVER_STRING);571 send(client, buf, strlen(buf), 0);572 sprintf(buf, "Content-Type: text/html\r\n");573 send(client, buf, strlen(buf), 0);574 sprintf(buf, "\r\n");575 send(client, buf, strlen(buf), 0);576 sprintf(buf, "
Method Not Implemented\r\n");577 send(client, buf, strlen(buf), 0);578 sprintf(buf, " \r\n");579 send(client, buf, strlen(buf), 0);580 sprintf(buf, "HTTP request method not supported.\r\n");581 send(client, buf, strlen(buf), 0);582 sprintf(buf, "\r\n");583 send(client, buf, strlen(buf), 0);584 }585 586 /**********************************************************************/587 588 int main(void)589 {590 /**591 * 局部变量说明:592 * 1.server_sock : 服务器端的socket描述符;593 * 2.port : 服务器端的socket端口号,如果是0的,startup()将会采用594 * 自动生成的方式生成新的端口号供使用;595 * 3.client_sock : 客户端连接进来产生的客户端socket描述符;596 * 4.client_name : 用于保存客户端连接进来的socket信息;597 * 5.client_name_len : struct sockaddr_in结构体的大小,在accpet的时候598 * 需要用到,这个参数必须传,否则会出错;599 * 6.newthread : 用于保存新创建的线程的ID.600 */601 int server_sock = -1; 602 u_short port = 0; 603 int client_sock = -1; 604 struct sockaddr_in client_name; 605 int client_name_len = sizeof(client_name);606 pthread_t newthread;607 608 /**609 * startup 函数完成内容:610 * 1.获取一个作为服务器的socket;611 * 2.帮定服务器断的sockt;612 * 3.通过判断参数port的值,确定是否需要动态分配端口号;613 * 4.服务器开启监听.614 */615 server_sock = startup(&port);616 printf("httpd running on port %d\n", port);617 618 while (1)619 {620 /**621 * 等待客户端的连接,使用client_name保存客户端socket信息,622 * client_name_len是client_name对应结构体的长度.623 */624 client_sock = accept(server_sock,625 (struct sockaddr *)&client_name,626 &client_name_len);627 if (client_sock == -1)628 error_die("accept");629 /**630 * 创建一个新的线程来处理任务,并把客户端的socket描述符作为参数传给accept_request,631 * accept_request 函数说明:632 * 1.获取请求方式,目前只支持GET、POST请求;633 * 2.在本程序中所有的POST请求、带参数的GET请求都都被定义为访问cgi程序;634 * 3.从带参数的GET请求中分离出请求参数;635 * 4.如果没有指定需要访问的文件,使用index.html文件作为默认访问文件;636 * 5.检查需要访问的文件是否存在,以及其是否具有对应的权限;637 * 6.根据是否是cgi程序访问,来执行对应的任务.638 */639 if (pthread_create(&newthread , NULL, accept_request, client_sock) != 0)640 perror("pthread_create");641 }642 643 /**644 * 不知道为什么,这条语句在while外边,竟然会影响到程序的关闭 :(645 * 这行代码注释掉才能连续访问,不注释,只能访问一次,所以直接注释了646 * 反正程序停止都使用ctrl+c,不影响程序的运行.647 */648 //close(server_sock);649 650 return(0);651 }