Tinyhttp源码分析及知识点总结

1,038 阅读6分钟

前言

Tinyhttp是一个用C语言编写的超轻量级服务器,对于有C语言基础的初学者来说,是一个非常好的练手项目,仔细读完代码对LINUX网络编程也有了初步的认识。由于本人之前对于网络编程没有了解,所以看源码时,都是边查阅相关的概念边学习。本文前半部分总结了我学习过程中所有的知识点,有了这些基本感念后再去读源码是相当有帮助的。

HTTP

这是一个重点,只有了解了要解析的数据格式后才能理解代码中的逻辑。由于这部分内容太多,所以我们重点来看下面的两幅图,其他的详细内容可查看参考链接, 参考链接:www.cnblogs.com/linliquan/p…

请求报文格式

  • 在本文中只能识别两种请求方法GETPOST
  • GET:当客户端要从服务器中读取某个资源时,使用GET方法。GET 方法要求服务器将URL定位的资源放在响应报文的部分,回送给客户端,即向服务器请求某个资源。使用GET方法时,请求参数和对应的值附加在URL后面,利用一个问号(“?”)代表URL 的结尾与请求参数的开始,传递参数长度受限制。例如, GET / HTTP/1.1
  • POST:当客户端给服务器提供信息较多时可以使用POST方法,GET一般用于获取/查询资源信息,POST会附带用户数据,一般用于更新资源信息。例:POST /color.cgi HTTP/1.1

响应报文格式

  • "HTTP/1.0 200 OK\r\n"代表着成功接收。

socket编程

这是linux网络编程的基础,下面是使用TCP传输的流程图。

  • socket函数建立一个scoket通信,即向系统注册,通知系统建立一个通信端口。
  • bind函数用来给scoket端口设置一个名称。
  • listen函数用来等待scoket连线,并可以指定同时处理的最大连接要求。
  • accept函数用于接受远程计算机的连接请求,建立与客户机之间的通信连接。当函数接受一个请求,会产生一个新的套接字,数据传输都用这新的套接字。

fork函数

fork 创建一个子进程,代码和父进程完全一样,执行fork之后的语句,fork调用一次,返回两次。

  • 在父进程中,fork返回新创建子进程的ID
  • 在子进程中,fork返回0
  • 如果出现错误,fork返回一个负值。

管道

pipe 管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。

  • 其本质是一个伪文件(实为内核缓冲区)
  • 由两个文件描述符引用,一个表示读端,一个表示写端。
  • 规定数据从写端流入,从读端流出

原理:实为内核使用环形队列机制,借助内核缓冲区实现

局限性:

  • 数据自己读不能自己写
  • 只能读一次,读后不存在
  • 采用半双工通信方式,只能在一个方向上流动
  • 只能在有公共祖先的进程间使用管道

工作流程

在这部分将会介绍客户端或服务器执行相关操作后,程序中函数的调用过程,每个函数的具体实现将在下一个部分介绍。

源码分析

为了不显得代码冗长,只说明和展示重要代码。

main()函数

int main(void)
{
	...
    u_short port = 4000;							//#1
    server_sock = startup(&port);					//#2
    while (1)
    {
        client_sock = accept(server_sock,           //#3
                (struct sockaddr *)&client_name,
                &client_name_len);
        if (pthread_create(&newthread , NULL, (void   //#4
        *)accept_request, (void *)(intptr_t)client_sock) != 0)
            perror("pthread_create");
    }
    close(server_sock); 							//#5
    return(0);
}
  • 这部分完全按照《socket编程》部分讲解的TCP流程编写。
  • #1~#2startup函数完成socketbindlisten。并指定端口号为4000。
  • #3~#4: accpet操作,当有客户端发送一个请求时,就会生成一个新socket端口和线程,对于该请求的响应都通过该端口和线程处理,然后继续监听,直到有下一个请求。
  • 这就是服务器工作的主要过程。

startup()函数

startup函数完成socketbindlisten

int startup(u_short *port)
{
	...
    httpd = socket(PF_INET, SOCK_STREAM, 0);         //#1
    name.sin_family = AF_INET;						 //#2
    name.sin_port = htons(*port);					 //#3
    name.sin_addr.s_addr = htonl(INADDR_ANY);		 //#4
    if ((setsockopt(httpd, SOL_SOCKET, SO_REUSEADDR, //#5
    		&on, sizeof(on))) < 0)  
    {  
        error_die("setsockopt failed");
    }
    if (bind(httpd, (struct sockaddr *)&name,        //#6 
    			sizeof(name)) < 0)
        error_die("bind");
    if (listen(httpd, 5) < 0)						 //#7
        error_die("listen");
    return(httpd);									 //#8
}
  • #1: socket函数建立一个scoket通信,PF_INET表示IPv4通信,SOCK_STREAM表示TCP协议。
  • #2~#6:设置端口的信息,及端口号,ip地址等。
  • #7listen函数用来等待scoket连线,并可以指定同时处理的最大连接要求。

accept_request()函数

accept_request()函数是线程处理函数。主要功能就是解析请求命令,然后执行相应操作,http请求格式在前文中已经说明。

void accept_request(void *arg)
{
    int client = (intptr_t)arg;
    char buf[1024];
    size_t 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(client, buf, sizeof(buf));
    i = 0; j = 0;
    /*提取方法名直到检测到空格*/
    while (!ISspace(buf[i]) && (i < sizeof(method) - 1))
    {
        method[i] = buf[i];
        i++;
    }
    j=i;
    method[i] = '\0';
    /*当既不是GET请求也不是POST请求时,调用unimplemented()函数*/
    if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
    {
        unimplemented(client);
        return;
    }

    if (strcasecmp(method, "POST") == 0)
        cgi = 1;

    i = 0;
    /*跳过空格*/
    while (ISspace(buf[j]) && (j < numchars))
        j++;
    /*提取域名,即服务器地址*/
    while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < numchars))
    {
        url[i] = buf[j];
        i++; j++;
    }
    url[i] = '\0';
	/*区分GET方法和POST方法,因为两种方法格式不同,所以解析的代码也不同*/
    if (strcasecmp(method, "GET") == 0)
    {
        query_string = url;
         /*找到第一个?,判断后面带不带参数,若带参数,则需要调用cgi程序/
        while ((*query_string != '?') && (*query_string != '\0'))
            query_string++;
        if (*query_string == '?')
        {
            cgi = 1;
            *query_string = '\0';
            query_string++;
        }
    }
	 /*获取服务器端htdocs/index.html文件路径*/
    sprintf(path, "htdocs%s", url);
    if (path[strlen(path) - 1] == '/')
        strcat(path, "index.html");
	printf("%s\n", path);
    /*取得指定文件的文件属性,若找不到,就通过not_found()函数发送错误*/
    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_IFMT = 掩码, S_IFDIR : D
irectory表示路径是个文件夹*/
            strcat(path, "/index.html");
        if ((st.st_mode & S_IXUSR) ||    /*X表示文件所有者拥有执行权限*/
                (st.st_mode & S_IXGRP) ||
                (st.st_mode & S_IXOTH)    )
            cgi = 1;
         /*通过cgi标志位,判断是否需要执行可执行文件,若不需要执行cgi程序,则直接通过serve_file发送文件,若需要则通过execute_cgi执行cgi程序*/   
        if (!cgi)
            serve_file(client, path);
        else
            execute_cgi(client, path, method, query_string);
    }
    /*http是无连接的,所以执行完一次请求后,关闭端口*/  
    close(client);
}

execute_cgi()函数

执行相应的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)
        while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
            numchars = get_line(client, buf, sizeof(buf));
   
    else if (strcasecmp(method, "POST") == 0) /*POST*/
    {
        numchars = get_line(client, buf, sizeof(buf));
        /*根据请求报文格式获取所有请求头部,直到遇到“\n”*/
        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;
        }
    }
    else/*HEAD or other*/
    {
    }

	/*创建两个管道供父进程和子进程传输数据*/
    if (pipe(cgi_output) < 0) {
        cannot_execute(client);
        return;
    }
    if (pipe(cgi_input) < 0) {
        cannot_execute(client);
        return;
    }
	/*fork一个子进程*/
    if ( (pid = fork()) < 0 ) {
        cannot_execute(client);
        return;
    }
    /*发送响应报文,根据响应报文格式*/
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    send(client, buf, strlen(buf), 0);
    if (pid == 0)  /* child: CGI script */
    {
        char meth_env[255];
        char query_env[255];
        char length_env[255];
		/*子进程修改管道为标准输入输出,标准输入输出与cgi程序挂钩*/
        dup2(cgi_output[1], STDOUT);
        dup2(cgi_input[0], STDIN);
        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, NULL);   /*执行文件函数*/
		exit(0);
    } 
	
	else {    /* parent */
        close(cgi_output[1]);
        close(cgi_input[0]);
        if (strcasecmp(method, "POST") == 0){
        	/*父进程接收scoket端口的数据将其通过管道传给cgi程序*/
            for (i = 0; i < content_length; i++) {
                recv(client, &c, 1, 0);
                write(cgi_input[1], &c, 1);
            }
		}
		/*读不到数据会阻塞*/
        while (read(cgi_output[0], &c, 1) > 0){
            send(client, &c, 1, 0);
			printf("%s", &c);
		}
        close(cgi_output[0]);
        close(cgi_input[1]);
        /*等待子进程结束,然后才能结束父进程*/
        waitpid(pid, &status, 0);
    }
}

get_line()函数

根据从端口中读取数据,直到遇到'\r\n'、'\n'和'\r'就返回。

int get_line(int sock, char *buf, int size)
{
    int i = 0;
    char c = '\0';
    int n;

    while ((i < size - 1) && (c != '\n'))
    {
        n = recv(sock, &c, 1, 0);
        if (n > 0) /*若读到数据,判断是否是'\r\n'、'\n'和'\r'*/
        {
            if (c == '\r')
            {
            /*预览下一个字符,但不读出,判断是否是'\n',若是就读出,若不是直接返回,不读出字符*/
                n = recv(sock, &c, 1, MSG_PEEK);
                if ((n > 0) && (c == '\n'))
                    recv(sock, &c, 1, 0);
                else
                    c = '\n';
            }
            buf[i] = c;
            i++;
        }
        else /*若没有读到数据,直接返回*/
            c = '\n';
    }
    buf[i] = '\0';
    return(i);
}

测试结果

为了方便测试,我在程序中数据传输部分加上printf打印出来。

启动服务器

  • 可看到端口号为我们指定的4000。

客户端连接

  • 可看到客户端向服务器传来的第一个请求内容为GET / HTTP/1.1
  • 按照程序解析这条指令将会执行serve_file()函数,将htdocs/index.html文件发送给客户端,该文件内容就是第一张图显示的内容。

在网页上提交输入一个颜色并提交

  • 当客户端提交一个请求后,服务器收到了POST /color.cgi HTTP/1.1请求指令。
  • 然后根据请求格式一直读取全部的请求头部。
  • 读完头部,然后在读取数据,由上图可知数据为color = red,将他传给cgi程序,当执行完cgi程序后,从管道中read就可以读到要发给客户端的内容,即一个新页面的html格式的内容,这与页面的显示也一致。