HTTP协议介绍(分析tinyhttpd【下】)
2017-10-17 17:44
253 查看
为了更好的理解http协议的本质,我用开源的TinyHTTPd来做讲解。TinyHTTPd是一个超轻量型Http Server,使用C语言开发,去掉注释后代码500行左右,在分析代码之前建议先阅读我的上一篇博客《HTTP协议介绍(分析tinyhttpd【上】》) 关于tinyhttpd代码的分析,网上有很多类似的文章,所以我就以我分析的过程进行一个梳理,方便大家进行学习。
顺便贴上从网上偷来的流程图(实在懒得画流程图,(^__^) 嘻嘻……)
顺便提一下,web服务器和cgi的关系:
不多说,还是看图
下面是一些常用的CGI环境变量:
CONTENT_TYPE 这个环境变量的值指示所传递来的信息的MIME类型。目前,环境变量CONTENT_TYPE一般都是:application/x-www-form-urlencoded,他表示数据来自于HTML表单。
CONTENT_LENGTH 如果服务器与CGI程序信息的传递方式是POST,这个环境变量即使从标准输入STDIN中可以读到的有效数据的字节数。这个环境变量在读取所输入的数据时必须使用。
HTTP_COOKIE 客户机内的 COOKIE 内容。
HTTP_USER_AGENT 提供包含了版本数或其他专有数据的客户浏览器信息。
PATH_INFO 这个环境变量的值表示紧接在CGI程序名之后的其他路径信息。它常常作为CGI程序的参数出现。
QUERY_STRING 如果服务器与CGI程序信息的传递方式是GET,这个环境变量的值即使所传递的信息。这个信息紧跟在CGI程序名的后面,两者中间用一个问号’?’分隔。
REMOTE_ADDR 这个环境变量的值是发送请求的客户机的IP地址,例如上面的192.168.1.67。这个值总是存在的。而且它是Web客户机需要提供给Web服务器的唯一标识,可以在CGI程序中用它来区分不同的Web客户机。
REMOTE_HOST 这个环境变量的值包含发送CGI请求的客户机的主机名。如果不支持你想查询,则无需定义此环境变量。
REQUEST_METHOD 提供脚本被调用的方法。对于使用 HTTP/1.0 协议的脚本,仅 GET 和 POST 有意义。
SCRIPT_FILENAME CGI脚本的完整路径
SCRIPT_NAME CGI脚本的的名称
SERVER_NAME 这是你的 WEB 服务器的主机名、别名或IP地址。
SERVER_SOFTWARE 这个环境变量的值包含了调用CGI程序的HTTP服务器的名称和版本号。例如,上面的值为Apache/2.2.14(Unix)
开源版本代码见 github
本项目完整代码见码云
建议源码阅读顺序:main —> startup —> accept_request —> excute_cgi
//执行cgi动态解析
//将请求的文件发送回浏览器客户端
//启动服务端
测试页面test.html
test.cgi代码
(这里请勿吐槽python的版本O(∩_∩)O~,只做测试用)
运行结果
编译时要加上-pthread选项
到这里就差不多了,以后有时间会再补充一些http相关的知识的。
http://www.cnblogs.com/TankXiao/archive/2012/02/13/2342672.html#getpost
一、代码结构分析
tinyhttpd总共包含以下函数:void accept_request(int);//处理从套接字上监听到的一个 HTTP 请求 void bad_request(int);//告诉客户端请求出错,400 void cat(int, FILE *);//读取文件并发送给客户端 void cannot_execute(int);//处理发生在执行 cgi 程序时出现的错误 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);//返回给浏览器收到的HTTP请求所用的方法不被支持。(tinyhttpd只实现了GET、POST方法)
顺便贴上从网上偷来的流程图(实在懒得画流程图,(^__^) 嘻嘻……)
顺便提一下,web服务器和cgi的关系:
不多说,还是看图
下面是一些常用的CGI环境变量:
CONTENT_TYPE 这个环境变量的值指示所传递来的信息的MIME类型。目前,环境变量CONTENT_TYPE一般都是:application/x-www-form-urlencoded,他表示数据来自于HTML表单。
CONTENT_LENGTH 如果服务器与CGI程序信息的传递方式是POST,这个环境变量即使从标准输入STDIN中可以读到的有效数据的字节数。这个环境变量在读取所输入的数据时必须使用。
HTTP_COOKIE 客户机内的 COOKIE 内容。
HTTP_USER_AGENT 提供包含了版本数或其他专有数据的客户浏览器信息。
PATH_INFO 这个环境变量的值表示紧接在CGI程序名之后的其他路径信息。它常常作为CGI程序的参数出现。
QUERY_STRING 如果服务器与CGI程序信息的传递方式是GET,这个环境变量的值即使所传递的信息。这个信息紧跟在CGI程序名的后面,两者中间用一个问号’?’分隔。
REMOTE_ADDR 这个环境变量的值是发送请求的客户机的IP地址,例如上面的192.168.1.67。这个值总是存在的。而且它是Web客户机需要提供给Web服务器的唯一标识,可以在CGI程序中用它来区分不同的Web客户机。
REMOTE_HOST 这个环境变量的值包含发送CGI请求的客户机的主机名。如果不支持你想查询,则无需定义此环境变量。
REQUEST_METHOD 提供脚本被调用的方法。对于使用 HTTP/1.0 协议的脚本,仅 GET 和 POST 有意义。
SCRIPT_FILENAME CGI脚本的完整路径
SCRIPT_NAME CGI脚本的的名称
SERVER_NAME 这是你的 WEB 服务器的主机名、别名或IP地址。
SERVER_SOFTWARE 这个环境变量的值包含了调用CGI程序的HTTP服务器的名称和版本号。例如,上面的值为Apache/2.2.14(Unix)
二、代码分析
tinyhttp的开发环境是在solaris上的,所以在我的机子上(fedora)无法编译成功,下面所贴出来的代码都是改动之后的,结合流程图更容易理解。开源版本代码见 github
本项目完整代码见码云
建议源码阅读顺序:main —> startup —> accept_request —> excute_cgi
//接收客户端的连接,并读取请求数据 //根据请求的方法(get or post)决定调用什么处理方式(execute_cgi或serve_file) void *accept_request(void* from_client) { int client = *(int *)from_client; char buf[1024]; int numchars; char method[255]; 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; //获取一行HTTP报文数据 numchars = get_line(client, buf, sizeof(buf)); // i = 0; j = 0; //对于HTTP报文来说,第一行的内容即为报文的起始行,格式为<method> <request-URL> <version>, //每个字段用空白字符相连 while (!ISspace(buf[j]) && (i < sizeof(method) - 1)) { //提取其中的请求方式是GET还是POST method[i] = buf[j]; i++; j++; } method[i] = '\0'; //函数说明:strcasecmp()用来比较参数s1 和s2 字符串,比较时会自动忽略大小写的差异。 //返回值:若参数s1 和s2 字符串相同则返回0。s1 长度大于s2 长度则返回大于0 的值,s1 长度若小于s2 长度则返回小于0 的值。 if (strcasecmp(method, "GET") && strcasecmp(method, "POST")) { //tinyhttp仅仅实现了GET和POST unimplemented(client); return NULL; } //cgi为标志位,置1说明开启cgi解析 if (strcasecmp(method, "POST") == 0) //如果请求方法为POST,需要cgi解析 cgi = 1; i = 0; //将method后面的后边的空白字符略过 while (ISspace(buf[j]) && (j < sizeof(buf))) j++; //继续读取request-URL while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf))) { url[i] = buf[j]; i++; j++; } url[i] = '\0'; //如果是GET请求,url可能会带有?,有查询参数 if (strcasecmp(method, "GET") == 0) { query_string = url; while ((*query_string != '?') && (*query_string != '\0')) query_string++; if (*query_string == '?') { //如果带有查询参数,需要执行cgi,解析参数,设置标志位为1 cgi = 1; //将解析参数截取下来 *query_string = '\0'; query_string++; } } //以上已经将起始行解析完毕 //url中的路径格式化到path sprintf(path, "htdocs%s", url); //如果path只是一个目录,默认设置为首页index.html if (path[strlen(path) - 1] == '/') strcat(path, "index.html"); //函数定义: int stat(const char *file_name, struct stat *buf); //函数说明: 通过文件名file_name获取文件信息,并保存在buf所指的结构体stat中 //返回值: 执行成功则返回0,失败返回-1,错误代码存于errno(需要include <errno.h>) if (stat(path, &st) == -1) { //假如访问的网页不存在,则不断的读取剩下的请求头信息,并丢弃即可 while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */ numchars = get_line(client, buf, sizeof(buf)); //最后声明网页不存在 not_found(client); } else { //如果访问的网页存在则进行处理 if ((st.st_mode & S_IFMT) == S_IFDIR)//S_IFDIR代表目录 //如果路径是个目录,那就将主页进行显示 strcat(path, "/index.html"); if ((st.st_mode & S_IXUSR) || (st.st_mode & S_IXGRP) || (st.st_mode & S_IXOTH) ) //S_IXUSR:文件所有者具可执行权限 //S_IXGRP:用户组具可执行权限 //S_IXOTH:其他用户具可读取权限 cgi = 1; if (!cgi) //将静态文件返回 serve_file(client, path); else //执行cgi动态解析 execute_cgi(client, path, method, query_string); } close(client);//因为http是面向无连接的,所以要关闭 return NULL; }
//执行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)) numchars = get_line(client, buf, sizeof(buf)); else { //处理的请求为POST numchars = get_line(client, buf, sizeof(buf)); while ((numchars > 0) && strcmp("\n", buf)) { //循环读取头信息找到Content-Length字段的值 buf[15] = '\0';//目的是为了截取Content-Length: if (strcasecmp(buf, "Content-Length:") == 0) //"Content-Length: 15" content_length = atoi(&(buf[16]));//获取Content-Length的值 numchars = get_line(client, buf, sizeof(buf)); } if (content_length == -1) { //错误请求 bad_request(client); return; } } //返回正确响应码200 sprintf(buf, "HTTP/1.0 200 OK\r\n"); send(client, buf, strlen(buf), 0); //建立管道,这一步很重要 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; } //fork出一个子进程运行cgi脚本 if (pid == 0) { /* 子进程: 运行CGI 脚本 */ char meth_env[255]; char query_env[255]; char length_env[255]; dup2(cgi_output[1], 1);//1代表着stdout,0代表着stdin,将系统标准输出重定向为cgi_output[1] dup2(cgi_input[0], 0);//将系统标准输入重定向为cgi_input[0],这一点非常关键, //cgi程序中用的是标准输入输出进行交互 close(cgi_output[0]);//关闭了cgi_output中的读通道 close(cgi_input[1]);//关闭了cgi_input中的写通道 //CGI标准需要将请求的方法存储环境变量中,然后和cgi脚本进行交互 //存储REQUEST_METHOD sprintf(meth_env, "REQUEST_METHOD=%s", method); putenv(meth_env); if (strcasecmp(method, "GET") == 0) { //存储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);//执行CGI脚本 exit(0); } else { /* 父进程 */ close(cgi_output[1]);//关闭了cgi_output中的写通道,注意这是父进程中cgi_output变量和子进程要区分开 close(cgi_input[0]);//关闭了cgi_input中的读通道 if (strcasecmp(method, "POST") == 0) for (i = 0; i < content_length; i++) { //开始读取POST中的内容 recv(client, &c, 1, 0); //将数据发送给cgi脚本 write(cgi_input[1], &c, 1); } //读取cgi脚本返回数据 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); } }
//将请求的文件发送回浏览器客户端
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)) //将HTTP请求头读取并丢弃 numchars = get_line(client, buf, sizeof(buf)); //打开文件 resource = fopen(filename, "r"); if (resource == NULL) not_found(client);//如果文件不存在,则返回not_found else { //添加HTTP头 headers(client, filename); //并发送文件内容 cat(client, resource); } fclose(resource);//关闭文件句柄 }
//启动服务端
int startup(u_short *port) { int httpd = 0; struct sockaddr_in name; //设置http 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); //绑定端口 if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0) error_die("bind"); if (*port == 0) { /*动态分配一个端口 */ socklen_t namelen = sizeof(name); if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1) error_die("getsockname"); *port = ntohs(name.sin_port); } //监听连接 if (listen(httpd, 5) < 0) error_die("listen"); return(httpd); }
int main(void) { int server_sock = -1; u_short port = 0; int client_sock = -1; struct sockaddr_in client_name; socklen_t client_name_len = sizeof(client_name); pthread_t newthread; //启动server socket 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"); /*启动线程处理新的连接 */ if (pthread_create(&newthread , NULL, accept_request, (void*)&client_sock) != 0) perror("pthread_create"); } //关闭server socket close(server_sock); return(0); }
三、运行测试
开源的版本给的是perl写的cgi,我的虚拟机没有装perl环境,所以直接用python写cgi来测试。测试页面test.html
<html> <head> <title>测试信息</title> <meta charset="utf-8"> </head> <body> <p><h1 color="red" > post方法传输表单测试</h1></p> <form action="test.cgi" method="POST"> text测试:<input type="text" name="text" value="请输入值进行测试" size="20"> <br> checkbox测试:<input type="checkbox" name="checkbox1" checked="checked">a<input type="checkbox" name="checkbox2">b <br> <br> radio测试:<input type="radio" name="r_test" value="radio_value_1">radio_value_1 <input type="radio" name="r_test" value="radio_value_2">radio_value_2 <br> <br> textarea测试:<br> <textarea cols="30" rows="10" name="textarea"> 输入任意的值进行测试 </textarea> <br> <br> 下拉列表: <select name="Spinner_value"> <option value="value1">value1</option> <option value="value2">value2</option> <option value="value3">value3</option> </select> <br> <input type="reset" value="重置"> <input type="submit" value="提交"> </form> </body> </html>
test.cgi代码
(这里请勿吐槽python的版本O(∩_∩)O~,只做测试用)
#!/usr/bin/python import sys,os length = os.getenv('CONTENT_LENGTH') if length: postdata = sys.stdin.read(int(length)) print "Content-type: text/html\n" print '<html>' print '<head>' print '<title>POST</title>' print '</head>' print '<body>' print '<h2> POST data </h2>' print '<ul>' for data in postdata.split('&'): print '<li>'+data+'</li>' print '</ul>' print '</body>' print '</html>' else: print "Content-type:text/html\n" print 'NO FOUND !'
运行结果
编译时要加上-pthread选项
到这里就差不多了,以后有时间会再补充一些http相关的知识的。
四、参考资料
http://blog.csdn.net/wenqian1991/article/details/46011357http://www.cnblogs.com/TankXiao/archive/2012/02/13/2342672.html#getpost
相关文章推荐
- HTTP协议介绍(分析tinyhttpd【上】)
- dubbo源码分析1——SPI机制的概要介绍
- Android ANR介绍及分析ANR log信息的方法
- Heritrix源码分析(二) 配置文件order.xml介绍
- 几种常见算法的介绍及复杂度分析(转)
- java代码缺陷自动分析工具之FindBugs介绍
- Oracle索引质量介绍和分析脚本分享
- HTTP协议方法以及报头分析
- 红黑树介绍与分析
- tcp协议,ip协议,http协议三者的关系,从发起一个http url例子来分析
- BlogEngine.Net架构与源代码分析系列part1:开篇介绍
- 大数据日志分析系统-介绍
- 百亿级别数据量,又需要秒级响应的案例,需要什么系统支持呢?下面介绍下大数据实时分析工具Yonghong Z-Suite
- 归并排序介绍与分析
- Spark2.0机器学习系列之4:随机森林介绍、关键参数分析
- 股票交易应用系统的性能分析工具介绍(一)
- linux程序分析工具介绍(一)—-”/proc”
- spydroid源码分析(一):介绍spydroid每个包的大体功能
- Struts2的结果集的详细介绍及说明案例分析
- 磁盘阵列介绍、进程的查看管理、日志文件的查看分析,systemctl的控制