您的位置:首页 > 新闻 > 资讯 > 百度收录入口查询注意事项_重庆网络公司招聘_北京seo优化哪家公司好_seo怎么做最佳

百度收录入口查询注意事项_重庆网络公司招聘_北京seo优化哪家公司好_seo怎么做最佳

2024/10/6 16:27:33 来源:https://blog.csdn.net/m0_69323023/article/details/142715411  浏览:    关键词:百度收录入口查询注意事项_重庆网络公司招聘_北京seo优化哪家公司好_seo怎么做最佳
百度收录入口查询注意事项_重庆网络公司招聘_北京seo优化哪家公司好_seo怎么做最佳

目录

一、定义

二、Reactor 模式的角色构成

三、Reactor 模式的工作流程

四、epoll ET 服务器(Reactor 模式)

4.1、设计思路

4.2、Connection 结构

4.3、TcpServer 类

4.4、回调函数

4.4.1、Accepter

4.4.2、Recver

4.4.3、Sender

4.4.4、Excepter

4.5、Socket 套接字

4.6、服务器测试

五、总结


一、定义

Reactor 反应器模式,也被称为分发者模式或通知者模式,是一种将就绪事件派发给对应服务处理程序的事件设计模式。

二、Reactor 模式的角色构成

三、Reactor 模式的工作流程

  • 当向初始分发器注册具体事件处理器时,会标识出该事件处理器希望初始分发器在某个事件发生时向其通知,该事件与 Handle 关联。
  • 初始分发器会要求每个事件处理器向其传递内部的 Handle,该 Handle 向操作系统标识了事件处理器。
  • 当所有的事件处理器注册完毕后,启动初始分发器的事件循环,这时初始分发器会将每个事件处理器的 Handle 合并起来,并使用同步事件分离器等待这些事件的发生。
  • 当某个事件处理器的 Handle 变为 Ready 状态时,同步事件分离器会通知初始分发器。
  • 初始分发器会将 Ready 状态的 Handle 作为 key,来寻找其对应的事件处理器。
  • 初始分发器会调用其对应事件处理器中对应的回调方法来响应该事件。

四、epoll ET 服务器(Reactor 模式)

4.1、设计思路

 epoll ET 服务器

  • 读事件:若是监听套接字读事件就绪则调用 accept 函数获取底层连接,若是其他套接字读事件就绪则调用 recv 函数读取客户端发来的数据。
  • 写事件:写事件就绪则将待发送的数据写入到发送缓冲区中。
  • 异常事件:当某个套接字的异常事件就绪时不做过多处理,直接关闭该套接字。

当 epoll ET 服务器监测到某一事件就绪后,就会将该事件交给对应的服务处理程序进行处理。

Reactor 模式的五个角色

在这个 epoll ET 服务器中,Reactor 模式中的五个角色对应如下:

  • 句柄:文件描述符。
  • 同步事件分离器:I/O 多路复用 epoll。
  • 事件处理器:包括读回调、写回调和异常回调。
  • 具体事件处理器:读回调、写回调和异常回调的具体实现。
  • 初始分发器:TcpServer 中的 Dispatcher 函数。
  • Dispatcher 函数的工作即为:调用 epoll_wait 函数等待事件发生,有事件发生后将就绪的事件派发给对应的服务处理程序即可。

Connection 类

在 Reactor 的工作流程中说到,在注册事件处理器时需要将其与Handle关联,本质上就是需要将读回调、写回调和异常回调与某个文件描述符关联起来。这样做的目的就是为了当某个文件描述符上的事件就绪时可以找到其对应的各种回调函数,进而执行对应的回调方法来处理该事件。可以设计一个 Connection 类,该类中的成员包括了一个文件描述符,以及该文件描述符对应的各种回调函数,以及其他成员。

TcpServer 类

在 Reactor 的工作流程中说到,当所有事件处理器注册完毕后,会使用同步事件分离器等待这些事件发生,当某个事件处理器的Handle变为 Ready 状态时,同步事件分离器会通知初始分发器,然后初始分发器会将 Ready 状态的 Handle 作为 key 来寻找其对应的事件处理器,并调用该事件处理器中对应的回调方法来响应该事件。
本质就是当事件注册完毕后,会调用 epoll_wait 函数来等待这些事件发生,当某个事件就绪时epoll_wait 函数会告知调用方,然后调用方就根据就绪的文件描述符来找到其对应的各种回调函数,并调用对应的回调函数进行事件处理。

Reactor 类:

  • 该类当中有一个成员函数 Dispatcher,即初始分发器,在该函数内部会调用 epoll_wait 函数等待事件的发生,当事件发生后会告知 Dispatcher 已经就绪的事件。
  • 当事件就绪后需要根据就绪的文件描述符来找到其对应的各种回调函数,由于会将每个文件描述符及其对应的各种回调都封装到一个 Connection 结构中,所以可以根据文件描述符找到其对应的 Connection 结构。
  • 使用 C++ STL 中的 unordered_map,来建立各个文件描述符与其对应的 Connection 结构之间的映射,这个 unordered_map 可以作为 TcpServer 类的一个成员变量,当需要找某个文件描述符的 Connection 结构时就可以通过该成员变量找到。

  • TcpServer 类中还需要提供成员函数 AddConnection,用于向初始分发器中注册事件。

epoll ET 服务器的工作流程

  • epoll ET 服务器的初始化:需进行套接字的创建、绑定、监听,创建 epoll 模型。
  • 为监听套接字创建对应的 Connection 结构,并调用 TcpServer 类中提供的 AddConnection 函数将监听套接字添加到 epoll 模型中,并建立监听套接字与其对应的 Connection 结构之间的映射关系。
  • 之后就可以不断调用 TcpServer 类中的 Dispatcher 函数进行事件派发。
  • 在事件处理过程中,会不断向 Dispatcher 中新增事件,每个事件就绪时都会自动调用其对应的回调函数处理,不断调用 Dispatcher 函数进行事件派发即可。

4.2、Connection 结构

Connection 结构中除了包含文件描述符和其对应的读回调、写回调和异常回调外,还包含一个输入缓冲区 _inBuffer、一个输出缓冲区 _outBuffer 以及一个回指指针 _svrPtr。

        1、当某个文件描述符的读事件就绪时,调用 recv 函数读取客户端发来的数据,但并不能保证读到了一个完整报文,因此需要将读取到的数据暂时存放到该文件描述符对应的 _inBuffer 中,当 _inBuffer 中可以分离出一个完整的报文后再将其分离出来进行数据处理,_inBuffer 本质就是用来解决粘包问题的。

        2、当处理完一个报文请求后,需将响应数据发送给客户端,但并不能保证底层 TCP 的发送缓冲区中有足够的空间写入,因此需将要发送的数据暂时存放到该文件描述符对应的 _outBuffer 中,当底层 TCP 的发送缓冲区中有空间,即写事件就绪时,再依次发送 _outBuffer 中的数据。

        3、Connection 结构中设置回指指针_svrPtr,便于快速找到 TcpServer 对象,因为后续需要根据 Connection 结构找到这个 TcpServer 对象。如上层业务处理函数 NetCal 函数向 _outBuffer 输出缓冲区递交数据后,需通过 Connection 中的回指指针,“提醒” TcpServer 进行处理。

Connection 结构中需提供一个管理回调的成员函数,便于外部对回调进行设置:

4.3、TcpServer 类

在 TcpServer 类中有一个 unordered_map 成员,用于建立文件描述符和与其对应的 Connection 结构之间的映射,还有一个 _epoll 成员,该成员是封装的 Epoll 对象。在初始化 TcpServer 对象时就可以调用封装的 EpollCreate 函数创建 Epoll 对象,并将该 epoll 模型对应的文件描述符记录在该对象的成员变量 _epollFd 中,便于后续使用。TcpServer 对象析构时,Epoll 对象的析构会自动调用close函数将epoll模型关闭。

封装 Epoll 类

TcpServer 类部分代码

 AddConnection 函数

TcpServer 类中的 AddConnection 函数用于进行事件注册。

在注册事件时需要传入一个文件描述符和三个回调函数,表示当该文件描述符上的事件(默认只关心读事件)就绪后应该执行的回调方法。
在 AddConnection 函数内部要做的就是,设置套接字为非阻塞(ET 模型要求),将套接字和回调函数等属性封装为一个 Connection,在将套接字添加到 epoll 模型中,对象建立文件描述符和 Connection 的映射关系并管理。

Dispatcher函数(初始分发器)

TcpServer 中的 Dispatcher 函数即初始分发器,其要做的就是调用 epoll_wait 函数等待事件发生。当某个文件描述符上的事件发生后,先通过 unordered_map 找到该文件描述符对应的 Connection 结构,然后调用 Connection 结构中对应的回调函数对该事件进行处理即可。

  • 本代码没有用 switch 或 if 语句对 epoll_wait 函数的返回值进行判断,而是借用 for 循环对其返回值进行了判断。
  • 若 epoll_wait 的返回值为 -1 则说明 epoll_wait 函数调用失败,此时不会进入到 for 循环内部进行事件处理。
  • 若 epoll_wait 的返回值为 0 则说明 epoll_wait 函数超时返回,此时也不会进入到 for 循环内部进行事件处理。
  • 若 epoll_wait 的返回值大于 0 则说明 epoll_wait 函数调用成功,此时才会进入到 for 循环内部调用对应的回调函数对事件进行处理。
  • 事件处理时先对异常事件进行处理,将异常事件交给回调函数进行处理。

 EnableReadWrite 函数

TcpServer 类中的 EnableReadWrite 函数,用于使能某个文件描述符的读写事件。

  • 调用 EnableReadWrite 函数时需要传入一个文件描述符,表示需要设置的是哪个文件描述符对应的事件。
  • 传入两个 bool 值,分别表示是否关心读事件以及是否关心写事件。
  • EnableReadWrite 函数内部会调用封装 EpollCtrl 函数修改该文件描述符的监听事件。

4.4、回调函数

  • Accepter:当连接事件到来时调用该回调函数获取底层建立好的连接。
  • Recver:当读事件就绪时调用该回调函数读取客户端发来的数据并处理。
  • Sender:当写事件就绪时调用该回调函数向客户端发送响应数据。
  • Excepter:当异常事件就绪时调用该函数进行一系列资源的释放。

为某个文件描述符创建Connection结构时,可以调用Connection类提供的SetCallBack函数,将这些回调函数添加到Connection结构中

  • 监听套接字对应的 Connection 结构中的 _recvCb 为 Accepter,因为监听套接字的读事件就绪就意味着连接事件就绪了,而监听套接字一般只关心读事件,因此监听套接字对应的 _sendCb 和 _exceptCb 可以设置为 nullptr。
  • 当 Dispatcher 监测到监听套接字的读事件就绪时,会调用监听套接字对应的Connection结构中的 _recvCb 回调,此时就会调用 Accepter 回调获取底层建立好的连接
  • 对于与客户端建立连接的套接字,其对应的 Connection 结构中的 _recvCb、_sendCb 和 _exceptCb 分别为 Recver、Sender 和 Excepter。
  • 当 Dispatcher 监测到这些套接字的事件就绪时,就会调用其对应的 Connection 结构中对应的回调函数,即 Recver、Sender 和 Excepter。

4.4.1、Accepter

Accepter 回调用于处理连接事件,其工作流程如下:

  • 调用封装的 Accept 函数获取底层建立好的连接。
  • 使用 AddConnection 函数将获取到的套接字封装为 Connection 并添加至服务器的管理中。
  • 此时套接字及其对应需要关心的事件就已注册到 Dispatcher 中。

下一次 Dispatcher 在进行事件派发时就会关注该套接字对应的事件,当事件就绪时就会执行该套接字对应的 Connection 结构中对应的回调方法。

这里实现的 ET 模式下的 epoll 服务器,因此在获取底层连接时需要循环调用 accept 函数进行读取,并且监听套接字必须设置为非阻塞。

  • 因为 ET 模式下只有当底层建立的连接从无到有或是从有到多时才会通知上层,若没有一次性将底层建立好的连接全部获取,并且此后再也没有建立好的连接,那么底层没有读取完的连接就相当于丢失了。
  • 循环调用 accept 函数也意味着,当底层连接全部被获取后再调用 accept 函数,此时就会因为底层已经没有连接了而被阻塞住,因此需要将监听套接字设置为非阻塞,这样当底层没有连接时 accept 就不会被阻塞。accept 获取到的新的套接字也需设置为非阻塞,为了避免将来循环调用 recv、send 等函数时被阻塞

  • 设置非阻塞的操作都在 AddConnection 函数中的 SetNonBlock 函数完成。

设置非阻塞

设置文件描述符为非阻塞时,需先调用 fcntl 函数获取该文件描述符对应的文件状态标记,然后在该文件状态标记的基础上添加非阻塞标记 O_NONBLOCK,最后调用 fcntl 函数对该文件描述符的状态标记进行设置即可。

监听套接字设置为非阻塞后,当底层连接不就绪时,accept 函数会以出错的形式返回,因此当调用 accept 函数的返回值小于 0 时,需继续判断错误码。

  • 若错误码为 EAGAIN 或 EWOULDBLOCK,说明本次出错返回是因为底层已经没有可获取的连接了,此时底层连接全部获取完毕,这时可以返回 0,表示本次 Accepter 调用成功。
  • 若错误码为 EINTR,说明本次调用 accept 函数获取底层连接时被信号中断了,这时还应该继续调用 accept 函数进行获取。
  • 除此之外,才说明 accept 函数是真正调用失败了,此时可以返回 -1,表示本次 accepter 调用失败。

accept、recv 和 send 等 IO 系统调用为什么会被信号中断?

IO 系统调用函数出错返回并且将错误码设置为 EINTR,表明本次在进行数据读取或数据写入之前被信号中断了,即IO系统调用在陷入内核,但并没有返回用户态的时候内核去处理其他信号

  • 在内核态返回用户态之前会检查信号的 pending 位图,即未决信号集,若 pending 位图中有未处理的信号,那么内核就会对该信号进行处理
  • IO 系统调用函数在进行IO操作前就被信号中断了,这是一个特例,因为 IO 过程分为 “等” 和 “拷贝” 两个步骤,一般 “等” 的过程比较漫长,而在这个过程中执行流其实是处于闲置状态的,因此在 “等” 的过程中若有信号产生,内核就会立即进行信号的处理。

写事件按需打开

Accepter 获取上来的套接字在添加到 Dispatcher 中时,只添加了 EOPLLIN 和 EPOLLET 事件,即只让 epoll 关心该套接字的读事件。

之所以没有添加写事件,是因为并没有要发送的数据,因此没有必要让 epoll 关心写事件。一般读事件是会被设置的,而写事件则是按需打开的,只当有数据要发送时才会将写事件打开,并且在数据全部写入完毕后又会立即将写事件关闭。

4.4.2、Recver

recver 回调用于处理读事件,其工作流程如下:

  • 循环调用recv函数读取数据,并将读取到的数据添加到该套接字对应 Connection 结构的 _inBuffer 中。
  • 对 _inBuffer 中的数据进行切割,将完整的报文切割出来,剩余的留在 inbuffer 中。
  • 调用业务处理函数。
  • 当 recv 函数的返回值小于 0 时需要进一步判断错误码,若错误码为 EAGAIN 或 EWOULDBLOCK 则说明底层数据读取完毕了,若错误码为 EINTR 则说明读取过程被信号中断了,此时还需继续调用 recv 函数进行读取,否则就是读取出错了。
  • 当读取出错时,直接调用该套接字对应的 _exceptCb 回调,在 _exceptCb 回调中将该套接字进行关闭。

 报文切割

报文切割本质就是为了防止粘包问题,而粘包问题还涉及到协议定制。

  • 需要根据协议知道如何将各个报文进行分离,如 UDP 分离报文采用的就是定长报头+自描述字段。
  • 本博客目的是演示整个数据处理的过程,为了简单起见就不进行过于复杂的协议定制了,就以 "X" 作为各个报文之间的分隔符,每个报文的最后都会以一个 "X" 作为报文结束的标志。
  • 因此现在要做的就是以 "X" 作为分隔符对 _inBuffer 中的字符串进行切割。
  • SpliteMessage 函数要做的就是对 _inBuffer 中的字符串进行切割,将切割出来的一个个报文放到 vector 中,对于最后无法切出完整报文的数据就留在 _inBuffer 中。

业务处理函数

  • 对切割出来的完整报文进行反序列化。
  • 业务处理。
  • 业务处理后形成响应报文。
  • 将响应报头添加到对应 Conection 结构的 _outBuffer 中,并打开写事件。

下一次 Dispatcher 在进行事件派发时就会关注该套接字的写事件,当写事件就绪时就会执行该套接字对应的 Connection 结构中写回调方法,进而将 _outBuffer 中的响应数据发送给客户端。

协议定制

4.4.3、Sender

  • 循环调用 send 函数发送数据,并将发送出去的数据从该套接字对应 Connection 结构的 _outBuffer 中删除。
  • 若循环调用 send 函数后该套接字对应的 _outBuffer 中的数据被全部发送,此时就需要将该套接字对应的写事件关闭,因为已没有要发送的数据了,若 _outBuffer 中的数据还有剩余,那么该套接字对应的写事件就应继续打开。

  • send 函数的返回值小于0时需进一步判断错误码,若错误码为 EAGAIN 或 EWOULDBLOCK 则说明底层 TCP 发送缓冲区已被写满了,这时将已经发送的数据从 _outBuffer 中移除。
  • 若错误码为 EINTR 则说明发送过程被信号中断了,此时还需要继续调用send函数进行发送,否则就是发送出错了。
  • 当发送出错时直接调用该套接字对应的 _exceptCb 回调,在 _exceptCb 回调中将该套接字进行关闭
  • 若最终 _outBuffer 中的数据全部发送成功,则 _outBuffer 被清空,可以关闭对写事件的关心。

4.4.4、Excepter

  • 对于异常事件就绪的套接字不做过多处理,调用 close 函数将该套接字关闭即可。
  • 但在关闭该套接字前,需先将该套接字从 epoll 模型中删除,并取消该套接字与其对应的 Connection 结构的映射关系。
  • 释放 Connection 对象。

4.5、Socket 套接字

4.6、服务器测试

当客户端连接服务器后,在服务器端会显示客户端使用的是 5 号文件描述符,因为 4 号文件描述符已被 epoll 模型使用了。

此时客户端可以向服务器发送一些简单计算任务,计算任务间用 "X" 隔开,服务器收到计算请求处理后会将计算结果发送给客户端,计算结果之间也是用 "X" 隔开的。若发送的不是完整报文,则会保存在 socket 对应的 Connection 结构中的 _inBuffer 中。

由于使用了多路转接技术,虽然 epoll 服务器是一个单进程的服务器,但却可同时为多个客户端提供服务。

五、总结

基于多路转接方案,当事件就绪的时候,采用回调的方式,进行业务处理的模式就被称为反应堆模式(Reactor)。上述代码中的 TcpServer 就是一个反应堆,其中一个个 Connection 对象就称为事件。每一个事件中都有:

  • 文件描述符
  • 独立的缓冲区
  • 回调方法
  • 回指向反应堆的指针

反应堆中有一个事件派发函数,当epoll中的某个事件就绪,事件派发函数回调用此事件的回调函数。

特性

  • 单进程:既负责事件派发又负责 IO。
  • 半异步半同步:异步,事件到来是随机的。
  • 同步:当前线程参与 IO。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com