自主HttpServer实现(C++实战项目)

Posted 林慢慢脑瓜子嗡嗡的

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自主HttpServer实现(C++实战项目)相关的知识,希望对你有一定的参考价值。

文章目录

项目介绍

该项目是一个基于Http和Tcp协议自主实现的WebServer,用于实现服务器对客户端发送过来的GET和POST请求的接收、解析、处理,并返回处理结果给到客户端。该项目主要背景知识涉及C++、网络分层协议栈、HTTP协议、网络套接字编程、CGI技术、单例模式、多线程编程、线程池等。

项目源码Click

CGI技术

CGI技术可能大家比较陌生,单拎出来提下。

概念

CGI(通用网关接口,Common Gateway Interface)是一种用于在Web服务器上执行程序并生成动态Web内容的技术。CGI程序可以是任何可执行程序,通常是脚本语言,例如Perl或Python。

CGI技术允许Web服务器通过将Web请求传递给CGI程序来执行任意可执行文件。CGI程序接收HTTP请求,并生成HTTP响应以返回给Web服务器,最终返回给Web浏览器。这使得Web服务器能够动态地生成网页内容,与静态html文件不同。CGI程序可以处理表单数据、数据库查询和其他任务,从而实现动态Web内容。一些常见的用途包括创建动态网页、在线购物车、用户注册、论坛、网上投票等。

原理

通过Web服务器将Web请求传递给CGI程序,CGI程序处理请求并生成响应,然后将响应传递回Web服务器,最终返回给客户端浏览器。这个过程可以概括为:

  1. 客户端发送HTTP请求到Web服务器。
  2. Web服务器检查请求类型,如果是CGI请求,Web服务器将环境变量和请求参数传递给CGI程序,并等待CGI程序的响应。
  3. CGI程序接收请求参数,并执行相应的操作,例如读取数据库或处理表单数据等。
  4. CGI程序生成HTTP响应,将响应返回给Web服务器。
  5. Web服务器将响应返回给客户端浏览器。

在这个过程中,Web服务器和CGI程序之间通过标准输入和标准输出(建立管道并重定向到标准输入输出)进行通信。Web服务器将请求参数通过环境变量传递给CGI程序,CGI程序将生成的响应通过标准输出返回给Web服务器。此外,CGI程序还可以通过其他方式与Web服务器进行通信,例如通过命令行参数或文件进行交互。

设计框架

日志文件

用于记录下服务器运行过程中产生的一些事件。日志格式如下:

日志级别说明:

  • INFO: 表示正常的日志输出,一切按预期运行。
  • WARNING: 表示警告,该事件不影响服务器运行,但存在风险。
  • ERROR: 表示发生了某种错误,但该事件不影响服务器继续运行。
  • FATAL: 表示发生了致命的错误,该事件将导致服务器停止运行。

文件名称和行数可以通过C语言中的预定义符号__FILE____LINE__,分别可以获取当前文件的名称和当前的行数。

#define INFO    1
#define WARNING 2
#define ERROR   3
#define FATAL   4

// #将宏参数level转为字符串格式
#define LOG(level, message) Log(#level, message, __FILE__, __LINE__) 

TCPServer

思路是:创建一个TCP服务器,并通过初始化、绑定和监听等步骤实现对外服务。

具体实现中,单例模式通过一个名为GetInstance的静态方法实现,该方法首先使用pthread_mutex_t保证线程安全,然后使用静态变量 _svr指向单例对象,如果 _svr为空,则创建一个新的TcpServer对象并初始化,最后返回 _svr指针。由于 _svr是static类型的,因此可以确保整个程序中只有一个TcpServer实例。

Socket方法用于创建一个监听套接字,Bind方法用于将端口号与IP地址绑定,Listen方法用于将监听套接字置于监听状态,等待客户端连接。Sock方法用于返回监听套接字的文件描述符。

#define BACKLOG 5

class TcpServer
    private:
        int _port; // 端口号
        int _listen_sock; // 监听套接字
        static TcpServer* _svr; // 指向单例对象的static指针
    private:
        TcpServer(int port)
            :_port(port)
            ,_listen_sock(-1)
        
        TcpServer(const TcpServer&) = delete;
        TcpServer* operator=(const TcpServer&) = delete;
    public:
        static TcpServer* GetInstance(int port)// 单例
        
            static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
            if (_svr == nullptr)
            
                pthread_mutex_lock(&mtx); 
                if (_svr == nullptr)// 为什么要两个if? 原因:当首个拿锁者完成了对象创建,之后的线程都不会通过第一个if了,而这期间阻塞的线程开始唤醒,它们则需要靠第二个if语句来避免再次创建对象。
                
                    _svr = new TcpServer(port);
                    _svr -> InitServer();
                
                pthread_mutex_unlock(&mtx);
            
            return _svr;
        
        void InitServer()
        
            Socket(); // 创建
            Bind();   // 绑定
            Listen(); // 监听
            LOG(INFO, "TcpServer Init Success");
        
        void Socket() // 创建监听套接字
        
            _listen_sock = socket(AF_INET, SOCK_STREAM, 0);
            if (_listen_sock < 0)
            
                LOG(FATAL, "socket error!");
                exit(1);
            
            int opt = 1;// 将 SO_REUSEADDR 设置为 1 将允许在端口上快速重启套接字
            setsockopt(_listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
            LOG(INFO, "creat listen_sock success");
        
        void Bind() // 绑定端口
        
            struct sockaddr_in local;
            memset(&local, 0, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_port = htons(_port);
            local.sin_addr.s_addr = INADDR_ANY;
            
            if (bind(_listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
            
                LOG(FATAL, "bind error");
                exit(2);
            
            LOG(INFO, "port bind listen_sock success");
        
        void Listen() // 监听
        
            if (listen(_listen_sock, BACKLOG) < 0) // 声明_listen_sock处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果接收到更多的连接请求就忽略
            
                LOG(FATAL, "listen error");
                exit(3);
            
            LOG(INFO, "listen listen_sock success");
        
        int Sock() // 获取监听套接字fd
        
            return _listen_sock;
        
        ~TcpServer()
        
            if (_listen_sock >= 0)
            
                close(_listen_sock);
            
        
;

// 单例对象指针初始化
TcpServer* TcpServer::_svr = nullptr;

任务类

// 任务类
class Task
    private:
        int _sock;          // 通信套接字
        CallBack _handler;  // 回调函数
    public:
        Task()
        
        ~Task()
        

        Task(int sock)     // accept建立连接成功产生的通信套接字sock
            :_sock(sock)
        
        
        // 执行任务
        void ProcessOn()
        
            _handler(_sock); //_handler对象的运算符()已经重装,直接调用重载的()
        
;

初始化与启动HttpServer

这部分包含一个初始化服务器的方法InitServer()和一个启动服务器的方法Loop()。其中InitServer()函数注册了一个信号处理函数,忽略SIGPIPE信号(避免写入崩溃)。而Loop()函数则通过调用TcpServer类的单例对象获取监听套接字,然后通过accept()函数等待客户端连接,每当有客户端连接进来,就创建一个线程来处理该客户端的请求,并把任务放入线程池中。这里的Task是一个简单的封装,它包含一个处理客户端请求的成员函数,该成员函数读取客户端请求,解析请求,然后调用CGI程序来执行请求,最后将响应发送给客户端。

#define PORT 8081

class HttpServer

private:
    int _port;// 端口号
public:
    HttpServer(int port)
        :_port(port)
    
    ~HttpServer()
    

    // 初始化服务器
    void InitServer()
    
        signal(SIGPIPE, SIG_IGN); // 直接粗暴处理cgi程序写入管道时崩溃的情况,忽略SIGPIPE信号,避免因为一个被关闭的socket连接而使整个进程终止
    

    // 启动服务器
    void Loop()
    
        LOG(INFO, "loop begin");
        TcpServer* tsvr = TcpServer::GetInstance(_port); // 获取TCP服务器单例对象
        int listen_sock = tsvr->Sock(); // 获取单例对象的监听套接字
        while(true)
        
            struct sockaddr_in peer;
            memset(&peer, 0, sizeof(peer));
            socklen_t len = sizeof(peer);
            int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);// 跟客户端建立连接
            if (sock < 0)
            
                continue;
            

            // 打印客户端信息
            std::string client_ip = inet_ntoa(peer.sin_addr);
            int client_port = ntohs(peer.sin_port);
            LOG(INFO, "get a new link:[" + client_ip + ":" + std::to_string(client_port) + "]");

            // 搞个线程池,代替下面简单的线程分离方案
            // 构建任务并放入任务队列
            Task task(sock);
            ThreadPool::GetInstance()->PushTask(task);
        
    
;

HTTP请求结构

将HTTP请求封装成一个类,这个类当中包括HTTP请求的内容、HTTP请求的解析结果以及是否需要使用CGI模式的标志位。后续处理请求时就可以定义一个HTTP请求类,读取到的HTTP请求的数据就存储在这个类当中,解析HTTP请求后得到的数据也存储在这个类当中。

class HttpRequest
    public:
        // Http请求内容
        std::string _request_line;                 // 请求行
        std::vector<std::string> _request_header;  // 请求报头
        std::string _blank;                        // 空行
        std::string _request_body;                 // 请求正文

        // 存放解析结果
        std::string _method;       // 请求方法                 
        std::string _uri;          // URI               
        std::string _version;      // 版本号
        std::unordered_map<std::string, std::string> _header_kv; // 请求报头的内容是以键值对的形式存在的,用hash保存
        int _content_length;       // 正文长度
        std::string _path;         // 请求资源的路径  
        std::string _query_string; // URI携带的参数

        // 是否使用CGI
        bool _cgi;
    public:
        HttpRequest()
            :_content_length(0)  // 默认请求正文长度为0
            ,_cgi(false)         // 默认不适用CGI模式
        
        ~HttpRequest()
        
;

HTTP响应结构

类似的,HTTP响应也封装成一个类,这个类当中包括HTTP响应的内容以及构建HTTP响应所需要的数据。构建响应需要使用的数据就存储在这个类当中,构建后得到的响应内容也存储在这个类当中。

class HttpResponse
    public:
        // Http响应内容
        std::string _status_line;                  // 状态行
        std::vector<std::string> _response_header; // 响应报头
        std::string _blank;                        // 空行
        std::string _response_body;                // 响应正文(如果CGI为true(即Get带_query_string或者Post),响应正文才存在)

        // 所需数据
        int _status_code;    // 状态码
        int _fd;             // 响应文件的fd
        int _size;           // 响应文件的大小
        std::string _suffix; // 响应文件的后缀
    public:
        HttpResponse()
            :_blank(LINE_END)  
            ,_status_code(OK)
            ,_fd(-1)
            ,_size(0)
        
        ~HttpResponse()
        
;

线程回调

该回调函数实际上是一个函数对象,其重载了圆括号运算符“()”。当该函数对象被调用时,会传入一个int类型的套接字描述符作为参数,代表与客户端建立的连接套接字。该函数对象内部通过创建一个EndPoint对象来处理该客户端发来的HTTP请求,包括读取请求、处理请求、构建响应和发送响应。处理完毕后,该连接套接字将被关闭,EndPoint对象也会被释放。

class CallBack
    public:
        CallBack()
        
        ~CallBack()
        
        // 重载运算符 ()
        void operator()(int sock)
        
            HandlerRequest(sock);
        
        void HandlerRequest(int sock)
        
            LOG(INFO, "HandlerRequest begin");
            EndPoint* ep = new EndPoint(sock);
            ep->RecvHttpRequest();          //读取请求
            if (!ep->IsStop())
            
                LOG(INFO, "RecvHttpRequest Success");
                ep->HandlerHttpRequest();  //处理请求
                ep->BulidHttpResponse();   //构建响应
                ep->SendHttpResponse();    //发送响应
                if (ep->IsStop())
                
                    LOG(WARNING, "SendHttpResponse Error, Stop Send HttpResponse");
                
            
            else 
            
                LOG(WARNING, "RecvHttpRequest Error, Stop handler Response");
            
            close(sock); //响应完毕,关闭与该客户端建立的套接字
            delete ep;
            LOG(INFO, "handler request end");
        
;

EndPoint类

EndPoint主体框架

EndPoint类中包含三个成员变量:

  • sock:表示与客户端进行通信的套接字。
  • http_request:表示客户端发来的HTTP请求。
  • http_response:表示将会发送给客户端的HTTP响应。
  • _stop:是否异常停止本次处理

EndPoint类中主要包含四个成员函数:

  • RecvHttpRequest:读取客户端发来的HTTP请求。
  • HandlerHttpRequest:处理客户端发来的HTTP请求。
  • BuildHttpResponse:构建将要发送给客户端的HTTP响应。
  • SendHttpResponse:发送HTTP响应给客户端。
//服务端EndPoint
class EndPoint
    private:
        int _sock;                   //通信的套接字
        HttpRequest _http_request;   //HTTP请求
        HttpResponse _http_response; //HTTP响应
    	bool _stop;                          //是否停止本次处理
    public:
        EndPoint(int sock)
            :_sock(sock)
        
        //读取请求
        void RecvHttpRequest();
        //处理请求
        void HandlerHttpRequest();
        //构建响应
        void BuildHttpResponse();
        //发送响应
        void SendHttpResponse();
        ~EndPoint()
        
;

读取HTTP请求

读取HTTP请求的同时可以对HTTP请求进行解析,这里我们分为五个步骤,分别是读取请求行、读取请求报头和空行、解析请求行、解析请求报头、读取请求正文。

// 读取请求:如果请求行和请求报头正常读取,那先解析请求行和请求报头,然后读取请求正文
void RecvHttpRequest()

    if (!RecvHttpRequestLine() && !RecvHttpRequestHeader())// 请求行与请求报头读取均正常读取
    
        ParseHttpRequestLine();
        ParseHttpRequestHeader();
        RecvHttpRequestBody();
    

处理HTTP请求

首先判断请求方法是否为GET或POST,如果不是则返回错误信息;然后判断请求是GET还是POST,设置对应的cgi、路径和查询字符串;接着拼接web根目录和请求资源路径,并判断路径是否以/结尾,如果是则拼接index.html;获取请求资源文件的属性信息,并根据属性信息判断是否需要使用CGI模式处理;获取请求资源文件的后缀,进行CGI或非CGI处理。

// 处理请求
void HandlerHttpRequest()

    auto& code = _http_response._status_code;

    //非法请求
    if (_http_request._method != "GET" && _http_request._method != "POST")
    
        LOG(WARNING, "method is not right");
        code = BAD_REQUEST;
        return;
    

    // 判断请求是get还是post,设置cgi,_path,_query_string
    if (_http_request._method == "GET")
    
        size_t pos = _http_request._uri.find('?');
        if (pos != std::string::npos)// uri中携带参数
        
            // 切割uri,得到客户端请求资源的路径和uri中携带的参数
            Util::CutString(_http_request._uri, _http_request._path, _http_request._query_string, "?");
            LOG(INFO, "GET方法分割路径和参数");
            _http_request._cgi = true;// 上传了参数,需要使用CGI模式
        
        else // uri中没有携带参数
        
            _http_request._path = _http_request._uri;// uri即是客户端请求资源的路径
        
    
    else if (_http_request._method == "POST")
    
        _http_request._path = _http_request._uri;// uri即是客户端请求资源的路径
        _http_request._cgi = true; // 上传了参数,需要使用CGI模式
    
    else 
    
        // 只是为了代码完整性
    

    // 为请求资源路径拼接web根目录
    std::string path = _http_request._path;
    _http_request._path = WEB_ROOT;
    _http_request._path += path;

    // 请求资源路径以/结尾,说明请求的是一个目录
    if (_http_request._path[_http_request._path.size() - 1] == '/')
    
        _http_request._path += HOME_PAGE; // 拼接上该目录下的index.html
    
    LOG(INFO, _http_request._path);

    //获取请求资源文件的属性信息
    struct stat st;
    if (stat(_http_request._path.c_str(), &st) == 0) // 属性信息获取成功,说明该资源存在
    
        if (S_ISDIR(st.st_mode)) // 该资源是一个目录
        
            _http_request._path += "/"; // 以/结尾的目录前面已经处理过了,这里处理不是以/结尾的目录情况,需要拼接/
            _http_request._path += HOME_PAGE; // 拼接上该目录下的index.html
            stat(_http_request._path.c_str(), &stC++实战项目机房预约管理系统

mongoose实现httpserver,client

mongoose实现httpserver,client

Go语言HTTPServer开发的六种实现

有哪些适合学生参与的 C++,网络编程方面的开源项目?

想成为 C++ 实战高手?3 天带你实现