hiredis接口分析2:基于libev的异步调用

上一篇文章我们分析了hiredis同步接口调用的原理,在本文我们将进一步分析下异步调用的方法,该方法主要用于处理多客户端并发连接读写redis数据库的情况,hiredis继承了libev库,配合libev使用可以高效地实现并发异步操作。示例代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>

#include <hiredis/hiredis.h>
#include <hiredis/async.h>
#include <adapters/libev.h>

void getCallback(redisAsyncContext *c, void *r, void *privdata) {
    redisReply *reply = r;
    if (reply == NULL) return;
    printf("argv[%s]: %s\n", (char*)privdata, reply->str);

    /* Disconnect after receiving the reply to GET */
    redisAsyncDisconnect(c);
}

void connectCallback(const redisAsyncContext *c, int status) {
    if (status != REDIS_OK) {
        printf("Error: %s\n", c->errstr);
        return;
    }
    printf("Connected...\n");
}

void disconnectCallback(const redisAsyncContext *c, int status) {
    if (status != REDIS_OK) {
        printf("Error: %s\n", c->errstr);
        return;
    }
    printf("Disconnected...\n");
}

int main (int argc, char **argv) {
    signal(SIGPIPE, SIG_IGN);

    redisAsyncContext *c = redisAsyncConnect("127.0.0.1", 6379);
    if (c->err) {
        /* Let *c leak for now... */
        printf("Error: %s\n", c->errstr);
        return 1;
    }

    redisLibevAttach(EV_DEFAULT_ c);
    redisAsyncSetConnectCallback(c,connectCallback);
    redisAsyncSetDisconnectCallback(c,disconnectCallback);
    redisAsyncCommand(c, NULL, NULL, "SET key %b", argv[argc-1], strlen(argv[argc-1]));
    redisAsyncCommand(c, getCallback, (char*)"end-1", "GET key");
    ev_loop(EV_DEFAULT_ 0);
    return 0;
}

进入main函数,第一行代码signal(SIGPIPE, SIG_IGN)的作用是,当服务器close一个连接时,若client端接着发数据。根据TCP 协议的规定,会收到一个RST响应,client再往这个服务器发送数据时,系统会发出一个SIGPIPE信号给进程,告诉进程这个连接已经断开了,不要再写了。根据信号的默认处理规则SIGPIPE信号的默认执行动作是terminate(终止、退出),所以client会退出。若不想客户端退出可以把SIGPIPE设为SIG_IGN ,如:    signal(SIGPIPE,SIG_IGN);  这时SIGPIPE交给了系统处理。服务器采用了fork的话,要收集垃圾进程,防止僵尸进程的产生,可以这样处理:signal(SIGCHLD,SIG_IGN); 交给系统init去回收。这里子进程就不会产生僵尸进程了。  

接下来与同步接口类似这里调用redisAsyncConnect来连接服务器,redisAsyncContext的结构如下:

typedef struct redisAsyncContext {
    /* Hold the regular context, so it can be realloc'ed. */
    redisContext c;

    /* Setup error flags so they can be used directly. */
    int err;
    char *errstr;

    /* Not used by hiredis */
    void *data;

    /* Event library data and hooks */
    struct {
        void *data;

        /* Hooks that are called when the library expects to start
         * reading/writing. These functions should be idempotent. */
        void (*addRead)(void *privdata);
        void (*delRead)(void *privdata);
        void (*addWrite)(void *privdata);
        void (*delWrite)(void *privdata);
        void (*cleanup)(void *privdata);
    } ev;

    /* Called when either the connection is terminated due to an error or per
     * user request. The status is set accordingly (REDIS_OK, REDIS_ERR). */
    redisDisconnectCallback *onDisconnect;

    /* Called when the first write event was received. */
    redisConnectCallback *onConnect;

    /* Regular command callbacks */
    redisCallbackList replies;

    /* Subscription callbacks */
    struct {
        redisCallbackList invalid;
        struct dict *channels;
        struct dict *patterns;
    } sub;
} redisAsyncContext;

他是一个用于维护异步链接中各种状态的上下文结构,其第一个成员为redisContext c,即同步连接中的上下文结构。结构体ev中包含了当Hiredis异步API与事件库(libev,libevent, Redis ev)一起工作时,用于注册和删除读写事件的函数;回调函数onDisconnect表示断链时会调用的函数,该属性可以通过redisAsyncSetDisconnectCallback函数设置;回调函数onConnect表示TCP建链成功或失败之后会调用的函数,该属性可以通过redisAsyncSetConnectCallback函数设置;replies属性是一个redisCallbackList结构,也就是由回调结构redisCallback组成的单链表。当发送普通命令时,会依次将该命令对应的回调结构追加到链表中,当Redis服务器回复普通命令时,会依次调用该链表中的每个redisCallback结构中的回调函数; 结构体sub用于处理订阅模式,其中的字典channels,以频道名为key,以回调结构redisCallback为value。当客户端使用Hiredis异步API发送”subscribe”命令后,服务器产生回复时,就会根据回复信息中的频道名查询字典channels,找到对应的回调结构,调用其中的回调函数。字典patterns与channels类似,只不过它用于”psubscirbe”命令,其中的key是频道名模式;回调链表invalid,当客户端处于订阅模式下,服务器发来了意想不到的回复时,会依次调用该链表中,每个回调结构中的回调函数。

     redisAsyncConnect代码如下:

redisAsyncContext *redisAsyncConnect(const char *ip, int port) {
    redisContext *c;
    redisAsyncContext *ac;

    c = redisConnectNonBlock(ip,port);
    if (c == NULL)
        return NULL;

    ac = redisAsyncInitialize(c);
    if (ac == NULL) {
        redisFree(c);
        return NULL;
    }

    __redisAsyncCopyError(ac);
    return ac;
}

首先调用redisConnectNonBlock创建非阻塞socket并连接到服务端,随后通过redisAsyncInitialize初始化redisAsyncContext 数据结构。接下来执行redisLibevAttach(EV_DEFAULT_ c),即redisLibevAttach(ev_default_loop (0),EV_DEFAULT_ c)

static int redisLibevAttach(EV_P_ redisAsyncContext *ac) {
    redisContext *c = &(ac->c);
    redisLibevEvents *e;

    /* Nothing should be attached when something is already attached */
    if (ac->ev.data != NULL)
        return REDIS_ERR;

    /* Create container for context and r/w events */
    e = (redisLibevEvents*)hi_malloc(sizeof(*e));
    e->context = ac;
#if EV_MULTIPLICITY
    e->loop = loop;
#else
    e->loop = NULL;
#endif
    e->reading = e->writing = 0;
    e->rev.data = e;
    e->wev.data = e;

    /* Register functions to start/stop listening for events */
    ac->ev.addRead = redisLibevAddRead;
    ac->ev.delRead = redisLibevDelRead;
    ac->ev.addWrite = redisLibevAddWrite;
    ac->ev.delWrite = redisLibevDelWrite;
    ac->ev.cleanup = redisLibevCleanup;
    ac->ev.data = e;

    /* Initialize read/write events */
    ev_io_init(&e->rev,redisLibevReadEvent,c->fd,EV_READ);
    ev_io_init(&e->wev,redisLibevWriteEvent,c->fd,EV_WRITE);
    return REDIS_OK;
}

首先定义redisLibevEvents指针e,分配内存,redisLibevEvents定义如下:

typedef struct redisLibevEvents {
    redisAsyncContext *context;
    struct ev_loop *loop;
    int reading, writing;
    ev_io rev, wev;
} redisLibevEvents;

可以看到其中包括异步连接结构的上下文redisAsyncContext *context,还有libev的主循环结构体loop,以及io watcher,关于libev的介绍可以参考https://blog.csdn.net/fangfanglovezhou/article/details/104923064

redisLibevAttach函数中首先定义了redisLibevEvents  *e,并为其分配内存,之后其成员e指向之前成功连接返回的结构体描述符c,其loop通过ev_default_loop (0)初始化,随后会有一些函数和变量的初始化。随后调用:

    ev_io_init(&e->rev,redisLibevReadEvent,c->fd,EV_READ);
    ev_io_init(&e->wev,redisLibevWriteEvent,c->fd,EV_WRITE);

执行异步连接的读初始化和写初始化,对应回调函数分别为redisLibevReadEvent和redisLibevWriteEvent。

接下来执行函数redisAsyncSetConnectCallback(c,connectCallback);实际上是将redisAsyncContext* c的成员onConnect指向connectCallback:

int redisAsyncSetConnectCallback(redisAsyncContext *ac, redisConnectCallback *fn) {
    if (ac->onConnect == NULL) {
        ac->onConnect = fn;

        /* The common way to detect an established connection is to wait for
         * the first write event to be fired. This assumes the related event
         * library functions are already set. */
        _EL_ADD_WRITE(ac);
        return REDIS_OK;
    }
    return REDIS_ERR;
}

然后调用_EL_ADD_WRITE(ac); 即函数c->ev.addWrite(c->ev.data),即调用redisLibevAddWrite(c->ev.data),而data在前面初始化中指向了redisLibevEvents *e。

static void redisLibevAddWrite(void *privdata) {
    redisLibevEvents *e = (redisLibevEvents*)privdata;
    struct ev_loop *loop = e->loop;
    ((void)loop);
    if (!e->writing) {
        e->writing = 1;
        ev_io_start(EV_A_ &e->wev);
    }
}

故redisAsyncSetConnectCallback(c,connectCallback)作用是,指明OnConnect回调,然后将watcher e->wev加入e->loop,对应的回调函数为redisLibevWriteEvent:

static void redisLibevWriteEvent(EV_P_ ev_io *watcher, int revents) {
#if EV_MULTIPLICITY
    ((void)loop);
#endif
    ((void)revents);

    redisLibevEvents *e = (redisLibevEvents*)watcher->data;
    redisAsyncHandleWrite(e->context);
}

同样,watcher->data也指向redisLibevEvents *e,实际调用redisAsyncHandleWrite:

void redisAsyncHandleWrite(redisAsyncContext *ac) {
    redisContext *c = &(ac->c);
    int done = 0;

    if (!(c->flags & REDIS_CONNECTED)) {
        /* Abort connect was not successful. */
        if (__redisAsyncHandleConnect(ac) != REDIS_OK)
            return;
        /* Try again later when the context is still not connected. */
        if (!(c->flags & REDIS_CONNECTED))
            return;
    }

    if (redisBufferWrite(c,&done) == REDIS_ERR) {
        __redisAsyncDisconnect(ac);
    } else {
        /* Continue writing when not done, stop writing otherwise */
        if (!done)
            _EL_ADD_WRITE(ac);
        else
            _EL_DEL_WRITE(ac);

        /* Always schedule reads after writes */
        _EL_ADD_READ(ac);
    }
}

若连接成功c->flags与REDIS_CONNECTED对应位为1,故会执行redisBufferWrite(c,&done),此函数在上一章分析过,即将相关内容写入。也就是说,example中首先以非阻塞的方式connect服务端,连接成功后把写入监测(watcher)加入事件循环loop,一旦事务循环启动并且连接成功,写入缓存为空就可以写入了。写入完成后,会调用_EL_DEL_WRITE,调用c->redisLibevDelWrite,是即调用ev_io_stop(EV_A_ &e->wev),将写事件监控从loop中去除,下次循环将不再调用写回调。最后调用_EL_ADD_READ,调用c->redisLibevAddRead,是即调用ev_io_start(EV_A_ &e->rev);将读监控e->rev加入loop,也就是说每次写操作后都要添加读任务,因为发送指令后,都有回复,需要接收回复。e->rev对应的回调是

void redisAsyncHandleRead(redisAsyncContext *ac) {
    redisContext *c = &(ac->c);

    if (!(c->flags & REDIS_CONNECTED)) {
        /* Abort connect was not successful. */
        if (__redisAsyncHandleConnect(ac) != REDIS_OK)
            return;
        /* Try again later when the context is still not connected. */
        if (!(c->flags & REDIS_CONNECTED))
            return;
    }

    if (redisBufferRead(c) == REDIS_ERR) {
        __redisAsyncDisconnect(ac);
    } else {
        /* Always re-schedule reads */
        _EL_ADD_READ(ac);
        redisProcessCallbacks(ac);
    }
}

在正常连接的情况下,会调用redisProcessCallbacks(ac),处理相应消息响应的回调函数。

同理, redisAsyncSetDisconnectCallback,会将c的成员onDisconnect指向disconnectCallback,但没有后续的操作。

接下来就可以通过redisAsyncCommand函数发送指令了:

int redisAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *format, ...) {
    va_list ap;
    int status;
    va_start(ap,format);
    status = redisvAsyncCommand(ac,fn,privdata,format,ap);
    va_end(ap);
    return status;
}

int redisvAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *format, va_list ap) {
    char *cmd;
    int len;
    int status;
    len = redisvFormatCommand(&cmd,format,ap);

    /* We don't want to pass -1 or -2 to future functions as a length. */
    if (len < 0)
        return REDIS_ERR;

    status = __redisAsyncCommand(ac,fn,privdata,cmd,len);
    free(cmd);
    return status;
}

 

redisvFormatCommand解析用户输入的命令,转换成统一请求协议格式的字符串cmd,接着调用status =  __redisAsyncCommand(ac,fn,privdata,cmd,len),将cmd发送给Redis,并且记录相应的回调函数

static int __redisAsyncCommand(redisAsyncContext *ac, redisCallbackFn *fn, void *privdata, const char *cmd, size_t len) {
    redisContext *c = &(ac->c);
    redisCallback cb;
    struct dict *cbdict;
    dictEntry *de;
    redisCallback *existcb;
    int pvariant, hasnext;
    const char *cstr, *astr;
    size_t clen, alen;
    const char *p;
    sds sname;
    int ret;

    /* Don't accept new commands when the connection is about to be closed. */
    if (c->flags & (REDIS_DISCONNECTING | REDIS_FREEING)) return REDIS_ERR;

    /* Setup callback */
    cb.fn = fn;
    cb.privdata = privdata;
    cb.pending_subs = 1;

    /* Find out which command will be appended. */
    p = nextArgument(cmd,&cstr,&clen);
    assert(p != NULL);
    hasnext = (p[0] == '$');
    pvariant = (tolower(cstr[0]) == 'p') ? 1 : 0;
    cstr += pvariant;
    clen -= pvariant;

    if (hasnext && strncasecmp(cstr,"subscribe\r\n",11) == 0) {
        c->flags |= REDIS_SUBSCRIBED;

        /* Add every channel/pattern to the list of subscription callbacks. */
        while ((p = nextArgument(p,&astr,&alen)) != NULL) {
            sname = sdsnewlen(astr,alen);
            if (pvariant)
                cbdict = ac->sub.patterns;
            else
                cbdict = ac->sub.channels;

            de = dictFind(cbdict,sname);

            if (de != NULL) {
                existcb = dictGetEntryVal(de);
                cb.pending_subs = existcb->pending_subs + 1;
            }

            ret = dictReplace(cbdict,sname,&cb);

            if (ret == 0) sdsfree(sname);
        }
    } else if (strncasecmp(cstr,"unsubscribe\r\n",13) == 0) {
        /* It is only useful to call (P)UNSUBSCRIBE when the context is
         * subscribed to one or more channels or patterns. */
        if (!(c->flags & REDIS_SUBSCRIBED)) return REDIS_ERR;

        /* (P)UNSUBSCRIBE does not have its own response: every channel or
         * pattern that is unsubscribed will receive a message. This means we
         * should not append a callback function for this command. */
     } else if(strncasecmp(cstr,"monitor\r\n",9) == 0) {
         /* Set monitor flag and push callback */
         c->flags |= REDIS_MONITORING;
         __redisPushCallback(&ac->replies,&cb);
    } else {
        if (c->flags & REDIS_SUBSCRIBED)
            /* This will likely result in an error reply, but it needs to be
             * received and passed to the callback. */
            __redisPushCallback(&ac->sub.invalid,&cb);
        else
            __redisPushCallback(&ac->replies,&cb);
    }

    __redisAppendCommand(c,cmd,len);

    /* Always schedule a write when the write buffer is non-empty */
    _EL_ADD_WRITE(ac);

    return REDIS_OK;
}

在函数中,首先将回调函数fn,以及用户提供的该回调函数的私有参数privdata,封装到redisCallback回调结构的cb中。当然,用户如果没有提供回调函数和参数,则cb中相应的属性为NULL。然后解析用户输入命令,根据不同的命令,将回调函数追加到不同的链表或字典中: 如果用户输入命令为"subscribe"或者"psubscribe",首先将REDIS_SUBSCRIBED标记增加到上下文标志中,表示当前客户端进入订阅模式;然后循环解析命令中的后续参数,这些参数表示订阅的频道名("subscribe"),或者订阅的频道名的匹配模式("psubscribe")。以这些频道名或匹配模式为key,以回调结构cb为value,插入到异步上下文的字典ac->sub.patterns或ac->sub.channels中。如果用户输入命令为"unsubscribe",这种情况无需记录回调函数。但是该命令只有客户端处于订阅模式下才有效,否则直接返回REDIS_ERR;  如果用户输入命令为"monitor",则将REDIS_MONITORING标记增加到上下文标志位中,表示客户端进入monitor模式,然后调用__redisPushCallback,将回调结构cb追加到上下文的回调链表ac->replies中;如果用户输入的是其他命令,则若当前客户端处于订阅模式,因处于订阅模式中,客户端只能发送”subscribe/psubscribe/unsubscribe/punsubscribe”命令,走到这一步,说明客户端发送了其他命令,因此将回调结构cb追加到链表ac->sub.invalid中;其他情况,将回调结构cb追加到链表ac->replies中;

最后调用__redisAppendCommand(c,cmd,len);记录完回调函数之后,剩下的,就是调用__redisAppendCommand,将cmd追加到上下文的输出缓存中。然后调用_EL_ADD_WRITE,注册可写事件。对于使用Redis的ae事件库的客户端来说,该宏定义实际上就是调用redisAeAddWrite函数,可写事件的回调函数是redisAeWriteEvent,该函数调用redisAsyncHandleWrite实现。前面已经做过分析,回复消息将通过事务循环redisAsyncHandleRead处理。

最后需要注意一点,只要当前是连接状态,redisAsyncCommand的返回值就是REDIS_OK,因为其为异步操作,最后操作的返回值需要相应的回调函数才可以进行处理。

最后, ev_loop(EV_DEFAULT_ 0);会开启loop循环。

运行程序,打印结果如下:

Connected...
argv[end-1]: ./example-libev1
Disconnected...

可以看到,首先调用异步连接,这一步还没有启动事务循环,也没有注册接收函数和发送函数,直接调用异步连接,连接到服务器,之后调用redisLibevAttach,初始化监听器和相应的回调,随后调用redisAsyncSetConnectCallback绑定监听器到事务循环。随后的redisAsyncCommand指令的执行以及相关的回调都是在事务循环中实现的,在最后执行ev_loop(EV_DEFAULT_ 0);才会开启事件循环执行相应的收费指令和回调。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值