【学习0MQ】二、认识sockets

认识sockets

通过之前对ZeroMQ有个基本了解后,我们了解一下关于sockets的以下两点:

  • publish-subscribe 模式
  • pipeline 模式

publish-subscribe 模式

首先我们了解一下第二经典模式,publish-subscribe模式,该方式可以通过服务端发送一系列消息给客户端子集。是一种一对多的模型。
基本的意图是publisher发送一条消息 ,subscribers接受消息,断开连接的subscribers将丢弃消息。publisher和subscribers松耦合的,并不关心是否有subscribers存在。
就好像是TV频道或者电台工作方式。TV频道总是广播电视节目,只有打开该频道的观看者才可以接受到。如果你错误了时间,你将错过你喜爱的节目。publish-subscribe模式的优点是提供更加动态的网络拓扑。

publish-subscribe模式可以总结为以下主要点:

  • publish: pulisher发布事件
  • Notify: subscriber被通知有事件
  • Subscribe: 一个新的订阅被subscriber发出
  • Unsubscribe: subscriber移除订阅

我们来看个例子吧。考虑我们要开发一个股票交易的程序。存有经纪人,他们想知道有多少股票。我们股票是publisher,而subscriber是经济人.

我们不实际去拿股票信息,我们随机一些随机股票值

server.c

/*
 *  Stock Market Server
 * Binds PUB socket to tcp://*:4040
 * Publishes random stock values of random companies 
 */
#include <string.h>
#include "zmq.h"

 int main(int argc, char *argv[])
 {
    void *context = zmq_ctx_new();
    void *publisher = zmq_socket(context, ZMQ_PUB);
    printf("Starting server...\n");

    int conn = zmq_bind(publisher, "tcp://*:4040");

    const char *companies[2] = {"Company1", "Company2"};
    int count = 0;
    for (;;) {
        int price = count % 2;
        int which_company = count % 2;
        int index = strlen(companies[0]);
        char update[12];
        snprintf(update, sizeof(update), "%s", companies[which_company]);

        zmq_msg_t message;
        zmq_msg_init_size(&message, index);
        memcpy(zmq_msg_data(&message), update, index);
        zmq_msg_send(&message, publisher, 0);
        zmq_msg_close(&message);
        count++;
    }

    zmq_close(publisher);
    zmq_ctx_destroy(context);
 }

client.c

/*
 *  Stock Market Client
 * Connects PUB socket to tcp://localhost:4040
 * Collects stock exchange values 
 */
 #include <stdlib.h>
 #include <string.h>
 #include "zmq.h"

 int main(int argc, char *argv[])
 {
    void *context = zmq_ctx_new();
    void *subscriber = zmq_socket(context, ZMQ_SUB);
    printf("Collecting stock information from the server.\n");

    int conn = zmq_connect(subscriber, "tcp://localhost:4040");
    conn = zmq_setsockopt(subscriber, ZMQ_SUBSCRIBE, 0, 0);

    int i;
    for (i = 0; i < 10; i++) {
        zmq_msg_t reply;
        zmq_msg_init(&reply);
        zmq_msg_recv(&reply, subscriber, 0);
        int length = zmq_msg_size(&reply);
        char* value = malloc(length+1);
        memcpy(value, zmq_msg_data(&reply), length);
        zmq_msg_close(&reply);
        value[length] = '\0';
        printf("%s\n", value);
        free(value);
    }
    zmq_close(subscriber);
    zmq_ctx_destroy(context);

    return 0;
 }
 

使用SUB socket时,必须使用zmq_setsockopt()设置subscribe为subscription,否则将无法收到任何消息。这是一个很常见的错误。

subscriber可以设置多个subscriptions,也可以单个取消subscription。

subscriber使用zmq_msg_recv()来接收消息。zmq_msg_recv()从socket取得消息,并存储消息。之前的消息将会销毁。

int zmq_msg_recv (zmq_msg_t *msg, void *socket, int flags);

flags只能设置为一个值,ZMQ_DONTWAIT.如果指定ZMQ_DONTWAIT,那么操作执行为non-blocking模式。如果消息成功接受,zmq_msg_recv()返回消息大小(bytes),否则返回-1,以及错误消息flag。

publish-subscribe模式是异步的,发送一个消息到SUB socket来触发错误。你可以使用zmq_msg_send()发送任何消息,但是你不能使用zmq_msg_recv()在PUB socket上进行调用。

我们看看输出:

 $ ./client 
Collecting stock information from the server.
Company2
Company1
Company2
Company1
Company2
Company1
Company2
Company1
Company2
Company1

publisher在没有subscriber的情况下仍然会发送消息。

$ ./server 
Starting server...
Sending: Company1
Sending: Company2
Sending: Company1
Sending: Company2
Sending: Company1
Sending: Company2
Sending: Company1
Sending: Company2
Sending: Company1
Sending: Company2
Sending: Company1
Sending: Company2
Sending: Company1
Sending: Company2
Sending: Company1
Sending: Company2
Sending: Company1
Sending: Company2

我们假设只想取得Company1或者传入的参数的消息。

/*
 *  Stock Market Client
 * Connects PUB socket to tcp://localhost:4040
 * Collects stock exchange values 
 */
 #include <stdlib.h>
 #include <string.h>
 #include "zmq.h"

 int main(int argc, char *argv[])
 {
    void *context = zmq_ctx_new();
    void *subscriber = zmq_socket(context, ZMQ_SUB);

    const char* filter;

    if (argc > 1) {
        filter = argv[1];
    } else {
        filter = "Company1";
    }

    printf("Collecting stock information from the server.\n");

    int conn = zmq_connect(subscriber, "tcp://localhost:4040");
    conn = zmq_setsockopt(subscriber, ZMQ_SUBSCRIBE, filter, strlen(filter));

    int i;
    for (i = 0; i < 10; i++) {
        zmq_msg_t reply;
        zmq_msg_init(&reply);
        zmq_msg_recv(&reply, subscriber, 0);

        int length = zmq_msg_size(&reply);
        char* value = malloc(length+1);
        memcpy(value, zmq_msg_data(&reply), length);
        zmq_msg_close(&reply);
        value[length] = '\0';
        printf("%s\n", value);
        free(value);
    }
    zmq_close(subscriber);
    zmq_ctx_destroy(context);

    return 0;
 }

$ ./client2
Collecting stock information from the server.
Company1
Company1
Company1
Company1
Company1
Company1
Company1
Company1
Company1
Company1

过滤消息

我们的股票交易应用发送消息给subscribers。看起来好像程序正常工作了,是吗?悲哀,不是。使用SUB socket时,必须使用zmq_setsockopt()设置subscribe为subscription,否则将无法收到任何消息。这是一个很常见的错误。

subscriber可以设置多个subscriptions,也可以单个取消subscription。

subscriber使用zmq_msg_recv()来接收消息。zmq_msg_recv()从socket取得消息,并存储消息。之前的消息将会销毁。

int zmq_msg_recv (zmq_msg_t *msg, void *socket, int flags);

flags只能设置为一个值,ZMQ_DONTWAIT.如果指定ZMQ_DONTWAIT,那么操作执行为non-blocking模式。如果消息成功接受,zmq_msg_recv()返回消息大小(bytes),否则返回-1,以及错误消息flag。

publish-subscribe模式是异步的,发送一个消息到SUB socket来触发错误。你可以使用zmq_msg_send()发送任何消息,但是你不能使用zmq_msg_recv()在PUB socket上进行调用。

我们看看输出:

 $ ./client 
Collecting stock information from the server.
Company2
Company1
Company2
Company1
Company2
Company1
Company2
Company1
Company2
Company1

publisher在没有subscriber的情况下仍然会发送消息。

$ ./server 
Starting server...
Sending: Company1
Sending: Company2
Sending: Company1
Sending: Company2
Sending: Company1
Sending: Company2
Sending: Company1
Sending: Company2
Sending: Company1
Sending: Company2
Sending: Company1
Sending: Company2
Sending: Company1
Sending: Company2
Sending: Company1
Sending: Company2
Sending: Company1
Sending: Company2

我们假设只想取得Company1或者传入的参数的消息。

/*
 *  Stock Market Client
 * Connects PUB socket to tcp://localhost:4040
 * Collects stock exchange values 
 */
 #include <stdlib.h>
 #include <string.h>
 #include "zmq.h"

 int main(int argc, char *argv[])
 {
    void *context = zmq_ctx_new();
    void *subscriber = zmq_socket(context, ZMQ_SUB);

    const char* filter;

    if (argc > 1) {
        filter = argv[1];
    } else {
        filter = "Company1";
    }

    printf("Collecting stock information from the server.\n");

    int conn = zmq_connect(subscriber, "tcp://localhost:4040");
    conn = zmq_setsockopt(subscriber, ZMQ_SUBSCRIBE, filter, strlen(filter));

    int i;
    for (i = 0; i < 10; i++) {
        zmq_msg_t reply;
        zmq_msg_init(&reply);
        zmq_msg_recv(&reply, subscriber, 0);

        int length = zmq_msg_size(&reply);
        char* value = malloc(length+1);
        memcpy(value, zmq_msg_data(&reply), length);
        zmq_msg_close(&reply);
        value[length] = '\0';
        printf("%s\n", value);
        free(value);
    }
    zmq_close(subscriber);
    zmq_ctx_destroy(context);

    return 0;
 }

$ ./client2
Collecting stock information from the server.
Company1
Company1
Company1
Company1
Company1
Company1
Company1
Company1
Company1
Company1

过滤消息

我们的股票交易应用发送消息给subscribers。看起来好像程序正常工作了,是吗?悲哀,不是。

ZeroMQ通过使用prefix来匹配内容,意味着ZeroMQ将仅返回Company1, Company10, 以及Company101。

我们来修改一下代码:

/*
 *  Stock Market Server
 * Binds PUB socket to tcp://*:4040
 * Publishes random stock values of random companies 
 */
#include <string.h>
#include "zmq.h"

 int main(int argc, char *argv[])
 {
    void *context = zmq_ctx_new();
    void *publisher = zmq_socket(context, ZMQ_PUB);
    printf("Starting server...\n");

    int conn = zmq_bind(publisher, "tcp://*:4040");

    const char *companies[3] = {"Company1", "Company10", "Company101"};

    int count = 0;
    for (;;) {
        int which_company = count % 3;
        int index = strlen(companies[which_company]);
        char update[64];
        snprintf(update, sizeof(update), "%s", companies[which_company]);

        zmq_msg_t message;
        zmq_msg_init_size(&message, index);
        printf("Sending: %s\n", update);
        memcpy(zmq_msg_data(&message), update, index);
        zmq_msg_send(&message, publisher, 0);
        zmq_msg_close(&message);
        count++;
    }

    zmq_close(publisher);
    zmq_ctx_destroy(context);

    return 0;
 }

在修改subscriber为如下:

/*
 *  Stock Market Client
 * Connects PUB socket to tcp://localhost:4040
 * Collects stock exchange values 
 */
 #include <stdlib.h>
 #include <string.h>
 #include "zmq.h"

 int main(int argc, char *argv[])
 {
    void *context = zmq_ctx_new();
    void *subscriber = zmq_socket(context, ZMQ_SUB);

    const char* filter;

    if (argc > 1) {
        filter = argv[1];
    } else {
        filter = "Company1";
    }

    printf("Collecting stock information from the server.\n");

    int conn = zmq_connect(subscriber, "tcp://localhost:4040");
    conn = zmq_setsockopt(subscriber, ZMQ_SUBSCRIBE, filter, strlen(filter));

    int i;
    for (i = 0; i < 10; i++) {
        zmq_msg_t reply;
        zmq_msg_init(&reply);
        zmq_msg_recv(&reply, subscriber, 0);

        int length = zmq_msg_size(&reply);
        char* value = malloc(length+1);
        memcpy(value, zmq_msg_data(&reply), length);
        zmq_msg_close(&reply);
        printf("msg: %s, length: %d\n", value, length);
        free(value);
    }
    zmq_close(subscriber);
    zmq_ctx_destroy(context);

    return 0;
 }
 

输出结果如下:

$ ./client3
Collecting stock information from the server.
msg: Company1, length: 8
msg: Company10, length: 9
msg: Company101, length: 10
msg: Company101, length: 8
msg: Company101, length: 9
msg: Company101, length: 10
msg: Company101, length: 8
msg: Company101, length: 9
msg: Company101, length: 10
msg: Company101, length: 8

我们的subscriber设置filter为Company1.但是publisher也给了我们Company10以及Company101。这不是我们想要的结果。

最简单的方式是使用delimiter

publisher

/*
 *  Stock Market Server
 * Binds PUB socket to tcp://*:4040
 * Publishes random stock values of random companies 
 */
#include <stdlib.h>
#include <string.h>
#include "zmq.h"

 int main(int argc, char *argv[])
 {
    void *context = zmq_ctx_new();
    void *publisher = zmq_socket(context, ZMQ_PUB);
    printf("Starting server...\n");

    int conn = zmq_bind(publisher, "tcp://*:4040");

    const char *companies[3] = {"Company1", "Company10", "Company101"};

    int count = 0;
    for (;;) {
        int price = count % 17;
        int which_company = count % 3;
        char update[64];
        sprintf(update, "%s| %d", companies[which_company], price);
        int index = strlen(update);

        zmq_msg_t message;
        zmq_msg_init_size(&message, index);
        printf("Sending: %s\n", update);
        memcpy(zmq_msg_data(&message), update, index);
        zmq_msg_send(&message, publisher, 0);
        zmq_msg_close(&message);
        count++;
    }

    zmq_close(publisher);
    zmq_ctx_destroy(context);

    return 0;
 }
 

subscriber

/*
 *  Stock Market Client
 * Connects PUB socket to tcp://localhost:4040
 * Collects stock exchange values 
 */
 #include <stdlib.h>
 #include <string.h>
 #include "zmq.h"

 int main(int argc, char *argv[])
 {
    void *context = zmq_ctx_new();
    void *subscriber = zmq_socket(context, ZMQ_SUB);

    const char* filter;

    filter = "Company1|";
    printf("Collecting stock information from the server.\n");

    int conn = zmq_connect(subscriber, "tcp://localhost:4040");
    conn = zmq_setsockopt(subscriber, ZMQ_SUBSCRIBE, filter, strlen(filter));

    int i;
    for (i = 0; i < 10; i++) {
        zmq_msg_t reply;
        zmq_msg_init(&reply);
        zmq_msg_recv(&reply, subscriber, 0);

        int length = zmq_msg_size(&reply);
        char* value = malloc(length+1);
        memcpy(value, zmq_msg_data(&reply), length);
        zmq_msg_close(&reply);
        printf("msg: %s, length: %d\n", value, length);
        free(value);
    }
    zmq_close(subscriber);
    zmq_ctx_destroy(context);

    return 0;
 }
 

我们就得到了想要的结果

$ ./client4
Collecting stock information from the server.
msg: Company1| 12, length: 12
msg: Company1| 15, length: 12
msg: Company1| 15, length: 11
msg: Company1| 45, length: 11
msg: Company1| 75, length: 11
msg: Company1| 10, length: 12
msg: Company1| 13, length: 12
msg: Company1| 16, length: 12
msg: Company1| 26, length: 11
msg: Company1| 56, length: 11

socket选项

在publish-subscribe模式中,我们使用了ZMQ_SUBSCRIBE选项

conn = zmq_setsockopt(subscriber, ZMQ_SUBSCRIBE, option_value,strlen(option_value));

socket选项通过zmq_setsockopt()函数设置,接受的参数为:

  • socket
  • 选项名
  • 选项值
  • 选项长度

    int zmq_setsockopt (void socket, int option_name, const void option_value, size_t option_len);

订阅

ZMQ_SUBSCRIBE实现接受ZMQ_SUB中接收消息,如果option_value不为空,那么只订阅option_value开头的所有消息。你可以给一个ZMQ_SUB socket添加多个过滤器

取消订阅

ZMQ_UNSUBSCRIBE从ZMQ_SUB socket中移除消息。如果有多个过滤器,仅移除一个。

一个要注意的事项是publisher-subscriber socket中我们不知道subscriber何时收到消息。本例中,最好的办法是先启动subscriber然后启动publisher.因为subscriber总是会错过第一条消息,因为连接publisher需要时间,而那时publisher有可能已经发送了一条消息。

我们会讨论如何同步publisher和subscribers。我们会在subscribers没有连上不发送任何消息。

使用publisher-subscriber模式注意事项

  • 使用TCP时,如果subscriber接受消息太慢,在publisher端可能队列会爆满。后面我们会讨论解决措施。
  • subscriber可以连接多个publishers。数据传输通过fair-queue策略
  • publisher发送所有消息到所有subscribers,过滤发生在subscriber端。
  • 我们后面会谈及如何解决慢subscribers

pipline 模式

我们接着了解一下pipeline模式, pipline模式通过pipline有序传输数据到节点。数据持续不断传输,每步pipeline连接一个或多个节点。round-robin策略被用来节点中传输数据。和request-reply模式相像。

divide 和 conquer 策略

在编程过程逃不了divide和conquer策略。

http://en.wikipedia.org/wiki/Divide_and_conquer_algorithms

我们来在ZeroMQ中执行一些并行任务。考虑一个场景,我们有一个producer生成一些随机数字,我们有workers通过牛顿方法计算这些数的平方根。然后我们有collector来搜集这么workers的结果。

以下是服务端代码:

#include <stdio.h>
#include <stdlib.h>
#include <unstd.h>
#include <string.h>
#include <sys/time.h>
#include <time.h>
#include "zmq.h"

int main(int argc, char* argv[])
{
    void* context = zmq_ctx_new();

    // This is the socket that we send message
    void* socket = zmq_socket(context, ZMQ_PUSH);
    zmq_bind(socket, "tcp://*:4040");

    // This is the socket that we send batch message
    void* connector = zmq_socket(context, ZMQ_PUSH);
    zmq_connect(connector, "tcp://localhost:5050");

    printf("Please press enter when workers are ready...");
    getchar();
    printf("Sending tasks to workers...\n");

    // The first message. It's also the signal start of batch.
    int length = strlen("-1");
    zmq_msg_t message;
    zmq_msg_init_size(&message, length);
    memcpy(zmq_msg_data(&message), "-1", length);
    zmq_msg_send(&message, connector, 0);
    zmq_msg_close(&message);

    // Generate some random numbers;
    srandom((unsigned) time(NULL));

    // Send the tasks.
    int count;
    int msec = 0;
    for (count = 0; count < 100; count++) {
        int load = (int) ((double)(100) * random() / RAND_MAX);
        msec += load;
        char string[10];
        sprintf(string, "%d", load);
    }
    printf("Total: %d msec\n", msec);
    sleep(1);

    zmq_close(connector);
    zmq_close(socket);
    zmq_ctx_destroy(context);

    return 0;
}

以下是worker代码:

#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "zmq.h"

double square(double x)
{
    return x * x;
}

double average(double x, double y)
{
    return (x + y) / 2.0;
}

double good_enough(double guess, double x)
{
    return abs(square(guess) - x) < 0.000001;
}

double improve(double guess, double x)
{
    return average(guess, x / guess);
}

double sqrt_inner(double guess, double x)
{
    if (good_enough(guess, x))
        return guess;
    else
        return sqrt_inner(improve(guess, x), x);
}

double newton_sqrt(double x) {
    return sqrt_inner(1.0, x);
}

int main(int arc, char* argv[])
{
    void* context = zmq_ctx_new();

    // Let's initialize a socket to receive messages
    void* receiver = zmq_socket(context, ZMQ_PULL);
    zmq_connect(receiver, "tcp://localhost:4040");

    // Let's initialize a socket to send the messages.
    void* sender = zmq_socket(context, ZMQ_PUSH);
    zmq_connect(sender, "tcp://localhost:5050");

    for(;;) {
        zmq_msg_t reply;
        zmq_msg_init(&reply);
        zmq_msg_recv(&reply, receiver, 0);

        int length = zmq_msg_size(&reply);
        char *msg = malloc(length+1);
        memcpy(msg, zmq_msg_data(&reply), length);
        zmq_msg_close(&reply);

        fflush(stdout);
        double val = atof(msg);
        printf("%.1f: %.1f\n", value, newton_sqrt(val));

        sleep(1);
        free(msg);

        zmq_msg_t message;
        char* ssend = "T";
        int t_length = strlen(ssend);
        zmq_msg_init_size(&messge, t_length);
        memcpy(zmq_msg_data(&message), ssend, t_length);
        zmq_msg_send(&message, sender, 0);
        zmq_msg_close(&message);

    }

    zmq_close(receiver);
    zmq_close(sender);
    zmq_ctx_detroy(context);

    return 0;
}

reciver代码:

#include <stdlib.h>
#include <string.h>
#include "zmq.h"

int main(int arc, char* argv[])
{
    void* context = zmq_ctx_new();
    void* receiver = zmq_socket(context, ZMQ_PULL);
    zmq_bind(receiver, "tcp://*:5050");

    // We receiver the first message and discard it since it's the
    // signal start of batch which is -1.
    zmq_msg_t reply;
    zmq_msg_init(&reply);
    zmq_msg_recv(&reply, receiver, 0);

    int length = zmq_msg_size(&reply);
    char *msg = malloc(length+1);
    memcpy(msg, zmq_msg_data(&reply), length);
    zmq_msg_close(&reply);
    free(msg);

    int count;
    for(count = 0; count < 100; count++) {
        zmq_msg_t reply;
        zmq_msg_init(&reply);
        zmq_msg_recv(&reply, receiver, 0);

        int length = zmq_msg_size(&reply);
        char *value = malloc(length+1);
        memcpy(value, zmq_msg_data(&reply), length);
        zmq_msg_close(&reply);
        free(value);

        if (cout / 10 == 0)
            printf("10 Tasks have been processed.\n");

        fflush(stdout);
    }

    zmq_close(receiver);
    zmq_ctx_detroy(context);

    return 0;
}

做了啥:

  • 我们需要在worker运行时确保消息发送同步
  • collector的PULL socket使用fair_queue调度来取得结果
  • server的PUSH socket来发送任务给workers
  • wokers下游连接collector,上游连接server,你可以指定多个workers

我们来深入了解一下上下游关系:

// Let's initialize a socket to receive messages.
void* receiver = zmq_socket(context, ZMQ_PULL);
zmq_connect(receiver, "tcp://localhost:4040");

ZMQ_PULL socket

当我们想从上游节点获取数据时使用ZMQ_PULL.ZMQ_PULL类型sockets在pipeline中获取上游节点消息,就如早期所提及,该工作fair-queue调度。

注意:zmq_send()不能用在ZMQ_PULL

ZMQ_PUSH socket

当我们想要于下游节点通信时使用ZMQ_PUSH.ZMQ_PUSH类型sockets用来在pipline中发送消息到下游。

ZMQ_PUSH从不丢弃消息,直到下游节点收到消息之前zmq_send()操作都会阻塞。

获取 ZeroMQ context

你一定意识到我们所有的示例都是从zmq_ctx_new()开始,ZeroMQ应用总是从先创建context开始。所有的sockets都应该使用contex放入单个进程中.ZeroMQ context是线程安全,你可以共享在多个线程中。

注意:如果ZeroMQ context无法创建,将会返回NULL。

虽然可以创建多个context,建议仅仅只创建一个。

销毁 ZeroMQ context

在应用程序末,你需要销毁创建的context,使用zmq_ctx_destroy()。一旦调用了zmq_ctx_destroy(),所有进程将返回错误码(ETERM),并且zmq_ctx_destroy()将在成功使用zmq_close()之前阻塞。

清理

当你在使用例如Python, Java 语言时,你无法担心内存管理,因为这些语言内置了清理内存机制。

不过使用C语言的时候,内存管理就属于你的责任了。以下是在结束应用程序后需要考虑的事情:

  • 正如前面所提及的,你需要使用zmq_ctx_detroy()来销毁context。如果还有sockets是打开的,zmq_ctx_destroy()将会一直等待,所以你需要先关闭sockets再销毁context
  • zmq_ctx_detroy()将会还有连接或者消息在队列中时进行等待。
  • 每当处理完消息,你都需要使用zmq_msg_close()立即关闭,否则可能会出现memory leaks,你可以这样想象,当你离开家时,关闭房门,你不会让他开着的。
  • 不开关太多sockets。如果你的应用存在这样的问题的话,你可能要考虑下如何重新设计应用架构。

你可能感兴趣的:(socket)