C++分析TinyHTTPd源码
2021/9/12 20:06:34
本文主要是介绍C++分析TinyHTTPd源码,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
有关于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); }
这篇关于C++分析TinyHTTPd源码的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2025-01-11国产医疗级心电ECG采集处理模块
- 2025-01-10Rakuten 乐天积分系统从 Cassandra 到 TiDB 的选型与实战
- 2025-01-09CMS内容管理系统是什么?如何选择适合你的平台?
- 2025-01-08CCPM如何缩短项目周期并降低风险?
- 2025-01-08Omnivore 替代品 Readeck 安装与使用教程
- 2025-01-07Cursor 收费太贵?3分钟教你接入超低价 DeepSeek-V3,代码质量逼近 Claude 3.5
- 2025-01-06PingCAP 连续两年入选 Gartner 云数据库管理系统魔力象限“荣誉提及”
- 2025-01-05Easysearch 可搜索快照功能,看这篇就够了
- 2025-01-04BOT+EPC模式在基础设施项目中的应用与优势
- 2025-01-03用LangChain构建会检索和搜索的智能聊天机器人指南