sds与二进制安全
在技术群里有人问了这么一个问题,redis的sds不是号称二进制安全吗?但是sdsnew函数里有对strlen进行调用啊。
跟着仔细看了一下源码,还真的是这样:
sds sdsnew(const char *init) {
size_t initlen = (init == NULL) ? 0 : strlen(init);
return sdsnewlen(init, initlen);
}
但是说sds二进制安全是说错了么?也不是,我们看看sdslen这个函数:
static inline size_t sdslen(const sds s) {
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
return sh->len;
}
直接读取struct sdshdr就输出了,所以也是正常的人称时间复杂度O(1)的长度计算。
我们注意到上面的sdsnew还调用了sdsnewlen这个函数,也来看看它的定义:
sds sdsnewlen(const void *init, size_t initlen) {
struct sdshdr *sh;
if (init) {
sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
} else {
sh = zcalloc(sizeof(struct sdshdr)+initlen+1);
}
if (sh == NULL) return NULL;
sh->len = initlen;
sh->free = 0;
if (initlen && init)
memcpy(sh->buf, init, initlen);
sh->buf[initlen] = '\0';
return (char*)sh->buf;
}
看起来sdsnewlen这个初始化函数才是真正二进制安全的,那么我们怎么判断redis在set a "abc\x00d"值的时候能够保证这个二进制安全呢?
其实就是跟踪一下setCommand的运行流程啦。redis的这些command的命名也很规律,我们直接到setCommand的函数就可以。
/* SET key value [NX] [XX] [EX <seconds>] [PX <milliseconds>] */
void setCommand(client *c) {
int j;
robj *expire = NULL;
int unit = UNIT_SECONDS;
int flags = OBJ_SET_NO_FLAGS;
for (j = 3; j < c->argc; j++) {
char *a = c->argv[j]->ptr;
robj *next = (j == c->argc-1) ? NULL : c->argv[j+1];
// 循环里基本都是处理EX NX PX之类的和key value没关系的参数了,这里略过
}
// 重点在这里
c->argv[2] = tryObjectEncoding(c->argv[2]);
setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}
最终key/value是调用setGenericCommand被塞到全局的dict里去,再看看setGenericCommand的定义,略去了不关心的部分:
void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) {
long long milliseconds = 0; /* initialized to avoid any harmness warning */
// 这里
setKey(c->db,key,val);
server.dirty++;
}
跟着setKey一路下去,发现后面都是赋值操作,没有任何对sds类型的特殊操作。
所以是不是安全,需要从setGenericCommand向前找。首先是tryObjectEncoding,查看后发现只有sdslen,没有对strlen的调用。
看来只需要看从Client接收到的变量的最初的值了~
也就是最前面的那个c->argv[2]。
acceptCommonHandler => createClient => readQueryFromClient => processInputBuffer => processInlineBuffer
readQueryFromClient里的重点是:
readlen = REDIS_IOBUF_LEN;
// 读入内容到查询缓存
nread = read(fd, c->querybuf+qblen, readlen);
整个过程就是网络编程里的简单read,tcp网络传输是没什么二进制安全问题的,因为读取结束的标准是端关闭连接发送EOF。这里之前的理解有些问题,从端读取过来的command内容是遵循redis协议的 RESP 中的 bulk 字符串数组,在 bulk string的传输过程中会先给出字符串的长度,所以读取的时候按照这个长度往后读即可,这里就不存在二进制安全的问题了。。
当内容完全读到client的querybuf(也是一个sds)中之后,调用processInputBuffer,识别客户端命令,构造输入参数。
int processInlineBuffer(redisClient *c) {
// ....
argv = sdssplitargs(aux,&argc);
// ....
return REDIS_OK;
}
终于定位到了最后一步,sdssplitargs。这个函数一堆ifelse不是很容易看,好在redis的作者另外提供了剥离的sds库(其实我们自己把redis的sds.c、sds.h、zmalloc拷贝出来也可以)。
简单写几个测试demo:
~/t/c/sds git:master ❯❯❯ cat binary_safe_test.c
#include "sds.h"
#include "stdio.h"
#include "string.h"
int main() {
sds a = sdsnewlen("set a \"ohnot \\x00hisisthe value\"", 100);
int *cnt;
sds * arr = sdssplitargs(a, cnt);
printf("argc is %d\n", *cnt);
printf("argv[0] is %s\n", arr[0]);
printf("argv[1] is %s\n", arr[1]);
printf("argv[2] is %s\n", arr[2]);
printf("sdslen is %zu\n", sdslen(arr[2]));
printf("strlen is %lu\n", strlen(arr[2]));
}
~/t/c/sds git:master ❯❯❯ ./a.out
argc is 3
argv[0] is set
argv[1] is a
argv[2] is ohnot //因为是用%s打印,所以遇到\0就结束了
sdslen is 21
strlen is 6
确实没什么问题,字符串两边的引号也已经在存储的时候被处理掉了。
这么跟了半天,完全不见sdsnew的痕迹。还真是奇怪,写了一个sdsnew,那么为什么反而不会被使用呢?来grep一发,发现直接使用sdsnew的函数大多是些内部的字符串,或者是在hiredis的客户端,又或者是在测试程序里。
好吧orz。