tinyhttpd学习笔记:过程的高级语言描述

源码来自[https://github.com/EZLippi/Tinyhttpd/blob/master/httpd.c]
线程的解释:[http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html]
http请求的解释:https://blog.csdn.net/weixin_44347636/article/details/98078723
client端、server端、web app之间的联系:https://www.cnblogs.com/hwlong/p/9076764.html
网络协议:描述报文内容详细语义的协议
通用网关接口cgi:server端与web app之间遵守的协议
什么是pipe:https://blog.csdn.net/qq_42914528/article/details/82023408

client端发送的http请求报文的内容格式:
在这里插入图片描述

在这里插入图片描述

main函数

int main(void)
{
    int server_sock = -1;
    u_short port = 4000;
    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(&client_sock); */
        
/**********************************************************************/
/*int pthread_create(pthread_t *restrict tidp,const pthread_attr_t *restrict_attr,void*(*start_rtn)(void*),void *restrict arg);

返回值:若成功则返回0,否则返回出错编号    

参数:
第一个参数为指向线程标识符的指针。
第二个参数用来设置线程属性。
第三个参数是线程运行函数func的地址。
最后一个参数是运行函数func的参数。*/

		if (pthread_create(&newthread , NULL, (void *)accept_request, (void *)(intptr_t)client_sock) != 0)
            perror("pthread_create");
            /**********************************************************************/
    }

    close(server_sock);

    return(0);
}

startup函数

/**********************************************************************/
/*startup函数启动 侦听(listen)指定端口(port)上的 Web 连接 的过程*/
/*如果端口为 0,则动态分配端口并修改原始端口变量以反映实际端口。*/
/*参数:指向包含要连接的端口的变量的指针*/
/*返回值:套接字socket*/
/**********************************************************************/
int startup(u_short *port)
{
    int httpd = 0;
    int on = 1;
    struct sockaddr_in name;
/**********************************************************************/
/* int socket(int af, int type, int protocol);
/*af:地址描述*/
/*PF_INET:IPv4协议*/
/*type:新套接口的通信类型描述。
/*SOCKET_STREAM:双向可靠数据流,对应TCP*/
/*protocol:套接口所用的协议。如调用者不想指定,可用0指定,表示缺省。*/

    httpd = socket(PF_INET, SOCK_STREAM, 0);
    /*创建一个IPv4、TCP协议套接口httpd*/
    if (httpd == -1)
        error_die("socket");/*socket创建失败*/
         
/**********************************************************************/
/* void *memset(void *s, int ch, size_t n);*/
/*函数解释:
/*将s中当前位置后面的n个字节用 ch 替换并返回 s */
/*(typedef unsigned int size_t )*/

    memset(&name, 0, sizeof(name));
    /*将结构体name的全部内容,即地址族、端口、地址置0*/
    name.sin_family = AF_INET;
    /*设置地址族*/
    name.sin_port = htons(*port);
    /*设置端口*/
    name.sin_addr.s_addr = htonl(INADDR_ANY);
    /*设置地址*/
    
/**********************************************************************/ 
/*int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
/*函数解释:设置套接口sockfd的各种属性
/*sockfd:标识一个套接口的描述字。
/*level:选项定义的层次。支持SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP和IPPROTO_IPV6。
/*如果想要在套接字级别上设置选项,就必须把level设置为SOL_SOCKET。
/*optname:需设置的选项。
/*SO_REUSEADDR:端口释放后可立即被再次使用
/*optval:指针,指向存放选项待设置的新值的缓冲区。
optlen:optval缓冲区长度。*/
   
    if ((setsockopt(httpd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))) < 0)  
    {  
        error_die("setsockopt failed");
    }
    /*将套接口httpd设置成SO_REUSEADDR属性*/

/**********************************************************************/ 
/*server端要用 bind() 函数将套接字与特定的 IP 地址和端口绑定起来*/

    if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
        error_die("bind");
    /*将套接字http和包含IP地址和端口的结构体name绑定*/

/**********************************************************************/
/*
网络字节顺序,NBO是网络数据在传输中的规定的数据格式,从高到低位顺序存储,即数据低位存储在高地址,数据高位存储在低地址;即“大端模式”。网络字节顺序可以避免不同主机字节顺序的差异。

主机字节顺序,HBO则与机器CPU相关,数据的存储顺序由CPU决定。

在C/C++写网络程序的时候,往往会遇到字节的网络顺序和主机顺序的问题。这是就可能用到htons(), ntohl(), ntohs(),htons()这4个函数,也就是网络字节顺序n与本地字节顺序h之间的转换函数:

htonl()--"Host to Network Long"
ntohl()--"Network to Host Long"
htons()--"Host to Network Short"
ntohs()--"Network to Host Short"

int getsockname(int sockfd, struct sockaddr *localaddr,socklen_t *addrlen);
参数:   
sockfd:需要获取名称的套接字。
localaddr:存放所获取套接字名称的缓冲区。
addrlen:作为入口参数,name指向空间的最大长度。作为出口参数,name的实际长度*/

    if (*port == 0)  /*若端口为0,则动态分配端口*/
    {
        socklen_t namelen = sizeof(name);
        if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1)
            error_die("getsockname");
            /*获取一个套接口的本地名字*/
        *port = ntohs(name.sin_port);
    }

/**********************************************************************/
/*int listen(SOCKET sock, int backlog);  //Windows
/*sock :需要进入监听状态的套接字
/*backlog :请求的等待队列最大长度
/*通过 listen() 函数可以让套接字进入被动监听状态。当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求*/


    if (listen(httpd, 5) < 0)
        error_die("listen");
        /*缓冲区的请求数最大为5*/
    return(httpd);
/**************************************************************************/ } 



/*当套接字处于监听状态时,可以通过 accept() 函数来接收客户端请求
/*int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);
/*accept() 返回一个新的套接字来和客户端通信,sock为server端的套接字,addr 保存了client端的IP地址和端口号*/

accept_request函数

/**********************************************************************/
/* A request has caused a call to accept() on the server port to
 * 返回值:Process the request appropriately.
 * 参数: 已经连接到client端的socket */
/**********************************************************************/
void accept_request(void *arg)
{
/**************************************************************************/
/*intptr_t是为了跨平台,其长度总是所在平台的位数,所以用来存放地址。*/
/*size_t是为了方便系统之间的移植而定义的,它是一个无符号整型,在32位系统上定义为:unsigned int;在64位系统上定义为unsigned long。*/ 
/*以下是client端的http请求报文所包含的内容:*/
/*结构体stat记录文件的各类参数*/
/*method:请求方法*/
/*url:*/

    int client = (intptr_t)arg;
    char buf[1024];		/*接收server端发送的数据的缓存区*/
    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));
    /*将client端套接口收到的数据的第一行经过“转换成换行”处理后,存放在buf中*/

/**************************************************************************/
/*ISspace():判断字符是否为空白字符' '
/*若返回0,则表示字符不是空白的;否则为空白字符
/*ISspace()表示若存在空白字符,则执行操作*/
/*提取buf中的method:*/

    i = 0; j = 0;
    while (!ISspace(buf[i]) && (i < sizeof(method) - 1))
    /*若遇到空格,则停止提取操作*/
    /*将buf收到的文本中的method提取出来*/
    {
        method[i] = buf[i];
        i++;
    }
    j=i;
    method[i] = '\0';

/**************************************************************************/
/*strcasecmp()函数:判断字符串是否相等(忽略大小写)*/

    if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
    /*当method既不是GET也不是POST时,执行unimplemented函数*/
    {
        unimplemented(client);
        return;
    }

    if (strcasecmp(method, "POST") == 0)
    /*当method是POST时*/
        cgi = 1;

/**************************************************************************/
/*提取buf中的url:*/
   
    i = 0;
    while (ISspace(buf[j]) && (j < numchars))
    /*找到buf[j]的第一个非空字符*/
    /*上面的操作找到method后,通过该循环操作使得index移动至url的开头,为提取url做准备*/
        j++;
    while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < numchars))
    {
        url[i] = buf[j];
        i++; j++;
        /*提取url*/
    }
    url[i] = '\0';

/**************************************************************************/
/*提取url中的query,用于给cgi传递参数*/
/*若请求为GET,则提取query
/*url的组成:
/*[]表示该部分可省略
/*protocol :// hostname[:port] / path / [;parameters][?query]#fragment
/*hostname之后的部分就是url*/


    if (strcasecmp(method, "GET") == 0)
    {
        query_string = url;
        while ((*query_string != '?') && (*query_string != '\0'))
        /*将index移动到url的字符'?'位置*/
            query_string++;
        if (*query_string == '?')
        {
            cgi = 1;
            *query_string = '\0';
            query_string++;
        }
    }

/**************************************************************************/
/*sprintf:格式化输出字符*/
/* /htdocs 目录是用于存放用户网站程序文件的默认站点目录 */
/*index.html是主页的html文件*/

    sprintf(path, "htdocs%s", url);
    /*在url前面加上"htdocs",并将拼接后的url赋给path*/
    if (path[strlen(path) - 1] == '/')
        strcat(path, "index.html");
        /*若url末尾是/,则将index.html拼在紧接path的后面变成/index.html */

/**************************************************************************/
/*文件权限定义:
/*#define S_IFMT  0170000 type of file 文件类型掩码
/*#define S_IFDIR 0040000 directory 目录文件
/*S_IXUSR	所有者拥有执行权限
/*S_IXGRP	群组拥有执行权限
/*S_IXOTH	其他用户拥有执行权限*/

    if (stat(path, &st) == -1)
	/*若在path路径下文件信息获取失败,则:*/
	 {
        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)
            strcat(path, "/index.html");
            /*若文件类型是目录文件,则path末尾添加/index.html */
            
        if ((st.st_mode & S_IXUSR) ||
                (st.st_mode & S_IXGRP) ||
                (st.st_mode & S_IXOTH)    )
            cgi = 1;
            /*若对文件的操作权限满足上述任意一种执行权限,则cgi=1*/
            
/**************************************************************************/
            
        if (!cgi)
            serve_file(client, path);
        else
        /*若cgi=1,则执行execute_cgi函数*/
            execute_cgi(client, path, method, query_string);
    }
/**************************************************************************/

    close(client);
}

get_line函数:

/**************************************************************************/
/* 从套接字获取一行,无论该行以换行符、回车符还是 CRLF 组合结束。 
/* 终止使用空字符读取的字符串。
/* 如果在缓冲区结束之前未找到正行指示器,则字符串将终止为 null。 
/* 如果读取上述三行终止符中的任何一个,则字符串的最后一个字符将是换行符,字符串将以空字符终止。
 * 参数:套接字描述符
 * 		 用于将数据保存在缓冲区
 * 		 缓冲区大小
 * 返回:存储的字节数(不包括空) *
/*读取套接字的一行,把回车\r换行\n等情况都统一为换行符\n结束。
/*目的是使得windows的文件和unix的数据都能以同一方式打开,即结尾都是换行符
/**************************************************************************/

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

    while ((i < size - 1) && (c != '\n'))
    {
/**************************************************************************/
/*int recv( SOCKET s,char* buf,int len,int flags);   
不论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接收数据。
该函数的第一个参数指定接收端套接字描述符;
第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
第三个参数指明buf的长度;
第四个参数一般置0。
返回:缓存区的字节数*/

        n = recv(sock, &c, 1, 0);
		/*在这里的作用是:
		/*client端的1字节缓冲区接收server端发送的数据*/

		/* 若要DEBUG,此处应添加下语句来检验数据是否正确接收 	
		/* printf("%02X\n", c); */
/**************************************************************************/
/*在Windows中:
/*'\n' 换行,换到当前位置的下一行,而不会回到行首
/*'\r' 回车,回到当前行的行首,而不会换到下一行,如果接着输出的话,本行以前的内容会被逐一覆盖;*/      

        if (n > 0)
        {
            if (c == '\r')/*出现回车符号时,可能将要换行*/
            {
                n = recv(sock, &c, 1, MSG_PEEK);
                /*MSG_PEEK表示只查看数据,不取走数据*/
                /*0表示取走数据*/
                /* DEBUG printf("%02X\n", c); */
                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);
}

unimplemented函数:

/**********************************************************************/
/* 告知client that the requested web method has not been
 * 实现.
 * 参数: client端套接字 */
/**********************************************************************/
void unimplemented(int client)
{
    char buf[1024];

    sprintf(buf, "HTTP/1.0 501 Method Not Implemented\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, SERVER_STRING);
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "Content-Type: text/html\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "<HTML><HEAD><TITLE>Method Not Implemented\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "</TITLE></HEAD>\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "<BODY><P>HTTP request method not supported.\r\n");
    send(client, buf, strlen(buf), 0);
    sprintf(buf, "</BODY></HTML>\r\n");
    send(client, buf, strlen(buf), 0);
}

/**********************************************************************/

serve_file函数:

/**********************************************************************/
/* Send a regular file to the client.  Use headers, and report
 * errors to client if they occur.
 * Parameters: a pointer to a file structure produced from the socket
 *              file descriptor
 *             the name of the file to serve */
/**********************************************************************/
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));

    resource = fopen(filename, "r");
    if (resource == NULL)
        not_found(client);
    else
    {
        headers(client, filename);
        cat(client, resource);
    }
    fclose(resource);
}

execute_cgi函数

在这里插入图片描述

/**********************************************************************/
/* 执行 CGI 脚本。 将会根据需要设置环境变量。
 * 参量: client socket descriptor
 *             path to the CGI script */
/**********************************************************************/
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];
    /*这两个都是为了pipe的创建,其中[0]代表pipe读的端口,[1]代表pipe写的端口*/
    /*pipe是半双工,读了的数据就不会继续存在于pipe内*/

    pid_t pid;
    /*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));
        while ((numchars > 0) && strcmp("\n", buf))
        {
            buf[15] = '\0';
            if (strcasecmp(buf, "Content-Length:") == 0)
                content_length = atoi(&(buf[16]));
                /*将字符串里的数字提取出来并转换成整形,找到content_length*/
            numchars = get_line(client, buf, sizeof(buf));
        }
        if (content_length == -1) {
            bad_request(client);
            return;
        }
    }
    else/*HEAD or other*/
    {
    }

/**********************************************************************/

    if (pipe(cgi_output) < 0) 
     /*若成功创建pipe,则返回0,否则返回-1*/
    {
        cannot_execute(client);
        return;
    }
    if (pipe(cgi_input) < 0) {
        cannot_execute(client);
        return;
    }

    if ( (pid = fork()) < 0 ) 
	/*若fork返回-1,则未能创建进程*/
	{
        cannot_execute(client);
        return;
    }
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    /*若成功接收,则将HTTP/1.0 200 OK\r\n附在原内容后面*/
    /*HTTP/1.0是版本号,200是状态码*/
    send(client, buf, strlen(buf), 0);
    /*发送响应报文的状态行给套接字client_sock*/

/**********************************************************************/
/*fork函数子进程返回0,父进程返回子进程的pid。*/

    if (pid == 0)  /* child: CGI script */
    {
        char meth_env[255];
        char query_env[255];
        char length_env[255];
        
		/*close(fd):
		/*关闭事先已经打开的文件描述符*/
		/*cgi数据的具体流向:
		/*输入数据:(cgi_input[1]-->STDIN(cgi_input[0]))
		/*响应数据:(STDOUT(cgi_output[1])--> cgi_output[0])*/

        dup2(cgi_output[1], STDOUT);
        /*使得cgi_output[1]与STDOUT建立映射关系,指向同一个文件表项*/
        dup2(cgi_input[0], STDIN);
        /*使得cgi_input[0]与STDIN建立映射关系,指向同一个文件表项*/
        close(cgi_output[0]);
        /*关闭cgi_output读的端口*/
        close(cgi_input[1]);
        /*关闭cgi_input写的端口*/
        
/**********************************************************************/
/*putenv(const char)
/*按照char的内容配置对应的环境变量*/

        sprintf(meth_env, "REQUEST_METHOD=%s", method);
        putenv(meth_env);
        /*将环境变量REQUEST_METHOD配置为 已提取的method对应的请求方法*/
        
        if (strcasecmp(method, "GET") == 0) {
            sprintf(query_env, "QUERY_STRING=%s", query_string);
            putenv(query_env);
            /*将环境变量QUERY_STRING配置为已提取的query_string对应的请求方法*/
        }
        else {   /* POST */
            sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
            putenv(length_env);
            /*将环境变量CONTENT_LENGTH配置为已提取的content_length对应的字节长度*/
        }
        
        execl(path, NULL);
        /*执行path所代表的文件路径*/
        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);
                /*将套接字client要发送的文本长度的内容存在c内*/
                write(cgi_input[1], &c, 1);
                /*将c内,直到内容为空*/
            }
        while (read(cgi_output[0], &c, 1) > 0)
        /*将cgi_output[0]收到的一字节内容存在c内,直到内容为空*/
            send(client, &c, 1, 0);
			/*将c存的一字节内容发送到套接字client*/
        close(cgi_output[0]);
        close(cgi_input[1]);
        waitpid(pid, &status, 0);/*等待子进程结束*/
    }
}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值