前言


这篇文章主要介绍整个框架用到的最核的一个设计模式:反应器模式。这个设计模式可以在《面向对象的软件架构》中详细了解,没有这本书的小伙伴不要急,我通过咱们的SimpleRpc来告诉大家这个设计模式是如何运用的。之所以它叫反应器模式,是因为它是处理事件的一种比较优美的框架。如何优美,我们慢慢道来。

如何设计一个高吞吐量的web服务?


web服务会面对大量的网络请求,服务要对这些请求进行处理,如何设计我们的web服务呢?有如下几种模型供参考。

  1. 单线程模型 系统中使用唯一的一个线程处理网络数据的读,对请求数据的处理,以及发送响应。当一个网络请求到来时,工作线程通过accept获取到活动socket,接下来该线程顺序的读取数据、处理数据、生成响应数据、写回socket。这个模型有一个明显的缺点:当服务处理一个请求时,其它请求将阻塞得不到响应。它难以胜任高并发大吞吐量的web服务。
  2. 多线程模型 每当accept返回一个新的活动socket后,主线程就创建一个新线程进行数据的读、处理、响应。这样做的好处就是当一个线程处理请求时,其它线程可以接收新的请求并进行响应。它的缺点也很明显:当请求的并发量多的时候,系统会同时有多个线程工作。当线程非常多时,cpu需要在多个线程之间做切换,切换的开销将会增大,不是一个伸缩性很好的设计。我们可以使用线程池来优化这个设计,但是这么做也有一个问题:如果某些客户端与服务端建立连接后并不是马上发送数据,那么此时服务端会有大量的线程就都hang在socket的读上面(因为网络数据迟迟不发送),cpu使用率不高。

如何克服上面两种模型的弊端呢?我们的反应器模式开始大展身手。

反应器模式


  • Select/Epoll简介: 上面两种模型没有用到操作系统提供的更高级的网络数据处理机制:select模型/Epoll模型。这是一个能够同时监听多个socket句柄上的动作的机制,由操作系统支持。它维护一个监听列表,用户可以动态添加和删除需要监听的socket句柄。如果一个句柄被加入了它的监听列表,当句柄有新数据到来或者句柄可写时就会产生一个电位差提醒操作系统有socket句柄可以操作(读或者写),这时select/Epoll就会告诉用户可以在哪些socket句柄上做操作。它的优点是不会因为其中任何一个句柄的阻塞而忽略其它句柄上的可操作事件。

有了Select/Epoll这个利器,我们就可以设计反应器了:

  • Reactor(反应器)接口定义:

regist:在一个socket句柄上注册操作函数。

remove:从反应器中移除对某个句柄的监听。

handle_events:通过系统提供的select/epoll_wait获取所监听句柄上的事件通知,并调用对应句柄所注册的函数进行处理。

  • EventHandler接口定义:

handle_read: 对句柄上的读事件进行操作

handle_write: 对句柄上的写事件进行操作

SimpleRpc中的核心框架实现


Reactor使用Epoll实现,Epoll的具体使用方式这里就不赘述了,因为它不是本文的主要写作目的。这里面讲述一下SimpleRpc网络事件处理的最核心的三个类:Acceptor,UpstreamHandler,DownstreamHandler。

  • Acceptor接受器:
Acceptor::Acceptor(const InetAddr &addr, Reactor *reactor){
_reactor = reactor;
int sock_fd = socket(AF_INET, SOCK_STREAM, );
sockaddr sock_addr = addr.addr();
int ret = bind(sock_fd, &sock_addr, sizeof(sockaddr));
if(ret != ){
LOG("bind error");
exit();
}
ret = listen(sock_fd, );
if(ret != ) {
LOG("listen error");
exit();
}
_reactor->regist(sock_fd, this);
}

首先,服务端使用socket函数创建一个socket_fd,绑定好IP和端口后,acceptor把这个socket_fd加入到reactor监听列表中,当有客户端主动发起与该服务的连接时,服务端该socket_fd上会有读事件产生,Acceptor的handle_read函数将会被调用。

void Acceptor::handle_read(int sock_fd) {
struct sockaddr addr;
socklen_t size = sizeof(struct sockaddr_in);
int fd = accept(sock_fd, &addr, &size);
_reactor->regist(fd, new UpstreamHandler(fd, _reactor)); //UpstreamHandler用来处理客户端请求。
}

handle_read函数接收到这个socket_fd后,知道这是客户端的请求连接,于是调用accept函数获取这个新连接的socket句柄fd。站在服务端角度看,客户端就是它的上游,对于上游事件的处理需要用UpstreamHandler。Acceptor在reactor上绑定fd与UpstreamHandler,reactor等待后续的事件的到来。Acceptor就像一只老母鸡,不断的下蛋(蛋就是新生产出的fd),并把蛋放入到reactor等待进一步孵化。

  • UpstreamHandler:上游请求事件处理
void UpstreamHandler::handle_read(int fd) {
if(fd != _sock_fd){
return;
} StreamEvent e;
e.fd = _sock_fd;
e.type = 0; //客户端请求事件
_reactor->remove(fd); //移除fd
//由每个工作线程自己去读fd,客户端请求事件
ThreadPool<UpstreamEvent>::get_instance()->put_event(e); //放入队列
//自杀
delete this;
}

客户端请求服务端建立连接后,第一步就是要向服务端发送请求数据。客户端发送数据后,服务端reactor发现socket句柄上有读事件,就调用对应句柄上的事件处理函数(UpstreamHandler::handle_read())。该事件处理函数并不直接进行数据的读取和计算,而是先从reactor中移除该fd(防止后续数据到来,reactor重复获取读事件),之后把fd封装成一个事件结构体放入阻塞队列中,由共享该阻塞队列的线程池进行后续处理。

  • DownstreamHandler:下游事件处理
void DownstreamHandler::handle_read(int fd) {
char head[];
if(fd != _sock_fd){
return;
} _reactor->remove(fd);
Connection conn(fd);
conn.recv_n(head, );
int size = *((int *)head);
char *buf = new char[size];
conn.recv_n(buf, size);
close(fd);
printf("Downstream Handler close fd:%d\n", fd);
//下游响应
_response->deserialize(buf, size);
if(_result_handler != NULL) {
_result_handler->data_comeback();
} delete[] buf;
//自杀
delete this;
}

站在客户端的角度来看,服务端就是其下游,客户端对服务端的响应数据的处理需要使用DownstreamHandler。当服务端返回响应数据时,客户端的reactor会检测到对应socket句柄上的读事件,随后调用对应事件处理函数(handle_read)。该函数首先从reactor移除对该fd的监听,防止reactor重复检测事件并调用处理函数;之后就接收数据、反序列化得到响应结构体。

SimpleRpc-网络事件响应Reactor设计模式的更多相关文章

  1. 浅析Reactor设计模式

    简介:Reactor 设计模式是一种事件驱动的设计模式,将一个或者多个客户端请求分发到不同的处理器上,来提升事件处理的效率.主要的应用场景就是java NIO当中用户处理网络请求.使用的是异步非阻塞I ...

  2. 利用epoll写一个"迷你"的网络事件库

    epoll是linux下高性能的IO复用技术,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率.另一点原因就是获取 ...

  3. 追踪app崩溃率、事件响应链、Run Loop、线程和进程、数据表的优化、动画库、Restful架构、SDWebImage的原理

    1.如何追踪app崩溃率,如何解决线上闪退 当 iOS设备上的App应用闪退时,操作系统会生成一个crash日志,保存在设备上.crash日志上有很多有用的信息,比如每个正在执行线程的完整堆栈 跟踪信 ...

  4. iOS中的事件响应链、单例模式、工厂模式、观察者模式

    学习内容 欢迎关注我的iOS学习总结--每天学一点iOS:https://github.com/practiceqian/one-day-one-iOS-summary iOS中事件传递和相应机制 i ...

  5. iOS事件响应链

    首先,当发生事件响应时,必须知道由谁来响应事件.在IOS中,由响应者链来对事件进行响应,所有事件响应的类都是UIResponder的子类,响应者链是一个由不同对象组成的层次结构,其中的每个对象将依次获 ...

  6. DuiLib事件分析(一)——鼠标事件响应

    最近在处理DuiLib中自定义列表行元素事件,因为处理方案得不到较好的效果,于是只好一层一层的去剥离DuiLib事件是怎么来的,看能否在某一层截取消息,自己重写. 我这里使用CListContaine ...

  7. mvc ajax dropdownlist onchang事件响应

    <script type="text/javascript"> $("#Cycle").on("change", functio ...

  8. Legolas工业自动化平台入门(三)交互事件响应动作

    在上一篇Legolas工业自动化平台入门(二)数据响应动作 一文中,我们介绍了"动作"相关内容,了解到"动作"分为多种,各种动作的添加方式相同,但是应用方式各自 ...

  9. JS代码的位置与事件响应代码块的封装问题

    JS代码的位置       我们可以将JavaScript代码放在html文件中任何位置,但是我们一般放在网页的head或者body部分.   放在<head>部分最常用的方式是在页面中h ...

随机推荐

  1. OpenCms模块创建图解

    登录OpenCms后,切换到"管理(Administration)"视图,点击"模块管理",这时窗口显示已安装模块的列表. 确定当前不在"online ...

  2. sizeof(void)有什么用

    偶然发现在C中sizeof(void)是合法的,于是,对它的作用产生了疑问.查阅资料在GNU文档中发现如下解释: In GNU C, addition and subtraction operatio ...

  3. Linux的netstat查看端口是否开放见解(0.0.0.0与127.0.0.1的区别)

    linux运维都需要对端口开放查看  netstat 就是对端口信息的查看 # netstat -nltp p 查看端口挂的程序 [root@iz2ze5is23zeo1ipvn65aiz ~]# n ...

  4. Java异常的性能分析

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt276 在Java中抛异常的性能是非常差的.通常来说,抛一个异常大概会消耗10 ...

  5. Java数据库连接池比较(c3p0,dbcp,proxool和BoneCP)

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcytp21 Java框架数据库连接池比较(c3p0,dbcp和proxool,Bo ...

  6. C# 导出Excel的示例(转)

    using System; using System.Collections.Generic; using System.Text; using System.Data; using System.W ...

  7. Eslint配置

    //ESLint 4.5.0,参考了eslint官方以及alloyteam团队配置 module.exports = { parser: 'babel-eslint', parserOptions: ...

  8. 交换机的Ethernet Channel

    端口聚合也叫做以太通道(ethernet channel),主要用于交换机之间连接.由于两个交换机之间有多条冗余链路的时候,STP会将其中的几条链路关闭,只保留一条,这样可以避免二层的环 路产生.但是 ...

  9. 个人作业2——必应词典APP分析

    第一部分:调研.评测 1.刚刚打开必应词典的时候,它给我的第一反应就是界面美观,最上面是一个查询框,下面有一些经典的句子.单词以及一些精选的文章,所有的功能都可以一目了然,看一眼就知道要怎么去使用,这 ...

  10. java中synchronized的使用

    synchronized是Java中的关键字,是一种同步锁. synchronized分对象锁和类的锁两种. (一)通常synchronized 方法和synchronized(this){}都是属于 ...