0


自主Web服务器Http_Server

目录

自主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数据库,可以设计更多样的具体应用;

项目扩展方向

技术层面扩展

  1. 使用epoll机制(我们用的多线程只是用中小型业务)
  2. redis;
  3. 请求转发服务器(代理功能,梯子)

应用层面扩展

  1. 在线博客(制定对应的格式text和前端功能,建立对应数据库,实现博客的上传查询与修改)
  2. 在线画图板(返回一个在线画图板网页,用户画完,存入指定路径path,path插入对应数据库用于下次查看)
  3. 在线音视频播放(已经支持了)
  4. 在线网络计算器(我们已经实现了建议的±*/)
标签: 服务器 http 网络

本文转载自: https://blog.csdn.net/wtl666_6/article/details/127472921
版权归原作者 谜一样的男人1 所有, 如有侵权,请联系我们删除。

“自主Web服务器Http_Server”的评论:

还没有评论