有关于TinyHTTPd的源码解析网站已经很多,本文仅记录学习
运行环境CentOS 8,QT;代码可以运行但是有bug,但是用于理解阅读还算可以
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>
#include <strings.h>
#include <string.h>
#include <sys/stat.h>
#include <pthread.h>
#include <sys/wait.h>
#include <stdlib.h>
#define ISspace(x) isspace((int)(x))//判断是否为空格,是空格返回1
#define SERVER_STRING "Server: jdbhttpd/0.1.0\r\n"
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 *);//
int get_line(int, char *, int);//
void headers(int, const char *);
void not_found(int);//
void serve_file(int, const char *);//
int startup(u_short *);//
void unimplemented(int);//
int main(void)
{
//调试测试
printf("run 1");
int server_sock=-1;//服务器的套接字
int client_sock=-1;//客户端的套接字
//struct sockaddr_in client_sock;
//套接字本身是文件描述符,可以用int,也可以用SOCKS
u_short port=0;//端口号
struct sockaddr_in client_name;//用于存储客户端的端口号和网络地址
pthread_t newthread;//定义一个线程
//调试测试
printf("run 1");
//先对服务器新建套接字
server_sock=startup(&port);//将port地址传给startup函数,此时server_sock是一个正在监听的,且有网络地址信息的服务器套接字
//cout<<"server_sock running on port:"<<port<<endl;
printf("server_sock running on port : %d \n",port);
int client_name_len=sizeof(client_name);
while(1){
client_sock = accept(server_sock,(struct sockaddr*)&client_name,&client_name_len);
if(client_sock==-1){
error_die("accept");
}
//收到连接请求后派生线程
if(pthread_create(&newthread,NULL,accept_request,client_sock)){
perror("pthread_create");
}
}
close(server_sock);//关闭套接字,关闭TCP连接
return 0;
}
int startup(u_short * port)
{
int server_sock=0;//定义局部变量,服务器套接字
struct sockaddr_in server_name;
//建立套接字
server_sock=socket(PF_INET,SOCK_STREAM,0);//第一个参数代表IPV4协议族,第二个代表TCP连接,0无意义,server_sock保存套接字的文件符,即代表套接字
if(server_sock==-1){
error_die("socket");//建立套接字错误返回-1
}
memset(&server_name,0,sizeof(server_name));//把server_name结构里面的每一个成员的value设置为0,相当于初始化。
server_name.sin_family=AF_INET;//AF_INET和PF_INET相同,都代表IPV4协议簇
server_name.sin_port=htons(*port);//htons将机器字符序转换成网络字符序,是必要的
server_name.sin_addr.s_addr=htonl(INADDR_ANY);//表示所有本机ip,即0.0.0.0
if(bind(server_sock,(struct sockaddr*)& server_name,sizeof(server_name))<0){
//将服务器端的套接字信息和服务器端的套接字绑定
error_die("bind");
}
//如果端口号为0,随即分配端口号
if(*port == 0){
int server_name_length=sizeof(server_name);
if(getsockname(server_sock,(struct sockaddr*)&server_name, &server_name_length)== -1){
error_die("getsockname");
//server_name里面保存server_sock的信息
}
*port=ntohs(server_name.sin_port);//将网络字符序转换成机器字符序
}
//开始监听
if(listen(server_sock,5)<0){
error_die("listen");
}
return server_sock;//返回服务器的套接字
}
void error_die(const char * error)
{
perror(error);
exit(1);
}
void accept_request(int client_sock)
{
//int client=*(int*)client_sock;//类型转换一下,以适应pthread_create的参数
char buf[1024];//缓冲池
int numchars;
char method[255];//请求方法GET POST
char url[255];//请求的资源路径
char path[512];//文件相对路径
size_t i,j;
struct stat st;//文件结构类型
int cgi=0;//是否执行cgi脚本的参数
char *query_string=NULL;//POST方法的参数指针;
numchars=get_line(client_sock,buf,sizeof(buf));//从client套接字里读取一个buf大小的数据
i=0,j=0;
//读取完之后就开始解析http请求报文的内容
//第一部分的每个信息都是以空格分开的, 请求模式 url http协议版本
while(!ISspace(buf[j]) && i<sizeof(method)-1){
method[i] = buf[j];//第一部分是请求方法,GET 或 POST
i++;
j++;
}
method[i]='\0';//c风格字符串
//确定方法,strcasecmp匹配成功返回0
if(strcasecmp(method,"GET") && strcasecmp(method,"POST")){
unimplemented(client_sock);//两种方法都不是报错
return;
}
if(strcasecmp(method,"POST") == 0){
//POST 类型
cgi=1;
}
i=0;
//接着上次的j读取,过滤空格,method空格后面是url
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){
//GET 方法
query_string = url;//query_string 指针指向url
while((*query_string)!='?' && (*query_string)!= '\0'){
//截取url ? 之前的字符,即域名,可能会包括路径,统一来将,问好前面是资源路径
query_string++;
}
//如果有'?'说明有参数,因为参数放在?的后面,既然有参数,就属于有参数的GET模式,执行cgi脚本
if(*query_string == '?'){
cgi=1;
*query_string='\0';
query_string++;
}
}
//下面是TinyHTTPd项目的htodcs文件下的文件
sprintf(path,"htdocs%s",url);//打印 htdocs+url,并将url从头到'\0'的部分给path,'\0'后面还有别的字符
if(path[strlen(path)-1] == '/'){
//最后一个符号是'/'说明文件类型是目录,则返回index.html,即目录里面的默认文件
strcat(path,"index.html");
}
//根据path路径,找文件,并获取path信息保存在 文件结构体st中
if(stat(path,&st)==-1){
//执行失败,文件未找到,则丢弃请求报文的所有行的字符,并报错退出
while(numchars>0 && strcmp("\n",buf)){//不为空且不为换行符
numchars=get_line(client_sock,buf,sizeof(buf));//把字符从客户端套接字里面全部取出来
not_found(client_sock);
}
}
else{
//获取文件信息成功
//如果是目录,则默认使用目录下的index.html 文件
if((st.st_mode & S_IFMT) == S_IFDIR){
//文件模式与S_IFMT相与,得到的结果如果等于S_IFDIR,意思就是如果文件模式是目录
strcat(path,"/index.html");//在路径后面加上index.html,是目录就默认打开目录下的index.html文件
}
if((st.st_mode & S_IXUSR)||
(st.st_mode & S_IXGRP)||
(st.st_mode & S_IXOTH)){
cgi=1;
}
if(!cgi){
//如果不是cgi程序,则就是静态页面请求
serve_file(client_sock,path);//直接返回文件信息给客户端,静态页面被返回
}
else{
//cgi程序,执行cgi脚本
execute_cgi(client_sock,path,method,query_string);
//query_string此时正指向url '?'之后的第一个字符,是参数部分
//如果是POST模式,则就需要在 请求报文的body部分提取参数
//如果是GET模式,就要在 query_string之后提取参数
//path是保存的资源路径
}
}
close(client_sock);
//return;
}
void serve_file(int client_sock, const char *path)
{
//用来返回文件
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_sock,buf,sizeof (buf));//把客户端套接字读取完
}
resource = fopen(path,"r");//r:只读的方式打开文件
if(resource == NULL){
not_found(client_sock);
}
else{
headers(client_sock,path);//先返回文件头部信息
cat(client_sock,resource);//将resource描述符指定文件中的数据发送给客户端的套接字
}
fclose(resource);
}
void headers(int client_sock, const char *path)
{
char buf[1024];
(void)path;//这句是什么意思?强制类型转化一下
strcpy(buf, "HTTP/1.0 200 OK\r\n");
send(client_sock, buf, strlen(buf), 0);
strcpy(buf, SERVER_STRING);
send(client_sock, buf, strlen(buf), 0);
sprintf(buf, "Content-Type: text/html\r\n");
send(client_sock, buf, strlen(buf), 0);
strcpy(buf, "\r\n");
send(client_sock, buf, strlen(buf), 0);
}
void cat(int client_sock, FILE *resource)
{
//将文件结构指针resourece中的数据发送给client_sock
char buf[1024];
fgets(buf,sizeof (buf),resource);//从文件结构指针resource中读取数据,保存在buf中
//处理文件流剩下的字符
while(!feof(resource)){
//指针没有到尾部
send(client_sock,buf,strlen(buf),0);//将文件流中的字符全部发送给client_sock
fgets(buf,sizeof (buf),resource);
}
}
//从客户端套接字读取一行数据,以\r\n为结束符
//http的请求报文第一行也就是第一部分 是 请求方式 url http版本,GET的参数信息在url的?之后
//第二部分是请求头部,第一行之后的部分,说明服务器要使用的附加信息
//第三部分是空行,请求头部后面的空行是必须的
//最后一部分是主体,body,可以添加数据,POST的提交数据就在body里面
int get_line(int client_sock, char *buf, int buf_size)
{
int i=0;
int n;
char c='\0';
//读取一行
while(i<buf_size-1 && c!='\n'){
n=recv(client_sock,&c,1,0);//每次从client_sock里面读取一个字符
if(n>0){
if(c=='\r'){
//读取到回车符就再读取一个,如果没有则结束,如果是换行符则丢弃回车符,以换行符为结尾
n=recv(client_sock,&c,1,MSG_PEEK);//MSG_PECK标志将读取的字符保留在窗口
if(n>0 && c=='\n'){
recv(client_sock,&c,1,0);//将上次保留的字符读取并丢弃
}
else{
c='\n';//窗口里的字符不是回车符或没有字符,则以回车符结尾
}
}
buf[i]=c;
i++;
}
//没有读取到数据
else{
c='\n';
}
}
buf[i]='\0';//c风格字符串
return i;
//返回结束后,buf里面存有client_sock的一行数据,包括空格,但只有一行
}
void unimplemented(int client_sock)
{
char buf[1024];
//http method 不被支持
sprintf(buf,"HTTP/1.0 501 Method Not Implemented\r\n");
send(client_sock,buf,strlen(buf),0);
//服务器信息
sprintf(buf,SERVER_STRING);
send(client_sock,buf,strlen(buf),0);
sprintf(buf,"Content-Type:text/html\r\n");
send(client_sock,buf,strlen(buf),0);
sprintf(buf,"\r\n");
send(client_sock,buf,strlen(buf),0);
sprintf(buf,"<HTML><HEAD><TITLE>Method Not Implemented\r\n");
send(client_sock,buf,strlen(buf),0);
sprintf(buf,"</TITLE></HEAD>\r\n");
send(client_sock,buf,strlen(buf),0);
sprintf(buf,"<BODY><P>HTTP request method not supported.\r\n");
send(client_sock,buf,strlen(buf),0);
sprintf(buf,"</BODY></HTML>\r\n");
send(client_sock,buf,strlen(buf),0);
}
void not_found(int client_sock)
{
char buf[1024];
sprintf(buf, "HTTP/1.0 404 NOT FOUND\r\n");
send(client_sock, buf, strlen(buf), 0);
sprintf(buf, SERVER_STRING);
send(client_sock, buf, strlen(buf), 0);
sprintf(buf, "Content-Type: text/html\r\n");
send(client_sock, buf, strlen(buf), 0);
sprintf(buf, "\r\n");
send(client_sock, buf, strlen(buf), 0);
sprintf(buf, "<HTML><TITLE>Not Found</TITLE>\r\n");
send(client_sock, buf, strlen(buf), 0);
sprintf(buf, "<BODY><P>The server could not fulfill\r\n");
send(client_sock, buf, strlen(buf), 0);
sprintf(buf, "your request because the resource specified\r\n");
send(client_sock, buf, strlen(buf), 0);
sprintf(buf, "is unavailable or nonexistent.\r\n");
send(client_sock, buf, strlen(buf), 0);
sprintf(buf, "</BODY></HTML>\r\n");
send(client_sock, buf, strlen(buf), 0);
}
void execute_cgi(int client_sock, 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;//处理POST模式
//这两个是什么意思?
buf[0]='A';
buf[1]='\0';
if(strcasecmp(method,"GET")==0){
//GET方法,一般用于获取or查询信息
while(numchars>0 && strcmp("\n",buf)){
numchars=get_line(client_sock,buf,sizeof(buf));//把客户端套接字里面的数据全部读取并存放在buf中,其实get模式的参数在url中
}
}
else{
//POST模式
numchars = get_line(client_sock,buf,sizeof (buf));//读取一行
//获取http消息实体的传输长度 content_length
while(numchars>0 && strcmp("\n",buf)){
buf[15]='\0';//说明buf留了前14位,看看是不是content-length:这十四个字符
if(strcasecmp(buf,"Content-Length:")==0){
content_length = atoi(&(buf[16]));//报文格式,content-length:之后就是长度,保存下来即可
}
numchars = get_line(client_sock,buf,sizeof (buf));//循环
}
if(content_length==-1){
bad_request(client_sock);//请求的页面数据为空,没有数据,就是我们打开网页经常出现空白页面
return;
}
}
sprintf(buf,"HTTP/1.0 200 OK\r\n");
send(client_sock,buf,strlen(buf),0);
//建立管道,两个通道,cgi_output[0]:读取端,cgi_output[1]:写入端
//管道只能在具有公共祖先的进程之间进行,这里是父子进程之间
if(pipe(cgi_output)<0){
//建立管道失败
cannot_execute(client_sock);
return;
}
if(pipe(cgi_input)<0){
//建立管道失败
cannot_execute(client_sock);
return;
}
//创建子进程,这样就创建了父子进程之间的IPC通道
if((pid=fork())<0){
//创建进程失败
cannot_execute(client_sock);
return;
}
//下面是实现进程之间的管道通信机制
/*子进程继承了父进程的pipe,然后通过关闭子进程output管道的输出端,input管道的写入端;
关闭父进程output管道的写入端,input管道的输出端*/
//子进程
if(pid == 0){
char meth_env[255];
char query_env[255];
char length_env[255];
//复制文件句柄,重定向进程的标准输入输出
//dup2 的第一个参数描述符关闭
dup2(cgi_output[1],1);//标准输出重定向到output的写入端
dup2(cgi_input[0],0);//标准输入重定向到input的输入端
close(cgi_input[1]);
close(cgi_output[0]);
//这样outpput就是输出,input就是输入,理论是只用一个管道也可以
sprintf(meth_env,"REQUEST_METHOD=%s",method);
putenv(meth_env);//该函数作用是改变或增加环境变量,现在很少有程序用这个东西了,毕竟tinyhttpd也是一个很老的程序,搞不懂这个函数是干啥用的
if(strcasecmp(method,"GET")==0){
//GET模式
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);//exec函数簇,执行CGI脚本,获取cgi的标准输出作为相应内容发送给客户端
//通过dup2重定向,标准输出内容进入管道output的输入端
exit(0);//子进程退出
}
else{
//父进程
close(cgi_output[1]);//关闭管道的一端,这样可以建立父子进程间的管道通信
close(cgi_input[0]);
/*通过关闭对应管道的通道,然后重定向子进程的管道某端,这样就在父子进程之间构建一条单双工通道如果不重定向,将是一条典型的全双工管道通信机制*/
if(strcasecmp(method,"POST")==0){
//POST模式
for(i=0;i<content_length;i++){
recv(client_sock,&c,1,0);//从客户端的套接字一次接受一个字符
write(cgi_input[1],&c,1);//写入input,重定向到标准输入,就是作为输入用
//数据传送:input[1]父进程 -> input[0]子进程 标准输入 执行cgi程序 -> STDIN -> STDOUT -> output[1]子进程 标准输出 -> output[0] 父进程得到子进程的输出,发送给客户端套接字
}
while(read(cgi_output[0],&c,1)>0){
//读取output的管道输出到客户端,output输出端为cgi脚本执行后的内容
//即将cgi执行结果发送给客户端,即send到浏览器,如果不是POST则只有这一处理
send(client_sock,&c,1,0);
}
//关闭剩下的管道端,子进程在执行dup2之后,就已经关闭了管道的一端,这里关闭另一端
close(cgi_output[0]);
close(cgi_input[1]);
waitpid(pid,&status,0);//等待子进程结束
}
}
}
void bad_request(int client_sock)
{
char buf[1024];
/*将字符串存入缓冲区,再通过send函数发送给客户端*/
sprintf(buf, "HTTP/1.0 400 BAD REQUEST\r\n");
send(client_sock, buf, sizeof(buf), 0);
sprintf(buf, "Content-type: text/html\r\n");
send(client_sock, buf, sizeof(buf), 0);
sprintf(buf, "\r\n");
send(client_sock, buf, sizeof(buf), 0);
sprintf(buf, "<P>Your browser sent a bad request, ");
send(client_sock, buf, sizeof(buf), 0);
sprintf(buf, "such as a POST without a Content-Length.\r\n");
send(client_sock, buf, sizeof(buf), 0);
}
void cannot_execute(int client)
{
char buf[1024];
/*回馈出错信息*/
sprintf(buf, "HTTP/1.0 500 Internal Server Error\r\n");
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, "<P>Error prohibited CGI execution.\r\n");
send(client, buf, strlen(buf), 0);
}