最近项目在使用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判定字符串结束,所以如果字符串乱码,可能被判定为\0
if
(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,方便快捷。