TinyHTTP开源项目总结

  最近学习计算机网络知识,在查阅 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
关于三者之间的联系,如下图示:
图1
清楚了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服务效果图

1
2

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精读解析

发布了52 篇原创文章 · 获赞 81 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/xiaoma_2018/article/details/104705177