最近学习计算机网络知识,在查阅 Socket 网络实战项目时,发现了代码量仅500多行的轻量型 webserver,很兴奋,开始着手学习大神 J. David Blackstone 在1999年写的 Tinyhttpd,代码简洁精炼,稍作改动就可以完成一个迷你的Web服务器程序,非常适合于Web新手学习(官网链接 tinyhttpd) TinyHTTPd 虽是迷你 Web 服务器程序,代码简短,完整涉及到 HTTP、TCP、CGI等基本协议,先来看一下这三者对于Web服务的作用。 这里就主要代码接口实现做解析(结合代码框架图),源码结构也是非常清晰,实现了一个最基本的Web服务程序,完整源码可到GitHub下载。 本人在 Ubuntu18 编译并测试该程序,修复了一些环境配置及函数接口的坑,可以直接编译运行,感兴趣可以到 Github 获取 commit f0795b8ae53c1fc71f63107b4e1ebd75e23cfd52 仓库链接:TinyHTTPdWeb、HTTP 和 CGI 之间的关系
Web客户端与服务器之间通过HTTP协议交互,传输层使用TCP协议来进行可靠性传输。(本篇不谈论安全协议,只分析最基本的Web服务框架)
CGI(Common Gateway Interface),也叫做通用网关接口。CGI是一种标准,它定义了动态文档应如何创建,输入数据应如何提供给应用程序,以及输出结果应如何使用。TinyHTTPd中用 Perl 写了CGI脚本程序,也可以用C/C++ 或 其他脚本语言替代。对于CGI可以看一下 万法归宗——CGI
关于三者之间的联系,如下图示:
清楚了Web服务器的大致构成,下面就开始学习Tinyhttpd源码吧主要代码框架图
程序流程解析图
程序源码及解析
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_sock = startup(&port); printf("httpd running on port %dn", port); while (1) { // 接收客户端请求 client_sock = accept(server_sock, (struct sockaddr *)&client_name, &client_name_len); if (client_sock == -1) { error_die("accept"); } // 开辟新的线程来运行线程函数 accept_request if (pthread_create(&newthread , NULL, accept_request, (void *)&client_sock) != 0){ perror("pthread_create"); } } close(server_sock); return(0); } int startup(u_short *port) { int httpd = 0; struct sockaddr_in name; // 建立服务端套接字 httpd = socket(PF_INET, SOCK_STREAM, 0); // 使用 IPv4 TCP if (httpd == -1) { error_die("socket"); } memset(&name, 0, sizeof(name)); name.sin_family = AF_INET; // IPv4 地址 name.sin_port = htons(*port); // 端口 name.sin_addr.s_addr = htonl(INADDR_ANY); // ip地址 // 将该套接字与IP地址和端口绑定 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); } // 让套接字进入被动监听状态,请求队列长度 5 if (listen(httpd, 5) < 0) { error_die("listen"); } return httpd; } void *accept_request(void *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; // 读取套接字的一行 numchars = get_line(*((int *)client), buf, sizeof(buf)); i = 0; j = 0; // 复制buf中的方法到 method while (!ISspace(buf[j]) && (i < sizeof(method) - 1)) { method[i++] = buf[j++]; } method[i] = ' '; // strcasecmp函数比较时忽略大小写 // 如果请求类型不是 GET 和 POST,则返回 if (strcasecmp(method, "GET") && strcasecmp(method, "POST")) { unimplemented(*((int *)client)); return NULL; } if (strcasecmp(method, "POST") == 0) { cgi = 1; } // 去除buf首部的空格 while (ISspace(buf[j]) && (j < sizeof(buf))) { j++; } // 复制buf中的路径到url i = 0; while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf))) { url[i++] = buf[j++]; } url[i] = ' '; if (strcasecmp(method, "GET") == 0) { query_string = url; while ((*query_string != '?') && (*query_string != ' ')) { query_string++; } if (*query_string == '?') { cgi = 1; *query_string = ' '; query_string++; } } sprintf(path, "htdocs%s", url); if (path[strlen(path) - 1] == '/') { strcat(path, "index.html"); } if (stat(path, &st) == -1) { while ((numchars > 0) && strcmp("n", buf)) { /* read & discard headers */ numchars = get_line(*((int *)client), buf, sizeof(buf)); } not_found(*((int *)client)); } else { if ((st.st_mode & S_IFMT) == S_IFDIR) { strcat(path, "/index.html"); } // 如果有执行权限,则认为是 .cgi 脚本 if ((st.st_mode & S_IXUSR) || (st.st_mode & S_IXGRP) || (st.st_mode & S_IXOTH)) { cgi = 1; } if (!cgi) { serve_file(*((int *)client), path); // 发送htdocs/index.html } else { execute_cgi(*((int *)client), path, method, query_string); // 执行 CGI 脚本 } } close(*((int *)client)); return NULL; } 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] = ' '; if (strcasecmp(method, "GET") == 0) { while ((numchars > 0) && strcmp("n", buf)) { /* read & discard headers */ numchars = get_line(client, buf, sizeof(buf)); } } else { /* POST */ numchars = get_line(client, buf, sizeof(buf)); while ((numchars > 0) && strcmp("n", buf)) { buf[15] = ' '; if (strcasecmp(buf, "Content-Length:") == 0) { content_length = atoi(&(buf[16])); } numchars = get_line(client, buf, sizeof(buf)); } if (content_length == -1) { bad_request(client); return; } } sprintf(buf, "HTTP/1.0 200 OKrn"); 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; } // 子进程执行 CGI 脚本 if (pid == 0) { /* child: CGI script */ char meth_env[255]; char query_env[255]; char length_env[255]; dup2(cgi_output[1], 1); // 子进程标准输出定向到 cgi_output[1] dup2(cgi_input[0], 0); // 子进程标准输入定向到 cgi_input[0] close(cgi_output[0]); close(cgi_input[1]); sprintf(meth_env, "REQUEST_METHOD=%s", method); putenv(meth_env); if (strcasecmp(method, "GET") == 0) { sprintf(query_env, "QUERY_STRING=%s", query_string); putenv(query_env); } else { /* POST */ sprintf(length_env, "CONTENT_LENGTH=%d", content_length); putenv(length_env); } execl(path, path, NULL); // 子进程执行 CGI 脚本 exit(0); } else { /* parent */ close(cgi_output[1]); close(cgi_input[0]); if (strcasecmp(method, "POST") == 0) { for (i = 0; i < content_length; i++) { recv(client, &c, 1, 0); write(cgi_input[1], &c, 1); } // 从管道 cgi_output[0] 读到子进程处理后的信息,发送给客户端 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] = ' '; while ((numchars > 0) && strcmp("n", buf)) /* read & discard headers */ numchars = get_line(client, buf, sizeof(buf)); // 打开 index.html 文件 resource = fopen(filename, "r"); if (resource == NULL) not_found(client); else { headers(client, filename); // 发送 HTTP 响应头信息 cat(client, resource); // 发送 index.html 文件内容给客户端 } fclose(resource); }
Web服务效果图
GitHub仓库地址
更新日志如下:
Date: Tue Mar 10 20:19:37 2020 +0800
整理代码缩进,注释 serve_file,execute_cgi,accept_request 函数
commit 3821737d71bc1dbf60f2bdc0230a870b6eb45534
Date: Mon Mar 9 23:43:12 2020 +0800
修改缩进,注释 main、startup 函数
commit c168529bd1bc6eed4a9c65992338117debad767a
Date: Mon Mar 9 22:23:29 2020 +0800
修改 client_name_len 类型为 socklen_t; 修改accept_request接口及参数类型,编译执行web都ok
commit ad36881b897118354badb7235635fdd82dbc5518
Date: Mon Mar 9 21:44:20 2020 +0800
增加头文件 stdlib.h
commit 05dfce0e65d3683770d803c84e3389bbd8d84cb8
Date: Mon Mar 9 21:42:46 2020 +0800
修改 perl 解释器路径
commit 34b0fc108eaddf4081333bcd4d0fa76e3e168e53
Date: Mon Mar 9 21:40:58 2020 +0800
解决编译 ld -lsocket not found.
参考【1】《UNIX环境高级编程》
参考【2】《计算机网络》
参考【3】Tinyhttpd精读解析
本网页所有视频内容由 imoviebox边看边下-网页视频下载, iurlBox网页地址收藏管理器 下载并得到。
ImovieBox网页视频下载器 下载地址: ImovieBox网页视频下载器-最新版本下载
本文章由: imapbox邮箱云存储,邮箱网盘,ImageBox 图片批量下载器,网页图片批量下载专家,网页图片批量下载器,获取到文章图片,imoviebox网页视频批量下载器,下载视频内容,为您提供.
阅读和此文章类似的: 全球云计算