【网络】高级IO——Reactor版TCP服务器
1.什么是ReactorReactor 是一种应用在服务器端的开发模式(也有说法称 Reactor 是一种 IO 模式),目的是提高服务端步伐的并发能力
它要办理什么题目呢?
传统的 thread per connection 用法中,线程在真正处置惩罚请求之前首先必要从 socket 中读取网络请求,而在读取完成之前,线程本身被阻塞,不能做任何事,这就导致线程资源被占用,而线程资源本身是很珍贵的,尤其是在处置惩罚高并发请求时。
而 Reactor 模式指出,在等候 IO 时,线程可以先退出,如许就不会因为有线程在等候 IO 而占用资源。但是如许原先的执行流程就没法还原了,因此,我们可以利用变乱驱动的方式,要求线程在退出之前向 event loop 注册回调函数,如许 IO 完成时 event loop 就可以调用回调函数完成剩余的操作。
所以说,Reactor 模式通过减少服务器的资源消耗,提高了并发的能力。固然,从实现角度上,变乱驱动编程会更难写,难 debug 一些。
1.1.餐厅里的Reactor模式
我们用“餐厅”类比的话,就像下图:https://i-blog.csdnimg.cn/direct/dfec626365604f5689e5fb8a4cb5935a.png
对于每个新来的顾客,前台都必要找到一个服务员和厨师来服务这个顾客。
[*]服务员给出菜单,并等候点菜
[*]顾客检察菜单,并点菜
[*]服务员把菜单交给厨师,厨师照着做菜
[*]厨师做好菜后端到餐桌上
这就是传统的多线程服务器。每个顾客都有自己的服务团队(线程),在人少的环境下是可以良好的运作的。如今餐厅的口碑好,顾客人数不停增加,这时服务员就有点处置惩罚不过来了。
这时老板发现,每个服务员在服务完客人后,都要去休息一下,因此老板就说,“你们都别休息了,在旁边待命”。如许大概 10 个服务员也来得及服务 20 个顾客了。这也是“线程池”的方式,通过重用线程来减少线程的创建和销毁时间,从而提高性能。
但是客人又进一步增加了,仅仅靠剥削服务员的休息时间也没有办法服务这么多客人。老板细致观察,发现着实服务员并不是不停在干活的,大部分时间他们只是站在餐桌旁边等客人点菜。
于是老板就对服务员说,客人点菜的时候你们就别傻站着了,先去服务其它客人,有客人点好的时候喊你们再过去。对应于下图:
https://i-blog.csdnimg.cn/direct/6f0cab46579c4989bec4934c6f9019b1.png
最后,老板发现根本不必要那么多的服务员,于是裁了一波员,终极甚至可以只有一个服务员。
这就是 Reactor 模式的核心思想:减少等候。当遇到必要等候 IO 时,先释放资源,而在 IO 完成时,再通过变乱驱动 (event driven) 的方式,继续接下来的处置惩罚。从团体上减少了资源的消耗。
2.Reactor的由来
如果要让服务器服务多个客户端,那么最直接的方式就是为每一条毗连创建线程。
着实创建历程也是可以的,原理是一样的,历程和线程的区别在于线程比较轻量级些,线程的创建和线程间切换的本钱要小些,为了描述简述,背面都以线程为例。
处置惩罚完业务逻辑后,随着毗连关闭后线程也同样要销毁了,但是如许不停地创建和销毁线程,不但会带来性能开销,也会造成浪费资源,而且如果要毗连几万条毗连,创建几万个线程去应对也是不现实的。
要这么办理这个题目呢?我们可以使用「资源复用」的方式。
也就是不用再为每个毗连创建线程,而是创建一个「线程池」,将毗连分配给线程,然后一个线程可以处置惩罚多个毗连的业务。
不过,如许又引来一个新的题目,线程怎样才能高效地处置惩罚多个毗连的业务?
当一个毗连对应一个线程时,线程一般接纳「read -> 业务处置惩罚 -> send」的处置惩罚流程,如果当前毗连没有数据可读,那么线程会阻塞在 read 操作上( socket 默认环境是阻塞 I/O),不过这种阻塞方式并不影响其他线程。
但是引入了线程池,那么一个线程要处置惩罚多个毗连的业务,线程在处置惩罚某个毗连的 read 操作时,如果遇到没有数据可读,就会发生阻塞,那么线程就没办法继续处置惩罚其他毗连的业务。
要办理这一个题目,最简单的方式就是将 socket 改成非阻塞,然后线程不停地轮询调用 read 操作来判定是否有数据,这种方式虽然该能够办理阻塞的题目,但是办理的方式比较粗暴,因为轮询是要消耗 CPU 的,而且随着一个 线程处置惩罚的毗连越多,轮询的服从就会越低。
上面的题目在于,线程并不知道当前毗连是否有数据可读,从而必要每次通过 read 去试探。
那有没有办法在只有当毗连上有数据的时候,线程才去发起读请求呢?答案是有的,实现这一技能的就是 I/O 多路复用。
I/O 多路复用技能会用一个系统调用函数来监听我们全部关心的毗连,也就说可以在一个监控线程内里监控许多的毗连。
https://i-blog.csdnimg.cn/direct/b7c3997e3093466592c51044ec495522.png
我们熟悉的 select/poll/epoll 就是内核提供给用户态的多路复用系统调用,线程可以通过一个系统调用函数从内核中获取多个变乱。
select/poll/epoll 是如何获取网络变乱的呢?
在获取变乱时,先把我们要关心的毗连传给内核,再由内核检测:
如果没有变乱发生,线程只需阻塞在这个系统调用,而无需像前面的线程池方案那样轮训调用 read 操作来判定是否有数据。
如果有变乱发生,内核会返回产生了变乱的毗连,线程就会从阻塞状态返回,然后在用户态中再处置惩罚这些毗连对应的业务即可。
[*]当下开源软件能做到网络高性能的原因就是 I/O 多路复用吗?
是的,根本是基于 I/O 多路复用,用过 I/O 多路复用接口写网络步伐的同砚,肯定知道是面向过程的方式写代码的,如许的开发的服从不高。
于是,大佬们基于面向对象的思想,对 I/O 多路复用作了一层封装,让使用者不用思量底层网络 API 的细节,只必要关注应用代码的编写。大佬们还为这种模式取了个让人第一时间难以理解的名字:Reactor 模式。
Reactor 翻译过来的意思是「反应堆」,大概各人会联想到物理学里的核反应堆,实际上并不是的这个意思。
这里的反应指的是「对变乱反应」,也就是来了一个变乱,Reactor 就有相对应的反应/相应。
究竟上,Reactor 模式也叫 Dispatcher 模式,我觉得这个名字更贴合该模式的含义,即 I/O 多路复用监听变乱,收到变乱后,根据变乱类型分配(Dispatch)给某个历程 / 线程。
Reactor 模式主要由 Reactor 和处置惩罚资源池这两个核心部分构成,它俩负责的事情如下:
[*]Reactor 负责监听和分发变乱,变乱类型包含毗连变乱、读写变乱;
[*]处置惩罚资源池负责处置惩罚变乱,如 read -> 业务逻辑 -> send;
Reactor 模式是机动多变的,可以应对差别的业务场景,机动在于:
[*]Reactor 的数量可以只有一个,也可以有多个;
[*]处置惩罚资源池可以是单个历程 / 线程,也可以是多个历程 /线程;
将上面的两个因素排列组设一下,理论上就可以有 4 种方案选择:
方案具体使用历程照旧线程,要看使用的编程语言以及平台有关:
[*]单 Reactor 单历程 / 线程;
[*]单 Reactor 多历程 / 线程;
[*]多 Reactor 单历程 / 线程;
[*]多 Reactor 多历程 / 线程; 其中,「多 Reactor 单历程 / 线程」实现方案相比「单 Reactor 单历程 / 线程」方案,不但复杂而且也没有性能优势,因此实际中并没有应用。
剩下的 3 个方案都是比较经典的,且都有应用在实际的项目中:
[*]单 Reactor 单历程 / 线程;
[*]单 Reactor 多线程 / 历程;
[*]多 Reactor 多历程 / 线程;
[*]Java 语言一般使用线程,好比 Netty;
[*]C 语言使用历程和线程都可以,比方 Nginx 使用的是历程,Memcache 使用的是线程。
[*] 接下来,分别先容这三个经典的 Reactor 方案。
2.1.单 Reactor 单历程 / 线程
一般来说,C 语言实现的是「单 Reactor 单历程」的方案,因为 C 语编写完的步伐,运行后就是一个独立的历程,不必要在历程中再创建线程。
我们来看看「单 Reactor 单历程」的方案示意图:https://i-blog.csdnimg.cn/direct/964e682ba7b9415892068b744f62a46b.png
可以看到历程里有 Reactor、Acceptor、Handler 这三个对象:
[*]Reactor 对象的作用是监听和分发变乱;
[*]Acceptor 对象的作用是获取毗连;
[*]Handler 对象的作用是处置惩罚业务;
[*] 对象里的 select、accept、read、send 是系统调用函数,dispatch 和 「业务处置惩罚」是必要完成的操作,其中 dispatch 是分发变乱操作。
接下来,先容下「单 Reactor 单历程」这个方案:
[*]Reactor 对象通过 select (IO 多路复用接口) 监听变乱,收到变乱后通过 dispatch 举行分发,具体分发给 Acceptor 对象照旧 Handler 对象,还要看收到的变乱类型;
[*]如果是毗连创建的变乱,则交由 Acceptor 对象举行处置惩罚,Accep
[*]tor 对象会通过 accept 方法 获取毗连,并创建一个 Handler 对象来处置惩罚后续的相应变乱;
[*]如果不是毗连创建变乱, 则交由当前毗连对应的 Handler 对象来举行相应;
[*]Handler 对象通过 read -> 业务处置惩罚 -> send 的流程来完成完整的业务流程。
[*]单 Reactor 单历程的方案因为全部工作都在同一个历程内完成,所以实现起来比较简单,不必要思量历程间通信,也不用担心多历程竞争。
[*] 但是,这种方案存在 2 个缺点:
[*]第一个缺点,因为只有一个历程,无法充分利用 多核 CPU 的性能;
[*]第二个缺点,Handler 对象在业务处置惩罚时,整个历程是无法处置惩罚其他毗连的变乱的,如果业务处置惩罚耗时比较长,那么就造成相应的延迟;
[*]所以,单 Reactor 单历程的方案不实用计算机麋集型的场景,只实用于业务处置惩罚非常快速的场景。 Redis 是由 C 语言实现的,它接纳的正是「单 Reactor 单历程」的方案,因为 Redis 业务处置惩罚主要是在内存中完成,操作的速度是很快的,性能瓶颈不在 CPU 上,所以 Redis 对于命令的处置惩罚是单历程的方案。
2.2.单 Reactor 多线程 / 多历程
如果要克服「单 Reactor 单线程 / 历程」方案的缺点,那么就必要引入多线程 / 多历程,如许就产生了单 Reactor 多线程/ 多历程的方案。
闻其名不如看其图,先来看看「单 Reactor 多线程」方案的示意图如下https://i-blog.csdnimg.cn/direct/feb8d5a212ec4877bf2cd66ec4e5995d.png
多历程
详细说一下这个方案:
[*]Reactor 对象通过 select (IO 多路复用接口) 监听变乱,收到变乱后通过 dispatch 举行分发,具体分发给 Acceptor 对象照旧 Handler 对象,还要看收到的变乱类型;
[*]如果是毗连创建的变乱,则交由 Acceptor 对象举行处置惩罚,Acceptor 对象会通过 accept 方法 获取毗连,并创建一个 Handler 对象来处置惩罚后续的相应变乱;
[*]如果不是毗连创建变乱, 则交由当前毗连对应的 Handler 对象来举行相应 上面的三个步骤和单 Reactor 单线程方案是一样的,接下来的步骤就开始不一样了:
[*]Handler 对象不再负责业务处置惩罚,只负责数据的接收和发送,Handler 对象通过 read 读取到数据后,会将数据发给子线程里的 Processor 对象举行业务处置惩罚;
[*] 子线程里的 Processor 对象就举行业务处置惩罚,处置惩罚完后,将效果发给主线程中的 Handler 对象,接着由 Handler 通过 send 方法将相应效果发送给 client;
[*] 单 Reator 多线程的方案优势在于能够充分利用多核 CPU 的能,那既然引入多线程,那么自然就带来了多线程竞争资源的题目。
比方,子线程完成业务处置惩罚后,要把效果传递给主线程的 Reactor 举行发送,这里涉及共享数据的竞争。
要制止多线程由于竞争共享资源而导致数据错乱的题目,就必要在操作共享资源前加上互斥锁,以保证恣意时间里只有一个线程在操作共享资源,待该线程操作完释放互斥锁后,其他线程才有时机操作共享数据。
聊完单 Reactor 多线程的方案,接着来看看单 Reactor 多历程的方案
[*]究竟上,单 Reactor 多历程相比单 Reactor 多线程实现起来很麻烦,主要因为要思量子历程 <-> 父历程的双向通信,而且父历程还得知道子历程要将数据发送给哪个客户端。
[*] 而多线程间可以共享数据,虽然要额外思量并发题目,但是这远比历程间通信的复杂度低得多,因此实际应用中也看不到单 Reactor 多历程的模式。
另外,「单 Reactor」的模式另有个题目,因为一个 Reactor 对象承担全部变乱的监听和相应,而且只在主线程中运行,在面对瞬间高并发的场景时,容易成为性能的瓶颈的地方。
2.3.多 Reactor 多历程 / 线程
要办理「单 Reactor」的题目,就是将「单 Reactor」实现成「多 Reactor」,如许就产生了第 多 Reactor 多历程 / 线程的方案。
老规矩,闻其名不如看其图。多 Reactor 多历程 / 线程方案的示意图如下(以线程为例):
https://i-blog.csdnimg.cn/direct/d3dc5f69748b42eb8f1a75550d6a533f.png
方案详细阐明如下:
[*]主线程中的 MainReactor 对象通过 select 监控毗连创建变乱,收到变乱后通过 Acceptor 对象中的 accept 获取毗连,将新的毗连分配给某个子线程;
[*]子线程中的 SubReactor 对象将 MainReactor 对象分配的毗连加入 select 继续举行监听,并创建一个 Handler 用于处置惩罚毗连的相应变乱。
[*]如果有新的变乱发生时,SubReactor 对象会调用当前毗连对应的 Handler 对象来举行相应。
[*]Handler 对象通过 read -> 业务处置惩罚 -> send 的流程来完成完整的业务流程。
[*] 多 Reactor 多线程的方案虽然看起来复杂的,但是实际实现时比单 Reactor 多线程的方案要简单的多,原因如下:
[*]主线程和子线程分工明确,主线程只负责接收新毗连,子线程负责完成后续的业务处置惩罚。
[*]主线程和子线程的交互很简单,主线程只必要把新毗连传给子线程,子线程无须返回数据,直接就可以在子线程将处置惩罚效果发送给客户端。 大名鼎鼎的两个开源软件 Netty 和 Memcache 都接纳了「多 Reactor 多线程」的方案。
接纳了「多 Reactor 多历程」方案的开源软件是 Nginx,不过方案与尺度的多 Reactor 多历程有些差异。
具体差异体如今主历程中仅仅用来初始化 socket,并没有创建 mainReactor 来 accept 毗连,而是由子历程的 Reactor 来 accept 毗连,通过锁来控制一次只有一个子历程举行 accept(防止出现惊群征象),子历程 accept 新毗连后就放到自己的 Reactor 举行处置惩罚,不会再分配给其他子历程。
3.实现单 Reactor 单历程版本的TCP服务器
接下来我们就要来实现一个单 Reactor 单历程版本的TCP服务器
首先我们必要下面这些文件
https://i-blog.csdnimg.cn/direct/2b597f85bdcf45ed821c2b036b487b32.png
这个Socket.hpp,nocopy.hpp,Epoller.hpp都是我们封装好了的,我们直接使用就行了,接下来我们只必要编写tcpserver.hpp和main.cc
3.1.Connection类
承接上一节中的 epoll 服务器:如今的题目是,来自用户的数据大概会被 TCP 协议拆分成多个报文,那么服务器怎么才能知道什么时候最后一个小报文被接收了呢?要保证完整地读取客户端发送的数据,服务器必要将这次读取到的数据保存起来,对它们举行一定的处置惩罚(报文大概会有报头,以办理粘包题目),最后将它们拼接起来,再向上层应用步伐交付。
题目是 Recver 中的缓冲区 buffer 是一个局部变量,每次循环都会重置。而服务端大概会有成百上千个来自客户端创建毗连后打开的文件描述符,这无法保证为每个文件描述符都保存本轮循环读取的数据。
办理办法是为套接字文件描述符创建独立的接收和发送缓冲区,因为套接字是基于毗连的,所以用一个名为 Connection 的类来保存全部和毗连相干的属性,比方文件描述符,收发缓冲区,以及对文件描述符的操作(包括读、写和异常操作),所以要设置三个回调函数以供后续在差别的分支调用,最后还要设置一个回指指针,它将会保存服务器对象的地点,到背面会先容它的用处。
#pragma once
#include<iostream>
#include<string>
#include<functional>
#include<memory>
class Connection;
using func_t =std::function<void(std::shared_ptr<Connection>)>;
class Connection
{
private:
int _sock;
std::string _inbuffer;//这里来当输入缓冲区,但是这里是有缺点的,它不能处理二进制流
std::string _outbuffer;
func_t _recv_cb;//读回调函数
func_t _send_cb;//写回调函数
func_t _except_cd;//
//添加一个回指指针
std::shared_ptr<TcpServer> _tcp_server_ptr;
};
class TcpServer
{
};
Connection布局中除了包含文件描述符和其对应的读回调、写回调和异常回调外,还包含一个输入缓冲区_inbuffer、一个输出缓冲区_outbuffer以及一个回指指针_tcp_setver_ptr
当某个文件描述符的读变乱就绪时,调用recv函数读取客户端发来的数据,但并不能保证读到了一个完整报文,因此必要将读取到的数据暂时存放到该文件描述符对应的_inBuffer中,当_inBuffer中可以分离出一个完整的报文后再将其分离出来举行数据处置惩罚,_inBuffer本质就是用来办理粘包题目的
当处置惩罚完一个报文请求后,需将相应数据发送给客户端,但并不能保证底层TCP的发送缓冲区中有足够的空间写入,因此需将要发送的数据暂时存放到该文件描述符对应的_outBuffer中,当底层TCP的发送缓冲区中有空间,即写变乱就绪时,再依次发送_outBuffer中的数据
Connection布局中设置回指指针_svrPtr,便于快速找到TcpServer对象,因为后续必要根据Connection布局找到这个TcpServer对象。如上层业务处置惩罚函数NetCal函数向_outBuffer输出缓冲区递交数据后,需通过Connection中的回指指针,"提醒"TcpServer举行处置惩罚
Connection布局中需提供一个管理回调的成员函数,便于外部对回调举行设置
3.2.TcpServer类
按照之前的经验,我们很快就能写出下面这个
#include <memory> // 包含shared_ptr和unique_ptr等智能指针的头文件
#include <unordered_map> // 包含unordered_map的头文件
class TcpServer
{
public:
// 构造函数,初始化TCP服务器
// 接收一个端口号作为参数,用于创建监听套接字,并初始化事件轮询器
TcpServer(uint16_t port)
: _port(port), // 存储端口号
_listensock_ptr(new Sock()), // 创建一个Sock对象用于监听,并使用shared_ptr管理
_epoller_ptr(new Epoller()) // 创建一个Epoller对象用于事件轮询,并使用shared_ptr管理
{
_listensock_ptr->Socket(); // 调用Sock的Socket方法创建套接字
_listensock_ptr->Bind(_port); // 绑定端口号
_listensock_ptr->Listen(); // 开始监听
// 注意:这里可能还需要将监听套接字添加到事件轮询器中,但代码中没有显示
}
// 初始化方法,可以在这里添加额外的初始化逻辑
// 但从提供的代码来看,这个方法目前是空的
void Init()
{
// 可以在这里添加初始化代码
}
// 启动方法,用于启动服务器的事件循环
// 但从提供的代码来看,这个方法目前是空的
// 通常,这里会包含启动事件轮询器的逻辑
void Start()
{
// 启动事件轮询器,开始处理网络事件
// 注意:实际代码中需要实现这部分逻辑
}
// 析构函数,用于清理资源
// 注意:由于使用了shared_ptr,这里的资源清理将是自动的
// 但如果还有其他需要手动清理的资源(如文件描述符等),则应该在这里处理
~TcpServer()
{
// 析构函数会自动调用shared_ptr的析构,从而销毁Sock和Epoller对象
// 如果需要,可以在这里添加额外的清理代码
}
private:
uint16_t _port; // 存储服务器监听的端口号
std::shared_ptr<Epoller> _epoller_ptr; // 事件轮询器的智能指针
std::unordered_map<int, std::shared_ptr<Connection>> _connections; // 存储连接的哈希表,键为套接字描述符,值为Connection对象的智能指针
std::shared_ptr<Sock> _listensock_ptr; // 监听套接字的智能指针
};
这里答复一个题目
[*]当服务器开始运行时,一定会有大量的Connection布局体对象必要被new出来,那么这些布局体对象需不必要被管理呢?
固然是必要的,所以在服务器类内里,定义了一个哈希表_connections,用sock来作为哈希表的键值,sock对应的布局体connection和sock一起作为键值对,也就是哈希桶中存储的值(存储键值对<sock, connection>),今天是不会出现哈希冲突的,所以每个键值下面的哈希桶只会挂一个键值对,即一个<sock, connection>.
初始化服务器时,第一个必要被添加到哈希表中的sock,一定是listensock,所以在init方法中,先把listensock添加到哈希表内里,添加的同时还要传该listensock所对应的关心变乱的方法,对于listensock来说,只必要关注读方法即可,其他两个方法设为nullptr即可。
接下来是使用ET模式的,所以我们要实施下面这三点
相比于LT模式,关于ET(边缘触发)模式的使用,确实必要关注这几个关键点:
1.设置EPOLLET标记:在将文件描述符(如socket)添加到epoll实例时,你必要通过epoll_event布局体中的events字段设置EPOLLET标记,以启用边缘触发模式。这告诉epoll,你希望在该文件描述符的状态从非就绪变为就绪时只接收一次变乱通知。
2.将socket文件描述符设置为非阻塞:由于ET模式要求你能够在一个变乱通知中尽大概多地处置惩罚数据,直到没有更多数据可读,因此将socket设置为非阻塞模式是非常告急的。这允许你的read调用在没有数据可读时立即返回EAGAIN或EWOULDBLOCK,而不是阻塞等候。
3.循环调用read直到返回EAGAIN或EWOULDBLOCK:在接收到一个ET模式的变乱通知后,你必要在一个循环中调用read函数,不停尝试从socket中读取数据,直到read返回EAGAIN或EWOULDBLOCK。这表现当前没有更多数据可读,你可以安全地继续等候下一个变乱通知。
为了方便使用,我们将设置文件描述符为非阻塞的代码封装到set_non_blocking函数内里去了
TcpServer类
class TcpServer : public nocopy
{
static const int num = 64;
public:
TcpServer(uint16_t port) : _port(port),
_listensock_ptr(new Sock()),
_epoller_ptr(new Epoller()),
_quit(true)
{
_listensock_ptr->Socket();
set_non_blocking(_listensock_ptr->Fd()); // 设置listen文件描述符为非阻塞
_listensock_ptr->Bind(_port);
_listensock_ptr->Listen();
}
~TcpServer()
{
}
// 设置文件描述符为非阻塞
int set_non_blocking(int fd)
{
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1)
{
perror("fcntl: get flags");
return -1;
}
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1)
{
perror("fcntl: set non-blocking");
return -1;
}
return 0;
}
void Init()
{
}
void Start()
{
_quit = false;
// 将listen套接字添加到epoll中->将listensock和他关心的事件,添加到内核的epoll模型中的红黑树里面
// 1.将listensock添加到红黑树
_epoller_ptr->EpollUpDate(EPOLL_CTL_ADD, _listensock_ptr->Fd(), (EPOLLIN | EPOLLET)); // 注意这里的EPOLLET设置了ET模式
// 设置listen文件描述符为非阻塞,这个在初始化已经完成了
struct epoll_event revs; // 专门用来处理事件的
while (!_quit)
{
int n=_epoller_ptr->EpollerWait(revs,num);
for(int i=0;i<num;i++)
{
}
}
_quit = true;
}
private:
uint16_t _port;
std::shared_ptr<Epoller> _epoller_ptr;
std::unordered_map<int, std::shared_ptr<Connection>> _connections;
std::shared_ptr<Sock> _listensock_ptr;
bool _quit;
};
3.3.Connection的真正用处
写到这里,我们发现我们这个代码和之前的不是差不多吗?但究竟上,如今才到关键的地方!!
我们在上面提及:
当某个文件描述符的读变乱就绪时,调用recv函数读取客户端发来的数据,但并不能保证读到了一个完整报文,因此必要将读取到的数据暂时存放到该文件描述符对应的_inBuffer中,当_inBuffer中可以分离出一个完整的报文后再将其分离出来举行数据处置惩罚,_inBuffer本质就是用来办理粘包题目的
当处置惩罚完一个报文请求后,需将相应数据发送给客户端,但并不能保证底层TCP的发送缓冲区中有足够的空间写入,因此需将要发送的数据暂时存放到该文件描述符对应的_outBuffer中,当底层TCP的发送缓冲区中有空间,即写变乱就绪时,再依次发送_outBuffer中的数据
写到这里,我们必要理解,我们不但仅必要将listen套接字加入到我们的epoll的红黑树内里,我还必要将listen套接字创建一个Connection对象,将listen套接字加入到Connection中,同时还要即要将<listen套接字,Connection>放到我们的unordered_map对象_connections内里去。只要我们把Connection对象管理好了,我们的毗连就管理好了。
所以,我们如今必要重新编写一下我们的毗连类——Connection类
Connection类
class Connection
{
public:
Connection(int sock, std::shared_ptr<TcpServer> tcp_server_ptr) : _sock(sock), _tcp_server_ptr(tcp_server_ptr)
{
}
~Connection()
{
}
void setcallback(func_t recv_cb, func_t send_cb, func_t except_cb)
{
_recv_cb = recv_cb;
_send_cb = send_cb;
_except_cd = except_cb;
}
private:
int _sock;
std::string _inbuffer; // 这里来当输入缓冲区,但是这里是有缺点的,它不能处理二进制流
std::string _outbuffer;
func_t _recv_cb; // 读回调函数
func_t _send_cb; // 写回调函数
func_t _except_cd; //
// 添加一个回指指针
std::shared_ptr<TcpServer> _tcp_server_ptr;
};
如今我们就必要来实现我们上面的内容了
void AddConnection(int sock, uint32_t event, func_t recv_cb, func_t send_cb, func_t except_cb,
std::string clientip="0.0.0.0",uint16_t clientport=0)
{
// 1.给listen套接字创建一个Connection对象
std::shared_ptr<Connection> new_connection = std::make_shared<Connection>(sock, std::shared_ptr<TcpServer>(this)); // 创建Connection对象
new_connection->setcallback(recv_cb, send_cb, except_cb);
// 2.添加到_connections里面去
_connections.insert(std::make_pair(sock, new_connection));
// 将listen套接字添加到epoll中->将listensock和他关心的事件,添加到内核的epoll模型中的红黑树里面
// 3.将listensock添加到红黑树
_epoller_ptr->EpollUpDate(EPOLL_CTL_ADD, sock, event); // 注意这里的EPOLLET设置了ET模式
std::cout << "add a new connection success,sockfd:" << sock << std::endl;
}
正如上面所说,这个函数完成了三件事情。
[*]1.给listen套接字创建一个Connection对象
[*]2.添加到_connections内里去
[*]3.将listensock添加到红黑树
接下来我们就可以修改我们的Init函数
init函数
void Init()
{
_listensock_ptr->Socket();
set_non_blocking(_listensock_ptr->Fd()); // 设置listen文件描述符为非阻塞
_listensock_ptr->Bind(_port);
_listensock_ptr->Listen();
AddConnection(_listensock_ptr->Fd(),(EPOLLIN|EPOLLET),nullptr,nullptr,nullptr);//暂时设置成nullptr
}
3.4.Dispatcher——变乱派发器
我们在这里将Start函数更名为Loop函数,而且创建一个新函数Dispatcher
bool IsConnectionSafe(int fd)
{
auto iter = _connections.find(fd);
if (iter == _connections.end())
{
return false;
}
else
{
return true;
}
}
void Dispatcher() // 事件派发器
{
int n = _epoller_ptr->EpollerWait(revs, num); // 获取已经就绪的事件
for (int i = 0; i < num; i++)
{
uint32_t events = revs.events;
int sock = revs.data.fd;
// 如果出现异常,统一转发为读写问题,只需要处理读写就行
if (events & EPOLLERR) // 出现错误了
{
events |= (EPOLLIN | EPOLLOUT);
}
if (events & EPOLLHUP)
{
events |= (EPOLLIN | EPOLLOUT);
}
// 只需要处理读写就行
if (events & EPOLLIN&&IsConnectionSafe(sock)) // 读事件就绪
{
if(_connections->_recv_cb)
_connections->_recv_cb(_connections);
}
if (events & EPOLLOUT&&IsConnectionSafe(sock)) // 写事件就绪
{
if(_connections->_send_cb)
_connections->_send_cb(_connections);
}
}
}
void Loop()
{
_quit = false;
while (!_quit)
{
Dispatcher();
}
_quit = true;
}
变乱派发器是真正服务器要开始运行了,服务器会迁就绪的每个毗连都举行处置惩罚,首先如果毗连不在哈希表中,那就阐明这个毗连中的sock还没有被添加到epoll模子中的红黑树,不能直接举行处置惩罚,必要先添加到红黑树中,然后让epoll_wait来拿取就绪的毗连再告知步伐员,这个时候再举行处置惩罚,如许才不会等候,而是直接举行数据拷贝。
Loop中处置惩罚就绪的变乱的方法非常非常的简单,如果该就绪的fd关心的是读变乱,那就直接调用该sock所在毗连布局体内部的读方法即可,如果是写变乱那就调用写方法即可。有人说那如果fd关心异常变乱呢?着实异常变乱大部分也都是读变乱,不过也有写变乱,所以处置惩罚异常的逻辑我们直接放到读方法和写方法内里即可,当有异常变乱到来时,直接去对应的读方法或写方法内里执行对应的逻辑即可。
以下是一些大概触发 EPOLLERR 变乱的环境的示例:
[*]1.毗连错误:当使用非阻塞套接字举行毗连时,如果毗连失败,套接字的 epoll 变乱集中将包含 EPOLLERR 变乱。可以通过检査 events 字段中是否包含 EPOLLERR 来处置惩罚毗连错误。 2.接收错误:在非阻塞套接字上举行读取操作时,如果发生错误,比方对方关闭了毗连或者接收缓冲区溢出,套接字的 epol 变乱集中将包含 EPOLLERR 变乱。
3.发送错误:在非阻塞套接字上举行写入操作时,如果发生错误,比方对方关闭了毗连或者发送缓冲区溢出,套接字的 epol 变乱集中将包含 EPOLLERR 变乱。
[*]4.文件操作错误:当使用 epol 监听文件描述符时,如果在读取或写入文件时发生错误,文件描述符的 epol 变乱集中将包含 EPOLLERR 变乱。
必要留意的是,EPOLLERR 变乱通常与 EPOLLIN 或 EPOLLOUT 变乱一起使用。当发生 EPOLLERR 变乱时,通常也会同时发生 EPOLLIN 或EPOLLOUT 变乱..
假设某个异常变乱发生了,那么这个异常变乱会自动被内核设置到epoll_wait返回的变乱集中,这个异常变乱一定会和一个sock关联,好比客户端和服务器用sock通信着,忽然客户端关闭毗连,那么服务器的sock上原本关心着读变乱,此时内核会自动将异常变乱设置到该sock关心的变乱聚集里,在处置惩罚sock关心的读变乱时,读方法会捎带处置惩罚掉这个异常变乱,处置惩罚方式为服务器关闭通信的sock,因为客户端已经把毗连断开了,服务器没必要维护和这个客户端的毗连了,服务器也断开就好,如许的逻辑在读方法内里就可以实现。
3.5.回调函数
我们看上面的变乱派发器,最后都是派发给_send_cb和_recv_cb函数,这两个函数我们还没有设置呢!所以我们必要来设置一下
void Init()
{
_listensock_ptr->Socket();
set_non_blocking(_listensock_ptr->Fd()); // 设置listen文件描述符为非阻塞
_listensock_ptr->Bind(_port);
_listensock_ptr->Listen();
AddConnection(_listensock_ptr->Fd(), (EPOLLIN | EPOLLET),
std::bind(&TcpServer::Accepter, this, std::placeholders::_1), nullptr, nullptr); // 暂时设置成nullptr
}
void Accepter(std::shared_ptr<Connection> conection)
{
while (1)
{
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
int sock=accept(conection->Getsock(),(sockaddr*)&peer,&len);
if(sock>0)
{
set_non_blocking(sock);//设置非阻塞
AddConnection (sock,EPOLLIN,nullptr,nullptr,nullptr);
}
else{
if(errno==EWOULDBLOCK) break;
else if(errno==EINTR) break;
else break;
}
}
}
[*] 在代码实现上,给AddConnection传参时,用到了一个C++11的知识,就是bind绑定的使用,一般环境下,如果你将包装器包装的函数指针类型传参给包装器类型时,是没有任何题目的,因为包装器本质就是一个仿函数,内部调用了被包装的对象的方法,所以传参是没有任何题目的。
[*] 但如果你要是在类内传参,那就有题目了,会出现类型不匹配的题目,这个题目真的很恶心,而且这个题目一报错就劈里啪啦的报一大堆错,因为function是模板,C++报错最恶心的就是模板报错,一报错人都要炸了。话说回来,为什么是类型不匹配呢?因为在类内调用类内方法时,着实是通过this指针来调用的,如果你直接将Accepter方法传给AddConnection,两者类型是不匹配的,因为Accepter的第一个参数是this指针,精确的做法是利用包装器的适配器bind来举行传参,bind将Accepter举行绑定,前两个参数为绑定的对象类型 和 给绑定的对象所传的参数,因为Accepter第一个参数是this指针,所以第一个参数就可以固定传this,背面的一个参数不应该是如今传,而应该是调用Accepter方法的时候再传,只有如许才能在类内将类成员函数指针传给包装器类型。
[*] 不过吧另有一种不常用的方法,就是利用lambda表达式来举行传参,lambda可以捕捉上下文的this指针,然后再把lambda类型传给包装器类型,这种方式不常用,用起来也怪别扭的,function和bind是适配模式,两者搭配在一起用照旧更顺眼一些,lambda这种方式了解一下就好。
为了演示效果,我们写了一个打印函数来展示!
void Accepter(std::shared_ptr<Connection> conection)
{
while (1)
{
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
int sock=accept(conection->Getsock(),(sockaddr*)&peer,&len);
if(sock>0)
{
//获取客户端信息
uint16_t clientport=ntohs(peer.sin_port);
char buffer;
inet_ntop(AF_INET,&(peer.sin_addr),buffer,sizeof(buffer));
std::cout<<"get a new client from:"<<buffer<<conection->Getsock()<<std::endl;
set_non_blocking(sock);//设置非阻塞
AddConnection (sock,EPOLLIN,nullptr,nullptr,nullptr);
}
else{
if(errno==EWOULDBLOCK) break;
else if(errno==EINTR) break;
else break;
}
}
}
void PrintConnection()
{
std::cout<<"_connections fd list: "<<std::endl;
for(auto&connection:_connections)
{
std::cout<<connection.second->Getsock()<<" ";
}
std::cout<<std::endl;
}
tcpserver.hpp
#pragma once#include <iostream>#include <string>#include <functional>#include <memory>#include <unordered_map> #include "Socket.hpp"#include "Epoller.hpp"#include "nocopy.hpp" class Connection;using func_t = std::function<void(std::shared_ptr<Connection>)>;class TcpServer; class Connection{public: Connection(int sock, std::shared_ptr<TcpServer> tcp_server_ptr) : _sock(sock), _tcp_server_ptr(tcp_server_ptr) { } ~Connection() { } void setcallback(func_t recv_cb, func_t send_cb, func_t except_cb) { _recv_cb = recv_cb; _send_cb = send_cb; _except_cb = except_cb; } int Getsock() { return _sock; } private: int _sock; std::string _inbuffer; // 这里来当输入缓冲区,但是这里是有缺点的,它不能处置惩罚二进制流 std::string _outbuffer; public: func_t _recv_cb; // 读回调函数 func_t _send_cb; // 写回调函数 func_t _except_cb; // // 添加一个回指指针 std::shared_ptr<TcpServer> _tcp_server_ptr;};class TcpServer : public nocopy{ static const int num = 64; public: TcpServer(uint16_t port) : _port(port), _listensock_ptr(new Sock()), _epoller_ptr(new Epoller()), _quit(true) { } ~TcpServer() { } // 设置文件描述符为非阻塞 int set_non_blocking(int fd) { int flags = fcntl(fd, F_GETFL, 0); if (flags == -1) { perror("fcntl: get flags"); return -1; } if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) { perror("fcntl: set non-blocking"); return -1; } return 0; } void Init() { _listensock_ptr->Socket(); set_non_blocking(_listensock_ptr->Fd()); // 设置listen文件描述符为非阻塞 _listensock_ptr->Bind(_port); _listensock_ptr->Listen(); AddConnection(_listensock_ptr->Fd(), (EPOLLIN | EPOLLET), std::bind(&TcpServer::Accepter, this, std::placeholders::_1), nullptr, nullptr); // 暂时设置成nullptr } void Accepter(std::shared_ptr<Connection> conection)
{
while (1)
{
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
int sock=accept(conection->Getsock(),(sockaddr*)&peer,&len);
if(sock>0)
{
//获取客户端信息
uint16_t clientport=ntohs(peer.sin_port);
char buffer;
inet_ntop(AF_INET,&(peer.sin_addr),buffer,sizeof(buffer));
std::cout<<"get a new client from:"<<buffer<<conection->Getsock()<<std::endl;
set_non_blocking(sock);//设置非阻塞
AddConnection (sock,EPOLLIN,nullptr,nullptr,nullptr);
}
else{
if(errno==EWOULDBLOCK) break;
else if(errno==EINTR) break;
else break;
}
}
}
void PrintConnection()
{
std::cout<<"_connections fd list: "<<std::endl;
for(auto&connection:_connections)
{
std::cout<<connection.second->Getsock()<<" ";
}
std::cout<<std::endl;
} void AddConnection(int sock, uint32_t event, func_t recv_cb, func_t send_cb, func_t except_cb) { // 1.给listen套接字创建一个Connection对象 std::shared_ptr<Connection> new_connection = std::make_shared<Connection>(sock, std::shared_ptr<TcpServer>(this)); // 创建Connection对象 new_connection->setcallback(recv_cb, send_cb, except_cb); // 2.添加到_connections内里去 _connections.insert(std::make_pair(sock, new_connection)); // 将listen套接字添加到epoll中->将listensock和他关心的变乱,添加到内核的epoll模子中的红黑树内里 // 3.将listensock添加到红黑树 _epoller_ptr->EpollUpDate(EPOLL_CTL_ADD, sock, event); // 留意这里的EPOLLET设置了ET模式 std::cout<<"add a new connection success,sockfd:"<<sock<<std::endl; } bool IsConnectionSafe(int fd) { auto iter = _connections.find(fd); if (iter == _connections.end()) { return false; } else { return true; } } void Dispatcher() // 变乱派发器 { int n = _epoller_ptr->EpollerWait(revs, num); // 获取已经就绪的变乱 for (int i = 0; i < num; i++) { uint32_t events = revs.events; int sock = revs.data.fd; // 如果出现异常,同一转发为读写题目,只必要处置惩罚读写就行 if (events & EPOLLERR) // 出现错误了 { events |= (EPOLLIN | EPOLLOUT); } if (events & EPOLLHUP) { events |= (EPOLLIN | EPOLLOUT); } // 只必要处置惩罚读写就行 if (events & EPOLLIN && IsConnectionSafe(sock)) // 读变乱就绪 { if (_connections->_recv_cb) _connections->_recv_cb(_connections); } if (events & EPOLLOUT && IsConnectionSafe(sock)) // 写变乱就绪 { if (_connections->_send_cb) _connections->_send_cb(_connections); } } } void Loop() { _quit = false; while (!_quit) { Dispatcher(); PrintConnection(); } _quit = true; } private: uint16_t _port; std::shared_ptr<Epoller> _epoller_ptr; std::unordered_map<int, std::shared_ptr<Connection>> _connections; std::shared_ptr<Sock> _listensock_ptr; bool _quit; struct epoll_event revs; // 专门用来处置惩罚变乱的};
我们看看效果
https://i-blog.csdnimg.cn/direct/03d5176ff40b4f9fa3bd1db9c4d9844b.png
https://i-blog.csdnimg.cn/direct/38705aedcd1240558a63c98ccf47c617.png
https://i-blog.csdnimg.cn/direct/9317d15ebdff4ed494c75e8a617c455e.png
https://i-blog.csdnimg.cn/direct/000e58a6c70849f192da49b9c03ff52d.png
https://i-blog.csdnimg.cn/direct/cceb8ce4e8864c3aaa9713a59fa4926a.png
还可以啊!!!到这里我们就算是买通了我们毗连的过程,我们接着看
3.6.区分读写异常变乱
我们看看这个Accepter函数
void Accepter(std::shared_ptr<Connection> conection)
{
while (1)
{
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
int sock=accept(conection->Getsock(),(sockaddr*)&peer,&len);
if(sock>0)
{
//获取客户端信息
uint16_t clientport=ntohs(peer.sin_port);
char buffer;
inet_ntop(AF_INET,&(peer.sin_addr),buffer,sizeof(buffer));
std::cout<<"get a new client from:"<<buffer<<conection->Getsock()<<std::endl;
set_non_blocking(sock);//设置非阻塞
AddConnection (sock,EPOLLIN,nullptr,nullptr,nullptr);
}
else{
if(errno==EWOULDBLOCK) break;
else if(errno==EINTR) break;
else break;
}
}
}
留意内里的AddConnection函数,这个函数是用文件描述符来创建Connection对象的。但是文件描述符有两种啊!一个是listen套接字,一个是普通套接字。Listen套接字只关心读变乱,而其他文件描述符则是关心读,写,异常变乱的!!但是我们在这里却同一使用了AddConnection (sock,EPOLLIN,nullptr,nullptr,nullptr);一棍子打死,只关心读变乱。如许子是非常不公道的。
所以对应AddConnection (sock,EPOLLIN,nullptr,nullptr,nullptr);这里,我们必要修改
//连接管理器
void Accepter(std::shared_ptr<Connection> conection)
{
while (1)
{
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
int sock=accept(conection->Getsock(),(sockaddr*)&peer,&len);
if(sock>0)
{
//获取客户端信息
uint16_t clientport=ntohs(peer.sin_port);
char buffer;
inet_ntop(AF_INET,&(peer.sin_addr),buffer,sizeof(buffer));
std::cout<<"get a new client from:"<<buffer<<conection->Getsock()<<std::endl;
set_non_blocking(sock);//设置非阻塞
AddConnection (sock,EPOLLIN,
std::bind(&TcpServer::Recver, this, std::placeholders::_1),
std::bind(&TcpServer::Sender, this, std::placeholders::_1),
std::bind(&TcpServer::Excepter, this, std::placeholders::_1);
}
else{
if(errno==EWOULDBLOCK) break;
else if(errno==EINTR) break;
else break;
}
}
}
//事件管理器
void Recver(std::shared_ptr<Connection> conection)
{
std::cout<<"haha ,got you"<<conection->Getsock()<<std::endl;
}
void Sender(std::shared_ptr<Connection> conection)
{}
void Excepter(std::shared_ptr<Connection> conection)
{} 我们可以测试一下,到底有没有用
https://i-blog.csdnimg.cn/direct/30b82b006eb64205b7347537c85b6446.png
https://i-blog.csdnimg.cn/direct/c39bfa860a834e62862bb4c721c466d8.png
我们发现它在不停打印!阐明我们成功了!!
3.7.读变乱的处置惩罚
好了,我们如今就应该来处置惩罚读写异常变乱
[*]读变乱的处置惩罚
Recver这里照旧和之前一样的题目,也是前面在写三个多路转接接口服务器时,不停没有处置惩罚的题目,你怎么保证你一次就把全部数据全部都读上来了呢?
如果不能保证,那就和Accepter一样,必须打死循环来举行读取,当recv返回值大于0,那我们就把读取到的数据先放入缓冲区,缓冲区在哪里呢?
着实就在connection参数所指向的布局体内里,布局体里会有sock所对应的收发缓冲区。然后就调用外部传入的回调函数_service,对服务器收到的数据举行应用层的业务逻辑处置惩罚。
[*]当recv读到0时,阐明客户端把毗连关了,那这
[*]就算异常变乱,直接回调sock对应的异常处置惩罚方法即可。
[*]当recv的返回值小于0,同时错误码被设置为EAGAIN或EWOULDBLOCK时,则阐明recv已经把sock底层的数据全部读走了,则此时直接break跳出循环即可。也有大概是被信号给停止了,则此时应该继续执行循环
[*]另外一种环境就是recv系统调用真的出错了,则此时也调用sock的异常方法举行处置惩罚即可。
[*] 业务逻辑处置惩罚方法应该在本次循环读取到全部的数据之后再举行处置惩罚。
class Connection
{
......
void Append(std::string info)//读取成功了,就把读取的内容存到这里来!
{
_inbuffer+=info;
}
private:
int _sock;
std::string _inbuffer; // 这里来当输入缓冲区,但是这里是有缺点的,它不能处理二进制流
std::string _outbuffer;
};
我们接着写,
为了方便,我们这里给Connection类加入两个新成员,下面是修改的部分
class Connection
{
public:
uint16_t _clientport;
std::string _clientip;
};
class TcpServer : public nocopy
{
// 连接管理器
void Accepter(std::shared_ptr<Connection> conection)
{
while (1)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(conection->Getsock(), (sockaddr *)&peer, &len);
if (sock > 0)
{
// 获取客户端信息
uint16_t clientport = ntohs(peer.sin_port);
char buffer;
inet_ntop(AF_INET, &(peer.sin_addr), buffer, sizeof(buffer));
std::cout << "get a new client from:" << buffer << conection->Getsock() << std::endl;
set_non_blocking(sock); // 设置非阻塞
AddConnection(sock, EPOLLIN,
std::bind(&TcpServer::Recver, this, std::placeholders::_1),
std::bind(&TcpServer::Sender, this, std::placeholders::_1),
std::bind(&TcpServer::Excepter, this, std::placeholders::_1),
buffer,clientport);
}
else
{
if (errno == EWOULDBLOCK)
break;
else if (errno == EINTR)
break;
else
break;
}
}
}
// 事件管理器
void Recver(std::shared_ptr<Connection> conection)
{
int sock = conection->Getsock();
while (1)
{
char buffer;
memset(buffer, 0, sizeof(buffer));
ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0); // 非阻塞读取
if (n > 0) // 成功了!!
{
conection->Append(buffer); // 把读取的数据放到Connection对象的输入缓冲区里面
}
else if (n == 0)
{
}
else
{
}
}
}
void AddConnection(int sock, uint32_t event, func_t recv_cb, func_t send_cb, func_t except_cb,
std::string clientip="0.0.0.0",uint16_t clientport=0)
{
// 1.给listen套接字创建一个Connection对象
std::shared_ptr<Connection> new_connection = std::make_shared<Connection>(sock, std::shared_ptr<TcpServer>(this)); // 创建Connection对象
new_connection->setcallback(recv_cb, send_cb, except_cb);
new_connection->_clientip=clientip;
new_connection->_clientport=clientport;
// 2.添加到_connections里面去
_connections.insert(std::make_pair(sock, new_connection));
// 将listen套接字添加到epoll中->将listensock和他关心的事件,添加到内核的epoll模型中的红黑树里面
// 3.将listensock添加到红黑树
_epoller_ptr->EpollUpDate(EPOLL_CTL_ADD, sock, event); // 注意这里的EPOLLET设置了ET模式
std::cout << "add a new connection success,sockfd:" << sock << std::endl;
}
};具体修改看代码即可
我们接着美满我们的读变乱的处置惩罚
// 事件管理器
void Recver(std::shared_ptr<Connection> conection)
{
int sock = conection->Getsock();
while (1)
{
char buffer;
memset(buffer, 0, sizeof(buffer));
ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0); // 非阻塞读取
if (n > 0) // 成功了!!
{
conection->Append(buffer); // 把读取的数据放到Connection对象的输入缓冲区里面
}
else if (n == 0) // 客户端
{
std::cout << "sockfd:" << sock << ",client:" << conection->_clientip << ":" << conection->_clientport << "quit" << std::endl;
Excepter(conection);
}
else
{
if (errno == EWOULDBLOCK)
break;
else if (errno == EINTR)
break;
else
{
std::cout << "sockfd:" << sock << ",client:" << conection->_clientip << ":" << conection->_clientport << "recv err" << std::endl;
Excepter(conection);
}
}
}
}
void Sender(std::shared_ptr<Connection> conection)
{
}
void Excepter(std::shared_ptr<Connection> conection)
{
}
好,我们把源代码拿出来测试一下
tcpserver.hpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <memory>
#include <unordered_map>
#include "Socket.hpp"
#include "Epoller.hpp"
#include "nocopy.hpp"
class Connection;
using func_t = std::function<void(std::shared_ptr<Connection>)>;
class TcpServer;
class Connection
{
public:
Connection(int sock, std::shared_ptr<TcpServer> tcp_server_ptr) : _sock(sock), _tcp_server_ptr(tcp_server_ptr)
{
}
~Connection()
{
}
void setcallback(func_t recv_cb, func_t send_cb, func_t except_cb)
{
_recv_cb = recv_cb;
_send_cb = send_cb;
_except_cb = except_cb;
}
int Getsock()
{
return _sock;
}
void Append(std::string info)
{
_inbuffer += info;
}
public:
int _sock;
std::string _inbuffer; // 这里来当输入缓冲区,但是这里是有缺点的,它不能处理二进制流
std::string _outbuffer;
public:
func_t _recv_cb; // 读回调函数
func_t _send_cb; // 写回调函数
func_t _except_cb; //
std::shared_ptr<TcpServer> _tcp_server_ptr; // 添加一个回指指针
uint16_t _clientport;
std::string _clientip;
};
class TcpServer : public nocopy
{
static const int num = 64;
public:
TcpServer(uint16_t port) : _port(port),
_listensock_ptr(new Sock()),
_epoller_ptr(new Epoller()),
_quit(true)
{
}
~TcpServer()
{
}
// 设置文件描述符为非阻塞
int set_non_blocking(int fd)
{
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1)
{
perror("fcntl: get flags");
return -1;
}
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1)
{
perror("fcntl: set non-blocking");
return -1;
}
return 0;
}
void Init()
{
_listensock_ptr->Socket();
set_non_blocking(_listensock_ptr->Fd()); // 设置listen文件描述符为非阻塞
_listensock_ptr->Bind(_port);
_listensock_ptr->Listen();
AddConnection(_listensock_ptr->Fd(), (EPOLLIN | EPOLLET),
std::bind(&TcpServer::Accepter, this, std::placeholders::_1), nullptr, nullptr); // 暂时设置成nullptr
}
// 连接管理器
void Accepter(std::shared_ptr<Connection> conection)
{
while (1)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(conection->Getsock(), (sockaddr *)&peer, &len);
if (sock > 0)
{
// 获取客户端信息
uint16_t clientport = ntohs(peer.sin_port);
char buffer;
inet_ntop(AF_INET, &(peer.sin_addr), buffer, sizeof(buffer));
std::cout << "get a new client from:" << buffer << conection->Getsock() << std::endl;
set_non_blocking(sock); // 设置非阻塞
AddConnection(sock, EPOLLIN,
std::bind(&TcpServer::Recver, this, std::placeholders::_1),
std::bind(&TcpServer::Sender, this, std::placeholders::_1),
std::bind(&TcpServer::Excepter, this, std::placeholders::_1),
buffer, clientport);
}
else
{
if (errno == EWOULDBLOCK)
break;
else if (errno == EINTR)
break;
else
break;
}
}
}
// 事件管理器
void Recver(std::shared_ptr<Connection> conection)
{
int sock = conection->Getsock();
while (1)
{
char buffer;
memset(buffer, 0, sizeof(buffer));
ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0); // 非阻塞读取
if (n > 0) // 成功了!!
{
conection->Append(buffer); // 把读取的数据放到Connection对象的输入缓冲区里面
}
else if (n == 0) // 客户端
{
std::cout << "sockfd:" << sock << ",client:" << conection->_clientip << ":" << conection->_clientport << "quit" << std::endl;
Excepter(conection);
}
else
{
if (errno == EWOULDBLOCK)
break;
else if (errno == EINTR)
break;
else
{
std::cout << "sockfd:" << sock << ",client:" << conection->_clientip << ":" << conection->_clientport << "recv err" << std::endl;
Excepter(conection);
}
}
}
}
void Sender(std::shared_ptr<Connection> conection)
{
}
void Excepter(std::shared_ptr<Connection> conection)
{
std::cout<<"Execpted ! fd:"<<conection->Getsock()<<std::endl;
}
void PrintConnection()
{
std::cout << "_connections fd list: " ;
for (auto &connection : _connections)
{
std::cout << connection.second->Getsock() << " ";
std::cout<<connection.second->_inbuffer;
}
std::cout << std::endl;
}
void AddConnection(int sock, uint32_t event, func_t recv_cb, func_t send_cb, func_t except_cb,
std::string clientip = "0.0.0.0", uint16_t clientport = 0)
{
// 1.给listen套接字创建一个Connection对象
std::shared_ptr<Connection> new_connection = std::make_shared<Connection>(sock, std::shared_ptr<TcpServer>(this)); // 创建Connection对象
new_connection->setcallback(recv_cb, send_cb, except_cb);
new_connection->_clientip = clientip;
new_connection->_clientport = clientport;
// 2.添加到_connections里面去
_connections.insert(std::make_pair(sock, new_connection));
// 将listen套接字添加到epoll中->将listensock和他关心的事件,添加到内核的epoll模型中的红黑树里面
// 3.将listensock添加到红黑树
_epoller_ptr->EpollUpDate(EPOLL_CTL_ADD, sock, event); // 注意这里的EPOLLET设置了ET模式
std::cout << "add a new connection success,sockfd:" << sock << std::endl;
}
bool IsConnectionSafe(int fd)
{
auto iter = _connections.find(fd);
if (iter == _connections.end())
{
return false;
}
else
{
return true;
}
}
void Dispatcher() // 事件派发器
{
int n = _epoller_ptr->EpollerWait(revs, num); // 获取已经就绪的事件
for (int i = 0; i < num; i++)
{
uint32_t events = revs.events;
int sock = revs.data.fd;
// 如果出现异常,统一转发为读写问题,只需要处理读写就行
if (events & EPOLLERR) // 出现错误了
{
events |= (EPOLLIN | EPOLLOUT);
}
if (events & EPOLLHUP)
{
events |= (EPOLLIN | EPOLLOUT);
}
// 只需要处理读写就行
if (events & EPOLLIN && IsConnectionSafe(sock)) // 读事件就绪
{
if (_connections->_recv_cb)
_connections->_recv_cb(_connections);
}
if (events & EPOLLOUT && IsConnectionSafe(sock)) // 写事件就绪
{
if (_connections->_send_cb)
_connections->_send_cb(_connections);
}
}
}
void Loop()
{
_quit = false;
while (!_quit)
{
Dispatcher();
PrintConnection();
}
_quit = true;
}
private:
uint16_t _port;
std::shared_ptr<Epoller> _epoller_ptr;
std::unordered_map<int, std::shared_ptr<Connection>> _connections;
std::shared_ptr<Sock> _listensock_ptr;
bool _quit;
struct epoll_event revs; // 专门用来处理事件的
};
https://i-blog.csdnimg.cn/direct/c29b4c4b2f77466ba18ac8a26a7301b9.png
https://i-blog.csdnimg.cn/direct/345f5df71e60481293fdd7d7e3a671d4.png
https://i-blog.csdnimg.cn/direct/2cf4be60fa424c89bab0d90a9bad3570.png
https://i-blog.csdnimg.cn/direct/ebb817cbe8314b06845464274c5d9a51.png
https://i-blog.csdnimg.cn/direct/abfdf75456da481ea015b267be49a385.png
https://i-blog.csdnimg.cn/direct/e7a2c832dd114d48b2ebc0f454cd183e.png
https://i-blog.csdnimg.cn/direct/68419c330f1d4bfdab659039e9909b45.png
好像没有什么题目啊!!! 我们确实做到了读取数据!但是我们照旧没有处置惩罚这些数据!!
所以我们又要弄一个回调函数来处置惩罚这些数据
class TcpServer : public nocopy
{
public:
TcpServer(uint16_t port,func_t OnMessage) : _port(port),
_listensock_ptr(new Sock()),
_epoller_ptr(new Epoller()),
_quit(true),
_OnMessage(OnMessage)
{
}
...
// 事件管理器
void Recver(std::shared_ptr<Connection> conection)
{
int sock = conection->Getsock();
while (1)
{
char buffer;
memset(buffer, 0, sizeof(buffer));
ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0); // 非阻塞读取
if (n > 0) // 成功了!!
{
conection->Append(buffer); // 把读取的数据放到Connection对象的输入缓冲区里面
}
else if (n == 0) // 客户端
{
std::cout << "sockfd:" << sock << ",client:" << conection->_clientip << ":" << conection->_clientport << "quit" << std::endl;
Excepter(conection);
}
else
{
if (errno == EWOULDBLOCK)
break;
else if (errno == EINTR)
break;
else
{
std::cout << "sockfd:" << sock << ",client:" << conection->_clientip << ":" << conection->_clientport << "recv err" << std::endl;
Excepter(conection);
}
}
_OnMessage(conection);//将读取的数据交给上层处理
}
}
private:
....
//上层处理数据
func_t _OnMessage;//将数据交给上层
}; 我们要求上层来完成检测,来处置惩罚粘包的题目!这个_OnMessage不就是回调函数吗!!!
我们简单的写一个
mian.cc
#include"tcpserver.hpp"
#include<memory>
void DefaultOmMessage(std::shared_ptr<Connection> connection_ptr)
{
std::cout<<"上层得到了数据:"<<connection_ptr->_inbuffer<<std::endl;
}
int main()
{
std::unique_ptr<TcpServer> svr(new TcpServer(8877,DefaultOmMessage));
svr->Init();
svr->Loop();
} 我们来测试一下行不可
https://i-blog.csdnimg.cn/direct/cdd7dd0e0cc147438019a7372b88e6a4.png
https://i-blog.csdnimg.cn/direct/b08346c0a5a74e2bb3c476e401176c30.png
https://i-blog.csdnimg.cn/direct/0283c6cf11604632a888792f62cc868c.png
3.8.处置惩罚数据 ——(反)序列化/编解码
可以哦!!! 我们已经拿到数据了,那我们应该怎么处置惩罚数据呢?我们应该在这里完成序列和反序列化!!!由于我之前写过序列和反序列化,以及编码解码的代码,所以我直接拿过复制粘贴好了。
Serialization.hpp
#pragma
#define CRLF "\t" // 分隔符
#define CRLF_LEN strlen(CRLF) // 分隔符长度
#define SPACE " " // 空格
#define SPACE_LEN strlen(SPACE) // 空格长度
#define OPS "+-*/%" // 运算符
#include <iostream>
#include <string>
#include <cstring>
#include<assert.h>
//参数len为in的长度,是一个输出型参数。如果为0代表err
std::string decode(std::string& in,size_t*len)
{
assert(len);//如果长度为0是错误的
// 1.确认in的序列化字符串完整(分隔符)
*len=0;
size_t pos = in.find(CRLF);//查找\t第一次出现时的下标
//查找不到,err
if(pos == std::string::npos){
return "";//返回空串
}
// 2.有分隔符,判断长度是否达标
// 此时pos下标正好就是标识大小的字符长度
std::string inLenStr = in.substr(0,pos);//从下标0开始一直截取到第一个\t之前
//到这里我们要明白,我们这上面截取的是最开头的长度,也就是说,我们截取到的一定是个数字,这个是我们序列化字符的长度
size_t inLen = atoi(inLenStr.c_str());//把截取的这个字符串转int,inLen就是序列化字符的长度
//传入的字符串的长度 - 第一个\t前面的字符数 - 2个\t
size_t left = in.size() - inLenStr.size()- 2*CRLF_LEN;//原本预计的序列化字符串长度
if(left<inLen){//真实的序列化字符串长度和预计的字符串长度进行比较
return ""; //剩下的长度(序列化字符串的长度)没有达到标明的长度
}
// 3.走到此处,字符串完整,开始提取序列化字符串
std::string ret = in.substr(pos+CRLF_LEN,inLen);//从pos+CRLF_LEN下标开始读取inLen个长度的字符串——即序列化字符串
*len = inLen;
// 4.因为in中可能还有其他的报文(下一条)
// 所以需要把当前的报文从in中删除,方便下次decode,避免二次读取
size_t rmLen = inLenStr.size() + ret.size() + 2*CRLF_LEN;//长度+2个\t+序列字符串的长度
in.erase(0,rmLen);//移除从索引0开始长度为rmLen的字符串
// 5.返回
return ret;
}
//编码不需要修改源字符串,所以const。参数len为in的长度
std::string encode(const std::string& in,size_t len)
{
std::string ret = std::to_string(len);//将长度转为字符串添加在最前面,作为标识
ret+=CRLF;
ret+=in;
ret+=CRLF;
return ret;
}
class Request//客户端使用的
{
public:
// 将用户的输入转成内部成员
// 用户可能输入x+y,x+ y,x +y,x + y等等格式
// 提前修改用户输入(主要还是去掉空格),提取出成员
Request()
{
}
// 删除输入中的空格
void rmSpace(std::string &in)
{
std::string tmp;
for (auto e : in)
{
if (e != ' ')
{
tmp += e;
}
}
in = tmp;
}
// 序列化 (入参应该是空的,会返回一个序列化字符串)
void serialize(std::string &out)//这个是客户端在发送消息给服务端时使用的,在这之后要先编码,才能发送出去
{
// x + y
out.clear(); // 序列化的入参是空的
out += std::to_string(_x);
out += SPACE;
out += _ops; // 操作符不能用tostring,会被转成ascii
out += SPACE;
out += std::to_string(_y);
// 不用添加分隔符(这是encode要干的事情)
}
//序列化之后应该要编码,去加个长度
// 反序列化(解开
bool deserialize(const std::string &in)//这个是服务端接收到客户端发来的消息后使用的,在这之前要先解码
{
// x + y 需要取出x,y和操作符
size_t space1 = in.find(SPACE);// 第一个空格
if (space1 == std::string::npos) // 没找到
{
return false;
}
size_t space2 = in.rfind(SPACE); // 第二个空格
if (space2 == std::string::npos) // 没找到
{
return false;
}
// 两个空格都存在,开始取数据
std::string dataX = in.substr(0, space1);
std::string dataY = in.substr(space2 + SPACE_LEN); // 默认取到结尾
std::string op = in.substr(space1 + SPACE_LEN, space2 - (space1 + SPACE_LEN));
if (op.size() != 1)
{
return false; // 操作符长度有问题
}
// 没问题了,转内部成员
_x = atoi(dataX.c_str());
_y = atoi(dataY.c_str());
_ops = op;
return true;
}
public:
int _x;
int _y;
char _ops;
};
class Response // 服务端必须回应
{
public:
Response(int code = 0, int result = 0)
: _exitCode(code), _result(result)
{
}
// 序列化
void serialize(std::string &out)//这个是服务端发送消息给客户端使用的,使用之后要编码
{
// code ret
out.clear();
out += std::to_string(_exitCode);
out += SPACE;
out += std::to_string(_result);
out += CRLF;
}
// 反序列化
bool deserialize(const std::string &in)//这个是客户端接收服务端消息后使用的,使用之前要先解码
{
// 只有一个空格
size_t space = in.find(SPACE);// 寻找第一个空格的下标
if (space == std::string::npos) // 没找到
{
return false;
}
std::string dataCode = in.substr(0, space);
std::string dataRes = in.substr(space + SPACE_LEN);
_exitCode = atoi(dataCode.c_str());
_result = atoi(dataRes.c_str());
return true;
}
public:
int _exitCode; // 计算服务的退出码
int _result; // 结果
};
Response Caculater(const Request& req)
{
Response resp;//构造函数中已经指定了exitcode为0
switch (req._ops)
{
case '+':
resp._result = req._x + req._y;
break;
case '-':
resp._result = req._x - req._y;
break;
case '*':
resp._result = req._x * req._y;
break;
case '%':
{
if(req._y == 0)
{
resp._exitCode = -1;//取模错误
break;
}
resp._result = req._x % req._y;//取模是可以操作负数的
break;
}
case '/':
{
if(req._y == 0)
{
resp._exitCode = -2;//除0错误
break;
}
resp._result = req._x / req._y;//取模是可以操作负数的
break;
}
default:
resp._exitCode = -3;//操作符非法
break;
}
return resp;
} 接下来我们就可以对读取的数据举行处置惩罚了!!!怎么处置惩罚呢?我们照旧跟自定义协议那篇一样,搞一个计算器好了!
Caculater函数
Response Caculater(const Request& req)
{
Response resp;//构造函数中已经指定了exitcode为0
switch (req._ops)
{
case '+':
resp._result = req._x + req._y;
break;
case '-':
resp._result = req._x - req._y;
break;
case '*':
resp._result = req._x * req._y;
break;
case '%':
{
if(req._y == 0)
{
resp._exitCode = -1;//取模错误
break;
}
resp._result = req._x % req._y;//取模是可以操作负数的
break;
}
case '/':
{
if(req._y == 0)
{
resp._exitCode = -2;//除0错误
break;
}
resp._result = req._x / req._y;//取模是可以操作负数的
break;
}
default:
resp._exitCode = -3;//操作符非法
break;
}
return resp;
} 我们把这个放到了我们的 Serialization.hpp
接下来就接着修改我们的main.cc好了!
main.cc
#include "tcpserver.hpp"
#include <memory>
#include "Serialization.hpp"
void DefaultOmMessage(std::shared_ptr<Connection> connection_ptr)
{
std::cout << "上层得到了数据:" << connection_ptr->_inbuffer << std::endl;
std::string inbuf = connection_ptr->_inbuffer;
size_t packageLen = inbuf.size();
//由于我们是使用telnet来测试的所以,我们就不解码了
/*
// 3.1.解码和反序列化客户端传来的消息
std::string package = decode(inbuf, &packageLen); // 解码
if (packageLen == 0)
{
printf("decode err: %s[%d] status: %d", connection_ptr->_clientip, connection_ptr->_clientport, packageLen);
// 报文不完整或有误
}
*/
Request req;
bool deStatus = req.deserialize(inbuf); // 使用Request的反序列化,packsge内部各个成员已经有了数值
if (deStatus) // 获取消息反序列化成功
{
// 3.2.获取结构化的相应
Response resp = Caculater(req); // 将计算任务的结果存放到Response里面去
// 3.3.序列化和编码响应
std::string echoStr;
resp.serialize(echoStr); // 序列化
//由于我们使用的是telnet来测试的,所以我们不编码了
// echoStr = encode(echoStr, echoStr.size()); // 编码
// 3.4.写入,发送返回值给输出缓冲区
connection_ptr->_outbuffer=echoStr;
std::cout<<connection_ptr->_outbuffer<<std::endl;
}
else // 客户端消息反序列化失败
{
printf("deserialize err: %s[%d] status: %d", connection_ptr->_clientip, connection_ptr->_clientport, deStatus);
return;
}
}
int main()
{
std::unique_ptr<TcpServer> svr(new TcpServer(8877, DefaultOmMessage));
svr->Init();
svr->Loop();
} 我们测试一下
https://i-blog.csdnimg.cn/direct/9671baa8059e475b95341eaaeb67c5f7.png
https://i-blog.csdnimg.cn/direct/491aaa773a7749139b2666380723f67b.png
https://i-blog.csdnimg.cn/direct/9fd911b4c24647c28282f8bc9bc3fc9e.png
可以啊!!! 接下来就是专门处置惩罚写变乱
3.9.写变乱的处置惩罚
之前写服务器时,我们从来没处置惩罚过写变乱,写变乱和读变乱不太一样,关心读变乱是要常设置的,但写变乱一般都是就绪的,因为内核发送缓冲区大概率都是有空间的,如果每次都要让epoll帮我们关心读变乱,这着实是一种资源的浪费,因为大部分环境下,你send数据,都是会直接将应用层数据拷贝到内核缓冲区的,不会出现等候的环境,而recv就不太一样,recv在读取的时候,有大概数据还在网络内里,所以recv要等候的概率是比较高的,所以对于读变乱来说,经常都要将其设置到sock所关心的变乱聚集中。
但写变乱并不是如许的,写变乱应该是偶尔设置到关心聚集中,好比你这次没把数据一次性发完,但你又没设置该sock关心写变乱,当下次写变乱就绪了,也就是内核发送缓冲区有空间了,epoll_wait也不会通知你,那你还怎么发送剩余数据啊,所以这个时候你就应该设置写变乱关心了,让epoll_wait帮你监督sock上的写变乱,以便于下次epoll_wait通知你时,你还能够继续发奉上次没发完的数据。
这个时候大概有人会问,ET模式不是只会通知一次吗?如果我这次设置了写关心,但下次发送数据的时候,照旧没发送完毕(因为内核发送缓冲区大概没有剩余空间了),那背面ET模式是不是就不会通知我了呀,那我还怎么继续发送剩余的数据呢?ET模式在底层就绪的变乱状态发生变化时,还会再通知上层一次的,对于读变乱来说,当数据从无到有,从有到多状态发生变化时,ET就还会通知上层一次,对于写变乱来说,当内核发送缓冲区剩余空间从无到有,从有到多状态发生变化时,ET也还会通知上层一次,所以不用担心数据发送不完的题目产生,因为ET是会通知我们的。
在循环外,我们只必要通过判定outbuffer是否为空的环境,来决定是否要设置写变乱关心,当数据发送完了那我们就取消对于写变乱的关心,不占用epoll的资源,如果数据没发送完,那就设置对于写变乱的关心,因为我们要保证下次写变乱就绪时,epoll_wait能够通知我们对写变乱举行处置惩罚。
main.cc
#include "tcpserver.hpp"
#include <memory>
#include "Serialization.hpp"
void DefaultOmMessage(std::shared_ptr<Connection> connection_ptr)
{
std::cout << "上层得到了数据:" << connection_ptr->_inbuffer << std::endl;
std::string inbuf = connection_ptr->_inbuffer;
size_t packageLen = inbuf.size();
//由于我们是使用telnet来测试的所以,我们就不解码了
/*
// 3.1.解码和反序列化客户端传来的消息
std::string package = decode(inbuf, &packageLen); // 解码
if (packageLen == 0)
{
printf("decode err: %s[%d] status: %d", connection_ptr->_clientip, connection_ptr->_clientport, packageLen);
// 报文不完整或有误
}
*/
Request req;
bool deStatus = req.deserialize(inbuf); // 使用Request的反序列化,packsge内部各个成员已经有了数值
if (deStatus) // 获取消息反序列化成功
{
// 3.2.获取结构化的相应
Response resp = Caculater(req); // 将计算任务的结果存放到Response里面去
// 3.3.序列化和编码响应
std::string echoStr;
resp.serialize(echoStr); // 序列化
//由于我们使用的是telnet来测试的,所以我们不编码了
// echoStr = encode(echoStr, echoStr.size()); // 编码
// 3.4.写入,发送返回值给输出缓冲区
connection_ptr->_outbuffer=echoStr;
std::cout<<connection_ptr->_outbuffer<<std::endl;
//发送
connection_ptr->_tcp_server_ptr->Sender(connection_ptr);//调用里面的方法来发送
}
else // 客户端消息反序列化失败
{
printf("deserialize err: %s[%d] status: %d", connection_ptr->_clientip, connection_ptr->_clientport, deStatus);
return;
}
} 我们留意最后一句,我们直接把发生认为交给了Tcpserver类里的Sender函数,不过这个函数还没有写,我们来写一下!
Tcpserver类里的Sender函数
void Sender(std::shared_ptr<Connection> connection) // 使用shared_ptr管理Connection对象的生命周期
{
// 无限循环,直到输出缓冲区为空或发生需要退出的错误
while (true)
{
// 引用当前连接的输出缓冲区
auto &outbuffer = connection->_outbuffer;
// 尝试发送数据,send函数返回发送的字节数,或者-1表示错误
ssize_t n = send(connection->Getsock(), outbuffer.data(), outbuffer.size(), 0);
// 如果n大于0,表示部分或全部数据发送成功
if (n > 0)
{
// 从输出缓冲区中移除已发送的数据
outbuffer.erase(0, n);
// 如果输出缓冲区为空,则退出循环
if (outbuffer.empty())
break;
}
// 如果n等于0,表示连接已关闭(对端执行了关闭操作),退出函数
else if (n == 0)
{
return;
}
// 处理发送失败的情况
else
{
// 根据errno的值判断错误类型
if (errno == EWOULDBLOCK)
{
// EWOULDBLOCK表示非阻塞模式下资源暂时不可用,可稍后重试,但这里选择直接退出循环
break;
}
else if (errno == EINTR)
{
// EINTR表示操作被信号中断,可以安全地重新尝试
continue;
}
else
{
// 打印错误信息,包含套接字描述符和客户端IP地址及端口
std::cout << "sockfd:" << connection->Getsock() << ",client:" << connection->_clientip << ":" << connection->_clientport << "send error" << std::endl;
// 调用异常处理回调函数
Excepter(conection);
// 退出循环
break;
}
}
}
} 但是这还不敷啊,万一我没写完数据呢?我们还必要举行写处置惩罚,这里我们就借助epoll机制来帮我们。
// Sender函数负责通过给定的Connection对象发送数据。
// 它使用epoll机制来管理套接字的读写事件,根据发送情况调整对写事件的关注。
void Sender(std::shared_ptr<Connection> connection)
{
// 引用Connection对象的输出缓冲区
auto &outbuffer = connection->_outbuffer;
// 循环发送数据,直到输出缓冲区为空或发生错误
while (true)
{
// 尝试发送数据
ssize_t n = send(connection->Getsock(), outbuffer.data(), outbuffer.size(), 0); // 注意:使用.data()获取缓冲区首地址
// 如果发送成功(n > 0)
if (n > 0)
{
// 从输出缓冲区中移除已发送的数据
outbuffer.erase(0, n);
// 如果输出缓冲区为空,则退出循环
if (outbuffer.empty())
break;
}
// 如果n == 0,表示连接已正常关闭(对端调用了close),退出函数
else if (n == 0)
{
return;
}
// 处理发送失败的情况
else
{
// 检查errno以确定错误类型
if (errno == EWOULDBLOCK)
{
// EWOULDBLOCK表示资源暂时不可用,通常发生在非阻塞模式下,退出循环
break;
}
else if (errno == EINTR)
{
// EINTR表示操作被信号中断,继续循环尝试发送
continue;
}
else
{
// 打印错误信息,并调用Connection对象的异常处理回调函数
std::cout << "sockfd:" << connection->Getsock() << ",client:" << connection->_clientip << ":" << connection->_clientport << "send error" << std::endl;
Excepter(conection);
break;
}
}
}
// 根据输出缓冲区是否为空,调整对写事件的关注
if (!outbuffer.empty())
{
// 如果输出缓冲区不为空,开启对写事件的关注
EnableEvent(connection->Getsock(), true, true); // 同时关心读和写事件
}
else
{
// 如果输出缓冲区为空,关闭对写事件的关注,但保持对读事件的关注
EnableEvent(connection->Getsock(), true, false); // 只关心读事件
}
}
// EnableEvent函数用于根据给定的参数,更新epoll事件监听器对套接字的事件关注。
void EnableEvent(int sock, bool readable, bool sendable)
{
// 初始化events变量,用于存储要设置的事件标志
uint32_t events = 0;
// 如果需要关注读事件,则设置EPOLLIN标志
events |= (readable ? EPOLLIN : 0);
// 如果需要关注写事件,则设置EPOLLOUT标志
events |= (sendable ? EPOLLOUT : 0);
// 总是设置EPOLLET标志,表示使用边缘触发模式(Edge Triggered)
events |= EPOLLET;
// 调用epoll更新函数,修改对指定套接字的事件关注
// 注意:_epoller_ptr应该是一个指向epoll事件监听器对象的指针,EPOLL_CTL_MOD表示修改现有事件
_epoller_ptr->EpollUpDate(EPOLL_CTL_MOD, sock, events);
} 我们测试一下,这能不能完成写的使命啊!https://i-blog.csdnimg.cn/direct/4d58e855eab34ce7b692a16eb5a547a6.png
https://i-blog.csdnimg.cn/direct/7cd9dbab56514a8da09ced412242aef8.png
很好,显然是成功了!!!
3.10.异常变乱的处置惩罚
各人细致看看上面的代码就会知道,读写变乱出题目了之后,都是立马将错误和异常都传递给了Exceptrt函数!
下面是异常变乱的处置惩罚方法,我们同一对全部异常变乱,都先将其从epoll模子中移除,然后关闭文件描述符,最后将conn从哈希表_connecions中移除。
void Excepter(std::shared_ptr<Connection> conection)
{
std::cout << "Execpted ! fd:" << conection->Getsock() << std::endl;
//1.将文件描述符从epoll模型里面移除来
_epoller_ptr->EpollUpDate(EPOLL_CTL_DEL,conection->Getsock(),0);
//2.关闭异常的文件描述符
close(conection->Getsock());
//3.从unordered_map中删除
_connections.erase(conection->Getsock());
}
我们来测试一下:
我们毗连我们的服务器,然退却出,服务器就会打印下面这些内容
https://i-blog.csdnimg.cn/direct/5c36d7d3327e453cb0d12bc960df65de.png
很显然失败了!
为什么呢?就是connection指针的生命周期有题目
值得留意的是,connnection指针指向的毗连布局体空间,必须由我们自己释放,有人说,为什么啊?你哈希表不是都已经erase了么?为什么还要步伐员自己再delete毗连布局体空间呢?
这里要给各人阐明一点的是,全部的容器在erase的时候,都只释放容器自己所new出来的空间,像哈希表如许的容器,它会new一个存储键值对的节点空间,节点内里存储着conn指针和sockfd,当调用哈希表的erase时,哈希表只会释放它自己new出来的节点空间,至于这个节点空间内里存储了一个Connection类型的指针,而且这个指针变量指向一个布局体空间,这些事情哈希表才不会管你呢,容器只会释放他自己开辟的空间,哈希表是vector挂单链表的方式来实现的。
所以我们要自己手动释放conn指向的空间,如果你不想自己手动释放conn指向的堆空间资源,则可以存储智能指针对象,如许在哈希表erase时,就会释放智能指针对象的空间,从而自动调用Connection类的析构函数。
如许搞起来着实照旧很麻烦的,所以我们就自己手动释放就好了,如果不手动释放那就会造成内存泄露。
由于实现起来比较麻烦,必要修改代码的许多地方,我就不改了,但是最核心的部分就像下面如许子https://i-blog.csdnimg.cn/direct/e7bd6de7e48d4e7bb5f5c92209781a1c.png
至此,我们的浅易版本的单Reactor单历程版本的TCP服务器就算完成了!
3.11.源代码
tcpserver.hpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <memory>
#include <unordered_map>
#include "Socket.hpp"
#include "Epoller.hpp"
#include "nocopy.hpp"
#include <sys/socket.h>
#include <sys/types.h>
class Connection;
using func_t = std::function<void(std::shared_ptr<Connection>)>;
class TcpServer;
class Connection
{
public:
Connection(int sock, std::shared_ptr<TcpServer> tcp_server_ptr) : _sock(sock), _tcp_server_ptr(tcp_server_ptr)
{
}
~Connection()
{
}
void setcallback(func_t recv_cb, func_t send_cb, func_t except_cb)
{
_recv_cb = recv_cb;
_send_cb = send_cb;
_except_cb = except_cb;
}
int Getsock()
{
return _sock;
}
void Append(std::string info)
{
_inbuffer += info;
}
public:
int _sock;
std::string _inbuffer; // 这里来当输入缓冲区,但是这里是有缺点的,它不能处理二进制流
std::string _outbuffer;
public:
func_t _recv_cb; // 读回调函数
func_t _send_cb; // 写回调函数
func_t _except_cb; //
std::shared_ptr<TcpServer> _tcp_server_ptr; // 添加一个回指指针
uint16_t _clientport;
std::string _clientip;
};
class TcpServer : public nocopy
{
static const int num = 64;
public:
TcpServer(uint16_t port, func_t OnMessage) : _port(port),
_listensock_ptr(new Sock()),
_epoller_ptr(new Epoller()),
_quit(true),
_OnMessage(OnMessage)
{
}
~TcpServer()
{
}
// 设置文件描述符为非阻塞
int set_non_blocking(int fd)
{
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1)
{
perror("fcntl: get flags");
return -1;
}
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1)
{
perror("fcntl: set non-blocking");
return -1;
}
return 0;
}
void Init()
{
_listensock_ptr->Socket();
set_non_blocking(_listensock_ptr->Fd()); // 设置listen文件描述符为非阻塞
_listensock_ptr->Bind(_port);
_listensock_ptr->Listen();
AddConnection(_listensock_ptr->Fd(), (EPOLLIN | EPOLLET),
std::bind(&TcpServer::Accepter, this, std::placeholders::_1), nullptr, nullptr); // 暂时设置成nullptr
}
// 连接管理器
void Accepter(std::shared_ptr<Connection> conection)
{
while (1)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(conection->Getsock(), (sockaddr *)&peer, &len);
if (sock > 0)
{
// 获取客户端信息
uint16_t clientport = ntohs(peer.sin_port);
char buffer;
inet_ntop(AF_INET, &(peer.sin_addr), buffer, sizeof(buffer));
std::cout << "get a new client from:" << buffer << conection->Getsock() << std::endl;
set_non_blocking(sock); // 设置非阻塞
AddConnection(sock, EPOLLIN,
std::bind(&TcpServer::Recver, this, std::placeholders::_1),
std::bind(&TcpServer::Sender, this, std::placeholders::_1),
std::bind(&TcpServer::Excepter, this, std::placeholders::_1),
buffer, clientport);
}
else
{
if (errno == EWOULDBLOCK)
break;
else if (errno == EINTR)
break;
else
break;
}
}
}
// 事件管理器
void Recver(std::shared_ptr<Connection> conection)
{
int sock = conection->Getsock();
while (1)
{
char buffer;
memset(buffer, 0, sizeof(buffer));
ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0); // 非阻塞读取
if (n > 0) // 成功了!!
{
conection->Append(buffer); // 把读取的数据放到Connection对象的输入缓冲区里面
}
else if (n == 0) // 客户端
{
std::cout << "sockfd:" << sock << ",client:" << conection->_clientip << ":" << conection->_clientport << "quit" << std::endl;
Excepter(conection);
}
else
{
if (errno == EWOULDBLOCK)
break;
else if (errno == EINTR)
break;
else
{
std::cout << "sockfd:" << sock << ",client:" << conection->_clientip << ":" << conection->_clientport << "recv err" << std::endl;
Excepter(conection);
}
}
_OnMessage(conection); // 将读取的数据交给上层处理
}
}
void Sender(std::shared_ptr<Connection> conection)
{
std::string outbuffer = conection->_outbuffer;
while (1)
{
ssize_t n = send(conection->Getsock(), outbuffer.data(), outbuffer.size(), 0);
if (n > 0) // 发成功了
{
outbuffer.erase(0, n);
if (outbuffer.empty())
break;
}
else if (n == 0)
{
return;
}
else
{
if (errno == EWOULDBLOCK)
break;
else if (errno == EINTR)
continue;
else
{
std::cout << "sockfd:" << conection->Getsock() << ",client:" << conection->_clientip << ":" << conection->_clientport << "send error" << std::endl;
Excepter(conection);
break;
}
}
}
if (!outbuffer.empty())
{
// 开启对写事件的关心
EnableEvent(conection->Getsock(), true, true); // 关心读写
}
else
{
// 关闭对写事件的关心
EnableEvent(conection->Getsock(), true, false); // 关心读,不关心写
}
}
void EnableEvent(int sock, bool readable, bool sendable)
{
uint32_t events = 0;
events |= (readable ? EPOLLIN : 0) | (sendable ? EPOLLOUT : 0) | EPOLLET;
_epoller_ptr->EpollUpDate(EPOLL_CTL_MOD, sock, events);
}
void Excepter(std::shared_ptr<Connection> conection)
{
if (!conection)
{
// Connection 对象可能已被销毁,无需进一步操作
std::cout << "Connection already destroyed, no further action taken." << std::endl;
return;
}
std::cout << "Execpted ! fd:" << conection->Getsock() << std::endl;
// 1.将文件描述符从epoll模型里面移除来
_epoller_ptr->EpollUpDate(EPOLL_CTL_DEL, conection->Getsock(), 0);
// 2.关闭异常的文件描述符
close(conection->Getsock());
// 3.从unordered_map中删除
_connections.erase(conection->Getsock());
}
void PrintConnection()
{
std::cout << "_connections fd list: ";
for (auto &connection : _connections)
{
std::cout << connection.second->Getsock() << " ";
std::cout << connection.second->_inbuffer;
}
std::cout << std::endl;
}
void AddConnection(int sock, uint32_t event, func_t recv_cb, func_t send_cb, func_t except_cb,
std::string clientip = "0.0.0.0", uint16_t clientport = 0)
{
// 1.给listen套接字创建一个Connection对象
std::shared_ptr<Connection> new_connection = std::make_shared<Connection>(sock, std::shared_ptr<TcpServer>(this)); // 创建Connection对象
new_connection->setcallback(recv_cb, send_cb, except_cb);
new_connection->_clientip = clientip;
new_connection->_clientport = clientport;
// 2.添加到_connections里面去
_connections.insert(std::make_pair(sock, new_connection));
// 将listen套接字添加到epoll中->将listensock和他关心的事件,添加到内核的epoll模型中的红黑树里面
// 3.将listensock添加到红黑树
_epoller_ptr->EpollUpDate(EPOLL_CTL_ADD, sock, event); // 注意这里的EPOLLET设置了ET模式
std::cout << "add a new connection success,sockfd:" << sock << std::endl;
}
bool IsConnectionSafe(int fd)
{
auto iter = _connections.find(fd);
if (iter == _connections.end())
{
return false;
}
else
{
return true;
}
}
void Dispatcher() // 事件派发器
{
int n = _epoller_ptr->EpollerWait(revs, num); // 获取已经就绪的事件
for (int i = 0; i < num; i++)
{
uint32_t events = revs.events;
int sock = revs.data.fd;
// 如果出现异常,统一转发为读写问题,只需要处理读写就行
if (events & EPOLLERR) // 出现错误了
{
events |= (EPOLLIN | EPOLLOUT);
}
if (events & EPOLLHUP)
{
events |= (EPOLLIN | EPOLLOUT);
}
// 只需要处理读写就行
if (events & EPOLLIN && IsConnectionSafe(sock)) // 读事件就绪
{
if (_connections->_recv_cb)
_connections->_recv_cb(_connections);
}
if (events & EPOLLOUT && IsConnectionSafe(sock)) // 写事件就绪
{
if (_connections->_send_cb)
_connections->_send_cb(_connections);
}
}
}
void Loop()
{
_quit = false;
while (!_quit)
{
Dispatcher();
PrintConnection();
}
_quit = true;
}
private:
uint16_t _port;
std::shared_ptr<Epoller> _epoller_ptr;
std::unordered_map<int, std::shared_ptr<Connection>> _connections;
std::shared_ptr<Sock> _listensock_ptr;
bool _quit;
struct epoll_event revs; // 专门用来处理事件的
// 上层处理数据
func_t _OnMessage; // 将数据交给上层
};
Epoller.hpp
v#pragma once
#include <iostream>
#include <sys/epoll.h>
#include <unistd.h>
#include <cerrno>
#include "nocopy.hpp"
class Epoller : public nocopy
{
static const int size = 128;
public:
Epoller()
{
_epfd = epoll_create(size);
if (_epfd == -1)
{
perror("epoll_creat error");
}
else
{
printf("epoll_creat successful:%d\n", _epfd);
}
}
~Epoller()
{
if (_epfd > 0)
{
close(_epfd);
}
}
int EpollerWait(struct epoll_event revents[],int num)
{
int n=epoll_wait(_epfd,revents,num,3000);
return n;
}
int EpollUpDate(int oper,int sock,uint16_t event)
{
int n;
if(oper==EPOLL_CTL_DEL)//将该事件从epoll红黑树里面删除
{
n=epoll_ctl(_epfd,oper,sock,nullptr);
if(n!=0)
{
printf("delete epoll_ctl error");
}
}
else{//添加和修改,即EPOLL_CTL_MOD和EPOLL_CTL_ADD
struct epoll_event ev;
ev.events=event;
ev.data.fd=sock;//方便我们知道是哪个fd就绪了
n=epoll_ctl(_epfd,oper,sock,&ev);
if(n!=0)
{
perror("add epoll_ctl error");
}
}
return n;
}
private:
int _epfd;
};
nocopy.hpp
#pragma once
class nocopy
{
public:
// 允许使用默认构造函数(由编译器自动生成)
nocopy() = default;
// 禁用拷贝构造函数,防止通过拷贝来创建类的实例
nocopy(const nocopy&) = delete;
// 禁用赋值运算符,防止类的实例之间通过赋值操作进行内容复制
nocopy& operator=(const nocopy&) = delete;
};
Serialization.hpp
#pragma
#define CRLF "\t" // 分隔符
#define CRLF_LEN strlen(CRLF) // 分隔符长度
#define SPACE " " // 空格
#define SPACE_LEN strlen(SPACE) // 空格长度
#define OPS "+-*/%" // 运算符
#include <iostream>
#include <string>
#include <cstring>
#include<assert.h>
//参数len为in的长度,是一个输出型参数。如果为0代表err
std::string decode(std::string& in,size_t*len)
{
assert(len);//如果长度为0是错误的
// 1.确认in的序列化字符串完整(分隔符)
*len=0;
size_t pos = in.find(CRLF);//查找\t第一次出现时的下标
//查找不到,err
if(pos == std::string::npos){
return "";//返回空串
}
// 2.有分隔符,判断长度是否达标
// 此时pos下标正好就是标识大小的字符长度
std::string inLenStr = in.substr(0,pos);//从下标0开始一直截取到第一个\t之前
//到这里我们要明白,我们这上面截取的是最开头的长度,也就是说,我们截取到的一定是个数字,这个是我们序列化字符的长度
size_t inLen = atoi(inLenStr.c_str());//把截取的这个字符串转int,inLen就是序列化字符的长度
//传入的字符串的长度 - 第一个\t前面的字符数 - 2个\t
size_t left = in.size() - inLenStr.size()- 2*CRLF_LEN;//原本预计的序列化字符串长度
if(left<inLen){//真实的序列化字符串长度和预计的字符串长度进行比较
return ""; //剩下的长度(序列化字符串的长度)没有达到标明的长度
}
// 3.走到此处,字符串完整,开始提取序列化字符串
std::string ret = in.substr(pos+CRLF_LEN,inLen);//从pos+CRLF_LEN下标开始读取inLen个长度的字符串——即序列化字符串
*len = inLen;
// 4.因为in中可能还有其他的报文(下一条)
// 所以需要把当前的报文从in中删除,方便下次decode,避免二次读取
size_t rmLen = inLenStr.size() + ret.size() + 2*CRLF_LEN;//长度+2个\t+序列字符串的长度
in.erase(0,rmLen);//移除从索引0开始长度为rmLen的字符串
// 5.返回
return ret;
}
//编码不需要修改源字符串,所以const。参数len为in的长度
std::string encode(const std::string& in,size_t len)
{
std::string ret = std::to_string(len);//将长度转为字符串添加在最前面,作为标识
ret+=CRLF;
ret+=in;
ret+=CRLF;
return ret;
}
class Request//客户端使用的
{
public:
// 将用户的输入转成内部成员
// 用户可能输入x+y,x+ y,x +y,x + y等等格式
// 提前修改用户输入(主要还是去掉空格),提取出成员
Request()
{
}
// 删除输入中的空格
void rmSpace(std::string &in)
{
std::string tmp;
for (auto e : in)
{
if (e != ' ')
{
tmp += e;
}
}
in = tmp;
}
// 序列化 (入参应该是空的,会返回一个序列化字符串)
void serialize(std::string &out)//这个是客户端在发送消息给服务端时使用的,在这之后要先编码,才能发送出去
{
// x + y
out.clear(); // 序列化的入参是空的
out += std::to_string(_x);
out += SPACE;
out += _ops; // 操作符不能用tostring,会被转成ascii
out += SPACE;
out += std::to_string(_y);
// 不用添加分隔符(这是encode要干的事情)
}
//序列化之后应该要编码,去加个长度
// 反序列化(解开
bool deserialize(const std::string &in)//这个是服务端接收到客户端发来的消息后使用的,在这之前要先解码
{
// x + y 需要取出x,y和操作符
size_t space1 = in.find(SPACE);// 第一个空格
if (space1 == std::string::npos) // 没找到
{
return false;
}
size_t space2 = in.rfind(SPACE); // 第二个空格
if (space2 == std::string::npos) // 没找到
{
return false;
}
// 两个空格都存在,开始取数据
std::string dataX = in.substr(0, space1);
std::string dataY = in.substr(space2 + SPACE_LEN); // 默认取到结尾
std::string op = in.substr(space1 + SPACE_LEN, space2 - (space1 + SPACE_LEN));
if (op.size() != 1)
{
return false; // 操作符长度有问题
}
// 没问题了,转内部成员
_x = atoi(dataX.c_str());
_y = atoi(dataY.c_str());
_ops = op;
return true;
}
public:
int _x;
int _y;
char _ops;
};
class Response // 服务端必须回应
{
public:
Response(int code = 0, int result = 0)
: _exitCode(code), _result(result)
{
}
// 序列化
void serialize(std::string &out)//这个是服务端发送消息给客户端使用的,使用之后要编码
{
// code ret
out.clear();
out += std::to_string(_exitCode);
out += SPACE;
out += std::to_string(_result);
out += CRLF;
}
// 反序列化
bool deserialize(const std::string &in)//这个是客户端接收服务端消息后使用的,使用之前要先解码
{
// 只有一个空格
size_t space = in.find(SPACE);// 寻找第一个空格的下标
if (space == std::string::npos) // 没找到
{
return false;
}
std::string dataCode = in.substr(0, space);
std::string dataRes = in.substr(space + SPACE_LEN);
_exitCode = atoi(dataCode.c_str());
_result = atoi(dataRes.c_str());
return true;
}
public:
int _exitCode; // 计算服务的退出码
int _result; // 结果
};
Response Caculater(const Request& req)
{
Response resp;//构造函数中已经指定了exitcode为0
switch (req._ops)
{
case '+':
resp._result = req._x + req._y;
break;
case '-':
resp._result = req._x - req._y;
break;
case '*':
resp._result = req._x * req._y;
break;
case '%':
{
if(req._y == 0)
{
resp._exitCode = -1;//取模错误
break;
}
resp._result = req._x % req._y;//取模是可以操作负数的
break;
}
case '/':
{
if(req._y == 0)
{
resp._exitCode = -2;//除0错误
break;
}
resp._result = req._x / req._y;//取模是可以操作负数的
break;
}
default:
resp._exitCode = -3;//操作符非法
break;
}
return resp;
}
main.cc
#include "tcpserver.hpp"
#include <memory>
#include "Serialization.hpp"
void DefaultOmMessage(std::shared_ptr<Connection> connection_ptr)
{
std::cout << "上层得到了数据:" << connection_ptr->_inbuffer << std::endl;
std::string inbuf = connection_ptr->_inbuffer;
size_t packageLen = inbuf.size();
//由于我们是使用telnet来测试的所以,我们就不解码了
/*
// 3.1.解码和反序列化客户端传来的消息
std::string package = decode(inbuf, &packageLen); // 解码
if (packageLen == 0)
{
printf("decode err: %s[%d] status: %d", connection_ptr->_clientip, connection_ptr->_clientport, packageLen);
// 报文不完整或有误
}
*/
Request req;
bool deStatus = req.deserialize(inbuf); // 使用Request的反序列化,packsge内部各个成员已经有了数值
if (deStatus) // 获取消息反序列化成功
{
// 3.2.获取结构化的相应
Response resp = Caculater(req); // 将计算任务的结果存放到Response里面去
// 3.3.序列化和编码响应
std::string echoStr;
resp.serialize(echoStr); // 序列化
//由于我们使用的是telnet来测试的,所以我们不编码了
// echoStr = encode(echoStr, echoStr.size()); // 编码
// 3.4.写入,发送返回值给输出缓冲区
connection_ptr->_outbuffer=echoStr;
std::cout<<connection_ptr->_outbuffer<<std::endl;
connection_ptr->_tcp_server_ptr->Sender(connection_ptr);//调用里面的方法来发送
}
else // 客户端消息反序列化失败
{
printf("deserialize err: %s[%d] status: %d", connection_ptr->_clientip, connection_ptr->_clientport, deStatus);
return;
}
}
int main()
{
std::unique_ptr<TcpServer> svr(new TcpServer(8877, DefaultOmMessage));
svr->Init();
svr->Loop();
} 3.12.总结
Reactor主要围绕变乱派发和自动反应睁开的,就好比毗连请求到来,epoll_wait提醒步伐员就绪的变乱到来,应该尽快处置惩罚,则与就绪变乱相干联的sock会对应着一个connection布局体,这个布局体我觉得就是反应堆模式的英华所在,无论是什么样就绪的变乱,每个sock都会有对应的回调方法,所以处置惩罚就绪的变乱很容易,直接回调connection内的对应方法即可,是读变乱就调用读方法,是写变乱就调用写方法,是异常变乱,则在读方法或写方法中处置惩罚IO的同时,趁便处置惩罚掉异常变乱。
所以我感觉Reactor就像一个化学反应堆,你向这个反应堆内里扔一些毗连请求,或者网络数据,则这个反应堆会自动匹配相对应的处置惩罚机制来处置惩罚到来的变乱,很方便,同时由于ET模式和EPOLL,这就让Reactor在处置惩罚高并发毗连时,展现出了不俗的实力。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]