关于 Reactor
Reactor 是目前业界对于海量 TCP 处理的最佳解决方案,该模型最早在 Netty 中被实现。
Netty是Java的一个高性能网络库,看了一下架构设计,主要是两个模块:
1.网络套接字处理。对于IO多路复用以及线程池的抽象,采用reactor模型实现高性能。
2.对应用层协议的支持。粗略看了一眼HTTP/redis/sock5代理/smtp/dns/protobuf。感兴趣的同学可以自行查看4.1最新的JavaDoc。Netty API Reference
那么对于 C++ 而言,有没有类似的网络库呢?答案肯定是有的,所以在写这个模型的时候,本人也试图写出一些新意。
新在哪里?
- TCP的协议族共三个:ipv4、ipv6、unix。unix域协议族底层跟fifo/pipe走的是一套代码,从socketpair这个系统调用大概就能窥见一般,有的系统上甚至将sock文件直接表示为fifo文件。使用reactor处理并没有实际意义。但对于ipv6的支持却是一个小的创新,在cppev中提供了这样的支持。
- 封装。在设计TCP Server/Client时,尽量提供给用户更舒服的使用方式,更加一致的内存管理,数据几乎都存放于堆区。
- 性能。这个见仁见智,虽然并没有看过其他库的代码,但自问性能是优化到极致的,如果哪位能发现疏漏,也欢迎指正。
架构设计
线程与IO的处理
对于 Reactor 模型的实现架构如下:
请注意 IO 均采用非阻塞模式;对于Server/Client来说,唯一的不同就是已连接套接字的获取方式。
详细讲解:
- Server使用Acceptor线程,内含eventloop(即IO多路复用)对监听套接字注册可读事件监听;Client使用Connector线程,内含eventloop对管道文件描述符注册可读事件监听(这里的管道是一个self-pipe技巧,在对这个并发客户端投入task时会在管道另一端写入以触发读事件,使得线程被唤醒,作用与条件变量类似)。
- Server/Client分别通过accept/connect系统调用生成已连接套接字,以某个负载均衡算法从线程池中选出一个IO线程,并立即对其eventloop注册可写事件监听。
注意这里的立即,也就是说连接建立时的回调函数是由IO线程去执行的,而不是由Acceptor/Connector线程去执行,这样可以保证它们自身负载是最低的,从而最大化响应速度。 - 对Server来说,可写事件会被立即触发;对于Client来说,可写事件会等到连接建立时(即三次握手的第二次完成)触发,需要使用getsockopt去判断连接是否建立成功。
- IO线程执行完回调函数后,会执行彻底的清理操作,然后重新对自身的eventloop注册回调函数,并enable可读事件监听,此处开始该连接的处理被IO线程完全接管。
接口设计
Server:
on_accept / on_read_complete / on_write_complete / on_closed
Client:
on_connet / on_read_complete / on_write_complete / on_closed
- 这里将连接建立分别作为 on_accept 和 on_connect,注意这两个回调函数在上述的Step2中被注册到IO线程并在Step3中被执行。
- 套接字缓冲区的异步读写由lib自行完成,将读写完成抽象为了两个函数on_read_complete和on_write_complete,最后加上了on_closed。由于在lib的IO模块设计中是考虑到了连接被对端关闭或对端崩溃的情况,on_closed这个函数也可以由用户自行通过非阻塞IO类的对象指针判断并处理,加上这个回调函数也是帮用户减轻一些负担。
案例/性能/稳定性
服务端:
- 指定线程池线程数量。
- 注册回调函数。
- 使用 IPv4 / IPv6 / Unix 协议族分别监听。
- 启动 IO 线程池。
/*
* Simple Server
*
* The simple server just send a message to the client, then echo any message back to the client.
*/
#include "config.h"
#include "cppev/cppev.h"
/*
* Define handler
*
* On the socket accepted : Put the message to the write buffer, then trying to send it.
* The async_write here means if the message is very long then would register writable
* event to do asynchrous write.
*
* On the socket read from sys-buffer completed : Retrieve the message from read buffer, then
* put to the write buffer and trying to send it.
*
* On the socket write to sys-buffer completed : Log the info.
*
* On the socket closed : Log the info.
*/
cppev::reactor::tcp_event_handler on_accept = [](const std::shared_ptr<cppev::nsocktcp> &iopt) -> void
{
iopt->wbuf()->put_string("Cppev is a C++ event driven library");
cppev::reactor::async_write(iopt);
cppev::log::info << "Connection " << iopt->fd()