首页
最新活动
服务器租用
香港服务器租用
台湾服务器租用
美国服务器租用
日本服务器租用
新加坡服务器租用
高防服务器
香港高防服务器
台湾高防服务器
美国高防服务器
裸金属
香港裸金属服务器
台湾裸金属服务器
美国裸金属服务器
日本裸金属服务器
新加坡裸金属服务器
云服务器
香港云服务器
台湾云服务器
美国云服务器
日本云服务器
CDN
CDN节点
CDN带宽
CDN防御
CDN定制
行业新闻
官方公告
香港服务器资讯
帮助文档
wp博客
zb博客
服务器资讯
联系我们
关于我们
机房介绍
机房托管
登入
注册
帮助文档
专业提供香港服务器、香港云服务器、香港高防服务器租用、香港云主机、台湾服务器、美国服务器、美国云服务器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;i
0){ body+=c; } else{ stop = true; break; } } return stop; } } bool IsNeedRecvHttpRequestBody() //判断需不需要读 POST方法+存在contentlength,就要读取body了 { auto &method = http_request.method; auto &mp = http_request.header_kv; if (method == "POST") { if (mp.find("Content-Length") != mp.end()) { http_request.size = atoi(mp["Content-Length"].c_str()); //记录一下body的size return true; } return false; } return false; } bool RecvHttpRequestBody() { if (IsNeedRecvHttpRequestBody()) { int len = http_request.size; //这里不能&,不然下面循环 原来的size就减没了,为啥这么精确 -->防止粘包 auto body = http_request.request_body; for (int i = 0; i < len; i++) { char c; int s = recv(sock, &c, 1, 0); //流式读取 if (s > 0) { body += c; } else { stop = true; break; } } cout << endl; cout << body << endl; return stop; } } 注意正文的读取需要配合后面的parse先解析拿出参数,再判断有没有正文读取; 解析请求 解析请求的过程为将读取的request报文的对应属性和内容存入特定的请求类中;用于后续构建响应直接对照构建; 请求行的三个属性提取出来分别放入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插入对应数据库用于下次查看)在线音视频播放(已经支持了)在线网络计算器(我们已经实现了建议的±*/)
上一篇
香港IP地址列表
下一篇
香港服务器WDCP部署
相关文章
电脑远程控制怎么弄VPs
C# Tcplistener,Tcp服务端,Tcp心跳包服务器简易封装
godaddy怎么添加二级域名
防火墙服务器怎么打开
北斗卫星时钟同步服务器对电力系统有多重要?
【问题解决】VSCode1.86.0版+拓展Remote-SSHv0.108 无法连接到VSCode服务器(VSCode无法远程连接到Linux)
Ubuntu16.04服务器安装LLaVA对应的CUDA
修改ssh端口
腾讯云轻量服务器通过Docker搭建外网可访问连接的redis5.x集群
香港云服务器租用推荐
服务器租用资讯
·广东云服务有限公司怎么样
·广东云服务器怎么样
·广东锐讯网络有限公司怎么样
·广东佛山的蜗牛怎么那么大
·广东单位电话主机号怎么填写
·管家婆 花生壳怎么用
·官网域名过期要怎么办
·官网邮箱一般怎么命名
·官网网站被篡改怎么办
服务器租用推荐
·美国服务器租用
·台湾服务器租用
·香港云服务器租用
·香港裸金属服务器
·香港高防服务器租用
·香港服务器租用特价
7*24H在线售后
高可用资源,安全稳定
1v1专属客服对接
无忧退款试用保障
德讯电讯股份有限公司
电话:00886-982-263-666
台湾总部:台北市中山区建国北路一段29号3楼
香港分公司:九龙弥敦道625号雅兰商业二期906室
服务器租用
香港服务器
日本服务器
台湾服务器
美国服务器
高防服务器购买
香港高防服务器出租
台湾高防服务器租赁
美国高防服务器DDos
云服务器
香港云服务器
台湾云服务器
美国云服务器
日本云服务器
行业新闻
香港服务器租用
服务器资讯
香港云服务器
台湾服务器租用
zblog博客
香港VPS
关于我们
机房介绍
联系我们
Copyright © 1997-2024 www.hkstack.com All rights reserved.