之前在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);
}
}