数据结构:
/*
* simple dynamic string
*/
typedef char *sds;
/*
* simple dynamic string header
*/
struct sdshdr
{
int len; // buf数组中已使用的长度
int free; // buf数组中剩余长度
char buf[]; // 存放字符的数组 知识点关键词: C99 柔性数组结构成员
};
显而易见,sds本身是个char类型的数据。而char类型和C语言的字符串类型完全兼容。
这个sds header的结构体,可以理解成header包含len和free信息,数据在buf中。sds实际是指向sdshdr结构体的字符串空间 char buf[]。
众所周知,C语言中字符串的结尾是’\0’,那如果数据本身有0,会导致字符串读到0就认为结束了。
这里可以理解到,为何Redis的sds是二进制安全的,sds在真正读数据时不会因为数据中的0导致截断,因为header有长度信息len,可以读到完整数据。
关键方法:
sds sdsnewlen(const void *init, size_t initlen) {
struct sdshdr *sh;
// 根据init判断是否初始化分配的内存空间
// zmalloc zcalloc 是redis自身实现的内存分配方法
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; // 新创建的sds没有free空间
// 如果有初始化内容,将初始化数据复制到sdshdr的buf数组
if (initlen && init)
memcpy(sh->buf, init, initlen);
// buf数组以 \0 结尾,和C的字符串一致
sh->buf[initlen] = '\0';
// 返回buf的指针,sds指针,后面会有sdshdr的获取方式介绍
return (char*)sh->buf;
}
这是创建sds的方法。注意初始化buf时,结尾仍然是C接口兼容的’\0’结尾。
static inline size_t sdslen(const sds s)
{
struct sdshdr *sh = (void *)(s - (sizeof(struct sdshdr)));
return sh->len;
}
这是获取sds真正长度的方法。
在C99标准中,新增了一个标准:incomlete type,形式同int a[]; 中文翻译为柔性数组,数组大小未知,长度为0。结构体的最后一个元素可以是柔性数组,柔性数组成员前至少有一个其他成员。(理解:一个结构体最多只能有一个柔性数组,还得放在最后。)使用malloc函数进行内存分配时,分配的内存应该大于结构的大小,以适应柔性数组的预期大小。(理解:结构体的大小不包含柔性数组的大小,但是分配内存时要注意给它分配。结合sds来讲,sds不占header的空间,只是一个header的地址偏移量,内存上是连续的。)
以上获取sds长度方法,其中 s - (sizeof(struct sdshdr))
是关注点。源码中普遍使用这种做法,通过sds的绝对地址去计算header的地址。
sizeof(struct sdshdr)
实际上等于sizeof(len) + sizeof(free)
, 柔性数组buf的大小不计入struct。
sdshdr的内存分布是连续的, {[len][free]}[buf...]
sds指向buf, 所以s的地址减去struct的size,就是sdshdr的地址。
sdsfree
释放sds空间 zfree(header地址)
sdsclear
惰性删除buf内容,len=0,buf[0] = ‘\0’
sdsmakeRoomFor
增加预分配空间。如果新长度小于1M就按照新长度*2分配,新长度不小于1M就再增加1M。
sdsRemoveFreeSpace
回收free空间。
sdsAllocSize
获取sds占用内存字节数。sizeof(header)+len+free+1。
sdsIncrLen
增加incr参数的长度到len,较少free对应长度。
sdsgrowzero
将sds扩充到指定长度,未使用的用0填充。
sdscatlen
sdscat
sdscatsds
追加长度为len的字符串t到sds的末尾。
sdscpylen
sdscpy
将字符串的钱len个字符复制到sds中。
#define SDS_LLSTR_SIZE 21
long long string 长度21 (unsigned longlong最大值是20位; longlong是19位,带符号是20位,这边设置成21位有1位空余,用来放字符串结尾’\0’)
sdsll2str
将long long转化为字符串存储。返回长度。
sdsull2str
将unsingned long long转化为字符串存储。返回长度。
sdsfromlonglong
根据longlong值,创建sds。
sdscatvprintf
sdscatprintf
格式化的字符串连接到sds上,用到了vsnprintf。(不定长参数…,用va_list读取。)
sdacatfmt
这是redis自己实现的sprintf方法,而不是上面用的libc库的方法。
/*
* %s - C String
* %S - SDS string
* %i - signed int
* %I - 64 bit signed integer (long long, int64_t)
* %u - unsigned int
* %U - 64 bit unsigned integer (unsigned long long, uint64_t)
* %% - Verbatim "%" character.
*/
sds sdscatfmt(sds s, char const *fmt, ...)
实现过程朴实无华,每次扩容一个字符,将fmt下个字符拷贝到s,遇到格式符再分情况扩容拷贝。
sdstrim
对sds左右进行清理,去掉指定字符。用到了strchr来判断字符是否在指定字符串中,用到了memmove
来调整最终字符串。
memmove
是比memcpy
更安全的方法。如果目标区域和源区域有重叠的话,memmove
()
能够保证源串在被覆盖之前将重叠区域的字节拷贝到目标区域中,复制后源区域的内容会被更改。如果目标区域与源区域没有重叠,则和
memcpy
() 函数功能相同。
sdsrange
截取sds字符串中段区间[start, end]
sdstolower
将 sds 字符串中的所有字符转换为小写
sdstoupper
将 sds 字符串中的所有字符转换为大写
sdscmp
类似strcmp 的 sds 版本 (int :相等返回 0 ,s1 较大返回正数, s2 较大返回负数)
sdssplitlen
使用分隔符对sds进行分割,返回sds数组
sdsfreesplitres
用来释放sdssplitlen产生的sds数组
sdscatrepr
将长度为 len 的字符串 p 以带引号的格式,追加到给定 sds 的末尾
sdscatrepr
里有个isprint函数,判断是否是可打印字符。查看ctype.h源码可知是一个宏定义:
# define isprint(c) __isctype((c), _ISprint)
# define __isctype(c, type) \
((*__ctype_b_loc ())[(int) (c)] & (unsigned short int) type)
/*
_ISprint = _ISbit (6); //其实就是位移操作。根据系统是little endian还是big endian返回不同位移结果。
关键的__ctype_b_loc数组,个人测试结果是针对ASCII字符做了标记,这样能通过位运算把ASCII码做分组。
isprint的ASCII码范围是[0x20,0x7f)
*/
sdssplitargs
将文本分割成多个参数。应用于长文本或者配置文件。例如"ip 127.0.0.1\r\nport 6793\r\n" => ["ip", "127.0.0.1", "port": "6793"]
sdsmapchars
对sds做字符替换,在from中出现的替换为to中的字符。
sdsjoin
对字符串数组做连接,可以由分隔符。
sds.c最后是一份单元测试代码,用宏定义来做开关。
SDS印象最深的点:
typedef char *sds;
- SDS的数据结构,柔性数组的使用。
struct sdshdr *sh = (void *)(s -(sizeof(struct sdshdr)));
通过sds绝对地址获取header结构指针。- 二进制安全,部分接口兼容C字符串接口函数。