日志信息类实现
日志等级
- INFO: 正常执行
- WARNING: 告警信息
- ERROR: 部分逻辑运行出错,但不会影响整个服务的正常运行
- FATAL: 致使整个服务直接崩掉的错误
#pragma once
#include <iostream>
#define INFO 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
#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;
}
套接字服务类
套接字服务类
- 编写创建套接字,绑定套接字,获取连接…等业务逻辑
- 设计此类为单例对象,使用懒汉模式实现单例对象,外部调用getinstance() 获取当前单例对象单例对象:需将构造私有化,且将拷贝构造, 赋值运算符重载俩个函数禁掉
#define BACKLOG 5 // listen 函数第二个参数 class TcpServer { private: int _port; int listen_sock; static TcpServer * svr; private: TcpServer(int port) : _port(port), listen_sock(-1) {} TcpServer(const TcpServer& s) = delete; TcpServer& operator= (const TcpServer& s) = delete; 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(); } pthread_mutex_unlock(&lock); } return svr; } void InitServer() // 套接字初始化 { Socket(); Bind(); Listen(); LOG(INFO, "tcp_server init ... success "); } int Sock() { return listen_sock; } // 创建套接字 void Socket() { listen_sock = socket(AF_INET, SOCK_STREAM, 0); if (listen_sock < 0) { LOG(FATAL, "socket error!"); exit(1); } int opt = 1; setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // socket 端口重用 我们的服务异常终止时 是先进入着time_wait状态的 不能立马重启 就会有很大问题 LOG(INFO, "create socket ... success "); } // 绑定服务端口 void Bind() { struct sockaddr_in local; memset(&local, 0, sizeof local); local.sin_family = AF_INET; local.sin_port = htons(_port); // 主机序列转网络序列 local.sin_addr.s_addr = INADDR_ANY; // 云服务器上不能直接绑定公网IP if (bind(listen_sock, (struct sockaddr *)&local, sizeof(local)) < 0) { LOG(FATAL,"bind error!"); exit(2); } LOG(INFO, "bind socket ... success "); } // 将服务置于就绪状态 等待连接 void Listen() { if (listen(listen_sock, BACKLOG) < 0) { LOG(FATAL, "listen error!"); exit(3); } LOG (INFO, "listen socket ... success "); } ~TcpServer() { if (listen_sock >= 0) { close(listen_sock); } } }; TcpServer* TcpServer::svr = nullptr;
说明:
1.服务端不能绑定公网ip,通常使用INADDR_ANY(netinet.h里面定义的宏值),作用是使用所有当前服务器能用的ip接收请求,通常服务端都是服务器,而服务器不止配备了一张网卡,如果只绑定一个ip的话,就只不能将多网卡的优势发挥
2.PTHREAD_MUTEX_INITIALIZER 可以用于初始化全局或静态锁,且该互斥锁不需要我们自己销毁
3.使用setsockopt接口解决,服务器服务因为某种原因异常终止了不能立即重启服务的问题; 因为服务异常终止时,主打断开连接的一方最后会处于TIME_WAIT状态,即服务器服务异常终止后,连接会被保持一段时间,而导致服务器端口被一直占用;使用该接口可以使得对应端口进行复用
4.双判断搞高单例对象获取效率:只有第一次获取该对象的时候, 需要加锁进行初始化,后续都只需直接获取该对象即可,所以我们可以多做一层判断,如果该单例对象已经不为空了, 我们就不需要再进行加锁判断了我们访问某个资源时,实际上uri部分是划分为俩部分的(path?query_string) query_string 即一些与任务处理相关的参数
-
class HttpRequest { public: std::string request_line; // 请求行 std::vector<std::string> request_header; // http报头 std::string blank; // 空行 std::string request_body; // http正文字段 // 保存请求行信息 std::string method; std::string uri; // path?query_string std::string version; // uri std::string path; std::string query_string; // 保存报头的key: value std::unordered_map<std::string, std::string> header_kv; int content_length; bool Cgi; int size; // 请求的资源的大小 std::string suffix; // 所求资源后缀 public: HttpRequest() :content_length(0), Cgi(false) {} ~HttpRequest(){} };
-
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; public: HttpResponse() :blank(END_LINE), status_code(OK) {} ~HttpResponse(){} };
http服务端主体逻辑
- 不断获取连接(循环),将连接及其往后的业务处理打包成任务丢进任务队列里
-
class HttpServer { private: int port; // 服务端口 默认为8081 bool stop; // 判断服务是否停止 public: HttpServer(int _port = PORT) : port(_port), stop(false) {} void InitServer() { // 该进程忽略SIGPIPE信号,不然会在线程进行写入时,对端忽然间关闭连接, 会导致server崩溃 signal(SIGPIPE, SIG_IGN); } void Loop() { LOG(INFO, "Http_server Loop Begin "); TcpServer* tsvr = TcpServer::getinstance(port); while(!stop) { struct sockaddr_in peer; socklen_t len = sizeof(peer); int sock = accept(tsvr->Sock(), (struct sockaddr*)&peer, &len); // 获取连接 if (sock < 0){ continue; // 获取连接失败 继续获取 } LOG(INFO, "Get a new link "); Task task(sock); // 将任务推送到任务队列里 ThreadPool::GetInstance()->PushTask(task); } } ~HttpServer() {} };
任务类
只是简单抽离出来的一个概念,算是线程池和服务端之间的一个联系的桥梁,将任务塞到任务队列里,线程拿到任务,就可以调用该任务的回调函数,拿到任务的线程就可以开始工作了
-
class Task { private: int sock; Callback handler; public: Task() {} Task(int _sock) : sock(_sock) { } // 处理任务 void ProcessOn() { handler(sock); } ~Task() {} };
线程池及任务队列
懒汉模式下实现的线程池
- 先创建好一批线程,这批线程创建好之后,都会在任务队列下进行等待(准确来说应该是描述该任务队列资源状况的条件变量下),只要被插入到队列里,就会唤醒其中一个线程(通过改变条件变量状态的方式通知),而每个线程又是互斥的,所以在线程访问任务队列时,需要有互斥锁的保护
-
#pragma once #include <iostream> #include <pthread.h> #include <queue> #include "Task.hpp" #include "Log.hpp" #define NUM 6 class ThreadPool{ private: std::queue<Task> task_queue; bool stop; int num; pthread_mutex_t lock; pthread_cond_t cond; static ThreadPool* single_instance; private: ThreadPool(int _num = NUM): num (_num), stop(false) { pthread_mutex_init(&lock, nullptr); pthread_cond_init(&cond, nullptr); } ThreadPool (const ThreadPool& tp) = delete; ThreadPool& operator=(const ThreadPool & tp) = delete; public: static ThreadPool* GetInstance() { static pthread_mutex_t _mutex = PTHREAD_MUTEX_INITIALIZER; if (single_instance == nullptr) { pthread_mutex_lock(&_mutex); if(single_instance == nullptr) { single_instance = new ThreadPool(); single_instance->InitThreadPool(); } pthread_mutex_unlock(&_mutex); } return single_instance; } bool TaskQueueIsEmpty() { return task_queue.empty(); } void Lock() { pthread_mutex_lock(&lock); } void Unlock() { pthread_mutex_unlock(&lock); } void ThreadWakeup() { pthread_cond_signal(&cond); } void ThreadWait() { pthread_cond_wait(&cond, &lock); } static void * ThreadRoutine(void *args) { ThreadPool * tp = (ThreadPool*) args; while(true){ Task t; tp->Lock(); while (tp->TaskQueueIsEmpty()){ tp->ThreadWait(); } 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, "ThreadPool Init error"); return false; } LOG(INFO, "ThreadPool Init success"); return true; } } void PushTask(Task& task) { Lock(); task_queue.push(task); Unlock(); ThreadWakeup(); } void PopTask(Task& task) { task = task_queue.front(); task_queue.pop(); } ~ThreadPool() { pthread_mutex_destroy(&lock); pthread_cond_destroy(&cond); } }; ThreadPool* ThreadPool::single_instance = nullptr;
//业务端EndPoint
class EndPoint{
private:
HttpRequest _http_request; //HTTP请求
HttpResponse _http_response; //HTTP响应
int _sock; //通信的套接字
bool stop; // 判断业务是是否需要终止
public:
EndPoint(int sock)
:_sock(sock)
{}
//读取请求
void RecvHttpRequest();
//处理请求
void HandlerHttpRequest();
//构建响应
void BuildHttpResponse();
//发送响应
void SendHttpResponse();
~EndPoint()
{}
};
接收和解析请求报文(RecvHttpRequest)
再将其细分
- 读取请求行
- 读取请求报头
- 解析请求行
- 解析请求报头
- 读取请求正文(post)
读取请求行 RecvHttpRequestLine
因为HTTP请求报文都是以一行为分割符的,所以我们可以直接按行读,将请求行和请求报文进行读取,但是每行的结束可能会有以下3种情况: \r\n \r \n
我们需要将其都统一处理成同一种格式
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)
{
// 处理\r 和 \r\n的情况 将这俩种情况都转化成 \n
if(ch == '\r')
{
// 因为不能判断\r 后面是否具有\n 所以需要先偷窥一下\r 后面是否具有\n
recv(sock, &ch, 1, MSG_PEEK);
if (ch == '\n')
{
// 窥探成功 \r 后面具有 \n
recv(sock, &ch, 1, 0); // 取出\n
}else{
ch = '\n' ; //将\r 处理成 \n
}
}
// \n 或者是正常字符
out.push_back(ch);
}
else if (s == 0)
{
// 客户端断开连接
return 0;
}
else{
return -1; // 出现错误
}
}
}
说明:
1.当我们按字符读,读取到\r时,我们不能贸然读取下一个字符,不然可能会导致下一行数据不完整,于是我们就可以使用recv函数进行窥探下一个字符是什么(选项填MSG_PEEK)表示窥探下一个字符,作用是查看该字符,但不从中取走该字符
RecvHttpRequestLine方法具体实现
2.因为\n在解析字符串并没有什么作用,所以可以在存储时将其去掉,前面不去掉的原因是为了是按行读取的逻辑更清晰
bool RecvHttpRequestLine() // 读取请求行
{
auto& line = http_request.request_line;
if( Util::ReadLine(sock, line) <= 0){
stop =true; // 读取请求行出现错误,停止后续的操作, 直接将该套接字关掉即可
}
line.resize(line.size() - 1); // 去掉\n
LOG(INFO, line);
return stop;
}
读请求报头RecvHttpRequestHeader
同上面思路一致,也是按行读取即可,但为了后续分解报头中的属性字段更方便,使用vector将每行数据存储起来;而读取请求报头的停止标志也简单,就是当我们读取到连续的\n时,就说明我们已经读到了空行,就可以停
bool RecvHttpRequestHeader() // 读取报头
{
std::string line;
while(true){
line.clear();
if (Util::ReadLine(sock,line) <= 0){
stop = true;
break;
}
if (line == "\n"){
http_request.blank = line;
break;
}
line.resize(line.size() - 1);
http_request.request_header.push_back(line);
LOG(INFO,line);
}
return stop;
}
解析请求行ParseHttpRequestLine
*将3个字段提取出来
*将请求方法统一装成大写,因为http并没有统一标准的,所以可能有些http报文的请求方法并不是全大写的
*请求行大致格式
1 GET / HTTP/1.1
*请求行分为3个字段,请求方法,URI,HTTP版本,3个字段以空格作为分割符,所以我们切分时通过空格进行切分
void ParseHttpRequestLine()
{
auto & line = http_request.request_line;
std::stringstream ss(line);
ss >> http_request.method >> http_request.uri >> http_request.version;
//std::cout << "debug: " << http_request.method << std::endl;
auto &method = http_request.method;
std::transform(method.begin(), method.end(), method.begin(), ::toupper);
}
解析请求报头ParseHttpRequestHeader
报头中的属性字段都是以这样形式存在的,所以我们可以直接使用": "作为分割符,将报头中的每行解析出来
key: value
具体实现:
void ParseHttpRequestHeader()
{
std::string key;
std::string value;
for( auto &iter : http_request.request_header)
{
if(Util::CutString(iter, key, value, SEP)){
http_request.header_kv.insert({key,value});
}
}
}
// 字符串切割方法
static bool CutString(const std::string &target , std::string &sub1_out, std::string &sub2_out, std::string sep) // sep用&会报错
{
size_t pos = target.find(sep);
if (pos != std::string::npos){
sub1_out = target.substr(0, pos);
sub2_out = target.substr(pos + sep.size());
return true;
}
return false;
}
说明:
*因为字符串切割后面还会用得到,所以将其也放到工具类,设计成一个方法
解析请求正文ParseHttpRequestBody
*因为只有POST才需要从Http报文中提取正文,所以我们可以设计出一个子函数,判断该报文是否需要读取正文,如果需要读取就顺便将报头中的(Content-Length)字段解析出来,方便后续正文数据进行提取
bool IsNeedParseHttpRequestBody() //子函数
{
auto &method = http_request.method;
if(method == "POST"){
auto &header_kv = http_request.header_kv;
auto iter = header_kv.find("Content-Length");
if(iter != header_kv.end()){
LOG(INFO, "POST Method, Content-Length: "+iter->second);
http_request.content_length = atoi(iter->second.c_str());
return true;
}
}
return false;
}
void ParseHttpRequestBody()
{
if(IsNeedParseHttpRequestBody()){
int content_length = http_request.content_length;
auto &body = http_request.request_body;
char ch = 0;
while(content_length){
ssize_t s = recv(sock, &ch, 1, 0);
if(s > 0){
body.push_back(ch);
content_length --;
}
else{
break;
}
}
LOG(INFO, body);
}
}
*构建响应报文 BuildHttpresponse
*构建响应报文其实可以分为俩大部分,一部分是处理上述解析出来的用户请求,另一部分是构建响应报文,所以实际上EndPoint还可以在细分为4部分,这样逻辑可能更清楚一点,但看个人想法,因为觉得是构建响应报文这一类我就将其归进来了
*构建响应报文部分组成:
*构建响应状态行
*构建响应报文(报头,空行,正文)
*处理用户请求
*判断请求方法是否合法,因为本项目只构建了GET和POST方法,所以其他的请求方法都是不合法的
*将web根目录转化为本地路径,判断用户请求资源是否存在
2.1 请求资源不存在,直接返回错误响应(404)
*请求资源存在情况下,继续判断请求资源的类型
3.1 请求的只是网页内容或是目录文件,直接返回对应的静态网页即可(index.html)即可
3.2 请求的是可执行程序,留给CGI机制处理
*CGI机制处理Post请求方法,及其GET请求方法带参情况
void BuildHttpResponse()
{
std::string _path;
struct stat st;
std::size_t found = 0;
auto &code = http_response.status_code;
if(http_request.method != "GET" && http_request.method != "POST" ){
//非法请求
LOG(WARNING, "mothod is not right ");
code = BAD_REQUEST;
goto END;
}
if(http_request.method == "GET"){
ssize_t pos = http_request.uri.find("?");
if (pos != std::string::npos)
{
Util::CutString(http_request.uri, http_request.path, http_request.query_string, "?");
http_request.Cgi = true;
}
else
{
http_request.path = http_request.uri;
}
}
else if (http_request.method == "POST"){
// POST
http_request.path = http_request.uri;
http_request.Cgi = true;
}
else{
// do nothing
}
// 转化uri为网络根目录
_path = http_request.path;
http_request.path = WEB_ROOT;
http_request.path += _path;
if (http_request.path[http_request.path.size() - 1] == '/' ) {
http_request.path += HOME_PAGE; // path后面没有带指定路径 就只是获取当前网站首页
}
//判断请求资源是否存在
if (stat(http_request.path.c_str(), &st) == 0){
//资源存在
//判断资源是什么 1.目录 2.可执行文件 3.资源
if (S_ISDIR(st.st_mode)){
//1.目录文件 将该目录的默认目录首页html 显示当前目录的首页
http_request.path += '/';
http_request.path += HOME_PAGE;
stat(http_request.path.c_str(), &st) ; // 更新一下st
}
if (st.st_mode & S_IXUSR || st.st_mode & S_IXGRP || st.st_mode & S_IXOTH){
//请求的资源是可执行程序 特殊处理
http_request.Cgi = true;
}
http_request.size = st.st_size; //获取对应文件的大小 即响应报文的正文大小
}
else{
//资源不存在
std::string Info = http_request.path;
Info += "NOT_FOUND";
LOG(WARNING, Info);
code = NOT_FOUND;
goto END;
}
found = http_request.path.rfind("."); // 查询获取资源的后缀
if (found == std::string::npos)
{
http_request.suffix = ".html";
}
else{
http_request.suffix = http_request.path.substr(found);
}
std::cout << "path: " << http_request.path << std::endl;
if (http_request.Cgi){
//ProcessCgi
code = ProcessCgi();
}
else {
//ProcessNonCgi -- 返回静态网页 -- 简单的网页返回
code = ProcessNonCgi();
}
END:
BuildHttpResponseHelper();
}
构建响应状态行BuildHttpResponseHelper
状态行包括如下格式:
1 HTTP/1.1 200 OK
状态行通常是由3个字段组成:HTTP版本号, 请求状态码, 请求状态码描诉
所以我们只需将这3个字段拼接起来即可
注:
Code2Desc是将状态码转成字符描诉的方法,这里只展示大体思路,如果想了解细节,可以后续去浏览代码源码
void BuildHttpResponseHelper()
{
//构建状态行
auto &code = http_response.status_code;
auto &status_line = http_response.status_line;
status_line = HTTP_VERSION;
status_line += " ";
status_line += std::to_string(code);
status_line += " ";
status_line += Code2Desc(code);
status_line += END_LINE;
//出现差错时 path并未被构建 或是有错误
std::string path = WEB_ROOT;
path += "/";
switch(code)
{
case OK:
BuildOkResponse();
break;
case BAD_REQUEST:
path += PAGE_400;
HandlerError(path);
break;
case NOT_FOUND:
path += PAGE_404;
HandlerError(path);
break;
case SERVER_ERROR:
path += PAGE_500;
HandlerError(path);
break;
default:
break;
}
}
*构建响应报文HandlerError , BuildOKResponse
HandlerError
主要用于构建错误情况下的报文处理;
这里只提及几点处理细节:
*因为是错误报文,所以Content-Length字段都是“text/html”类型
*错误报文的正文数据是错误码对应的html资源,所以我们构建时需要使用stat函数获取该html网页文件的大小,方便后续发送报文时进行数据传输
*BuildResponse
*报头部分
Content-Type字段使用Suffix2Desc()将文件后缀转成对应的文件类型
Content-Length字段
1.处理机制非CGI: 正文的长度即为http所请求的资源大小
2.处理机制为CGI:正文长度为CGI程序处理之后的结果大小(内容放在repsonsebody)
void HandlerError(std::string page)
{
http_request.Cgi = false;
http_response.fd = open(page.c_str(), O_RDONLY);
if (http_response.fd > 0)
{
struct stat st;
stat(page.c_str(), &st);
std::string line = "Content-Type: text/html";
line += END_LINE;
http_response.response_header.push_back(line);
http_request.size = st.st_size; // 如果这里不更新 size就会为0 因为前面并不会更新 因为请求的资源不存在
line = "Content-Length: ";
line += std::to_string(http_request.size);
line += END_LINE;
http_response.response_header.push_back(line);
}
}
void BuildOkResponse()
{
std::string 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){
line += std::to_string (http_response.response_body.size());
}
else {
line += std::to_string(http_request.size); //Get 不带uri -- 返回静态网页部分
}
line += END_LINE;
http_response.response_header.push_back(line);
}
组装和发送响应报文SendHttpResponse
*非CGI机制处理:错误处理或者是请求静态网页资源,正文数据都在对应打开的文件缓冲区里
CGI机制处理:正文数据都放在http_responsebody字段里
说明:
*非CGI机制处理:其中没有调用write,或者send接口将对应文件里面的数据写入到套接字里面的原因是:套接字,资源文件本质都是一个被打开的文件,都有对应的内核级别的文件缓冲区;我们只需将一个内核文件缓冲区的数据直接拷贝到另一个内核文件缓冲区即可;这也是为什么非CGI机制时,我们不采取将对应文件里面的数据读入到body字段的原因,因为这多了一个步骤,多此一举(且body属于用户级缓冲区),而io效率是很慢的,所以不采取这种方案
void SendHttpResponse()
{
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);
}
send(sock, http_response.blank.c_str(), http_response.blank.size(), 0);
//std::cout << "debug: " << http_response.response_body.c_str() << std::endl;
if (http_request.Cgi){
auto &response_body = http_response.response_body;
int total = 0;
int size = 0;
const char * start = response_body.c_str();
while (total < response_body.size() && (size = send(sock, start + total, response_body.size() - total, 0) ) > 0){
total += size;
}
}
else{
sendfile(sock, http_response.fd, nullptr, http_request.size);
close(http_response.fd);
}
}