最近学习计算机网络知识,在查阅 Socket 网络实战项目时,发现了代码量仅500多行的轻量型 webserver,很兴奋,开始着手学习大神 J. David Blackstone 在1999年写的 Tinyhttpd,代码简洁精炼,稍作改动就可以完成一个迷你的Web服务器程序,非常适合于Web新手学习(官网链接 tinyhttpd)
Web、HTTP 和 CGI 之间的关系
TinyHTTPd 虽是迷你 Web 服务器程序,代码简短,完整涉及到 HTTP、TCP、CGI等基本协议,先来看一下这三者对于Web服务的作用。
Web客户端与服务器之间通过HTTP协议交互,传输层使用TCP协议来进行可靠性传输。(本篇不谈论安全协议,只分析最基本的Web服务框架)
CGI(Common Gateway Interface),也叫做通用网关接口。CGI是一种标准,它定义了动态文档应如何创建,输入数据应如何提供给应用程序,以及输出结果应如何使用。TinyHTTPd中用 Perl 写了CGI脚本程序,也可以用C/C++ 或 其他脚本语言替代。对于CGI可以看一下 万法归宗——CGI
关于三者之间的联系,如下图示:
清楚了Web服务器的大致构成,下面就开始学习Tinyhttpd源码吧
主要代码框架图
程序流程解析图
程序源码及解析
这里就主要代码接口实现做解析(结合代码框架图),源码结构也是非常清晰,实现了一个最基本的Web服务程序,完整源码可到GitHub下载。
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 %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
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] = '\0';
// 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] = '\0';
if (strcasecmp(method, "GET") == 0) {
query_string = url;
while ((*query_string != '?') && (*query_string != '\0')) {
query_string++;
}
if (*query_string == '?') {
cgi = 1;
*query_string = '\0';
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] = '\0';
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] = '\0';
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 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;
}
// 子进程执行 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] = '\0';
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仓库地址
本人在 Ubuntu18 编译并测试该程序,修复了一些环境配置及函数接口的坑,可以直接编译运行,感兴趣可以到 Github 获取
更新日志如下:
commit f0795b8ae53c1fc71f63107b4e1ebd75e23cfd52
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.
仓库链接:TinyHTTPd
参考【1】《UNIX环境高级编程》
参考【2】《计算机网络》
参考【3】Tinyhttpd精读解析