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)重启该回调函数。

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