上次讲到在我们在接收到请求以后,创建一个新的线程去处理这个请求,HandlerRequest为我们线程的入口函数,我们将accept到的文件描述符作为参数传递给线程
所以这里我们来详细来看一下对请求处理的过程,但是在将之前我们先来谈一个概念:cgi
CGI(Common Gateway Interface)是外部应用程序与Web服务器交互的一个标准接口。CGI应用程序可以完成客户端与服务器的交互操作,它打破了服务器软件的局限性, 允许用户根据需要采用各种语言去实现无法用HTTP、HTML实现的功能,给WWW提供了更为广阔的应用空间。例如,一个能够访问外部数据库的CGI程序 可以使客户端用户通过Web服务器进行数据库的查询。同时,CGI也为如何在不同的平台之间进行沟通提供了范例。
CGI工作的主要流程是:1.一个用户请求激活一个CGI应用程序;2.CGI应用程序将交互主页中用户输入的信息提取出来;3.将用户输入的信息传给服 务器主机应用程序(如数据库查询);4.将服务器处理结果通过HTML文件返回给用户;5.CGI进程结束。
CGI程序的工作原理是:客户端的Web浏览器浏览到某个主页后,利用一定的方式提交数据,并通过HTTP协议向Web服务器发出请求,服务器端的 HTTP Daemon(守护进程)将描述的主页信息通过标准输入stdin和环境变量(environment variable)传递给主页指定的CGI程序,并启动此应用程序进行处理(包括对数据库的处理),处理结果通过标准输出stdout返回给HTTP Daemon守护进程,再由HTTP Daemon进程通过HTTP协议返回给客户端的浏览器,由浏览器负责解释执行,将最终的结果显示给用户。
---------------------
以上CGI部分摘抄自:https://blog.csdn.net/chdhust/article/details/8838536
在大概说了cgi的概念之后我们来看我们这块对请求处理响应的过程
在进行处理之前我们定义一个Tools类,来存放我们其他类中可能需要用到的一些公共的工具函数
MakeKV函数用于对Request函数中请求头的键值构建KV结构
IntToStr函数用于将int型转换为对应值的字符串,这是一个用来转换的小的工具函数
GetCodeDesr用来将状态码转换为对应的说明
SuffixToType函数用来将文件后缀转换为对应的type
#define OK 200
#define NOT_FOUND 404
#define BAD_REQUEST 400
#define SERVER_ERROR 500
#define HTTP_VERSION "HTTP/1.0"
std::unordered_map stuffix_map{
{".html","text/html"},
{".htm", "text/html"},
{".css", "text/css"},
{".js", "application/x-javascript"}
};
class Tools
{
public:
static void MakeKV(std::unordered_map &kvmap,std::string &str)
{
std::size_t pos = str.find(": ");
if(std::string::npos == pos)
{
return;
}
std::string k = str.substr(0,pos);
std::string v = str.substr(pos+2);
kvmap.insert(make_pair(k,v));
}
static std::string IntToStr(int code)
{
std::stringstream ss;
ss << code;
return ss.str();
}
static std::string GetCodeDesr(int code)
{
switch(code)
{
case 200:
return "OK";
case 400:
return "Bad Request";
case 404:
return "Not Found";
default:
return "Unknow";
}
}
static std::string SuffixToType(std::string &suffix)
{
return stuffix_map[suffix];
}
};
在对请求处理之前我们再定义Request类来存放用户的请求
在Request类中我们定义了多个方法
首先是对请求行进行,通过解析请求行我们可以拿到 请求方法method 以及请求的 uri 以及协议版本
接着是判断请求方法是否OK,这里我们通过把我们前面拿到的 method与GET POST相比较,用来区分不同的请求类型,如果是POST请求我们则直接将cgi标记设置为true
接着我们对Uri进行解析,通过Uri我们可以解析出用户请求的目标文件path,这里又分为两种情况
POST请求的情况下path就等于Uri
GET请求的情况下我们判断请求是否代参数,如果带参数我们的cgi标志也将被设置为true,同时path的值需要我们对uri进行处理去掉参数,否则的话只是普通GET那么path也将是等于path
接着我们对path路径是否合法进行判断,这里我们使用stat进行判断,他可以用来判断文件是否存在,同时可以判断文件的类型,也可以对文件的权限进行判断,判断是否可执行,这里我们也将会得到文件类型,_resource_suffix,以备后面对请求进行响应
如果请求的路径合法,我们要对请求头部进行解析,通过前面对Http请求格式的介绍,我们知道这一部分是一组一组的kv键值对,kv直接用: 隔开,所以这里我们对每一行的键值对进行解析,将得到的kv值放入map中,以备后面其他部分的使用
当我们将请求头部解析完毕之后,我们接下来要面对的是空行以及请求正文,我们知道POST请求会在正文部分存放参数,而GET方法正文部分为空,所以我们要通过请求的方法来判断是否需要读取正文部分
接着如果是POST请求,我们则需要读取正文部分,但是我们正文读多少呢??所以这里就要借助之前请求头部中的 Content-Length字段了,这个字段的val代表了正文部分的长度,我们可以在之前创建的map中找到
然后将正文部分作为参数赋值给_param,这部分就是针对请求的解析所构建的Request类
class Request
{
public:
std::string _rq_line;
std::string _rq_head;
std::string _blank;
std::string _rq_text;
private:
std::string _method; //请求方法
std::string _uri;
std::string _version; //协议版本
bool _cgi; //是否是cgi
std::string _path;
std::string _param;
int _resource_size;
std::string _resource_suffix;
int _content_length;
std::unordered_map _head_kvmap;
public:
Request()
{
_blank = "\n";
_cgi = false;
_path = WEB_ROOT;
_resource_size = 0;
_content_length = -1;
_resource_suffix = ".html";
}
bool RequestLineParse() //请求行解析
{
LOG(INFO, "Request:" + _rq_line.substr(0,_rq_line.size()-2));
if(_rq_line[0] == ' ')
{
LOG(WARNING, "请求行为空!");
return false;
}
std::stringstream ss(_rq_line);
ss >> _method >> _uri >> _version;
return true;
}
bool IsMethodOK() //判断请求方法是否OK
{
if(strcasecmp(_method.c_str(), "GET") == 0)
return true;
if(strcasecmp(_method.c_str(),"POST") == 0)
{
_cgi = true;
return true;
}
return false;
}
void UriParse()
{
if(strcasecmp(_method.c_str(), "GET") == 0)
{
std::size_t pos = _uri.find('?');
if(std::string::npos != pos)
{
_cgi = true;
_path += _uri.substr(0,pos);
_param += _uri.substr(pos+1);
}
else
{
_path += _uri;
}
}
else
{
_path += _uri;
}
if(_path[_path.size()-1] == '/')
{
_path += HOME_PAGE;
}
}
bool IsPathOK()
{
struct stat st;
if(stat(_path.c_str(), &st) < 0)
{
LOG(WARNING, "Path Not Found");
return false;
}
if(S_ISDIR(st.st_mode))
{
_path += '/';
_path += HOME_PAGE;
}
else
{
if((st.st_mode & S_IXUSR ) || (st.st_mode & S_IXGRP) || (st.st_mode & S_IXOTH))
{
_cgi = true;
}
}
_resource_size = st.st_size;
std::size_t pos = _path.rfind(".");
if(std::string::npos != pos)
{
_resource_suffix = _path.substr(pos);
}
return true;
}
bool RequestHeadParse()
{
std::size_t begin = 0;
while(begin < _rq_head.size())
{
std::size_t pos = _rq_head.find('\n', begin);
if(std::string::npos == pos)
{
break;
}
std::string substr = _rq_head.substr(begin, pos-begin);
if(substr.empty())
{
break;
}
else
{
Tools::MakeKV(_head_kvmap, substr);
}
begin = pos + 1;
}
return true;
}
bool IsNeedRecvText()
{
if(strcasecmp(_method.c_str(),"POST")==0)
{
return true;
}
return false;
}
bool IsCgi()
{
return _cgi;
}
int GetResourceSize()
{
return _resource_size;
}
void SetResourceSize(int rs)
{
_resource_size = rs;
}
std::string& GetSuffix()
{
return _resource_suffix;
}
void SetSuffix(std::string suffix)
{
_resource_suffix = suffix;
}
std::string& GetPath()
{
return _path;
}
void SetPath(std::string path)
{
_path = path;
}
int GetContentLength()
{
std::string cl = _head_kvmap["Content-Length"];
if(!cl.empty())
{
std::stringstream ss(cl);
ss >> _content_length;
}
return _content_length;
}
std::string& GetParam()
{
return _param;
}
};
接着对于上面我们介绍的用于解析请求的Request类,我们需要给它提供请求报文它才能做相应的解析,所以这里我们再创建一个Connect类来从sock文件描述符读取内容,同时在相应处理完毕之后发送反馈给客户端
Connect类:
通过调用RecvOneLine函数可以每次读取一行的数据来供Request类来进行解析,这里要考虑到有些浏览器的换行是/n换行,有的是/r换行,有的是/r/n换行,这里进行统一化为/n
RecvRequestHead函数进行请求头部的读取
RecvRequestText函数进行正文部分的读写
SendRespons函数可以将对于Request的响应反馈给用户,但是这里也分两种情况
非cgi:正文部分直接将用户请求的html文件内容反馈给用户即可
class Connect
{
private:
int _sock;
public:
Connect(int sock)
{
_sock = sock;
}
~Connect()
{
close(_sock);
}
int RecvOneLine(std::string& line)
{
char c = 'c';
while(c != '\n')
{
//ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t size = recv(_sock, &c, 1, 0);
if(size > 0)
{
if(c == '\r')
{
recv(_sock, &c, 1, MSG_PEEK);
if(c == '\n')
{
recv(_sock, &c, 1, 0);
}
else
{
c = '\n';
}
}
line.push_back(c);
}
else
{
break;
}
}
return line.size();
}
void RecvRequestHead(std::string& head)
{
head = "";
std::string line;
while(line != "\n")
{
line = "";
RecvOneLine(line);
head += line;
}
}
void RecvRequestText(std::string& text, int Length, std::string& param)
{
char c;
for(int i = 0; i < Length; ++i)
{
recv( _sock, &c, 1, 0);
text.push_back(c);
}
param = text;
}
void SendResponse(Request* &rq, Response* &rsp, bool cgi)
{
std::string& rsp_line = rsp->_rsp_line;
std::string& rsp_head = rsp->_rsp_head;
std::string& rsp_blank = rsp->_blank;
send(_sock, rsp_line.c_str(), rsp_line.size(), 0);
send(_sock, rsp_head.c_str(), rsp_head.size(), 0);
send(_sock, rsp_blank.c_str(), rsp_blank.size(), 0);
if(cgi)
{
//cgi处理
std::string& rsp_text = rsp->_rsp_text;
send(_sock, rsp_text.c_str(), rsp_text.size(), 0);
}
else
{
int &fd = rsp->_fd;
sendfile(_sock, fd, NULL, rq->GetResourceSize());
}
}
};
Response类
用来存放响应报文的一个类
包括用来构建响应首行的MakeResponseLine方法,用来构建响应头部的MakeResponseHead方法,用来打开用户请求文件的OpenResource类
class Response
{
public:
std::string _rsp_line;
std::string _rsp_head;
std::string _blank;
std::string _rsp_text;
int _code;
int _fd;
public:
Response()
{
_blank = "\n";
_code = OK;
_fd = -1;
}
void MakeResponseLine()
{
_rsp_line = HTTP_VERSION;
_rsp_line += " ";
_rsp_line += Tools::IntToStr(_code);
_rsp_line += " ";
_rsp_line += Tools::GetCodeDesr(_code);
_rsp_line += "\n";
}
void MakeResponseHead(Request* &rq)
{
_rsp_head = "Content-Length: ";
_rsp_head += Tools::IntToStr(rq->GetResourceSize());
_rsp_head += "\n";
_rsp_head += "Content-Type: ";
_rsp_head += Tools::SuffixToType(rq->GetSuffix());
_rsp_head += "\n";
}
void OpenResource(Request* &rq)
{
_fd = open((rq->GetPath()).c_str(), O_RDONLY);
}
~Response()
{
if(_fd > 0)
{
close(_fd);
}
}
};
接着就是Entry类作为解析过程中的一个“场所”,所有的解析过程都是在Entry类中调用
HandlerRequest函数作为之前线程创建的入口函数,它的内部承载了整个请求从接收到解析到响应到反馈给用户的整个过程
首先创建Request,Connet,Response三个对象,然后调用前面的各个部分的模块函数进行处理
MakeResponseNonCgi以及MakeResponseCgi两个函数实现对非cgi和cgi模式两种不同情况下的响应报文的构建
在cgi模式下因为响应的正文内容为cgi程序的处理结果,所以这里需要将用户请求中的参数交给对应的cgi程序去运行,然后将处理结果作为响应正文反馈给用户,所以这里使用fock函数,然后打开两个pipe用于父子进程通讯,父进程通过input管道向子进程传输参数,子进程在被fock成功之后先用dup2函数将output和input文件描述符指定为标准输出和标准输入,再 使用execl进行程序替换,然后在cgi程序中通过读取标准输入就可以读取到父进程发送给自己的参数,这里父进程发送完毕参数之后会将input文件描述符关闭,所以这里子进程(也就是cgi程序)在读到0的时候就意味着参数读取完毕了,然后将处理结果发送给标准输出,父进程这边就可以通过output管道读取到子进程的处理结果,然后将结果作为响应报文的正文反馈给用户
class Entry
{
public:
static void MakeResponseNonCgi(Connect* &conn, Request* &rq, Response* &rsp)
{
rsp->MakeResponseLine();
rsp->MakeResponseHead(rq);
rsp->OpenResource(rq);
conn->SendResponse(rq, rsp, false);
}
static void MakeResponseCgi(Connect* &conn, Request* &rq, Response* &rsp)
{
int &code = rsp->_code;
int input[2];
int output[2];
std::string param = rq->GetParam();
std::string &rsp_text = rsp->_rsp_text;
pipe(input);
pipe(output);
pid_t pid = fork();
if(pid < 0)
{
code = SERVER_ERROR;
return;
}
else if(pid == 0)
{
//child
close(input[1]);
close(output[2]);
const std::string &path = rq->GetPath();
dup2(input[0], 0);
dup2(output[1], 1);
execl(path.c_str(), path.c_str(), NULL);
exit(1);
}
else
{
//parent
close(input[0]);
close(output[1]);
size_t size = param.size();
size_t total = 0;
size_t curr = 0;
const char *p = param.c_str();
while( total < size &&(curr = write(input[1], p + total, size - total)) > 0 )
{
total += curr;
}
close(input[1]);
char c;
while(read(output[0], &c, 1) > 0)
{
rsp_text.push_back(c);
}
waitpid(pid, NULL, 0);
close(output[0]);
rsp->MakeResponseLine();
rq->SetResourceSize(rsp_text.size());
rsp->MakeResponseHead(rq);
conn->SendResponse( rq, rsp, true);
}
}
static void MakeResponse(Connect* &conn, Request* &rq, Response* &rsp)
{
if(rq->IsCgi())
{
MakeResponseCgi(conn, rq, rsp);
}
else
{
MakeResponseNonCgi(conn, rq, rsp);
}
}
static void Process404(Connect* &conn, Request* &rq, Response* rsp)
{
std::string path = WEB_ROOT;
path += "/";
path += PAGE_404;
struct stat st;
stat(path.c_str(),&st);
rq->SetResourceSize(st.st_size);
rq->SetSuffix(".html");
rq->SetPath(path);
MakeResponseNonCgi(conn, rq, rsp);
}
static void HandlerError(Connect* &conn, Request* &rq, Response* &rsp)
{
int &code = rsp->_code;
switch(code){
case 400:
Process404(conn, rq, rsp);
break;
case 404:
Process404(conn, rq, rsp);
break;
case 500:
break;
case 503:
break;
}
}
static void* HandlerRequest(int sock)
{
Connect* conn = new Connect(sock);
Request* rq = new Request();
Response* rsp = new Response();
int &code = rsp->_code;
conn->RecvOneLine(rq->_rq_line);
if((rq->RequestLineParse()) == false )
{
LOG(ERROR,"Error Request");
code = BAD_REQUEST;
goto end;
}
if(!rq->IsMethodOK())
{
conn->RecvRequestHead(rq->_rq_head);
code = BAD_REQUEST;
goto end;
}
rq->UriParse();
if(!rq->IsPathOK())
{
conn->RecvRequestHead(rq->_rq_head);
code = NOT_FOUND;
goto end;
}
LOG(INFO,"Request Path Is OK!!");
conn->RecvRequestHead(rq->_rq_head);
if(rq->RequestHeadParse())
{
LOG(INFO,"Request Head Parse Succes")
}
else
{
code = BAD_REQUEST;
goto end;
}
if(rq->IsNeedRecvText())
{
conn->RecvRequestText(rq->_rq_text, rq->GetContentLength(),rq->GetParam());
}
MakeResponse(conn, rq, rsp);
end:
if(code!=OK)
{
HandlerError(conn, rq, rsp);
}
LOG(INFO,"Make Response Succes!!");
delete conn;
delete rq;
delete rsp;
}
};
以上部分就是创建线程之后进行请求处理响应的过程