? ? ?今天分析的是Redis源碼中的字符串操作類的代碼實現。有了上幾次的分析經驗,漸漸覺得我得換一種分析的方法,如果每個API都進行代碼分析,有些功能性的重復,導致分析效率的偏低。所以下面我覺得對于代碼的分析偏重的是一種功能整體的思維實現來講解,其中我也會挑出一個比較有特點的方法進行拆分了解,這也可以讓我們見識一下里面的一些神奇的代碼。好,回歸正題,說到字符串,這不管放到哪個編程語言中,都是使用頻率極高的操作類。什么new String, concat, strcopy,substr, splitStr,這些方法我們也一定是非常熟悉的了。其實這些方法在我們所說的高級語言中是比較多的,像C語言這種更基礎的語言中還沒有開放那么多的API,而且人家也沒有String這個類,取而代之的實現手法是char[] 數組的形式。所以今天我們所講的sds字符串操作類也是基于char[] 的操作。
? 首頁我們先列出sds.h頭文件:
~~~
/* SDSLib, A C dynamic strings library
*
* Copyright (c) 2006-2010, Salvatore Sanfilippo <antirez at gmail dot com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of Redis nor the names of its contributors may be used
* to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef __SDS_H
#define __SDS_H
/* 最大分配內存1M */
#define SDS_MAX_PREALLOC (1024*1024)
#include <sys/types.h>
#include <stdarg.h>
/* 聲明了sds的一種char類型 */
typedef char *sds;
/* 字符串結構體類型 */
struct sdshdr {
//字符長度
unsigned int len;
//當前可用空間
unsigned int free;
//具體存放字符的buf
char buf[];
};
/* 計算sds的長度,返回的size_t類型的數值 */
/* size_t,它是一個與機器相關的unsigned類型,其大小足以保證存儲內存中對象的大小。 */
static inline size_t sdslen(const sds s) {
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
return sh->len;
}
/* 根據sdshdr中的free標記獲取可用空間 */
static inline size_t sdsavail(const sds s) {
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
return sh->free;
}
sds sdsnewlen(const void *init, size_t initlen); //根據給定長度,新生出一個sds
sds sdsnew(const char *init); //根據給定的值,生出sds
sds sdsempty(void); //清空sds操作
size_t sdslen(const sds s); //獲取sds的長度
sds sdsdup(const sds s); //sds的復制方法
void sdsfree(sds s); //sds的free釋放方法
size_t sdsavail(const sds s); //判斷sds獲取可用空間
sds sdsgrowzero(sds s, size_t len); // 擴展字符串到指定的長度
sds sdscatlen(sds s, const void *t, size_t len);
sds sdscat(sds s, const char *t); //sds連接上char字符
sds sdscatsds(sds s, const sds t); //sds連接上sds
sds sdscpylen(sds s, const char *t, size_t len); //字符串復制相關
sds sdscpy(sds s, const char *t); //字符串復制相關
sds sdscatvprintf(sds s, const char *fmt, va_list ap); //字符串格式化輸出,依賴已有的方法sprintf,效率不及下面自己寫的
#ifdef __GNUC__
sds sdscatprintf(sds s, const char *fmt, ...)
__attribute__((format(printf, 2, 3)));
#else
sds sdscatprintf(sds s, const char *fmt, ...);
#endif
sds sdscatfmt(sds s, char const *fmt, ...); //字符串格式化輸出
sds sdstrim(sds s, const char *cset); //字符串縮減
void sdsrange(sds s, int start, int end); //字符串截取函數
void sdsupdatelen(sds s); //更新字符串最新的長度
void sdsclear(sds s); //字符串清空操作
int sdscmp(const sds s1, const sds s2); //sds比較函數
sds *sdssplitlen(const char *s, int len, const char *sep, int seplen, int *count); //字符串分割子字符串
void sdsfreesplitres(sds *tokens, int count); //釋放子字符串數組
void sdstolower(sds s); //sds字符轉小寫表示
void sdstoupper(sds s); //sds字符統一轉大寫
sds sdsfromlonglong(long long value); //生出數組字符串
sds sdscatrepr(sds s, const char *p, size_t len);
sds *sdssplitargs(const char *line, int *argc); //參數拆分
sds sdsmapchars(sds s, const char *from, const char *to, size_t setlen); //字符映射,"ho", "01", h映射為0, o映射為1
sds sdsjoin(char **argv, int argc, char *sep); //以分隔符連接字符串子數組構成新的字符串
/* Low level functions exposed to the user API */
/* 開放給使用者的API */
sds sdsMakeRoomFor(sds s, size_t addlen);
void sdsIncrLen(sds s, int incr);
sds sdsRemoveFreeSpace(sds s);
size_t sdsAllocSize(sds s);
#endif
~~~
里面定義了我們希望看到的很多常用的方法,不錯,看起來非常的全面,還是很佩服源碼的編寫者,用C語言把這些功能都給實現了。 在開放sds字符串實現方法之前,我說一下,sds主要是通過什么原理實現操作的呢,答案是sdshdr結構體,很多的操作都是先將sds轉化為sdshdr結構,通過里面的一些變量(相當于此字符串的屬性了),來設置當前字符串的一些狀態,再返回sdshdr->buf返回操作后的結果。在這里,可以理解sdshdr為String的對象了,sds只是里面的具體的值。很多的形式都是基于如下的操作:
比如清空操作方法;
~~~
/* Modify an sds string on-place to make it empty (zero length).
* However all the existing buffer is not discarded but set as free space
* so that next append operations will not require allocations up to the
* number of bytes previously available. */
/* 清空字符串 */
void sdsclear(sds s) {
struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
//空閑的長度增多
sh->free += sh->len;
sh->len = 0;
//字符串中的緩存其實沒有被丟底,只是把第一個設成了結束標志,以便下次操作可以復用
sh->buf[0] = '\0';
}
~~~
比如創建方法,通過返回結構體sdshdr->buf返回新的字符串:
~~~
/* Create a new sds string with the content specified by the 'init' pointer
* and 'initlen'.
* If NULL is used for 'init' the string is initialized with zero bytes.
*
* The string is always null-termined (all the sds strings are, always) so
* even if you create an sds string with:
*
* mystring = sdsnewlen("abc",3");
*
* You can print the string with printf() as there is an implicit \0 at the
* end of the string. However the string is binary safe and can contain
* \0 characters in the middle, as the length is stored in the sds header. */
/* 創建新字符串方法,傳入目標長度,初始化方法 */
sds sdsnewlen(const void *init, size_t initlen) {
struct sdshdr *sh;
if (init) {
sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
} else {
//當init函數為NULL時候,又來了zcalloc的方法
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);
//最末端同樣要加‘\0’結束符
sh->buf[initlen] = '\0';
//最后是通過返回字符串結構體中的buf代表新的字符串
return (char*)sh->buf;
}
~~~
下面重點推薦幾個特殊的方法,平時在C語言中看到的方法,沒想到在這里看到具體的實現方法了,格式化輸出方法C,語言實現:
~~~
/* This function is similar to sdscatprintf, but much faster as it does
* not rely on sprintf() family functions implemented by the libc that
* are often very slow. Moreover directly handling the sds string as
* new data is concatenated provides a performance improvement.
*
* However this function only handles an incompatible subset of printf-alike
* format specifiers:
*
* %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, ...) {
struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
size_t initlen = sdslen(s);
const char *f = fmt;
int i;
va_list ap;
va_start(ap,fmt);
f = fmt; /* Next format specifier byte to process. */
i = initlen; /* Position of the next byte to write to dest str. */
//關鍵再次,以此比較輸入的格式類型
while(*f) {
char next, *str;
unsigned int l;
long long num;
unsigned long long unum;
/* Make sure there is always space for at least 1 char. */
if (sh->free == 0) {
s = sdsMakeRoomFor(s,1);
sh = (void*) (s-(sizeof(struct sdshdr)));
}
switch(*f) {
case '%':
/*如果是%,記住百分號后面的類型操作值*/
next = *(f+1);
f++;
switch(next) {
case 's':
case 'S':
str = va_arg(ap,char*);
//判斷普通的str,還是sds類型,計算長度的方法不一樣
l = (next == 's') ? strlen(str) : sdslen(str);
if (sh->free < l) {
s = sdsMakeRoomFor(s,l);
sh = (void*) (s-(sizeof(struct sdshdr)));
}
//如果是字符串,直接復制到后面
memcpy(s+i,str,l);
sh->len += l;
sh->free -= l;
i += l;
break;
case 'i':
case 'I':
if (next == 'i')
num = va_arg(ap,int);
else
num = va_arg(ap,long long);
{
char buf[SDS_LLSTR_SIZE];
//如果是數字,調用添加數值字符串方法
l = sdsll2str(buf,num);
if (sh->free < l) {
s = sdsMakeRoomFor(s,l);
sh = (void*) (s-(sizeof(struct sdshdr)));
}
memcpy(s+i,buf,l);
sh->len += l;
sh->free -= l;
i += l;
}
break;
case 'u':
case 'U':
//無符號整型同上
if (next == 'u')
unum = va_arg(ap,unsigned int);
else
unum = va_arg(ap,unsigned long long);
{
char buf[SDS_LLSTR_SIZE];
l = sdsull2str(buf,unum);
if (sh->free < l) {
s = sdsMakeRoomFor(s,l);
sh = (void*) (s-(sizeof(struct sdshdr)));
}
memcpy(s+i,buf,l);
sh->len += l;
sh->free -= l;
i += l;
}
break;
default: /* Handle %% and generally %<unknown>. */
s[i++] = next;
sh->len += 1;
sh->free -= 1;
break;
}
break;
default:
//非操作類型,直接單字符添加
s[i++] = *f;
sh->len += 1;
sh->free -= 1;
break;
}
f++;
}
va_end(ap);
/* Add null-term */
s[i] = '\0';
return s;
}
~~~
看完的確讓我感覺很強大,小小的格式化輸出,竟然也沒有那么簡單,應該java,里的格式化輸出算法應該都是跟這差不多的吧。另外一個拆分split方法,這個里面竟然還因為內存的問題的用了goto清空間操作,這個也是漲見識了:
~~~
/* Split 's' with separator in 'sep'. An array
* of sds strings is returned. *count will be set
* by reference to the number of tokens returned.
*
* On out of memory, zero length string, zero length
* separator, NULL is returned.
*
* Note that 'sep' is able to split a string using
* a multi-character separator. For example
* sdssplit("foo_-_bar","_-_"); will return two
* elements "foo" and "bar".
*
* This version of the function is binary-safe but
* requires length arguments. sdssplit() is just the
* same function but for zero-terminated strings.
*/
/* sds字符串分割方法類似java.lang.String的spilt方法 */
sds *sdssplitlen(const char *s, int len, const char *sep, int seplen, int *count) {
int elements = 0, slots = 5, start = 0, j;
sds *tokens;
if (seplen < 1 || len < 0) return NULL;
//分割的子字符串初始值只有5組
tokens = zmalloc(sizeof(sds)*slots);
//如果內存溢出,直接返回NULL值
if (tokens == NULL) return NULL;
if (len == 0) {
*count = 0;
return tokens;
}
//從前往后掃描,到最后一個能匹配分隔符字符串的位置len-seplen
for (j = 0; j < (len-(seplen-1)); j++) {
/* make sure there is room for the next element and the final one */
//如果當前字符串數組數量少于當前已存在數組+2個的時候,動態添加
if (slots < elements+2) {
sds *newtokens;
slots *= 2;
newtokens = zrealloc(tokens,sizeof(sds)*slots);
//如果內存此時溢出,goto語句free釋放內存,終于看到了goto語句的派上用處了
if (newtokens == NULL) goto cleanup;
tokens = newtokens;
}
/* search the separator */
//分成單字符比較和多字符比較匹配
if ((seplen == 1 && *(s+j) == sep[0]) || (memcmp(s+j,sep,seplen) == 0)) {
//賦值子字符串
tokens[elements] = sdsnewlen(s+start,j-start);
if (tokens[elements] == NULL) goto cleanup;
elements++;
start = j+seplen;
j = j+seplen-1; /* skip the separator */
}
}
/* Add the final element. We are sure there is room in the tokens array. */
//最后一個字符串添加
tokens[elements] = sdsnewlen(s+start,len-start);
//如果內存溢出,再次清空,也直接直接返回NULL
if (tokens[elements] == NULL) goto cleanup;
elements++;
*count = elements;
return tokens;
cleanup:
{
//清除空間
int i;
for (i = 0; i < elements; i++) sdsfree(tokens[i]);
zfree(tokens);
*count = 0;
return NULL;
}
}
~~~
第一次看的共同的用法了,少見少見,spilt的方法實現,也不容易,還考慮了OOM的情況,還考慮了動態擴展內存,如果子字符串比較多的話,在C語言中,可以看到上面代碼在擴增方面還是非常謹慎的。果然平時搞高級語言基本考慮不到這些問題的,還是佩服佩服。下面再來看看數字字符串的添加操作,這個跟我們最早的算法是一樣的,逐位求余,再倒序添加到主字符串中。
~~~
/* Helper for sdscatlonglong() doing the actual number -> string
* conversion. 's' must point to a string with room for at least
* SDS_LLSTR_SIZE bytes.
*
* The function returns the lenght of the null-terminated string
* representation stored at 's'. */
/* 字符串末尾添加數值字符串組成新的字符串 */
#define SDS_LLSTR_SIZE 21
int sdsll2str(char *s, long long value) {
char *p, aux;
unsigned long long v;
size_t l;
/* Generate the string representation, this method produces
* an reversed string. */
v = (value < 0) ? -value : value;
p = s;
//用最傳統的逐位取商算出每一個位置上的數,注意現在的順序其實是逆序的
do {
*p++ = '0'+(v%10);
v /= 10;
} while(v);
//后面別忘了正負號的添加
if (value < 0) *p++ = '-';
/* Compute length and add null term. */
l = p-s;
*p = '\0';
/* Reverse the string. */
//將剛才的添加的逆序的數字字符串進行倒敘添加到本身的字符串s中
p--;
while(s < p) {
aux = *s;
*s = *p;
*p = aux;
s++;
p--;
}
return l;
}
~~~
還有人家的字符映射功能,其實相當與我們的replace方法,不過redis版本的是單字符映射,代碼如下:
~~~
/* Modify the string substituting all the occurrences of the set of
* characters specified in the 'from' string to the corresponding character
* in the 'to' array.
*
* For instance: sdsmapchars(mystring, "ho", "01", 2)
* will have the effect of turning the string "hello" into "0ell1".
*
* The function returns the sds string pointer, that is always the same
* as the input pointer since no resize is needed. */
/* 字符映射,"ho", "01", h映射為0, o映射為1 */
sds sdsmapchars(sds s, const char *from, const char *to, size_t setlen) {
size_t j, i, l = sdslen(s);
for (j = 0; j < l; j++) {
for (i = 0; i < setlen; i++) {
if (s[j] == from[i]) {
s[j] = to[i];
break;
}
}
}
return s;
}
~~~
? ?好了,總的來說,字符串操作的實現也是非常龐大的代碼量,已經超過千行了,redis代碼通過調用最原始的char[] 數組的一些方法,實現了字符串的功能,讓我明白了一些高級語言中的API的底層實現方法,收獲不錯,字符串中也有很多體現了函數式編程的思想,有好多函數當參數的呢,根本沒有具體數值類型的。好,今天的分析就這么多。
?
- 前言
- (一)--Redis結構解析
- (二)--結構體分析(1)
- (三)---dict哈希結構
- (四)-- sds字符串
- (五)--- sparkline微線圖
- (六)--- ziplist壓縮列表
- (七)--- zipmap壓縮圖
- (八)--- t_hash哈希轉換
- (九)--- t_list,t_string的分析
- (十)--- testhelp.h小型測試框架和redis-check-aof.c日志檢測
- (十一)--- memtest內存檢測
- (十二)--- redis-check-dump本地數據庫檢測
- (十三)--- redis-benchmark性能測試
- (十四)--- rdb.c本地數據庫操作
- (十五)--- aof-append only file解析
- (十六)--- config配置文件
- (十七)--- multi事務操作
- (十八)--- db.c內存數據庫操作
- (十九)--- replication主從數據復制的實現
- (二十)--- ae事件驅動
- (二十一)--- anet網絡通信的封裝
- (二十二)--- networking網絡協議傳輸
- (二十三)--- CRC循環冗余算法和RAND隨機數算法
- (二十四)--- tool工具類(2)
- (二十五)--- zmalloc內存分配實現
- (二十六)--- slowLog和hyperloglog
- (二十七)--- rio系統I/O的封裝
- (二十八)--- object創建和釋放redisObject對象
- (二十九)--- bio后臺I/O服務的實現
- (三十)--- pubsub發布訂閱模式
- (三十一)--- latency延遲分析處理
- (三十二)--- redis-cli.c客戶端命令行接口的實現(1)
- (三十三)--- redis-cli.c客戶端命令行接口的實現(2)
- (三十四)--- redis.h服務端的實現分析(1)
- (三十五)--- redis.c服務端的實現分析(2)
- (三十六)--- Redis中的11大優秀設計