【Linux】自主WEB服务器实现

自主web服务器实现

  • 1️⃣构建TcpServer
  • 2️⃣构建HttpServer
  • 3️⃣构建HttpRequest和HttpResponse
    • Http请求报文格式
    • Http相应报文
    • 读取、处理请求&构建响应
      • 读取请求中的一行
      • 读取请求中需要注意的点
  • 4️⃣CGI模式
    • 判断是否需要用CGI处理请求
    • 构建任务&线程池管理
  • 5️⃣实验结果及总结
    • 项目源码:
    • 测试服务器各种情况
    • 总结

在这里插入图片描述

1️⃣构建TcpServer

首先根据通过Tcp/Ip协议获取到客户端的套接字:

  1. 创建监听套接字listen_sock
  2. 绑定bind监听套接字和相应端口号
  3. 监听listen来自客户端的连接
    在这里插入图片描述
#pragma once#include "log.hpp"#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <cstdlib>
#include <unistd.h>namespace ns_tcp
{const uint16_t g_port = 8080;const int backlog = 5;enum{SOCK_ERR = 2,BIND_ERR,LISTEN_ERR};class TcpServer{private:uint16_t _port;int _listen_sock;static TcpServer* svr;//单例模式private:TcpServer(uint16_t port = g_port):_port(port){}TcpServer(const TcpServer& s){}public:static TcpServer* getInstance(int port){if(nullptr == svr){static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;//静态初始化锁pthread_mutex_lock(&lock);//加锁if(nullptr == svr){svr = new TcpServer(port);}pthread_mutex_unlock(&lock);//解锁}return svr;}void InitTcpServer(){Socket();Bind();Listen();LOG(INFO, "init tcp success");}void Socket(){_listen_sock = socket(AF_INET, SOCK_STREAM, 0);if(_listen_sock < 0){LOG(FATAL, "socket error.");exit(SOCK_ERR);}//std::cout << "listen_sock create success" << std::endl;int opt = 1;setsockopt(_listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));//设置地址复用LOG(INFO, "socket success.");}void Bind(){struct sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(_port);local.sin_addr.s_addr = INADDR_ANY;//云服务器不绑定公网ipif(bind(_listen_sock, (sockaddr*)&local, sizeof(local)) < 0){LOG(FATAL, "bind error.");exit(BIND_ERR);}//std::cout << "bind success:" << _port << std::endl;LOG(INFO, "bind success.");}void Listen(){if(listen(_listen_sock, backlog) < 0){LOG(FATAL, "listen error.");exit(LISTEN_ERR);}LOG(INFO, "listen success");}int Sock(){return _listen_sock;}~TcpServer(){if(_listen_sock >= 0)close(_listen_sock);}};TcpServer* TcpServer::svr = nullptr;}

2️⃣构建HttpServer

启动TcpServer监听客户端的连接请求,当监听到来自客户端的连接后,由HttpServer接收获取accept对端套接字:

#pragma once#include "TcpServer.hpp"
#include "Protocal.hpp"
#include "Task.hpp"
#include "ThreadPool.hpp"#include <strings.h>
#include <signal.h>using namespace ns_tcp;namespace ns_http
{const uint16_t g_port = 8080;class HttpServer{private:int _port;bool _stop; // 服务器是否停止服务public:HttpServer(uint16_t port = g_port) : _port(port), _stop(false){}void InitHttpServer(){//忽略SIGPIPE信号,如果不忽略,则会在写入失败时崩溃Serversignal(SIGPIPE, SIG_IGN);}void Loop(){//std::cout << "http " << _port << std::endl;//_port = 0? 自己写的bug含泪也要找出来,传参传了argv[0]导致绑定的端口号有问题TcpServer* tsvr = TcpServer::getInstance(_port);tsvr->InitTcpServer();//std::cout << tsvr->Sock() << std::endl;while (!_stop){// 获取对端链接sockaddr_in peer;socklen_t len = sizeof(peer);bzero(&peer, len);int sock = accept(tsvr->Sock(), (sockaddr *)&peer, &len);if (sock < 0) // accept error{continue;}std::cout << "获取到新连接:" << sock << std::endl;Task t(sock);//创建任务ThreadPool::getInstance()->PushTask(t);//往线程池中添加任务//temporary cope/*pthread_t tid;int *_sock = new int(sock);pthread_create(&tid, nullptr, Entrance::HandlerRequest, _sock);pthread_detach(tid);*/}}~HttpServer(){}};
}

3️⃣构建HttpRequest和HttpResponse

Http请求报文格式

在这里插入图片描述
由上图可知,Http请求报文分为四部分:

  1. 请求行request_line
  2. 请求报头request_header
  3. 空行blank
  4. 请求报文request_body

其中请求行分为请求方法method(GET or POST)、请求URI、Http协议及版本号
而请求报头是由一个个的K-V键值对构成

Http相应报文

在这里插入图片描述
Http响应报文与Http请求报文构成类似,亦是由四部分组成:

  1. 状态行status_line
  2. 响应报头response_header
  3. 空行blank
  4. 响应报文response_body

其中状态行由Http协议及版本号version、状态码status_code、状态码描述信息三部分构成
响应报头与请求报头一样,都是由键值对构成

由此构建出HttpRequest和HttpResponse类:

/*** 请求报文:* 1.请求行* 2.请求报头* 3.空行* 4.请求正文
*/
class HttpRequest
{public:std::string request_line;std::vector<std::string> request_header;std::string blank;std::string request_body;//请求行: 方法 uri 版本std::string method;std::string uri;//path?query_stringstd::string version;std::string path;std::string query_string;std::string suffix;//请求的资源后缀//请求报头: key - valuestd::unordered_map<std::string, std::string> headerkv;int content_length;bool cgi;public:HttpRequest():content_length(0),cgi(false){}~HttpRequest(){}
};/*** 响应报文* 1.状态行* 2.响应报头* 3.空行* 4.响应报文
*/
class HttpResponse
{public:std::string status_line;std::vector<std::string> response_header;std::string blank;std::string response_body;int status_code;//状态码int fd;//打开网页所在的文件size_t size;//所访问资源的大小public:HttpResponse():blank(END_LINE),status_code(OK),fd(-1),size(0){}~HttpResponse(){}
};

读取、处理请求&构建响应

当一个Http请求到来时,首先我们需要把Http请求读取并解析出来:

读取请求中的一行

读取Http请求,需要化繁为简,先读取报文中的一行:
在Http协议中,一行的结束符一般有三种情况:
"\n","\r"或者"\r\n"结束
因此,为了确保应对所有情况,我们在读取请求的时候将这三种情况统一转换为"\n"进行处理。
其次,对应HTTP请求报头的键值对,我们通过": "将其切分

class Util
{//存放工具的类public:/*** 用于读取报文中的一行*/static int ReadLine(int sock, std::string& out){char ch = 'X';while(ch != '\n'){ssize_t s = recv(sock, &ch, 1, 0);if(s > 0){if(ch == '\r'){//统一报文一行的格式为末尾\n结束recv(sock, &ch, 1, MSG_PEEK);//对报文\r后的一个字符进行窥探,不从缓冲区中移除if(ch == '\n'){//窥探成功,后一个字符为\n,将整体\r\n替换为\n//将后一个从缓冲区中读出并移除recv(sock, &ch, 1, 0);}else{//替换报文一行中最后一个字符为\nch = '\n';}}//走到此处要么是报文内容,要么就是\nout += ch;//将该字符添加到out中输出}else if(s == 0){//对端链接关闭return 0;}else{//读取出错return -1;}}return out.size();}/*** 用于划分报文的请求报头为key-value*/static bool SepString(const std::string& header, std::string& out_str1, std::string& out_str2, std::string sep){auto pos = header.find(sep);if(pos != std::string::npos){out_str1 = header.substr(0, pos);out_str2 = header.substr(pos + sep.size());return true;}return false;}
};

读取请求中需要注意的点

在读取请求的时候,为了美观,会通过resize将一行的行结束符"\n"去除,但是去除的前提是对端发来的请求是完整的,若是读取的请求为空,那么会导致

std::length_error,what(): basic_string::resize 这样的运行错误

//读取请求 处理请求 构建响应
class EndPoint
{private:int _sock;//对端套接字HttpRequest _http_request;HttpResponse _http_response;bool stop;//用于服务器的启停private:bool RecvHttpRequestLine(){//读取请求行if(Util::ReadLine(_sock, _http_request.request_line) > 0)//读取成功{          _http_request.request_line.resize(_http_request.request_line.size() - 1);LOG(INFO, _http_request.request_line);}else{stop = true;//读取失败,终止服务器}return stop;}bool RecvHttpRequestHeader(){std::string line;while(true){line.clear();//清空lineif(Util::ReadLine(_sock, line) <= 0)//读取失败,停止服务{stop = true;break;}if(line == "\n")//空行{_http_request.blank = line;LOG(INFO, line);break;//忘记break了,死活半天没找着这个bug,导致没跳出循环,之后走出if语句resize出错了}line.resize(line.size() - 1);//将该行的\n去除_http_request.request_header.push_back(line);LOG(INFO, line);}return stop;}void ParseHttpRequestLine(){std::stringstream ss(_http_request.request_line);//按空格解析请求行ss >> _http_request.method >> _http_request.uri >> _http_request.version;//LOG(INFO, _http_request.method);//LOG(INFO, _http_request.uri);//LOG(INFO, _http_request.version);std::transform(_http_request.method.begin(), _http_request.method.end(), _http_request.method.begin(), ::toupper);//将请求行全部转成大写}void ParseHttpRequestHeader(){std::string key;std::string value;for(auto& header : _http_request.request_header){Util::SepString(header, key, value, SEP);//std::cout << "debug: " << key << std::endl;//std::cout << "debug: " << value << std::endl;_http_request.headerkv.insert({key, value});//分割完报头后忘记构建映射关系,导致出现bug}}//通过method以及Content-Length字段判断该http请求是否需要读取请求报文bool IsNeedRecvHttpRequestBody(){if(_http_request.method == "POST"){auto iter = _http_request.headerkv.find("Content-Length");if(iter != _http_request.headerkv.end()){_http_request.content_length = atoi(iter->second.c_str());LOG(INFO, "recv http request body: " + std::to_string(_http_request.content_length));return true;}}return false;}//读取Http请求报文void RecvHttpRequestBody(){if(IsNeedRecvHttpRequestBody()){int content_length = _http_request.content_length;char ch;while(content_length){ssize_t s = recv(_sock, &ch, 1, 0);if(s > 0){_http_request.request_body.push_back(ch);content_length--;}else{//TODOstop = false;//读取失败break;}}//std::cout << "debug: " << _http_request.request_body << std::endl;LOG(INFO, _http_request.request_body);}}int ProcessNonCgi(){_http_response.fd = open(_http_request.path.c_str(), O_RDONLY);//打开要访问的网页所在的文件if(_http_response.fd >= 0)//打开文件成功{               return OK;}return 404;}int ProcessCgi(){int code = OK;std::cout << "debug: " << "CGI MODEL" << std::endl;auto& bin = _http_request.path;//让子进程执行的目标程序,一定存在auto& method = _http_request.method;std::string& query_string = _http_request.query_string;std::string body_text = _http_request.request_body.c_str();std::string method_env;std::string query_string_env;std::string content_length_env;int input[2];//创建input管道,父进程从input中读取数据,子进程向input中写入数据int output[2];//创建output管道,父进程向output中写入数据,子进程从output中读取数据if(pipe(input) < 0){LOG(ERROR, "pipe error");code = SERVER_ERROR;return code;}if(pipe(output) < 0){LOG(ERROR, "pipe error");code = SERVER_ERROR;return code;}pid_t pid = fork();//创建子进程处理传来的数据if(pid == 0){//child//约定:子进程向input写入数据,从output中读取数据,关闭input的读端和output的写端close(input[0]);close(output[1]);//子进程需要通过环境变量知道请求方法,再通过请求方法来判断从何处读取消息method_env = "METHOD=";method_env += method;putenv((char*)method_env.c_str());if("GET" == method){//若请求方法为GET,则提交的数据一般较少,此时通过环境变量传送query_string_env = "QUERY_STRING=";query_string_env += query_string;putenv((char*)query_string_env.c_str());}else if("POST" == method){content_length_env = "CONTENT_LENGTH=";//环境变量忘记加上=导致后续获取环境变量时导致了访问nullptr的错误content_length_env += std::to_string(_http_request.content_length);putenv((char*)content_length_env.c_str());//LOG(WARNING, getenv("CONTENT_LENGTH"));}else{//Do Nothing}//std::cout << "debug: bin -- " << bin << std::endl;//为了保证子进程在切换程序后仍然能够接收到父进程发送的数据同时给父进程发数据//让子进程的文件描述符0(标准输入)和1(标准输出)重定向为读取和写入//内核的数据结构在进程切换过程中会被保留//为了保证stdout的数据不影响子进程接收,将重定向放至最后进行dup2(output[0], 0);dup2(input[1], 1);//子进程切换到处理数据的进程execl(bin.c_str(), bin.c_str(), nullptr);exit(1);//到此处说明进程没有切换成功,则直接退出子进程}else if(pid > 0){//parent//父进程向output写入数据,从input中读取数据,关闭input的写端和output的读端close(input[1]);close(output[0]);//管道创建完成,向子进程发送数据if("POST" == method){//请求方法为POST,通过管道向子进程发送数据const char* start = body_text.c_str();size_t size = 0;size_t total = 0;while((total < _http_request.content_length) && (size = write(output[1], start + total, body_text.size() - total)) > 0){total += size;}//LOG(WARNING, std::to_string(total));}char ch;while(read(input[0], &ch, 1) > 0){//从管道读取的数据放到http响应报文中_http_response.response_body.push_back(ch);}LOG(INFO, _http_response.response_body);int status = 0;pid_t ret = waitpid(pid, &status, 0);if(ret == pid){if(WIFEXITED(status))//正常退出{if(WEXITSTATUS(status) == 0)//且退出码为0(数据正确){code = OK;//LOG(INFO, "Cgi success");}else{code = BAD_REQUEST;//LOG(WARNING, "Cgi Process Error");}}else{code = SERVER_ERROR;//LOG(WARNING, "Cgi Exit Error");}}//为了尽量不浪费服务器资源,父进程等待完子进程后关闭管道close(input[0]);close(output[1]);}else{//创建子进程失败LOG(ERROR, "fork error!");code = SERVER_ERROR;return code;}return code;}void BuildOKResponse(){//状态行已经构建完成,开始构建响应报头std::string line;line = "Content-Type: ";line += Suffix2Desc(_http_request.suffix);line += END_LINE;_http_response.response_header.push_back(line);line = "Content-Length: ";if(_http_request.cgi){//cgi模式,Content-Length为响应报文长度line += std::to_string(_http_response.response_body.size());}else{//非cgi模式,Content-Length为静态网页的大小line += std::to_string(_http_response.size);}line += END_LINE;_http_response.response_header.push_back(line);}void HandlerError(std::string page){LOG(INFO, page);_http_request.cgi = false;//错误处理返回静态网页,故发送Http相应报文默认按非cgi模式//打开相应的静态网页_http_response.fd = open(page.c_str(), O_RDONLY);std::cout << "debug HandlerError: " << _http_response.fd << std::endl;std::string line;//返回相应的静态网页//构建响应报头line = "Content-Type: text/html";line += END_LINE;_http_response.response_header.push_back(line);line = "Content-Length: ";struct stat st;stat(page.c_str(), &st);//获取静态网页的信息_http_response.size = st.st_size;//需要将错误信息网页的大小带回,否则会出现报文长度为0line += std::to_string(st.st_size);line += END_LINE;_http_response.response_header.push_back(line);}/*** 根据不同的状态码构建相应的Http响应*/void BuildHttpResponseHelper(){auto& code = _http_response.status_code;//构建状态行std::string& status_line = _http_response.status_line;status_line = HTTP_VERSION;status_line += " ";status_line += std::to_string(code);status_line += " ";status_line += Code2Desc(code);status_line += END_LINE;std::string path = WEB_ROOT;path += "/";//根据状态码构建对应的Http响应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 SERVER_ERROR:path += PAGE_500;HandlerError(path);break;default:break;}}public:EndPoint(int sock):_sock(sock), stop(false){}bool Stop(){return stop;}void RecvHttpRequest(){//接收读取Http请求if(!RecvHttpRequestLine() && !RecvHttpRequestHeader()){//只有当请求行和请求报头都读取成功后,才解析处理Http//解析处理HttpParseHttpRequestLine();ParseHttpRequestHeader();RecvHttpRequestBody();}} //构建Http响应/*** 1.判断method是否为GET或者POST* 2.分析url获取path和query(如果有)* 3.判断对应path是否存在,若存在先判断是否为目录;若不存在则返回404* */void BuildHttpResponse(){std::string _path;struct stat st;size_t found = 0;//查找资源后缀if(_http_request.method != "GET" && _http_request.method != "POST"){_http_response.status_code = BAD_REQUEST;LOG(WARNING, "http request method is not right");goto END;}if(_http_request.method == "GET"){//判断GET方法是否有资源请求size_t pos = _http_request.uri.find("?");if(pos != std::string::npos){//有数据上传//path?query_stringUtil::SepString(_http_request.uri, _http_request.path, _http_request.query_string, "?");//std::cout << "debug: " << _http_request.path << std::endl;//std::cout << "debug: " << _http_request.query_string << std::endl;_http_request.cgi = true;//处理数据采用cgi模式}else{//path_http_request.path = _http_request.uri;//std::cout << "debug: " << _http_request.uri << std::endl;}}else if(_http_request.method == "POST"){//POST方法,采用cgi模式_http_request.cgi = true;_http_request.path = _http_request.uri;//POST方法需要将路径名修改为uri}else{//DO NOTHING }//添加为WEB默认根目录_path = _http_request.path;//std::cout << "debug: " << _path << std::endl;_http_request.path = WEB_ROOT + _path;if(_http_request.path[_http_request.path.size() - 1] == '/'){//默认路径_http_request.path += HOME_PAGE;}//std::cout << "debug: " << _http_request.path << std::endl;//解析path判断其是否存在于当前WEB目录中if(stat(_http_request.path.c_str(), &st) == 0){//路径存在,判断是否为目录,若为目录,则访问默认文件if(S_ISDIR(st.st_mode)){//当前路径为目录,但是末尾不带有/_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)){//特殊处理_http_request.cgi = true;}_http_response.size = st.st_size;//获取要访问的资源的大小//std::cout << "debug: " << size << std::endl;}else{//路径不存在,返回NOT FOUNDstd::string info = _http_request.path;info += " NOT FOUND";LOG(WARNING, info);_http_response.status_code = NOT_FOUND;goto END;}//走到此处说明路径正确//处理该路径获取请求资源的类型(.html等),从后往前找.found = _http_request.path.rfind('.');if(found == std::string::npos){//后缀设置为默认.html_http_request.suffix = ".html";}else{_http_request.suffix = _http_request.path.substr(found);}if(_http_request.cgi == true){_http_response.status_code = ProcessCgi();//按cgi模式处理请求}else{//1.返回不单单是返回网页//2.而是要构建HTTP响应_http_response.status_code = ProcessNonCgi();//简单的返回网页,只返回静态网页}
END:BuildHttpResponseHelper();}//发送Http响应void SendHttpResponse(){send(_sock, _http_response.status_line.c_str(), _http_response.status_line.size(), 0);//发送Http响应状态行LOG(INFO, _http_response.status_line.c_str());for(auto iter : _http_response.response_header){send(_sock, iter.c_str(), iter.size(), 0);//发送Http响应报头LOG(INFO, iter.c_str());}send(_sock, _http_response.blank.c_str(), _http_response.blank.size(), 0);//发送Http响应空行LOG(INFO, _http_response.blank.c_str());if(_http_request.cgi){//cgi模式,发送Http相应报文ssize_t size = 0;size_t total = 0;const char* start = _http_response.response_body.c_str();//size = 表达式的右括号写道 > 0的后面去了,导致每次size赋值都是1,从而出现一直发送的现象while(total < _http_response.response_body.size() && (size = send(_sock, start + total, _http_response.response_body.size() - total, 0)) > 0){total += size;std::cout << "debug: total: " << total <<std::endl;}}else{//非cgi模式,发送静态网页ssize_t s = sendfile(_sock, _http_response.fd, nullptr, _http_response.size);//发送Http响应报文,不通过用户层缓冲区string,直接从内核fd拷贝到内核sock//std::cout << "debug : s -- " << s << std::endl;LOG(INFO, std::to_string(_http_response.size));close(_http_response.fd);}}~EndPoint(){close(_sock);}
};
class CallBack
{
public:CallBack(){}void operator()(int sock){HandlerRequest(sock);}static void HandlerRequest(int sock){//对sock发来的报文做处理,协议为Http/*int sock = *(int*)_sock;delete (int*)_sock;*/std::cout << "----------------begin----------------" << std::endl;EndPoint* ep = new EndPoint(sock);ep->RecvHttpRequest();if(!ep->Stop()){LOG(INFO, "Recv Success, Begin Build and Send");ep->BuildHttpResponse();ep->SendHttpResponse();}else{LOG(WARNING, "Recv Error, Stop Build and Send");}std::cout << "-----------------end-----------------" << std::endl;LOG(INFO, "Handler Request End");  }~CallBack(){}
};

4️⃣CGI模式

HTTP CGI机制
CGI(Common Gateway Interface) 是WWW技术中重要的技术之一,有着不可替代的重要地位。CGI是外部应用程序(CGI程序)与WEB服务器之间的接口标准,是在CGI程序和Web服务器之间传递信息的过程。
在这里插入图片描述
通过上面这张图可以很清晰的理解CGI模式。

判断是否需要用CGI处理请求

首先对应GET方法,若请求行的URI中通过?带有参数,则说明需要CGI处理,而POST请求由于有数据传来,因此也需要CGI处理。

GET /test_cgi?a=100&b=200 HTTP/1.1

CGI模式为进程切换,由于HTTP服务器进程不能终止,因此应该创建子进程来进行进程切换到CGI处理程序。至于父子进程间的通信,若为GET方法,传入的参数一般不大,通过环境变量交互即可;若为POST方法,一般所传参数可能较大,则采用管道进行进程间通信。
CGI处理请求:

        int ProcessNonCgi(){_http_response.fd = open(_http_request.path.c_str(), O_RDONLY);//打开要访问的网页所在的文件if(_http_response.fd >= 0)//打开文件成功{               return OK;}return 404;}int ProcessCgi(){int code = OK;std::cout << "debug: " << "CGI MODEL" << std::endl;auto& bin = _http_request.path;//让子进程执行的目标程序,一定存在auto& method = _http_request.method;std::string& query_string = _http_request.query_string;std::string body_text = _http_request.request_body.c_str();std::string method_env;std::string query_string_env;std::string content_length_env;int input[2];//创建input管道,父进程从input中读取数据,子进程向input中写入数据int output[2];//创建output管道,父进程向output中写入数据,子进程从output中读取数据if(pipe(input) < 0){LOG(ERROR, "pipe error");code = SERVER_ERROR;return code;}if(pipe(output) < 0){LOG(ERROR, "pipe error");code = SERVER_ERROR;return code;}pid_t pid = fork();//创建子进程处理传来的数据if(pid == 0){//child//约定:子进程向input写入数据,从output中读取数据,关闭input的读端和output的写端close(input[0]);close(output[1]);//子进程需要通过环境变量知道请求方法,再通过请求方法来判断从何处读取消息method_env = "METHOD=";method_env += method;putenv((char*)method_env.c_str());if("GET" == method){//若请求方法为GET,则提交的数据一般较少,此时通过环境变量传送query_string_env = "QUERY_STRING=";query_string_env += query_string;putenv((char*)query_string_env.c_str());}else if("POST" == method){content_length_env = "CONTENT_LENGTH=";//环境变量忘记加上=导致后续获取环境变量时导致了访问nullptr的错误content_length_env += std::to_string(_http_request.content_length);putenv((char*)content_length_env.c_str());//LOG(WARNING, getenv("CONTENT_LENGTH"));}else{//Do Nothing}//std::cout << "debug: bin -- " << bin << std::endl;//为了保证子进程在切换程序后仍然能够接收到父进程发送的数据同时给父进程发数据//让子进程的文件描述符0(标准输入)和1(标准输出)重定向为读取和写入//内核的数据结构在进程切换过程中会被保留//为了保证stdout的数据不影响子进程接收,将重定向放至最后进行dup2(output[0], 0);dup2(input[1], 1);//子进程切换到处理数据的进程execl(bin.c_str(), bin.c_str(), nullptr);exit(1);//到此处说明进程没有切换成功,则直接退出子进程}else if(pid > 0){//parent//父进程向output写入数据,从input中读取数据,关闭input的写端和output的读端close(input[1]);close(output[0]);//管道创建完成,向子进程发送数据if("POST" == method){//请求方法为POST,通过管道向子进程发送数据const char* start = body_text.c_str();size_t size = 0;size_t total = 0;while((total < _http_request.content_length) && (size = write(output[1], start + total, body_text.size() - total)) > 0){total += size;}//LOG(WARNING, std::to_string(total));}char ch;while(read(input[0], &ch, 1) > 0){//从管道读取的数据放到http响应报文中_http_response.response_body.push_back(ch);}LOG(INFO, _http_response.response_body);int status = 0;pid_t ret = waitpid(pid, &status, 0);if(ret == pid){if(WIFEXITED(status))//正常退出{if(WEXITSTATUS(status) == 0)//且退出码为0(数据正确){code = OK;//LOG(INFO, "Cgi success");}else{code = BAD_REQUEST;//LOG(WARNING, "Cgi Process Error");}}else{code = SERVER_ERROR;//LOG(WARNING, "Cgi Exit Error");}}//为了尽量不浪费服务器资源,父进程等待完子进程后关闭管道close(input[0]);close(output[1]);}else{//创建子进程失败LOG(ERROR, "fork error!");code = SERVER_ERROR;return code;}return code;}

构建任务&线程池管理

对于一个个的Http请求,若需要采用CGI模式,可以通过任务回调的方法来实现HTTP服务器与CGI处理程序的解耦,并且我们可以采用线程池来处理任务,因为一般服务器接收的请求量会很大,若每个请求都要创建线程的话最终可能导致服务器崩溃,但是即便采用线程池也并不能非常好的解决这个问题,可以采用Epoll多路转接技术,不过本项目在于学习Http协议及CGI机制,因此此处处理采用线程池。

#pragma once#include "Protocal.hpp"/*** 管理任务对象,负责将相应任务通过回调进行处理
*/
class Task{
private:int sock;//该任务对应的sockCallBack handler;//处理任务的回调方法
public:Task(){}Task(int _sock):sock(_sock){}void ProcessOn(){handler(sock);}~Task(){}
};
#pragma once#include <queue>
#include <pthread.h>#include "Task.hpp"
#include "log.hpp"#define NUM 6class ThreadPool
{
private:int num;//线程池中线程的数量std::queue<Task> task_queue;//任务队列,线程从该队列中获取任务pthread_mutex_t lock;//处理临界区的锁pthread_cond_t cond;//条件变量bool stop;//判断线程池是否停止工作ThreadPool(int _num = NUM):num(_num), stop(false){pthread_mutex_init(&lock, nullptr);pthread_cond_init(&cond, nullptr);}ThreadPool(const ThreadPool &){}static ThreadPool* single_instance;
public:static ThreadPool* getInstance(){static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;//静态锁if(nullptr == single_instance){pthread_mutex_lock(&mtx);if(nullptr == single_instance){single_instance = new ThreadPool();single_instance->InitThreadPool();}pthread_mutex_unlock(&mtx);}return single_instance;}void Lock(){pthread_mutex_lock(&lock);}void UnLock(){pthread_mutex_unlock(&lock);}bool IsStop(){return stop;}bool IsTaskQueueEmpty(){return task_queue.size() == 0 ? true : false;}static void* ThreadRoutine(void* args)//线程例程{ThreadPool* tp = (ThreadPool*)args;while(!tp->IsStop()){Task t;tp->Lock();//加锁while(tp->IsTaskQueueEmpty()){//任务队列为空,线程休眠tp->Wait();//当线程唤醒之后,此时一定是带有锁的}//获取任务tp->PopTask(t);tp->UnLock();//解锁t.ProcessOn();//调用任务回调函数处理任务}}bool InitThreadPool(){for(int i = 0; i < num; i++){pthread_t pid;if(pthread_create(&pid, nullptr, ThreadRoutine, this) != 0){LOG(FATAL, "create threadpool error!");return false;}}LOG(INFO, "create threadpool success");return true;}/*** 若暂时无任务处理,则让线程休眠*/void Wait(){pthread_cond_wait(&cond, &lock);}/*** 当有任务到来时,唤醒休眠的线程*/void WakeUp(){pthread_cond_signal(&cond);}/*** 服务器调用PushTask往线程池的任务队列中添加任务*/void PushTask(const  Task& t){Lock();task_queue.push(t);UnLock();WakeUp();//任务到来,唤醒任务}/*** 线程从任务队列中取任务*/void PopTask(Task& t){t = task_queue.front();task_queue.pop();}~ThreadPool(){pthread_mutex_destroy(&lock);pthread_cond_destroy(&cond);}
};ThreadPool* ThreadPool::single_instance = nullptr;

5️⃣实验结果及总结

项目源码:

自主web服务器项目源码

测试服务器各种情况

启动服务器后,访问页面:
GET方法提交数据及构建响应:
在这里插入图片描述

POST方法提交数据
在这里插入图片描述
点击提交数据后构建响应:
在这里插入图片描述
差错处理:
服务器处理数据异常时:
在这里插入图片描述

访问资源不存在时:
在这里插入图片描述

总结

http协议被广泛使用,从移动端,pc端浏览器,http协议无疑是打开互联网应用窗口的重要协议,http在网络应用层中的地位不可撼动,是能准确区分前后台的重要协议。
本次对http协议的理论学习,认识理解并运用CGI模式处理请求,在完成项目的过程中也遇到过许许多多,大大小小的bug,通过不断的调试最终获得较为不错的结果。该项目亦有许多不足值得完善,比如线程池的改良,接入MYSQL数据库进行数据管理,以及实现HTTP/1.1长连接功能等待,仍然有许多值得学习的地方。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/695305.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

《图解设计模式》笔记(二)交给子类

三、Template Method模式&#xff1a;将具体处理交给子类 示例程序类图 public static void main(String[] args) {// 生成一个持有H的CharDisplay类的实例AbstractDisplay d1 new CharDisplay(H);// 生成一个持有"Hello, world."的StringDisplay类的实例AbstractD…

C++ Linux多线程

1. C语言线程安全问题 1.1 线程安全问题 #include <stdio.h> #include <tinycthread.h> #include <io_utils.h>int count 0; int Counter(void*arg) {for(int i 0;i<100000;i){count;/** int temp count;* counttemp1;* return temp;* */}return 0; …

大模型训练流程(三)奖励模型

为什么需要奖励模型 因为指令微调后的模型输出可能不符合人类偏好&#xff0c;所以需要利用强化学习优化模型&#xff0c;而奖励模型是强化学习的关键一步&#xff0c;所以需要训练奖励模型。 1.模型输出可能不符合人类偏好 上一篇讲的SFT只是将预训练模型中的知识给引导出来…

Vue+SpringBoot打造大学兼职教师管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、研究内容三、界面展示3.1 登录注册3.2 学生教师管理3.3 课程管理模块3.4 授课管理模块3.5 课程考勤模块3.6 课程评价模块3.7 课程成绩模块3.8 可视化图表 四、免责说明 一、摘要 1.1 项目介绍 大学兼职教师管理系统&#xff0c;旨…

基于 QUIC 协议的 HTTP/3 正式发布!

近期&#xff0c;超文本传输协议新版本 HTTP/3 RFC 文档&#xff0c;已由互联网工程任务组&#xff08;IETF&#xff09;对外发布。HTTP/3 全称为 HTTP-over-QUIC&#xff0c;指在 QUIC&#xff08;Quick UDP Internet Connections, 快速 UDP 互联网连接&#xff09;上映射 HTT…

基于Java+小程序点餐系统设计与实现(源码+部署文档)

博主介绍&#xff1a; ✌至今服务客户已经1000、专注于Java技术领域、项目定制、技术答疑、开发工具、毕业项目实战 ✌ &#x1f345; 文末获取源码联系 &#x1f345; &#x1f447;&#x1f3fb; 精彩专栏 推荐订阅 &#x1f447;&#x1f3fb; 不然下次找不到 Java项目精品实…

如何使用CanaryTokenScanner识别Microsoft Office文档中的Canary令牌和可疑URL

关于CanaryTokenScanner CanaryTokenScanner是一款功能强大的Canary令牌和可疑URL检测工具&#xff0c;该工具基于纯Python开发&#xff0c;可以帮助广大研究人员快速检测Microsoft Office和Zip压缩文件中的Canary令牌和可疑URL。 在网络安全领域中&#xff0c;保持警惕和主动…

Leo赠书活动-17期 《基础软件之路:企业级实践及开源之路》

✅作者简介&#xff1a;大家好&#xff0c;我是Leo&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Leo的博客 &#x1f49e;当前专栏&#xff1a; 赠书活动专栏 ✨特色专栏&#xff1a;…

【网站项目】167校园失物招领小程序

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

Day10_面向对象-抽象类-接口-课后练习-参考答案

文章目录 代码编程题第1题第2题第3题 代码编程题 第1题 知识点&#xff1a;抽象类语法点&#xff1a;继承&#xff0c;抽象类按步骤编写代码&#xff0c;效果如图所示&#xff1a; 编写步骤&#xff1a; 定义抽象类A&#xff0c;抽象类B继承A&#xff0c;普通类C继承BA类中&…

Java,SpringBoot项目中,Postman的测试方法。

目录 展示查询搜索 根据id展示数据 根据id删除数据 根据id更新数据 添加数据 展示查询搜索 // 根据姓名分页查询用户GetMapping("/getUsersByName")public IPage<User> getUsersByName(RequestParam(defaultValue "1") Long current,RequestPar…

(っ•̀ω•́)っ 如何在PPT中为文本框添加滚动条

本人在写技术分享的PPT时&#xff0c;遇到问题&#xff1a;有一大篇的代码&#xff0c;如何在一张PPT页面上显示&#xff1f;急需带有滚动条的文本框&#xff01;百度了不少&#xff0c;自己也来总结一篇&#xff0c;如下&#xff1a; 1、找到【文件】-【选项】 2、【自定义功…

《深入浅出 Spring Boot 3.x》预计3月份发版

各位&#xff0c;目前本来新书《深入浅出 Spring Boot 3.x》已经到了最后编辑排版阶段&#xff0c;即将在3月份发布。 目录&#xff1a; 现在把目录截取给大家&#xff1a; 主要内容&#xff1a; 本书内容安排如下。 ● 第 1 章和第 2 章讲解 Spring Boot 和传统 Spri…

万界星空科技MES系统,实现数字化智能工厂

万界星空科技帮助制造型企业解决生产过程中遇到的生产过程不透明&#xff0c;防错成本高&#xff0c;追溯困难&#xff0c;品质不可控&#xff0c;人工效率低下&#xff0c;库存积压&#xff0c;交期延误等问题&#xff0c;从而达到“降本增效”的目标。打通各个信息孤岛&#…

深入解析SDRAM:从工作原理到实际应用

深入解析SDRAM&#xff1a;从工作原理到实际应用 在众多内存技术中&#xff0c;同步动态随机访问存储器&#xff08;SDRAM&#xff09;因其出色的性能和广泛的应用而备受关注。本文将从SDRAM的工作原理入手&#xff0c;探讨其性能优化策略和在现代电子设备中的应用。 SDRAM工作…

Meta 发布 MMCSG (多模态智能眼镜对话数据集)

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

Redis突现拒绝连接问题处理总结

一、问题回顾 项目突然报异常 [INFO] 2024-02-20 10:09:43.116 i.l.core.protocol.ConnectionWatchdog [171]: Reconnecting, last destination was 192.168.0.231:6379 [WARN] 2024-02-20 10:09:43.120 i.l.core.protocol.ConnectionWatchdog [151]: Cannot reconnect…

【LeetCode】树的BFS(层序遍历)精选6题

目录 1. N 叉树的层序遍历&#xff08;中等&#xff09; 2. 二叉树的锯齿形层序遍历&#xff08;中等&#xff09; 3. 二叉树的最大宽度&#xff08;中等&#xff09; 4. 在每个树行中找最大值&#xff08;中等&#xff09; 5. 找树左下角的值&#xff08;中等&#xff09…

win10编译openjdk源码

上篇文章作者在ubuntu系统上实践完成openjdk源码的编译&#xff0c;但是平常使用更多的是window系统&#xff0c;ubuntu上编译出来JDK无法再windows上使用。所以作者又花费了很长时间在windows系统上完成openjdk源码的编译&#xff0c;陆续花费一个月的时间终于完成了编译。 本…

【设计模式】使用适配器模式做补偿设计

文章目录 1.概述2.两种适配器模式2.1.类适配器2.2.对象适配器 3.总结 1.概述 适配器模式是一种结构型设计模式&#xff0c;它提供了一个中间层&#xff0c;通过这个中间层&#xff0c;客户端可以使用统一的接口与具有不同接口的类进行交互&#xff0c;也就是说&#xff0c;将一…