参考:
I/O模型与IO多路复用
Reactor模式
Reactor模式(反应器模式)是一种处理一个或多个客户端并发交付服务请求的事件设计模式。当请求抵达后,服务处理程序使用I/O多路复用策略,然后同步地派发这些请求至相关的请求处理程序。
Reactor结构
类图如下
- Handle 文件描述符
Handle
在Linux中一般称为文件描述符,在Windows中称为句柄,两者含义一样。Handle是事件的发源地。比如网络socket
、磁盘文件等。发生在Handle上的事件可以有connection
、ready for read
、ready for write
等。
Handle
是操作系统的句柄,是对资源在操作系统上的一种抽象,它可以是打开的文件、一个Socket连接、Timer定时器等。由于Rector模式一般使用在网络编程中,因而这里一般指的是Socket Handle
,也就是一个网络连接(connection/channel)。这个channel
注册到同步事件分离器中,以监听Handle
中发生的事件,对ServerSocketChannel
可以是CONNECT
事件,对SocketChannel
可以是read
、write
、close
事件等。
- Synchronous Event Demultiplexer 同步(多路)事件分离器
同步事件分离器本质上是系统调用,比如Linux中的select
、poll
、epoll
等。比如select()
方法会一致阻塞直到文件描述符handle
上有事件发生时才会返回。
无限循环等待新请求的到来,一旦发现有新的事件到来就会通知初始事件分发器去调取特定的时间处理器。
- Event Handler 事件处理器
事件处理器,定义一些回调方法或称为钩子函数,当handle
文件描述符上有事件发生时,回调方法便会执行。供初始事件分发器回调使用。
- Concrete Event Handler 具体的事件处理器
具体的事件处理器,实现了Event Handler,在回调方法中实现具体的业务逻辑。
- Initiation Dispatcher 初始事件分发器
初始事件分发器,提供了注册、删除、转发Event Handler
的方法。当Synchronous Event Demultiplexer
检测到handler
上有事件发生时,便会通知initiation dispatcher
调用特定的event handler
的回调方法。
初始事件分发器用于管理Event Handler
,定义注册、移除EventHandler
等。它还作为Rector模式的入口调用Synchronous Event Demultiplexer
同步多路事件分离器的select
方法以阻塞等待事件返回,当阻塞等待返回时,根据事件发生的Handle
将其分发给对应的Event Handle
事件处理器进行处理,也就是回调EventHandler
中的handle_event
方法。
Reactor模式的实现方式
单线程Reactor模式
Reactor的单线程模式的单线程主要是针对于I/O操作而言,也就是所以的I/O的accept()、read()、write()以及connect()操作都在一个线程上完成的。
但在目前的单线程Reactor模式中,不仅I/O操作在该Reactor线程上,连非I/O的业务操作也在该线程上进行处理了,这可能会大大延迟I/O请求的响应。所以我们应该将非I/O的业务逻辑操作从Reactor线程上卸载,以此来加速Reactor线程对I/O请求的响应。
改进:使用工作者线程池
与单线程Reactor模式不同的是,添加了一个工作者线程池,并将非I/O操作从Reactor线程中移出转交给工作者线程池来执行。这样能够提高Reactor线程的I/O响应,不至于因为一些耗时的业务逻辑而延迟对后面I/O请求的处理。
I/O操作依旧由一个Reactor来完成,包括I/O的accept()、read()、write()以及connect()操作。
对于一些小容量应用场景,可以使用单线程模型。但是对于高负载、大并发或大数据量的应用场景却不合适,主要原因如下:
① 一个NIO线程同时处理成百上千的链路,性能上无法支撑,即便NIO线程的CPU负荷达到100%,也无法满足海量消息的读取和发送;
② 当NIO线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了NIO线程的负载,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈;
多Reactor线程模式
Reactor线程池中的每一Reactor线程都会有自己的Selector、线程和分发的事件循环逻辑。
mainReactor可以只有一个,但subReactor一般会有多个。mainReactor线程主要负责接收客户端的连接请求,然后将接收到的SocketChannel传递给subReactor,由subReactor来完成和客户端的通信。
所有的I/O操作(包括,I/O的accept()、read()、write()以及connect()操作)依旧还是在Reactor线程(mainReactor线程 或 subReactor线程)中完成的。Thread Pool(线程池)仅用来处理非I/O操作的逻辑。
多Reactor线程模式将“接受客户端的连接请求”和“与该客户端的通信”分在了两个Reactor线程来完成。mainReactor完成接收客户端连接请求的操作,它不负责与客户端的通信,而是将建立好的连接转交给subReactor线程来完成与客户端的通信,这样一来就不会因为read()数据量太大而导致后面的客户端连接请求得不到即时处理的情况。并且多Reactor线程模式在海量的客户端并发请求的情况下,还可以通过实现subReactor线程池来将海量的连接分发给多个subReactor线程,在多核的操作系统中这能大大提升应用的负载和吞吐量。
单线程Reactor模式的C实现
根据 Reactor结构 可以再简化为:
- 抽象的事件,直接被具体化为fd
- 事件多路分发器, 直接被具体化为epoll
- 一个Reactor用来管理整个流程
单线程Reactor模式
GitHub : SimpleReactorWithC
主要函数与结构体说明:
抽象事件结构体
/// 抽象事件
struct myevent_s{
int fd; // 这里是 连接 socket 必然要保存
int events; // 记录所关怀的事件类型,读事件还是写事件 必然要保存
void *arg; // 指向自己的指针 必然要保存
Callback_t call_back; // 回调函数 必然要保存
int status; // 记录自己是否在epoll树上 1 在epoll树上
int index;
char buf[BUFFLEN]; // 一个缓冲区
int len; // 缓冲区有效数据的长度
};
int g_efd; // epoll 实例
struct myevent_s g_events[MAX_EVENTS+1];
initlistensocket:
void initlistensocket(int epfd, unsigned short port)
创建服务监听套接字,构建 抽象事件 (accept 事件)设置其事件回调函数 (acceptConn),并将 抽象事件 添加到epoll的红黑树中进行监控。
eventSet
void eventSet(struct myevent_s *ev, int fd, int events, Callback_t call_back, void * arg, int index)
构建 抽象事件
eventAdd
void eventAdd(int efd, struct myevent_s* ev)
添加抽象事件 到epoll 进行监控
acceptConn
void acceptConn(int fd, int events, void *arg)
accept 触发 处理回调函数,客户端连接成功后创建连接套接字,注意设置为非阻塞
构建 抽象事件 (read事件)并添加到epoll
//设置套接字非阻塞
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd , F_SETFL , flags|O_NONBLOCK);
recvdata
void recvdata(int fd, int events, void *arg)
read 事件 触发 处理回调函数
接受从客户端传过来的数据并处理
构建 抽象事件 (send事件)并添加到epoll
senddata
void senddata(int fd, int events, void *arg)
send 事件 触发 处理回调函数
将处理后的数据 回发给 客户端
完成了一次数据收发,先 删除 epoll 树上的 事件,再构建 抽象事件 (read事件)并添加到epoll
eventDel
void eventDel(int efd, struct myevent_s* ev)
从 epoll 中删除 监听事件
服务端:
GitHub : SimpleReactorWithC
/// 完整代码 见 GitHub
int main()
{
unsigned short port = 5555;
/// 创建epoll文件描述符
/// EPOLL_CLOEXEC标志与open 时的O_CLOEXEC 标志类似,即进程被替换时会关闭文件描述符
g_efd = epoll_create1(EPOLL_CLOEXEC);
my_error(g_efd, "epoll_create error");
/// 创建监听套接字 并 注册 events
initlistensocket(g_efd, port);
struct epoll_event events[MAX_EVENTS+1];
printf("server running:port[%d]\n", port);
int checkpos = 0, i;
while(1)
{
/// 等待内核 I/O 事件的分发
int nfd = epoll_wait(g_efd, events, MAX_EVENTS + 1, 1000);
my_error(nfd, "epoll_wait error");
/// nfd 分发的事件的个数 这里遍历处理
for(i = 0; i < nfd; ++i)
{
/// 抽象的事件 处理 监听 与 连接 套接字
struct myevent_s *ev = (struct myevent_s *)events[i].data.ptr;
/// 回调函数处理事件 回调函数 在 eventSet 注册
if((events[i].events & EPOLLIN) && (ev->events & EPOLLIN))
ev->call_back(ev->fd, events[i].events, ev->arg);
if((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT))
ev->call_back(ev->fd, events[i].events, ev->arg);
}
}
return 0;
}
客户端:
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <stdlib.h>
#define MAXLINE 4096
#define SERV_PORT 5555
int main() {
int sockfd;
struct sockaddr_in servaddr;
// 创建了一个本地套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror( "create socket failed");
}
// 初始化目标服务器端的地址, TCP 编程中,使用的是服务器的 IP 地址和端口作为目标
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
// 发起对目标套接字的 connect 调用
if (connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) {
perror("connect failed");
}
char send_line[MAXLINE];
bzero(send_line, MAXLINE);
char recv_line[MAXLINE];
bzero(recv_line, MAXLINE);
// 从标准输入中读取字符串,向服务器端发送
while (1) {
bzero(recv_line,MAXLINE); // 注意每次清空
if (fgets(send_line, MAXLINE, stdin) == NULL)
break;
int nbytes = sizeof(send_line);
if (send(sockfd, send_line, nbytes,0) != nbytes)
perror("write error");
bzero(recv_line, MAXLINE); // 注意每次清空
if (recv(sockfd, recv_line, MAXLINE,0) == 0)
perror("server terminated prematurely");
fputs(recv_line, stdout);
}
exit(0);
}
单线程Reactor模式的C++实现
相关类说明
Handler
handler负责抽象事件。
- 设置fd
- 设置关注的事件类型
- 设置回调函数
等基本事件。
Reactor
reactor负责将handler注册到Synchronous Event Demultiplexer 同步(多路)事件分离器(这里是EpollPoller)上。
EpollPoller
对 epoll 的封装
UpperCharacterServer
创建监听线程,事件回调函数的实现
服务端:
详见: GitHub: SimpleReactorWithC++
UpperCharacterServer:
//
// Created by leacock on 2020/11/16.
//
#include "UpperCharacterServer.h"
UpperCharacterServer::UpperCharacterServer(int port)
: port_(port)
, base_(new Reactor) {
}
UpperCharacterServer::~UpperCharacterServer() {
}
void UpperCharacterServer::start() {
startup();
base_->loop();
}
/**
* 创建 服务端的监听套接字
* 创建 设置 accpet 事件 到 epoll
*/
void UpperCharacterServer::startup() {
/// 创建监听套接字
int lfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
my_error(lfd, "socket error");
/// 创建 accpet handler 实例
auto lHandler = std::make_shared<Handler>(lfd);
// auto lHandler = new Handler(lfd);
// if (handleList_[lfd]!= nullptr){
// delete(handleList_[lfd]);
// }
handleList_[lfd] = lHandler;
int reuse = 1;
setsockopt(lHandler->fd(), SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof reuse);
struct sockaddr_in addr{};
bzero(&addr, sizeof addr);
addr.sin_family = AF_INET;
addr.sin_port = htons(port_);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
int ret = bind(lHandler->fd(), (sockaddr*)&addr, sizeof addr);
my_error(ret, "bind error");
ret = listen(lHandler->fd(), 16);
my_error(ret, "listen error");
/// 设置回调函数 accpet 事件的回调 accpetConn
lHandler->setReadCallback(std::bind(&UpperCharacterServer::accpetConn, this, _1));
/// 使能 read 事件类型
lHandler->enableRead();
/// 注册 描述符到 epoll 中
base_->registerHandler(lHandler.get());
//base_->registerHandler(lHandler);
}
/**
* accept 事件 处理 回调函数
* @param fd
*/
void UpperCharacterServer::accpetConn(int fd) {
printf("in accept\n");
/// 这种方式也可以设置 为 SOCK_NONBLOCK 非阻塞
int cfd = accept4(fd, NULL, NULL, SOCK_NONBLOCK);
my_error(cfd, "accept4");
/// 创建 read handler 实例
auto connHandler = std::make_shared<Handler>(cfd);
// auto connHandler = new Handler(cfd);
// if (handleList_[cfd]!= nullptr){
// delete(handleList_[cfd]);
// }
handleList_[cfd] = connHandler;
/// 设置回调函数 read 事件的回调 readdata
connHandler->setReadCallback(std::bind(&UpperCharacterServer::readdata, this, _1));
/// 使能 read 事件类型
connHandler->enableRead();
/// 注册 描述符到 epoll 中
base_->registerHandler(connHandler.get());
//base_->registerHandler(connHandler);
}
/**
* read 事件 处理 回调函数
* @param fd
*/
void UpperCharacterServer::readdata(int fd) {
printf("in read\n");
auto pHandler = handleList_[fd];
bzero(pHandler->buff(),MAX_BUFFLEN);
int nbytes = read(fd, pHandler->buff(), MAX_BUFFLEN);
if(nbytes > 0)
{
/// TODO 这里len 可能 == MAX_BUFFLEN
int len = strlen(pHandler->buff());
for (int i = 0; i < len; ++i) {
pHandler->buff()[i] = convert_char(pHandler->buff()[i]);
}
printf("%s\n", pHandler->buff());
pHandler->setLen(len);
/// 设置回调函数 send 事件的回调 senddata
pHandler->setWriteCallback(std::bind(&UpperCharacterServer::senddata, this, _1));
/// 使能 write 事件类型
pHandler->enableWrite();
/// 注册 描述符到 epoll 中
base_->registerHandler(pHandler.get());
//base_->registerHandler(pHandler);
}
else if(nbytes == 0){
printf("close fd\n");
close(fd);
base_->removeHandler(pHandler.get());
//base_->removeHandler(pHandler);
handleList_.erase(fd);
}
else{
close(fd);
printf("read error\n");
base_->removeHandler(pHandler.get());
//base_->removeHandler(pHandler);
handleList_.erase(fd);
}
}
/**
* send 事件 处理 回调函数
* @param fd
*/
void UpperCharacterServer::senddata(int fd) {
auto pHandler = handleList_[fd];
int nbytes = write(fd, pHandler->buff(), pHandler->length());
if(nbytes >= 0)
{
pHandler->setReadCallback(std::bind(&UpperCharacterServer::readdata, this, _1));
pHandler->enableRead();
base_->registerHandler(pHandler.get());
//base_->registerHandler(pHandler);
}
else {
printf("write error\n");
close(fd);
base_->removeHandler(pHandler.get());
//base_->removeHandler(pHandler);
handleList_.erase(fd);
}
}
char UpperCharacterServer::convert_char(char c) {
if ( 'A' <= c && c <= 'Z')
return c + 32; // 转换小写
else if ( 'a' <= c && c <= 'z')
return c - 32; // 转换大写
else
return c; // 其他不变
}
客户端:
代码同上