说到后台开发,网络编程大约是躲不开的了。说到网络编程,I/O模型大约是躲不开的了。那且先来说说I/O模型好了。
先从 primitive 回顾一下,server accept/accept4一个新的connection来。此时accept会block住当前线程,直到一个client连接过来(其实准确的说,是从连接队列中取出)。注意,通常,我们用TCP连接的话(因为你在socket调用中使用了 SOCK_STREAM参数),那么这里是三次握手完成,连接已建立后才返回的。这时系统会找一个高位的随机端口作为这次连接的临时服务端端口,而 accept 则返回了这个临时端口的 file descriptor。接下来你可以通过read族调用,获取到请求的内容,即应用层报文。拿到报文,你就可以做对应的处理了。
好了,建立连接的课补完了,让我们看看这里说的有什么问题。client 是随机连接进来的,accept只能阻塞等待,这没有问题。但如果没有连接的话,其实是可以告诉系统,把“我”放进等待事件的进程队列里,放弃当前时间片的。这样显然可以提高整个系统的利用率。这时候 epoll 就横空出世了。这里暂不讲 select/poll,咱们一步到位先说 epoll,那两个得另开一篇了。
通过epoll_ctl 将一系列 fd 加入监听集合中,然后 epoll_wait 阻塞当前线程,放弃时间片,开始监听事件。其函数签名如 int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout)。将最后一个timeout参数设为-1则会使该调用无限期的阻塞,直到指定的监听事件发生。比如你现在监听集合中只有服务端 fd,当该 fd 可读时,即 accept 调用会立即返回获得建立好连接的 fd,epoll_wait才返回。随后调用 accept,并将该 client fd 也放进监听集合中,这样当 client fd 可读时,epoll_wait 会返回通知你可读,随后可调用 read 族函数。
这便是我们说的 Reactor。
那么 Proactor 是什么样的呢?上面例子中,得到 client fd 后,无需等待可读事件后 read 才获取到读取的内容,而是当调用返回时,你的读取buffer已经获取到了对应的内容。
这就好比,Reactor只是一个反应装置(看它的名字),一有消息来我就通知,后续要干啥你自己干去。而Proactor是一个有眼力见的门房,有消息来了,得,这消息该读我帮你读了,该写我帮你写了,完成了再告诉你。Reactor的翻译“反应器”似还像那么回事,那Proactor的翻译“前摄器”我只能说,这都什么玩意了。
select/epoll等就是典型的Reactor,一如我上面的举例。而Proactor的典型代表,则是Windows下的IOCP和 boost.asio库。
最后 “show me the f**king code”。在大名鼎鼎的 web server nginx 代码中,就有这两种模式的应用。在 nginx 的 event 模块中,作者为了尽可能的跨平台,将各OS的 multiplex接口封装了一个个module,上文中的 Reactor课代表 epoll的实现就在 ngx_epoll_module.c 文件中,而Proactor课代表 iocp的实现则见 ngx_iocp_module.c 文件。
在ngx_iocp_init中,通过 CreateIoCompletionPort 创建了一个 iocp 句柄,以后关于 iocp 的操作都将以此句柄作为资源的描述符。在 ngx_iocp_add_event 中,通过同样的接口 CreateIoCompletionPort ,但用不同的参数,将要监听的句柄放到 iocp 中,最后 ngx_iocp_process_events 通过 GetQueuedCompletionStatus 把读出来的数据取出,并交还给业务层处理。一个 iocp 的处理流程大致就是这样了。
贴一点点代码。注意到当创建 iocp 时,第二个参数是 NULL ,表明还并没有分配 iocp 资源句柄。而在后续的 add 操作中,则使用了该句柄作为第二个参数。
static ngx_int_tngx_iocp_init(ngx_cycle_t *cycle, ngx_msec_t timer)
{
...
if (iocp == NULL) {
iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, cf->threads);
}
if (iocp == NULL) {
...
}
static ngx_int_t
ngx_iocp_add_event(ngx_event_t *ev, ngx_int_t event, ngx_uint_t key)
{
...
if (CreateIoCompletionPort((HANDLE) c->fd, iocp, key, 0) == NULL) {
ngx_log_error(NGX_LOG_ALERT, c->log, ngx_errno,
"CreateIoCompletionPort() failed");
return NGX_ERROR;
}
...
}
好了,差不多就这些了。至于 ngx_epoll_module.c ,建议大家自己去看看,不再赘述。
后记
其实这个题目早就定下了,当然定这个标题有一段小故事。本来打算技术少写,主要提提这个故事,不过时间过久了,似乎也没想说太多了。
大致就是,之前有个项目找到我,但因为我不在项目当地,没法一个人大包大揽地做了。其实活还是容易的,只是经常不能当面交流很麻烦。所以想到当地有个好朋友,可以让他帮我做需要当面交流的那部分。结果在我和朋友前期磋商的过程中,便出现了 Reactor 的情况,迟迟没能给我一个计划表,整个磨合过程中就已经完全风险不可控了,只好放弃项目了。
这便是初衷了。其实在工作学习中,无论是向上还是向下,主要都是要把控好一件事的风险。别人交给你一项任务,且不说到底能不能完成,重要的是及时暴露风险,如果预计10天,千万不要做到第9天了才告诉他,对不起才完成10%,我需要更多人力/时间/技术协助。到了那个时候,即便项目组真的有资源可调配,但也很难力挽狂澜。
这篇文章也是拖了很久了,导致都掉了一个粉了,真令人捉鸡。9月底就定下的题目,十月熬了一个复杂的需求,今天咬咬牙还是写完了。那个取消关注了的同学,你快回来吧。
对了,题图是梵净山,很棒,推荐!