Reactor 模式

目录

1. 实现代码

2. Reactor 模式

3. 分析服务器的实现具体细节

3.1. Connection 结构 

3.2. 服务器的成员属性 

3.2. 服务器的构造

3.3. 事件轮询

3.4. 事件派发

3.5. 连接事件

3.6. 读事件

3.7. 写事件

3.8. 异常事件

4. 服务器上层的处理

5. Reactor 总结 


1. 实现代码

EventLoop 服务器实现代码已上传到gitee中。

https://gitee.com/qiange-c/linux-daily-testing-code/tree/0a25822f5ded574a70a2eb65ed6b32d963343bed/2024_4_22/Reactoricon-default.png?t=N7T8https://gitee.com/qiange-c/linux-daily-testing-code/tree/0a25822f5ded574a70a2eb65ed6b32d963343bed/2024_4_22/Reactor

2. Reactor 模式

Reactor 也称之为反应堆模式, 其核心思想就是将事件的产生和处理解耦,通过事件轮询和事件处理器来实现事件的分发和处理,从而提高服务器的并发性能和可扩展性。

3. 分析服务器的实现具体细节

实现大致思路如下:

3.1. Connection 结构 

我们写过 epoll 服务器的代码,说过一个问题,由于 TCP 是面向字节流的,服务器调用read/recv 后,无法保证获得的就是一个完整报文,因此,上层需要定制协议,诸如序列化和反序列化过程,判断能否构成一个完整报文 (解决粘包问题) 等等,可是,如果服务端收到的数据不能构成一个完整报文,那么这些数据是不是服务器自身应该保存起来,如果能构成一个完整报文,服务器在将构成完整报文的那一部分数据在清除,在进行后续处理,同时,服务器未来会为众多客户端提供服务,即会有众多的服务套接字,因此:

  1. 为了保证每个服务套接字的数据正确处理,其实每一个套接字都要有属于自己的发送缓冲区和接收缓冲区,可是如果这个缓冲区是一个局部的临时变量,是不符合需求的,因此,我们将服务套接字套接字和缓冲区 (还包含其他字段) 封装到一起;
  2. 其次,我们知道,未来服务套接字都需要处理读事件、写事件、异常事件,而对于监听套接字而言,它只需要关心读事件 (即获取新连接),因此,我们可以将它们统一看待,认为每个套接字都需要关读事件、写事件、异常事件,而监听套接字特殊处理即可,那么如何表示这三个事件呢? 我们通过三个回调函数表示读、写、异常事件,如果相应的事件发生,就调用相应的事件回调
  3. 再然后,我们需要一个回指指针,在这里无法说清楚,只能在后面代码解释;
  4. 最后,我们增加了一个地址信息,这个用来描述客户端的地址信息,服务端采用默认值 (服务端的无意义);

具体字段如下:

// 处理IO的回调函数类型
using IoCallBack = std::function<void(Connection*)>;
// 上层处理的回调函数
using UserCallBack = std::function<void(Connection*)>;class User
{
public:void SetUserInfo(const std::string& ip = "0.0.0.0", uint16_t port = 0){_ip = ip;_port = port;}uint16_t _port;std::string _ip;
};class Connection
{
public:Connection(int sock, TcpServer* back_ptr):_sock(sock), _back_ptr(back_ptr){}// 设置回调void SetIOEventCallBack(IoCallBack read_call_back, IoCallBack write_call_back, IoCallBack except_call_back){_read_call_back = read_call_back;_write_call_back = write_call_back;_except_call_back = except_call_back;}// 这里用public, 主要是不想写太多的Get和Set方法
public:// 监听套接字 + 服务套接字int _sock;// 每个套接字需要有自己的接收缓冲区和发送缓冲区std::string _inbuffer; // 接收缓冲区std::string _outbuffer; // 发送缓冲区// 每个套接字需要有自己的回调, 用来处理读、写、异常事件IoCallBack _read_call_back;   // 处理读时间的回调IoCallBack _write_call_back;  // 处理写事件的回调IoCallBack _except_call_back; // 处理异常事件的回调TcpServer* _back_ptr; // 回值指针, 指向服务器User _user; // 客户端地址信息
};

3.2. 服务器的成员属性 

服务器的成员属性如下:

class TcpServer
{private:// 作为服务器, 自然需要端口uint16_t _port;// 也需要监听套接字int _listensock;// 套接字对象, 封装了套接字的接口Sock _sock;// epoll 模型Epoll _epoll;// 将文件描述符和Connection以哈希表组织起来std::unordered_map<int, Connection*> _Fd_Connection_Map;// 就绪事件的最大值int _revents_num;// 存放就绪事件的数组struct epoll_event* _revents;// 上层业务的回调函数UserCallBack _OnUserCallBack;
};

作为一款服务器,端口和监听套接字是必要的,当然,因为这款服务器是基于 epoll 的,因此,也需要一个 epoll 模型,不再多说,更重要的是下面的思路。

上面说了,我们需要将套接字封装到 Connection 这个结构,换言之,未来的套接字不会单独出现,而是以 Connection 为载体出现的。

而服务端面对的是众多客户端,因此,自然会有众多的服务套接字,那么服务器是需要将它们进行管理起来的,写了这么久,我们一提到管理两个字,就应该能想到,管理,就需要先描述再组织,很碰巧,Connection 这个结构不就是一个描述的过程吗? 因此,我们只需要在进行组织即可,STL 为我们提供了这个便利,因此我们通过哈希表,将文件描述符和Connection对象组织起来。

至于这个上层业务的回调函数,该如何理解呢?

我们说过,当服务端收到数据后,无法直接对这些数据做处理,而应该交给上层 (一层中间软件层) ,让上层自己判断,服务端读到的数据,能否组成一个完整报文,如果可以,那么这层中间软件层,在将数据交给上层业务处理,否则,直接返回,不做任何处理,因此,服务器需要一个字段,这个字段指向上层定义的方法。

3.2. 服务器的构造

服务器的构造具体思路:

  • 第一步:作为服务器,毫无疑问,需要监听套接字。 过程就是:创建监听套接字、绑定、监听;
  • 第二步: 作为基于 epoll 的服务器,肯定是需要一个 epoll 模型的。 通过这个 epoll 模型,用户告诉内核,哪些文件描述符的哪些事件需要被内核关心 (epoll_ctl), 以及, 内核告诉用户,哪些文件描述符的哪些事件已经就绪 (epoll_wait),具体细节,就不论述了;
  • 第三步:
    • 首先,Reactor 模式的服务器的工作方式是ET的,因此,需要将套接字设置为非阻塞状态;
    • 其次,因为套接字是以Connection呈现的,因此需要构建Connection对象,并需要用户设置相应的回调;
    • 然后,我们需要让内核关心这些套接字,没有内核的参与,上层再怎么设计,也是无用之功,那么如何关心? 本质是将这个套接字及其关心的是间添加到epoll模型中;
    • 接着,我们要将这个套接字,及其刚刚构建的Connection对象组合起来并添加到映射表中;
    • 最后,将服务套接字对应的客户端的地址信息也设置一下,对于监听套接字而言,采用默认值 (无意义的);
    • 实际上,关于上面这几步,我们会封装成为一个函数,让所有的套接字 (监听套接字和服务套接字) 都通过这个接口完成第三步。
  • 上面的三大步,就是服务器的构造函数的具体实现思路,代码如下:
// 默认端口
const static uint16_t g_port = 8080;
// revents数组的默认大小
const static int g_revents_num = 64;
// read/recv 缓冲区的默认大小
const static int g_buffer_num = 1024;TcpServer(uint16_t port = g_port, UserCallBack OnUserCallBack = nullptr)
:_port(port)
, _revents_num(g_revents_num)
, _OnUserCallBack(OnUserCallBack)
{//  创建套接字, 绑定, 监听_sock.Socket();_sock.Bind("", _port);_sock.Listen();_listensock = _sock._sock;// 创建 epoll 模型, 返回一个 epfd_epoll.Create_Epoll();// 将套接字封装到了Connection里// 本质上是将套接字和Connection强关联到了一起, 即是一个先描述的过程// 因此, 未来不会有单独的套接字, 而是以一个整体Connection 出现// 而作为一个服务器, 是会为大量的客户端提供服务的// 换言之, 服务器会存在大量的套接字, 即Connection对象, 因此服务器// 就需要将所有的Connection对象管理起来. // 如何管理, 先描述, 再组织, 前者的工作已经就绪// 现在只需要用一个数据结构将其组织起来即可// 因此, 用一个哈希映射表, 将文件描述符和connection 对象映射,并管理起来// 监听套接字 默认为 "0.0.0.0" 和 0;AddFdConnectionToMap(_listensock, std::bind(&TcpServer::Accepter, this, std::placeholders::_1), nullptr, nullptr, "0.0.0.0", 0);// 定义数组, 用于存储就绪事件_revents = new struct epoll_event[_revents_num];LogMessage(DEBUG, "server init success");
}void AddFdConnectionToMap(int sock, IoCallBack read_call_back, IoCallBack write_call_back, IoCallBack except_call_back,const std::string& client_ip, uint16_t client_port)
{// step 0: 将所有套接字都设置为非阻塞状态Xq::Sock::SetNonBlock(sock);// step 1: 创建Connection对象Connection* connection = new Connection(sock, this);connection->SetIOEventCallBack(read_call_back, write_call_back, except_call_back);// step 2: 将套接字添加到Epoll模型中// 任何多路转接服务器, 一般默认只会打开对读事件的关心, 写事件会按需打开// 且该服务器的工作模式是ET模式, 故需要添加EPOLLET_epoll.AddIoEvent_Epoll(sock, EPOLLIN | EPOLLET);// step 3: 将套接字和connection对象添加到映射表中_Fd_Connection_Map[sock] = connection;// step 4: 设置地址信息connection->_user.SetUserInfo(client_ip, client_port);
}

3.3. 事件轮询

作为一款服务器,肯定是需要启动服务器的接口,而对于 Reactor 模式的服务器而言,它是基于事件轮询 (Event Loop) 的,其负责监听事件,当事件就绪时,根据相应回调进行处理事件;

在每一次轮询过程中,就需要调用 EventDispatch 接口,即事件派发,当事件就绪后,根据不同的事件,做不同的处理。

// 事件轮询
void EventLoop(void)
{// 阻塞式int timeout = -1;while (true){LoopOnce(timeout);}
}void LoopOnce(int timeout)
{// 事件派发EventDispatch(timeout);
}

3.4. 事件派发

事件派发的处理思路很简单,通过 epoll 模型提供的 epoll_wait 接口,获得就绪的事件,处理这些就绪的事件即可;

不过需要注意的是:

  • 对于异常事件的处理,如果出现了事件异常,我们将其统一转化为读写事件,此时进行读写时,就会读写错误,因而触发读写中的异常处理,换言之,我们将所有异常情况都会统一在一起,当发生异常时,调用统一的接口,统一处理异常情况;
  • 因此,有了统一处理异常情况的前提,服务器只需要处理读写事件即可,根据 Connection 对象以及事件的类型,调用 Connection 中的回调,进而处理事件;
  • 最后,为了严谨性,在执行相应的回调时,我们需要判断这个Connection对象是否还存在,如果存在,在根据相应的事件,调用相应的回调。

代码如下:

void EventDispatch(void)
{int num = _epoll.Wait_Epoll(_revents, _revents_num, timeout);if (num < 0) {LogMessage(ERROR, "errno: %d, error message: %s", errno, strerror(errno));}else if (num == 0) {LogMessage(NORMAL, "time out...");}else{for (int pos = 0; pos < num; ++pos){int sock = _revents[pos].data.fd;uint32_t events = _revents[pos].events;// 把异常事件统一转化成读写事件if (events & EPOLLERR)events |= (EPOLLIN | EPOLLOUT);if (events & EPOLLHUP)events |= (EPOLLIN | EPOLLOUT);// 只需要处理 EPOLLIN 和 EPOLLOUT// 读事件就绪if (IsConnectionExist(sock) && (events & EPOLLIN)){_Fd_Connection_Map[sock]->_read_call_back(_Fd_Connection_Map[sock]);}// 写事件就绪if (IsConnectionExist(sock) && (events & EPOLLOUT)){_Fd_Connection_Map[sock]->_write_call_back(_Fd_Connection_Map[sock]);}}}
}bool IsConnectionExist(int sock)
{return _Fd_Connection_Map.find(sock) != _Fd_Connection_Map.end();
}

3.5. 连接事件

因为服务器是根据事件派发中的相应回调,调用这个函数的,因此,走到这里,不会存在阻塞的情况,换言之,此时,底层是一定有就绪的连接,等待上层 accept 的;

此外,因为是ET的工作模式,故需要轮询accept:

  • 如果返回值小于0,代表accept 失败了,此时就需要根据 errno 这个全局变量,来判断,是底层没有连接了 (EWOULDBLOCK 或者 EAGAIN),还是这次的 accept 被信号中断了 (EINTR),还是真的 accept 出错了呢?
  • 如果返回值大于0,代表成功获取连接,那么也要做下面这几件事情:
    • ​​​​​​首先,Reactor 模式的服务器的工作方式是ET的,因此,需要将套接字设置为非阻塞状态;
    • 其次,因为套接字是以Connection呈现的,因此需要构建Connection对象,并需要用户设置相应的回调;
    • 然后,我们需要让内核关心这些套接字,没有内核的参与,上层再怎么设计,也是无用之功,那么如何关心? 本质是将这个套接字及其关心的是间添加到epoll模型中;
    • 接着,我们要将这个套接字,及其刚刚构建的Connection对象组合起来并添加到映射表中;
    • 最后,将服务套接字对应的客户端的地址信息也设置一下,对于监听套接字而言,采用默认值 (无意义的);
    • 很明显,为了降低复杂度和解耦,我们是需要将上面这几个过程封装为一个接口的。
  • 这就是连接事件处理的具体过程,思路很清晰,应该很好理解。

代码如下:

void Accepter(Connection* connection)
{// 如果服务器走到这里, 绝不会被阻塞// 因此可以直接获取新连接// 可是, 对于服务器而言, 可能底层会有很多完成三次握手过程的连接// 即底层不止一个链接需要被accept// 因此, 服务器通过监听套接字获取新连接, 也要以轮询的方案获取// 保证将底层的所有连接获取上来while (true){std::string clientip;uint16_t clientport;// 在轮询的过程中, 当accept失败时, 会有下面三种情况三种情况:// case 1: errno == EAGAIN || errno == EWOULDBLOCK, 代表底层连接已全部获取, 跳出循环即可;// case 2: errno == EINTR, 代表此次accept被信号中断, 重新accept获取连接即可// case 3: errno 等于其他值, 代表真的出错了;int sock = _sock.Accept(connection->_sock, clientip, &clientport);if (sock == -1){if (errno == EAGAIN || errno == EWOULDBLOCK){// 代表底层连接获取完, 跳出循环即可break;}else if (errno == EINTR){// 代表accept被某个信号中断了, 重新accept即可continue;}else{LogMessage(ERROR, "errno: %d, errno message: %s", errno, strerror(errno));break;}}else{// 获取新连接成功, 需要做三件事情:// 0. 将这个套接字设置为非阻塞状态// 1. 用得到的套接字构造 Connection 对象// 2. 将该套接字添加到epoll模型中// 3. 将套接字和Connection对象 Load 到映射表中// 4. 设置地址信息// 这几件事情不就是AddFdConnectionToMap 吗?AddFdConnectionToMap(sock, std::bind(&TcpServer::Reader, this, std::placeholders::_1), \std::bind(&TcpServer::Writer, this, std::placeholders::_1), \std::bind(&TcpServer::Excepter, this, std::placeholders::_1), \clientip, clientport);LogMessage(DEBUG, "连接成功: %d", sock);}}
}

3.6. 读事件

与连接事件一样,走到这里,说明是通过回调执行到这里的,因此,此时底层一定有数据就绪,等待服务器读取数据。

不过,在这之前,在强调一下,由于TCP是面向字节流的,因此,当服务器调用 read/recv 时,根本就无法保证获得的数据能否构成一个完整的报文,因此,是需要上层定制协议的,进行序列化和反序列化,解决粘包问题。

因此,服务器数据读取成功后,首先是需要将这部分数据保存起来,让上层进行验证 (通过设置的上层回调):

  • 如果上层验证后,可以得到一个完整报文,服务器再将保存数据中的这部分数据移除掉;
  • 如果上层验证后,没有完整报文,此时上层不会做任何处理,但不影响,因为这部分数据被服务器保存起来了,后续可以继续处理。

此外,服务器的工作模式是ET模式,因此必须要以轮询式读取数据,这个就不解释了。

最后,当服务器读取失败时,是需要根据 errno 做判断的:

  • 如果 errno == EWOULDBLOCK 或者 errno == EAGAIN,代表底层数据读完了,跳出循环即可;
  • 如果 errno == EINTR,代表此次读取数据被信号中断了,重新读即可;
  • 如果是其他情况,那么代表是异常事件,执行这个Connection的异常回调,服务器返回即可。

当然,如果服务器读取返回0,那么代表对端关闭连接了,此时服务器也将这种情况按异常事件处理,执行这个Connection的异常回调;

代码如下:

// 读回调
void Reader(Connection* connection)
{// 当服务套接字的读事件就绪后, 代表底层有数据了// 上层可以读取, 并且要以非阻塞读取, 为什么呢?// 因为服务器是ET工作模式, 底层只会通知一次// 上层必须在一次处理过程中将数据全部拷贝到应用层, 因此, 必须以非阻塞轮询式读取while (true){char buffer[g_buffer_num] = { 0 };ssize_t real_size = read(connection->_sock, buffer, sizeof buffer - 1);if (real_size == -1){if (errno == EAGAIN || errno == EWOULDBLOCK){// 说明接收缓冲区的数据全部拷贝到应用层, 此次读取 donebreak;}else if (errno == EINTR){// 说明这次读取被某个信号中断了, 继续读即可continue;}else{// 真正的读取错误了// 采用统一的方式处理异常情况connection->_except_call_back(connection);LogMessage(ERROR, "errno: %d, errno message: %s", errno, strerror(errno));return;}}else if (real_size == 0){//  如果对端连接关闭//  将这种情况也认为是异常事件, 调用这个Connection的异常回调LogMessage(NORMAL, "client close the link");connection->_except_call_back(connection);break;}else{// 读取成功, 上面说了, 这部分读取的数据不能直接交付给上层业务// 而应该先放在这个连接对象Connection 中的接收缓冲区里buffer[real_size] = 0;connection->_inbuffer += buffer;}if (IsConnectionExist(connection->_sock)){// 上层回调// 用于判断此时的_inbuffer里面的数据能否构成一个完整报文// 如果可以, 再进行上层业务处理// 如果不可以, 啥也不做_OnUserCallBack(connection);}}
}

3.7. 写事件

对于 select/poll/epoll 而言,写事件是经常就绪的,因为对于服务器而言, 发送缓冲区经常是有空间的,因此,如果服务器设置对 EPOLLOUT 的关心,那么所有的服务套接字的写事件每次都会就绪,导致 epoll 的 epoll_wait 频繁返回,这是不利的(比如浪费CPU资源),因此, 对于读事件EPOLLIN,默认设置关心,对于写事件 EPOLLOUT,服务器应该按需设置,不可以默认设置

什么是按需设置呢? 就是如果发送缓冲区还有数据,就设置,没有就不设置;
因此,EPOLLOUT 是动态设置的。当服务器走到了这里,说明这个 conn 连接的发送缓冲区一定有数据,因此,直接发送。
又因为,这个服务器的工作模式是ET模式,因此也要轮询式的发送数据;

直至将 outbuffer 的数据写完,或者服务器底层缓冲区没有能力在接受数据:

  • 如果是前者,即发送缓冲区没数据了,那么此时就去掉这个Connection中对写事件 (EPOLLOUT) 的关心;
  • 如果是后者,即发生缓冲区还有数据,那么对这个Connection设置对写事件 (EPOLLOUT)  的关心;
void Writer(Connection* conn)
{while (true){ssize_t real_size = send(conn->_sock, conn->_outbuffer.c_str(), conn->_outbuffer.size(), 0);if (real_size < 0){// 写入失败, 也要分析情况if (errno == EAGAIN || errno == EWOULDBLOCK){// 代表服务器底层的缓冲区已被写满, 暂时不能再写了break;}else if (errno == EINTR){// 此次send被信号中断, 重新写即可continue;}else{// 真正的写错了, 统一交给异常处理conn->_except_call_back(conn);return;}}else if (real_size == 0){break;}else{// send success// 将写入的这部分数据, 从outbuffer里面移除conn->_outbuffer.erase(0, real_size);if (conn->_outbuffer.empty()) break;}}// 跳出循环, 两种情况// 第一种发送缓冲区没数据了, 那么去掉对这个连接写事件的关心if (conn->_outbuffer.empty())SetReadAndWriteConcern(conn, true, false);// 如果outbuffer还有数据, 那么让这个套接字关心写事件// 下次epoll_wait时, 写事件就绪, 自动调用WriterelseSetReadAndWriteConcern(conn, true, true);
}void SetReadAndWriteConcern(Connection* conn, bool ReadEvent, bool WriteEvent)
{uint32_t events = 0;// 无论如何, 都是ET工作模式events |= EPOLLET;ReadEvent == true ? (events |= EPOLLIN) : events |= 0;WriteEvent == true ? (events |= EPOLLOUT) : events |= 0;_epoll.ModIoEvent_EPoll(conn->_sock, events);
}

3.8. 异常事件

对于异常事件,我们进行统一处理,因为服务器要关闭这个连接了。

实现如下:

void Excepter(Connection* conn)
{// 走到这里, 说明这个连接出现异常事件了// 但服务器不需要判别是什么原因// 因为服务器要关闭这个连接了int sock = conn->_sock;// 关闭连接分四个过程// step 1: 将这个连接中的套接字从epoll模型中删除_epoll.DelIoEvnet_Epoll(conn->_sock);// step 2: 将连接中的套接字closeclose(conn->_sock);// step 3: 将套接字和conn构成的节点从映射表中移除_Fd_Connection_Map.erase(sock);LogMessage(DEBUG, "the sock %d closed", sock);// step 4: 释放这个节点delete conn;
}

4. 服务器上层的处理

当服务器读 (read / recv) 到数据后,服务器首先需要将数据保存起来,然后,调用上层回调,让上层自己根据协议判断,这些数据是否能够构成一个完整报文,如果可以,上层接下来就可以处理业务逻辑;如果不可以,上层直接返回,不做任何业务逻辑处理。

大致过程如图所示:

当上层获得了若干个完整报文后,它就会自动将这些完整报文构成一个一个的任务,并将这些任务 push 进线程池中的任务队列中,线程池中的线程会自动处理这个任务,并将任务结果构成一个响应,并对其进行序列化,然后,将序列化后的数据push进这个连接中的发送缓冲区中 (outbuffer),此时上层业务逻辑就完成了。

大致过程如图所示:

当服务器中的发送缓冲区数据就绪,此时,上层业务可以直接通过Connection对象中的写回调,向服务器的对端发送数据,因为此时服务器中的发送缓冲区有数据。

在调用写回调过程中,服务器是需要根据写的结果来判定这个套接字后续是否还要关心写事件,如果这个连接的发送缓冲区还有数据,那么这个套接字应该要关心写事件,如果没有数据了,那么去掉对这个套接字的写事件的关心。

大致过程如下:

上述过程就是上层处理的全部过程。

至于其中的具体细节,包括,序列化和反序列化、分割报文、任务的封装、线程的封装、线程池的封装、锁的封装等等工作,如果有兴趣,可以看下代码,当然,你也可以自己实现,对于上层的实现并不是固定的,我们的重心并不是上层如何处理的,而是理解 Reactor 模式服务器的具体思路和过程。

5. Reactor 总结 

总结: Reactor服务器是一种常见的网络服务器架构,通常用于处理大量并发连接和请求。其核心思路就在于 Reactor 将事件的产生和处理进行分离,它包含如下模块 :

  • 事件轮询:Reactor服务器采用了事件驱动的架构模式,其中包括一个主事件循环(Event Loop),负责监听和派发事件;
  • 多路复用:Reactor服务器通常使用多路复用技术来监听多个I/O通道的事件。这样可以在单进程中同时处理多个连接,提高服务器的性能和吞吐量;
  • 事件处理器:Reactor服务器通过事件处理器来处理不同类型的事件。每个事件处理器通常负责特定类型的事件,例如获取连接、读取数据、发送数据、异常事件等。通过将事件处理器分离开来,可以使服务器代码更易于管理和扩展;
  • ET 工作模式:为了提高服务器的性能,Reactor 服务器一般采用 ET 工作模式,即所有的套接字的工作模式是非阻塞的。因为ET模式会减少IO的次数,提高效率,且ET模式会要求一次处理过程将数据全部读取,因此可以给对端发送一个更大的窗口大小,因此,对端就有可能存在更大的滑动窗口,发送的数据就更多,进而提高网络吞吐量。

总的来说,Reactor 服务器是一种高效的并发服务器架构,通过事件轮询、多路复用、事件处理器、ET工作模式等技术,能够有效地处理大规模并发请求,适用于许多网络应用的场景。

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

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

相关文章

公钥密码学Public-Key Cryptography

公钥或非对称密码学的发展是整个密码学历史上最伟大的&#xff0c;也许是唯一真正的革命。The development of public-key, or asymmetric, cryptography is the greatest and perhaps the only true revolution in the entire history of cryptography. 公钥算法基于数学函数…

node.js如何实现留言板功能?

一、实现效果如下&#xff1a; 20240422_160404 二、前提配置&#xff1a; 配置&#xff1a;需要安装并且导入underscore模板引擎 安装&#xff1a;在控制台输入npm install underscore -save 文件目录配置&#xff1a; 1》在文件里建一个data文件夹&#xff0c;此文件夹下…

ContextMenuStrip内容菜单源对象赋值学习笔记(含源码)

一、前言 MetroTileItem属于第三方控件,无法定义ContextMenuStrip属性 想实现某子项点击菜单时,与源控件(按钮metroTileItem)的某值对应,用于动态控制按钮的状态或方法 1.1 效果 二、实现方法 2.1 方法1 (代码,说明见注释) private void metroTileItem_MouseDown(o…

【题解】AB5 点击消除(栈)

https://www.nowcoder.com/practice/8d3643ec29654cf8908b5cf3a0479fd5?tpId308&tqId40462&ru/exam/oj 把string当栈用&#xff0c;扫一遍就可以了&#xff0c;时间复杂度O(n) #include <iostream> #include <string> using namespace std;int main() {…

向量的点积和叉积的几何意义

1. 点积 点积(dot product)&#xff0c;又称标量积&#xff08;scalar product&#xff09;。结果等于。 可用于 判断的是否垂直求投影长度求向量是抑制作用还是促进作用 2. 叉积 叉积(cross product)&#xff0c;又称为向量积(vector product)。模长等于&#xff0c;方向…

Golang | Leetcode Golang题解之第43题字符串相乘

题目&#xff1a; 题解&#xff1a; func multiply(num1 string, num2 string) string {if num1 "0" || num2 "0" {return "0"}m, n : len(num1), len(num2)ansArr : make([]int, m n)for i : m - 1; i > 0; i-- {x : int(num1[i]) - 0fo…

详细说说,中介怎么做!CLHLS数据库探索抑郁症状的中介作用发文二区

零基础CHARLS发论文&#xff0c;不容错过&#xff01; 长期回放更新指导&#xff01;适合零基础&#xff0c;毕业论文&#xff0c;赠送2011-2020年CHARLS清洗后的数据全套代码&#xff01; 2024年3月28日&#xff0c;中国学者用CLHLS数据库最新数据&#xff08;2018年&#xff…

java-Arrays

一、Arrays的概述 Arrays是操作数组的工具类 二、Arrays的常用方法 Arrays的常用方法基本上都被static静态修饰&#xff0c;因此在使用这些方法时&#xff0c;可以直接通过类名调用 1.toString 语法&#xff1a;Arrays.toString(数组) 用于将数组的元素转换为一个字符串&a…

蓝桥杯第17169题——兽之泪II

问题描述 在蓝桥王国&#xff0c;流传着一个古老的传说&#xff1a;在怪兽谷&#xff0c;有一笔由神圣骑士留下的宝藏。 小蓝是一位年轻而勇敢的冒险家&#xff0c;他决定去寻找宝藏。根据远古卷轴的提示&#xff0c;如果要找到宝藏&#xff0c;那么需要集齐 n 滴兽之泪&#…

Git | 分支管理

Git | 分支管理 文章目录 Git | 分支管理1、理解分支2、创建分支&&切换分支3、合并分支4、删除分支5、合并冲突6、分支管理策略合并分支模式实际工作中分支策略bug分支删除临时分支 1、理解分支 分支就类似分身。 在版本回退中&#xff0c;每次提交Git都会将修改以git…

简单学量化——pandas的应用26——sort_values函数5

简单学量化——pandas的应用26——sort_values函数5 sort_values是pandas中的排序函数&#xff0c;语法如下&#xff1a; DataFrame.sort_values(by,axis0,ascendingTrue,inplaceFalse,kindquicksort,na_positionlast, ignore_indexFalse,keyNone) 前面我们学习了by、axis、a…

C++之写时复制(CopyOnWrite)

设计模式专栏&#xff1a;http://t.csdnimg.cn/4j9Cq 目录 1.简介 2.实现原理 3.QString的实现分析 3.1.内部结构 3.2.写入时复制 4.示例分析 5.使用场景 6.总结 1.简介 CopyOnWrite (COW) 是一种编程思想&#xff0c;用于优化内存使用和提高性能。COW 的基本思想是&am…

go的编译以及运行时环境

开篇 很多语言都有自己的运行时环境&#xff0c;go自然也不例外&#xff0c;那么今天我们就来讲讲go语言的运行时环境&#xff01; 不同语言的运行时环境对比 我们都知道Java的运行时环境是jvm &#xff0c;javascript的运行时环境是浏览器内核 Java -->jvm javascript…

FastWiki一分钟本地离线部署本地企业级人工智能客服

介绍 FastWiki是一个开源的企业级人工智能客服系统&#xff0c;它使用了一系列先进的技术和框架来支持其功能。 技术栈 前端框架&#xff1a;React LobeUI TypeScript后端框架&#xff1a;MasaFramework 基于 .NET 8动态函数&#xff1a;基于JavaScript V8引擎实现向量搜索…

物联网配网工具多元化助力腾飞——智能连接,畅享未来

随着物联网技术的迅猛发展&#xff0c;智能插座、蓝牙网关作为其中常见的智能物联设备&#xff0c;无论是功能还是外观都有很大的改进&#xff0c;在智能化越来越普遍的情况下&#xff0c;它们的应用场景也在不断拓宽。对于智能设备而言&#xff0c;配网方式的选择对于设备的成…

Jenkins CI/CD 持续集成专题一 Jenkins的安装和配置

一 jenkins 官方教程 安装Jenkins 二 安装 2.1 安装方式一 通过安装包的package方式安装 第一步下载链接&#xff1a; Download the latest package 第二步操作方式&#xff1a;打开包并按照说明操作即可安装 2.2 安装方式二 brew安装 第一 安装最新版本jenkins brew in…

【Java框架】SpringMVC(二)——SpringMVC数据交互

目录 前后端数据交互RequestMapping注解基于RequestMapping注解设置接口的请求方式RequestMapping注解的常用属性一个方法配置多个接口method属性params属性headers属性consumes属性produces属性 SpringMVC中的参数传递默认单个简单参数默认多个简单参数默认参数中有基本数据类…

山与路远程控制 一个基于electron和golang实现的远控软件

山与路远程控制 &#x1f3a5;项目演示地址 还在制作… ♻️项目基本介绍 山与路远程控制是基于electron(vue3)和golang实现的远程控制软件(项目界面主要模仿向日葵远程软件,如有侵权请告知),代码可能有点臃肿毕竟只花了一周左右写的无聊项目,如果对其感兴趣的大佬可以fork自…

【JavaScriptThreejs】判断路径在二维平面上投影的方向顺逆时针

原理分析 可以将路径每个连续的两点向量叉乘相加&#xff0c;根据正负性判断路径顺逆时针性 当我们计算两个向量的叉积时&#xff0c;结果是一个新的向量&#xff0c;其方向垂直于这两个向量所在的平面&#xff0c;并且其大小与这两个向量构成的平行四边形的面积成正比。这个新…

爱普生RX-8130CE内置电池控制 RTC

特点&#xff1a;(1)封装极小&#xff0c;集成度高RX-8130CE是一个带|2C接口的实时时钟模块&#xff0c;内部集成32.768KHz晶体振荡器。实时时钟功能不仅集成了年、月、日、星期、小时、分、秒的日历和时钟计数器&#xff0c;同时也有时间闹钟、间隔定时器、时间更新中断等功能…