SimpleRpc-网络事件响应Reactor设计模式
前言
这篇文章主要介绍整个框架用到的最核的一个设计模式:反应器模式。这个设计模式可以在《面向对象的软件架构》中详细了解,没有这本书的小伙伴不要急,我通过咱们的SimpleRpc来告诉大家这个设计模式是如何运用的。之所以它叫反应器模式,是因为它是处理事件的一种比较优美的框架。如何优美,我们慢慢道来。
如何设计一个高吞吐量的web服务?
web服务会面对大量的网络请求,服务要对这些请求进行处理,如何设计我们的web服务呢?有如下几种模型供参考。
- 单线程模型 系统中使用唯一的一个线程处理网络数据的读,对请求数据的处理,以及发送响应。当一个网络请求到来时,工作线程通过accept获取到活动socket,接下来该线程顺序的读取数据、处理数据、生成响应数据、写回socket。这个模型有一个明显的缺点:当服务处理一个请求时,其它请求将阻塞得不到响应。它难以胜任高并发大吞吐量的web服务。
- 多线程模型 每当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, 0); sockaddr sock_addr = addr.addr(); int ret = bind(sock_fd, &sock_addr, sizeof(sockaddr)); if(ret != 0){ LOG("bind error"); exit(0); } ret = listen(sock_fd, 1000); if(ret != 0) { LOG("listen error"); exit(0); } _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[4]; if(fd != _sock_fd){ return; } _reactor->remove(fd); Connection conn(fd); conn.recv_n(head, 4); 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重复检测事件并调用处理函数;之后就接收数据、反序列化得到响应结构体。