当前位置: 首页 > news >正文

网络实践——基于epoll_ET工作、Reactor设计模式的HTTP服务

文章目录

  • 基于epoll_ET工作、Reactor设计模式的HTTP服务
    • 实现目标
    • 代码的实现过程
      • 基础框架
        • ReactorServer
        • Epoller——epoll模型的封装
        • Connection
        • Reactor的主逻辑
        • Listener的读事件
        • 模块结合——理解整体逻辑
      • Reactor反应堆模式的理解
      • 添加业务
        • 调用HTTP服务位置
        • HTTP业务绑定
        • 业务逻辑修改
        • 处理多路转接下写的问题
        • 异常处理
      • 总结

基于epoll_ET工作、Reactor设计模式的HTTP服务

在此前,我们学习了很多的网络通信的内容:

如网络基础的认识、网络编程的基础Socket,也学习了协议的作用及自定义了协议。
针对于应用层的协议,我们学习了最常用的HTTP协议。
学习网络原理的时候,我们自顶向下的了解了TCP/IP协议,理解了每一层的作用。
最后,我们还学习了高级IO,了解了多路转接的方案!

现在,我们需要对上述学习的知识进行一个大的整合,从而加深对上述内容的理解:

而我们要做的:
就是基于epoll的多路转接方案,ET工作模式下,以Reactor设计模式实现HTTP的服务!

实现目标

1.首先,我们需要尽可能地把我们今天要实现的服务,每个模块尽可能地解耦!
2.需要正确地使用epoll实现IO服务!
3.要尽可能地将过往学到的内容进行结合!
4.需要尽可能地体现出Reactor地设计模式!

这里,我们先不讲什么是Reactor模式。一时半会是讲不清楚的。我们将会在写一小部分代码后,再来讲解这里的Reactor到底是什么!

代码的实现过程

下面,我们一起来看一下代码的实现过程是如何实现的,同时,这里给出源码:
https://gitee.com/yangnp/linux-network-learning/tree/master/2025_9_24

这里的HTTP服务,其实就是之前实现过的HTTP服务!只不过进行了一些修改。
因为要嵌入到今天这一份代码内,所以需要进行一定的修改。

基础框架

首先,我们需要先把基础框架搭建出来!

ReactorServer

今天的服务端,就用Reactor来进行指代我们的Reatcor服务器!(后序解释这其实不是服务器)
对于今天这个服务端来说,它内部需要至少包含以下两个方面:

1.需要一个epoll的模型!来进行多路转接的。
2.需要有一个专门用来进行链接管理的模块。

但是,我们今天需要的是:把每一个模块进行解耦合,所以我们需要对Epoll模型进行封装,也许需要对连接管理的类进行封装!

Epoller——epoll模型的封装

Epoller.hpp

#include <sys/epoll.h>
#include <unistd.h>
#include "Log.hpp"
#include "Common.hpp"using namespace myLog;const static int default_epfd = -1;class Epoller{
private://用于epoll_ctl的公共部分void EpollCtlHelper(int op, int fd, uint32_t events){epoll_event ev;ev.events = events;ev.data.fd = fd; //这个是有用的! -> 到时候用于查询对应的_connections来进行操作!int n = epoll_ctl(_epfd, op, fd, &ev);if(n < 0) LOG(LogLevel::WARNING) << "epoll ctl error!";else LOG(LogLevel::INFO) << "epoll ctl success! fd is " << fd;}public:Epoller():_epfd(default_epfd){int n = _epfd = epoll_create(256);if(n < 0){LOG(LogLevel::FATAL) << "epoll create error!!!";exit(EPOLL_CREATE_ERROR);}else LOG(LogLevel::INFO) << "epoll create success, epfd is " << _epfd;}~Epoller(){int close_id = close(_epfd);if(close_id == 0) LOG(LogLevel::INFO) << "the epoll fd is closed" << _epfd;}//添加事件void AddEvent(int fd, uint32_t events){EpollCtlHelper(EPOLL_CTL_ADD, fd, events);}//删除事件void DelEvent(int fd){int n = epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, nullptr);(void)n;}//修改事件void ModEvent(int fd, uint32_t events){EpollCtlHelper(EPOLL_CTL_MOD, fd, events);}   //等待事件就绪int WaitEvent(epoll_event evs[], int max_evs, int timeout){int n = epoll_wait(_epfd, evs, max_evs, timeout);if(n < 0) LOG(LogLevel::WARNING) << "epoll_wait error!!!";else if(n == 0) LOG(LogLevel::WARNING) << "epoll_wait timeout...";else LOG(LogLevel::WARNING) << "epoll_wait success!!!";return n;}private:int _epfd;
};
Connection

虽然我们说是要有专门的一个专门管理连接的类,我们称它为Listenner。
但是,其实监听套接字和普通套接字,它们差别很大吗?其实根本不算很大!它们本质上,处理的工作都是进行读和写!对于Listenner来说,甚至都不用写操作。

所以,如果写两份重合度比较高的代码:链接管理类和普通的链接类,这是非常费事费力费时间的!所以,针对于二者在一定的程度上相同,我们可以使用继承 + 多态!

所以,我们就看这三份代码就知道三者之间的关系了:
Connection.hpp -> 表示链接的基类,内部仅仅是实现纯虚方法和存放公共属性!
Listener.hpp -> 表示专门用于监听套接字的类!
Connection.hpp -> 专门用于普通文件描述符操作的类!

Reactor的主逻辑

既然,我们今天要实现的是一个基于epoll的ET模式的IO处理服务。
所以,主逻辑自然是要基于底层的epoll的模型进行就绪事件的等待!

Epoller内部的成员变量

在这里插入图片描述
每个变量都有相应的注释,这里就不进行解释了!

主逻辑

在这里插入图片描述

在这里插入图片描述
LoopOnce是循环内进行等待的逻辑!如果能够从这里往下走到后序逻辑:
就说明,就绪事件已经返回了(ET模式)!

虽然我们还没有讲解这里是如何变为ET模式的,但是我们就先暂且当为ET模式来处理!

一旦有事件就绪,就绪的事件就会存储在ReactorServer内存放的一个_revs_arr内!
返回值n就是就绪事件的个数,然后进行不同个数对应的不同处理!
当n > 0,说明真的有就绪事件需要上层进行处理,在ET模式下,我们需要尽快地处理完所有的就绪事件,提高工作的效率,同时也是工程实践的要求!

👉尽快交付给Dispatcher——事件派发器

在这里插入图片描述
对于就绪的事件,我们目前知道它们肯定是非阻塞的fd,且是ET工作模式!
然后就需要把就绪事件的文件描述符和事件取出来,进行操作!(这里的fd是从位图拿出来的!一般来说,这个位图内用的都是fd字段,这个在epoll模式的echo server那里说过)。

但是,就绪事件可不只是EPOLLIN、EPOLLOUT,还可能是其它的一些错误事件!
如果一个个处理还是太麻烦了,代码会很乱。我们直接把它转化为IO错误!

具体的操作方式是:如果是其他错误,直接把事件追加读和写!
如果真的出错了,后序读写的时候肯定是也会出错的!直接在IO模块内进行异常处理即可!

所以,在Dispatcher这里,只需要处理fd的读和写即可!

_connections中存的都是一个个的Connection基类指针,内部有读Recver和写Sender方法,所以在Dispatcher内调用的Recver和Sender方法,其实是子类重写的!

所以,我们后序只需要针对于不同的文件描述符做读写处理即可!

Listener的读事件

我们先不管普通连接的读写处理,我们来关心Listener的读取!因为这个类虽然是继承了Connection基类,但其实它只需要真正地把都方法给重写即可!

在这里插入图片描述
此时,我们能只进行一次Accept吗?答案是当然不能!
因为今天使用的ET模式,如果只读一次,获取一个连接,那么后序都读不到了!

所以,这里需要通过while(true)来进行循环读取!
读取出来sockfd < 0,不一定是出错的!上述的逻辑早已讲过,我们这里就不再说了。

我们只需要了解,当真的拿到了一个合法的sockfd,应该怎么样操作?能直接读写吗?
答案肯定是不能的!因为不确定该文件描述符是否就绪!即使给他设置称非阻塞了,可以读,但是不符合我们本次实现目标的需求!
我们需要想办法把这个新获取到的连接(fd),交给epoll进行关注。等待它下一次就绪再处理。

但是,当前是在Listener模块啊,但是epoll模型是在ReactorServer模块内,只有ReactorServer有资格进行epoll关心和所有地连接地管理。

👇所以,这就是为什么在Connection基类中,会存在一个回调指针!
在这里插入图片描述

在这里插入图片描述

这个回调指针回指Reactor模块,从而进行操作!

所以,这就可以很轻而易举地完成获取新连接后,加入到内核和Reactor模块管理的工作了:
在这里插入图片描述
在这里插入图片描述

而且,每个Channel被建立得时候,都需要做一件事情:
在这里插入图片描述
就是把该文件描述符设置为非阻塞!
所以,这就是为什么,我们可以保证:拿到的就绪事件,一定是ET模式下得非阻塞的fd!

模块结合——理解整体逻辑

但是,上述的过程中:仅仅只是讲解了每个模块的作用!
没有讲清楚,这个服务是如何启动的。

其实就是因为,监听套接字没有设置给epoll关心!一开始epoll模型内什么关心的fd都没有。

在前面实现多路转接的时候,我们都是需要先把至少一个监听套接字设置给内核的。所以,这里也是一样的,我们也需要一开始就把监听套接字设置给epoll去关心。
从而获取新连接,或者是后续对普通的连接做IO操作。

所以,在主函数开启之前,就需要提前创建好监听模块,然后就后通过ReactorServer的添加连接接口来交付给内核,和底层的哈希表:
在这里插入图片描述
在这里插入图片描述
然后再来进行启动服务!

在这里插入图片描述
Listener的创建,默认就是要设置非阻塞和ET模式的!

至此,逻辑就闭环了。为什么我们可以在前面读取就绪时间的时候,直接就认为该文件描述符是非阻塞的!也知道为什么是ET模式!

Reactor反应堆模式的理解

上面理解了基础框架,这个时候,我们就可以来正式地理解,为什么该服务是Reactor模式,即反应堆模式!

下面用一张图给予解释:
在这里插入图片描述
这是我们本次软件服务实现的分层结构。

内核中有一个epoll模型,它负责关注着所有的被关心的文件描述符是否就绪。
一旦就绪,它就会交给管理着所有ConnectionReactorServer类,然后根据就绪事件进行事件的派发!有一个fd就绪,就处理一个fd!

这不就像是核反应堆吗?一个一个的就绪,然后一个个的处理!
如果说需要进行连接的管理,就可以通过Connection内的回调指针进行操作!

至此,我们就能大致理解,我们本次实现的服务,为什么是Reactor反应堆模式了!

就像是下面这个打地鼠的场景一样:
在这里插入图片描述

添加业务

上面,我们也仅仅是把基础的框架给搭出来了。处理的是宏观逻辑。
但是,每个连接读取的数据,或者要写的数据,是什么呢?——这个需要结合需要的业务!

而且,我们使用的是TCP来创建的Listener,所以,有一个很大的问题就是:

TCP是面向字节流的!我们在读取的时候,没办法保证报文的完整性!

所以,是需要引入协议的!以保证通信的双方能够正确地读取到请求和应答。
这里,我们也不打算自定义协议了,因为有现成的可以使用:
1.网络版本的计算器
2.HTTP服务

如果想要简单一点,就直接拿网络版本的计算器来使用。需要带上客户端!
但是,今天我不想写客户端了,所以直接拿HTTP服务来写!

调用HTTP服务位置

首先,每个普通的文件描述符就绪后,就会进行ChannelRecv操作:
在这里插入图片描述

这里虽然recv读取的时候,是把选项传为0,但是因为我们早已设置过非阻塞了!
所以,这里的读取一定是非阻塞读取的!至于非阻塞的读取,早在高级IO那里讲过了!

因为是TCP协议读取的,所以没办法保证报文的完整性!发送的时候也是:
在这里插入图片描述
所以每个Channel内都会有自己的接收缓冲区和发送缓冲区!

Tips:这里为了简单,直接使用string了!
但如果是图片、音频、视频等二进制内容,就可能会处理出错的!
要想正确使用的话应该是用vector< string >的!
但我这里为了简单,我就直接使用string了!

然后当Recv的读取模块结束后,就需要进入到_handler_conn的服务处理了!因为要接收到对方的HTTP请求然后处理后返回应答!
if(!_inbuffer.empty()) _outbuffer = _handler_conn(_inbuffer);
把HTTP请求进行处理,返回应答的反序列化串,然后添加到发送缓冲区!

HTTP业务绑定

上述的_handler_conn,是每个文件描述符加入到内核和Reactor的时候,就已经为它注册好了的服务的!我们会把它放在基类Connection上!

但是,具体的HTTP的业务,是如何绑定给每个链接的呢?
难道是添加到内核的时候,Listener给每个文件描述符都进行注册了吗?

如果是上述的做法,就需要在ListenerRecv参数部分加一个绑定服务的参数,要不然就会得把服务写死!这是不愿意看到的!
但是,加了一个参数,导致基类和子类的函数参数就不一样了,就没办法构成重写了!
所以,应该怎么办呢?
👇
其实可以这样做:
1.在Listener交给内核关心的时候,就先把服务注册到Listener上!
2.Listener的基类部分就拿了服务了,然后,在Recv的时候,把绑定在Listener上的服务一一注册给新来的链接!
在这里插入图片描述
在这里插入图片描述
至此,就可以很轻松的把服务绑定给每个连接了!

主逻辑中还对HTTP进行了交互服务的注册!这个根据自己的需求来就好。

业务逻辑修改

我们上一次写的HTTP服务的代码并不是直接就能拿来用的!还是需要进行一定的修改。
我们给每个链接绑定的服务,就是HTTP内的HandlerRequest函数

但是原来的HandlerRequest函数,是通过TCP套接字和客户端地址来作为参数,进行一个处理,然后直接发送序列化后的应答给对端的!

但是,今天绑定的服务类型是:
在这里插入图片描述
参数为string,返回值也是string!
其实就是:今天的HTTP服务不再需要考虑读取和发送数据的问题,只需要接收到一串报文,正确解析报文,然后把应答以返回值的形式返回!

//这里就只需要做一件事情,就是把接收到输入缓冲区给进行处理!(因为不需要这里来读取)std::string HandleHttpRequest(std::string& readbuffer){std::string response_str;while(1){//首先,得保证读到完整的请求->如果这一次没能成功读到完整请求,就不进行处理了!std::string all_reqline;if(ReadAllRequestHeader(readbuffer, &all_reqline) == false) break;//读到完整的请求报头 -> all_reqline//all_reqline里面有一个字段是指向正文长度的(前提是,Http请求中,正文部分长度 > 0,要不然其实是看不到的!)//如果发送来的正文长度 == 0,看不到这个字段!std::string text_len;if(AnalyseRequestLine(all_reqline, "Content-Length", &text_len) == false) text_len = "0";//成功读取长度到text_len -> 需要转成整数int len = std::stoi(text_len);//读取正文(从readbuffer中, 长度为len)if(readbuffer.size() < len) break; //正文长度不对!//这里就能够正确地把整个http请求报文都出来了!在req_str内std::string text = readbuffer.substr(0, len);std::string req_str = all_reqline + text;//需要手动的把读到的一个完整的报文从缓冲区内拿下来!readbuffer.erase(0, req_str.size());//反序列化HttpRequest hreq;hreq.DeSerialize(req_str);//应答对应的协议结构HttpResponse hresp;//今天这里加多一步,反序列化后,就需要知道当前是否需要进行交互了if(hreq.Is_Interact()){//需要进行交互std::string service_name = hreq.GetUri();//但是,这个服务可能不存在于_route表中if(_route.find(service_name) == _route.end()){//重定向到对应的404网页hresp.SetCodeAndDesc(301);hresp.SetHeaders("Location", "/404.html");response_str += hresp.Serialize();                    }else{_route[service_name](hreq, hresp);response_str += hresp.Serialize();}}//如果不需要进行交互访问,只访问静态资源,就走原来的逻辑!else{//分析请求 + 制作应答//反序列化的时候,已经把要访问的web根目录底下的文件进行处理了!hresp.SetTargetFile(hreq.GetUri());hresp.MakeResponse();//应答进行序列化std::string resp_str = hresp.Serialize();response_str += resp_str;}}return response_str;}

只需要在原来的基础上做一点点小修改即可!
同时,因为可能一连串发送来多个报文的粘包报,所以这里采取一次性解析完所有的请求报文,然后一次性的生成多个报文。所有的序列化后的应答报文都在response_str

然后发送的时候粘报不用怕,对端是浏览器,浏览器自己会处理的!

处理多路转接下写的问题

HTTP业务返回后,每个Channel就有可能得到完整的应答报文!
然后就需要想办法给对端进行发送了!

但是,我们一直没有处理多路转接下的写问题,这里应该怎么处理呢?


我们需要秉持着以下几个结论:

1.写事件默认是就绪的!不能把写事件的关心直接加入到epoll!因为那样会导致一直就绪!
2.写事件应该是按需设置的!如果写就绪,那就直接发送给对端!如果没能发送完,那就只能加入到epoll内进行写事件的关心!
3.一旦触发 EPOLLOUT 事件并成功写入数据后,应立即移除 EPOLLOUT 监听!
4.把fd的写事件加入epoll关心,默认会触发一次写就绪!

所以,这就是为什么,Recv接口在接受处理好数据后,就直接进行发送了!
同时,我们再来看Sender的实现:
在这里插入图片描述
进行非阻塞发送,发送的内部处理逻辑其实和读取时一样的!

非阻塞发送模块结束后:
发送缓冲区不为空 -> 底层发送缓冲区没有空间 -> 没法再发送 -> 对端接收到不完整报文!
👉只能把写事件加入到epoll内关心!下一次就同时关心读和写!

发送缓冲区为空 -> 底层还有空间可以发送 -> 但是,不确定此时fd是否在内核中被关心了!
👉需要手动地把写事件的关心给关闭!

但是,关心和不关心,都是Reactor做的事情,所以,还是需要通过回调指针来做:
在这里插入图片描述

异常处理

最后,还有一个模块没有进行操作:就是异常处理!
其实异常处理,无论怎么样,这个文件描述符就是出问题了!直接删除关心和链接即可!

在这里插入图片描述
在这里插入图片描述
这样,所有的异常,都被归一到了IO处理异常了!

总结

至此,我们就完成了这样一个代码的编写!后序就不再展示实验效果了,其实和之前写的HTTP服务比起来看不出太大的差别。
差别只能在于:没办法处理图片、音视频等内容!

http://www.dtcms.com/a/414058.html

相关文章:

  • 设计模式-行为型设计模式(针对对象之间的交互)
  • 选手机网站彩票网站开发制作模版
  • qq钓鱼网站在线生成器北京网站设计公司地址
  • SQL流程控制函数完全指南
  • 做电商网站前端的技术选型是移动商城积分和积分区别
  • 弄一个关于作文的网站怎么做微信分销网站建设官网
  • 怎么做站旅游网站上泡到妞平面设计师服务平台
  • 温室大棚建设 网站及排名转卖类似淘宝网站建设有哪些模板
  • 广西网站建设-好发信息网阿里邮箱 wordpress
  • 便捷网站建设费用搜关键词网站
  • 网站添加百度地图导航wordpress安装 centos
  • 如何自己建一个网站企业简介宣传片视频
  • 成都美誉网站设计建设优惠券网站
  • 整形网站源码一个网站如何做盈利
  • 机械设备东莞网站建设石家庄开发区网站建设
  • 代制作网站公司网站建设包括
  • 怎么手动安装网站程序搭建微信小程序
  • 郑州建网站371怎么把东西发布到网上卖
  • wordpress 点图片链接拼多多seo怎么优化
  • 石家庄做网站wordpress 文章摘要
  • 网站建设服务类型现状做兼职上哪个网站
  • 重庆网站seo排名用dw制作一个网站
  • 太原模板建站定制深圳网站建设及推广
  • vps 网站 需要绑定域名吗建设部网站拆除资质
  • 六安网站自然排名优化价格遵义网站建设网帮你
  • 网站版面设计流程包括哪些盐城手机网站建设
  • 重庆网站搭建昆明网站建设报价
  • 设计制作网站的公司深圳全网整合营销
  • 辽宁建设厅查询网站首页怎么给自己的网站做优化
  • 专业集团门户网站建设方案两学一做网站飘窗