初识 ZeroMQ

 

 

        由于网上和官方的ZeroMQ主要是讲解和说明大都是基于C、PHP、Java偏偏.Net的很少,可能你看完80多页的官方文档仍被C代码搞的晕晕乎乎的,我这里就将资料收集整理成几篇博文同时用c#重新实现Demo。

        目前ZeroMQ已经有了30多种语言的实现。从这里可以看出它的流行程度,真不愧号称要进入Linux内核的MQ组件。这里对比官方文档做一个ZeroMQ的.Net实现(NetMQ)的介绍(下文中如无特殊说明NetMQ即指ZeroMQ)。

        如果你愿意放弃持久化和极端的一致性换 了超高速的信息交换速率和最低的通信复杂度,您绝对要试试Zeromq。对于熟悉Socket和WCF的.Net程序猿来说,一切都变得简洁起来。我不能 说WCF和ZeroMQ谁参考了谁,但是他们的运行方式实在是太像了。例如都能Host到任一进程中,都能绑定多中通信协议,同时绑定多个端口进行通信等 等。

 

ZeroMQ简介


        没什么好说的,直接Copy 维基百科的介绍:

        ØMQ (也拼写作ZeroMQ0MQZMQ)是一个为可伸缩的分布式或并发应用程序设计的高性能异步消息库。它提供一个消息队列, 但是与面向消息的中间件不同,ZeroMQ的运行不需要专门的消息代理(message broker)。该库设计成常见的套接字风格的API。

        ZeroMQ是由iMatix公司和大量贡献者组成的社群共同开发的。ZeroQ通过许多第三方软件支持大部分流行的编程语言 。类库提供一些套接字(对传统Berkeley套接字和Unix domain socket的泛化),每一个套接字可以代表一个端口之间的多对多连接。以消息的粒度进行操作,套接字需要使用一种消息模式(message pattern),然后专门为那种模式进行了优化。

        http://zh.wikipedia.org/wiki/%C3%98MQ

        注:NetMQ是使用c#对ZeroMQ的实现,基于.Net4.0, GitHub地址见  这里

        官方的文档见 这里

ZeroMQ特点


如果说ZeroMQ最突出的三个特点是什么? 答案是 快,很快,非常快。相信如果您的单线程队列能够在几秒内完成千万级别的数据入列和出列您一定会有同样的感觉。

看看下面这个简单的入列测试在我个人PC上运行的结果(出列的速度更快):

message count:10.000.000
data size:
200b  2K     5K     2M
-------------------------
excute time(ms):
8985 33918 123536 470810
9036 33046 123192 464866
9157 32829 122357 469790
9086 32987 125057 463099
9144 33225 124900 470090
9046 33113 122607 465853
9036 32765 121454 470146
9069 33129 124751 466800
9189 32677 122177 463923
9158 32686 122688 469044
-------------------------
avg(s):
9.100 32.700 123.000 466.000

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
class Program
{
    static string msg200B = "";
    static string msg2K = "";
    static string msg10K = "";
    static string msg20K = "";
    static List<long> msg200BResult = new List<long>() { 0};
    static List<long> msg2KResult = new List<long>() { 0 };
    static List<long> msg10KResult = new List<long>() { 0 };
    static List<long> msg20KResult = new List<long>() { 0 };
    static void Main(string[] args)
    {
        for (int i = 0; i < 10; i++)
        {
                long deq = 0;
                msg200BResult.Add(InQueue( msg200B));
                msg2KResult.Add(InQueue( msg2K));
                msg10KResult.Add(InQueue( msg10K));
                msg20KResult.Add(InQueue( msg20K));
        }
        Console.WriteLine("200B inqueue 1000W, ElapsedMilliseconds:{0}", msg200BResult.Average());
        Console.WriteLine("msg2K inqueue 1000W, ElapsedMilliseconds:{0}", msg2KResult.Average());
        Console.WriteLine("msg10K inqueue 1000W, ElapsedMilliseconds:{0}", msg10KResult.Average());
        Console.WriteLine("msg20K inqueue 1000W, ElapsedMilliseconds:{0}", msg20KResult.Average());
        Console.Read();
    }
    static long InQueue(string msg)
    {
        ManualResetEvent rest = new ManualResetEvent(false);
        using (NetMQContext context = NetMQContext.Create())
        using (var pub = context.CreatePublisherSocket())
        {
            pub.Bind("tcp://127.0.0.1:9991");
             
            long enElapse = 0;
            ThreadPool.QueueUserWorkItem((o) =>
            {
                Stopwatch sw = new Stopwatch();
                Random ran = new Random(Guid.NewGuid().GetHashCode());
                sw.Start();
                long enCount = 0;
                for(int i = 0; i < 1000*10000) //1000W ;
                {
                    if (enCount % 100000 == 0)
                        Console.Write(".");
 
                    pub.SendMore("AAA");
                    pub.Send(msg + ran.Next(-10000, 99999999));//追加随机数避免字符串消息内容相同;
                }
                sw.Stop();
                Console.WriteLine("\n Has InQueue 1000W Mesage.Length={1} , ElapsedMilliseconds:{0}\n", sw.ElapsedMilliseconds, msg.Length);
                enElapse = sw.ElapsedMilliseconds;
                rest.Set();
            });
            rest.WaitOne();
            return enElapse;
        }
         
    }
    static Program()
    {
        StringBuilder temp = new StringBuilder();
        Random ran = new Random(Guid.NewGuid().GetHashCode());
        for(int i = 0; i < 20 * 1024;i++)
        {
            temp.Append((char)ran.Next(0, 65535));
            if (i == 100) //一个Char两个字节;
                msg200B = temp.ToString();
            if (i ==  1024)
                msg2K = temp.ToString();
            if (i == 5 * 1024)
                msg10K = temp.ToString();
        }
        msg20K = temp.ToString();
    }
}

 

不仅仅是速度,就连使用起来ZeroMQ和其他的MQ产品比起来差别太大,以至于有种疑问,ZeroMQ还算的上队列产品麽?

       我们来看看通常的MQ,你需要一台运行在服务器上的MQ服务端,它需要启动一个或多个进程来维护队列中的数据并做持久化以防止宕机后数据丢失,当然还必 须要有有个监控组件保证能尽快的发现队列的阻塞延迟处理速率等运行状况。然后你至少要有一个客户端通过订阅发布或者请求响应的方式与队列服务端交换数据。

        ZeroMQ则完全不同,它不需要独立部署,反之它仅提供一个DLL,你可以将它Host到任何运行的进程中去,IIS、Windows Servers、Window Form都可以。你只需要告诉运行模式和通信端口,就能马上运行起来。 像不像WCF ?

另外一个不得不说的特点是ZeroMQ彻底放弃了持久化,虽然你可以设置一个磁盘Swarp区域用以临时存放队列数据,但一旦工作线程结束,所有数据都将丢失。

       最后说一说ZeroMQ的多对多的优势,一般的MQ产品如果要实现多个队列服务和多个客户端的自由连接就不得不额外的做自己的实现。但ZeroMQ天生就具备这个能力,甚至在可以任何时候添加和取消服务端和客户端的任一个。

ZeroMQ的运行模式和主要API


        ZeroMQ在C和C++下的提供最大的自由度,你可以在初始的几种构件下自由组合,创建各种其他MQ不可能或至少很难实现的运用方式。但记住一 点:ZeroMQ只保证经典的组合方式的健壮和可靠性,其他的需要你自己负责。NetMQ在ZeroMQ之上做了一些封装,但仍然开放底层的API,如果 您觉得仍不能满足需要可以修改源码DIY自己的消息队列运行方式。

        可组合的构件如下(图片点击放大):

你可以将上图中的构件组合出各种的队列运用模式,例如经典的请求应答模式就是RequestSocket + ResponseSockt,而发布订阅则是PublisherSocket + SubscriberSocket组合的结果,就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//发布 
using (NetMQContext context = NetMQContext.Create())
using (var pub = context.CreatePublisherSocket())
{
     pub.Bind("tcp://127.0.0.1:9991");
     pub.Bind("tcp://*:9999");//注意不要多次绑定同一端口;
     pub.Bind("ipc://myserver.ipc");
     pub.Send("****");
}
//订阅
using (var sub = context.CreateSubscriberSocket())
{
     sub.Connect("tcp://127.0.0.1:9991");
     sub.Subscribe("");
 }

        这简直太简单是吧,四句代码帮您做了创建运行上下文,创建Socket连接,信息入列的所有工作。

        如果您需要发布和订阅保证最大的灵活性和可扩展性那么PublisherSocket + RouterSocket + DealerSocket + SubscriberSocket就是首选。总之你可以根据你的想象力和需要组合出各种模式。而这一切只需要你创建一个NetMQ的上下文 (NetMQContext),并向搭积木一样添加各种封装好的Socket构件既可以了。

        当然作为队列产品中核心功能,那就是任一构件在下游构件不能连接或处理速率小于输入的速率时都会缓存未处理的消息,但这种缓存仅在线程生存期间有效。

        为了方便拼装,对各个构件逐个介绍下:

1. RequestSocket:经典的请求Socket构件,一般和ResponseSocket一起组合成请求应答模式。

2. ResponseSocket:请求应答中的应答方,中间可以加入XPublishSocket,RouterSocket等扩展最终到达RequestSocket。

3. RouterSocket、DealerSocket: 当需要保证请求应答模式中可扩展性时需要在两者之间添加一个中间方隔离两端的耦合。这时候就需要RouterSocket+DealerSocket组 合。RouterSocket负责连接RequestSocket,DealerSocket则负责Response的一头

4. PublisherSocket:发布订阅中的发布方。注意由于ZeroMQ的高效,注意尽量让订阅方先启动,保证不丢失消息。

5. SubscriberSocket:发布订阅模式中的订阅方,注意由于发布订阅模式实际是在订阅方做消息筛选的,所有实际上订阅方将接收所有的发布消息再更加自己的订阅清理不需要的。

6. XSubscriberSocket、XPublisherSocket:可能您的发布订阅又是会需要跨网络的广播,这时候您需要在另一个网络中有一个代理,XSubscriberSocket + XPublisherSocket就是为此而生的,XSubscriberSocket负责承上,XPublisherSocke负责承上。

7. PairSocket:当你的一个任务需要跨线程、跨进程甚至跨服务器时就会用到PairSocket模式,它可以在自己任务启动线程指向第一步的队列,然后等待最后一步所在的队列返回结果即可,开始和结束队列直接可以有多个步骤队列,以流水线的方式连接再一起工作。

8. PushSocket:当你不满足于PariSocket只能单线管道模式之下时,你会用到推拉模式,这种模式允许你在任任务流水线的任一环节做并行处理,并在并行后的下一环节归拢整理结果。

9. Pullsocket:推拉模式中的拉的一方。

 

初步了解了Zeromq的构件接下来我们就将它们拼装在一起看看能不能按照我们期望的那样运作起来。

 

ZeroMQ的经典模式


请求应答模式

        请求应答是最经典的CS架构,在ZeroMQ中你只需要在请求端执行Connect方法就能连接到响应端进行通信,不用考虑任何的连接细节,如延时、缓冲器、断开重连等等。

        如果你有多个Req客户端连接一个Rep服务端的话,服务端将以负载均衡的方式将消息分发出去,内部使用了公平队列的算法保证分发的不会照成客户端饥渴。

一个简单的Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private static void Main(string[] args)
        {
            using (NetMQContext ctx = NetMQContext.Create())
            {
                ThreadPool.QueueUserWorkItem((o) =>
                    {
                        using (var server = ctx.CreateResponseSocket())
                        {
                            server.Bind("tcp://127.0.0.1:5556");
                            //server.Monitor("", NetMQ.zmq.SocketEvent.All);//必要时可以将队列延迟,阻塞等事件发往Monitor队列;
                            while (true)
                            {
                                string m1 = server.ReceiveString();
                                Console.WriteLine("From Client: {0}", m1);
                                server.Send("Hi Back");
                            }
                        }
                    });
                using (var client = ctx.CreateRequestSocket())
                {
                    client.Connect("tcp://127.0.0.1:5556");
                    client.Send("Hello");
                    string m2 = client.ReceiveString();
                    Console.WriteLine("From Server: {0}", m2);
                    Thread.Sleep(1000);
                    client.Send("Word");
                    m2 = client.ReceiveString();
                    Console.WriteLine("From Server: {0}", m2);
                    Console.ReadLine();
                }
                Console.Read();
            }
        }

 

发布订阅模式

        发布订阅和请求响应一样是Zero推荐的三种经典模式之一,一个简单的发布订阅结果应该如下图所示:

        Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//发布端;
using (NetMQContext context = NetMQContext.Create())
using (var pub = context.CreatePublisherSocket())
{
    pub.Bind("tcp://127.0.0.1:9991");
    ThreadPool.QueueUserWorkItem((o) =>
    {
        Stopwatch sw = new Stopwatch();
        Random ran = new Random(Guid.NewGuid().GetHashCode());
        sw.Start();
         
        for(int i = 0; i < 10000000; i++) //1000W ;
        {
            if (enCount % 100000 == 0)
                Console.Write(".");
            //pub.SendMore("AAA");//必要时可以给消息加个名称为“AAA”的信封,这样订阅端可以有选择的接受消息;
            pub.Send(msg + ran.Next(-10000, 99999999));//追加随机数放在消息内容字符串一样;
        }
        sw.Stop();
        Console.WriteLine("\n Has InQueue 1000W Mesage, ElapsedMilliseconds:{0}\n", sw.ElapsedMilliseconds);
    });
 
    //订阅端
    using (NetMQContext context = NetMQContext.Create())
    using (var sub = context.CreateSubscriberSocket())
    {
        sub.Connect("tcp://127.0.0.1:9991");
        sub.Subscribe("");//空字符串表示订阅所有,仅订阅“AAA”:sub.Subscribe("AAA");这时第一次ReceiveString()将返回“AAA”,之后才是真正的消息。
        while (true)
        {
            var msg = sub.ReceiveString();//接收消息;
            Console.WriteLine("msg:{0}", msg);
        }
    }

 

推拉模式

         推拉模式或称为管道模式,三种经典模式最后一种。当你需要做并行处理时你会用到这种模式,他允许你将任务推送到并行执行的Worker节点 上,Worker执行完成后再将结果发送Pull节点上做结果合并。有点MapRaduce的意思是吧。使用Socket构件是PushSocet和 PullSocket 结果如下图:

Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
static void Main(string[] args)
{
    using (var ctx = NetMQContext.Create())
    {
        ThreadPool.QueueUserWorkItem((o) =>
        {
            //ventilator
            using (var ventilator = ctx.CreatePushSocket())
            {
                ventilator.Bind("tcp://127.0.0.1:9992");
                Thread.Sleep(20);
                ventilator.SendMore("A");
                ventilator.Send("#InputInfo#");
                //sink
                using (var sink = ctx.CreatePullSocket())
                {
                    sink.Bind("tcp://127.0.0.1:9993");
                    while (true)
                    {
                        var result = sink.Receive();
                        Console.WriteLine(Formate(result));
                    }
                }
            }
        });
        //worker;
        ThreadPool.QueueUserWorkItem((o) =>
        {
            using (var ctxWorker = NetMQContext.Create())
            {
                var recv = ctxWorker.CreatePullSocket();
                recv.Connect("tcp://127.0.0.1:9992");
                var send = ctxWorker.CreatePushSocket();
                send.Connect("tcp://127.0.0.1:9993");
                while (true)
                {
                    var input = recv.Receive();
                    Console.WriteLine("Input:{0}", Formate(input));
                    Thread.Sleep(1000);// do work;
                    send.Send(string.Format("Worker {2} Input:{0},Output:{1}", Formate(input), "*****", id));
                }
            }
        });
        Console.Read();
    }
}
static string Formate(byte[] input)
{
    return System.Text.Encoding.Default.GetString(input);
}

在刚开始尝试的时候你可能发现各种不Work的情况,一般是发不出或收不到。你可以参照下图来做次TroubleShooting,图中的API是基于C的,不过你应该很快就能找到对应的.Net的API :

                                                           

 

熟悉了三种ZeroMQ的经典队列模式,接下来我们来整点更高级的。

posted @ 2015-06-10 16:15  Liffey  Views(7544)  Comments(2Edit  收藏  举报