基于Linux的C++轻量级web服务器/webserver/httpserver——httpconnect模块介绍
HTTP连接处理详解
背景
服务器和核心功能是完成对HTTP请求报文的解析,并向客户端发出HTTP响应报文。在Httpconnection模块正是要完成上述的功能需求。
为了完成报文解析、资源定位、发送响应等功能需求,该项目一共是写了4个头文件,与此对应的就是4个模块:Httpconnection模块、Httprequest模块、Httpresponse模块以及Buffer缓冲区模块。四个模块之间的相互关系如下图所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wWaMy8LW-1650472188539)(…/images/img1.png)]
Httpconnection类成员变量介绍
Httpconnection是对HTTP连接的抽象,在模块中要定义一些变量保存socket通信客户端的信息:(建立socket连接由其他模块负责,这里只负责通信和关闭通信)一条socket连接就需要一个Httpconnection模块,所以我们使用m_fd唯一的标记它;并且使用m_isClose判断连接是否被关闭,便于调用closeConn关闭客户端连接。一个HTTP连接还需要读写数据,所以给每一个HTTP连接定义一个读缓冲区和一个写缓冲区。在解析请求和响应请求的时候,我们借助Httprequest和Httpresponse完成,所以也需要各种定义一个这两种变量。
private:
int m_fd;
struct sockaddr_in m_addr; // 获取IP和port
bool isClose;
int m_iovCnt;
struct iovec m_iov[2]; // 用于分散写writev的结构体
Buffer m_readBuf;
Buffer m_writeBuf;
Httprequest m_request;
Httpresponse m_response;
此外,还定义了三个static静态成员:isET, srcDir, userCount。类的静态成员不属于某一个类对象,是属于整个类的,所有的类对象都可以访问该静态成员。
isET布尔变量表征所有的Httpconnection对象是否在使用epoll的ET边缘触发模式。
srcDir定位资源的根路径
userCount是当前连接的数量。由于对该变量有多线程读取和写入,需要作线程保护。我们可以使用std::atomic对变量进行线程保护
public:
static bool isET;
static const char *srcDir;
static std::atomic<int> userCount;
Httpconnection任务流程介绍
成员变量的介绍到这里就结束了。下面对Httpconnection模块的执行流程作一个介绍:
完成工作任务的主要是3个函数:
ssize_t readBuffer(int *saveErrno);
bool handleHTTPConn();
ssize_t writeBuffer(int *saveErrno);
readBuffer函数从m_fd对应的socket中读取数据,由于在Buffer缓冲区中我们已经定义好了读取函数recvFd,这里只是简单的对该函数进行封装即可。读取到的HTTP请求报文会保存在之前定义的读取缓冲m_readBuf
handlerHTTPConn整个类执行任务的核心。在该函数中,我们将m_readBuf交由请求解析模块m_request进行报文解析,成功解析后,会调用报文响应模块m_response完成响应报文状态行和首部字段的填写、分散写结构体m_iov的装填等一系列操作。函数执行成功后,将会返回1。
writeBuffer函数是将数据发送到m_fd对应的socket中去。由于需要发送服务器上的文件资源,针对这种情况通常都是使用writev分散写函数将多块内存的数据一同发送到socket对端。当handlerHTTPConn函数执行成功后,m_iov结构体已经填充好了要发送的响应报文(状态行、首部字段、报文主体),此时调用writeBuffer函数就可以将响应报文发送给客户端。
以上便是Httpconnection模块的工作流程。当然,在执行任务之前,需要调用initHTTPConn函数对成员变量进行初始化。此外,还定义了一些外部接口函数,以供访问内部的成员变量。
Httpconnection类的实现:
#ifndef __HTTPCONNECT_H__
#define __HTTPCONNECT_H__
#include <arpa/inet.h> //sockaddr_in
#include <sys/uio.h> //readv/writev
#include <iostream>
#include <sys/types.h>
#include <assert.h>
#include <atomic>
#include "httprequest.h"
#include "httpresponse.h"
#include "bufferV2.h"
class Httpconnection
{
public:
Httpconnection();
~Httpconnection();
void initHTTPConn(int socketFd, const sockaddr_in &addr);
//每个连接中定义的对缓冲区的读写接口
ssize_t readBuffer(int *saveErrno);
ssize_t writeBuffer(int *saveErrno);
//关闭HTTP连接的接口
void closeHTTPConn();
//定义处理该HTTP连接的接口,主要分为request的解析和response的生成
bool handleHTTPConn();
//其他方法
const char *getIP() const;
int getPort() const;
int getFd() const;
sockaddr_in getAddr() const;
int writeBytes();
bool isKeepAlive() const;
static bool isET;
static const char *srcDir;
static std::atomic<int> userCount;
private:
int m_fd;
struct sockaddr_in m_addr;
bool isClose;
int m_iovCnt;
struct iovec m_iov[2]; // 用于分散写writev的结构体
Buffer m_readBuf;
Buffer m_writeBuf;
Httprequest m_request;
Httpresponse m_response;
};
#endif
Httprequest类成员的介绍
通过上面对Httpconnection类的介绍,相信你对Httprequest类所需要完成的功能有了一个大概的印象:完成对HTTP请求的解析。
一个HTTP请求由三个部分组成:请求行、首部字段和报文主体。为了判断程序正在解析哪一部分,这里使用状态机技术概念构建我们这个解析类。换句话说,针对每一部分的解析,编写对应的解析函数,根据一个状态标志位,决定在某一个时刻程序应该运行哪一个解析函数。解析状态一共可分为四种:解析请求行、解析首部字段、解析报文主体、结束解析。这里使用了enum枚举类预先定义了解析状态PARSE_STATE
此外,定义了一个哈希表用于保存解析得到的首部字段的详细信息,以及一个用于查找网页的哈希表。
PARSE_STATE m_state;
std::string m_method, m_path, m_version, m_body;
std::unordered_map<std::string, std::string> m_header; // 首部字段的哈希表
static const std::unordered_set<std::string>DEFAULT_HTML; // 用于查找网页的哈希表
Httprequest任务流程介绍
Httprequest类对外最重要的一个函数:parse
bool parse(Buffer &buff); //解析HTTP请求
他在内部调用多个私有的解析函数,完成对传入的Buffer进行解析
几个私有函数如下所示:
bool _parseRequestLine(const std::string &line); //解析请求行
void _parseRequestHeader(const std::string &line); //解析请求头
void _parseDataBody(const std::string &line); //解析数据体
void _parsePath(); //解析请求资源的网址
Httprequest请求解析类的工作流程通过这几个函数就做完了,具体的细节大家可以去查看一下源代码,还是比较简单的。
这里插一句,代码中使用到了C++11的正则表达式匹配(std::regex),代替了常用的string字符串匹配,缩减了代码量,增加了程序的可读性。感兴趣的同学可以自己查阅资料了解一下
Httpresponse类成员介绍
下面介绍一下响应报文类Httpresponse。这里再次给大家讲一下HTTP服务器的处理流程:获取请求报文并解析报文–>获取请求的资源 -->生成响应报文,并将响应报文和资源一同发送给客户端。
在Httprequest类中,只完成了解析报文这部分的任务,查找请求资源和发送响应报文这两部分的任务在Httpresponse类中完成。
客户端请求服务器上的“资源”,其一般保存在系统的硬盘/磁盘上。我们可以使用内存映射函数,将位于磁盘上的文件,映射到内存上,加快对文件的读写速度,所以这里定了一个指向映射文件的指针m_mmfile。此外,针对客户端请求的文件名,我们需要判断文件的合法性,这里定义了stat结构体保存文件的信息。如果文件被成功获取,可以从结构体mmFileState中获取文件大小等信息,非常的方便。
其他的变量通过变量名想必大家都能知道其大致的作用。m_code是获取资源的状态,同时也是响应报文中的状态码(常见的有200,403,404)。
最后还定义了三个【静态】的哈希表,这是为了在程序中能够快速的根据m_code或者资源路径打开对应的资源,具体的细节可以查看源码。
int m_code;
bool m_keepalive;
std::string m_path;
std::string m_srcDir;
char *m_mmfile;
struct stat mmFileState;
static const std::unordered_map<std::string, std::string> SUFFIX_TYPE;
static const std::unordered_map<int, std::string> CODE_STATUS;
static const std::unordered_map<int, std::string> CODE_PATH;
Httpresponse任务流程介绍
类需要完成的功能都整合到了对外接口函数makeResponse上。
void makeResponse(Buffer &buffer);
首先,在调用Httpresponse类对象时,一开始通过init函数完成成员变量的初始化。在makeResponse函数中,通过路径判断需要访问的资源是否合法,下一步就是调用函数填写响应报文
填写响应报文我们定义了三个私有函数,分别用来填写状态行、首部字段和报文主体。在addContent函数中,通过根路径和子路径组合得到的绝对路径,通过内存映射访问对应的资源文件(合法路径打开对应的文件,非法路径打开预先设定好的HTML文件)
// 填写首部字段
void addStateLine(Buffer &buffer);
void addHeader(Buffer &buffer);
void addContent(Buffer &buffer);
到这里,Httpresponse类需要完成的工作就做好了。这个类主要就是打开需要访问的资源,最后交由上层————Httpconnection类对象调用writeBuffer函数将获取到的资源以正确的响应报文格式发送出去。
HTTP连接处理详解完