自主HttpServer实现(C++实战项目)
Posted 林慢慢脑瓜子嗡嗡的
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自主HttpServer实现(C++实战项目)相关的知识,希望对你有一定的参考价值。
文章目录
项目介绍
该项目是一个基于Http和Tcp协议自主实现的WebServer,用于实现服务器对客户端发送过来的GET和POST请求的接收、解析、处理,并返回处理结果给到客户端。该项目主要背景知识涉及C++、网络分层协议栈、HTTP协议、网络套接字编程、CGI技术、单例模式、多线程编程、线程池等。
项目源码:Click
CGI技术
CGI技术可能大家比较陌生,单拎出来提下。
概念
CGI(通用网关接口,Common Gateway Interface)是一种用于在Web服务器上执行程序并生成动态Web内容的技术。CGI程序可以是任何可执行程序,通常是脚本语言,例如Perl或Python。
CGI技术允许Web服务器通过将Web请求传递给CGI程序来执行任意可执行文件。CGI程序接收HTTP请求,并生成HTTP响应以返回给Web服务器,最终返回给Web浏览器。这使得Web服务器能够动态地生成网页内容,与静态html文件不同。CGI程序可以处理表单数据、数据库查询和其他任务,从而实现动态Web内容。一些常见的用途包括创建动态网页、在线购物车、用户注册、论坛、网上投票等。
原理
通过Web服务器将Web请求传递给CGI程序,CGI程序处理请求并生成响应,然后将响应传递回Web服务器,最终返回给客户端浏览器。这个过程可以概括为:
- 客户端发送HTTP请求到Web服务器。
- Web服务器检查请求类型,如果是CGI请求,Web服务器将环境变量和请求参数传递给CGI程序,并等待CGI程序的响应。
- CGI程序接收请求参数,并执行相应的操作,例如读取数据库或处理表单数据等。
- CGI程序生成HTTP响应,将响应返回给Web服务器。
- Web服务器将响应返回给客户端浏览器。
在这个过程中,Web服务器和CGI程序之间通过标准输入和标准输出(建立管道并重定向到标准输入输出)进行通信。Web服务器将请求参数通过环境变量传递给CGI程序,CGI程序将生成的响应通过标准输出返回给Web服务器。此外,CGI程序还可以通过其他方式与Web服务器进行通信,例如通过命令行参数或文件进行交互。
设计框架
日志文件
用于记录下服务器运行过程中产生的一些事件。日志格式如下:
日志级别说明:
- INFO: 表示正常的日志输出,一切按预期运行。
- WARNING: 表示警告,该事件不影响服务器运行,但存在风险。
- ERROR: 表示发生了某种错误,但该事件不影响服务器继续运行。
- FATAL: 表示发生了致命的错误,该事件将导致服务器停止运行。
文件名称和行数可以通过C语言中的预定义符号__FILE__
和__LINE__
,分别可以获取当前文件的名称和当前的行数。
#define INFO 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
// #将宏参数level转为字符串格式
#define LOG(level, message) Log(#level, message, __FILE__, __LINE__)
TCPServer
思路是:创建一个TCP服务器,并通过初始化、绑定和监听等步骤实现对外服务。
具体实现中,单例模式通过一个名为GetInstance
的静态方法实现,该方法首先使用pthread_mutex_t保证线程安全,然后使用静态变量 _svr指向单例对象,如果 _svr为空,则创建一个新的TcpServer对象并初始化,最后返回 _svr指针。由于 _svr是static类型的,因此可以确保整个程序中只有一个TcpServer实例。
Socket
方法用于创建一个监听套接字,Bind
方法用于将端口号与IP地址绑定,Listen
方法用于将监听套接字置于监听状态,等待客户端连接。Sock
方法用于返回监听套接字的文件描述符。
#define BACKLOG 5
class TcpServer
private:
int _port; // 端口号
int _listen_sock; // 监听套接字
static TcpServer* _svr; // 指向单例对象的static指针
private:
TcpServer(int port)
:_port(port)
,_listen_sock(-1)
TcpServer(const TcpServer&) = delete;
TcpServer* operator=(const TcpServer&) = delete;
public:
static TcpServer* GetInstance(int port)// 单例
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
if (_svr == nullptr)
pthread_mutex_lock(&mtx);
if (_svr == nullptr)// 为什么要两个if? 原因:当首个拿锁者完成了对象创建,之后的线程都不会通过第一个if了,而这期间阻塞的线程开始唤醒,它们则需要靠第二个if语句来避免再次创建对象。
_svr = new TcpServer(port);
_svr -> InitServer();
pthread_mutex_unlock(&mtx);
return _svr;
void InitServer()
Socket(); // 创建
Bind(); // 绑定
Listen(); // 监听
LOG(INFO, "TcpServer Init Success");
void Socket() // 创建监听套接字
_listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (_listen_sock < 0)
LOG(FATAL, "socket error!");
exit(1);
int opt = 1;// 将 SO_REUSEADDR 设置为 1 将允许在端口上快速重启套接字
setsockopt(_listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
LOG(INFO, "creat listen_sock 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;
if (bind(_listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
LOG(FATAL, "bind error");
exit(2);
LOG(INFO, "port bind listen_sock success");
void Listen() // 监听
if (listen(_listen_sock, BACKLOG) < 0) // 声明_listen_sock处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果接收到更多的连接请求就忽略
LOG(FATAL, "listen error");
exit(3);
LOG(INFO, "listen listen_sock success");
int Sock() // 获取监听套接字fd
return _listen_sock;
~TcpServer()
if (_listen_sock >= 0)
close(_listen_sock);
;
// 单例对象指针初始化
TcpServer* TcpServer::_svr = nullptr;
任务类
// 任务类
class Task
private:
int _sock; // 通信套接字
CallBack _handler; // 回调函数
public:
Task()
~Task()
Task(int sock) // accept建立连接成功产生的通信套接字sock
:_sock(sock)
// 执行任务
void ProcessOn()
_handler(_sock); //_handler对象的运算符()已经重装,直接调用重载的()
;
初始化与启动HttpServer
这部分包含一个初始化服务器的方法InitServer()和一个启动服务器的方法Loop()。其中InitServer()函数注册了一个信号处理函数,忽略SIGPIPE信号(避免写入崩溃)。而Loop()函数则通过调用TcpServer类的单例对象获取监听套接字,然后通过accept()函数等待客户端连接,每当有客户端连接进来,就创建一个线程来处理该客户端的请求,并把任务放入线程池中。这里的Task是一个简单的封装,它包含一个处理客户端请求的成员函数,该成员函数读取客户端请求,解析请求,然后调用CGI程序来执行请求,最后将响应发送给客户端。
#define PORT 8081
class HttpServer
private:
int _port;// 端口号
public:
HttpServer(int port)
:_port(port)
~HttpServer()
// 初始化服务器
void InitServer()
signal(SIGPIPE, SIG_IGN); // 直接粗暴处理cgi程序写入管道时崩溃的情况,忽略SIGPIPE信号,避免因为一个被关闭的socket连接而使整个进程终止
// 启动服务器
void Loop()
LOG(INFO, "loop begin");
TcpServer* tsvr = TcpServer::GetInstance(_port); // 获取TCP服务器单例对象
int listen_sock = tsvr->Sock(); // 获取单例对象的监听套接字
while(true)
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
socklen_t len = sizeof(peer);
int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);// 跟客户端建立连接
if (sock < 0)
continue;
// 打印客户端信息
std::string client_ip = inet_ntoa(peer.sin_addr);
int client_port = ntohs(peer.sin_port);
LOG(INFO, "get a new link:[" + client_ip + ":" + std::to_string(client_port) + "]");
// 搞个线程池,代替下面简单的线程分离方案
// 构建任务并放入任务队列
Task task(sock);
ThreadPool::GetInstance()->PushTask(task);
;
HTTP请求结构
将HTTP请求封装成一个类,这个类当中包括HTTP请求的内容、HTTP请求的解析结果以及是否需要使用CGI模式的标志位。后续处理请求时就可以定义一个HTTP请求类,读取到的HTTP请求的数据就存储在这个类当中,解析HTTP请求后得到的数据也存储在这个类当中。
class HttpRequest
public:
// Http请求内容
std::string _request_line; // 请求行
std::vector<std::string> _request_header; // 请求报头
std::string _blank; // 空行
std::string _request_body; // 请求正文
// 存放解析结果
std::string _method; // 请求方法
std::string _uri; // URI
std::string _version; // 版本号
std::unordered_map<std::string, std::string> _header_kv; // 请求报头的内容是以键值对的形式存在的,用hash保存
int _content_length; // 正文长度
std::string _path; // 请求资源的路径
std::string _query_string; // URI携带的参数
// 是否使用CGI
bool _cgi;
public:
HttpRequest()
:_content_length(0) // 默认请求正文长度为0
,_cgi(false) // 默认不适用CGI模式
~HttpRequest()
;
HTTP响应结构
类似的,HTTP响应也封装成一个类,这个类当中包括HTTP响应的内容以及构建HTTP响应所需要的数据。构建响应需要使用的数据就存储在这个类当中,构建后得到的响应内容也存储在这个类当中。
class HttpResponse
public:
// Http响应内容
std::string _status_line; // 状态行
std::vector<std::string> _response_header; // 响应报头
std::string _blank; // 空行
std::string _response_body; // 响应正文(如果CGI为true(即Get带_query_string或者Post),响应正文才存在)
// 所需数据
int _status_code; // 状态码
int _fd; // 响应文件的fd
int _size; // 响应文件的大小
std::string _suffix; // 响应文件的后缀
public:
HttpResponse()
:_blank(LINE_END)
,_status_code(OK)
,_fd(-1)
,_size(0)
~HttpResponse()
;
线程回调
该回调函数实际上是一个函数对象,其重载了圆括号运算符“()”。当该函数对象被调用时,会传入一个int类型的套接字描述符作为参数,代表与客户端建立的连接套接字。该函数对象内部通过创建一个EndPoint对象来处理该客户端发来的HTTP请求,包括读取请求、处理请求、构建响应和发送响应。处理完毕后,该连接套接字将被关闭,EndPoint对象也会被释放。
class CallBack
public:
CallBack()
~CallBack()
// 重载运算符 ()
void operator()(int sock)
HandlerRequest(sock);
void HandlerRequest(int sock)
LOG(INFO, "HandlerRequest begin");
EndPoint* ep = new EndPoint(sock);
ep->RecvHttpRequest(); //读取请求
if (!ep->IsStop())
LOG(INFO, "RecvHttpRequest Success");
ep->HandlerHttpRequest(); //处理请求
ep->BulidHttpResponse(); //构建响应
ep->SendHttpResponse(); //发送响应
if (ep->IsStop())
LOG(WARNING, "SendHttpResponse Error, Stop Send HttpResponse");
else
LOG(WARNING, "RecvHttpRequest Error, Stop handler Response");
close(sock); //响应完毕,关闭与该客户端建立的套接字
delete ep;
LOG(INFO, "handler request end");
;
EndPoint类
EndPoint主体框架
EndPoint类中包含三个成员变量:
- sock:表示与客户端进行通信的套接字。
- http_request:表示客户端发来的HTTP请求。
- http_response:表示将会发送给客户端的HTTP响应。
- _stop:是否异常停止本次处理
EndPoint类中主要包含四个成员函数:
- RecvHttpRequest:读取客户端发来的HTTP请求。
- HandlerHttpRequest:处理客户端发来的HTTP请求。
- BuildHttpResponse:构建将要发送给客户端的HTTP响应。
- SendHttpResponse:发送HTTP响应给客户端。
//服务端EndPoint
class EndPoint
private:
int _sock; //通信的套接字
HttpRequest _http_request; //HTTP请求
HttpResponse _http_response; //HTTP响应
bool _stop; //是否停止本次处理
public:
EndPoint(int sock)
:_sock(sock)
//读取请求
void RecvHttpRequest();
//处理请求
void HandlerHttpRequest();
//构建响应
void BuildHttpResponse();
//发送响应
void SendHttpResponse();
~EndPoint()
;
读取HTTP请求
读取HTTP请求的同时可以对HTTP请求进行解析,这里我们分为五个步骤,分别是读取请求行、读取请求报头和空行、解析请求行、解析请求报头、读取请求正文。
// 读取请求:如果请求行和请求报头正常读取,那先解析请求行和请求报头,然后读取请求正文
void RecvHttpRequest()
if (!RecvHttpRequestLine() && !RecvHttpRequestHeader())// 请求行与请求报头读取均正常读取
ParseHttpRequestLine();
ParseHttpRequestHeader();
RecvHttpRequestBody();
处理HTTP请求
首先判断请求方法是否为GET或POST,如果不是则返回错误信息;然后判断请求是GET还是POST,设置对应的cgi、路径和查询字符串;接着拼接web根目录和请求资源路径,并判断路径是否以/结尾,如果是则拼接index.html;获取请求资源文件的属性信息,并根据属性信息判断是否需要使用CGI模式处理;获取请求资源文件的后缀,进行CGI或非CGI处理。
// 处理请求
void HandlerHttpRequest()
auto& code = _http_response._status_code;
//非法请求
if (_http_request._method != "GET" && _http_request._method != "POST")
LOG(WARNING, "method is not right");
code = BAD_REQUEST;
return;
// 判断请求是get还是post,设置cgi,_path,_query_string
if (_http_request._method == "GET")
size_t pos = _http_request._uri.find('?');
if (pos != std::string::npos)// uri中携带参数
// 切割uri,得到客户端请求资源的路径和uri中携带的参数
Util::CutString(_http_request._uri, _http_request._path, _http_request._query_string, "?");
LOG(INFO, "GET方法分割路径和参数");
_http_request._cgi = true;// 上传了参数,需要使用CGI模式
else // uri中没有携带参数
_http_request._path = _http_request._uri;// uri即是客户端请求资源的路径
else if (_http_request._method == "POST")
_http_request._path = _http_request._uri;// uri即是客户端请求资源的路径
_http_request._cgi = true; // 上传了参数,需要使用CGI模式
else
// 只是为了代码完整性
// 为请求资源路径拼接web根目录
std::string 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; // 拼接上该目录下的index.html
LOG(INFO, _http_request._path);
//获取请求资源文件的属性信息
struct stat st;
if (stat(_http_request._path.c_str(), &st) == 0) // 属性信息获取成功,说明该资源存在
if (S_ISDIR(st.st_mode)) // 该资源是一个目录
_http_request._path += "/"; // 以/结尾的目录前面已经处理过了,这里处理不是以/结尾的目录情况,需要拼接/
_http_request._path += HOME_PAGE; // 拼接上该目录下的index.html
stat(_http_request._path.c_str(), &stC++实战项目机房预约管理系统