上一节讲过用epoll管理fd,但是这个管理方法很粗糙,都不知道是哪个客户端的,只是遍历epoll,然后有数据的打印,没有数据就等待,这样的程序是不能用于实际的工程项目的,假如客户端A要向客户端C发送数据,中间经过服务器,服务器怎么快速的找到客户端B的fd,并进行发送,那么多个客户端,怎么管理,需要查找的时候能快速找到,所以这一节我们介绍一下服务器reactor模型。
3.1 模型介绍
对搞并发编程,网络连接上的消息处理,分为两个阶段:等待消息准备好,消息处理
当使用默认的阻塞套接字(1个线程绑定1个连接),往往把这两个阶段和二为一了,这个对套接字的代码所在的线程就要睡眠来等待消息准备好,导致了高并发下线程频繁的睡眠。唤醒、从而影响CPU使用效率。
高并发编程方法当然是把两个阶段分开处理,等待消息准备好的代码段,与处理消息的代码段是分离的。
那等待消息这个阶段是怎么实现的? 线程主动查询或者让1个线程为所有连接而等待。这就是IO多路复用。多路复用就是处理等待消息这件事的,虽然它也会睡眠,但是因为只有一个线程不要紧,并且他实现了一对多,可以监控很多个连接,如果有连接准备好了,就可以唤醒,然后就交给处理消息阶段处理了。
高性能服务器需要考虑三类事件:IO事件,定时事件及信号。
3.2 reactor模型介绍
普通函数的机制,程序调用某函数,函数执行,然后等待,等到接收到了函数之后,把结果返回给程序,
而我们的reactor模型:应用程序不是主动的调用某个API完成处理,而是恰恰相反,应用程序需要提供相应的接口并注册到Reactor上,如果相应的事件发生,Reator讲主动调用应用程序注册的接口。
Reactor模型是处理并发IO比较常见的一种模式,用于同步IO,中心思想是将所有要处理IO事件注册到一个中心IO多路复用器上(这里用epoll),一旦有IO事件到来或是准备就绪,多路复用器返回并将事先注册的相应IO事件分发到对应的处理器中。
Reactor模型有三个重要组件:
- 多路复用器:由操作系统提供,在linux上一般是select、poll、epoll
- 事件分发器:将多路复用器中断返回就绪事件分到对应的处理函数中
- 事件处理器:负责处理特定事件的处理函数
3.2.1 程序思路
要记得上面说的三个重要组件,在分析程序的时候,我会重点提出的:
数据结构:
//数据结构是程序的灵魂
//反应堆的数据结构
struct nty_reactor
{
int epfd; //epoll的fd
struct nty_event *event; //对socket的封装
};
//对 一个socket的封装
struct nty_event
{
int fd; //每个连接的fd
int events; //epoll中的event事件
int status; //状态,可读可写
void *arg; //回调函数的参数
int (*callback)(int fd, int events, void *arg); //回调函数
long last_active; //上一次活跃的时间
char buffer[1024]; //接受和写的buff
int len; //数据长度
};
初始化:这个就简单了,申请reactor的内存和申请nty_event的内存
多路复用器:
//这里多路复用器用了epoll。所以多路复用器支持添加和删除
int nty_event_add(struct nty_event *event, int epfd, int events)
{
struct epoll_event ev;
int op = 0;
memset(&ev, 0, sizeof(struct epoll_event));
ev.data.fd = event->fd;
ev.data.ptr = event;
ev.events = event->events = events;
if(event->status == 0) {
op = EPOLL_CTL_ADD;
event->status = 1;
} else if(event->status == 1) {
op = EPOLL_CTL_MOD;
}
epoll_ctl(epfd, op, event->fd, &ev);
return 0;
}
int nty_event_del(struct nty_event *event, int epfd)
{
if(event->status == 0) {
return -1;
}
event->status = 0;
epoll_ctl(epfd, EPOLL_CTL_DEL, event->fd, NULL);
return 0;
}
事件分发器:(是不是就是反应堆)
//我们需要把上面的事件都添加到反应堆中,由反应堆去检测有没有事件发生,如果有时间发生就发到时间处理器中
int nty_reactor_addlistener(struct nty_reactor *reactor, int fd, NCALLBACK listener)
{
if(reactor == NULL) return -1;
if(reactor->event == NULL) return -1;
nty_event_set(&reactor->event[fd], fd, listener, reactor);
nty_event_add(&reactor->event[fd], reactor->epfd, EPOLLIN);
return 0;
}
//回调函数就是时间处理函数,反应堆管理fd是利用了数组的特性
//然后接着开启一个线程,等待epoll_wait有数据返回,如果有数据返回,获取到对应的回调函数,进行调用(实践处理器)
就在调用函数中处理了数据,具体实例可以在下节课说。
总结:
3.3 proactor模型介绍
proactor模型并不是我们的重点,这是windows系统常用的模型,但是linux系统还是常用reactor模型。这里就加入一个思维导图就可以了
3.4 用reactor模型模拟proactor模型
Boot。asio库采用的就是Proactor模型,在linux平台中采用epoll实现的reactor来模拟Proactor.
模拟的核心思想,就是处理客户端请求的时候,用另外一个线程去处理,这样做到了异步,主线程还是等待socket数据是否已经准备好,准备好就可以去读取,读取了数据之后,如果需要处理客户端请求,就发送到请求队列中,然后另外一个线程就会去处理,从而做到了异步。
3.5 总结
比较偷懒,就以两个图来总结把