消息队列ZeroMQ实践
来自于:服务端框架工具经验 作者:郭忆 2014-08-13 10:54
一、 ZeroMQ简介
ZeroMQ被称为史上最快消息队列,它处于会话层之上,应用层之下,使用后台异步线程完成消息的接受和发送,完美的封装了Socket API,大大简化了编程人员的复杂度,被称为史上最强大的消息中间件。ZMQ是用C语言编写的,30us内完成消息的传输,能够兼容多个平台,多种语言,可以使用多种方式实现N对N的Socket连接。本文仅以JAVA版本的ZMQ API为例,介绍ZMQ
ZMQ与传统的TCP Socket相比,具有以下优点:
ZMQ发送和接受的是具有固定长度的二进制对象,ZMQ的消息包最大254个字节,前6个字节是协议,然后是数据包。数据包由3个部分组成,第一个字节是包的长度,第二个字节是包的一些属性,最后是包的内容。如果超过255个字节(有一个字节表示包属性),则ZMQ会自动分包传输;而对于TCP Socket,是面向字节流的连接,编程人员需要处理数据块与数据块之间的边界问题,而ZMQ能够保证每次用户发送和接受的都是一个完整的数据块;
传统的TCP Socket的连接是1对1的,可以认为“1个Socket=1个连接”,每一个线程独立的维护一个Socket。但是ZMQ摒弃了这种1对1的模式,ZMQ的Socket可以很轻松的实现1对N,N对1和N对N的连接模式,一个ZMQ的Socket可以自动的维护一组连接,用户无法操作这些连接,用户只能操作套接字,而不是连接本身,所以说ZMQ的世界里,连接是私有的。这里大家关心的一点是,一个Socket是如何识别来自多个Socket的连接的,这里以请求响应模式为例介绍ZMQ是如何实现一个Socket连接N个Socket的;
MQ使用异步后台线程处理接受和发送请求,这意味着发送完消息,不可以立即释放资源,消息什么时候发送用户是无法控制的,同时,ZMQ自动重连,这意味着用户可以以任意顺序加入到网络中,服务器也可以随时加入或者退出网络;
ZMQ之所以能够在无状态的网络中实现1对N的连接,关键在于信封的机制,信封里保存了应答目标的位置。ZMQ涉及到请求-响应模式的Socket一共有4种类型:
DEALER是一种负载均衡,它会将消息分发给已连接的节点,并使用公平队列的机制处理接受到的消息。
REQ发送消息时会在消息顶部插入一个空帧,接受时会将空帧移去。其实REQ是建立在DEALER之上的,但REQ只有当消息发送并接受到回应后才能继续运行。
ROUTER在收到消息时会在顶部添加一个信封,标记消息来源。发送时会通过该信封决定哪个节点可以获取到该条消息。
REP在收到消息时会将第一个空帧之前的所有信息保存起来,将原始信息传送给应用程序。在发送消息时,REP会用刚才保存的信息包裹应答消息。REP其实是建立在ROUTER之上的,但和REQ一样,必须完成接受和发送这两个动作后才能继续。
在了解了4种类型的Socket之后,我们就不难理解ZMQ的信封机制了。ZMQ信封机制的核心是Router Socket,它的工作原理如下:
从ROUTER中读取一条消息时,ZMQ会包上一层信封,上面注明了消息的来源。向ROUTER写入一条消息时(包含信封),ZMQ会将信封拆开,并将消息递送给相应的对象。当REQ Socket向ROUTER Socket发送一条请求后,REP会从ROUTER收到一条消息,消息格式如下:
第三帧是REP从应用程序收到的数据,第二帧是空帧,是REQ在发送ROUTER数据之前添加的,用来表示结束,第一帧即信封,是ROUTER添加的,主要用来记录消息来源;整个数据包处理过程如下:
对于REQ Socket,可以在创建Socket的时候,为该Sock指定标示符,此时的Socket称为持久Socket,没有指定标示符的我们称为瞬时Socket,ROUTER会自动为瞬时Socket生成一个标示符;
这样REP返回包含信封的数据给ROUTER,ROUTER就可以根据信封上的标示符将该消息发送到对应的REQ上;
ZMQ使用注意事项:
ZMQ是在发送端缓存消息的,可以通过阈值控制消息的溢出;
ZMQ不可以线程之间共享Socket,否则会报org.zeromq.ZMQException: Operation cannot be accomplished in current state错误。
ZMQ一个进程只允许有一个Context,new Context(int arg) arg表示后台线程的数量。
ZMQ的Socket类有一个Linger参数,可以通过SetLinger设置,主要用于表示该Socket关闭以后,未发送成功的消息是否还保存,如果设置为-1表示该消息将永久保存(除非宕机,ZMQ是不持久化消息的),如果为0表示所有未发送成功的消息在Socker关闭以后都将立即清除,如果是一个正数,则表示该消息在Socket关闭后多少毫秒内被删除;这个方法非常有用,尤其在控制发送失败时,是否重发消息。
二、 ZeroMQ多线程编程
ZMQ与传统的TCP Socket最大的区别在于Socket连接不再是1:1的,而是1:N甚至N:N的,这意味ZMQ摒弃了每个后台请求处理线程都使用单独的一个 Socket来返回用户请求的。同时,ZMQ又不允许多个线程共享Socket,所以ZMQ在使用一个Socket处理请求的过程中,会阻塞同一个端口的 其他请求,显然这是无法满足需求的。ZMQ使用了信封的机制灵活的解决了这个问题。
正如上节介绍,ZMQ的Socket有4种类型:DEALER,REQ,REP,ROUTER。4种Socket之间的组合变化基本可以满足绝大多数消息 通信的需求,当然也可以实现多线程处理用户请求。信封机制的核心是Router Socket,它可以为每一个来自REQ的请求打上一个标记来标识该REQ,在返回的时候直接返回给对应的REQ,利用Router我们就可以实现利用多 线程处理用户请求。
这里我们采用经典的请求-响应模式为例,了解如何利用ZMQ实现多线程处理用户请求。整个消息通信系统的架构如图1-1所示,由客户端,服务器端监听线程,后台消息处理线程组成。
整个消息通信流程如下:
客户端使用REQ Socket发送消息给服务器端监听线程的Front Router,REQ Socket会为每一个用户请求消息前加入一个空帧(Frame);
Front Router在接到REQ的消息后,会判断该REQ是否是持久化Socket,即是否有标示符,如果没有,则自动为该REQ生成一个标示符。将该标识符作 为一个帧加入到整个消息的前面。如上节所述,ZMQ的消息(ZMsg)由若干个帧(ZFrame)组成,当前,整个ZMsg由三个部分组成,Client Identifier,空帧和消息实体。
Front Router在接到消息后,服务器端监听线程会将该消息交给一个后台线程进行处理,然后继续监听其他客户端请求,这样就不会阻塞端口;
后台消息处理线程将接到的消息进行拆分,取出客户端请求进行处理,然后按照ClientIdentifier,空帧和返回内容组装成新的Zmsg,交给该后台线程REQ。
后台消息处理线程的REQ在接到ZMsg后,会为该Zmsg外层再加入一个空帧,然后发送给服务器端监听线程的Backend Router;
Backend Router同理会自动在接到ZMsg的最外层加入一个后台线程的标示符,然后服务器监听线程对消息进行拆分,取出ClientIdentifier,然 后交给FrontRouter,利用Front Router发送给对应的客户端。
至此,整个消息通信过程结束。
三、 Windows下编译安装
zmq和jzmq目前官方没有提供已编译好的文件供下载,git上的也只有很老的(2.0)版本。由于在开发过程中都使用的windows系统,所以在 windows下能够使用jzmq对开发和调试系统来讲都会很方便。但是,通过官方网站上的介绍在windows下安装zmq3.2.2版本会有许多问 题,有各种坑,且网上相关资料缺乏。笔者经过不断的尝试终于成功的在windows下编译和使用了jzmq,这里把windows通过编译安装zmq和 jzmq的方式整理并记录了下来,以便需要的人能够参考。
环境要求:
安装了jdk(请选择对应的是32位版本还是64位版本)且设置了对应的环境变量,能够正确在cmd中执行javac命令。window环境中安装了msvc2008或更新版本。
zeromq的编译:
zeromq官方提供了已编译好的对应的installer版本下载,包含32位和64位。但是在使用时会遇到许多问题,故无论是使用zeromq还是通过zeromq库去编译jzmq,最好都是自己去编译一下zeromq。编译方法如下:
登录ZeroMQ | Download下载window的源码包,下载好源码包后用msvc打开builds\msvc\msvc.sln编译即可。注意这里有个小坑,3.2.2版本在编译中会抛出errorno.cpp文件找不到等异常,并导致编译失败,比较奇怪。这时候可以在 libzmq工程目录下Source Files中找到errorno.cpp文件,双击后发现msvc也无法打开它,难道这个文件本身不需要?直接删除这个文件,再次编译,通过。
编译好的库会放在lib子目录中。
jzmq的编译:
要在windows下使用jzmq就更悲催了。官方居然没有提供已编译好的window版本,只给出了源码和编译步骤。这样只能自己编译。jzmq编译也 分windows32位和64位两个版本。编译方式也不同特别是window64位版本的编译,你用官方的方法一定编译不过。下面具体讲讲在编译中遇到的 问题和处理方式。
jzmq的windows32位版本的编译:
下载源码包:git clone GitHub - zeromq/jzmq: Java binding for ZeroMQ。
用msvc打开builds\msvc\msvc.sln进入jzmq工程。
配置工程依赖路径:msvc2008配置在:Tools|Options|Projects and Solutions|VC++ Directories|Include files。2008后期后期版本略有不同,在对应的工程下,点击右键进入属性后找到VC++目录做对应操作即可。路径设置参考:
Include files:
\include\win32
\include
\include
Library files:
\lib
编译即可。注意这里有个小坑:如果你使用的是官方已打包好的zeromq库进行编译,下载对应的依赖库名称是类似libzmq-v90-mt- 3_2_2.dll(或lib)而非libzmq.dll(或lib)。这是你要将名称替换成libzmq.dll,libzmq.lib。这样才能编译 通过。
jzmq的windows64为版本的编译:
下载jzmq源码:git clone https://github.com/zeromq/jzmq.git
另外还需要下载cmake for window(注意官方没有说选择32位还是64位版本,但实际上这只是个编译中间工具,32位版本亦可,无需刻意寻找对应的64位版本,官方也没提 供):Download | CMake
在jzmq源码目录中创建一个新目录:build64(官方在步骤3中说Insert attached CMakeLists.txt,下载这个文件并覆盖到jzmq根目录。注意千万别这么做,那个CMakeLists.txt是很老的版本了。不适用现在的 环境了。好大个坑。实际上你无需替换任何CMakeLists.txt文件)
在build64目录中cmd运行D:\Microsoft Visual Studio 11.0\VC\bin\amd64\vcvars64.bat(请换成你机子上msvc对应目录。又是个大坑,官方居然没有这一步,cmd没有通过该步 设置对应的环境变量后续即使能工作都是错误的)
不要关闭cmd,再运行cmake .. -G "NMake Makefiles"
完成后再cmake-gui .打开cmake图形界面,设置CMAKE_BUILD_TYPE为Release,然后分别点击Configure,Generate
指定编译的include和lib(官方又没提该步,如果不熟悉nmake的话后续操作会比较困惑)请换成你机子上msvc对应目录,注意libzmq.dll也需要64位版本,可在http://miru.hk/archive /ZeroMQ-3.2.2rc2~miru1.5-x86.exe下载编译好的版本。然后进入lib目录下把任意一个lib版本换成 libzmq.lib,这样编译器才能够识别。如libzmq-v110-mt-3_2_2.lib换成libzmq.lib。cmd脚本例子如下:
set INCLUDE=%INCLUDE%;F:\zmq\zeromq-3.2.2\include;D:\jdk\include;D:\jdk\include\win32
set LIB=%LIB%;F:\zmq\zmq
最后运行nmake生成对应库文件。(可选)如果你安装了NSIS的话可以运行nmake package让它生成可安装文件
在使用jzmq时可能也会遇到一些问题,这里提供了一些解决方案可参考。
首先确保run->run configurations->arguments->VM arguments下指定了libraray path,如:-Djava.library.path=E:\cfg\lib\win64。由于jzmq.dll同时又依赖libzmq.dll,而 dll与dll之间的依赖是通过system path关联的,所以还需要将libzmq.dll放在system path环境变量中。
如果eclipse运行Can't find dependent libraries出现问题,请按如下方式依次检查:
确保libzmq.dll已放入了system path环境变量中?可通过打开cmd后输入where libzmq.dll检查
eclipse是否重启?如果system path环境变量变更,需要重启eclipse后eclipse才能识别
如果还是不行,从Dependency Walker (depends.exe) Home Page下载dependencywalker小工具,600k左右,它能够 查看window模块(exe,dll,sys等)依赖的模块有哪些,使用非常简单。通过它检查jzmq.dll模块具体依赖了哪些模块,在系统环境变量 中补上缺失的模块即可