goahead嵌入式Web服務器源碼解析(內存映射圖,hash算法,事件調度)

代碼最核心的就是數據結構,那麼goahead官方的源碼的數據結構又是如何?本人在開發web後端服務器,使用goahead和libwebcoket搭建嵌入式服務器,通過閱讀官方源碼和修改和設計等實戰,繪畫和總結了相關源碼的設計,不足之處望指正!

  • Sym(hash)和callback的內存映射關係圖:

說明:Sym和Callbacks指向的主映射表是不同,這裏只是爲了方便畫在一起。並且在主映射表的有效單元是可以出現多個Sym或Callbacks,或者任何void *類型的對象類型指針。主映射表是以16個單元爲增量進行遞增的,且每個單元爲一個void *類型的長度,在32位操作系統上爲4個字節。

  • HashTable的存儲和算法解析:

Hash,一般翻譯做散列、雜湊,或音譯爲哈希,是把任意長度的輸入(又叫做預映射pre-image)通過散列算法變換成固定長度的輸出,該輸出就是散列值。這種轉換是一種壓縮映射,也就是,散列值的空間通常遠小於輸入的空間,不同的輸入可能會散列成相同的輸出,所以不可能從散列值來確定唯一的輸入值。

若結構中存在和關鍵字K相等的記錄,則必定在f(K)的存儲位置上。由此,不需比較便可直接取得所查記錄。稱這個對應關係f爲散列函數(Hash function),按這個事先建立的表爲散列表。對不同的關鍵字可能得到同一散列地址,即key1≠key2,而f(key1)=f(key2),這種現象稱碰撞。

所有散列函數都有如下一個基本特性:如果兩個散列值是不相同的(根據同一函數),那麼這兩個散列值的原始輸入也是不相同的。這個特性是散列函數具有確定性的結果。但另一方面,散列函數的輸入和輸出不是一一對應的,如果兩個散列值相同,兩個輸入值很可能是相同的,但不絕對肯定二者一定相等(可能出現哈希碰撞)。

 

源碼解析:

這裏的源碼使用的hash函數使用的方法是:

除留餘數法。取關鍵字被某個不大於散列表表長m的數p除後所得的餘數爲散列地址。即 H(key) = key MOD p,p<=m。不僅可以對關鍵字直接取模,也可在摺疊、平方取中等運算之後取模。對p的選擇很重要,一般取素數或m,若p選的不好,容易產生碰撞。

typedef struct HashTable {              /* Symbol table descriptor */

    WebsKey     **hash_table;           /* Allocated at run time */

    int         inuse;                  /* Is this entry in use */

    int         size;                   /* Size of the table below */

} HashTable;

Size的取值必須爲素數(質數),如取值爲59。則會創建一個連續的長度爲59的hash表。每個單元格的存儲爲指針類型的變量(WebsKey * )。

typedef struct WebsKey {

    struct WebsKey  *forw;                  /* Pointer to next hash list */

    WebsValue       name;                   /* Name of symbol */

    WebsValue       content;                /* Value of symbol */

    int             arg;                    /* Parameter value */

    int             bucket;                 /* Bucket index */ // see to hashIndex()

} WebsKey;

插入到hash表的位置,即索引值訪問是:1 ~ 58。根基hash算法進行映射計算:

  while (*name) {

        sum += (((int) *name++) << i);

        i = (i + 7) % (BITS(int) - BITSPERBYTE);

    }

return sum % tp->size; /* 返回0 ~ 58範圍中的值 */

算法中對字符串name的每個字符進行偏移求和計算,確保每個的求和值相互離散,且降低碰撞的概率。算法中在32位的操作系統上,其實解析該算法的公式爲:(i+7)%24, i取值規律爲:7, 14, 21, 4, 11, 18, 1, 8, 15, 22, 5, 12, 19, 2, 9, 16, 23, 6, 13, 20, 3, 10, 17, 0, 7, 14, ...。

根據該hash算法求出的索引值範圍是:1~(tp->size-1)。然後根據索引值將要插入的內容的地址掛載到hash_table[索引值]下。若出現hash碰撞,即會出現不同的name且存在相同的索引值,此時將要插入的內容的地址掛載到hash_table[索引值]下鏈表的下一節點上。

搜索hash的時候,是根據name的內容依據hash算法計算出對應的索引值,然後返回hash_table[索引值]的地址即可,若該hash_table[索引值]下有多個節點,則根據name值與鏈表節點中的name值比較,找出與name值一致的節點地址取出內容即可。

 

  • Callback的事件調度:

由websStartEvent(int delay, WebsEventProc proc, void *arg)進行註冊事件的回調函數。

typedef struct Callback {

    void        (*routine)(void *arg, int id);

    void        *arg;

    WebsTime    at;

    int         id;

} Callback;

*routine是回調函數的指針,at爲該回調函數執行的時刻,單位爲秒。Id爲該回調函數在主映射表

中的位置的索引值。

s->at = ((delay + 500) / 1000) + time(0); 更新該回調函數的執行時間,delay+500是爲了實現對值的四捨五入。

由websRunEvents(void)來運行所有註冊到Callback中的回調函數,該函數的代碼如下:

 for (i = 0; i < callbackMax; i++) {

        if ((s = callbacks[i]) != NULL) {

            if (s->at <= now) {

                callEvent(i);

                delay = MAXINT / 1000;

                i = -1;

            } else {

                delay = (int) min(s->at - now, MAXINT / 1000);

            }

            nextEvent = min(delay, nextEvent);

        }

    }

當(s->at <= now)代表回調函數的執行時刻到達,執行該回調函數,即 callEvent(i);。I=-1的作用是一旦有回調函數被執行,則在該回調執行完畢中,重新從Callback的起始位置開始驗證所有的回調函數是執行的時刻。原因在於解決,可能出現某一個回調函數中執行太久,以至於錯過在該回調函數之前的Callback[i]的執行時刻。

websStopEvent(int id)停止該回調函數,即將該回調函數所映射的Callback[i]的內容置零。websRestartEvent(int id, int delay)重啓該回調函數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章