tufeiping.github.io

涂飞平的个人博客

Follow me on GitHub

TinyHttpd源码分析

原创 涂飞平 2018-04-20

TinyHttpd是一个C编写的极小HTTP服务器,代码量(包括注释)不到500行,但可以基本说明HTTP服务器的工作原理,从中也能知道如何进行Linux下的C编程(这是我看这个代码的主要原因)。 在Linux下面,我采用Eclipse CPP作为C/C++的IDE,设置好相应的环境变量,以便IDE可以查找链接相应的头文件和库。

tinyhttp.png

源码分析过程如下:

	// 函数声明
	void accept_request(int); //接受请求
	void bad_request(int); //错误请求
	void cat(int, FILE *);
	void cannot_execute(int); //运行失败
	void error_die(const char *); //异常
	void execute_cgi(int, const char *, const char *, const char *); //执行cgi程序
	int get_line(int, char *, int); // 读取每行
	void headers(int, const char *); //获取头部信息
	void not_found(int); // 404处理
	void serve_file(int, const char *); //静态文件请求
	int startup(u_short *); //启动服务
	void unimplemented(int); //未实现

下面先看启动函数– main

int main(void) {
	int server_sock = -1;
	u_short port = 0;
	int client_sock = -1;
	struct sockaddr_in client_name;
	int client_name_len = sizeof(client_name);
	pthread_t newthread;
	//初始化server socket port->返回随机绑定的端口,当然,port可以指定端口
	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, client_sock) != 0)
			perror("pthread_create");
	}
	//关闭server socket
	close(server_sock);

	return (0);
}

对于server部分,都是socket编程的标准套路。http server对于客户请求的响应逻辑在accept_request函数中。

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; /* becomes true if server decides this is a CGI
	* program */
	char *query_string = NULL;

	//从客户socket中按行读取到buf中
	numchars = get_line(client, buf, sizeof(buf)); 
	i = 0;
	j = 0;

	/** 
	HTTP头第一行格式如下
	METHOD QUERY_URL
	**/
	//读取mthod
	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;
	}

	//如果是POST请求,说明有参数存在,交给CGI处理
	if (strcasecmp(method, "POST") == 0)
		cgi = 1;

	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';

	if (strcasecmp(method, "GET") == 0) {
		query_string = url;
		while ((*query_string != '?') && (*query_string != '\0'))
			query_string++;
		//如果GET方法的请求串中带有?,说明有参数存在,交给CGI处理
		if (*query_string == '?') {
			cgi = 1;
			// 加入c字符串终结符\0,url就是?前部分
			*query_string = '\0';
			//query_string指向?后面的参数串
			query_string++; 
		}
	}

	//先将url与htdocs合并,查找是否有相关文件
	sprintf(path, "htdocs%s", url);
	//如果以/结尾,自动定位路径下的index.html
	if (path[strlen(path) - 1] == '/')
		strcat(path, "index.html");
	if (stat(path, &st) == -1) { //如果文件不存在
		while ((numchars > 0) && strcmp("\n", buf)) //将剩余的内容读取并丢弃
			numchars = get_line(client, buf, sizeof(buf));
		not_found(client); // 显示404 not found
	} else {
		if ((st.st_mode & S_IFMT) == S_IFDIR)
			strcat(path, "/index.html"); //如果是目录,指向目录下的index.html文件
		if ((st.st_mode & S_IXUSR) || (st.st_mode & S_IXGRP) || (st.st_mode & S_IXOTH)) //如果文件是可执行的
			cgi = 1;
		if (!cgi)
			serve_file(client, path); //将文件读出并送到客户端
		else
			execute_cgi(client, path, method, query_string); //交给cgi处理
	}

	close(client); // 处理完毕,关闭客户端连接
}

// 文件服务,将文件读取并传到客户端
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)) //读取丢弃剩余头部内容
		numchars = get_line(client, buf, sizeof(buf));
	//按文本方式读取文件
	resource = fopen(filename, "r");
	if (resource == NULL)
		not_found(client);
	else {
		headers(client, filename); //组装http响应头
		cat(client, resource); //返回文件内容
	}
	fclose(resource); //关闭文件
}

//组装http响应头
void headers(int client, const char *filename) {
	char buf[1024];
	(void) filename; /* could use filename to determine file type */
	//将内容拷贝到buf中并发出,返回状态码 200
	strcpy(buf, "HTTP/1.0 200 OK\r\n");
	send(client, buf, strlen(buf), 0);
	//返回server标识
	strcpy(buf, SERVER_STRING); 
	//#define SERVER_STRING "Server: jdbhttpd/0.1.0\r\n"
	send(client, buf, strlen(buf), 0);
	//返回mime-type信息
	sprintf(buf, "Content-Type: text/html\r\n");
	send(client, buf, strlen(buf), 0);
	strcpy(buf, "\r\n");
	//发生分割符\r\n,准备发送实际内容
	send(client, buf, strlen(buf), 0);
}

//发送文件内容
void cat(int client, FILE *resource) {
	char buf[1024];
	//按行读取文件,并按行发送到客户端
	fgets(buf, sizeof(buf), resource);
	while (!feof(resource)) {
		send(client, buf, strlen(buf), 0);
		fgets(buf, sizeof(buf), resource);
	}
}

以上的内容是静态文件处理方式。下面分析cgi的处理方式,从execute_cgi开始分析。 个人认为execute_cgi是最具价值的代码块,从这部分代码可以理解linux下面如何创建进程,如何共享环境变量,如何使用管道来进行进程间通信。

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) //GET方法
		while ((numchars > 0) && strcmp("\n", buf)) //忽略剩余的头部内容
			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) { 
			//如果POST没有Content-Length域,则认为是错误请求,因为后面会通过这个
			//域的大小读取内容并交给cgi处理
			bad_request(client);
			return;
		}
	}
	//先发送状态码 200
	sprintf(buf, "HTTP/1.0 200 OK\r\n");
	send(client, buf, strlen(buf), 0);
	//创建输出管道
	if (pipe(cgi_output) < 0) {
		cannot_execute(client); //如果管道创建失败,返回错误信息
		//个人感觉,状态码应该在这里发送,并返回50x状态码说明内部错误
		return;
	}
	//创建输入管道
	if (pipe(cgi_input) < 0) {
		cannot_execute(client);
		return;
	}
	//fork一个进程,如果失败,同样处理
	if ((pid = fork()) < 0) {
		cannot_execute(client);
		return;
	}
	if (pid == 0) /* child: CGI script */
	{
		//pid为0,说明fork分支路径在执行,下面是子进程处理逻辑
		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]);
		//即使在fork子进程中,所有的变量还是可以读取的
		sprintf(meth_env, "REQUEST_METHOD=%s", method);
		//将REQUEST_METHOD加入到env中
		putenv(meth_env);
		if (strcasecmp(method, "GET") == 0) { 
		//将请求串也加入到env中 QUERY_STRING
			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);
		//如果execl执行失败,退出
		exit(0);
	} else { /* parent */
		//pid不为0,说明还在自身的进程路径下执行
		close(cgi_output[1]);
		close(cgi_input[0]);
		//通过管道与子进程通信
		if (strcasecmp(method, "POST") == 0) //POST将请求内容发给管道
			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);

		close(cgi_output[0]);
		close(cgi_input[1]);
		//等待子进程退出
		waitpid(pid, &status, 0);
	}
}

分析完核心函数,cannot_execute,bad_request,not_found,unimplemented等函数均是显示一些错误提示信息,这里仅贴出unimplemented函数实现。

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, "\r\n");
	send(client, buf, strlen(buf), 0);
	sprintf(buf, "

	HTTP request method not supported.\r\n");
	send(client, buf, strlen(buf), 0);
	sprintf(buf, "\r\n");
	send(client, buf, strlen(buf), 0);
}

最后介绍一下startup函数,它启动server socket。

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);
	name.sin_addr.s_addr = htonl(INADDR_ANY);
	if (bind(httpd, (struct sockaddr *) &name, sizeof(name)) < 0)
		error_die("bind");
	if (*port == 0) /* if dynamically allocating a port */
	{
		int namelen = sizeof(name);
		if (getsockname(httpd, (struct sockaddr *) &name, &namelen) == -1)
			error_die("getsockname");
		*port = ntohs(name.sin_port);
	}
	if (listen(httpd, 5) < 0)
		error_die("listen");
	return (httpd);
}

整个项目很简单,可以理解linux下的socket和常规的进程启动、通信的编写方法。