菜鸟笔记
提升您的技术认知

记一次redis命令异常:参数截断-ag真人游戏

最近项目在使用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

```

  1. int redisvformatcommand(char **target, const char *format, va_list ap) {
  2. const char *c = format;
  3. ...
  4. while(*c != '\0') {
  5. if (*c != '%' || c[1] == '\0') {
  6. ...
  7. switch(c[1]) {
  8. case 's':
  9. arg = va_arg(ap,char*);
  10. size = strlen(arg); // strlen 以\0判定字符串结束,所以如果字符串乱码,可能被判定为\0
  11. if (size > 0)
  12. newarg = sdscatlen(curarg,arg,size);
  13. break;
  14. case 'b':
  15. arg = va_arg(ap,char*);
  16. size = va_arg(ap,size_t);
  17. if (size > 0)
  18. newarg = sdscatlen(curarg,arg,size);
  19. break;
  20. case '%':
  21. newarg = sdscat(curarg,"%");
  22. break;
  23. ...
  24. }

```

方式2 redisformatsdscommandargv

从如下代码可看出,字符串的拼接使用的是strcat 字符串实际长度。

```

  1. /* format a command according to the redis protocol using an sds string and
  2. * sdscatfmt for the processing of arguments. this function takes the
  3. * number of arguments, an array with arguments and an array with their
  4. * lengths. if the latter is set to null, strlen will be used to compute the
  5. * argument lengths.
  6. */
  7. int redisformatsdscommandargv(sds *target, int argc, const char **argv,
  8. const size_t *argvlen)
  9. {
  10. sds cmd;
  11. unsigned long long totlen;
  12. int j;
  13. size_t len;
  14. /* abort on a null target */
  15. if (target == null)
  16. return -1;
  17. /* calculate our total size */
  18. totlen = 1 countdigits(argc) 2;
  19. for (j = 0; j < argc; j ) {
  20. len = argvlen ? argvlen[j] : strlen(argv[j]); // ------ 确定这个是否用的strlen
  21. totlen = bulklen(len);
  22. }
  23. /* use an sds string for command construction */
  24. cmd = sdsempty();
  25. if (cmd == null)
  26. return -1;
  27. /* we already know how much storage we need */
  28. cmd = sdsmakeroomfor(cmd, totlen);
  29. if (cmd == null)
  30. return -1;
  31. /* construct command */
  32. cmd = sdscatfmt(cmd, "*%i\r\n", argc);
  33. for (j=0; j < argc; j ) {
  34. len = argvlen ? argvlen[j] : strlen(argv[j]); // --------确定这里是不是错用了strlen
  35. cmd = sdscatfmt(cmd, "$%t\r\n", len);
  36. cmd = sdscatlen(cmd, argv[j], len);
  37. cmd = sdscatlen(cmd, "\r\n", sizeof("\r\n")-1);
  38. }
  39. assert(sdslen(cmd)==totlen);
  40. *target = cmd;
  41. return totlen;
  42. }

```

## 解决方法:

业务代码切换为第二种方式进行命令拼接,如下所示:

1。 业务在做redis命令拼接的时候,尽量避免%s形式,除非能保证字符串不会被\0截断。

2。业务代码抓包可以使用strace,方便快捷。

网站地图