写点什么

探索 Reactor 网络模型在当今应用领域的革新

  • 2023-07-21
    广东
  • 本文字数:3863 字

    阅读完需:约 13 分钟

探索Reactor网络模型在当今应用领域的革新

本文分享自华为云社区《驾驭网络技术的未来:探索Reactor网络模型在当今应用领域的革新》,作者: Lion Long 。


本文介绍了 Linux 网络设计中的 Reactor 网络模型及其在实际应用中的重要性。Reactor 模型是一种经典的事件驱动设计模式,广泛应用于构建高性能、可扩展的网络服务器。我们将探讨 Reactor 模型的基本原理和组成部分,并详细介绍了 Reactors 模型在 Linux 网络编程中的实现方式。

一、reactor 网络模型编程介绍


reactor 是将对 IO 的检测转换为对事件的处理,是一种异步事件机制。reactor 会使用 IO 多路复用进行 IO 检测,IO 多路复用器一般是:select、poll、epoll。


reactor 大致逻辑:


(1)socket()创建一个套接字,listenfd;


(2)bind()、listen()配置 listenfd,绑定和监听;


(3)listenfd 注册读事件,交由 epoll 管理;


(4)读事件触发,回调 accept;


(5)客户端连接 clientfd 组成读事件;


(6)相关事件调用相关回调函数

1.1、建立连接


接收客户端连接。


//...
int epfd=epoll_create(1);//创建epoll对象
//...
int listenfd=socket(AF_INET,SOCK_STREAM,0);//创建套接字
//...
struct epoll_event ev;
ev.events=EPOLLIN;
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev)//注册事件
//...
// 当触发listenfd的读事件,调用accept接收连接
struct sockaddr_in clientaddr;
socklen_t len=sizeof(clientaddr);
int clientfd=accept(listenfd,(struct sockaddr *)&clientaddr,&len);
struct epoll_event ev;
ev.events=EPOLLIN;
epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&ev)//注册新连接的读事件
//...
复制代码


连接第三方服务。


//...
int epfd=epoll_create(1);//创建epoll对象
//...
int fd=socket(AF_INET,SOCK_STREAM,0);//创建套接字
//...
struct sockaddr_in clientaddr;
socklen_t len=sizeof(clientaddr);
connect(fd,(struct sockaddr *)&clientaddr,&len);//连接服务
//...
struct epoll_event ev;
ev.events=EPOLLOUT;
epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev)//注册事件
//...
// 当触发fd的写事件,连接建立成功
if(status==e_connecting && ev.events==EPOLLOUT)
{
status=e_connected;
epoll_ctl(epfd,EPOLL_CTL_DEL.fd,NULL);
}
//...
复制代码

1.2、断开连接


//...
if(ev.events & EPOLLRDHUP)
{
// 关闭服务器读端
close_read(fd);
}
if(ev.events & EPOLLHUP)
{
// 关闭服务器读写端
close(fd);
}
//...
复制代码

1.3、数据到达


// ...
if(ev.events & EPOLLIN)
{
while(1)
{
int n=recv(clientfd,buffer,buffer_size,0);
if(n<0)
{
if(errno==EINTR)
continue;
if(errno==EWOULDBLOCK)
break;
close(clientfd);
}
else if(n==0)
{
close_read();
}
else
{
// 处理业务
}
}
// ...
}
// ...
复制代码

1.4、数据发送


// ...
if(ev.events & EPOLLOUT)
{
int n=send(clientfd,buffer,buffer_size,0);
if(n<0)
{
if(errno==EINTR)
continue;
if(errno==EWOULDBLOACK)
{
struct epoll_event e;
e.events=EPOLLOUT;
epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&e)//注册事件
return;
// break;
}
close(clientfd);
}
else if(n==buffer_size)
{
epoll_ctl(epfd,EPOLL_CTL_DEL,clientfd,NULL);
// epoll_ctl(epfd,EPOLL_CTL_MOD,clientfd,&e);
}
// ...
}
//...
复制代码

1.5、reactor 常见疑问

1、epoll 惊群


何为”惊群“?网络编程经常使用多线程、多进程模型,每个线程或进程中都有一个 epoll 对象,通过 socket()、bind()、listen()生成的 listenfd 可能会给多个 epoll 对象管理,当一个 accept 到来时所有的 epoll 都收到通知,所有进程或线程同时响应这一事件,然而最终只有一个 accept 成功。这就是”惊群“。

2、水平触发和边沿触发


水平触发:当读缓冲区中有数据时,一直触发,直到数据被读完。


边沿触发:来一次事件触发一次。读写操作一般需要配合循环才能全部读写完成。

3、reactor 为什么要搭配非阻塞 IO?


主要是三方面原因:


(1)多线程环境下,一个 listenfd 会被多个 epoll(IO 多路复用器)对象管理,当一个连接到来时所有的 epoll 都收到通知,所有的 epoll 都会去响应,但最终只有一个 accept 成功;如果使用阻塞,那么其他的 epoll 将一直被阻塞着。所以最好使用非阻塞 IO 及时返回。


(2)边沿触发下,事件触发才会读事件,那么需要在一次事件循环中把缓冲区读空;如果使用阻塞模式,那么当读缓冲区的数据被读完后,就会一直阻塞住无法返回。


(3)select bug。当某个 socket 接收缓冲区有新数据分节到达,然后 select 报告这个 socket 描述符可读,但随后,协议栈检查到这个新分节检验和错误,然后丢弃了这个分节,这时调用 recv/read 则无数据可读;如果 socket 没有设置成 nonblocking,此 recv/read 将阻塞当前线程。

4、IO 多路复用一定要搭配非阻塞 IO 吗?


不是,也可以使用阻塞模式。比如 MySQL 使用 select 接收连接,然后一个连接使用一个线程进行处理;也可以使用一个系统调用先获取读缓冲区的字节数,然后读一次就把数据读完,但是这样就导致效率比较低。


int n=EVBUFFER_MAX_READ_DEFAULT;
ioctl(fd,FIONREAD,&n);//获取读缓冲区的数据字节数
复制代码

二、reactor 应用场景


使用单个 reactor 的场景和使用多个 reactor 的场景。使用多个 reactor 又有多线程和多进程的不同用法。

2.1、redis——使用单 reactor


redis 是一种 key-value 结构、有丰富的数据结构、对内存进行操作的网络数据库组件。redis 的命令处理是单线程的。

2.1.1、redis 为什么使用单 reactor?


要理解 redis 为什么只使用单个 reactor,需要明白 redis 的命令处理是单线程的


redis 提供丰富的数据结构,对这些数据结构进行加锁非常复杂,所以 redis 使用单线程进行处理;因为使用单线程进行命令处理,核心业务逻辑是单线程,那么使用再多的 reactor 是无法处理过来的;所以,redis 使用单个 reactor。


另外,redis 操作具体命令的时间复杂度比较低,更加没有必要使用多个 reactor。

2.1.2、redis 处理 reactor 框图


2.1.3、redis 对 reactor 的优化


对业务逻辑进行了优化,引入 IO 线程:


接收完数据后,将数据抛到 IO 线程进行处理;发送数据之前,将打包数据放在 IO 线程进行处理,再发送出去。参考上图,就是将(read+decode)放到线程中处理,将(encode+write)放在线程中处理。


原因:


对于单线程而言,当接收的数据或发送的数据过大时,会造成线程负载过大,需要引用多线程做 IO 数据处理。特别是解协议过程,数据庞大而且耗时,需要开一个 IO 线程进行处理。


场景例子:


客户端上传日志记录;客户端获取排行榜记录。

2.1.4、从 reactor 角度看 redis 源码


创建一个 epoll 对象:



创建套接字,绑定监听:



listenfd 放到 epoll 管理:



监听事件:



处理事件:



为 clientfd 注册读事件:


2.2、memcached——多线程方式使用多个 reator


memcached 是一种 key-value 结构、对内存进行操作的网络数据库组件。memcached 的命令处理是多线程的。


memcached 需要 libevent,libevent 就是一个事件驱动库,memcached 对于网络上的使用都是基于 libevent 的。

2.2.1、memcached 为什么使用多 reactor?


memcached 的 key-value 结构不像 redis 支持丰富的数据结构,它的 value 使用的数据结构相对简单,加锁也就相对容易。因此,可以引入多线程,提高效率。

2.2.2、memcached 如何处理 reactor?


memcached 主线程会有一个 reactor,主要负责接收连接;接收完连接后,经过负载均衡,通过 pipe(管道)告诉子线程的 reactor,将客户端的 fd 交由该线程的 reactor 管理;每个线程处理相对应的业务逻辑。


2.2.3、从 reactor 角度看 memcached 源码


github 上下载最新的memcached


开始源码分析:


创建套接字,绑定监听:


注册 listenfd 读事件:


分配 clientfd 到具体的线程中,添加读事件:


2.3、nginx——多进程方式使用多个 reator


nginx 可以反向代理,利用多进程处理业务。


master 会创建 listenfd,并 bind 和 listen;fork 出多个进程,每个进程都有一个自己的 epoll 对象,listenfd 交由多个 epoll 对象管理。这时会有惊群现象,需要处理;通过负载均衡处理事件。


2.3.1、解决"惊群"问题


加锁方式。nginx 会开辟一个共享内存,把锁放在共享内存当中,多个进程去争夺这把锁,争夺到锁的才能进行接受连接。

2.3.2、负载均衡


定义一个进程最大的连接数,当连接数量超过总连接数量的 7/8 时,该进程就会暂停接受连接,将机会留个其他进程。


这样不会让一个进程拥有过多的连接,而其他进程连接数量过少;从而使每个进程的连接数量相对平衡。


当所有的进程接受连接的数量都达到总连接数量的 7/8 时,这是 nginx 接受连接将变得很缓慢。

总结


在本文中深入探讨了 Linux Reactor 网络模型,并着重介绍了其在实际应用中的重要性和优势。Reactors 模型是一种高效的网络设计模式,它在处理并发连接时表现出色,使得我们能够构建高性能、可伸缩的网络应用程序。


首先,了解了 Reactors 模型的基本原理。它采用事件驱动的方式,通过一个主循环监听输入事件,一旦有事件发生,就会调用相应的处理程序。这种非阻塞的设计使得服务器能够高效地处理大量并发连接,而无需为每个连接创建一个线程。


接着,探讨了 Reactors 模型在 Linux 网络设计中的实际应用。还深入剖析了事件处理和回调机制,帮助读者理解如何优化网络应用的设计。


相比传统的多线程或多进程模型,Reactors 模型能够更好地利用系统资源,减少上下文切换和线程创建的开销,从而提高应用的并发处理能力。


本文旨在帮助读者全面了解 Linux Reactor 网络模型,并鼓励他们在自己的网络应用中运用这一模型,以构建出更高性能、更可靠的网络应用。掌握 Reactors 模型的知识,将使读者能够更加自信地驾驭网络技术的未来,迎接不断变化的挑战。


点击关注,第一时间了解华为云新鲜技术~

发布于: 刚刚阅读数: 4
用户头像

提供全面深入的云计算技术干货 2020-07-14 加入

生于云,长于云,让开发者成为决定性力量

评论

发布
暂无评论
探索Reactor网络模型在当今应用领域的革新_开发_华为云开发者联盟_InfoQ写作社区