帮助文档
专业提供香港服务器、香港云服务器、香港高防服务器租用、香港云主机、台湾服务器、美国服务器、美国云服务器vps租用、韩国高防服务器租用、新加坡服务器、日本服务器租用 一站式全球网络解决方案提供商!专业运营维护IDC数据中心,提供高质量的服务器托管,服务器机房租用,服务器机柜租用,IDC机房机柜租用等服务,稳定、安全、高性能的云端计算服务,实时满足您的多样性业务需求。 香港大带宽稳定可靠,高级工程师提供基于服务器硬件、操作系统、网络、应用环境、安全的免费技术支持。
服务器资讯 / 香港服务器租用 / 香港VPS租用 / 香港云服务器 / 美国服务器租用 / 台湾服务器租用 / 日本服务器租用 / 官方公告 / 帮助文档
自主Web服务器Http_Server
发布时间:2024-03-09 02:13:10   分类:帮助文档
自主Web服务器Http_Server 目录 自主web服务器背景目标描述技术特点项目定位 项目实现过程创建HttpServer基础框架TcpServer.hppHttpServer.hppLog.hppProtocol.hpp 解析C端发来的HTTP报文MSG_PEEK标志位Util.hpp 构建请求与响应类读取,解析请求构建响应读取请求解析请求构建响应stat系统函数 发送响应sendfile系统函数 Cgi技术cgi程序获取数据cgi程序处理并返回数据cgi技术总结 错误处理处理逻辑错误处理读取错误处理写入错误 引入线程池Task.hppThreadPool.hpp 提交表单测试cgi返回网页表单总结 补充数据库模拟注册运行展示 项目源代码链接项目总结项目扩展方向技术层面扩展应用层面扩展 自主web服务器 背景 http协议被广泛使用,从移动端,pc端浏览器,http协议无疑是打开互联网应用窗口的重要协议,http在网络应用层中的地位不可撼动,是能准确区分前后台的重要协议。 目标 在对http协议的理论学习的基础上,从零开始完成web服务器开发,坐拥下三层协议,从技术到应用,让网络难点无处遁形。 描述 采用C/S模型,编写支持中小型应用的http,并结合mysql,理解常见互联网应用行为,做完该项目,你可以从技术上 完全理解从你上网开始,到关闭浏览器的所有操作中的技术细节! 技术特点 网络编程(TCP/IP协议, socket流式套接字,http协议)多线程技术cgi技术线程池 项目定位 研发岗 开发环境 centos 7 + vim/gcc/gdb + C/C++; 项目实现过程 由于我们编写的是HTTP_SERVER,因此我们只需要编写s端,c端我们使用浏览器进行访问即可; 我们需要对应用层(主要)和传输层进行代码编写,网络层及一下,会有对应的TCP/IP协议来保证数据的交互; 下图表示短连接下,C端发起请求,S端响应请求,一来一回 之后关闭sock; 创建HttpServer基础框架 先创建一个能接收到浏览器HTTP报文的socket框架; TcpServer.hpp 这里将TcpServer中的socker,bind,listen进行了封装,用Init启动,同时设计了单例模式,一个HttpServer只需要一个监听listen_sock即可! #pragma once #include #include #include #include #include #include #include #include #include #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 #include #include #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 #include #include //日志处理 #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 #include #include #include 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 #include #include #include 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 request_header; //读取请求报头 string blank; //空行分隔符 string request_body; //请求报文主体(可能没有) //解析完毕之后的结果 //解析请求行三部分 string method; string uri; // path?args string version; //解析请求报头 unordered_map 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 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 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;i0){ 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报文的对应属性和内容存入特定的请求类中;用于后续构建响应直接对照构建; 请求行的三个属性提取出来分别放入method,uri,version请求报头数组中的一个个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() //解析请求行,入method,uri,version { // GET / HTTP/1.1 三部分用" "分隔 stringstream ss(http_request.request_line); ss >> http_request.method >> http_request.uri >> http_request.version; auto &method = http_request.method; std::transform(method.begin(), method.end(), method.begin(), ::toupper); //将请求方法大小写同一; // cout<mp(k,v) string k, v; Util::CutString(e, k, v, ":"); mp[k] = v; } // for(auto&e:mp){ // cout< #include #include int stat(const char *path, struct stat *buf);//Linux获取文件信息的系统接口 //参数1:文件路径 //参数2:stat 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 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 } //请求路径 我们上层得套wwwroot,index.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 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技术 简介CGI(Common Gateway Interface)公共网关接口,是外部扩展应用程序与 Web 服务器交互的一个标准接口。它可以使外部程序处理www上客户端送来的表单数据并对此作出反应,通过某些特定的方式处理数据返回给Web服务器进而返回给c端; 虽然我们是创建新线程执行每个c端请求的,但由于我们http_server的进程只有一个,想要到特定位置执行cgi程序,此处不能直接exec替换掉当前进程,否侧httpserver直接没了; 那么就需要创建子进程进行一系列替换操作了;为了实现数据的交互,我们需要同时引入进程间通信,由于是父子之间,那就匿名管道!(因为管道是单向通信,我们要双向通信,所以搞两个管道) 可我们打开两个管道后,父子进程可以看到没错,当子进程进行exec程序替换(只替换代码和数据)之后,这两个匿名管道是数据没了管道还是存在的,(虽然还是存在着的,但是替换的程序看不到的),因为相当于一个全新的进程开始运行,他的文件描述符数组只有初始的0,1,2号fd;3,4号这两个打开的管道被藏起来了,那怎么处理呢? 采用如下设计(一种约定): 我们采用dup2把0,1号标准fd重定向成当前的两个管道3,4;之后再exec替换,exec替换的程序里里是有0,1标准输入输出的,但是他其实已经被替换成两个管道了,用0,1就可以完成server与cgi.exe的交互了; cgi程序获取数据 当c端GET方法发送数据时,一般比较短,我们直接利用环境变量导入可以让cgi程序拿到;当c端POST方法发送数据时,我们直接通过管道写入cgi;当然至于是GET还是POST方法,我们需要导入一个METHOD方法环境变量,让cgi程序可以识别 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替换程序直接对0,1进行读取与写入,实际上就是与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 #include #include 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,进而返回给sock(c端链接) test_cgi.cc #include #include #include 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开发的高级语言,如php,java,底层都引用了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 #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 #include #include #include "Log.hpp" using std::queue; #define NUM 6 class Thread_Pool { private: int num; queue 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如下: TEST SUBMIT
x:
y:

表单里的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处理完数据将结果返回给浏览器 显示处理结果; 当中的method ="POST"时,提交如下: 由于我们表单采用的是GET方法,所以直接在浏览器的请求uri中就能看到提交的数据; 如果是POST方法,那么就会有更好的私密性,提交的数据会在request.body中传递给HTTP_SERVER; cgi返回网页 显然我们正常业务逻辑下HTTP_SERVER不可能只返回数据给C端,我们需要进行前端操作将数据处理以后嵌入网页返回给C端;(C++写这玩意有点麻烦,我们可以用javaweb php等写cgi程序,cgi程序支持所有语言的可执行程序,根据需求来) test_cgi #include #include #include 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 << ""; cout << ""; cout << ""; //往fd1输出,到httpserver了 cout << name1 << " : " << value1 << endl; cout << name2 << " : " << value2 << endl; cout << "

" << value1 << " + " << value2 << " = " << x + y << "

"; cout << "

" << value1 << " - " << value2 << " = " << x - y << "

"; cout << "

" << value1 << " * " << value2 << " = " << x * y << "

"; cout << "

" << value1 << " / " << value2 << " = " << x / y << "

"; //假设/0错误,cgi崩溃,父进程wait到的车状态就会异常,直接就错误处理返回静态错误网页了 不需要担心; cout << ""; cout << ""; 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 #include #include 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 << ""; cout << ""; cout << "

注册成功!信息已经插入后台数据库!

"; } } return 0; } 模拟注册运行展示 浏览器请求http_server,并填写账户信息准备提交注册: http_server中的sql_conn程序执行结果: http_server返回的网页给浏览器: 查看mysql中刚注册的账户信息: 项目源代码链接 Gitee仓库 项目总结 聚焦于处理HTTP的请求和构建对应响应; 我们主要研究基于 HTTP/1.0 短连接 的GET和POST方法; 获得请求,分析请求,错误处理等; 制定特定的网页src用于返回; 引入简单的日志系统 搭建CGI机制; 父子管道,设计dup2重定向,环境变量传参等 引入线程池; 采用多线程技术,缓解内存开销; 引入数据库; 链接mysql数据库,可以设计更多样的具体应用; 项目扩展方向 技术层面扩展 使用epoll机制(我们用的多线程只是用中小型业务)redis;请求转发服务器(代理功能,梯子) 应用层面扩展 在线博客(制定对应的格式text和前端功能,建立对应数据库,实现博客的上传查询与修改)在线画图板(返回一个在线画图板网页,用户画完,存入指定路径path,path插入对应数据库用于下次查看)在线音视频播放(已经支持了)在线网络计算器(我们已经实现了建议的±*/)
香港云服务器租用推荐
服务器租用资讯
·广东云服务有限公司怎么样
·广东云服务器怎么样
·广东锐讯网络有限公司怎么样
·广东佛山的蜗牛怎么那么大
·广东单位电话主机号怎么填写
·管家婆 花生壳怎么用
·官网域名过期要怎么办
·官网邮箱一般怎么命名
·官网网站被篡改怎么办
服务器租用推荐
·美国服务器租用
·台湾服务器租用
·香港云服务器租用
·香港裸金属服务器
·香港高防服务器租用
·香港服务器租用特价