目录
自主web服务器
背景
http协议被广泛使用,从移动端,pc端浏览器,http协议无疑是打开互联网应用窗口的重要协议,http在网络应用层中的地位不可撼动,是能准确区分前后台的重要协议。
目标
在对http协议的理论学习的基础上,从零开始完成web服务器开发,坐拥下三层协议,从技术到应用,让网络难点无处遁形。
描述
采用C/S模型,编写支持中小型应用的http,并结合mysql,理解常见互联网应用行为,做完该项目,你可以从技术上 完全理解从你上网开始,到关闭浏览器的所有操作中的技术细节!
技术特点
- 网络编程(TCP/IP协议, socket流式套接字,http协议)
- 多线程技术
- cgi技术
- 线程池
项目定位
研发岗
- 开发环境 centos 7 + vim/gcc/gdb + C/C++;
项目实现过程
由于我们编写的是HTTP_SERVER,因此我们只需要编写s端,c端我们使用浏览器进行访问即可;
我们需要对**应用层(主要)**和传输层进行代码编写,网络层及一下,会有对应的TCP/IP协议来保证数据的交互;
下图表示短连接下,C端发起请求,S端响应请求,一来一回 之后关闭sock;
创建HttpServer基础框架
先创建一个能接收到浏览器HTTP报文的socket框架;
TcpServer.hpp
这里将TcpServer中的socker,bind,listen进行了封装,用Init启动,同时设计了单例模式,一个HttpServer只需要一个监听listen_sock即可!
#pragmaonce#include<iostream>#include<cstdlib>#include<cstring>#include<sys/types.h>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>#include<unistd.h>#include<pthread.h>#include"Log.hpp"using std::cout;using std::endl;#defineBACKLOG5enumERR{
SOCK_ERR =1,
BIND_ERR,
LISTEN_ERR,
USAGE
};classTcpServer{private:int port;int listen_sock;static TcpServer* svr;private://单例模式TcpServer(int _port):port(_port)//私有构造{}TcpServer(const TcpServer &s)//私有拷贝构造{}public:static TcpServer *getinstance(int port)//单例模式{static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;if(nullptr== svr){pthread_mutex_lock(&lock);if(nullptr== svr){
svr =newTcpServer(port);
svr ->InitServer();//getinstance的时候就搞定了sock bind listen了;}pthread_mutex_unlock(&lock);}return svr;}public:voidInitServer(){Socket();Bind();Listen();LOG(INFO,"TcpServer begin");//日志}voidSocket(){
listen_sock =socket(AF_INET, SOCK_STREAM,0);if(listen_sock <0){LOG(FATAL,"socket error");exit(SOCK_ERR);}//防止bind errorint opt =1;setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR,&opt,sizeof(opt));}voidBind(){
sockaddr_in local;bzero(&local,sizeof(local));
local.sin_family = AF_INET;
local.sin_port =htons(port);
local.sin_addr.s_addr = INADDR_ANY;//云服务器这样绑if(bind(listen_sock,(sockaddr *)&local,sizeof(local))<0){LOG(FATAL,"bind error");exit(BIND_ERR);}}voidListen(){if(listen(listen_sock, BACKLOG)<0){LOG(FATAL,"listen error");exit(LISTEN_ERR);}}intSock(){return listen_sock;}~TcpServer(){if(listen_sock >0)close(listen_sock);}};//单例
TcpServer *TcpServer::svr =nullptr;
HttpServer.hpp
#pragmaonce#include<iostream>#include<signal.h>#include<pthread.h>#include"Log.hpp"#include"TcpServer.hpp"#include"Protocol.hpp"#definePORT8080//默认端口号classHttpServer{private:int port;bool stop;public:HttpServer(int _port = PORT):port(_port),stop(false){}voidInitServer(){// singal(SIGPIPE,SIG_IGN);}voidLoop()//循环监听c端逻辑{
TcpServer *tsvr =TcpServer::getinstance(port);// TcpServer里面就处理了,sock bind listen TcpServer里面就处理了LOG(INFO,"Loop Begin");while(!stop){
sockaddr_in peer;
socklen_t len =sizeof(peer);int sock =accept(tsvr->Sock(),(sockaddr *)&peer,&len);if(sock <0)continue;LOG(INFO,"Get a new link");//到这里 httpserver整体就能接收新连接了!//创建handler线程,将连接的sock甩进去,再loop循环以后的c端链接
pthread_t tid;int*psock =newint(sock);//注意局部变量的传参pthread_create(&tid,nullptr,Entrance::HandlerRequest,psock);pthread_detach(tid);}}~HttpServer(){}};
Log.hpp
建议的日志系统
#pragmaonce#include<iostream>#include<string>#include<ctime>//日志处理#defineINFO#defineWARNING#defineERROR#defineFATAL#defineLOG(level, message)Log(#level, message,__FILE__,__LINE__)//替换下列函数的宏,方便日志的传参voidLog(std::string level, std::string message, std::string file_name,int line){
std::cout <<"["<< level <<"] "<<"["<<time(nullptr)<<"] "<<"["<< message <<"] "<<"["<< file_name <<"] "<<"["<< line <<"] "<< std::endl;}
Protocol.hpp
订制一系列的协议,用于才做http报文。构建响应等;
#pragmaonce#include<iostream>#include<unistd.h>#include<sys/types.h>#include<sys/socket.h>using std::cout;using std::endl;classEntrance//临时方案{public://loop创建的线程执行任务的函数staticvoid*HandlerRequest(void*psock){int sock =*(int*)psock;delete(int*)psock;char buff[4022];int s =recv(sock, buff,4022,0);
buff[s-1]='\0';
cout <<"===============begin==============="<< endl;
cout << buff << endl;
cout <<"===============end==============="<< endl;returnnullptr;}};
运行结果
前三行是打印的日志信息,后面是c端浏览器访问我们server的时候发送的报文,我们将它打印出来了;
解析C端发来的HTTP报文
可见,报文都是一行一行的,我们需要按行读取,先来个按行读取的工具!
MSG_PEEK标志位
recv(sock, &c, 1, MSG_PEEK);
我们一般是设置为0,如果设置MSG_PEEK标志位,则仅仅是把tcp缓冲区中的数据拷贝式的读取到buf中,并没有把已读取的数据从tcp缓冲区中移除,相当于peek窥探一下; 这样我们就可以处理的同时,防止破坏下个报文的报头,造成数据报文不完整了;
Util.hpp
工具类Util
#pragmaonce#include<iostream>#include<string>#include<sys/types.h>#include<sys/socket.h>using std::string;//工具类classUtil{public:staticintReadLine(int sock, string &out)//按一行读取报文,返回长度;{char c ='X';while(c !='\n'){
ssize_t s =recv(sock,&c,1,0);//(注意,有的报文以\r\n 或者 \r结尾,统一处理为\n,同时考虑数据粘包问题进行读取!)if(s >0){if(c =='\r'){recv(sock,&c,1, MSG_PEEK);//窥探一下if(c =='\n'){//窥探成功!大胆拿走这个\n 放入c中recv(sock,&c,1,0);}else{//窥探失败,直接换掉这个\r
c ='\n';}}
out += c;}elseif(s ==0){return0;}else{return-1;}}return out.size();}};
用Entrance收到报文测试,然后调用按行读取一次,结果如下(调用一次,读取一行,即便请求行)
构建请求与响应类
Protocol.hpp
//请求类classHttpRequest{public:
string request_line;//读取请求行
vector<string> request_header;//读取请求报头
string blank;//空行分隔符
string request_body;//请求报文主体(可能没有)//解析完毕之后的结果//解析请求行三部分
string method;
string uri;// path?args
string version;//解析请求报头
unordered_map<string, string> header_kv;int content_length;//请求body的大小
string path;//请求路径
string suffix;//后缀 .html <-> query_string: type/html
string query_string;bool cgi;// cgi技术开关int size;//响应的html文件的size大小public:HttpRequest():content_length(0),cgi(false){}~HttpRequest(){}};//响应类classHttpResponse{public:
string status_line;//状态行
vector<std::string> response_header;//响应报头
string blank;//空行分隔符
string response_body;//响应报文主体(html)int status_code;int fd;public:HttpResponse():blank(LINE_END),status_code(OK),fd(-1){}~HttpResponse(){}};
上述部分成员后续解析报文详细讲解;
读取,解析请求构建响应
读取请求
读取请求的目的为将整个报文按照一定的格式读入请求类中;
- 请求行放入string request_line
- 请求报头存入vector<string> request_header;
- 空行分隔符放入string blank
- 请求正文(如果有)放入request_body;
//读取请求,分析请求,构建响应// IO通信classEndPoint{private:int sock;
HttpRequest http_request;
HttpResponse http_response;bool stop;public:EndPoint(int _sock):sock(_sock),stop(false){}public:boolRecvHttpRequestLine()//读取请求行{auto&line = http_request.request_line;if(Util::ReadLine(sock, line)<=0){
stop =true;}else{
line.resize(line.size()-1);//去掉多余的'\n',塞入日志;LOG(INFO, http_request.request_line);}// cout << "RecvHttpRequestLine: " << stop << endl;return stop;}boolRecvHttpRequestHeader()//读取请求报头 去掉多余的\n{auto&v = http_request.request_header;while(1)//注意 vector[0]没有值的时候只能push_back进去噢 v[0]=? 会段错误 越界;{
string line;if(Util::ReadLine(sock, line)<=0){
stop =true;break;}if(line =="\n"){
http_request.blank = line;//空行break;}//正常 k:v \n
line.resize(line.size()-1);//去\n
http_request.request_header.push_back(line);LOG(INFO, line);}return stop;}};boolIsNeedRecvHttpRequestBody()//判断需不需要读 POST方法+存在contentlength,就要读取body了{auto& method = http_request.method;auto& mp = http_request.header_kv;if(method =="POST"){if(mp.find("Content-Lenght")!=mp.end()){
http_request.size =atoi(mp["Content-Lenght"].c_str());//记录一下body的sizereturntrue;}returntrue;}}boolRecvHttpRequestBody(){if(IsNeedRecvHttpRequestBody()){int len = http_request.size;//这里不能&,不然下面循环 原来的size就减没了,为啥这么精确 -->防止粘包auto body = http_request.request_body;for(int i =0;i<len;i++){char c;int s =recv(sock,c,1,0);if(s>0){
body+=c;}else{
stop =true;break;}}return stop;}}boolIsNeedRecvHttpRequestBody()//判断需不需要读 POST方法+存在contentlength,就要读取body了{auto&method = http_request.method;auto&mp = http_request.header_kv;if(method =="POST"){if(mp.find("Content-Length")!= mp.end()){
http_request.size =atoi(mp["Content-Length"].c_str());//记录一下body的sizereturntrue;}returnfalse;}returnfalse;}boolRecvHttpRequestBody(){if(IsNeedRecvHttpRequestBody()){int len = http_request.size;//这里不能&,不然下面循环 原来的size就减没了,为啥这么精确 -->防止粘包auto body = http_request.request_body;for(int i =0; i < len; i++){char c;int s =recv(sock,&c,1,0);//流式读取if(s >0){
body += c;}else{
stop =true;break;}}
cout << endl;
cout << body << endl;return stop;}}
注意正文的读取需要配合后面的parse先解析拿出参数,再判断有没有正文读取;
解析请求
解析请求的过程为将读取的request报文的对应属性和内容存入特定的请求类中;用于后续构建响应直接对照构建;
- 请求行的三个属性提取出来分别放入method,uri,version
- 请求报头数组中的一个个k:v分别提出来进行unordered_map的映射{k,v},方便后续直接查询
Util.hpp添加一个工具函数
staticboolCutString(const std::string &target, std::string &sub1_out, std::string &sub2_out, std::string sep){
size_t pos = target.find(sep);if(pos!=string::npos){
sub1_out = target.substr(0,pos);
sub2_out = target.substr(pos+sep.size());//": "header以这个分割的,那就得+2!,注意细节,正常的"?"来分割就加1,实现了通用!!returntrue;}returnfalse;}
stringstream类用法
voidParseHttpRequestLine()//解析请求行,入method,uri,version{// GET / HTTP/1.1 三部分用" "分隔
stringstream ss(http_request.request_line);
ss >> http_request.method >> http_request.uri >> http_request.version;auto&method = http_request.method;
std::transform(method.begin(), method.end(), method.begin(),::toupper);//将请求方法大小写同一;// cout<<http_request.method<<http_request.uri<<http_request.version<<endl;}voidParseHttpRequestHeader()//解析请求报头,入header_kv;{auto&mp = http_request.header_kv;auto&v = http_request.request_header;for(auto&e : v){//"k:v"->mp(k,v)
string k, v;Util::CutString(e, k, v,":");
mp[k]= v;}// for(auto&e:mp){// cout<<e.first<<":"<<e.second<<endl;// }}
构建响应
响应格式
stat系统函数
#include<sys/types.h>#include<sys/stat.h>#include<unistd.h>intstat(constchar*path,structstat*buf);//Linux获取文件信息的系统接口//参数1:文件路径//参数2:stat st;&st 将特定目录下文件的信息保存在st中;//返回值:成功返回0,失败返回-1;
其中st_mode有:
static string Code2Desc(int code)//状态码->状态描述{
std::string desc;switch(code){case200:
desc ="OK";break;case404:
desc ="Not Found";break;default:break;}return desc;}static std::string Suffix2Desc(const std::string &suffix)//后缀->Content-Type{static std::unordered_map<std::string, std::string> suffix2desc ={{".html","text/html"},{".css","text/css"},{".js","application/javascript"},{".jpg","application/x-jpg"},{".xml","application/xml"},};auto iter = suffix2desc.find(suffix);if(iter != suffix2desc.end()){return iter->second;}return"text/html";//默认返回html的type}voidBuildHttpResponse(){structstat st;int size;
ssize_t rfound;
string _path;// tempauto&status_code = http_response.status_code;auto&method = http_request.method;if(method !="GET"&& method !="POST"){//非法method
status_code = BAD_REQUEST;LOG(WARNING,"method error!");goto END;}//构建请求路径path 和 请求文件大小size;if(method =="GET"){if(http_request.uri.find("?")!= string::npos)// get 带参// 引入cgi{// GET: path? content=...参数Util::CutString(http_request.uri, http_request.path, http_request.query_string,"?");//构建path路径
http_request.cgi =true;//有参数 引入cgi}else{
http_request.path = http_request.uri;}}elseif(method =="POST")// cgi{
http_request.path = http_request.uri;
http_request.cgi =true;}else{// DO Noting}//请求路径 我们上层得套wwwroot,index.html等默认
_path = http_request.path;
http_request.path = WEB_ROOT;
http_request.path += _path;//如果路径末尾为'/' 意味着是个目录,我们需要套上index.htmlif(http_request.path.find('/')== http_request.path.size()-1){
http_request.path += HOME_PAGE;}//判断文件存在?存在属性保存进stif(stat(http_request.path.c_str(),&st)==0){if(S_ISDIR(st.st_mode)){//是个目录不是html文件,特殊处理到默认
http_request.path +='/';
http_request.path += HOME_PAGE;stat(http_request.path.c_str(),&st);//更新path文件的信息}if((st.st_mode & S_IXUSR)||(st.st_mode & S_IXGRP)||(st.st_mode & S_IXOTH)){//是个可执行程序!不是html
http_request.cgi =true;//特殊处理cgi}
size = st.st_size;}else{//说明资源是不存在的LOG(WARNING, http_request.path +"Not Found!");
status_code = NOT_FOUND;goto END;}//构建suffix后缀
rfound = http_request.path.rfind(".");//构建suffix:<-->type映射;if(rfound == string::npos){//没有.后缀 //suffix 默认 .html
http_request.suffix =".html";}else{
http_request.suffix = http_request.path.substr(rfound);//.xxx 文件类型}// cgi处理还是Noncgi处理?if(http_request.cgi){// status_code = ProcessCgi(); //执行目标程序,拿到结果:http_response.response_body;}else{// 1. 目标网页一定是存在的// 2. 返回并不是单单返回网页,而是要构建HTTP响应!全套!
status_code =ProcessNonCgi(size);//简单的网页返回,返回静态网页,只需要打开即可}
END:return;BuildHttpResponseHelper();//状态行填充了,响应报头也有了, 空行也有了,正文有了}intProcessNonCgi(int size)//非cgi的静态网页响应{//这里一定有目的path了,构建response
http_response.fd =open(http_request.path.c_str(), O_RDONLY);if(http_response.fd >=0){//构建状态行
http_response.status_line += HTTP_VERSION;//版本号
http_response.status_line +=" ";
http_response.status_line += std::to_string(http_response.status_code);//状态码
http_response.status_line +=" ";
http_response.status_line +=Code2Desc(http_response.status_code);//状态码描述
http_response.status_line += LINE_END;
http_response.size = size;//构建报头
string header_line ="Content-Type: ";
header_line +=Suffix2Desc(http_request.suffix);
header_line += LINE_END;
http_response.response_header.push_back(header_line);
header_line ="Content-Length: ";
header_line += std::to_string(size);
header_line += LINE_END;
http_response.response_header.push_back(header_line);//构建空行分隔符
http_response.blank = LINE_END;//body不需要构建,是个html网页源码,不需要拉到用户层,等会直接sendfile出去就行,高效return OK;}return404;}
发送响应
sendfile系统函数
sendfile函数在两个文件描述符之间传递数据(完全在内核中操作),从而避免了内核缓冲区和用户缓冲区之间的数据拷贝,效率很高,被称为零拷贝。函数定义为:
#include<sys/sendfile.h>
ssize_t senfile(int out_fd,int in_fd,off_t* offset,size_t count);//in_fd参数是待读出内容的文件描述符,//out_fd参数是待写入内容的文件描述符。//offset参数指定从读入文件流的哪个位置开始读,如果为空,则使用读入文件流默认的起始位置。//count参数指定文件描述符in_fd和out_fd之间传输的字节数。
voidSendHttpResponset(){//发状态行send(sock, http_response.status_line.c_str(), http_response.status_line.size(),0);//发报头for(auto iter : http_response.response_header){send(sock, iter.c_str(), iter.size(),0);}//发\nsend(sock,"\n",1,0);//发bodysendfile(sock, http_response.fd,nullptr, http_response.size);close(http_response.fd);}
运行效果:
上面是我们调用非Cgi技术返回本地静态网页的过程,这显然是不够的,有时候c端请求会带参数需要我们server端处理,这时候就需要引入Cgi技术了;
Cgi技术
简介CGI(Common Gateway Interface)公共网关接口,是外部扩展应用程序与 Web 服务器交互的一个标准接口。它可以使外部程序处理www上客户端送来的表单数据并对此作出反应,通过某些特定的方式处理数据返回给Web服务器进而返回给c端;
虽然我们是创建新线程执行每个c端请求的,但由于我们http_server的进程只有一个,想要到特定位置执行cgi程序,此处不能直接exec替换掉当前进程,否侧httpserver直接没了;
那么就需要创建子进程进行一系列替换操作了;为了实现数据的交互,我们需要同时引入进程间通信,由于是父子之间,那就匿名管道!(因为管道是单向通信,我们要双向通信,所以搞两个管道)
可我们打开两个管道后,父子进程可以看到没错,当子进程进行exec程序替换(只替换代码和数据)之后,这两个匿名管道是数据没了管道还是存在的,(虽然还是存在着的,但是替换的程序看不到的),因为相当于一个全新的进程开始运行,他的文件描述符数组只有初始的0,1,2号fd;3,4号这两个打开的管道被藏起来了,那怎么处理呢?
采用如下设计(一种约定):
我们采用dup2把0,1号标准fd重定向成当前的两个管道3,4;之后再exec替换,exec替换的程序里里是有0,1标准输入输出的,但是他其实已经被替换成两个管道了,用0,1就可以完成server与cgi.exe的交互了;
cgi程序获取数据
- 当c端GET方法发送数据时,一般比较短,我们直接利用环境变量导入可以让cgi程序拿到;
- 当c端POST方法发送数据时,我们直接通过管道写入cgi;
- 当然至于是GET还是POST方法,我们需要导入一个METHOD方法环境变量,让cgi程序可以识别
intProcessCgi(){auto&bin = http_request.path;// cgi.exe的位置,子进程exec它auto&method = http_request.method;// GET OR POSTauto&body = http_request.request_body;// POST 多 直接write到childauto&querystring = http_request.query_string;// GET 少 利用环境变量
string query_string_env;
string method_env;//站在父进程的角度创建匿名管道;int input[2];int output[2];if(pipe(input)<0){LOG(ERROR,"pipe input error!");return404;}if(pipe(output)<0){LOG(ERROR,"pipe output error!");return404;}//创建子进程,进行cgi
pid_t pid =fork();if(pid ==0){// childclose(input[0]);close(output[1]);//在子进程角度// input[0]:读入->fd:0<->output[0];// input[1]:写出->fd:1<->input[1];dup2(output[0],0);dup2(input[1],1);//让替换的cgi程序知道GET还是POST方法,对应选择接收数据的方式
method_env ="METHOD=";
method_env += method;putenv((char*)method_env.c_str());if(method =="GET"){
query_string_env ="QUERY_STRING=";
query_string_env += querystring;putenv((char*)query_string_env.c_str());}// exec* bin// dup2替换fd之后,execl替换程序直接对0,1进行读取与写入,实际上就是与http_server的读取和写入execl(bin.c_str(), bin.c_str(),nullptr);exit(1);// execl失败}elseif(pid <0){// error;return404;LOG(ERROR,"fork error!");}else{// parentclose(input[1]);//父从cgi读,关掉写close(output[0]);//夫给cgi写,关掉读//post方法传的参数多,父进程直接cgi给exec程序if(method =="POST"){constchar*start = body.c_str();int total =0;int size =0;while((size =write(output[1], start + total, body.size()- total))>0){
total += size;}}waitpid(pid,nullptr,0);//fd资源释放close(input[0]);close(output[1]);}return OK;}
test_cgi.cc
#include<iostream>#include<cstdlib>#include<unistd.h>usingnamespace std;intmain(){
cerr <<"========================cgi begin==================="<< endl;//用cerr测试,亦谓cout已经被我们替换成管道了!
string method =getenv("METHOD");
cerr <<"METHOD = "<< method << endl;
string query_string;if(method =="GET"){
query_string =getenv("QUERY_STRING");
cerr <<"GET DeBug query_string = "<< query_string << endl;}elseif(method =="POST"){
cerr <<"Content-length = "<<getenv("CONTENT_LENGTH")<< endl;int count_length =atoi(getenv("CONTENT_LENGTH"));while(count_length--){char c;read(0,&c,1);
query_string += c;}
cerr <<"POST DeBug query_string = "<< query_string << endl;}else{}//数据处理...
cerr <<"========================cgi end==================="<< endl;return0;}
Makefile的封装
bin=server
cgi=test_cgi
cc=g++
LD_FLAGS=-std=c++11 -lpthread
curr=$(shell pwd)src=main.cc
ALL:$(bin)$(cgi)
.PHONY:ALL
$(bin):$(src)$(cc) -o $@ $^ $(LD_FLAGS)$(cgi):cgi/test_cgi.cc
$(cc) -o $@ $^
.PHONY:clean
clean:
rm -f $(bin)$(cgi)rm -rf output
.PHONY:output #发布软件 make out
output:
mkdir -p output
cp$(bin) output
cp -rf wwwroot output
cp$(cgi) output/wwwroot
运行结果:
GET:
POST:
cgi程序处理并返回数据
cgi程序对读入的数据进行处理;在返回给http_server,进而返回给sock(c端链接)
test_cgi.cc
#include<iostream>#include<cstdlib>#include<unistd.h>usingnamespace std;boolGetQueryString(string &query_string){bool result =false;
string method =getenv("METHOD");
cerr <<"METHOD = "<< method << endl;if(method =="GET"){
query_string =getenv("QUERY_STRING");
result =true;}elseif(method =="POST"){
cerr <<"Content-length = "<<getenv("CONTENT_LENGTH")<< endl;int count_length =atoi(getenv("CONTENT_LENGTH"));while(count_length--){char c;read(0,&c,1);
query_string += c;}
result =true;}else{
result =false;}return result;}voidCutString(string &in,const string &sep, string &out1, string &out2){int index;if((index = in.find(sep))!= string::npos){
out1 = in.substr(0, index);
out2 = in.substr(index + sep.size());}}intmain(){
cerr <<"========================cgi begin==================="<< endl;//用cerr测试,亦谓cout已经被我们替换成管道了!
string query_string;GetQueryString(query_string);// a=100&b=200// a,100,b,200//数据分析
string str1, str2;
string name1, value1;
string name2, value2;CutString(query_string,"&", str1, str2);CutString(str1,"=", name1, value1);CutString(str2,"=", name2, value2);//cout已经被重定向了,往fd1输出,实际上是往input[1]输出,httpserver用input[0]接收,再调用send,即可返回给浏览器;
cout << name1 <<" : "<< value1 << endl;
cout << name2 <<" : "<< value2 << endl;// cerr本地调试查看
cerr << name1 <<" : "<< value1 << endl;
cerr << name2 <<" : "<< value2 << endl;
cerr <<"========================cgi end==================="<< endl;return0;}
http_server的父进程添加下列从子进程cgi读取数据的代码
char c;while(read(input[0],&c,1)>0){
response_body += c;//读取的数据构建,响应报文,随后可以send}int status =0;
pid_t ret =waitpid(pid,&status,0);if(ret == pid){//等待有可能失败,得再做判断;if(WIFEXITED(status)){if(WEXITSTATUS(status)==0){
code = OK;}else{//正常退出,结果不正确
code =404;}}else{//不正确退出
code =404;}}
数据解析测试:
C端:
S端:
cgi技术总结
下面这张图详细的解释了我们这个http_server所引用的cgi技术
可以看到:
子CGI程序的标准输入是浏览器!
子CGI程序的标准输出也是是浏览器!
HTTP搭建了所有的通信细节
cgi程序可以用任何高级语言编写,以上http_server与cgi技术的设计高度解耦,是众多http_server都会使用的机制,众多与前端交互的高级语言,web开发的高级语言,如php,java,底层都引用了cgi技术;
也就意味着我们永远开发的是cgi程序,中间http_server的固定模式不用管,简化了我们开发只需要关心cgi程序,进行数据处理,不用再关心通信细节了(由HTTP完成);
(什么cookie session都能通过环境变量等传递给cgi… 进一步处理)
错误处理
- 逻辑错误(读取完毕了,需要给对方回应)-分析的时候出错eg请求资源不存在或者管道创建失败
- 读取错误(读取不一定完毕,读取的时候出错->不给对方回应->退出即可)-读取的时候出错eg读的时候浏览器sock断开
- 写入错误(send给c端的过程中,c端断开退出了,继续写就没意义了)
处理逻辑错误
请求出错,我们记录错误码,goto end:执行BuildHttpResponseHelper;
不管是cgi还是非cgi,其中有错误我们也记录错误码,进入BuildHttpResponseHelper;
这样在构建响应的时候,如果状态码不对,也能根据相应的状态码构建对应的返回网页,最后send回浏览器;
#defineOK200#defineNOT_FOUND404#defineBAD_REQUEST400#defineSERVER_ERROR500voidHandlerError(string page){
http_request.cgi =false;//只要出错,我们就cgi = false,最后send正常的静态错误网页//返回404.html
http_response.fd =open(page.c_str(), O_RDONLY);if(http_response.fd >0){structstat st;stat(page.c_str(),&st);
string line ="Cntent-Type: text/html";
line += LINE_END;
http_response.response_header.push_back(line);
line ="Cntent-Length: ";
line += std::to_string(st.st_size);
line += LINE_END;
http_response.response_header.push_back(line);
http_response.size = st.st_size;}}voidBuildOkResponse(){
string line ="Cntent-Type: ";
line +=Suffix2Desc(http_request.suffix);
line += LINE_END;
http_response.response_header.push_back(line);
line ="Content-Length: ";if(http_request.cgi){
line += std::to_string(http_response.response_body.size());// cgi程序 返回body}else{
line += std::to_string(http_response.size);// Noncgi 静态网页}
line += LINE_END;
http_response.response_header.push_back(line);}voidBuildHttpResponseHelper(){auto&status_code = http_response.status_code;//构建状态行auto&status_line = http_response.status_line;
status_line += HTTP_VERSION;
status_line +=" ";
status_line += std::to_string(status_code);
status_line +=" ";
status_line +=Code2Desc(status_code);
status_line += LINE_END;
string path = WEB_ROOT;//构建响应正文,可能包括headerswitch(status_code){case OK:BuildOkResponse();break;case NOT_FOUND:
path +='/';
path += PAGE_404;HandlerError(path);break;case BAD_REQUEST:
path +='/';
path += PAGE_404;HandlerError(path);break;case SERVER_ERROR:
path +='/';
path += PAGE_404;HandlerError(path);break;default:break;}}
浏览器请求不存在资源:
HTTP_SERVER返回404:
处理读取错误
添加stop停止标记;
在Recv的过程中如果read等方法出错,stop设置为true,最终stop如果还是false证明recv成功,再执行Build 和 Send;
处理写入错误
写入出现问题,c端关闭,他的管道也就都没了,系统会给server发送sigpipe信号中断挂掉server,这显然是不行的!
我们需要忽略他,简单粗暴的处理,保证server继续运行;
引入线程池
我们都知道原先的方法是,来一个sock扩建一个线程,这显然是不行的,如果海量请求来了,一直扩线程server是顶不住的,而且可可以利用这个特点不断的发送sock链接挂起导致http_server崩溃;\
这就要求软件硬件层面取平衡了,线程池是一个常常用来缓解这种情况的方式;
任务类,线程处理的task,我们将原先的Entrance改为CallBack,并且设置仿函数和回调函数,task类能直接回调执行sock处理!
Task.hpp
#pragmaonce#include<iostream>#include"Protocol.hpp"classTask{private:int sock;
CallBack handler;//设置回调public:Task(){}Task(int _sock):sock(_sock){}//处理任务voidProcessOn(){handler(sock);//调用callback类里面的仿函数 直接处理sock}~Task(){}};
ThreadPool.hpp
设计一个简易的:“线程池”
#pragmaonce#include"Task.hpp"#include<iostream>#include<pthread.h>#include<queue>#include"Log.hpp"using std::queue;#defineNUM6classThread_Pool{private:int num;
queue<Task> task_queue;bool stop;
pthread_mutex_t lock;
pthread_cond_t cond;static Thread_Pool *single_instance;Thread_Pool(int _num = NUM):num(_num),stop(false){pthread_mutex_init(&lock,nullptr);pthread_cond_init(&cond,nullptr);}Thread_Pool(const Thread_Pool &){}public:static Thread_Pool *getinstance()//单例{static pthread_mutex_t _mutex = PTHREAD_MUTEX_INITIALIZER;if(single_instance ==nullptr){pthread_mutex_lock(&_mutex);if(single_instance ==nullptr){
single_instance =newThread_Pool();
single_instance->InitThreadPool();}pthread_mutex_unlock(&_mutex);}return single_instance;}boolTaskQueueIsEmpty(){return task_queue.size()==0?true:false;}voidLock(){pthread_mutex_lock(&lock);}voidUnlock(){pthread_mutex_unlock(&lock);}boolIsStop(){return stop;}voidThreadWait(){pthread_cond_wait(&cond,&lock);}voidThreadWakeup(){pthread_cond_signal(&cond);}staticvoid*ThreadTRoutine(void*args){
Thread_Pool *tp =(Thread_Pool *)args;while(true){
Task t;
tp->Lock();while(tp->TaskQueueIsEmpty()){
tp->ThreadWait();//当我醒来一定占有互斥锁!}
tp->PopTask(t);
tp->Unlock();
t.ProcessOn();// CallBack回调处理,处理这个sock链接}}boolInitThreadPool(){for(int i =0; i < num; i++){
pthread_t tid;if(pthread_create(&tid,nullptr, ThreadTRoutine,this)!=0){LOG(FATAL,"create thread pool error");}}LOG(INFO,"create thread pool success");returntrue;}voidPushTask(const Task &task){Lock();
task_queue.push(task);Unlock();ThreadWakeup();}voidPopTask(Task &task){//上层调用pop加过锁了
task = task_queue.front();
task_queue.pop();}~Thread_Pool(){pthread_mutex_destroy(&lock);pthread_cond_destroy(&cond);}};
Thread_Pool *Thread_Pool::single_instance =nullptr;
提交表单测试
修改后的index.html如下:
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>TEST SUBMIT</title>
</head>
<body>
<form action = "/test_cgi" method="GET">
x:<input type = "text" name = "data_x"><br>
y:<input type = "text" name = "data_y"><br><br>
<input type = "submit" value = "提交运算">
</form>
</body>
</html>
表单里的action是提交路径,method是提交方法(我们用GET or POST);
测试结果:
提交前:
点击提交后:
可以看到,提交按钮将我们输入的数据x:100,y:200 上传到了路径test_cgi中;
本质上是浏览器又向我们HTTP_SERVER发送了请求报头为 GET /test_cgi?data_x=100&data_y=200 HTTP/1.0 的请求,之后cgi处理完数据将结果返回给浏览器 显示处理结果;
当<from>中的method ="POST"时,提交如下:
由于我们表单采用的是GET方法,所以直接在浏览器的请求uri中就能看到提交的数据;
如果是POST方法,那么就会有更好的私密性,提交的数据会在request.body中传递给HTTP_SERVER;
cgi返回网页
显然我们正常业务逻辑下HTTP_SERVER不可能只返回数据给C端,我们需要进行前端操作将数据处理以后嵌入网页返回给C端;(C++写这玩意有点麻烦,我们可以用javaweb php等写cgi程序,cgi程序支持所有语言的可执行程序,根据需求来)
test_cgi
#include<iostream>#include<cstdlib>#include<unistd.h>usingnamespace std;boolGetQueryString(string &query_string){bool result =false;
string method =getenv("METHOD");
cerr <<"METHOD = "<< method << endl;if(method =="GET"){
query_string =getenv("QUERY_STRING");
result =true;}elseif(method =="POST"){
cerr <<"Content-length = "<<getenv("CONTENT_LENGTH")<< endl;int count_length =atoi(getenv("CONTENT_LENGTH"));while(count_length--){char c;read(0,&c,1);
query_string += c;}
result =true;}else{
result =false;}return result;}voidCutString(string &in,const string &sep, string &out1, string &out2){int index;if((index = in.find(sep))!= string::npos){
out1 = in.substr(0, index);
out2 = in.substr(index + sep.size());}}intmain(){
cerr <<"========================cgi begin==================="<< endl;//用cerr测试,亦谓cout已经被我们替换成管道了!
string query_string;GetQueryString(query_string);// a=100&b=200// a,100,b,200//数据分析
string str1, str2;
string name1, value1;
string name2, value2;CutString(query_string,"&", str1, str2);CutString(str1,"=", name1, value1);CutString(str2,"=", name2, value2);int x =atoi(value1.c_str());int y =atoi(value2.c_str());//可能向进行某种计算(计算,搜索,登陆等),想进行某种存储(注册)
cout <<"<html>";
cout <<"<head><meta charset=\"utf-8\"></head>";
cout <<"<body>";//往fd1输出,到httpserver了
cout << name1 <<" : "<< value1 << endl;
cout << name2 <<" : "<< value2 << endl;
cout <<"<h3> "<< value1 <<" + "<< value2 <<" = "<< x + y <<"</h3>";
cout <<"<h3> "<< value1 <<" - "<< value2 <<" = "<< x - y <<"</h3>";
cout <<"<h3> "<< value1 <<" * "<< value2 <<" = "<< x * y <<"</h3>";
cout <<"<h3> "<< value1 <<" / "<< value2 <<" = "<< x / y <<"</h3>";//假设/0错误,cgi崩溃,父进程wait到的车状态就会异常,直接就错误处理返回静态错误网页了 不需要担心;
cout <<"</body>";
cout <<"</html>";
cerr <<"========================cgi end==================="<< endl;return0;}
运行结果:
提交前:
提交后:(GET方法)
表单总结
通过上述提交表单操作,我们能看出:
- GET通过uri传参,from提交的时候,会将参数自动拼接request的到请求uri中;
- POST通过正文传参,参数再request.body中;
GET因为通过uri传参,我们HTTP_SERVER内部对于get传参的方式优化为环境变量传参;但url长度是有限制的,所以GET方法的参数在某种程度上来说是短的,有限制的;
POST是通过request.body传参,底层通过管道,子进程cgi程序读取参数,所以可以参数很长,基本上不受限制;
补充数据库
数据是网络中的石油,实际业务场景中,需要存储数据日后查询使用的场景也很多,我们在此http_server的基础上引入一个简单地数据库,模拟一下用户注册用户名和密码时,后台连接数据库处理的流程!
需要下载安装好C链接mysql的套件;
创建存账户信息的数据库:
comm.hpp
编写完发现GetQueryString()和CutString()不论是普通cgi还是mysqlcgi都需要用到的处理数据的工具函数,我们把他俩单独封装入comm.hpp头文件中
#pragmaonce#include<iostream>#include<cstdlib>#include<unistd.h>usingnamespace std;boolGetQueryString(string &query_string){bool result =false;
string method =getenv("METHOD");
cerr <<"METHOD = "<< method << endl;if(method =="GET"){
query_string =getenv("QUERY_STRING");
result =true;}elseif(method =="POST"){
cerr <<"Content-length = "<<getenv("CONTENT_LENGTH")<< endl;int count_length =atoi(getenv("CONTENT_LENGTH"));while(count_length--){char c;read(0,&c,1);
query_string += c;}
result =true;}else{
result =false;}return result;}voidCutString(string &in,const string &sep, string &out1, string &out2){int index;if((index = in.find(sep))!= string::npos){
out1 = in.substr(0, index);
out2 = in.substr(index + sep.size());}}
mysql_conn.cc
#include"comm.hpp"#include"mysql.h"boolInsertSql(string sql){
MYSQL *conn =mysql_init(nullptr);//创建mysql句柄mysql_set_character_set(conn,"utf8");//程序和mysql通信的时候 采用utf-8 防止乱码//链接mysqlif(nullptr==mysql_real_connect(conn,"127.0.0.1","http_test","12345678","http_test",3306,nullptr,0)){
cerr <<"connect mysql error!"<< endl;return1;}
cerr <<"connect mysql success!"<< endl;
cerr <<"query : "<< sql << endl;int ret =mysql_query(conn, sql.c_str());//向mysql下发命令
cerr <<"ret : "<< ret << endl;mysql_close(conn);returntrue;}intmain(){
string query_string;if(GetQueryString(query_string))//从HTTP_SERVER获取参数{
cerr <<"query_string : "<< query_string.c_str()<< endl;//参数处理;类似于test_cgi的处理数据逻辑;// name=xxx&passwd=xxx
string name;
string passwd;CutString(query_string,"&", name, passwd);//参数进一步拆分
string _name;
string sql_name;CutString(name,"=", _name, sql_name);
string _passwd;
string sql_passwd;CutString(passwd,"=", _passwd, sql_passwd);//构建sql语句
string sql ="insert into user(name,passwd) values(\'";
sql +=(sql_name +"\',");
sql +=(sql_passwd +")");// sql语句构建号以后,插入数据库; 返回一个简单地提示网页!if(InsertSql(sql)){
cout <<"<html>";
cout <<"<head><meta charset=\"utf-8\"></head>";
cout <<"<body><h1>注册成功!信息已经插入后台数据库!</h1></body>";}}return0;}
模拟注册运行展示
浏览器请求http_server,并填写账户信息准备提交注册:
http_server中的sql_conn程序执行结果:
http_server返回的网页给浏览器:
查看mysql中刚注册的账户信息:
项目源代码链接
Gitee仓库
项目总结
聚焦于处理HTTP的请求和构建对应响应; 我们主要研究基于 HTTP/1.0 短连接 的GET和POST方法;
获得请求,分析请求,错误处理等; 制定特定的网页src用于返回; 引入简单的日志系统
搭建CGI机制;
父子管道,设计dup2重定向,环境变量传参等
引入线程池;
采用多线程技术,缓解内存开销;
引入数据库;
链接mysql数据库,可以设计更多样的具体应用;
项目扩展方向
技术层面扩展
- 使用epoll机制(我们用的多线程只是用中小型业务)
- redis;
- 请求转发服务器(代理功能,梯子)
应用层面扩展
- 在线博客(制定对应的格式text和前端功能,建立对应数据库,实现博客的上传查询与修改)
- 在线画图板(返回一个在线画图板网页,用户画完,存入指定路径path,path插入对应数据库用于下次查看)
- 在线音视频播放(已经支持了)
- 在线网络计算器(我们已经实现了建议的±*/)
版权归原作者 谜一样的男人1 所有, 如有侵权,请联系我们删除。