20.添加HTTP模块

添加一个简单的静态HTTP。

这里默认读者是熟悉http协议的。

来看看http请求Request的例子

客户端发送一个HTTP请求到服务器的请求消息,其包括:请求行、请求头部、空行、请求数据。

HTTP之响应消息Response 

服务器接收并处理客户端发过来的请求后会返回一个HTTP的响应消息,其包括:状态行、消息报头、空行和响应正文。

 前面所说的就是http的请求和响应答复。那我们可以封装出两个类。

HttpRequest:http请求类封装

HttpResponse:http响应类封装

注意:这里会使用到我们之前写的Buffer类。因为服务器是把读到的数据存储在Buffer中的,所以大家要熟悉Buffer类的一些用法

1、HttpRequest 类

该类的主要作用是客户端发送请求,服务端收到的数据存放于Buffer中,之后解析成HttpRequest请求对象,调用成员函数设置请求头、请求体等。

首先会有请求方式method_,http版本version_,请求头headers_(用map管理)。请求的路径path_(即是url),还有请求体query_

请求体有可能是在url中的"?"后面,也可能是在请求头后面的。

class HttpRequest
{
public:enum class Method{kInvalid, kGet, kPost, kHead, kPut, kDelete};enum class Version{kUnknown, kHttp10, kHttp11};HttpRequest():method_(Method::kInvalid),version_(Version::kUnknown){}void setVersion(Version v) { version_ = v; }Version getVersion()const { return version_; }bool setMethod(const char* start, const char* end){string m(start, end);if (m == "GET") {method_ = Method::kGet;}else if (m == "POST") {method_ = Method::kPost;}//省略"HEAD","DELETE"等等方式......return method_ != Method::kInvalid;}Method getMothod()const { return method_; }const char* methodString()const {const char* result = "UNKNOWN";switch (method_) {case Method::kGet:result = "GET";break;case Method::kPost:result = "POST";break;//省略"HEAD","DELETE"等等方式......}return result;}void setPath(const char* start, const char* end) {path_.assign(start, end);}const string& path()const { return path_; }void setQuery(const char* start, const char* end) {query_.assign(start, end);}const string& query()const { return query_; }void addHeader(const char* start, const char* colon, const char* end){//isspace(int c)函数判断字符c是否为空白符//说明:当c为空白符时,返回非零值,否则返回零。(空白符指空格、水平制表、垂直制表、换页、回车和换行符。// 要求冒号前无空格string field(start, colon);++colon;while (colon < end && isspace(*colon))// 过滤冒号后的空格++colon;string value(colon, end);while (!value.empty() && isspace(value[value.size() - 1]))//过滤value中的空格value.resize(value.size() - 1);headers_[field] = value;}string getHeader(const string& field)const{string result;auto it = headers_.find(field);if (it != headers_.end()) {return it->second;}return result;}const std::unordered_map<string, string>& headers()const { return headers_; }private:Method method_;Version version_;string path_;	//请求路径string query_;	//请求体std::unordered_map<string, string> headers_;
};

注意:添加请求头时,函数addHeader需要删除键值对的字符串左侧和右侧的空字符,保证解析正常。因为解析请求头时,对一行字符串用冒号“:”进行分割解析。

2、HttpResponse 类

服务器端得到的客户的请求信息后,再创建一个HttpResponse响应对象,也是会调用成员函数设置响应头部、响应体,并格式化到Buffer中,回复给客户端。

按照上面的响应例子,那应该有响应头headers_,响应的状态码statusCode_,状态码的文字描述statusMessage_,响应体body_等等。

成员函数就是一些设置状态码,设置响应头等操作。

class HttpResponse
{
public:enum class HttpStatusCode{kUnknown,k200Ok = 200,k301MovedPermanently = 301,k400BadRequest = 400,k404NotFound = 404,};explicit HttpResponse(bool close):statusCode_(HttpStatusCode::kUnknown),closeConnection_(close){}void setStatusCode(HttpStatusCode code) { statusCode_ = code; }void setstatusMessage(const string& message) { statusMessage_ = message; }void setCloseConnection(bool on) { closeConnection_ = on; }bool closeConnection()const { return closeConnection_; }void setContentType(const string& contentType) { addHeader("Content-Type", contentType); }void addHeader(const string& key, const string& value) {headers_[key] = value;}void setBody(const string& body) { body_ = body; }void appendToBuffer(Buffer* output)const;private:std::unordered_map<string, string> headers_;HttpStatusCode  statusCode_;    //状态码string statusMessage_;    //响应行中的状态码文字描述bool closeConnection_;    //是否关闭连接string body_;        //响应体
};

这里特别值得一说的是如何把响应消息格式化的操作格式化appendToBuffer(Buffer* output)

该函数默认使用HTTP1.1版本,按照HTTP协议对HttpResponse对象进行格式化输出到Buffer中。

按照要求添加响应行,响应头,空行,响应体。

void HttpResponse::appendToBuffer(Buffer* output) const
{//响应行string buf = "HTTP/1.1 " + std::to_string(static_cast<int>(statusCode_));output->append(buf);output->append(statusMessage_);output->append("\r\n");//响应头部if (closeConnection_) {output->append("Connection: close\r\n");}else {output->append("Connection: Keep-Alive\r\n");buf = "Content-Length:" + std::to_string(body_.size()) + "\r\n";output->append(buf);}for (const auto& header : headers_) {buf = header.first + ": " + header.second + "\r\n";output->append(buf);}output->append("\r\n");	//空行output->append(body_);	//响应体
}

3、HttpContext 类

服务端接收客户请求,存在Buffer中,那怎么从Buffer中解析得到我们想要的信息呢这时,需要一个解析类HttpContext,解析后数据封装到回复HttpRequest中。

其成员有处理的状态state_,响应request_。

class HttpContext
{
public:enum class HttpRequestPaseState{kExpectRequestLine,	//请求行kExpectHeaders,    // 请求头kExpectBody,        // 请求体kGotAll,            //表示都处理完全};HttpContext():state_(HttpRequestPaseState::kExpectRequestLine)//默认从请求行开始解析{}bool parseRequest(Buffer* buf);// 解析请求Bufferbool gotAll()const { return state_ == HttpRequestPaseState::kGotAll; }void reset()// 为了复用HttpContext{state_ = HttpRequestPaseState::kExpectRequestLine;HttpRequest dumy;request_.swap(dumy);}const HttpRequest& request() const{ return request_; }HttpRequest& request(){ return request_; }private:bool processRequestLine(const char* begin, const char* end);HttpRequestPaseState state_;	//需要处理的状态,状态机HttpRequest request_;
};

一个正常的请求,一般至少是有请求行的,默认解析状态为kExpectRequestLine。

这里就主要关注是如何解析Buffer的。

3.1、请求解析 parseRequest(Buffer* buf)

这里为了方便找到buf中的"\r\n",添加了Buffer::findCRLF()函数。

const char Buffer::kCRLF[] = "\r\n";//为了方便解析http "\r\n"位置
const char* findCRLF()const {const char* crlf = std::search(peek(), beginWirte(), kCRLF, kCRLF + 2);return crlf == beginWirte() ? nullptr : crlf;
}

传入需要解析的Buffer对象,根据期望解析的部分(即是状态state_)进行处理。

处理就三种情况:请求行,请求头,请求体。具体的流程可以看代码

bool HttpContext::parseRequest(Buffer* buf)
{bool ok = true;bool hasMore = true;while (hasMore) {if (state_ == HttpRequestPaseState::kExpectRequestLine) {	//解析请求行//查找出buf中第一次出现"\r\n"位置const char* crlf = buf->findCRLF();if (crlf) {//若是找到"\r\n",说明至少有一行数据,可以进行解析//buf->peek()为数据开始部分ok = processRequestLine(buf->peek(), crlf);if (ok) {//解析成功buf->retrieveUntil(crlf + 2);//buf->peek()向后移动2字节,到下一行state_ = HttpRequestPaseState::kExpectHeaders;}else {hasMore = false;}}else {hasMore = false;}}else if (state_ == HttpRequestPaseState::kExpectHeaders) {const char* crlf = buf->findCRLF();	//找到"\r\n"位置if (crlf) {const char* colon = std::find(buf->peek(), crlf, ':');//定位分隔符if (colon != crlf) {request_.addHeader(buf->peek(), colon, crlf);	//添加键值对}else {/*state_ = HttpRequestPaseState::kGotAll;hasMore = false;*/state_ = HttpRequestPaseState::kExpectBody;//这样就可以解析body}buf->retrieveUntil(crlf + 2);	//后移动2字节}else {hasMore = false;}}else if (state_ == HttpRequestPaseState::kExpectBody) {//解析请求体if (buf->readableBytes()) {//表明还有数据,那就是请求体request_.setQuery(buf->peek(), buf->beginWirte());}state_ = HttpRequestPaseState::kGotAll;hasMore = false;}}return ok;
}

3.1、请求行的解析 processRequestLine()

请求行有固定格式Method URL Version \r\n,URL中可能带有请求参数。根据空格符进行分割成三段字符。URL可能带有请求参数,使用"?”分割解析。

bool HttpContext::processRequestLine(const char* begin, const char* end)
{bool succeed = true;const char* start = begin;const char* space = std::find(start, end, ' ');//第一个空格前的字符串是请求方法 例如:postif (space != end && request_.setMethod(start, space)) {start = space + 1;space = std::find(start, end, ' ');//寻找第二个空格 urlif (space != end) {const char* question = std::find(start, space, '?');if (question != space) {// 有"?",分割成path和请求参数request_.setPath(start, question);request_.setQuery(question, space);}else {request_.setPath(start, space);//只有path}//最后一部分,解析http协议版本string version(space + 1, end);if (version == "HTTP/1.0")request_.setVersion(HttpRequest::Version::kHttp10);else if (version == "HTTP/1.1")request_.setVersion(HttpRequest::Version::kHttp11);elsesucceed = false;}}return succeed;
}

这样解析就完成了。

4、HttpServer类

为了可以方便使用,封装个HttpServer类。

该类内部会有Server类型成员,并提供了一个回调函数的接口,当服务器收到http请求时,调用客户端的处理函数进行处理。

HttpServer支持多线程,也可以使用单线程。

class HttpServer
{
public:using HttpCallback = std::function<void(const HttpRequest&, HttpResponse*)>;HttpServer(EventLoop* loop, const InetAddr& listenAddr);void setHttpCallback(const HttpCallback& cb) { httpCallback_ = cb; }void start(int numThreads);private:void onConnetion(const ConnectionPtr& conn);	//连接到来的回调函数void onMessage(const ConnectionPtr& conn, Buffer* buf);	//消息到来的回调函数void onRequest(const ConnectionPtr& conn, const HttpRequest&);Server server_;HttpCallback httpCallback_;};

函数setHttpCallback就是设置用户的业务处理回调函数的。

4.1HttpServer构造函数

//默认的回调函数
void defaultHttpCallback(const HttpRequest& req, HttpResponse* resp)
{resp->setStatusCode(HttpResponse::HttpStatusCode::k404NotFound);resp->setstatusMessage("Not Found");resp->setCloseConnection(true);
}
//构造函数
HttpServer::HttpServer(EventLoop* loop, const InetAddr& listenAddr):server_(listenAddr,loop), httpCallback_(defaultHttpCallback)
{//新连接到来回调该函数server_.setConnectionCallback([this](const ConnectionPtr& conn) {onConnetion(conn); });//消息到来回调该函数	server_.setMessageCallback([this](const ConnectionPtr& conn, Buffer* buf) {onMessage(conn, buf); });
}

这里就是初始化Server,并将HttpServer的回调函数传给Server。主要有两个函数。

前面的HttpResponse类和HttpRequest类已经在HttpServer使用了,但是解析类HttpContext还没有使用。

很容易想到是在回调函数中使用。在有消息到来的时候,就会进行解析数据,这时就会使用到HttpContext。可以在每次调用函数onMessage中创建HttpContext对象。这在短连接中使用是合适的。但是在长连接的情况下,这样可能效率不高

那么就可以在有新连接到来的时刻,就设置好HttpContext。

那就说到onConnetion函数

4.2 连接到来的回调函数onConnetion

//这里绑定一个HttpContext主要是为了长连接中仅分配一次对象,提高效率。
void HttpServer::onConnetion(const ConnectionPtr& conn)
{if (conn->connected()) {//conn->setContext(std::make_shared<HttpContext>()); //c++11的std::shared_ptr<void>conn->setContext(HttpContext());    //c++17的std::any}
}

该函数为一个新的Connection绑定一个HttpContext对象,绑定之后,HttpContext就相当于Connection的成员,TcpConection在MessageCallback中就可以随意的使用该HttpContext对象了。
这里绑定一个HttpContext主要是为了长连接中仅分配一次对象,提高效率

这里绑定使用的是c++17的std::any。std::any表示可以接受任意类型的变量。

来看看Conntection类中需要添加的变量

#include<any>
class Connection:public std::enable_shared_from_this<Connection>
{
public://省略之前的变量和函数//void setContext(std::shared_ptr<void> context) { context_ = context; }//std::shared_ptr<void> getConntext()const { return context_; }void setContext(const std::any& context) { context_ = context; }std::any* getMutableContext() { return &context_; }
private://std::shared_ptr<void> context_;	//c++11做法std::any context_;	//用来解析http或者websocket或者其他协议的
};

首先我们要明确为什么要的是接收任意类型的变量这总做法,为什么不是直接就是用HttpContext类替代std::any。

因为我们后续可能还需要解析其他协议的,例如websockte协议(下一节会讲解)。要是直接写HttpContext的话,那要解析websocket协议的时候,Connection类中还需要添加websocketContext类成员变量,这就很麻烦的。所以用std::any来就可以绑定所有的解析类。

那又有疑惑,为什么不直接用void*?简单点说是,它类型不安全,还需要用户手动去delete。

std::shared_ptr和void*一样不能解决类型安全的问题。详细的了解可以查看该文章https://www.cnblogs.com/gnivor/p/12793239.html

那说完std::any和回调函数onConnetion,那就到函数onMessage。

4.3 新消息到来的回调函数onMessage

void HttpServer::onMessage(const ConnectionPtr& conn, Buffer* buf)
{//HttpContext* context = reinterpret_cast<HttpContext*>(conn->getConntext().get());	//c++11做法auto context = std::any_cast<HttpContext>(conn->getMutableContext());	//c++117if (!context) {LOG_ERROR<<"context is bad\n";return;}if (!context->parseRequest(buf)) {conn->send("HTTP/1.1 400 Bad Request\r\n\r\n");conn->shutdown();}if (context->gotAll()) {onRequest(conn, context->request());context->reset();//一旦请求处理完毕,重置context,因为HttpContext和Connection绑定了,我们需要解绑重复使用}
}

当Connection中所拥有的连接有新消息到来时,会调用它的messageCallback_函数,其实就是调用HttpServer的onMessage()函数。而之前在函数onConnection()中把HttpContext利用std::any绑定给了Connection,那在该函数中就可以对Connection使用HttpContext类来解析数据包了。

onMessage()函数首先调用HttpContext的parserRequset()函数解析请求,判断请求是否合法,进而选择关闭连接,或者处理请求(函数onRequest)。

4.4处理请求的函数onRequest

void HttpServer::onRequest(const ConnectionPtr& conn, const HttpRequest& req)
{const std::string& connetion = req.getHeader("Connection");bool close = connetion == "close" || (req.getVersion() == HttpRequest::Version::kHttp10 && connetion != "Keep-Alive");HttpResponse response(close);//执行用户注册的回调函数httpCallback_(req, &response);Buffer buf;response.appendToBuffer(&buf);conn->send(&buf);//发送数据if (response.closeConnection()) {conn->shutdown();}
}

先判断是长连接还是短连接。接着使用close构造一个HttpResponse对象。之后很重要的是执行用户注册的回调函数,这个就是用户的业务函数。

5.HtttpServer的用法

#include"src/Server.h"
//省略一些其他头文件//用户的业务处理的函数
void onRequest(const HttpRequest& req, HttpResponse* resp)
{if (req.path() == "/") {// 根目录请求resp->setStatusCode(HttpResponse::HttpStatusCode::k200Ok);resp->setstatusMessage("OK");resp->setContentType("text/html");resp->addHeader("Server", "li");resp->setBody("<html><head><title>This is title</title></head>""<body><h1>Hello</h1>Now is hello" "</body></html>");}else if (req.path() == "/hello") {resp->setStatusCode(HttpResponse::HttpStatusCode::k200Ok);resp->setstatusMessage("OK");resp->setContentType("text/plain");resp->setBody("hello, world!\n");}else {resp->setStatusCode(HttpResponse::HttpStatusCode::k404NotFound);resp->setstatusMessage("Not Found");resp->setCloseConnection(true);}
}int main(int argc, char* argv[])
{EventLoop loop;HttpServer server(&loop, InetAddr(9999));server.setHttpCallback(onRequest);        //比普通的server添加了这行server.start(0);    //副io线程数量为0,单线程运行loop.loop();return 0;
}

主要就是用户自写的一个业务处理函数,之后调用HttpServer类的函数setHttpCallback来进行注册即可。

这里例子是创建了端口是9999的HTTPServer,提供访问的是/,/hello。

在浏览器输入 http://localhost:9999或者http://localhost:9999/hello即可访问成功。(localhost可以改成是自己linux的ip)

HTTP调用的流程图

HTTP服务器基本就是结束了,这里的是简单静态web服务器,我们没有解析客户发送过来的body。需要其他功能可以在这基础上进行完善或添加,比如支持fcgi。

完整源代码:https://github.com/liwook/CPPServer/tree/main/code/server_v20

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/103148.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Linux】工具Gdb调试轻度使用(C++)

目录 一、Gdb背景 二、Gdb基本命令 【2.1】list | l 【2.2】break | b 【2.5】delete | d 【2.6】disable 【2.7】enable 【2.3】info 【2.4】info locals 【2.6】run | r 【2.7】next | n 【2.8】step | s 【2.9】 continue | c 【2.10】bt 【2.11】finish 三…

DHTMLX Gantt 8.0.5 Crack -甘特图

8.0.5 2023 年 9 月 1 日。错误修复版本 修复 修复通过gantt.getGanttInstance配置启用扩展而触发的错误警告修复启用skip_off_time配置时gantt.exportToExcel()的不正确工作示例查看器的改进 8.0.4 2023 年 7 月 31 日。错误修复版本 修复 修复数据处理器不跟踪资源数据…

MNIST手写数字辨识-cnn网路 (机器学习中的hello world,加油)

用PyTorch实现MNIST手写数字识别(非常详细) - 知乎 (zhihu.com) 参考来源&#xff08;这篇文章非常适合入门来看&#xff0c;每个细节都讲解得很到位&#xff09; 一、模块函数用法-查漏补缺&#xff1a; 1.关于torch.nn.functional.max_pool2d()的用法&#xff1a; 上述示例…

优化VUE Element UI的上传插件

默认ElmentUI的文件列表只有一个删除按钮&#xff0c;我需要加预览、下载、编辑等&#xff0c;就需要优化显示结果。 优化后没用上传进度条&#xff0c;又加了一个进度条效果 代码 <template><div><el-uploadclass"upload-demo"action"/"…

OPPO/真我手机ColorOS13系统解账户锁-移除手机密码图案锁方法

在搞机之前&#xff0c;请确定自己的手机不是非法获取&#xff0c;本文只讲叙ColorOS13系统解锁方法&#xff0c;仅为个人测试研究出来的经验&#xff0c;未对官方系统进行任何修改。只推荐专业维修师傅从维修的角度进行解锁&#xff0c;不推荐个人用户对非自己的手机进行非法破…

传输层-TCP 的安全机制和高效策略

可靠性&#xff1a; 之前我们在UDP中谈到了&#xff0c;UDP不可靠但是简单&#xff0c;TCP可靠但是也要做更多的工作&#xff0c;那这些工作具体是什么呢&#xff1f;接下来让我们详细了解一下。 确认应答机制&#xff08;ACK机制&#xff09; 序号&#xff1a;我们可以把TCP…

如何按文件名称自动归类,助您轻松管理文件

在日常工作和生活中&#xff0c;我们经常会遇到大量的文件需要管理。如果这些文件没有得到良好的归类和整理&#xff0c;不仅会浪费我们的时间和精力&#xff0c;还会给我们带来困扰和混乱。今天我们一起来看看怎么按文件名称来自动归类&#xff0c;批量整理文件&#xff0c;让…

云数据库知识学习——概述

一、云计算是云数据库兴起的基础 云计算是分布式计算、并行计算、效用计算、网络存储、虚拟化、负载均衡等计算机和网络技术发展融合的产物。云计算是由一系列可以动态升级和被虚拟化的资源组成的&#xff0c;用户无需掌握云计算的技术&#xff0c;只要通过网络就可以访问这些资…

图床项目性能测试

文章目录 一、FastDFS文件性能测试1.1 上传文件测试1.2 下载测试1.3 删除文件测试1.4 如何提高 二、图床项目wrklua性能测试2.1 wrk2.2 MySQL索引优化2.2 注册测试2.2.1 无索引性能2.2.2 有索引性能 2.3 登录测试2.3.1 无索引性能2.3.2 有索引性能 2.4 读取文件测试2.4.1 无索引…

异步编程 - 13 高性能线程间消息传递库 Disruptor

文章目录 Disruptor概述Disruptor中的核心术语Disruptor 流程图 Disruptor的特性详解基于Disruptor实现异步编程 Disruptor概述 Disruptor是一个高性能的线程间消息传递库&#xff0c;它源于LMAX对并发性、性能和非阻塞算法的研究&#xff0c;如今构成了其Exchange基础架构的核…

C#事件event

事件模型的5个组成部分 事件拥有者&#xff08;event source&#xff09;&#xff08;类对象&#xff09;&#xff08;有些书将其称为事件发布者&#xff09; 事件成员&#xff08;event&#xff09;&#xff08;事件拥有者的成员&#xff09;&#xff08;事件成员就是事件本身…

山西电力市场日前价格预测【2023-09-09】

日前价格预测 预测明日&#xff08;2023-09-09&#xff09;山西电力市场全天平均日前电价为372.85元/MWh。其中&#xff0c;最高日前电价为435.72元/MWh&#xff0c;预计出现在18: 45。最低日前电价为342.46元/MWh&#xff0c;预计出现在04: 00。 价差方向预测 1&#xff1a; 实…