从零开始写redis客户端(deerlet-redis-client)之路——第一个纠结很久的问题,restore引发的血案

发布时间:2015-04-03 13:32:00作者:左潇龙阅读(1003 )评论(2)

    引言

      

      正如之前的一篇博文,LZ最近正在从零开始写一个redis的客户端,主要目的是为了更加深入的了解redis,当然了,LZ也希望deerlet客户端有一天能有一席之地。在写的过程当中,LZ遇到了一个非常奇葩的问题。虽然现在看起来是一个非常低级的错误,但是在未打开这个谜底之前,着实让LZ抓耳挠腮了一番,毕竟难者不会嘛。

      接下来,大家就来一起看下到底是什么问题吧。

      

    restore命令的奇葩之处

      

      刚开始写redis客户端时,LZ只支持了一些常用的命令,比如get,set。初次写这个客户端时,LZ采取的办法就是使用Socket和服务器进行TCP通信,传输的内容就是模拟在telnet端输入的命令。比如在telnet端使用set和get命令时,是如下的方式。

      因此在写deerlet时,LZ也是模仿的这种方式。比如在往服务器发送set命令时,LZ会采取以下的方式。

            if (command.name().indexOf(COMMAND_SEPARATOR) > 0) {
                String[] commands = command.name().split(COMMAND_SEPARATOR);
                outputStream.writeObject(commands[0]);
                outputStream.writeSpace();
                outputStream.writeObject(commands[1]);
            } else {
                outputStream.writeObject(command.name());
            }
            if (arguments != null) {
                for (int i = 0; i < arguments.length; i++) {
                    outputStream.writeSpace();
                    outputStream.writeObject(arguments[i]);
                }
            }
            outputStream.writeEnter();
            outputStream.flush();

      这段代码的逻辑很简单,也是LZ目前deerlet客户端当中统一的发送命令的方法。这段代码的逻辑如下。

      1,如果命令不包含下划线(_),则直接写入命令。否则的话,将下划线分割的两个命令依次写入,中间加一个空格("\r"),比如script_flush命令。

      2,写入命令后,如果参数不为空,则循环写入参数,每个参数用空格隔开。

      3,结束时,写入一个回车符("\n")。

      所以,如果是set命令的话,假设我们设置someKey的值为value,那么这段代码写入的实际内容就是如下这个字节数组。 

    ["s", "e", "t", "\r", "s", "o", "m", "e", "K", "e", "y", "\r", "\"", "v", "a", "l", "u", "e", "\"", ""]

      实践证明,这种方式支持很多redis的命令,比如get,set,flushall等等。这些命令,LZ的单元测试都完美通过。

      但是,问题来了,当LZ试图加入restore命令的支持时,竟然不管怎样都不行。这对于初次研究redis的LZ来说,真的是一个梦魇。因为尝试了各种办法,都无法让restore的单元测试通过,而且最要命的是,因为restore命令的参数中有字节数组,因此LZ无法在telnet端进行测试。

      

    求助于“专业人士”

      

      LZ最后实在没办法了,只能求助于“专业人士”。只不过不同的是,这个“专业人士”并不是某一个人,而是jedis。是的,LZ去翻阅了jedis的源码。

      jedis作为redis比较知名的java客户端,对于LZ来说,肯定是有一定的参考价值的。只不过为了保证deerlet是纯净的,因此LZ一开始没有去翻阅jedis的源码,避免思维受到影响,最终把deerlet写的和jedis如出一辙。

      不过现在遇到了这么奇葩的问题,而且迟迟没有解决,LZ也就顾不上那么多了。在深入研究了jedis的源码之后,LZ发现jedis发送命令的核心代码是以下这段代码。

            try {
                write(ASTERISK_BYTE);
                writeIntCrLf(args.length + 1);
                write(DOLLAR_BYTE);
                writeIntCrLf(command.length);
                write(command);
                writeCrLf();
                for (final byte[] arg : args) {
                    write(DOLLAR_BYTE);
                    writeIntCrLf(arg.length);
                    write(arg);
                    writeCrLf();
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }

      同样的,假设还是set命令,同样的参数,jedis发送的数据是以下这种形式的。

    *3
    $3
    set
    $7
    someKey
    $5
    value

      以上的数据,如果转换成字节数组的话,是如下的形式。

    ["*", "3", "\r", "", "$", "3", "\r", "", "s", "e", "t", "\r", "", "$", "7", "\r", "", "s", "o", "m", "e", "K", "e", "y", "\r", "", "$", "5", "\r", "", "v", "a", "l", "u", "e", "\r", ""]

      LZ这里对以上的数据格式做一个简单的介绍。星号(*)后面的3代表的是有三个参数。第一个美元符号($)后面的3是代表的set的长度,以此类推,第四行的美元符号后面的7代表的是someKey的长度。jedis就是把这么一个字符串发送给了服务器,让LZ惊讶的是,使用这种方式去进行restore命令的操作,服务器竟然正确的返回了响应。

      为什么这么一大串看似规整但又看似杂乱的命令,redis服务器会正确的返回结果呢?

      

    从问题的本质出发

      

      因为LZ实在想不通为什么redis会接受两种形式的命令,而且就算是redis接受,LZ也不明白为什么偏偏restore就不行。

      无奈之下,LZ只好从问题的本质出发。是的,LZ去翻阅了redis的源码。为此,LZ还专门在自己的Mac上面下载了xcode,学习了一番lldb,去尝试跟踪redis的服务器代码。

      经过一番折腾,LZ终于找到了根源。请看如下的代码,以下代码来自于networking.c。

    void processInputBuffer(redisClient *c) {
        server.current_client = c;
        /* Keep processing while there is something in the input buffer */
        while(sdslen(c->querybuf)) {
            /* Return if clients are paused. */
            if (!(c->flags & REDIS_SLAVE) && clientsArePaused()) break;
    
            /* Immediately abort if the client is in the middle of something. */
            if (c->flags & REDIS_BLOCKED) break;
    
            /* REDIS_CLOSE_AFTER_REPLY closes the connection once the reply is
             * written to the client. Make sure to not let the reply grow after
             * this flag has been set (i.e. don"t process more commands). */
            if (c->flags & REDIS_CLOSE_AFTER_REPLY) break;
    
            /* Determine request type when unknown. */
            if (!c->reqtype) {
                if (c->querybuf[0] == "*") {
                    c->reqtype = REDIS_REQ_MULTIBULK;
                } else {
                    c->reqtype = REDIS_REQ_INLINE;
                }
            }
    
            if (c->reqtype == REDIS_REQ_INLINE) {
                if (processInlineBuffer(c) != REDIS_OK) break;
            } else if (c->reqtype == REDIS_REQ_MULTIBULK) {
                if (processMultibulkBuffer(c) != REDIS_OK) break;
            } else {
                redisPanic("Unknown request type");
            }
    
            /* Multibulk processing could see a <= 0 length. */
            if (c->argc == 0) {
                resetClient(c);
            } else {
                /* Only reset the client when the command was executed. */
                if (processCommand(c) == REDIS_OK)
                    resetClient(c);
            }
        }
        server.current_client = NULL;
    }

      请注意循环当中的一句注释“Determine request type when unknown”,处在它下面的if判断,判断了命令的开头是否是星号(*)开头,并根据判断的结果,赋予了相应的类型——inline和multibulk。接下来,程序会根据命令的类型,分别调用相应的处理方法processInlineBuffer和processMultibulkBuffer。

      知道这个以后,LZ去翻阅了redis的官方文档,找到这样一句话,是用来解释inline格式的。

    Sometimes you have only telnet in your hands and you need to send a command to the Redis server. While the Redis protocol is simple 
    to implement it is not ideal to use in interactive sessions, and redis-cli may not always be available. For this reason Redis also
    accepts commands in a special way that is designed for humans, and is called the inline command format.

      这段话简单翻译过来就是:有时你可能只有telnet,并且你需要给redis服务器发送命令。redis的协议在交互式会话当中使用起来并不理想,而且redis-cli也不总是好用的。因此redis就专门为此设计了一套特殊的命令方式,称之为inline命令格式。

      总的来说,这下LZ总算是彻底明白了。inline协议,也就是deerlet客户端之前所使用的协议是redis为交互式会话提供的(比如telnet),主要目的是为了操作方便。如果要想做应用之间的交互,还是要使用multibulk协议,比如jedis在发送命令时,格式就是遵循multibulk协议的。如果大家想了解更多关于resp(即redis序列化协议)的内容,可以翻阅官方文档(地址:http://www.redis.io/topics/protocol),LZ这里就不再多做介绍了,只是起到一个抛砖引玉的作用。

     

    水落石出

      

      知道了以上内容,就不难去测试为什么restore命令不能使用了。我们可以猜想出来,之前restore单元测试失败的原因大概是因为dump后的字节数组中包含了空格字符。为了确认我们的猜测是正确的,LZ将dump命令执行后的数组在程序中打印了出来,如下。

    ["", "	", "T", "e", "s", "t", "V", "a", "l", "u", "e", "", "", "(", "B", "ᄁ", "ヨ", "ᅩ", "ム", "ᅧ", "!"]

      可以看到,第二个字符是一个空格字符,因此在使用inline格式发送时,会导致redis服务器进行错误的解析,它会把一个参数当作两个参数去解析,最终导致参数的数量不符合命令要求。

      这里也能够看出来,inline协议的好处在于方便简单,但是坏处也很明显,就是在某些情况下会导致出错,比如当传输的参数内容当中包含空格时就会导致redis解析失败。

      

    小结

      

      经过这一番问题的查找,可以看出,翻阅源码(如果有的话)是最有效直接的问题解决方式。LZ也建议大家,在遇到问题的时候,不要着急着百度,尝试去翻阅一下源码,这样能够帮助你对遇到的问题有一个比较深入的了解,以后再遇到的话,你将会游刃有余。

      好了,本文就到此结束,感谢大家的收看,如果deerlet再遇到问题的话,LZ再来与大家一起分享,也非常欢迎有志之士为deerlet贡献源码。



    版权声明:本文版权归作者(左潇龙)所有,欢迎转载。但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

    1
    精彩
    0
    感动
    0
    搞笑
    0
    开心
    0
    愤怒
    0
    无聊
    1
    灌水
    0
    惊讶
#1楼     时间:2015-04-03 13:47:00      来源:淮左
先收藏,晚上看
#2楼     时间:2015-06-01 17:31:35      来源:aaaaaaaaaaaaaaaaaaaaaaaa
fffffffffffff
发表评论

站内搜索