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>
不过这个问题可以通过把数据写入文件,通过命令行的方式解决:
[root@b8bd4b93c6cb src]# ./redis-cli SET test `cat ~/tmp.txt`
OK
[root@b8bd4b93c6cb src]# ./redis-cli STRLEN test
(integer) 4100
但是,如果文件中有转义字符的话,上面的方式就无法解决了,例如:
[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"
可以看到,文件中有转义字符的话,最终写入 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"
虽然 redis-cli 可以正常写入转义字符,但是又有 4096 字符数的限制,因此我们需要解决一下这个问题。
相关代码在 redis-cli.c 文件中,先看下 main 函数,核心逻辑:
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));
}
}
思路是限制了 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);
}
可以看到,读取终端里用户输入的数据是在 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);
}
}
可以看到,这里有一个宏 LINENOISE_MAX_LINE,值是 4096,因此在 redis-cli 的交互模式中,所有字符加起来最长是 4095(还有一个是 \0 结尾字符)。
直观的解决方案就是修改 4096 为更大的数字,比如 8192。修改完重新编辑即可,这样可以解决大部分问题。但是如果我们需要写入大于 8192 字符的数据呢,不可能一直修改这个宏定义,还是要利用上文件的能力。
注意,宏定义在 linenoise.c 文件中,需要重新编译 linenoise.c 文件,再编译 redis-cli.c 文件才可以。
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;
}
为了避免影响 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;
第二步,在 main 函数中给变量赋初值:
int main(int argc, char **argv) {
int firstarg;
config.hostip = sdsnew("127.0.0.1");
config.hostport = 6379;
config.input_file_escape = 0; // 新变量赋初值
// ...
}
第三步,解析参数,在函数 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;
}
第四步,非交互处理函数 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;
}
第五步,这里新增加了一个函数 sdscatescape(deps/hiredis/sds.c 文件中),用于对文件中的 \ 以及之后的字符进行转义相关处理,实现如下:
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;
}
至此,第二个方案代码就完成了,编译 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"