本文主要從三個角度來闡述php的二進制安全:1. 什麼叫php的二進制安全;2. 什麼結構確保了php的二進制安全;3. 這種結構還有哪些其它方面的應用?
做到知其然,也知其所以然。
一句話解釋:
php的內部函數在操作二進制數據時能保證達到預期的結果,例如str_replace、stristr、strcmp等函數,我們就說這些函數是二進制安全的。
舉個列子:
我們來對比一下C和php下的strcmp函數。
C代碼如下
main(){ char ab[] = "aa\0b"; char ac[] = "aa\0c"; printf("%d\n", strcmp(ab, ac)); printf("%d\n", strlen(ab)); }
結果:
0
2
解讀:
也就是說C語言認為ab和ac這兩個字符串是相等的,而且ab的長度為2.
php代碼如下
結果:int(-1)
int(4)
解讀:
也就是php語言認為ab和ac這兩個字符串是相等的,而且ab的長度為4。
聰明的你,應該已經發現問題在哪了吧,不錯,對於c語言‘\0’是字符串的結束符,所以在C語言中對於字符串“aa\0b”,它讀到'\0'就會默認字符讀取已經結束,而拋掉後面的字符串'b',導致我們看到strlen(“aa\0b”)的值為2
那問題又來了,php都是C來開發的,為什麼php做到了二進制安全呢?
先來看看php的變量存儲zval結構
php會根據type的值來決定訪問value的哪個成員,為字符串時,我們會訪問紅框標識的str結構,這便是底層字符串的存儲結構,它有兩個值,一個是指向字符串的指針val,另一個是記錄字符串長度的len值,就是因為有len這個值,導致了php是二進制安全的:因為它不需要像C一樣通過是否遇到'\0'結尾符來判斷整個字符串是否讀取完畢,而是通過len這個值指定的長度進行讀取。
struct sdshdr { // 記錄 buf 數組中已使用字節的數量 // 等於 SDS 所保存字符串的長度 int len; // 記錄 buf 數組中未使用字節的數量 int free; // 字節數組,用於保存字符串 char buf[]; };可以看到,我們又見到了熟悉的len值,又是它保證了redis的存儲是二進制安全的
C 字符串中的字符必須符合某種編碼(比如 ASCII),並且除了字符串的末尾之外,字符串裡面不能包含空字符,否則最先被程序讀入的空字符將被誤認為是字符串結尾 ——這些限制使得 C 字符串只能保存文本數據,而不能保存像圖片、音頻、視頻、壓縮文件這樣的二進制數據。
舉個例子,如果有一種使用空字符來分割多個單詞的特殊數據格式,如圖 2-17 所示,那麼這種格式就不能使用 C 字符串來保存,因為 C 字符串所用的函數只會識別出其中的
"Redis"
,而忽略之後的
"Cluster"
。
雖然數據庫一般用於保存文本數據,但使用數據庫來保存二進制數據的場景也不少見,因此,為了確保 Redis 可以適用於各種不同的使用場景,SDS 的 API 都是二進制安全的(binary-safe):所有 SDS API 都會以處理二進制的方式來處理 SDS 存放在
buf
數組裡的數據,程序不會對其中的數據做任何限制、過濾、或者假設 ——數據在寫入時是什麼樣的,它被讀取時就是什麼樣。
這也是我們將 SDS 的 buf
屬性稱為字節數組的原因 ——Redis 不是用這個數組來保存字符,而是用它來保存一系列二進制數據。
比如說,使用 SDS 來保存之前提到的特殊數據格式就沒有任何問題,因為 SDS 使用 len
屬性的值而不是空字符來判斷字符串是否結束,如圖 2-18 所示。
buf"]; buf [label = " { 'R' | 'e' | 'd' | 'i' | 's' | '\\0' | 'C' | 'l' | 'u' | 's' | 't' | 'e' | 'r' | '\\0' | '\\0' } "]; // sdshdr:buf -> buf;}">
通過使用二進制安全的 SDS ,而不是 C 字符串,使得 Redis 不僅可以保存文本數據,還可以保存任意格式的二進制數據。