tinyhttp整理(一)

  之前在git上学了一个tinyhttp的小项目,但今年考研书太多了,把之前笔记搞丢了,最近回顾一遍顺便整理出博客。

  另外涉及到的东西比较多,http基础、网络编程、linux系统函数、cgi、多线程、管道通讯。还有就是因为环境不一样要运行起来要改一些地方,参考了一些博客,如 点击这里 点击这里 。

  自己改完加注释的代码也上传到资源上了 点击这里

----------------看到讲解最好的一篇,详细!!!   -------- 点这里

  下一篇会把涉及到的知识点整理一下。

  一、流程

   tinyhttpd 是一个不到 500 行的超轻量型 Http Server, 对于网络编程和http中 get、post 的 Web 处理流程,有很清楚的展示。

  主干流程:

      服务器创建socket并监听某一端口->浏览器输入url发出请求->服务器收到请求,创建线程处理请求,主线程继续等待->新线程读取http请求,并解析相关字段,读取文件内容或者执行CGI程序并返回给浏览器->关闭客户端套接字,新线程退出。

二、详细函数

按顺序将主要函数的简要分析和注释代码写在下面。

一、main()函数

  1、调用startup()函数,输出服务端的端口号

  2、进入循环,通过accept()等待客户端连接

  3、连接成功后,服务器创建新线程调用accept_request()函数处理客户端请求

  4、循环结束关闭socket

//服务器主函数
int main(void)
{
	int server_sock = -1;	//服务端套接字接口
	u_short port = 0;	
	int client_sock = -1;	//已连接套接字描述符,初始化为-1(客户端)
	struct sockaddr_in client_name;
	socklen_t client_name_len = sizeof(client_name);
	pthread_t newthread;
	
	//调用startup()函数
	//建立一个监听套接字,在对应的端口建立httpd服务
	server_sock = startup(&port);
	//输出服务端口号,供客户端访问
	printf("httpd running on port %d\n", port);

	//进入循环,服务器调用accept()等待客户端的连接,accept()会议阻塞的方式运行
	//有客户端连接再回返回。
	//连接成功后,服务器创建新线程来处理客户端请求
	//完成后,重新等待新的客户端请求
	while (1)
	{
		//返回一个已连接套接字,即客户端
		client_sock = accept(server_sock,
			(struct sockaddr *)&client_name,
			&client_name_len);
		if (client_sock == -1)
			error_die("accept");
		//创建新线程用accept_request()函数处理新请求,同时将客户端socket作为参数传过去
		/* accept_request(client_sock); */
		if (pthread_create(&newthread , NULL, accept_request, (void*)&client_sock) != 0)
			perror("pthread_create");
 }

	close(server_sock);

 return(0);
}

 

二、startup()函数

  1、在服务端创建一个socket

  2、将socket绑定到对应的端口上(服务器在启动时绑定他们众说周知的端口)

  3、如果当前指定的端口是0,则动态分配一个端口

  4、监听请求

//初始化 httpd 服务,包括建立套接字,绑定端口,进行监听等。
int startup(u_short *port)
{
	int httpd = 0;
	//网络套接字地址结构
	struct sockaddr_in name;
	//在服务端创建一个socket,返回描述符
	httpd = socket(PF_INET, SOCK_STREAM, 0);
	if (httpd == -1)
		error_die("socket");
	memset(&name, 0, sizeof(name));
	name.sin_family = AF_INET;
	name.sin_port = htons(*port);//host to network short 转数据格式(下同)
	name.sin_addr.s_addr = htonl(INADDR_ANY);//任何网络接口
	//将socket绑定到对应端口上
	if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0)
		error_die("bind");
	//如果当前指定的端口是0,则动态分配一个端口
	if (*port == 0)  /* if dynamically allocating a port */
	{
		socklen_t namelen = sizeof(name);
		//在以端口号为0调用bind函数后,getsockname用于返回由内核赋予的本地端口号
		//Note:------ 后两个参数 为 结果参数
		if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1)
			error_die("getsockname");
		*port = ntohs(name.sin_port);
	}
	//开始监听
	//listen函数仅由tcp服务器调用
	if (listen(httpd, 5) < 0)
		error_die("listen");
	//返回socket描述符
	return (httpd);
}

三、accept_request()函数

  1、调用get_line()函数得到http请求的请求行

  2、get_line完后,就是开始解析第一行,判断是GET方法还是POST方法,目前只支持这两种。如果是POST,还是把cgi置1,表明要运行CGI程序;如果是GET方法且附带以?开头的参数时,也认为是执行CGI程序

  3、拼接获取要访问的url,可以是很常见的/,/index.html等等。该程序默认为根目录是在htdocs下的,且默认文件是index.html。另外还判断了给定文件是否有可执权限,如果有,则认为是CGI程序。最后根据变量cgi的值来进行相应选择:读取静态文件或者执行CGI程序返回结果。

  4、返回文件内容(serve_file)或执行cgi程序(execute_cgi)

//处理从套接字上监听到的一个HTTP请求
void* accept_request(void* pclient)
{
	int client = *(int*) pclient;//一手取址符要看明白,不能只知道是不知道为什么是
	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;
	// http请求报文包括报文头、请求头、空行、报文主体四部分
	// 调用getline()函数,读取一行到buf[]中
	// 先解析报文头,例如:  GET /index.html HTTP/1.1
	numchars = get_line(client, buf, sizeof(buf));
	i = 0; j = 0;
	// 1.方法字段保存在method中
	while (!ISspace(buf[j]) && (i < sizeof(method) - 1))
	{
		method[i] = buf[j];
		i++; j++;
	}
	method[i] = '\0';
	// 只能识别get、post
	if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
	{
		unimplemented(client);
		return NULL;
	}
	
	// post请求,则开启cgi
	if (strcasecmp(method, "POST") == 0)
		cgi = 1;
	
	// 2.请求的URL保存在url
	i = 0;
	while (ISspace(buf[j]) && (j < sizeof(buf)))
		j++;
	//从缓冲区中吧URL读取出来
	while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < sizeof(buf)))
	{
		url[i] = buf[j];
		i++; j++;
	}
	url[i] = '\0';

	// 如果是GET请求
	// get方法,请求参数和对应的值附加在URL后面,用一个?分隔
	// 3.将参数数据存放在query_string中
	if (strcasecmp(method, "GET") == 0)
	{
		query_string = url;
		// 移动指针直至参数部分
		while ((*query_string != '?') && (*query_string != '\0'))
			query_string++;
		// 如果有参数部分,说明这个请求需要脚本处理
		// 此时把请求字符串单独提取出来,即query_string所指
		if (*query_string == '?')
		{
			//开启cgi
			cgi = 1;
			*query_string = '\0';
			query_string++;
		}
	}

	// 保存有效的url地址并加上请求地址的主页索引。默认的根目录是htdocs/
	// 这里做路径拼接,因为url以'/'开头,所以不用拼接'/'
	// 格式化url到path数组,html文件都在htdocs中
	sprintf(path, "htdocs%s", url);
	// 如果访问路径的最后一个字符时'/',就为其补全,即默认访问index.html
	if (path[strlen(path) - 1] == '/')
		strcat(path, "index.html");
	// 访问请求文件
	// 如果文件不存在就直接返回,如果存在就调用cgi程序来处理
	if (stat(path, &st) == -1) {
		// 如果不存在,就把剩下的请求头从缓冲区读出去
		// 把所有的headers信息丢弃
		while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
			numchars = get_line(client, buf, sizeof(buf));
		// 然后返回404错误,回应客户端找不到
		not_found(client);
	}
	else 
	{
		// 如果文件存在却是个目录,则继续拼接路径,默认访问这个目录下的index.html
		if ((st.st_mode & S_IFMT) == S_IFDIR)
			strcat(path, "/index.html");
		// 如果文件可执行,就执行它
		// 如果需要调用cgi,在调用cgi之前有一段是对用户权限的判断
		// 含义如下:S_IXUSR:用户可以执行
        //          S_IXGRP:组可以执行
        //          S_IXOTH:其它人可以执行
		if ((st.st_mode & S_IXUSR) || 
				(st.st_mode & S_IXGRP) ||
				(st.st_mode & S_IXOTH)    )
			cgi = 1;
		// 不是cgi,直接把服务器文件返回,否则执行cgi
		if (!cgi)
			serve_file(client, path); 	// 调用函数,把文件内容返回
		else
			execute_cgi(client, path, method, query_string); 	// 执行cgi程序
	}

	close(client);  	// 断开连接(http特点,无连接)
	return NULL;
}

四、serve_fine()函数

  1、首先把客户端头部读完

  2、打开文件流

  3、调用readers()函数发送头部,调用cat()函数发送文件内容

// 调用 cat 把服务器文件返回给浏览器
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
	{
		// 为模拟http相应,首先向客户端发送头部(调用headers()函数)
		headers(client, filename);
		// 再调用cat()函数发送数据体部分,即文件内容
		cat(client, resource);
	}
	fclose(resource);//关闭文件流
}

五、execute_cgi()函数

  1、建立两个管道,并fork()一个子进程

  2、在子进程中,把 STDOUT 重定向到 cgi_outputt 的写入端,把 STDIN 重定向到 cgi_input 的读取端,关闭 cgi_input 的写入端 和 cgi_output 的读取端,设置 request_method 的环境变量,GET 的话设置 query_string 的环境变量,POST 的话设置 content_length 的环境变量,这些环境变量都是为了给 cgi 脚本调用,接着用 execl 运行 cgi 程序。在父进程中,关闭 cgi_input 的读取端 和 cgi_output 的写入端,如果 POST 的话,把 POST 数据写入 cgi_input,已被重定向到 STDIN,读取 cgi_output 的管道输出到客户端,该管道输入是 STDOUT。接着关闭所有管道,等待子进程结束。(如图)

// 运行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';
	// 首先需要根据请求是get还是post,来分别处理
	// 如果是GET,就忽略剩余的请求头
	if (strcasecmp(method, "GET") == 0)
		while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
			numchars = get_line(client, buf, sizeof(buf));
	else    /* POST */
	{
		// 如果是POST,那么要读出请求长度,即Content-Length
		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;
	}

	// fork()自身,生成两个进程
	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);		// 把STDOUT重定向到cgi_output的写入端
		dup2(cgi_input[0], 0);		// 把STDIN重定向到cgi_input的读取端
		// 关闭不必要的管道端
		close(cgi_output[0]);
		close(cgi_input[1]);
		sprintf(meth_env, "REQUEST_METHOD=%s", method);
		putenv(meth_env);
		// GET  设置query_string的环境变量
		if (strcasecmp(method, "GET") == 0) {
			sprintf(query_env, "QUERY_STRING=%s", query_string);
			putenv(query_env);
		}
		else {   /* POST */
			// POST 设置content_length的环境变量
			sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
			putenv(length_env);
		}
		// 运行cgi脚本
		execl(path, path, NULL);
		exit(0);
	} else {    /* parent */
	// 父进程 
		// 关闭不必要的管道端
		close(cgi_output[1]);
		close(cgi_input[0]);
		// 对于POST请求,直接write()给子进程
		// 这样子进程所调用的脚本就可以从标准输入取得POST数据
		if (strcasecmp(method, "POST") == 0)
			for (i = 0; i < content_length; i++) {
				recv(client, &c, 1, 0);
				// 把POST数据写入cgi_input,重定向到stdin
				write(cgi_input[1], &c, 1);
			}
		// 然后父进程再从输出管道里面读出所有结果,返回给客户端
		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);
	}
}

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值