概述
之前在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 %dn", 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] = '