接触过与性能有关的网络编程的*nix端后台开发的同步一定听说这这样的一个术语: 零拷贝(Zero-Copy). 你仔细回想我们通过网络编程接收, 发送消息的过程. 如果我们要发送一个消息, 我们需要把这个消息传递给发送相关的接口, 如果我们需要接收一个消息, 我们需要把我们的缓冲区提供给接收消息的函数.
这里就有一个性能痛点, 特别是在接收消息的时候: 在网络接口API底层, 一定有另外一个缓冲区率先接收了数据, 之后, 你调用收包函数, 诸如recv这样的函数, 将你的缓冲区提供给函数, 然后, 数据需要从事先收到数据的缓冲区, 拷贝至你自己提供给API的缓冲区.
如果我们向更底层追究一点, 会发现网络编程中, 最简单的发收消息模型里, 至少存在着两到三次拷贝, 不光收包的过程中有, 发包也有. 上面讲到的只是离应用开发者最近的一层发生的拷贝动作. 而实际上, 可能发生拷贝的地方有: 应用程序与API交互层, API与协议栈交互层, 协议栈/内核空间交互层, 等等.
对于更深层次来讲, 不是我们应用程序开发者应该关心的地方, 并且时至今日, 从协议栈到离我们最近的那一层, 操作系统基本上都做了避免拷贝的优化. 那么, ZMQ作为一个网络库, 在使用的进修, 应用程序开发就应当避免离我们最近的那一次拷贝.
这也是为什么ZMQ库除了zmq_send与zmq_recv之外, 又配套zmq_msg_t类型再提供了zmq_msg_send与zmq_msg_recv接口的原因. zmq_msg_t内置了一个缓冲区, 可以用来收发消息, 当你使用msg系的接口时, 收与发都发生在zmq_msg_t实例的缓冲区中, 不存在拷贝问题.
总之, 要避免拷贝, 需要以下几步:
使用zmq_msg_init_data()创建一个zmq_msg_t实例. 接口返回的是zmq_msg_t的句柄. 应用开发者看不到底层实现.
发送数据时, 将数据通过memcpy之类的接口写入zmq_msg_t中, 再传递给zmq_msg_send. 接收数据时, 直接将zmq_msg_t句柄传递给zmq_msg_recv
需要注意的是, zmq_msg_t被发送之后, 其中的数据就自动被释放了. 也就是, 对于同一个zmq_msg_t句柄, 你不能连续两次调用zmq_msg_send
zmq_msg_t内部使用了引用计数的形式来指向真正存储数据的缓冲区, 也就是说, zmq_msg_send会将这个计数减一. 当计数为0时, 数据就会被释放. ZMQ库对于zmq_msg_t的具体实现并没有做过多介绍, 也只点到这一层为止.
所以这时你应该明白, 多个zmq_msg_t是有可能共享同一段二进制数据的. 这也是zmq_msg_copy做的事情. 如果你需要将同一段二进制数据发送多次, 那么请使用zmq_msg_copy来生成额外的zmq_msg_t句柄. 每次zmq_msg_copy操作都将导致真正的数据的引用计数被+1. 每次zmq_msg_send则减1, 引用计数为0, 数据自动释放.
数据释放其实调用的是zmq_msg_close接口. 注意: 在zmq_msg_send被调用之后, ZMQ库自动调用了zmq_msg_close, 你可以理解为, 在zmq_msg_send内部, 完成数据发送后, 自动调用了zmq_msg_close
蛋疼的事在收包上. 由于zmq_msg_t的内部实现是一个黑盒, 所以如果要接收数据, 虽然调用zmq_msg_recv的过程中没有发生拷贝, 但应用程序开发者最终还是需要把数据读出来. 这就必须有一次拷贝. 这是无法避免的. 或者换一个角度来描述这个蛋疼的点: ZMQ没有向我们提供真正的零拷贝收包接口. 收包时的拷贝是无可避免的.
最后给大家一个忠告: 拷贝确实是一个后端服务程序的性能问题. 但瓶颈一般不在调用网络库时发生的拷贝, 而在于其它地方的拷贝. zmq_msg_t的使用重心不应该在"优化拷贝, 提升性能"这个点上, 而是第三章要提到和进一步深入讲解的多帧消息.
在发布-订阅套路中使用多帧消息, 即"信封"之前我们讲到的发布-订阅套路里, 发布者广播的消息全是字符串, 而订阅者筛选过滤消息也是按字符串匹配前几个字符, 这种策略有点土. 假如我们能把发布者广播的消息分成两段: 消息头与消息体. 消息头里写明信息类型, 消息体里再写具体的信息内容. 这样过滤器直接匹配消息头就能决定这个消息要还是不要, 这就看起来洋气多了.
ZMQ中使用多帧消息支持这一点. 发布者发布多帧消息时, 订阅者的过滤器在匹配时, 只匹配第一帧.
多说无益, 来看例子, 在具体展示发布者与订阅者代码之前, 需要为我们的zmq_help.h文件再加一个函数, 用于发送多帖消息的s_sendmore
/* * 把字符串作为字节数据, 发送至zmq socket, 但不发送字符串末尾的\'\0\'字节 * 并且通知socket, 后续还有帧要发送 * 发送成功时, 返回发送的字节数 */ static inline int s_sendmore(void * socket, const char * string) { return zmq_send(socket, string, strlen(string), ZMQ_SNDMORE); }