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