Introduction
本文分析libeasy服务端框架实现原理,主要分析libeasy是如何处理以下事件的
- libeasy服务端连接建立
- libeasy服务端连接关闭
- libeasy服务端可读事件
- libeasy服务端可写事件
libeasy服务端框架实现原理
本节先讨论libeasy的网络和线程模型,接着描述libeasy是如何组织连接、消息、请求等之间的关系,即libeasy资源管理,最后分析其如何处理服务端连接建立、服务端连接关闭、服务端可读事件以及服务端可写事件的。
libeasy网络和线程模型
libeasy网络模型基于libev的reactor模型,具体分析请参看文章(libev设计与实现),本节主要描述其线程模型。
libeasy支持两种常见的线程模型,一是IO线程和工作线程共用相同线程,二是IO线程和工作线程分开。
I/O线程和工作线程共用
如上图,I/O线程和工作线程共用的线程模型中,实际上是没有专门的工作线程的,I/O线程不仅需要负责处理I/O,还需要真正地处理请求,计算结果。一般典型的处理流程为
- Process Read I/O: 处理读I/O
- Process: 解析请求,计算结果
- Process Write I/O: 处理写I/O,把计算结果返回给客户端
这种线程模型的特点是
- 处理流程相对简单,解析好请求后就能直接在同一线程处理,省去了线程切换的开销,非常适合Process耗费时间较小的请求
- 由于Process过程需要耗费时间,对于大任务,可能时间较长,会影响其他请求的处理
I/O线程和工作线程独立
如上图,在I/O线程和工作线程独立的线程模型中,有专门的工作线程来处理请求,计算结果,I/O线程仅仅需要做读写数据相关的操作。在这种线程模型下,整个流程为
- Process Read I/O:处理读数据,然后解析请求,生成任务,推送到工作线程的队列中,然后以异步事件方式通知工作线程处理
- Process: 工作线程接收到异步事件后,从其工作队列中拿出任务,依次处理,处理完成后,生成结果,放到I/O线程的队列中,然后以异步事件方式通知I/O线程处理
- Process Write I/O:I/O线程收到通知后,依次处理写数据请求
这种线程模型的特点是
- I/O和计算分开处理,会引入线程切换开销,比较适合Process耗费时间长的任务请求
- 对于小任务请求不适合,大量时间耗费在线程切换开销
libeasy资源管理
libeasy的资源管理方式如上图所示,主要包括
- easy_baseth_t: libeasy中入口结构体,主要包括libev的网络框架和easy_io_t。
- easy_io_t: libeasy io入口结构体,包含监听相关的结构体,io thread pool 以及 request thread pool
- easy_thread_pool_t: libeasy线程池结构体,通过zero byte数组来作为线程池数组
- easy_thread_t: libeasy中不存在这个结构体,是笔者添加的,libeasy中采用的是宏定义实现这个功能
- easy_io_thread_t: libeasy中负责io的线程结构体,其中包含了连接队列,请求队列等等
- easy_request_thread_t: libeasy负责任务处理的线程结构体,其中包含了任务队列等
- easy_connection_t: libeasy处理网络链接的结构体,其中包括文件描述符fd,连接地址,监听事件处理程序,以及连接对应的请求队列等等。
- easy_message_t: libeasy中消息管理结构体,其中一个消息可能包含几个请求,一个请求是按照用户自定义格式的完整的消息包。
- easy_request_t: libeasy中管理请求的结构体,按照用户自定义协议,一个完整的协议包看作一个请求。
在libeasy中,资源之间有比较多的联系,这也是libeasy组织资源之间关系的方式
- 整个easy_io_t中包含IO线程池和工作线程池
- 一个io线程中(
easy_io_thread_t
)会一般会处理多个连接(easy_connection_t
) - 一个连接中(
easy_connection_t
)一般会包括一个或多个message(easy_message_t
) - 一个message中(
easy_message_t
)一般会有一个或多个request(easy_request_t
) - 一个工作线程中(
easy_request_thread_t
)一般会处理一个或多个request(easy_request_t
),其中request也称作是任务
除了上述资源之外,libeasy还有个非常重要的结构体,如下
1 | struct easy_io_handler_pt { |
这个结构体在easy_listen_t
和easy_connection_t
中有使用,在libeasy框架监控到网络事件发生后,其会调用easy_io_handler_pt
中的用户自定义的方法来处理。通过用户自定义的处理函数,可以把libeasy扩展到各种各样的应用场景,例如,http服务端,mysql服务端等等。具体地,每个函数在何时被调用,会在下节详细地分析。
libeasy事件处理
本节分析libeasy如何处理网络中的常见的四个事件
- libeasy服务端连接建立
- libeasy服务端连接关闭
- libeasy服务端可读事件
- libeasy服务端可写事件
libeasy服务端连接建立
在说明libeasy如何处理服务端连接建立前,先说明libeasy建立listen端口的流程。
整个调用流程如下
1 | easy_connection_listen_addr |
easy_connection_on_accept
负责listen端口处理链接建立的函数,具体地逻辑如下
1 | static void easy_connection_on_accept(struct ev_loop *loop, ev_io *w, int revents) |
其处理流程如下
- 调用accept,建立新的网络连接,然后创建
easy_connection_t
结构体来处理此连接 - 设置此fd是非阻塞的,这个是多路复用的标配了
- 初始化此connection的读事件、写事件和tiemout事件的处理程序
- 调用用户自定义的
on_connect
函数 - 把listen的监听任务让出来给其他的线程
- 把connection加入到io thread的connected_list中
- 开始尝试处理一次READ事件,这个具体地到处理可读事件逻辑
libeasy服务端连接关闭
连接关闭的处理逻辑在easy_connection_destroy
中,如下
1 | void easy_connection_destroy(easy_connection_t *c) |
- 调用用户自定义的on_disconnect函数
- 取消注册读写等事件,清理所有的request,清理所有的message
- 读完所有的剩余数据,关闭fd
libeasy服务端可读事件
其中,read事件的注册是在easy_connection_on_accept
中完成的,在libeasy服务端连接建立中已经说明过,这里再描述以下它的调用流程。
1 | ev_io_init(&c->read_watcher, easy_connection_on_readable, fd, EV_READ); |
其中,初始化了读、写和超时事件的处理函数,但只注册了读事件。
读事件的具体逻辑是在easy_connection_on_readable
中处理的,其代码如下
1 | static void easy_connection_on_readable(struct ev_loop *loop, ev_io *w, int revents) |
处理逻辑主要包括两块
- 如果第一次读数据,需要创建
easy_message_t
,作为载体 - 读数据
- 调用
easy_connection_do_request
处理读到的数据
easy_connection_do_request
的处理逻辑如下
1 | static int easy_connection_do_request(easy_message_t *m) |
主要的逻辑如下
- 解析读入数据,decode成一个个的请求
- 调用用户自定义地batch_process函数
- 遍历每个请求,调用用户自定义的process函数
- 当处理了一批请求后,调用
easy_connection_write_socket
写结果 - 继续加入读监控事件
decode函数的一个简单的例子如下
1 | static inline void *easy_simple_decode(easy_message_t *m) |
主要的逻辑就是按照既定地协议来解析数据。
batch_process和process一般是作为两种不同的方式,用户一般只使用其中的一种。不管采用那种处理方式,都分为以下两种处理方式
- I/O线程和工作线程独立的方式
- I/O线程和工作线程共用的方式
I/O线程和工作线程独立的方式
这种处理方式,process函数一般会调用easy_thread_pool_push_message
,如下
1 | int easy_thread_pool_push_message(easy_thread_pool_t *tp, easy_message_t *m, uint64_t hv) |
可以看出,该函数的最主要的功能是把message推送到request thread,由它继续处理,接着看其处理的流程,先分析easy_async_send(rth->loop, &rth->thread_watcher)
是如何唤醒request thread的,以及唤醒的是request thread的哪个处理函数。
1 | easy_thread_pool_create(cnt, callback, NULL) |
可以看出,最终唤醒的是request thread的easy_request_on_wakeup
来处理。
1 | static void easy_request_on_wakeup(struct ev_loop *loop, ev_async *w, int revents) |
easy_request_wakeup
会调用easy_request_doreq
来最终处理请求,它的工作逻辑是
- 遍历所有请求,调用创建线程池时,用户提供的自定义的函数
- 处理完成后,调用
easy_request_wakeup_ioth
唤醒I/O线程来处理结果数据的发送
其中,用户自定义的函数一般也是处理输入数据,计算结果,然后产生输出的结果数据,但这个和easy_io_handler_pt
中的process不是一个函数,但处理流程是类似的,主要是对请求的分析和计算过程。
easy_request_wakeup_ioth
的处理逻辑为
1 | static void easy_request_wakeup_ioth(easy_io_t *eio, char *ioth_flag, easy_list_t *ioth_list) |
接着看I/O thread的thread_watcher唤醒的是I/O线程的哪个处理函数。
1 | 在easy_eio_create函数中 |
I/O thread的唤醒是由easy_connection_on_wakeup
来处理,如下
1 | easy_connection_send_response(&request_list); |
而easy_connection_send_response
的处理逻辑为
1 | // foreach write socket |
调用easy_connection_write_socket
来处理一个个的easy_connection_t
的输出数据,具体的逻辑为
1 | int easy_connection_write_socket(easy_connection_t *c) |
调用easy_socket_write
来写数据,如果一次性没写完,则会在easy_connection_write_again
中继续处理,如下
1 | if (easy_list_empty(&c->output) == 0) { |
主要的逻辑就是继续监听写事件,等待下次唤醒。
I/O线程和工作线程共用
调用用户自定义的process处理(process,一般包含对请求的计算,生成输出的数据。),然后调用easy_connection_write_socket
写数据。注意,这种处理方式是I/O线程和工作线程共用的方式,process是在I/O线程中处理的,这种线程模型逻辑简单,比较容易理解,编码也相对容易很多。
libeasy服务端可写事件
当fd可写时,libeasy会调用easy_connection_on_writable
来处理,而它又会调用easy_connection_write_socket
来处理。在上面一节已经分析过其处理逻辑了,这里不再讨论了。
PS:
本博客更新会在第一时间推送到微信公众号,欢迎大家关注。