PHP數組/Hash表的實現、操作

來源:互聯網
上載者:User

catalogue

1. PHP Hash表1. PHP數組定義

1. PHP Hash表

0x1: 基本概念

雜湊表在實踐中使用的非常廣泛,例如編譯器通常會維護的一個符號表來儲存標記,很多進階語言中也顯式的支援雜湊表。 雜湊表通常提供尋找(Search),插入(Insert),刪除(Delete)等操作,這些操作在最壞的情況

下和鏈表的效能一樣為O(n)。 不過通常並不會這麼壞,合理設計的雜湊演算法能有效避免這類情況,通常雜湊表的這些操作時間複雜度為O(1)。 這也是它被鐘愛的原因

正是因為雜湊表在使用上的便利性及效率上的表現,目前大部分動態語言的實現中都使用了雜湊表

雜湊表是一種通過雜湊函數,將特定的鍵映射到特定值的一種資料結構,它維護鍵和值之間一一對應關係

1. 鍵(key): 用於操作資料的標示,例如PHP數組中的索引,或者字串鍵等等 2. 槽(slot/bucket): 雜湊表中用於儲存資料的一個單元,也就是資料真正存放的容器 3. 雜湊函數(hash function): 將key映射(map)到資料應該存放的slot所在位置的函數 4. 雜湊衝突(hash collision): 雜湊函數將兩個不同的key映射到同一個索引的情況 

雜湊表可以理解為數組的擴充或者關聯陣列,數組使用數字下標來定址,如果關鍵字(key)的範圍較小且是數位話, 我們可以直接使用數組來完成雜湊表,而如果關鍵字範圍太大,如果直接使用數組我們需要為所有可能的key申請空間。 很多情況下這是不現實的。即使空間足夠,空間利用率也會很低,這並不理想。同時鍵也可能並不是數字, 在PHP中尤為如此,所以人們使用一種映射函數(雜湊函數)來將key映射到特定的域中

h(key) -> index

通過合理設計的雜湊函數,我們就能將key映射到合適的範圍,因為我們的key空間可以很大(例如字串key), 在映射到一個較小的空間中時可能會出現兩個不同的key映射被到同一個index上的情況, 這就是我們所說的出現了衝突。 目前解決hash衝突的方法主要有兩種:連結法和開放定址法

1. 衝突解決: 連結法

連結法通過使用一個鏈表來儲存slot值的方式來解決衝突,也就是當不同的key映射到一個槽中的時候使用鏈表來儲存這些值。 所以使用連結法是在最壞的情況下,也就是所有的key都映射到同一個槽中了,這樣雜湊表就退化成了一個鏈表, 這樣的話操作鏈結表的時間複雜度則成了O(n),這樣雜湊表的效能優勢就沒有了, 所以選擇一個合適的雜湊函數是最為關鍵的

由於目前大部分的程式設計語言的雜湊表實現都是開源的,大部分語言的雜湊演算法都是公開的演算法, 雖然目前的雜湊演算法都能良好的將key進行比較均勻的分布,而這個假使的前提是key是隨機的,正是由於演算法的確定性, 這就導致了別有用心的駭客能利用已知演算法的可確定性來構造一些特殊的key,讓這些key都映射到同一個槽位導致雜湊表退化成單鏈表,導致程式的效能急劇下降,從而造成一些應用的吞吐能力急劇下降, 尤其是對於高並發的應用影響很大,通過大量類似的請求可以讓伺服器遭受DoS(服務拒絕攻擊)

雜湊衝突攻擊利用的雜湊表最根本的弱點是:

開源演算法和雜湊實現的確定性以及可預測性, 這樣攻擊者才可以利用特殊構造的key來進行攻擊。要解決這個問題的方法則是讓攻擊者無法輕易構造 能夠進行攻擊的key序列 

目前PHP中HashTable的雜湊衝突解決方案就是連結法

2. 衝突解決: 開放定址法

通常還有另外一種解決衝突的方法:開放定址法。使用開放定址法是槽本身直接存放資料,在插入資料時如果key所映射到的索引已經有資料了,這說明發生了衝突,這是會尋找下一個槽, 如果該槽也被佔用了則繼續尋找下一個槽,直到尋找到沒有被佔用的槽,在尋找時也使用同樣的策略來進行 由於開放定址法處理衝突的時候佔用的是其他槽位的空間,這可能會導致後續的key在插入的時候更加容易出現 雜湊衝突,所以採用開放定址法的雜湊表的裝載因子不能太高,否則容易出現效能下降

裝載因子是雜湊表儲存的元素數量和雜湊表容量的比1. 通常採用連結法解決衝突的雜湊表的裝載,因子最好不要大於1(等於1意味著Hash表已經存滿了,接下來開始儲存的索引值都會導致衝突發生即鏈表增長,而鏈表的效率是低於Hash表的)2. 而採用開放定址法的雜湊表最好不要大於0.5 

0x2: 雜湊表的實現

實現一個雜湊表也很容易,主要需要完成的工作只有三點

1. 實現雜湊函數2. 衝突的解決3. 操作介面的實現

在開始學習PHP原生核心的Hash表實現前,我們自己可以先手工實現一個簡易版的Hash表

1. 基礎資料結構定義

#ifndef _HASH_TABLE_H_#define _HASH_TABLE_H_ 1typedef struct _Bucket{    char *key;    void *value;    struct _Bucket *next;} Bucket;typedef struct _HashTable{    int size;    //HashTable size/lines    int elem_num;    //total elements count    Bucket** buckets;} HashTable;#endif

2. 雜湊函數實現

雜湊函數需要儘可能的將不同的key映射到不同的槽(slot或者bucket)中,首先我們採用一種最為簡單的雜湊演算法實現: 將key字串的所有字元加起來,然後以結果對雜湊表的大小模數,這樣索引就能落在數組索引的範圍之內了

//hashtable.c#include #include #include #include "hashtable.h" int hash_str(char *key); int hash_str(char *key){    int hash = 0;    char *cur = key;    while(*cur != '\0')        {        hash += *cur;        ++cur;        }        return hash;}//hashtable.h#define HASH_INDEX(ht, key) (hash_str((key)) % (ht)->size)

3. 操作介面的實現

為了操作雜湊表,實現了如下幾個操作介面函數

int hash_init(HashTable *ht);                               // 初始化雜湊表int hash_lookup(HashTable *ht, char *key, void **result);   // 根據key尋找內容int hash_insert(HashTable *ht, char *key, void *value);     // 將內容插入到雜湊表中int hash_remove(HashTable *ht, char *key);                  // 刪除key所指向的內容int hash_destroy(HashTable *ht);

4. 完整原始碼

hashtable.c

#include #include #include #include "hashtable.h"static void resize_hash_table_if_needed(HashTable *ht);static int hash_str(char *key);int hash_init(HashTable *ht){    ht->size         = HASH_TABLE_INIT_SIZE;    ht->elem_num     = 0;    ht->buckets        = (Bucket **)calloc(ht->size, sizeof(Bucket *));    if(ht->buckets == NULL) return FAILED;    LOG_MSG("[init]\tsize: %i\n", ht->size);    return SUCCESS;}int hash_lookup(HashTable *ht, char *key, void **result){    int index = HASH_INDEX(ht, key);    Bucket *bucket = ht->buckets[index];    if(bucket == NULL) goto failed;    while(bucket)    {        if(strcmp(bucket->key, key) == 0)        {            LOG_MSG("[lookup]\t found %s\tindex:%i value: %p\n",                key, index, bucket->value);            *result = bucket->value;                return SUCCESS;        }        bucket = bucket->next;    }failed:    LOG_MSG("[lookup]\t key:%s\tfailed\t\n", key);    return FAILED;}int hash_insert(HashTable *ht, char *key, void *value){    // check if we need to resize the hashtable    resize_hash_table_if_needed(ht);    int index = HASH_INDEX(ht, key);    Bucket *org_bucket = ht->buckets[index];    Bucket *tmp_bucket = org_bucket;    // check if the key exits already    while(tmp_bucket)    {        if(strcmp(key, tmp_bucket->key) == 0)        {            LOG_MSG("[update]\tkey: %s\n", key);            tmp_bucket->value = value;            return SUCCESS;        }        tmp_bucket = tmp_bucket->next;    }    Bucket *bucket = (Bucket *)malloc(sizeof(Bucket));    bucket->key      = key;    bucket->value = value;    bucket->next  = NULL;    ht->elem_num += 1;    if(org_bucket != NULL)    {        LOG_MSG("[collision]\tindex:%d key:%s\n", index, key);        bucket->next = org_bucket;    }    ht->buckets[index]= bucket;    LOG_MSG("[insert]\tindex:%d key:%s\tht(num:%d)\n",        index, key, ht->elem_num);    return SUCCESS;}int hash_remove(HashTable *ht, char *key){    int index = HASH_INDEX(ht, key);    Bucket *bucket  = ht->buckets[index];    Bucket *prev    = NULL;    if(bucket == NULL) return FAILED;    // find the right bucket from the link list     while(bucket)    {        if(strcmp(bucket->key, key) == 0)        {            LOG_MSG("[remove]\tkey:(%s) index: %d\n", key, index);            if(prev == NULL)            {                ht->buckets[index] = bucket->next;            }            else            {                prev->next = bucket->next;            }            free(bucket);            return SUCCESS;        }        prev   = bucket;        bucket = bucket->next;    }    LOG_MSG("[remove]\t key:%s not found remove \tfailed\t\n", key);    return FAILED;}int hash_destroy(HashTable *ht){    int i;    Bucket *cur = NULL;    Bucket *tmp = NULL;    for(i=0; i < ht->size; ++i)    {        cur = ht->buckets[i];        while(cur)        {            tmp = cur;            cur = cur->next;            free(tmp);        }    }    free(ht->buckets);    return SUCCESS;}static int hash_str(char *key){    int hash = 0;    char *cur = key;    while(*cur != '\0')    {        hash +=    *cur;        ++cur;    }    return hash;}static int hash_resize(HashTable *ht){    // double the size    int org_size = ht->size;    ht->size = ht->size * 2;    ht->elem_num = 0;    LOG_MSG("[resize]\torg size: %i\tnew size: %i\n", org_size, ht->size);    Bucket **buckets = (Bucket **)calloc(ht->size, sizeof(Bucket **));    Bucket **org_buckets = ht->buckets;    ht->buckets = buckets;    int i = 0;    for(i=0; i < org_size; ++i)    {        Bucket *cur = org_buckets[i];        Bucket *tmp;        while(cur)         {            // rehash: insert again            hash_insert(ht, cur->key, cur->value);            // free the org bucket, but not the element            tmp = cur;            cur = cur->next;            free(tmp);        }    }    free(org_buckets);    LOG_MSG("[resize] done\n");    return SUCCESS;}// if the elem_num is almost as large as the capacity of the hashtable// we need to resize the hashtable to contain enough elementsstatic void resize_hash_table_if_needed(HashTable *ht){    if(ht->size - ht->elem_num < 1)    {        hash_resize(ht);        }}

hashtable.h

#ifndef _HASH_TABLE_H_#define _HASH_TABLE_H_ 1#define HASH_TABLE_INIT_SIZE 6#define HASH_INDEX(ht, key) (hash_str((key)) % (ht)->size)#if defined(DEBUG)#  define LOG_MSG printf#else#  define LOG_MSG(...)#endif#define SUCCESS 0#define FAILED -1typedef struct _Bucket{    char *key;    void *value;    struct _Bucket *next;} Bucket;typedef struct _HashTable{    int size;        // 雜湊表的大小    int elem_num;    // 已經儲存元素的個數    Bucket **buckets;} HashTable;int hash_init(HashTable *ht);int hash_lookup(HashTable *ht, char *key, void **result);int hash_insert(HashTable *ht, char *key, void *value);int hash_remove(HashTable *ht, char *key);int hash_destroy(HashTable *ht);#endif

main.c

#include #include #include #include #include "hashtable.h"#define TEST(tcase) printf(">>> [START CASE] " tcase "<<<\n")#define PASS(tcase) printf(">>> [PASSED] " tcase " <<<\n")int main(int argc, char **argv){    HashTable *ht = (HashTable *)malloc(sizeof(HashTable));    int result = hash_init(ht);    assert(result == SUCCESS);    /* Data */    int  int1 = 10;    int  int2 = 20;    char str1[] = "Hello TIPI";    char str2[] = "Value";    /* to find data container */    int *j = NULL;    char *find_str = NULL;    /* Test Key insert */    TEST("Key insert");    hash_insert(ht, "KeyInt", &int1);    hash_insert(ht, "asdfKeyStrass", str1);    hash_insert(ht, "K13eyStras", str1);    hash_insert(ht, "KeyStr5", str1);    hash_insert(ht, "KeyStr", str1);    PASS("Key insert");    /* Test key lookup */    TEST("Key lookup");    hash_lookup(ht, "KeyInt", (void **)&j);    hash_lookup(ht, "KeyStr", (void **)&find_str);    assert(strcmp(find_str, str1) == 0);    assert(*j = int1);    PASS("Key lookup");    /* Test Key update */    TEST("Test key update");    hash_insert(ht, "KeyInt", &int2);    hash_lookup(ht, "KeyInt", (void **)&j);    assert(*j = int2);    PASS("Test key update");    TEST(">>>     Test key not found        <<< ");    result = hash_lookup(ht, "non-exits-key", (void **)&j);    assert(result == FAILED);    PASS("non-exist-key lookup");    TEST("Test key not found after remove");    char strMyKey[] = "My-Key-Value";    find_str = NULL;    hash_insert(ht, "My-Key", &strMyKey);    result = hash_remove(ht, "My-Key");    assert(result == SUCCESS);    result = hash_lookup(ht, "My-Key", (void **)&find_str);    assert(find_str == NULL);    assert(result == FAILED);    PASS("Test key not found after remove");    PASS(">>>     Test key not found        <<< ");    TEST("Add many elements and make hashtable rehash");    hash_insert(ht, "a1", &int2);    hash_insert(ht, "a2", &int1);    hash_insert(ht, "a3", &int1);    hash_insert(ht, "a4", &int1);    hash_insert(ht, "a5", &int1);    hash_insert(ht, "a6", &int1);    hash_insert(ht, "a7", &int1);    hash_insert(ht, "a8", str2);    hash_insert(ht, "a9", &int1);    hash_insert(ht, "a10", &int1);    hash_insert(ht, "a11", &int1);    hash_insert(ht, "a12", &int1);    hash_insert(ht, "a13", &int1);    hash_insert(ht, "a14", &int1);    hash_insert(ht, "a15", &int1);    hash_insert(ht, "a16", &int1);    hash_insert(ht, "a17", &int1);    hash_insert(ht, "a18", &int1);    hash_insert(ht, "a19", &int1);    hash_insert(ht, "a20", &int1);    hash_insert(ht, "a21", &int1);    hash_insert(ht, "a22", &int1);    hash_insert(ht, "a23", &int1);    hash_insert(ht, "a24", &int1);    hash_insert(ht, "a24", &int1);    hash_insert(ht, "a24", &int1);    hash_insert(ht, "a25", &int1);    hash_insert(ht, "a26", &int1);    hash_insert(ht, "a27", &int1);    hash_insert(ht, "a28", &int1);    hash_insert(ht, "a29", &int1);    hash_insert(ht, "a30", &int1);    hash_insert(ht, "a31", &int1);    hash_insert(ht, "a32", &int1);    hash_insert(ht, "a33", &int1);    hash_lookup(ht, "a23", (void **)&j);    assert(*j = int1);    hash_lookup(ht, "a30", (void **)&j);    assert(*j = int1);    PASS("Add many elements and make hashtable rehash");    hash_destroy(ht);    free(ht);    printf("Woohoo, It looks like HashTable works properly\n");    return 0;}

編譯運行

gcc -g -Wall -DDEBUG -o a.out main.c hashtable.c

0x3: 資料結構

在PHP中所有的資料,變數、常量、類、屬性、數組都用Hash表來實現\php-5.6.17\Zend\zend_hash.h

typedef struct bucket {    ulong h;                        /* Used for numeric indexing */    uint nKeyLength;                //key長度    void *pData;                    //指向 Bucke儲存的資料 指標    void *pDataPtr;                    //指標資料    struct bucket *pListNext;        //下一個元素指標    struct bucket *pListLast;        //上一個元素指標    struct bucket *pNext;    struct bucket *pLast;    const char *arKey;} Bucket;typedef struct _hashtable {    uint nTableSize;            //HashTable的大小    uint nTableMask;            //等於nTableSize-1    uint nNumOfElements;        //對象個數    ulong nNextFreeElement;        //指向下一個空元素位置 nTableSize+1    Bucket *pInternalPointer;    /* Used for element traversal 儲存當前遍曆的指標 */    Bucket *pListHead;            //頭元素指標    Bucket *pListTail;            //尾元素指標    Bucket **arBuckets;            //儲存hash數組資料    dtor_func_t pDestructor;    //類似於解構函式    zend_bool persistent;        //用哪種方法分配記憶體空間 PHP統一管理記憶體還是用普通的malloc    unsigned char nApplyCount;    //當前hash bucket被訪問的次數,是否遍曆過資料,防止無限遞迴迴圈    zend_bool bApplyProtection;#if ZEND_DEBUG    int inconsistent;#endif} H

Relevant Link:

http://www.imsiren.com/archives/6http://www.php-internals.com/book/?p=chapt03/03-01-01-hashtable https://github.com/reeze/tipi/tree/master/book/sample/chapt03/03-01-01-hashtable 

1. PHP數組定義

PHP中的數組實際上是一個有序映射。映射是一種把 values 關聯到 keys 的類型。此類型在很多方面做了最佳化,因此可以把它當成

1. 真正的數組2. 列表(向量)3. 散列表(是映射的一種實現)4. 字典5. 集合6. 棧7. 隊列以及更多可能性

數組元素的值也可以是另一個數組。樹形結構和多維陣列也是允許的,PHP中經常使用數組,使用數組最大的好處便是速度!讀寫都可以在O(1)內完成,因為它每個元素的大小都是一致的,只要知道下標,便可以瞬間計算出其對應的元素在記憶體中的位置,從而直接取出或者寫入

PHP大部分功能,都是通過HashTable來實現,其中就包括數組

HashTable即具有雙向鏈表的優點,PHP中的定義的變數儲存在一個符號表裡,而這個符號表其實就是一個HashTable,它的每一個元素都是一個zval*類型的變數。不僅如此,儲存使用者定義的函數、類、資源等的容器都是以HashTable的形式在核心中實現的

因此,PHP的數組讀寫都可以在O(1)內完成,這是非常高效的,因此開銷和C++、Java相比也就是hashtable的建立了,我們看一下PHP定義數組

 

在核心中使用宏來實現

0x1: 數組初始化

Zend/zend_vm_execute.h

static int ZEND_FASTCALL  ZEND_INIT_ARRAY_SPEC_CV_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS){    USE_OPLINE    array_init(&EX_T(opline->result.var).tmp_var);    if (IS_CV == IS_UNUSED) {        ZEND_VM_NEXT_OPCODE();#if 0 || IS_CV != IS_UNUSED    } else {        return ZEND_ADD_ARRAY_ELEMENT_SPEC_CV_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU);#endif    }}

\php-5.6.17\Zend\zend_API.c

ZEND_API int _array_init(zval *arg, uint size ZEND_FILE_LINE_DC) /* {{{ */{    ALLOC_HASHTABLE_REL(Z_ARRVAL_P(arg));    _zend_hash_init(Z_ARRVAL_P(arg), size, ZVAL_PTR_DTOR, 0 ZEND_FILE_LINE_RELAY_CC);    Z_TYPE_P(arg) = IS_ARRAY;    return SUCCESS;}

\php-5.6.17\Zend\zend_hash.c

ZEND_API int _zend_hash_init(HashTable *ht, uint nSize, dtor_func_t pDestructor, zend_bool persistent ZEND_FILE_LINE_DC){    uint i = 3;    SET_INCONSISTENT(HT_OK);    if (nSize >= 0x80000000) {        /* prevent overflow */        //HASH表大小大於0x80000000則初始化為0x80000000        ht->nTableSize = 0x80000000;    } else {        while ((1U << i) < nSize) {            i++;        }        //申請的數組Size空間調整為2的n次方,這樣有利於記憶體對齊,i=3,nTableSize最小值為8        ht->nTableSize = 1 << i;    }    ht->nTableMask = 0;    /* 0 means that ht->arBuckets is uninitialized */    ht->pDestructor = pDestructor;    //一個函數指標,當HashTable發生增,刪,改時調用    ht->arBuckets = (Bucket**)&uninitialized_bucket;    ht->pListHead = NULL;    ht->pListTail = NULL;    ht->nNumOfElements = 0;    ht->nNextFreeElement = 0;    ht->pInternalPointer = NULL;    ht->persistent = persistent;    //如果persisient為TRUE,則使用作業系統本身的記憶體配置函數為Bucket分配記憶體,否則使用PHP的記憶體配置函數    ht->nApplyCount = 0;    ht->bApplyProtection = 1;    return SUCCESS;}ZEND_API int _zend_hash_init_ex(HashTable *ht, uint nSize, dtor_func_t pDestructor, zend_bool persistent, zend_bool bApplyProtection ZEND_FILE_LINE_DC){    int retval = _zend_hash_init(ht, nSize, pDestructor, persistent ZEND_FILE_LINE_CC);    ht->bApplyProtection = bApplyProtection;    return retval;}

0x2: 數組添加索引值

0x3: 操作PHP數組的API

//初始化PHP數組array_init(zval *arg);array_init_size(zval *arg, uint size); //關聯陣列賦值的操作函數,等同於$array[$stringKey] = $value;add_assoc_null(zval *aval, char *key);add_assoc_bool(zval *aval, char *key, zend_bool bval);add_assoc_long(zval *aval, char *key, long lval);add_assoc_double(zval *aval, char *key, double dval);add_assoc_string(zval *aval, char *key, char *strval, int dup);add_assoc_stringl(zval *aval, char *key,char *strval, uint strlen, int dup);add_assoc_zval(zval *aval, char *key, zval *value);//上述函數均為宏函數,都是對add_assoc_*_ex函數的封裝 //數字索引數組賦值的操作函數,等效於$array[$numKey] = $value;ZEND_API int add_index_long(zval *arg, ulong idx, long n);ZEND_API int add_index_null(zval *arg, ulong idx);ZEND_API int add_index_bool(zval *arg, ulong idx, int b);ZEND_API int add_index_resource(zval *arg, ulong idx, int r);ZEND_API int add_index_double(zval *arg, ulong idx, double d);ZEND_API int add_index_string(zval *arg, ulong idx, const char *str, int duplicate);ZEND_API int add_index_stringl(zval *arg, ulong idx, const char *str, uint length, int duplicate);ZEND_API int add_index_zval(zval *arg, ulong index, zval *value); //使用內建數字索引的數組賦值的操作函數,等效於$array[] = $value;ZEND_API int add_next_index_long(zval *arg, long n);ZEND_API int add_next_index_null(zval *arg);ZEND_API int add_next_index_bool(zval *arg, int b);ZEND_API int add_next_index_resource(zval *arg, int r);ZEND_API int add_next_index_double(zval *arg, double d);ZEND_API int add_next_index_string(zval *arg, const char *str, int duplicate);ZEND_API int add_next_index_stringl(zval *arg, const char *str, uint length, int duplicate);ZEND_API int add_next_index_zval(zval *arg, zval *value); //數組元素賦值並返回,等效於{ $array[$key] = $value; return $value; }ZEND_API int add_get_assoc_string_ex(zval *arg, const char *key, uint key_len, const char *str, void **dest, int duplicate);ZEND_API int add_get_assoc_stringl_ex(zval *arg, const char *key, uint key_len, const char *str, uint length, void **dest, int duplicate);#define add_get_assoc_string(__arg, __key, __str, __dest, __duplicate) add_get_assoc_string_ex(__arg, __key, strlen(__key)+1, __str, __dest, __duplicate)#define add_get_assoc_stringl(__arg, __key, __str, __length, __dest, __duplicate) add_get_assoc_stringl_ex(__arg, __key, strlen(__key)+1, __str, __length, __dest, __duplicate)ZEND_API int add_get_index_long(zval *arg, ulong idx, long l, void **dest);ZEND_API int add_get_index_double(zval *arg, ulong idx, double d, void **dest);ZEND_API int add_get_index_string(zval *arg, ulong idx, const char *str, void **dest, int duplicate);ZEND_API int add_get_index_stringl(zval *arg, ulong idx, const char *str, uint length, void **dest, int duplicate);

Relevant Link:

http://thiniki.sinaapp.com/?p=155http://www.imsiren.com/archives/250http://www.cnblogs.com/ohmygirl/p/internal-4.htmlhttp://weizhifeng.net/write-php-extension-part2-1.htmlhttp://blog.csdn.net/a600423444/article/details/7073854

Copyright (c) 2016 Little5ann All rights reserved

  • 聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.