zeromq是一个最近比较火的消息中间件,我看了下源代码,感觉代码写的蛮精简的,故作为读书笔记。
1. zmq_init(1): 创建上下文ctx
1.1 ctx ctor:
进/线程间通信的方式: zeromq各个线程间使用消息来通信. mailbox作为进/线程间通信的管道, 其实在UNIX平台上就是一个双向的socketpair.
创建一个(socket最大数目 + io线程数目 + 3)的slots指针数组
a. 每个socket对象有自身的mailbox.
b. 每个io_thread对象也有自身的mailbox.
c. 另外的3个分别是zmq_term thread, reaper thread, log thread的mailbox.
创建io_threads指针vector保存io_thread的地址, 并且启动它.
创建empty_slots vector保存空的slots的index, 目前主要是所有socket对象的mailbox, 因为socket对象还没有建出来.
1.2 io_thread:
创建了一个poller, 其实就是经典的reactor模式. 根据不同平台创建该平台最好的poller, 目前基本上就是linux2.6内核的用epoll, mac-os用kqueue, 而windows暂时用select, (估计windows上性能会比较挫, ICE也有类似相应的poller实现, 不过称为selector, 在windows下的实现是用iocp模拟的reactor模式).
以下fd_t和handle_t是同一个类型, 表示文件的描述符
handle_t add_fd (fd_t fd_, struct i_poll_events *events_); // 添加fd以及相应的事件处理器,
// 其中包括in_event(for read), out_event(for write), timer_event(timetas).
void rm_fd (handle_t handle_); // 移除handle以及相应的事件处理器
void set_pollin (handle_t handle_); // 将handle的read事件激活
void reset_pollin (handle_t handle_); // 清除handle的read事件的激活状态
void set_pollout (handle_t handle_); // 将handle的write事件激活
void reset_pollout (handle_t handle_); // 清除handle的write事件的激活状态
void start (); // 启动
void stop (); // 停止
2. zmq_socket(2):
根据类型创建相应的zermq的socket, 比如req, rep, pair, push, pull...等等, 以后我们会详细解释每一种类型的socket.
在empty_slots中选择一个slot index, 注册所创建socket的mailbox地址到相应的slot中, 并且从empty_slots集合中移除之.
3. 作为client端 zmq_connect(2):
socket_base::connect(1):
根据提供的地址protocol:host:port来解析protocol, 我们先分析一下tcp的协议.
创建一个connect_session, 将它和一个io_thread绑定起来, 并且作为当前对象的child启动它.
object_t这边有一个父子关系的树, 基础类是object, 每一个object有其parent object, 底层的object有指向ctx和tid(所在的io_thread或者socket在slots中的index), 所有的object也保存着这两个东西.
而own_t则也是拥有类似的父子关系的树, 保存着拥有者(父亲)和被拥有者(儿子们)的指针, 而拥有者负责管理其下的被拥有者的生命周期, 负责创建和销毁被拥有者对象.
own_t::launch_child(1): 该方法负责plug object到它的io thread. 也就是发消息给与其绑定的io thread的mailbox中, 而io_thread将通知该object调用process_plugin(), 即connect_session::process_plugin()函数.
connect_session::process_plugin():
该函数调用创建一个zmq_connector_t对象也选择一个io_thread绑定起来, 并且作为当前对象的child启动它, 与创建connect_session中类似.
因此我们直接查看zmq_connector::process_plugin()函数
void zmq::zmq_connecter_t::process_plug ()
{
if (wait)
add_reconnect_timer();
else
start_connecting ();
}
void zmq::zmq_connecter_t::start_connecting ()
{
// Open the connecting socket.
int rc = tcp_connecter.open ();
// Connect may succeed in synchronous manner.
if (rc == 0) {
handle = add_fd (tcp_connecter.get_fd ());
handle_valid = true;
out_event ();
return;
}
// Connection establishment may be dealyed. Poll for its completion.
else if (rc == -1 && errno == EAGAIN) {
handle = add_fd (tcp_connecter.get_fd ());
handle_valid = true;
set_pollout (handle);
return;
}
// Handle any other error condition by eventual reconnect.
wait = true;
add_reconnect_timer();
}
如果连接成功, 则我们将其加入到io_thread的poller中去,因为zmq_connecter_t继承io_object,而后者能够获得相应的io_thread的poller, 并且io_object自身也是一个事件处理器。注意这边没有激活句柄。
tcp_connector是一个非阻塞connector的实现。大致就是先设置非阻塞的属性,然后调用connect,如果返回-1并且errno为E_INPROGRESS就认为已经发出了异步的连接,在该实现中我们吧这种情况的errno转义成EAGAIN,然后我们就再添加到poller并且激活句柄的写事件,当poller轮询到写事件的时候就表示连接成功,细节可以参考《UNIX网络
编程1》的实现。
zmq_connector除了调用tcp_connector完成上述所描述的非阻塞连接之外,还会利用reconnect timer完成重连的机制。
在连接成功以后我们会创建出一个zmq_init_t的对象,然后将其与一个所选的io_thread绑定, 然后作为兄弟启动它。该启动方式与作为儿子启动类似,也是先发plugin,再发own的command,然而发送own的dest对象不同而已。
void zmq::zmq_connecter_t::out_event ()
{
fd_t fd = tcp_connecter.connect ();
rm_fd (handle);
handle_valid = false;
// Handle the error condition by attempt to reconnect.
if (fd == retired_fd) {
tcp_connecter.close ();
wait = true;
add_reconnect_timer();
return;
}
// Choose I/O thread to run connecter in. Given that we are already
// running in an I/O thread, there must be at least one available.
io_thread_t *io_thread = choose_io_thread (options.affinity);
zmq_assert (io_thread);
// Create an init object.
zmq_init_t *init = new (std::nothrow) zmq_init_t (io_thread, NULL,
session, fd, options);
alloc_assert (init);
launch_sibling (init);
// Shut the connecter down.
terminate ();
}
zmq_init_t中主要委托zmq_engine_t对象来完成与socket交互的一些操作,其中实现了inout接口,绑定到zmq_engine_t对象上。
而zmq_engine_t有一个tcp_socket对象封装tcp socket的读写syscall,一个encoder,一个decoder,还有注册了poller的回调事件处理函数。
当可写事件发生的时候,即内核tcp缓冲区有空间可写的时候,就会调用encoder.get_data(2)去取数据,而后者基本上就调用绑定的inout接口的实现(zmq_init_t)的read(1)函数去获得相应的消息数据。
当可读事件发生的时候,即内核tcp缓冲区有数据可读的时候,就会调用decoder的相应函数去调用绑定的inout接口的实现(zmq_init_t)的write(1)函数去保存相应的消息数据。
后面的章节会详细说明这些实现。
void zmq::zmq_init_t::process_plug ()
{
zmq_assert (engine);
engine->plug (io_thread, this);
}
void zmq::zmq_engine_t::plug (io_thread_t *io_thread_, i_inout *inout_)
{
zmq_assert (!plugged);
plugged = true;
ephemeral_inout = NULL;
// Connect to session/init object.
zmq_assert (!inout);
zmq_assert (inout_);
encoder.set_inout (inout_);
decoder.set_inout (inout_);
inout = inout_;
// Connect to I/O threads poller object.
io_object_t::plug (io_thread_);
handle = add_fd (tcp_socket.get_fd ());
set_pollin (handle);
set_pollout (handle);
// Flush all the data that may have been already received downstream.
in_event ();
}
而看看zmq_init_t的read(1)和write(1)函数就知道客户端和服务端之间在交换identity,如果没有显式声明,则用生成一个uuid。
当完成交换后,则会执行zmq_init_t::finalise_initialisation()函数完成初始化的工作,用ephemeral_engine指针指向engine,并且engine指向NULL,unplug原来的engine。
void zmq::zmq_init_t::finalise_initialisation ()
{
// Unplug and prepare to dispatch engine.
if (sent && received) {
ephemeral_engine = engine;
engine = NULL;
ephemeral_engine->unplug ();
return;
}
}
于是会调用ephemeral_inout->flush()
void zmq::zmq_engine_t::in_event ()
{
...
// Flush all messages the decoder may have produced.
// If IO handler has unplugged engine, flush transient IO handler.
if (unlikely (!plugged)) {
zmq_assert (ephemeral_inout);
ephemeral_inout->flush ();
} else {
inout->flush ();
}
...
void zmq::zmq_engine_t::out_event ()
{
// If write buffer is empty, try to read new data from the encoder.
if (!outsize) {
outpos = NULL;
encoder.get_data (&outpos, &outsize);
// If IO handler has unplugged engine, flush transient IO handler.
if (unlikely (!plugged)) {
zmq_assert (ephemeral_inout);
ephemeral_inout->flush ();
return;
}
...
然后调用zmq_init_t::dispatch_engine()根据session的不同类型(transient, durable)创建不同的session,绑定到一个io_thread,并且作为兄弟启动它,还会发送attach的命令给session,让它去attach相应的管道以及zmq_engine对象,相应的代码在session_t::process_attach(2)函数中。
void zmq::zmq_init_t::flush ()
{
// Check if there's anything to flush.
if (!received)
return;
// Initialization is done, dispatch engine.
if (ephemeral_engine)
dispatch_engine ();
}
同理,当session_t对象plugin了zmq_engine对象之后,自身也是一个inout接口的实现,所以plugin的时候注册了给zmq_engine对象。这样当poller发生可读可写事件的时候会回调zmq_engine的out_event()和in_event()函数。后者的encoder和decoder也就相应调用了session_t的read()和write()函数。
因此我们看一下这两个函数:
bool zmq::session_t::read (::zmq_msg_t *msg_)
{
if (!in_pipe)
return false;
if (!in_pipe->read (msg_))
return false;
incomplete_in = msg_->flags & ZMQ_MSG_MORE;
return true;
}
bool zmq::session_t::write (::zmq_msg_t *msg_)
{
if (out_pipe && out_pipe->write (msg_)) {
zmq_msg_init (msg_);
return true;
}
return false;
}
这也说明了zeromq的数据收发是异步的,zmq::zmq_send()和zmq::zmq_recv()并不是立即调用内核相应的socket syscall写到内核缓冲区的,而是直接写到所谓的管道pipe中去。
4. 我们接下去再看看如果作为服务端,即调用zmq::socket_base_t::bind(1)的时候会怎么样工作呢?
发觉会创建一个与客户端zmq_connector对应的zmq_listener_t。而zmq_listener_t就将利用poller轮询等待连接事件
void zmq::zmq_listener_t::process_plug ()
{
// Start polling for incoming connections.
handle = add_fd (tcp_listener.get_fd ());
set_pollin (handle);
}
void zmq::zmq_listener_t::in_event ()
{
fd_t fd = tcp_listener.accept ();
// If connection was reset by the peer in the meantime, just ignore it.
// TODO: Handle specific errors like ENFILE/EMFILE etc.
if (fd == retired_fd)
return;
// Choose I/O thread to run connecter in. Given that we are already
// running in an I/O thread, there must be at least one available.
io_thread_t *io_thread = choose_io_thread (options.affinity);
zmq_assert (io_thread);
// Create and launch an init object.
zmq_init_t *init = new (std::nothrow) zmq_init_t (io_thread, socket,
NULL, fd, options);
alloc_assert (init);
launch_child (init);
}
5. 我们再来看看zmq::zmq_send()和zmq::zmq_recv():
我们已经知道zeromq的数据收发是异步的,我们会将消息数据写到相应管道中去。
通过zeromq的官方文档的学习,我想你应该了解到zeromq中定义了各种各样socket的类型,比如push,pull,req,rep,route, pub, sub等等。
因此我么这些不同类型的实现最终都会调用到对相应关联的管道的读写操作。 后面的章节我们会详细说明这些。
最后我们做一个基本代码流程的总结:
1. 创建ctx上下文对象,该对象会创建io_thread并启动它们,采用reactor模型作为poller不断轮询。线程间的通信使用mailbox来通信,而mailbox其实本质上就是一个双向的socketpair。
2. 创建zmq_socket对象,如果是client就使用connector去连接,如果是server就使用listener去监听。
3. connetor或者listener会创建除zmq_init_t对象,后者作为identity交换。
4. 结束上述的初始化之后会创建相应的session_t对象,并且关联相应的读/写管道,plugin zmq_engine, 进行真正的消息数据的读写。
5. 消息数据的读写是异步的,即调用zmq::zmq_send()和zmq::zmq_recv()只是将消息数据写到相应的管道中去。而session_t会在poller轮询到相应的读写事件的时候从管道里面读写消息数据。
下面是个结构图:
注意:这边的Endpoint其实就是zeromq的socket。为了区别普通的socket我才叫Endpoint。
接下去我会比较细致地分析每一块,可能有空的还会翻译或者解释一下zeromq的guide,希望有兴趣的朋友可以和我联系,一起学习。 [email protected]