• redis-cli写入超长转义字符串问题


    一、背景


    redis-cli 终端默认只能输入 4095 个字符,如果需要更长的 value 是没有办法的,比如下面的命令(终端里已经无法敲入字符了,因为 SET 命令和 key 的长度,导致 value 只有 4086 个字节):

    127.0.0.1:6379> SET test 01234567890123456......
    OK
    127.0.0.1:6379> STRLEN test
    (integer) 4086
    127.0.0.1:6379>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    不过这个问题可以通过把数据写入文件,通过命令行的方式解决:

    [root@b8bd4b93c6cb src]# ./redis-cli SET test `cat ~/tmp.txt`
    OK
    [root@b8bd4b93c6cb src]# ./redis-cli STRLEN test
    (integer) 4100
    
    • 1
    • 2
    • 3
    • 4

    但是,如果文件中有转义字符的话,上面的方式就无法解决了,例如:

    [root@b8bd4b93c6cb src]# cat ~/tmp.txt
    0123456789\test
    [root@b8bd4b93c6cb src]# ./redis-cli SET test `cat ~/tmp.txt`
    OK
    [root@b8bd4b93c6cb src]# ./redis-cli GET test
    "0123456789\\test"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    可以看到,文件中有转义字符的话,最终写入 redis 中会再加一个转义符号 \,导致最终数据和我们想要的不一样。不过 redis-cli 可以写入转义字符:

    [root@b8bd4b93c6cb src]# ./redis-cli
    127.0.0.1:6379> SET test "0123456789\test"
    OK
    127.0.0.1:6379> GET test
    "0123456789\test"
    
    • 1
    • 2
    • 3
    • 4
    • 5

    虽然 redis-cli 可以正常写入转义字符,但是又有 4096 字符数的限制,因此我们需要解决一下这个问题。

    二、解决方案


    相关代码在 redis-cli.c 文件中,先看下 main 函数,核心逻辑:

    • 初始化配置项,根据 redis-cli 后面参数可以设置相关参数;
    • 根据参数做相应的特殊处理;
    • 没有输入参数,则进入终端交互模式,循环执行 请求/响应 模式;
      • 上面的 可以正常写转义字符串的示例,不过有 4096 大小的限制。
    • 非终端交互模式,直接根据 redis-cli 后面的参数,直接进行处理。
      • 上面的 命令行写文件的示例,不过无法写入原始转义字符串。
    int main(int argc, char **argv) {
        // 配置项初始化
        config.hostip = sdsnew("127.0.0.1");
        config.hostport = 6379;
        
        // ...
        
    	/* Find big keys */
        if (config.bigkeys) {
            if (cliConnect(0) == REDIS_ERR) exit(1);
            findBigKeys(0, 0);
        }
    
        // 如果没有参数, 进入交互模式, 默认场景
        if (argc == 0 && !config.eval) {
            signal(SIGPIPE, SIG_IGN);
            cliConnect(0);	// 建立和redis-server的连接
            repl();	// 重复执行客户端输入的命令函数
        }
    
        if (cliConnect(0) != REDIS_OK) exit(1);
        
        if (config.eval) {
            return evalMode(argc,argv);
        } else {
            // 非终端交互模式, 直接读文件 或者 跟命令模式
            return noninteractive(argc,convertToSds(argc,argv));
        }
    }
    
    • 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
    1、扩大 redis-cli 中 4096 限制

    思路是限制了 4096,直接在代码里将对应数字限制改大。redis-cli 终端交互函数为 repl,看下代码:

    static void repl(void) {
        // 初始化相关帮助信息
        cliInitHelp();
    
        // 如果是tty终端, 读取历史命令文件, 方便用户上下找历史命令
        if (isatty(fileno(stdin))) {
            historyfile = getDotfilePath(REDIS_CLI_HISTFILE_ENV,REDIS_CLI_HISTFILE_DEFAULT);
            linenoiseHistoryLoad(historyfile);
        }
    
        // 循环执行client输入的命令(如果需要扩大最大长度 需要重新编译linenoise.c)
        while((line = linenoise(context ? config.prompt : "not connected> ")) != NULL) {
            if (line[0] != '\0') {
                // ...  执行redis命令
                issueCommandRepeat(argc-skipargs, argv+skipargs, repeat);
    
            }
            // 释放资源
            linenoiseFree(line);
        }
    
        exit(0);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    可以看到,读取终端里用户输入的数据是在 linenoise 函数中实现的。

    #define LINENOISE_MAX_LINE 4096
    
    char *linenoise(const char *prompt) {
        char buf[LINENOISE_MAX_LINE];
    
        if (!isatty(STDIN_FILENO)) {
            // 从文件中读取数据, 不做任何限制
            return linenoiseNoTTY();
            
        } else if (isUnsupportedTerm()) {
            // 不支持的终端 ...
            
        } else {
            count = linenoiseRaw(buf,LINENOISE_MAX_LINE,prompt);
            if (count == -1) return NULL;
            return strdup(buf);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    可以看到,这里有一个宏 LINENOISE_MAX_LINE,值是 4096,因此在 redis-cli 的交互模式中,所有字符加起来最长是 4095(还有一个是 \0 结尾字符)。

    直观的解决方案就是修改 4096 为更大的数字,比如 8192。修改完重新编辑即可,这样可以解决大部分问题。但是如果我们需要写入大于 8192 字符的数据呢,不可能一直修改这个宏定义,还是要利用上文件的能力。

    注意,宏定义在 linenoise.c 文件中,需要重新编译 linenoise.c 文件,再编译 redis-cli.c 文件才可以。

    2、文件数据支持转义

    redis-cli 非交互模式的实现是函数 noninteractive,函数根据 stdinarg 参数有两个分支,一个是从 stdin 中读取最后一个参数;另一个是参数已经足够,直接执行相关 redis 命令即可。

    比如命令:./redis-cli SET test `cat ~/tmp.txt`,文件 tmp 中的数据就是 argv 数组中的最后一个参数,我们需要针对 argv 数组做转义字符串的特殊处理。

    static int noninteractive(int argc, char **argv) {
        if (config.stdinarg) {
            // 将最后一个stdin参数 添加到argv数组中
            argv = zrealloc(argv, (argc+1)*sizeof(char*));
            argv[argc] = readArgFromStdin();
            retval = issueCommand(argc+1, argv);
            
        } else {
            retval = issueCommand(argc, argv);
        }
        return retval;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    为了避免影响 redis-cli 的原有功能,给 redis-cli 增加一个参数来实现此功能。实现思路:给 redis-cli 增加新参数 --input-file-escape,识别有此参数时,针对 argv 数组中的所有数据,进行转义处理。

    第一步,增加 --input-file-escape 参数,全局 config 结构体增加对应变量:

    static struct config {
        char *hostip;
        int hostport;
        // ...
        int input_file_escape;	// 新增变量
    } config;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    第二步,在 main 函数中给变量赋初值:

    int main(int argc, char **argv) {
        int firstarg;
    
        config.hostip = sdsnew("127.0.0.1");
        config.hostport = 6379;
        config.input_file_escape = 0;	// 新变量赋初值
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    第三步,解析参数,在函数 parseOptions 中增加对应参数的解析:

    static int parseOptions(int argc, char **argv) {
        int i;
        for (i = 1; i < argc; i++) {
            if (!strcmp(argv[i],"-h") && !lastarg) {
                sdsfree(config.hostip);
                config.hostip = sdsnew(argv[++i]);
            } else if (!strcmp(argv[i],"-h") && lastarg) {
                usage();
            } else if (!strcmp(argv[i], "--input-file-escape")) {	// 新增变量解析
                config.input_file_escape = 1;
            }
            // ...
        }
    
        return i;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    第四步,非交互处理函数 noninteractive 中,对输入的参数数组 argv 进行转义处理:

    static int noninteractive(int argc, char **argv) {
        if (config.stdinarg) {
            // ...
            
        } else {
            // 下面使用的是 hiredis里的sds.c文件
            if (config.input_file_escape) {
                // 循环对参数内的\进行转义处理
                for (int i=0; i<argc; ++i) {
                    sds arg = sdsempty();
                    argv[i] = sdscatescape(arg,argv[i],strlen(argv[i]));
                }
            }
    
            retval = issueCommand(argc, argv);
        }
        return retval;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    第五步,这里新增加了一个函数 sdscatescape(deps/hiredis/sds.c 文件中),用于对文件中的 \ 以及之后的字符进行转义相关处理,实现如下:

    • 函数参考 sdscatrepr 函数实现。
    sds sdscatescape(sds s, const char *p, size_t len) {
        while(len--) {
            switch(*p) {
            case '\\':
                len -= 1;
                if (len <= 0)
                    break;
                p++;
    
                switch (*p) {
                case '\\': s = sdscatlen(s,"\\",1); break;
                case '"': s = sdscatlen(s,"\"",1); break;
                case 'n': s = sdscatlen(s,"\n",1); break;
                case 'r': s = sdscatlen(s,"\r",1); break;
                case 't': s = sdscatlen(s,"\t",1); break;
                case 'a': s = sdscatlen(s,"\a",1); break;
                case 'b': s = sdscatlen(s,"\b",1); break;
                case 'x': 
                    len -= 2;
                    if (len <= 0)
                        break;
                    // 16进制 后两位转化为16进制的一个字节
                    if (is_hex_digit(*(p+1)) && is_hex_digit(*(p+2))) {
                        unsigned char byte;
    
                        byte = hex_digit_to_int(*(p+1))*16 + hex_digit_to_int(*(p+2));
                        s = sdscatlen(s,(char*)&byte,1);
                    }
                    p += 2;
                    break;
                default:
                    break;
                }
    
                break;
            default:
                s = sdscatprintf(s,"%c",*p);
                break;
            }
            p++;
        }
        return s;
    }
    
    • 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

    至此,第二个方案代码就完成了,编译 redis-cli 后,直接执行即可,效果如下:

    因为修改了 deps/hiredis/sds.c 文件,需要先编译 deps 目录下的文件,再编译 redis-cli。

    [root@b8bd4b93c6cb src]# cat ~/tmp.txt
    0123456789\test
    [root@b8bd4b93c6cb src]# ./redis-cli SET test `cat ~/tmp.txt`
    OK
    [root@b8bd4b93c6cb src]# ./redis-cli GET test
    "0123456789\\test"
    [root@b8bd4b93c6cb src]# ./redis-cli --input-file-escape SET test `cat ~/tmp.txt`
    OK
    [root@b8bd4b93c6cb src]# ./redis-cli GET test
    "0123456789\test"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
  • 相关阅读:
    【单线图的系统级微电网仿真】基于 PQ 的可再生能源和柴油发电机组微电网仿真(Simulink)
    【深度学习实验】注意力机制(四):点积注意力与缩放点积注意力之比较
    鹰潭恒温恒湿实验室设计方案总结
    2023-09-06力扣每日一题-摆烂暴力
    脑梗死和脑出血有什么关系吗?
    python 时间加法 输出t分钟后的时间
    深度学习踩坑笔记:载入内存,数据分配与重启问题,安装R
    Keeplived练习
    在Ubuntu上为ARM 8处理器安装Python 3.10.4虚拟环境指南
    LeetCode/LintCode 题解丨一周爆刷字符串:简化路径
  • 原文地址:https://blog.csdn.net/LT_lover/article/details/126296599