简介
Tinyhttp是一个轻量型Http Server,使用C语言开发,全部代码只500多行,还包括一个简单Client。
源码剖析
Tinyhttp程序的逻辑为:一个无线循环,一个请求,创建一个线程,之后线程函数处理每个请求,然后解析HTTP请求,做一些判断,之后判断文件是否可执行,不可执行,打开文件,输出给客户端(浏览器),可执行就创建管道,父子进程进行通信。其整体处理流程如下:
每个函数的作用如下:
// accept_request函数:处理从套接字上监听到的一个HTTP请求,此函数很大部分体现服务器处理请求流程。
void accept_request(void *);
// bad_request函数:返回给客户端这是个错误请求,HTTP状态码400 Bad Request。
void bad_request(int);
// cat函数:读取服务器上某个文件写到socket套接字。
void cat(int, FILE *);
// cannot_execute函数:处理发生在执行cgi程序时出现的错误。
void cannot_execute(int);
// error_die函数:把错误信息写到perror并退出。
void error_die(const char *);
// execute_cgi函数:运行cgi程序的处理,是主要的函数。
void execute_cgi(int, const char *, const char *, const char *);
// get_line函数:读取套接字的一行,把回车换行等情况都统一为换行符结束。
int get_line(int, char *, int);
// headers函数:把HTTP响应的头部写到套接字。
void headers(int, const char *);
// not_found函数:处理找不到请求的文件时的情况。
void not_found(int);
// serve_file函数:调用cat函数把服务器文件返回给浏览器
void serve_file(int, const char *);
// startup函数:初始化httpd服务,包括建立套接字,绑定端口,进行监听等。
int startup(u_short *);
// unimplemented函数:返回给浏览器表明收到的HTTP请求所用的method不被支持。
void unimplemented(int);
分析其程序,流程为:main()——>startup()——>accept_request()——>execute_cgi()等。
核心函数
1)main()函数
// 服务器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;
// 建立一个监听套接字,在对应的端口建立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函数处理新请求。
/* accept_request(client_sock); */
if (pthread_create(&newthread , NULL, (void *)accept_request, (void *)&client_sock) != 0)
perror("pthread_create");
}
// 出现意外退出的时候,关闭socket
close(server_sock);
return(0);
}
2)startup()函数
// startup函数:按照TCP连接的正常流程依次调用socket,bind,listen函数。
// 监听套接字端口既可以指定也可以动态分配一个随机端口
int startup(u_short *port)
{
int httpd = 0;
struct sockaddr_in name;
// 创建一个socket,建立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);
// 将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);
// 1.getsockname()可以获得一个与socket相关的地址
// 1)服务器端可以通过它得到相关客户端地址
// 2)客户端可以得到当前已连接成功的socket的IP和端口
// 2.在客户端不进行bind而直接连接服务器时,且客户端需要知道当前使用哪个IP地址
// 进行通信时比较有用(如多网卡的情况)
if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1)
error_die("getsockname");
*port = ntohs(name.sin_port);
}
// 开始监听
if (listen(httpd, 5) < 0)
error_die("listen");
// 返回socket id
return(httpd);
}
3)accept_request()函数
// 线程处理函数
void accept_request(void *arg)
{
int client = *(int*)arg;
char buf[1024]; // 读取行数据时的缓冲区
size_t numchars; // 读取了多少字符
char method[255]; // 存储HTTP请求名称(字符串)
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请求报文由请求行(requestline)、请求头部(header)、空行和请求数据4个部分
// 组成,请求行由请求方法字段(get或post)、URL字段和HTTP协议版本字段3个字段组成,它们
// 用空格分隔。如:GET /index.html HTTP/1.1。
// 解析请求行,把方法字段保存在method变量中。
// 读取HTTP头第一行:GET/index.php HTTP 1.1
numchars = get_line(client, buf, sizeof(buf));
i = 0; j = 0;
// 把客户端的请求方法存到method数组
while (!ISspace(buf[i]) && (i < sizeof(method) - 1))
{
method[i] = buf[i];
i++;
}
j=i;
method[i] = '\0';
// 只能识别get和post
if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
{
unimplemented(client);
return;
}
// POST的时候开启cgi
if (strcasecmp(method, "POST") == 0)
cgi = 1;
// 解析并保存请求的URL(如有问号,也包括问号及之后的内容)
i = 0;
// 跳过空白字符
while (ISspace(buf[j]) && (j < numchars))
j++;
// 从缓冲区中把URL读取出来
while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < numchars))
{
// 存在url
url[i] = buf[j];
i++; j++;
}
url[i] = '\0'; // 保存URL
// 先处理如果是GET请求的情况
// 如果是get方法,请求参数和对应的值附加在URL后面,利用一个问号(“?”)代表URL的结
// 尾与请求参数的开始,传递参数长度受限制。如index.jsp?10023,其中10023就是要传递
// 的参数。这段代码将参数保存在query_string中。
if (strcasecmp(method, "GET") == 0)
{
// 待处理请求为url
query_string = url;
// 移动指针,去找GET参数,即?后面的部分
while ((*query_string != '?') && (*query_string != '\0'))
query_string++;
// 如果找到了的话,说明这个请求也需要调用脚本来处理
// 此时就把请求字符串单独抽取出来
// GET方法特点,?后面为参数
if (*query_string == '?')
{
// 开启cgi
cgi = 1;
// query_string指针指向的是真正的请求参数
*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标志位置1)在调用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);
}
// 断开与客户端的连接(HTTP特点:无连接)
close(client);
}
4)execute_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;
// 首先需要根据请求是Get还是Post,来分别进行处理
buf[0] = 'A'; buf[1] = '\0';
// 如果是Get,那么就忽略剩余的请求头
if (strcasecmp(method, "GET") == 0)
// 把所有的HTTP header读取并丢弃
while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */
numchars = get_line(client, buf, sizeof(buf));
// 如果是Post,那么就需要读出请求长度即Content-Length
else if (strcasecmp(method, "POST") == 0) /*POST*/
{
// 对POST的HTTP请求中找出content_length
numchars = get_line(client, buf, sizeof(buf));
while ((numchars > 0) && strcmp("\n", buf))
{
// 使用\0进行分割
buf[15] = '\0';
// HTTP请求的特点
if (strcasecmp(buf, "Content-Length:") == 0)
content_length = atoi(&(buf[16]));
numchars = get_line(client, buf, sizeof(buf));
}
// 如果请求长度不合法(比如根本就不是数字),那么就报错,即没有找到content_length
if (content_length == -1) {
// 错误请求
bad_request(client);
return;
}
}
else/*HEAD or other*/
{
}
// 建立管道
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;
}
sprintf(buf, "HTTP/1.0 200 OK\r\n");
send(client, buf, strlen(buf), 0);
// 子进程要调用CGI脚本
if (pid == 0) /* child: CGI script */
{
// 环境变量缓冲区,会存在溢出风险
char meth_env[255];
char query_env[255];
char length_env[255];
// 重定向管道
// 把父进程读写管道的描述符分别绑定到子进程的标准输入和输出
// dup2功能与freopen()函数类似
dup2(cgi_output[1], STDOUT); // 把STDOUT重定向到cgi_output的写入端
dup2(cgi_input[0], STDIN); // 把STDIN重定向到cgi_input的读取端
// 关闭不必要的描述符
close(cgi_output[0]); // 关闭cgi_inout的写入端和cgi_output的读取端
close(cgi_input[1]);
// 服务器设置环境变量,即request_method的环境变量
// 设置基本的CGI环境变量,请求类型、参数、长度之类
sprintf(meth_env, "REQUEST_METHOD=%s", method);
putenv(meth_env);
if (strcasecmp(method, "GET") == 0) {
// 设置query_string的环境变量
sprintf(query_env, "QUERY_STRING=%s", query_string);
putenv(query_env);
}
else { /* POST */
// 设置content_length的环境变量
sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
putenv(length_env);
}
// 用execl运行cgi程序
execl(path, NULL);
exit(0);
} else { /* parent */
// 父进程代码
// 关闭cgi_input的读取端和cgi_output的写入端
close(cgi_output[1]);
close(cgi_input[0]);
// 对于Post请求,要直接write()给子进程
// 这样子进程所调用的脚本就可以从标准输入取得Post数据
if (strcasecmp(method, "POST") == 0)
// 接收POST过来的数据
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);
}
}
参考文献
http://armsword.com/2014/10/29/tinyhttpd-code-analyse/
http://blog.csdn.net/jcjc918/article/details/42129311
http://techlog.cn/article/list/10182680