经过前面一段时间的学习,已经初步完成了一个Server,但流程过于单一,也就是不够抽象,而大型服务器一般都采用更抽象的设计模式。
我们的服务器整个都是围绕epoll来编程的,服务器通过监听eooll上的事件,然后对不同的时间做不同的处理。这种以事件为核心的模式叫做事件驱动,据此引出两种服务器开发的经典模式————Reactor模式和Proactor模式。
经过一段时间的学习我们可以知道如果要服务器同时为多个客户端服务,最直接的方式就是为每一条连接创建线程\进程,处理完事件就可以销毁该线程,但不停创建和销毁会带来大量的性能开销,解决办法就是IO多路复用,该技术可以通过系统函数来监听我们关心的连接,在之前的博客中也提到过,selcet、poll、epoll都是Linux提供的现成的IO多路复用系统,区别就是select和poll都是基于轮询,同样会造成效率问题。
##Reactor
Reactor 模式就是对IO多路复用的封装,直译为反应堆,这里的反应是对事件反应。同时,Reactor 模式也叫 Dispatcher 模式,即 I/O 多路复用监听事件,收到事件后,根据事件类型分配(Dispatch)给某个进程 / 线程。
该模式主要有Reactor和处理资源池组成。
- Reactor负责监听和分发事件。
- 处理资源池负责处理事件。
该模式有多种组合方式,这里只介绍本项目目前会采用的单Reactor单进程模式。
- Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型;
- 如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件;
- 如果不是连接建立事件, 则交由当前连接对应的 Handler 对象来进行响应;
- Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。
缺点就是只有一个进程,无法利用多核CPU的优势,同时如果Handler对象处理业务耗时过长,会导致响应的延迟。
##Proactor
Reactor时非阻塞同步网络模式,而Proactor是异步网络模式。
在这里总结一下阻塞、非阻塞、同步、异步I/O
###阻塞I/O
当用户程序执行 read ,线程会被阻塞,一直等到内核数据准备好,并把数据从内核缓冲区拷贝到应用程序的缓冲区中,当拷贝过程完成,read 才会返回。这里需要等待两个过程。
###非阻塞I/O
read 请求在数据未准备好的情况下立即返回,可以继续往下执行,此时应用程序不断轮询内核,直到数据准备好,内核将数据拷贝到应用程序缓冲区,read 调用才可以获取到结果。
不管是阻塞还是非阻塞,都是同步调用,也就是一次只能做一个动作,比如read调用后需要等待系统把数据从内核区拷贝到用户区,如果拷贝效率不高,则需要等待较长时间。
而异步I/O是内核把数据准备好,数据从内核态拷贝到用户态这两个过程都不用等待。
当我们发起 aio_read (异步 I/O) 之后,就立即返回,内核自动将数据从内核空间拷贝到用户空间,这个拷贝过程同样是异步的,内核自动完成的,和前面的同步操作不一样,应用程序并不需要主动发起拷贝动作。
很明显,异步 I/O 比同步 I/O 性能更好,因为异步 I/O 在内核数据准备好和数据从内核空间拷贝到用户空间这两个过程都不用等待。
- Reactor 是非阻塞同步网络模式,感知的是就绪可读写事件。在每次感知到有事件发生(比如可读就绪事件)后,就需要应用进程主动调用 read 方法来完成数据的读取,也就是要应用进程主动将 socket 接收缓存中的数据读到应用进程内存中,这个过程是同步的,读取完数据后应用进程才能处理数据。
- Proactor 是异步网络模式, 感知的是已完成的读写事件。在发起异步读写请求时,需要传入数据缓冲区的地址(用来存放结果数据)等信息,这样系统内核才可以自动帮我们把数据的读写工作完成,这里的读写工作全程由操作系统来做,并不需要像 Reactor 那样还需要应用进程主动发起 read/write 来读写数据,操作系统完成读写工作后,就会通知应用进程直接处理数据。
因此,Reactor 可以理解为来了事件操作系统通知应用进程,让应用进程来处理,而 Proactor 可以理解为来了事件操作系统来处理,处理完再通知应用进程。这里的事件就是有新连接、有数据可读、有数据可写的这些 I/O 事件这里的处理包含从驱动读取到内核以及从内核读取到用户空间。
理论知识介绍完,接下来是实战部分。
照着敲还是比较快的,毕竟自己也才入门网络编程,要说靠自己想出来点什么有点不太可能,还是先理解为主。写完这部分代码后最大的感受的就是有些地方别硬用智能指针,不然自动析构了服务器就噶了,一个bug改了一天多才找出来问题,解决就是把智能指针换成普通指针,为了找bug甚至跑去看function的源码,发现是调用某函数的本体被析构了,导致空指针异常,一怒之下挨个把用到智能指针的地方换成普通指针,暴力查找,改完之后也是十分惆怅,慎用智能指针。。。
介绍一下主要的函数
int main() {
pEventLoop loop(new EventLoop());
pServer server(new Server(loop));
loop->loop();
return 0;
}
loop创建一个事件循环类,用来获取事件,然后通过回调函数进行分发。
Server::Server(pEventLoop _loop) : loop(_loop) {
pSocket servSock(new Socket());
pInetAddress servAddr(new InetAddress("127.0.0.1", 8888));
servSock->bind(servAddr.get());
servSock->listen();
servSock->setNonBlocking();
Channel* servChannel = new Channel(loop.get(), servSock->getFd());
std::function<void()> cb = std::bind(&Server::newConnection, this, servSock);
servChannel->setCallBack(cb);
servChannel->enableReading();
}
Server类则负责Socket的创建及监听,之前文章提到的Channel类在这次项目中有两次使用,一个是负责服务器fd,回调函数绑定的是创建新连接,
一个负责客户端fd,回调函数绑定读写事件。
void EventLoop::loop() const {
while (!quit) {
std::vector<pChannel> chs;
chs = ep->poll();
for(auto it : chs) {
it->handleEvent();
}
}
}
最后则是循环等待事件,事件发生就调用回调函数进行处理,回调函数采用function实现,可以事先进行绑定,在运行时根据Channel的不同调用具体函数,有点多态那味儿了。