tinyhttpd主要源码解析

accept_request函数

预备知识

  accept_request的作用是用来接受http请求消息。要看懂该函数,首先要先了解http请求消息结构,结构如下图:
在这里插入图片描述
  请求消息结构分为四部分:请求行(request line)请求头部(header)空行请求数据(body)
  常见请求方法:get、post、put、delete对应的操作是查、改、增、删,但tinyhttp是轻量级http服务器只实现了最常见的请求方法get和put
  get实例如下:
GET /562f25980001b1b106000338.jpg HTTP/1.1
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
Host: www.example.com
Accept-Language: en, mi

空行(即使请求数据没有,也要有空行)
  post实例如下:
POST / HTTP1.1
Host:www.wrox.com
User-Agent:Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)
Content-Type:application/x-www-form-urlencoded
Content-Length:40
Connection: Keep-Alive

空行
name=Professional%20Ajax&publisher=Wiley

源码

void accept_request(int 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;      /*判断是否需要使用CGI程序,若为true则需要。
                        CGI是http服务器与外部程序之间的接口协议,方便服务器与外部程序交互*/
 	char *query_string = NULL;

	//读http 请求的第一行数据(请求行)
	numchars = get_line(client, buf, sizeof(buf));
	i = 0;
	j = 0;
	//把请求方法存进 method 中
	while (!ISspace(buf[j]) && (i < sizeof(method) - 1)) 
	{
		method[i] = buf[j];
		i++;
		j++;
	}
	method[i] = '\0';

	//如果请求的方法不是 GET 或 POST 任意一个的话就直接发送 response 告诉客户端没实现该方法
	if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
	{
		unimplemented(client);
		return;
	}

	//如果是 POST 方法就将 cgi 标志位置为1(true)
	if (strcasecmp(method, "POST") == 0)
		cgi = 1;

	i = 0;
	//跳过所有的空白字符(空格)
	while (ISspace(buf[j]) && (j < sizeof(buf)))
		j++;

	//然后把 URL 读出来放到 url 数组中
	while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf)))
	{
		url[i] = buf[j];
		i++;
		j++;
	}
	url[i] = '\0';

	//如果这个请求是一个 GET 方法的话
	if (strcasecmp(method, "GET") == 0)
	{
		//用一个指针指向 url
		query_string = url;
		
		/*
			URL解析
			实例URL:https://www.baidu.com/s?ie=UTF-8&wd=%E6%88%91%E7%9C%9F%E5%B8%85
			域名:www.baidu.com
			端口:默认是80
			虚拟目录:/s
			文件名:无
			?后面是参数,多个参数有&连接
			参数:ie=UTF-8& 和 wd=%E6%88%91%E7%9C%9F%E5%B8%85
		*/
		//去遍历这个 url,跳过字符 ?前面的所有字符,如果遍历完毕也没找到字符 ?则退出循环
		while ((*query_string != '?') && (*query_string != '\0'))
			query_string++;

		//退出循环后检查当前的字符是 ?还是字符串(url)的结尾
		if (*query_string == '?')
		{
			//如果是 ? 的话,证明这个请求需要调用 cgi,将 cgi 标志位置为1(true)
			cgi = 1;
			//从字符 ? 处把字符串 url 给分隔会两份
			*query_string = '\0';
			//query_string指向 ? 后面的参数(查询语句)
			query_string++;
		}
	}

	//将htdocs + url放到path里形成路径
	sprintf(path, "htdocs%s", url);

	//如果 path 数组中的这个字符串的最后一个字符是以 '/' 结尾的话(代表是目录),就拼接上一个"index.html"的字符串(首页的意思)
	if (path[strlen(path) - 1] == '/')
		strcat(path, "index.html");

	//在系统上去查询该文件是否存在
	if (stat(path, &st) == -1)
	{
		//如果不存在,那把这次 http 的请求后续的内容(head 和 body)全部读完并忽略
		while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */
			numchars = get_line(client, buf, sizeof(buf));
		//然后返回一个找不到文件的 response 给客户端
		not_found(client);
	}
	else
	{
		//文件存在,那去跟常量S_IFMT相与,相与之后的值可以用来判断该文件是什么类型的
		//S_IFMT参读《TLPI》P281,与下面的三个常量一样是包含在<sys/stat.h>
		if ((st.st_mode & S_IFMT) == S_IFDIR)
			//如果这个文件是个目录,那就需要再在 path 后面拼接一个"/index.html"的字符串
			strcat(path, "/index.html");

		//S_IXUSR, S_IXGRP, S_IXOTH三者可以参读《TLPI》P295
		if ((st.st_mode & S_IXUSR) ||
			(st.st_mode & S_IXGRP) ||
			(st.st_mode & S_IXOTH))
			//如果这个文件是一个可执行文件,不论是属于用户/组/其他这三者类型的,就将 cgi 标志变量置一
			cgi = 1;

		if (!cgi)
			//如果不需要 cgi 机制的话,发送文件内容给client
			serve_file(client, path);
		else
			//如果需要则调用
			execute_cgi(client, path, method, query_string);
	}

	close(client);
}

execute_cgi函数

execute_cgi的作用是运行 cgi 程序的处理。
client:客户端套接字
path:CGI程序路径
method:请求方法
query_string:查询语句

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 中填东西以保证能进入下面的 while
	buf[0] = 'A';
	buf[1] = '\0';
	//如果是 http 请求是 GET 方法的话读取并忽略请求剩下的内容
	if (strcasecmp(method, "GET") == 0)
		while ((numchars > 0) && strcmp("\n", buf)) 
			numchars = get_line(client, buf, sizeof(buf));
	else /* POST */
	{
		//只有 POST 方法才继续读内容
		numchars = get_line(client, buf, sizeof(buf));
		
		//我们只需要从 header 中获得 内容长度(Content-Length)信息,其余忽略,循环到空行结束,body 还没有读
		while ((numchars > 0) && strcmp("\n", buf))
		{
			buf[15] = '\0';
			if (strcasecmp(buf, "Content-Length:") == 0)
				content_length = atoi(&(buf[16])); //记录 body 的长度大小
			numchars = get_line(client, buf, sizeof(buf));
		}

		//如果 http 请求的 header 没有 Content-Length 的参数,则报错返回
		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) 
	{ /*fork失败*/
		cannot_execute(client);
		return;
	}

	//子进程用来执行 cgi 脚本
	if (pid == 0)
	{ /* 孩子 */
		char meth_env[255];
		char query_env[255];
		char length_env[255];

		//dup2()作用复制文件描述符,用于重定向,包含<unistd.h>中
		//将子进程的输出由标准输出重定向到 cgi_ouput 的管道写端上
		dup2(cgi_output[1], 1);
		//将子进程的输出由标准输入重定向到 cgi_ouput 的管道读端上
		dup2(cgi_input[0], 0);
		//关闭 cgi_ouput 管道的读端与cgi_input 管道的写端
		close(cgi_output[0]);
		close(cgi_input[1]);

		//构造一个环境变量(环境变量的格式都是name=value)
		sprintf(meth_env, "REQUEST_METHOD=%s", method);
		//putenv()包含于<stdlib.h>中,其作用是添加环境变量,获取环境变量可通过getenv()
		putenv(meth_env);

		//根据http 请求的不同方法,构造并存储不同的环境变量
		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()包含于<unistd.h>中,参读《TLPI》P567
		//最后将子进程替换成另一个进程并执行 cgi 脚本
		execl(path, path, NULL);
		exit(0);
	}
	else
	{ /* parent */
		//父进程则关闭了 cgi_output管道的写端和 cgi_input 管道的读端
		close(cgi_output[1]);
		close(cgi_input[0]);

		//如果是 POST 方法的话就继续读 body 的内容,并写到 cgi_input 管道里让子进程去读
		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 管道中读子进程的输出,并发送到客户端去
		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);
	}
}

参考文档
https://www.cnblogs.com/ranyonsue/p/5984001.html
https://github.com/EZLippi/Tinyhttpd

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值