最近项目在使用linux平台c++做开发,redis用到了hiredis库。项目中用到redis list结构作为队列,生产者和消费者模式解耦异步任务:
生产者:
1. 将业务pb结构序列化为字符串 pbstr
2. 将字符串通过 rpush list-queue pbstr
消费者:
1. 从list-queue获取任务:lpop list-queue 获得字符串 pbstr
2. 将pbstr反向序列化为pb结构,执行业务逻辑
遇到问题:
消费者在步骤2中,获取到的pbstr反序列化为pb结构失败了!!!导致消费者后续的业务逻辑无法处理。
# 排查思路
1. 怀疑序列化问题,单独从业务层面对pb结构进行序列pbstr,然后在将pbstr反向序列化为pb结构,没有遇到问题,排除pb的问题。
2. 怀疑redis队列除了问题。有一下几个排查思路:
a. 系统多线程,比较难调试。
b. strace 对进程进行跟踪,比较容易,本文采用这种方法。
工具:strace -p [pid] -s 1024 -o s.out
图1是pb转为一个pbstr字符串:m_msgBody, 可见序列化后的长度是1029

图2是执行的redis命令,这里说一下redis命令的协议格式:
*[命令行参数个数]\r\n$[参数1长度]\r\n[参数1字符串]\r\n$[参数2长度]\r\n[参数2字符串]\r\n
例如:
RPUSH mylist Lippman
redis网络传输的命令传如下:
"*3\r\n$5\r\nRPUSH\r\n$6\r\nmylist\r\n$7\r\nLippman\r\n"

从图2看出,我们的1029长度的消息,莫名其妙变为了97!!!
结合代码层面的命令行拼接方式是基于字符串的fmt方式,怀疑是业务pb本身某些字段含有\0, 导致序列化后的字符串被截断了。

做个c预研字符串fmt遇到/0的实验:实验可以验证,
字符串 s = “abcded\n\0xxxxxxxxxxxxx”
s.length=21
s.size=21。因为C++类中的字符串长度是记录buffer使用的实际字节长度。
strlen(s.c_str())=7。 因为C语言以\0作为字符串结束符。
字符串通过printf("%s", s.c_str) 结果只打印了 abcded\n。因为遇到\0被截断了

## hiredis的两种命令行形式
方式1:redisvFormatCommand
从如下代码可看出,字符串的结束判定是\0
```
int redisvFormatCommand(char **target, const char *format, va_list ap) {const char *c = format;...while(*c != '\0') {if (*c != '%' || c[1] == '\0') {...switch(c[1]) {case 's': arg = va_arg(ap,char*); size = strlen(arg); // strlen 以\0判定字符串结束,所以如果字符串乱码,可能被判定为\0if (size > 0) newarg = sdscatlen(curarg,arg,size);break;case 'b': arg = va_arg(ap,char*); size = va_arg(ap,size_t);if (size > 0) newarg = sdscatlen(curarg,arg,size);break;case '%': newarg = sdscat(curarg,"%");break;...}```
方式2 redisFormatSdsCommandArgv
从如下代码可看出,字符串的拼接使用的是strcat+字符串实际长度。
```
/* Format a command according to the Redis protocol using an sds string and * sdscatfmt for the processing of arguments. This function takes the * number of arguments, an array with arguments and an array with their * lengths. If the latter is set to NULL, strlen will be used to compute the * argument lengths. */int redisFormatSdsCommandArgv(sds *target, int argc, const char **argv,const size_t *argvlen){ sds cmd;unsigned long long totlen;int j;size_t len;/* Abort on a NULL target */if (target == NULL)return -1;/* Calculate our total size */ totlen = 1+countDigits(argc)+2;for (j = 0; j < argc; j++) { len = argvlen ? argvlen[j] : strlen(argv[j]); // ------ 确定这个是否用的strlen totlen += bulklen(len);}/* Use an SDS string for command construction */ cmd = sdsempty();if (cmd == NULL)return -1;/* We already know how much storage we need */ cmd = sdsMakeRoomFor(cmd, totlen);if (cmd == NULL)return -1;/* Construct command */ cmd = sdscatfmt(cmd, "*%i\r\n", argc);for (j=0; j < argc; j++) { len = argvlen ? argvlen[j] : strlen(argv[j]); // --------确定这里是不是错用了strlen cmd = sdscatfmt(cmd, "$%T\r\n", len); cmd = sdscatlen(cmd, argv[j], len); cmd = sdscatlen(cmd, "\r\n", sizeof("\r\n")-1);}assert(sdslen(cmd)==totlen);*target = cmd;return totlen;}```
## 解决方法:
业务代码切换为第二种方式进行命令拼接,如下所示:

1。 业务在做redis命令拼接的时候,尽量避免%s形式,除非能保证字符串不会被\0截断。
2。业务代码抓包可以使用strace,方便快捷。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。