TinyHttpd源码分析
原创 涂飞平
2018-04-20
TinyHttpd是一个C编写的极小HTTP服务器,代码量(包括注释)不到500行,但可以基本说明HTTP服务器的工作原理,从中也能知道如何进行Linux下的C编程(这是我看这个代码的主要原因)。 在Linux下面,我采用Eclipse CPP作为C/C++的IDE,设置好相应的环境变量,以便IDE可以查找链接相应的头文件和库。
源码分析过程如下:
// 函数声明
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和常规的进程启动、通信的编写方法。