自主Web服务器Http_httpserver工具类

自主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中的sockerbindlisten进行了封装用Init启动同时设计了单例模式一个HttpServer只需要一个监听listen_sock即可

#pragma once

#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;
#define BACKLOG 5
enum ERR
{
    SOCK_ERR = 1,
    BIND_ERR,
    LISTEN_ERR,
    USAGE
};
class TcpServer
{
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 = new TcpServer(port);
                svr -> InitServer();//getinstance的时候就搞定了sock bind listen了;
            }
            pthread_mutex_unlock(&lock);
        }
        return svr;
    }

public:
    void InitServer()
    {
        Socket();
        Bind();
        Listen();
         
        LOG(INFO, "TcpServer begin");//日志
    }
    void Socket()
    {
        listen_sock = socket(AF_INET, SOCK_STREAM, 0);
        if (listen_sock < 0)
        {
            LOG(FATAL, "socket error");
            exit(SOCK_ERR);
        }
        //防止bind error
        int opt = 1;
        setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    }
    void Bind()
    {
        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);
        }
    }
    void Listen()
    {
        if (listen(listen_sock, BACKLOG) < 0)
        {
            LOG(FATAL, "listen error");
            exit(LISTEN_ERR);
        }
    }
    int Sock()
    {
        return listen_sock;
    }
    ~TcpServer()
    {
        if (listen_sock > 0)
            close(listen_sock);
    }
};
//单例
TcpServer *TcpServer::svr = nullptr;

HttpServer.hpp

#pragma once

#include <iostream>
#include <signal.h>
#include <pthread.h>
#include "Log.hpp"
#include "TcpServer.hpp"
#include "Protocol.hpp"

#define PORT 8080//默认端口号

class HttpServer
{
private:
    int port;
    bool stop;

public:
    HttpServer(int _port = PORT) : port(_port), stop(false)
    {
    }
    void InitServer()
    {
        // singal(SIGPIPE,SIG_IGN);
    }
    void Loop()//循环监听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 = new int(sock);//注意局部变量的传参
            pthread_create(&tid,nullptr,Entrance::HandlerRequest,psock);
            pthread_detach(tid);
        }
    }
    ~HttpServer() {}
};

Log.hpp

建议的日志系统

#pragma once

#include <iostream>
#include <string>
#include <ctime>

//日志处理
#define INFO    
#define WARNING  
#define ERROR   
#define FATAL   

#define LOG(level, message) Log(#level, message, __FILE__, __LINE__)//替换下列函数的宏方便日志的传参

void Log(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报文。构建响应等;

#pragma once

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
using std::cout;
using std::endl;
class Entrance//临时方案
{
public:
    //loop创建的线程执行任务的函数
    static void *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;

        return nullptr;
    }
};

运行结果
在这里插入图片描述

前三行是打印的日志信息后面是c端浏览器访问我们server的时候发送的报文我们将它打印出来了;

解析C端发来的HTTP报文

在这里插入图片描述

可见报文都是一行一行的我们需要按行读取,先来个按行读取的工具

MSG_PEEK标志位

recv(sock, &c, 1, MSG_PEEK);

我们一般是设置为0如果设置MSG_PEEK标志位则仅仅是把tcp缓冲区中的数据拷贝式的读取到buf中并没有把已读取的数据从tcp缓冲区中移除相当于peek窥探一下; 这样我们就可以处理的同时防止破坏下个报文的报头造成数据报文不完整了;

Util.hpp

工具类Util

#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
using std::string;
//工具类
class Util
{
public:
    static int ReadLine(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;
            }
            else if (s == 0)
            {
                return 0;
            }
            else
            { 
                return -1;
            }
        }

        return out.size();
    }
};

用Entrance收到报文测试然后调用按行读取一次结果如下(调用一次读取一行即便请求行)

在这里插入图片描述

构建请求与响应类

Protocol.hpp

//请求类
class HttpRequest
{
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() {}
};

//响应类
class HttpResponse
{
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通信
class EndPoint
{
private:
    int sock;
    HttpRequest http_request;
    HttpResponse http_response;
    bool stop;

public:
    EndPoint(int _sock) : sock(_sock), stop(false)
    {
    }

public:
    bool RecvHttpRequestLine() //读取请求行
    {
        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;
    }
    bool RecvHttpRequestHeader() //读取请求报头 去掉多余的\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;
    }
};

bool IsNeedRecvHttpRequestBody()//判断需不需要读 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的size
                return true;
            }

            return true;
        }
     }
     bool RecvHttpRequestBody()
     {
        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;
     
        }
     }
 bool IsNeedRecvHttpRequestBody() //判断需不需要读 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的size

                return true;
            }

            return false;
        }
        return false;
    }
    bool RecvHttpRequestBody()
    {
        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报文的对应属性和内容存入特定的请求类中;用于后续构建响应直接对照构建;

  • 请求行的三个属性提取出来分别放入methoduriversion
  • 请求报头数组中的一个个k:v分别提出来进行unordered_map的映射{k,v},方便后续直接查询

Util.hpp添加一个工具函数

  static bool CutString(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实现了通用
            return true;
        }
        return false;
     }

stringstream类用法

 void ParseHttpRequestLine() //解析请求行入methoduriversion
    {
        // 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;
    }
    void ParseHttpRequestHeader() //解析请求报头,入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>
int stat(const char *path, struct stat *buf);//Linux获取文件信息的系统接口
//参数1文件路径
//参数2stat st;&st    将特定目录下文件的信息保存在st中;
//返回值成功返回0失败返回-1

在这里插入图片描述

其中st_mode

在这里插入图片描述

static string Code2Desc(int code)//状态码->状态描述
{
    std::string desc;
    switch (code)
    {
    case 200:
        desc = "OK";
        break;
    case 404:
        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
}

void BuildHttpResponse()
    {
        struct stat st;
        int size;
        ssize_t rfound;
        string _path; // temp
        auto &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;
            }
        }
        else if (method == "POST") // cgi
        {
            http_request.path = http_request.uri;
            http_request.cgi = true;
        }
        else
        {
            // DO Noting
        }

        //请求路径 我们上层得套wwwrootindex.html等默认
        _path = http_request.path;
        http_request.path = WEB_ROOT;
        http_request.path += _path;

        //如果路径末尾为'/' 意味着是个目录我们需要套上index.html
        if (http_request.path.find('/') == http_request.path.size() - 1)
        {
            http_request.path += HOME_PAGE;
        }

        //判断文件存在存在属性保存进st

        if (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(); //状态行填充了响应报头也有了 空行也有了正文有了
    }

int ProcessNonCgi(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;
        }
        return 404;
    }

发送响应

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之间传输的字节数。

在这里插入图片描述

 void SendHttpResponset()
    {

        //发状态行
        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);
        }
        //发\n
        send(sock, "\n", 1, 0);
        //发body

        sendfile(sock, http_response.fd, nullptr, http_response.size);

        close(http_response.fd);
    }

运行效果:

在这里插入图片描述

上面是我们调用非Cgi技术返回本地静态网页的过程这显然是不够的有时候c端请求会带参数需要我们server端处理这时候就需要引入Cgi技术了;

Cgi技术

简介CGICommon Gateway Interface公共网关接口是外部扩展应用程序与 Web 服务器交互的一个标准接口。它可以使外部程序处理www上客户端送来的表单数据并对此作出反应通过某些特定的方式处理数据返回给Web服务器进而返回给c端;

在这里插入图片描述

虽然我们是创建新线程执行每个c端请求的但由于我们http_server的进程只有一个想要到特定位置执行cgi程序此处不能直接exec替换掉当前进程否侧httpserver直接没了;

那么就需要创建子进程进行一系列替换操作了;为了实现数据的交互我们需要同时引入进程间通信由于是父子之间那就匿名管道(因为管道是单向通信我们要双向通信所以搞两个管道)

在这里插入图片描述

可我们打开两个管道后父子进程可以看到没错当子进程进行exec程序替换只替换代码和数据之后这两个匿名管道是数据没了管道还是存在的虽然还是存在着的但是替换的程序看不到的因为相当于一个全新的进程开始运行他的文件描述符数组只有初始的012号fd;34号这两个打开的管道被藏起来了那怎么处理呢
在这里插入图片描述

采用如下设计(一种约定)

我们采用dup2把01号标准fd重定向成当前的两个管道34;之后再exec替换exec替换的程序里里是有01标准输入输出的但是他其实已经被替换成两个管道了用01就可以完成server与cgi.exe的交互了;

cgi程序获取数据

  • 当c端GET方法发送数据时一般比较短我们直接利用环境变量导入可以让cgi程序拿到;
  • 当c端POST方法发送数据时我们直接通过管道写入cgi;
  • 当然至于是GET还是POST方法我们需要导入一个METHOD方法环境变量让cgi程序可以识别
int ProcessCgi()
    {
        auto &bin = http_request.path;                 // cgi.exe的位置,子进程exec它
        auto &method = http_request.method;            // GET OR POST
        auto &body = http_request.request_body;        // POST 多 直接write到child
        auto &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!");
            return 404;
        }
        if (pipe(output) < 0)
        {
            LOG(ERROR, "pipe output error!");
            return 404;
        }

        //创建子进程进行cgi
        pid_t pid = fork();
        if (pid == 0)
        { // child
            close(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替换程序直接对01进行读取与写入实际上就是与http_server的读取和写入

            execl(bin.c_str(), bin.c_str(), nullptr);

            exit(1); // execl失败
        }
        else if (pid < 0)
        { // error;
            return 404;
            LOG(ERROR, "fork error!");
        }
        else
        {                     // parent
            close(input[1]);  //父从cgi读关掉写
            close(output[0]); //夫给cgi写关掉读
		   //post方法传的参数多父进程直接cgi给exec程序
            if (method == "POST")
            {
                const char *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>

using namespace std;

int main()
{
    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;
    }
    else if (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;
    return 0;
}

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,进而返回给sockc端链接

test_cgi.cc

#include <iostream>
#include <cstdlib>
#include <unistd.h>

using namespace std;
bool GetQueryString(string &query_string)
{
    bool result = false;
    string method = getenv("METHOD");
    cerr << "METHOD = " << method << endl;

    if (method == "GET")
    {

        query_string = getenv("QUERY_STRING");

        result = true;
    }
    else if (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;
}
void CutString(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());
    }
}
int main()
{
    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;
    return 0;
}

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开发的高级语言如phpjava底层都引用了cgi技术;

也就意味着我们永远开发的是cgi程序中间http_server的固定模式不用管简化了我们开发只需要关心cgi程序,进行数据处理不用再关心通信细节了(由HTTP完成);

什么cookie session都能通过环境变量等传递给cgi… 进一步处理

错误处理

  • 逻辑错误(读取完毕了需要给对方回应)-分析的时候出错eg请求资源不存在或者管道创建失败
  • 读取错误(读取不一定完毕读取的时候出错->不给对方回应->退出即可)-读取的时候出错eg读的时候浏览器sock断开
  • 写入错误send给c端的过程中c端断开退出了继续写就没意义了

处理逻辑错误

请求出错我们记录错误码goto end:执行BuildHttpResponseHelper;

不管是cgi还是非cgi其中有错误我们也记录错误码进入BuildHttpResponseHelper;

这样在构建响应的时候如果状态码不对也能根据相应的状态码构建对应的返回网页最后send回浏览器;
在这里插入图片描述

#define OK 200
#define NOT_FOUND 404
#define BAD_REQUEST 400
#define SERVER_ERROR 500

void HandlerError(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)
        {
            struct stat 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;
        }
    }
    void BuildOkResponse()
    {
        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);
    }

void BuildHttpResponseHelper()
    {
        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;

        //构建响应正文可能包括header
        switch (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

#pragma once

#include <iostream>
#include "Protocol.hpp"
class Task
{
private:
    int sock;
    CallBack handler; //设置回调
public:
    Task() {}
    Task(int _sock) : sock(_sock)
    {
    }
    //处理任务
    void ProcessOn()
    {
        handler(sock); //调用callback类里面的仿函数 直接处理sock
    }
    ~Task() {}
};

ThreadPool.hpp

设计一个简易的“线程池”

#pragma once
#include "Task.hpp"
#include <iostream>
#include <pthread.h>
#include <queue>
#include "Log.hpp"
using std::queue;

#define NUM 6

class Thread_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 = new Thread_Pool();
                single_instance->InitThreadPool();
            }
            pthread_mutex_unlock(&_mutex);
        }
        return single_instance;
    }

    bool TaskQueueIsEmpty()
    {
        return task_queue.size()==0?true:false;
    }
    void Lock()
    {
        pthread_mutex_lock(&lock);
    }

    void Unlock()
    {
        pthread_mutex_unlock(&lock);
    }
    bool IsStop()
    {
        return stop;
    }
    void ThreadWait()
    {
        pthread_cond_wait(&cond, &lock);
    }
    void ThreadWakeup()
    {
        pthread_cond_signal(&cond);
    }
    static void *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链接
        }
    }
    bool InitThreadPool()
    {
        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");
        return true;
    }

    void PushTask(const Task &task)
    {
        Lock();
        task_queue.push(task);
        Unlock();
        ThreadWakeup();
    }
    void PopTask(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>

using namespace std;
bool GetQueryString(string &query_string)
{
    bool result = false;
    string method = getenv("METHOD");
    cerr << "METHOD = " << method << endl;

    if (method == "GET")
    {

        query_string = getenv("QUERY_STRING");

        result = true;
    }
    else if (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;
}
void CutString(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());
    }
}
int main()
{
    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;
    return 0;
}

运行结果:

提交前:

在这里插入图片描述

提交后:(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头文件中

#pragma once

#include <iostream>
#include <cstdlib>
#include <unistd.h>

using namespace std;
bool GetQueryString(string &query_string)
{
    bool result = false;
    string method = getenv("METHOD");
    cerr << "METHOD = " << method << endl;

    if (method == "GET")
    {

        query_string = getenv("QUERY_STRING");

        result = true;
    }
    else if (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;
}
void CutString(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"

bool InsertSql(string sql)
{
    MYSQL *conn = mysql_init(nullptr);     //创建mysql句柄
    mysql_set_character_set(conn, "utf8"); //程序和mysql通信的时候 采用utf-8 防止乱码
    //链接mysql
    if (nullptr == mysql_real_connect(conn, "127.0.0.1", "http_test", "12345678", "http_test", 3306, nullptr, 0))
    {
        cerr << "connect mysql error!" << endl;
        return 1;
    }

    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);
    return true;
}
int main()
{
    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>";
        }
    }

    return 0;
}

模拟注册运行展示

浏览器请求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. 在线画图板返回一个在线画图板网页用户画完存入指定路径pathpath插入对应数据库用于下次查看
  3. 在线音视频播放已经支持了
  4. 在线网络计算器我们已经实现了建议的±*/
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: 服务器