当前位置: 首页 > news >正文

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

文章目录

  • 项目介绍
    • CGI技术
      • 概念
      • 原理
  • 设计框架
    • 日志文件
    • TCPServer
    • 任务类
    • 初始化与启动HttpServer
    • HTTP请求结构
    • HTTP响应结构
    • 线程回调
    • EndPoint类
      • EndPoint主体框架
      • 读取HTTP请求
      • 处理HTTP请求
        • CGI处理
        • 非CGI处理
    • 构建HTTP响应
    • 发送HTTP响应
    • 接入线程池
    • 简单测试
  • 项目扩展

项目介绍

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

项目源码:Click

image-20230311111150579

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服务器将响应返回给客户端浏览器。

image-20230311112450625

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

设计框架

日志文件

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

image-20230311113235121

日志级别说明:

  • 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 5class 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 8081class 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携带的参数// 是否使用CGIbool _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;             // 响应文件的fdint _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类

image-20230311134619091

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处理。

image-20230311134644961

// 处理请求
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_stringif (_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.htmlstat(_http_request._path.c_str(), &st); // 重新获取资源文件的属性信息}else if (st.st_mode&S_IXUSR||st.st_mode&S_IXGRP||st.st_mode&S_IXOTH) // 该资源是一个可执行程序{_http_request._cgi = true; //需要使用CGI模式}_http_response._size = st.st_size; //设置请求资源文件的大小}else // 属性信息获取失败,可以认为该资源不存在{LOG(WARNING, _http_request._path + "NOT_FOUND");code = NOT_FOUND;return;}// 获取请求资源文件的后缀size_t pos = _http_request._path.rfind('.');if (pos == std::string::npos){_http_response._suffix = ".html";}else{_http_response._suffix = _http_request._path.substr(pos);// 把'.'也带上}// 进行CGI或非CGI处理// CGI为true就三种情况,GET方法的uri带参(_query_string),或者POST方法,又或者请求的资源是一个可执行程序if (_http_request._cgi == true) {code = ProcessCgi(); // 以CGI的方式进行处理}else{code = ProcessNonCgi(); // 简单的网页返回,返回静态网页}
}

CGI处理

CGI处理时需要创建子进程进行进程程序替换,但是在创建子进程之前需要先创建两个匿名管道。这里站在父进程角度对这两个管道进行命名,父进程用于读取数据的管道叫做input,父进程用于写入数据的管道叫做output。

image-20230311132028564

创建匿名管道并创建子进程后,需要父子进程各自关闭两个管道对应的读写端:

  • 对于父进程来说,input管道是用来读数据的,因此父进程需要保留input[0]关闭input[1],而output管道是用来写数据的,因此父进程需要保留output[1]关闭output[0]。
  • 对于子进程来说,input管道是用来写数据的,因此子进程需要保留input[1]关闭input[0],而output管道是用来读数据的,因此子进程需要保留output[0]关闭output[1]。

此时父子进程之间的通信信道已经建立好了,但为了让替换后的CGI程序从标准输入读取数据等价于从管道读取数据,向标准输出写入数据等价于向管道写入数据,因此在子进程进行进程程序替换之前,还需要对子进程进行重定向。

假设子进程保留的input[1]和output[0]对应的文件描述符分别是3和4,那么子进程对应的文件描述符表的指向大致如下:
image-20230311132058745

现在我们要做的就是将子进程的标准输入重定向到output管道,将子进程的标准输出重定向到input管道,也就是让子进程的0号文件描述符指向output管道,让子进程的1号文件描述符指向input管道。

image-20230311132120757

此外,在子进程进行进程程序替换之前,还需要进行各种参数的传递:

  • 首先需要将请求方法通过putenv函数导入环境变量,以供CGI程序判断应该以哪种方式读取父进程传递过来的参数。
  • 如果请求方法为GET方法,则需要将URL中携带的参数通过导入环境变量的方式传递给CGI程序。
  • 如果请求方法为POST方法,则需要将请求正文的长度通过导入环境变量的方式传递给CGI程序,以供CGI程序判断应该从管道读取多少个参数。

此时子进程就可以进行进程程序替换了,而父进程需要做如下工作:

  • 如果请求方法为POST方法,则父进程需要将请求正文中的参数写入管道中,以供被替换后的CGI程序进行读取。
  • 然后父进程要做的就是不断调用read函数,从管道中读取CGI程序写入的处理结果,并将其保存到HTTP响应类的response_body当中。
  • 管道中的数据读取完毕后,父进程需要调用waitpid函数等待CGI程序退出,并关闭两个管道对应的文件描述符,防止文件描述符泄露。
// CGI = true,处理cgi
int ProcessCgi()
{int code = OK; // 要返回的状态码,默认设置为200auto& bin = _http_request._path;      // 需要执行的CGI程序auto& method = _http_request._method; // 请求方法//需要传递给CGI程序的参数auto& query_string = _http_request._query_string; // GETauto& request_body = _http_request._request_body; // POSTint content_length = _http_request._content_length;  // 请求正文的长度auto& response_body = _http_response._response_body; // CGI程序的处理结果放到响应正文当中// 1、创建两个匿名管道(管道命名站在父进程角度)// 在调用 pipe 函数创建管道成功后,pipefd[0] 用于读取数据,pipefd[1] 用于写入数据。// 1.1 创建从子进程到父进程的通信信道int input[2];if(pipe(input) < 0){ // 管道创建失败,pipe()返回-1LOG(ERROR, "pipe input error!");code = INTERNAL_SERVER_ERROR;return code;}// 1.2 创建从父进程到子进程的通信信道int output[2];if(pipe(output) < 0){ // 管道创建失败,pipe()返回-1LOG(ERROR, "pipe output error!");code = INTERNAL_SERVER_ERROR;return code;}//2、创建子进程pid_t pid = fork();if(pid == 0){ //child// 子进程关闭两个管道对应的读写端close(input[0]);close(output[1]);//将请求方法通过环境变量传参std::string method_env = "METHOD=";method_env += method;putenv((char*)method_env.c_str());if(method == "GET"){ //将query_string通过环境变量传参std::string query_env = "QUERY_STRING=";query_env += query_string;putenv((char*)query_env.c_str());LOG(INFO, "GET Method, Add Query_String env");}else if(method == "POST"){ //将正文长度通过环境变量传参std::string content_length_env = "CONTENT_LENGTH=";content_length_env += std::to_string(content_length);putenv((char*)content_length_env.c_str());LOG(INFO, "POST Method, Add Content_Length env");}else{//Do Nothing}//3、将子进程的标准输入输出进行重定向,子进程会继承了父进程的所有文件描述符dup2(output[0], 0); //标准输入重定向到管道的输入dup2(input[1], 1);  //标准输出重定向到管道的输出//4、将子进程替换为对应的CGI程序,代码、数据全部替换掉execl(bin.c_str(), bin.c_str(), nullptr);exit(1); // 替换失败则exit(1)}else if(pid < 0){ //创建子进程失败,则返回对应的错误码LOG(ERROR, "fork error!");code = INTERNAL_SERVER_ERROR;return code;}else{ //father//父进程关闭两个管道对应的读写端close(input[1]);close(output[0]);if(method == "POST") // 将正文中的参数通过管道传递给CGI程序{ const char* start = request_body.c_str();int total = 0;int size = 0;while(total < content_length && (size = write(output[1], start + total, request_body.size() - total)) > 0){total += size;}}// 读取CGI程序的处理结果char ch = 0;while(read(input[0], &ch, 1) > 0)// 不会一直读,当另一端关闭后会继续往下执行{response_body.push_back(ch);} // 等待子进程(CGI程序)退出// status 保存退出状态int status = 0;pid_t ret = waitpid(pid, &status, 0);if(ret == pid){if(WIFEXITED(status)){ // 子进程正常退出if(WEXITSTATUS(status) == 0){ // 子进程退出码结果正确LOG(INFO, "CGI program exits normally with correct results");code = OK;}else{LOG(INFO, "CGI program exits normally with incorrect results");code = BAD_REQUEST;}}else{LOG(INFO, "CGI program exits abnormally");code = INTERNAL_SERVER_ERROR;}}//关闭两个管道对应的文件描述符close(input[0]);close(output[1]);}return code; //返回状态码
}

非CGI处理

​ 非CGI处理时只需要将客户端请求的资源构建成HTTP响应发送给客户端即可,理论上这里要做的就是打开目标文件,将文件中的内容读取到HTTP响应类的response_body中,以供后续发送HTTP响应时进行发送即可,但这种做法还可以优化。

因为HTTP响应类的response_body属于用户层的缓冲区,而目标文件是存储在服务器的磁盘上的,按照这种方式需要先将文件内容读取到内核层缓冲区,再由操作系统将其拷贝到用户层缓冲区,发送响应正文的时候又需要先将其拷贝到内核层缓冲区,再由操作系统将其发送给对应的网卡进行发送。我们完全可以调用sendfile函数直接将磁盘当中的目标文件内容读取到内核,再由内核将其发送给对应的网卡进行发送

​ sendfile函数是一个系统调用函数,用于将一个文件描述符指向的文件内容直接发送给另一个文件描述符指向的套接字,从而实现了零拷贝(Zero Copy)技术。这种技术避免了数据在用户态和内核态之间的多次拷贝,从而提高了数据传输效率。

sendfile函数的使用场景通常是在Web服务器中,用于将静态文件直接发送给客户端浏览器,从而避免了将文件内容复制到用户空间的过程。在Linux系统中,sendfile函数的原型为:

#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

​ 其中,out_fd表示目标文件描述符,in_fd表示源文件描述符,offset表示源文件偏移量,count表示要发送的字节数。函数返回值表示成功发送的字节数,如果返回-1则表示出现了错误。

// CGI = false
int ProcessNonCgi()
{// 打开客户端请求的资源文件,以供后续发送_http_response._fd = open(_http_request._path.c_str(), O_RDONLY);if(_http_response._fd >= 0){ // 打开文件成功return OK;}return INTERNAL_SERVER_ERROR; // 打开文件失败
}

构建HTTP响应

构建 HTTP 响应报文,首先根据响应的状态码构建状态行(包含 HTTP 版本、状态码和状态码描述),然后根据状态码分别构建不同的响应报头和响应正文。如果状态码为 200 OK,则调用 BuildOkResponse() 函数构建成功的响应报头和响应正文;如果状态码为 404 NOT FOUND、400 BAD REQUEST 或 500 INTERNAL SERVER ERROR,则根据不同的状态码构建相应的错误响应报头和响应正文,并调用 HandlerError() 函数处理错误。

// 构建响应
void BulidHttpResponse()
{int code = _http_response._status_code;//构建状态行auto& status_line = _http_response._status_line;status_line += HTTP_VERSION;status_line += " ";status_line += std::to_string(code);status_line += " ";status_line += CodeToDesc(code); //根据状态码获取状态码描述status_line += LINE_END;//构建响应报头std::string path = WEB_ROOT;path += "/";switch(code){case OK:BuildOkResponse();break;case NOT_FOUND:path += PAGE_404;HandlerError(path);break;case BAD_REQUEST:path += PAGE_400;HandlerError(path);break;case INTERNAL_SERVER_ERROR:path += PAGE_500;HandlerError(path);break;default:break;}
}

发送HTTP响应

发送HTTP响应的步骤如下:

  • 调用send函数,依次发送状态行、响应报头和空行。
  • 发送响应正文时需要判断本次请求的处理方式,如果本次请求是以CGI方式成功处理的,那么待发送的响应正文是保存在HTTP响应类的response_body中的,此时调用send函数进行发送即可。
  • 如果本次请求是以非CGI方式处理或在处理过程中出错的,那么待发送的资源文件或错误页面文件对应的文件描述符是保存在HTTP响应类的fd中的,此时调用sendfile进行发送即可,发送后关闭对应的文件描述符。
// 发送响应
bool SendHttpResponse()
{//发送状态行if(send(_sock, _http_response._status_line.c_str(), _http_response._status_line.size(), 0) <= 0){_stop = true; //发送失败,设置_stop}//发送响应报头if(!_stop){for(auto& iter : _http_response._response_header){if(send(_sock, iter.c_str(), iter.size(), 0) <= 0){_stop = true; //发送失败,设置_stopbreak;}}}//发送空行if(!_stop){if(send(_sock, _http_response._blank.c_str(), _http_response._blank.size(), 0) <= 0){_stop = true; //发送失败,设置_stop}}//发送响应正文if(_http_request._cgi){if(!_stop){auto& response_body = _http_response._response_body;const char* start = response_body.c_str();size_t size = 0;size_t total = 0;while(total < response_body.size()&&(size = send(_sock, start + total, response_body.size() - total, 0)) > 0){total += size;}}}else{if(!_stop){// sendfile:这是一个系统调用,用于高效地从文件传输数据到套接字中。它避免了在内核空间和用户空间之间复制数据的需求,从而实现更快的数据传输。if(sendfile(_sock, _http_response._fd, nullptr, _http_response._size) <= 0){_stop = true; //发送失败,设置_stop}}//关闭请求的资源文件close(_http_response._fd);}return _stop;
}

接入线程池

当前多线程版服务器存在的问题:

  • 每当获取到新连接时,服务器主线程都会重新为该客户端创建为其提供服务的新线程,而当服务结束后又会将该新线程销毁,这样做不仅麻烦,而且效率低下。
  • 如果同时有大量的客户端连接请求,此时服务器就要为每一个客户端创建对应的服务线程,而计算机中的线程越多,CPU压力就越大,因为CPU要不断在这些线程之间来回切换。此外,一旦线程过多,每一个线程再次被调度的周期就变长了,而线程是为客户端提供服务的,线程被调度的周期变长,客户端也就迟迟得不到应答。

考虑接入线程池简单优化下(其实也可以直接上epoll)

  • 在服务器端预先创建一批线程和一个任务队列,每当获取到一个新连接时就将其封装成一个任务对象放到任务队列当中。
  • 线程池中的若干线程就不断从任务队列中获取任务进行处理,如果任务队列当中没有任务则线程进入休眠状态,当有新任务时再唤醒线程进行任务处理。
#define NUM 6//线程池
class ThreadPool{private:std::queue<Task> _task_queue; //任务队列int _num;                     //线程池中线程的个数pthread_mutex_t _mutex;       //互斥锁pthread_cond_t _cond;         //条件变量static ThreadPool* _inst;     //指向单例对象的static指针private://构造函数私有ThreadPool(int num = NUM):_num(num){//初始化互斥锁和条件变量pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond, nullptr);}// 删除拷贝构造函数(防拷贝)ThreadPool(const ThreadPool&)=delete;//判断任务队列是否为空bool IsEmpty(){return _task_queue.empty();}//任务队列加锁void LockQueue(){pthread_mutex_lock(&_mutex);}//任务队列解锁void UnLockQueue(){pthread_mutex_unlock(&_mutex);}//让线程在条件变量下进行等待void ThreadWait(){pthread_cond_wait(&_cond, &_mutex);}//唤醒在条件变量下等待的一个线程void ThreadWakeUp(){pthread_cond_signal(&_cond);}public://获取单例对象static ThreadPool* GetInstance(){static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; //定义静态的互斥锁//双检查加锁if(_inst == nullptr){pthread_mutex_lock(&mtx); //加锁if(_inst == nullptr){//创建单例线程池对象并初始化_inst = new ThreadPool();_inst->InitThreadPool();}pthread_mutex_unlock(&mtx); //解锁}return _inst; //返回单例对象}//线程的执行例程static void* ThreadRoutine(void* arg){pthread_detach(pthread_self()); //线程分离ThreadPool* tp = (ThreadPool*)arg;while(true){tp->LockQueue(); //加锁while(tp->IsEmpty()){//任务队列为空,线程进行waittp->ThreadWait();}Task task;tp->PopTask(task); //获取任务tp->UnLockQueue(); //解锁task.ProcessOn(); //处理任务}}//初始化线程池bool InitThreadPool(){//创建线程池中的若干线程pthread_t tid;for(int i = 0;i < _num;i++){if(pthread_create(&tid, nullptr, ThreadRoutine, this) != 0){LOG(FATAL, "create thread pool error!");return false;}}LOG(INFO, "create thread pool success");return true;}//将任务放入任务队列void PushTask(const Task& task){LockQueue();    //加锁_task_queue.push(task); //将任务推入任务队列UnLockQueue();  //解锁ThreadWakeUp(); //唤醒一个线程进行任务处理}//从任务队列中拿任务void PopTask(Task& task){//获取任务task = _task_queue.front();_task_queue.pop();}~ThreadPool(){//释放互斥锁和条件变量pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}
};
//单例对象指针初始化为nullptr
ThreadPool* ThreadPool::_inst = nullptr;

简单测试

默认页面测试:

image-20230311134104523

带query_string,CGI传参测试:

image-20230311134047088

项目扩展

当前项目的重点在于HTTP服务器后端的处理逻辑,主要完成的是GET和POST请求方法,以及CGI机制的搭建。还可以进行不少扩展,比如:

  • 当前项目编写的是HTTP1.0版本的服务器,每次连接都只会对一个请求进行处理,当服务器对客户端的请求处理完毕并收到客户端的应答后,就会直接断开连接。可以将其扩展为HTTP1.1版本,让服务器支持长连接,即通过一条连接可以对多个请求进行处理,避免重复建立连接(涉及连接管理)。
  • 当前项目虽然在后端接入了线程池,但是效果有限,可以将线程池换成epoll版本,让服务器的IO变得更高效。
  • 可以给当前的HTTP服务器新增代理功能,也就是可以替代客户端去访问某种服务,然后将访问结果再返回给客户端(比如课题中的数据备份、数据计算等等)。

相关文章:

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

文章目录项目介绍CGI技术概念原理设计框架日志文件TCPServer任务类初始化与启动HttpServerHTTP请求结构HTTP响应结构线程回调EndPoint类EndPoint主体框架读取HTTP请求处理HTTP请求CGI处理非CGI处理构建HTTP响应发送HTTP响应接入线程池简单测试项目扩展项目介绍 该项目是一个基…...

第26篇:Java数组API总结

目录 1、数组基本概念 2、Java如何声明数组 2.1中括号在数据类型之前 2.2 中括号在数据类型之后...

[C++] 信号

前言 信号与槽是QT的一个概念&#xff0c;原版C里并没有 使用 先声明一些类 Receiver负责接收信号&#xff0c;Emitter2则是负责发送 class Receiver : public ntl::Reflectible { public:void received(int num){std::cout << "received:" << num &…...

单片机——矩阵按键模块

主要目的 学会按键扫描 1.延时函数 延时函数部分详见链接: 单片机控制一盏灯的亮与灭程序解释 void delay (uint k) //定义延时函数{uint i,j;for(i<0;i<k;i){for(j0;j<113;j){;}}}这个程序里面的延时函数的目的是按键消抖。 2.按键扫描模块 这是本次实验的重点&a…...

Android学习之网络操作

网络操作 Android平台下的原生网络操作可以分为以下几步&#xff1a; 创建URL对象&#xff1b;通过URL对象获取HttpURLConnection对象&#xff1b;通过HttpURLConnection对象设置请求头键值对、网络连接超时时间等&#xff1b;通过HttpURLConnection对象的connect()方法建立网…...

Delphi XE开发android开发环境搭建

Delphi xe为使用Delphi作为开发工具的程序员,提供了开发APP的便捷工具,它的开发环境也是非常容易搭建,这里我简述一下Android的开发环境搭建,Delphi XE开发Android程序的开发环境需要三个软件支持:Java SE Development开发环境、Android SDK和Android Ndk开发环境。 1、安…...

flink入门-流处理

入门需要掌握&#xff1a;从入门demo理解、flink 系统架构&#xff08;看几个关键组件&#xff09;、安装、使用flink的命令跑jar包flink的webUI 界面的监控、常见错误、调优 一、入门demo&#xff1a;统计单词个数 0、单词txt 文本内容(words.txt)&#xff1a; hello world …...

【数据结构】单链表中,如何实现 将链表中所有结点的链接方向“原地”逆转

一.实现一个单链表&#xff08;无头单向不循环&#xff09; 我们首先实现一个无头单向不循环单链表。 写出基本的增删查改功能&#xff0c;以及其它的一些功能&#xff08;可忽略&#xff09;。 #include<stdio.h> #include<assert.h> #include<stdlib.h>…...

摘花生(简单DP)

Hello Kitty想摘点花生送给她喜欢的米老鼠。她来到一片有网格状道路的矩形花生地(如下图)&#xff0c;从西北角进去&#xff0c;东南角出来。地里每个道路的交叉点上都有种着一株花生苗&#xff0c;上面有若干颗花生&#xff0c;经过一株花生苗就能摘走该它上面所有的花生。Hel…...

2022济南大学acm新生赛题解

通过答题情况的难度系数&#xff1a; 签到&#xff1a;A 简单&#xff1a;BL 中等&#xff1a;D 困难&#xff1a;CM 极难&#xff1a;KNO A-和 算出n个数的和判断正负性即可&#xff01;&#xff01;&#xff01; 发现很多同学的代码错误&#xff1a;要么sum未赋初值&…...

策略模式教程

策略模式是一种行为型设计模式&#xff0c;它允许在运行时根据不同的情况选择不同的算法实现&#xff0c;从而使得算法可以独立于客户端而变化。本文将介绍策略模式的概念、应用场景、优点和缺点&#xff0c;并提供最佳的代码实践。本文的代码实现将使用Java语言&#xff0c;但…...

什么是刺猬理念

一、什么是刺猬理念刺猬理念是指把复杂的世界简化成单个有组织性的观点&#xff0c;一条基本原则或一个基本理念&#xff0c;发挥统帅和指导作用。核心是把事情简单化&#xff0c;把所有的挑战和进退维谷的局面压缩为简单的。二、刺猬理念的寓言故事狐狸是一种狡猾的动物&#…...

RPC通信相关

RPCRPC, 远程过程调用&#xff08;Remote Procedure Call&#xff0c;RPC&#xff09;是一个计算机通信协议&#xff0c;该协议允许运行于一台计算机的程序程调用另一台计算机的上的程序。通俗讲&#xff0c;RPC通过把网络通讯抽象为远程的过程调用&#xff0c;调用远程的过程就…...

Node.js + MongoDB 搭建博客 -- 登录页面

准备工作 安装Node.js安装express等相关库MongoDB数据库电脑系统&#xff1a;win11 功能分析 搭建一个简单的具有多人注册、登录、发表文章以及登出功能的博客。 设计目标 未登录&#xff1a;主页左侧导航栏显示home、login、register&#xff0c;右侧显示已发表的文章、发…...

互联网新理念,对于WEB 3.0 你怎么看?

WEB 3.0 这个名词走进大众视野已经有一段时间了&#xff0c;也曾在各个圈子里火热一时&#xff0c;至今各大互联网企业任旧在 WEB 3.0 上不断探索。但关于 WEB 3.0 是什么这个问题&#xff0c;其实大部分人都没有一个比较明确的认知&#xff0c;包括区块链和元宇宙等相关行业的…...

Git使用教程:最详细、最傻瓜、最浅显、真正手把手教

GITGIT版本控制版本控制的意义分布式图形化客户端环境搭建仓库的操作分支使用场景命令远程仓库操作生成公钥命令冲突忽略列表的配置时机配置方式版本回退练习&#xff1a;GIT 版本控制 把文件系统中的文件&#xff0c;按照修改的版本进行记录&#xff0c;进行管理的操作。 版…...

【面试题】Redis面试题汇总(无解答)

Redis为何这么快&#xff1f;缓存问题及解决入库和缓存策略问题及处理redis数据类型缓存过期删除策略内存淘汰机制Redis 回收进程如何工作的&#xff1f;Redis持久化RDB和AOFredis流式pipeline处理原生批命令 (mset, mget) 与 Pipeline 区别?Pipeline 有什么好处&#xff0c;为…...

RHCSA-用户和组管理和文件系统权限(3.11)

目录 用户&#xff08;UID&#xff09; 用户类别&#xff08;UID&#xff09;&#xff1a; 用户的增删改查&#xff1a; 修改用户密码&#xff1a; 查看用户是否存在&#xff1a; 组&#xff08;GID&#xff09; 组的增删改查&#xff1a; 设置组密码&#xff1a; 用户…...

RK3588平台开发系列讲解(同步与互斥篇)信号量介绍

平台内核版本安卓版本RK3588Linux 5.10Android 12文章目录 一、信号量介绍二、信号量API1、结构体2、API三、函数调用流程沉淀、分享、成长,让自己和他人都能有所收获!😄 📢上一章我们看了自旋锁的原理,本章我们一起学习下信号量的用法。 一、信号量介绍 和自旋锁一样,…...

One-YOLOv5 v1.2.0发布:支持分类、检测、实例分割

One-YOLOv5 v1.2.0正式发布。完整更新列表请查看链接&#xff1a;https://github.com/Oneflow-Inc/one-yolov5/releases/tag/v1.2.0&#xff0c;欢迎体验新版本&#xff0c;期待你的反馈。 1 新版本特性 1. 同步了Ultralytics YOLOv5的上游分支v7.0&#xff0c;同时支持分类、目…...

Zookeeper的Java API操作

Zookeeper的Java API操作一、先启动Zookeeper集群二、IDEA 环境搭建三、创建子节点四、获取子节点并监听节点变化五、判断 Znode 是否存在六、Watcher工作流程一、先启动Zookeeper集群 二、IDEA 环境搭建 1.创建一个Maven工程&#xff1a;ZookeeperProject 2.在pom.xml文件添…...

Web3:前端知识和后端知识基础

三.Web3:前端知识和后端知识基础 1.了解前端开发 2.了解JSP 3.了解JAVAWeb的三大组件 4.Servlet的使用 5.Filter的使用 6.了解thymeleaf 未更新 三.Web3:前端知识和后端知识基础 1.了解前端开发 ①前端架构 HTML超文本标记语言CSS层叠样式表JavaS...

调试射频TX和rx实验工程出现的问题与反思

1.今天用ADS仿真 发现 加上SMA 插损就到了4db&#xff0c;但是直接用传输线就在1db以内 这个问题我目前想到的排查思路是换成IPEX&#xff0c; 换成IPEX插损就变成2db 拿最新的7626去看 看到上面是SMA-3G 小针 还是结合参考的demo PCB来看 2.用射频的ipex测试LNA 发现校准…...

代码随想录刷题-数组总结篇

文章目录数组二分查找原理习题题目1思路和代码题目-2移除元素习题我的想法暴力解法双指针有序数组的平方习题暴力排序双指针长度最小的子数组习题暴力解法滑动窗口螺旋矩阵 II习题我的解法别人的解法总结数组 二分查找 本节对应代码随想录中&#xff1a;代码随想录-二分查找 …...

Qt读xml文件

QXmlStreamReaderQXmlStreamReader类通过简单的流式API为我们提供了一种快速的读取xml文件的方式。他比Qt自己使用的SAX解析方式还要快。所谓的流式读取即将一个xml文档读取成一系列标记的流&#xff0c;类似于SAX。而QXmlStreamReader类和SAX的主要区别就是解析这些标记的方式…...

Qt样式表

1>样式表介绍 样式表可通过 QApplication::setStyleSheet()函数将其设置到整个应用程序上&#xff0c;也可以使用 QWidget::setStyleSheet()将其设置到指定的部件或子部件上&#xff0c;不同级别均可设置样式表&#xff0c;称为样式表的层叠。样式表也可通过设计模式编辑样…...

Docker与微服务实战2022

基础篇(零基小白)1.Docker简介1.1 是什么问题&#xff1a;为什么会有docker出现&#xff1f;您要如何确保应用能够在这些环境中运行和通过质量检测&#xff1f;并且在部署过程中不出现令人头疼的版本、配置问题&#xff0c;也无需重新编写代码和进行故障修复&#xff1f; 答案就…...

Linux(传输层二)

文章目录0. 前言1. TCP协议1-1 TCP协议段格式1. TCP如何解包&#xff1f;2. TCP协议如何交付&#xff08;应用层- - 客户&#xff09;&#xff1f;3. 如何理解报文本身&#xff1f;4. 如何理解报文字段&#xff1f;1-2 确认应答(ACK)机制1-3 超时重传机制1-4 连接管理机制1. TC…...

4.Spring Cloud (Hoxton.SR8) 学习笔记—Nacos微服务治理、Nacos配置管理

本文目录如下&#xff1a;一、Nacos微服务治理Nacos 下载 与 启动Spring Cloud 集成 NacosIDEA 同一个 Application 启动多次Nacos - 配置集群Nacos - 设置负载均衡Nacos - 设置服务实例的权重二、Nacos 配置管理Nacos - 合理的把配置信息迁移到 Nacos 中Nacos - 配置命名规范N…...

卷王都在偷偷准备金三银四了...

年终奖没发&#xff1b; 简历石沉大海&#xff1b; 发消息只读不回 打开某招聘&#xff0c;看了看岗位&#xff0c;这个厂还不错&#xff0c;可是要求好高&#xff0c;我啥都不会。 “哎&#xff0c;算了&#xff0c;我简历还没更新呢&#xff0c;我躺到6月份拿到年终奖再跑…...