Reactor 模式

news2024/9/24 1:22:20

目录

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对象组合起来并添加到映射表中;
    • 最后,将服务套接字对应的客户端的地址信息也设置一下,对于监听套接字而言,采用默认值 (无意义的);
    • 实际上,关于上面这几步,我们会封装成为一个函数,让所有的套接字 (监听套接字和服务套接字) 都通过这个接口完成第三步。
  • 上面的三大步,就是服务器的构造函数的具体实现思路,代码如下:
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 = 3000;
	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);
	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, 如何判定accept将底层连接全获取, 和accept出错了呢?
		// 因此, 我们需要修改一下我们的Accept, 传一个err (输出型参数),
		// 用它来表明errno, 上层通过errno来判断accept失败的原因
		int err = 0;
		int sock = _sock.Accept(connection->_sock, clientip, &clientport, &err /*用这个err来获取errno*/);
		if (sock == -1)
		{
			if (err == EAGAIN || err == EWOULDBLOCK)
			{
				// 代表底层连接获取完, 跳出循环即可
				break;
			}
			else if (err == EINTR)
			{
				// 代表accept被某个信号中断了, 重新accept即可
				continue;
			}
			else
			{
				LogMessage(ERROR, "errno: %d, errno message: %s", err, strerror(err));
			}
		}
		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* conn)
{
	// 当服务套接字的读事件就绪后, 代表底层有数据了
	// 上层可以读取, 并且要以非阻塞读取, 为什么呢?
	// 因为服务器是ET工作模式, 底层只会通知一次
	// 上层必须在一次处理过程中将数据全部拷贝到应用层, 因此, 必须以非阻塞轮询式读取
	while (true)
	{
		char buffer[g_buffer_num] = { 0 };
		ssize_t real_size = read(conn->_sock, buffer, sizeof buffer - 1);
		if (real_size == -1)
		{
			if (errno == EAGAIN || errno == EWOULDBLOCK)
			{
				// 说明接收缓冲区的数据全部拷贝到应用层, 此次读取 done
				break;
			}
			else if (errno == EINTR)
			{
				// 说明这次读取被某个信号中断了, 重新读
				continue;
			}
			else
			{
				// 真正的读取错误了
				// 采用统一的方式处理异常情况
				conn->_except_call_back(conn);
				LogMessage(ERROR, "errno: %d, errno message: %s", errno, strerror(errno));
				return;
			}
		}
		else if (real_size == 0)
		{
			LogMessage(NORMAL, "client close the link");
			conn->_except_call_back(conn);
			break;
		}
		else
		{
			// 读取成功, 上面说了, 这部分读取的数据不能直接交付给上层业务
			// 而应该先放在这个连接对象Connection 中的接收缓冲区里
			buffer[real_size] = 0;
			conn->_inbuffer += buffer;
		}
		if (IsConnectionExist(conn->_sock))
		{
            // 上层业务回调
			_OnUserCallBack(conn);
		}
	}
}

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时, 写事件就绪, 自动调用Writer
	else
		SetReadAndWriteConcern(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: 将连接中的套接字close
	close(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.coloradmin.cn/o/1616273.html

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

相关文章

开源啦!一键部署免费使用!Kubernetes上直接运行大数据平台!

市场上首个K8s上的大数据平台&#xff0c;开源啦&#xff01; 智领云自主研发的首个 完全基于Kubernetes的容器化大数据平台 Kubernetes Data Platform (简称KDP) 开源啦&#x1f680;&#x1f680; 开发者只要准备好命令行工具&#xff0c;一键部署 Hadoop&#xff0c;Hi…

JavaScript(二)

JavaScript的语法 1.JavaScript的大小写 在JavaScript中&#xff0c;大小写是敏感的&#xff0c;这意味着大小写不同的标识符被视为不同的变量或函数。例如&#xff0c;myVariable 和 myvariable 被视为两个不同的变量。因此&#xff0c;在编写JavaScript代码时&#xff0c;必…

函数声明与调用:接口原型、参数传递顺序、返回值

示例&#xff1a; /*** brief how about function-declare-call? show you here.* author wenxuanpei* email 15873152445163.com(query for any question here)*/ #define _CRT_SECURE_NO_WARNINGS//support c-library in Microsoft-Visual-Studio #include <stdio.h&…

上位机图像处理和嵌入式模块部署(树莓派4b实现多进程通信)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 和mcu固件、上位机软件不太一样的地方&#xff0c;嵌入式设备上面上面的工业软件一般都是多进程的形式。相比较多线程而言&#xff0c;整个系统就不…

springcloudgateway集成knife4j

上篇我们聊聊springboot是怎么继承knife4j的。springboot3 集成knife4j-CSDN博客 本次我们一起学习springcloudgateway集成knife4j。 环境介绍 java&#xff1a;17 SpringBoot&#xff1a;3.2.0 SpringCloud&#xff1a;2023.0.0 knife4j &#xff1a; 4.4.0 引入maven配置…

# 从浅入深 学习 SpringCloud 微服务架构(四)Ribbon

从浅入深 学习 SpringCloud 微服务架构&#xff08;四&#xff09;Ribbon 段子手168 一、ribbon 概述以及基于 ribbon 的远程调用。 1、ribbon 概述&#xff1a; Ribbon 是 Netflixfa 发布的一个负载均衡器,有助于控制 HTTP 和 TCP客户端行为。 在 SpringCloud 中 Eureka …

就业班 第三阶段(负载均衡) 2401--4.19 day3 nginx3

二、企业 keepalived 高可用项目实战 1、Keepalived VRRP 介绍 keepalived是什么keepalived是集群管理中保证集群高可用的一个服务软件&#xff0c;用来防止单点故障。 ​ keepalived工作原理keepalived是以VRRP协议为实现基础的&#xff0c;VRRP全称Virtual Router Redundan…

用python selenium实现短视频一键推送

https://github.com/coolEphemeroptera/VIVI 效果如下 demo 支持youtube视频搬运

iPerf 3 测试UDP和TCP方法详解

文章目录 前言一、What is iPerf / iPerf3 ?二、功能1. TCP and SCTP2. UDP3. 其他 三、 Iperf的使用1.Iperf的工作模式2. 通用指令3. 服务端特有选项4. 客户端特有选项5. -t -n参数联系 四、Iperf使用实例1. 调整 TCP 连接1. 1TCP 窗口大小调节1. 2 最大传输单元 (MTU)调整 2…

【python项目推荐】键盘监控--统计打字频率

原文&#xff1a;https://greptime.com/blogs/2024-03-19-keyboard-monitoring 代码&#xff1a;https://github.com/GreptimeTeam/demo-scene/tree/main/keyboard-monitor 项目简介 该项目实现了打字频率统计及可视化功能。 主要使用的库 pynput&#xff1a;允许您控制和监…

kafka 命令行使用 消息的写入和读取 quickstart

文章目录 Intro命令日志zookeeper serverkafka servercreate topic && describe topic Intro Kafka在大型系统中可用作消息通道&#xff0c;一般是用程序语言作为客户端去调用kafka服务。 不过在这之前&#xff0c;可以先用下载kafka之后就包含的脚本文件等&#xff0…

在Spring Boot应用中实现阿里云短信功能的整合

1.程序员必备程序网站 天梦星服务平台 (tmxkj.top)https://tmxkj.top/#/ 2.导入坐标 <dependency><groupId>com.aliyun</groupId><artifactId>aliyun-java-sdk-core</artifactId><version>4.5.0</version></dependency><…

Spring IOC 和 DI详解

目录 一、IOC介绍 1、什么是IOC 2、通过案例来了解IoC 2.1 传统程序开发 2.2 问题分析 2.3 解决方案 2.4 IoC程序开发 2.5 IoC 优势 二、DI介绍 三、IOC 详解 3.1 Bean的存储 3.1.1 Controller&#xff08;控制器存储&#xff09; 3.1.2 Service&#xff08;服务存…

照片相似性搜索引擎Embed-Photos;赋予大型语言模型(LLMs)视频和音频理解能力;OOTDiffusion的基础上可控制的服装驱动图像合成

✨ 1: Magic Clothing Magic Clothing是一个以可控制的服装驱动图像合成为核心的技术项目&#xff0c;建立在OOTDiffusion的基础上 Magic Clothing是一个以可控制的服装驱动图像合成为核心的技术项目&#xff0c;建立在OOTDiffusion的基础上。通过使用Magic Clothing&#xf…

hadoop安装记录

零、版本说明 centos [rootnode1 ~]# cat /etc/redhat-release CentOS Linux release 7.9.2009 (Core)jdk [rootnode1 ~]# java -version java version "1.8.0_311" Java(TM) SE Runtime Environment (build 1.8.0_311-b11) Java HotSpot(TM) 64-Bit Server VM (…

STL_List与萃取

List 参考文章: https://blog.csdn.net/weixin_45389639/article/details/121618243 List源码 List中节点的定义&#xff1a; list是双向列表&#xff0c;所以其中节点需要包含指向前一节点和后一节点的指针&#xff0c; data是节点中存储的数据类型 template <class _Tp&g…

海康Visionmaster-常见问题排查方法-启动阶段

VM试用版启动时&#xff0c;弹窗报错&#xff1a;加密狗未安装或检测异常&#xff1b;  问题原因&#xff1a;安装VM 的时候未选择软加密&#xff0c;选择了加密狗驱动&#xff0c;此时要使用软授权就出现了此现象。  解决方法&#xff1a; ① 首先确认软加密驱动正确安装…

网络工程师----第十一天

OSPF&#xff1a; 对称加密算法&#xff1a; 也称为私钥加密或单密钥算法&#xff0c;是一种加密方式&#xff0c;其中加密和解密使用相同的密钥。这种算法的优点包括加密解密速度快、计算量小&#xff0c;适用于大量数据的加密。然而&#xff0c;它的缺点是密钥的安全性难以保…

OpenCV-基于阴影勾勒的图纸清晰度增强算法

作者&#xff1a;翟天保Steven 版权声明&#xff1a;著作权归作者所有&#xff0c;商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处 实现原理 大家在工作和学习中&#xff0c;无论是写报告还是论文&#xff0c;经常有截图的需求&#xff0c;比如图表、图纸等&…

医学影像图像去噪:滤波器方法、频域方法、小波变换、非局部均值去噪、深度学习与稀疏表示和字典学习

医学影像图像去噪是指使用各种算法从医学成像数据中去除噪声,以提高图像质量和对疾病的诊断准确性。MRI(磁共振成像)和CT(计算机断层扫描)是两种常见的医学成像技术,它们都会受到不同类型噪声的影响。 在医学影像中,噪声可能来源于多个方面,包括成像设备的电子系统、患…