游戏架构 游戏架构设计(4)


4.1、服务器内存管理

  对于服务器内存管理我们将采用内存池的方法,也称为静态内存管理。其概念为在服务器初始化的时候,申请一块非常大的内存,称为内存池(Memory pool),同时也申请一小块内存空间,称为垃圾回收站(Garbage recollecting station)。其大体思路如下:当程序需要申请内存的时候,首先检查垃圾回收站是否为空,如果不为空的话,则从垃圾回收站中找一块可用的内存地址,在内存池中根据地址找到相应的空间,分配给程序用,如果垃圾回收站是空的话,则直接从内存池的当前指针位置申请一块内存;当程序释放空间的时候,给那块内存打上已经释放掉的标记,然后把那块内存的地址放入垃圾回收站。
  下面具体谈谈该方法的详细设计,首先,我们将采用类似于操作系统的段页式系统来管理内存,这样的好处是可以充分的利用内存池,其缺点是管理起来比较麻烦。嗯,下面来具体看看我们怎么样来定义页和段的结构:

  typedef struct m_segment_s
  {
    struct m_segment_s *next; /* 双线链表 + 静态内存可以达到随机访问和顺序访问的目的,
                   真正的想怎么访问,就怎么访问。 */
    struct m_segment_s *pre; int flags;  // 该段的一些标记。
    int start;              // 相对于该页的首地址。
    int size;               // 长度。
    struct m_page_s *my_owner;      // 我是属于哪一页的。
    char *data;              // 内容指针。
  }m_segment_t;

  typedef struct m_page_s
  {
    unsigned int flags;   /* 使用标记,是否完全使用,是否还有空余 */
    int size;        /* 该页的大小,一般都是统一的,最后一页除外 */
    int end;         /* 使用到什么地方了 */
    int my_index;      /* 提供随机访问的索引 */
    m_segment_t *segments;  // 页内段的头指针。
  }m_page_t;

  那么内存池和垃圾回收站怎么构建呢?下面也给出一些构建相关的伪代码:

  static m_page_t *all_pages;
  // total_size是总共要申请的内存数,num_pages是总共打算创建多少个页面。
  void initialize_memory_pool( int total_size, int num_pages )
  {
    int i, page_size, last_size;    // 算出每个页面的大小。
    page_size = total_size / num_pages; // 分配足够的页面。
    all_pages = (m_page_t*) calloc( num_pages, sizeof(m_page_t*) );
    for ( i = 0; i < num_pages; i ++ )
    {
      // 初始化每个页面的段指针。
      all_pages[i].m_segment_t = (m_segment_t*) malloc( page_size );
      // 初始化该页面的标记。
      all_pages[i].flags |= NEVER_USED;
      // 除了最后一个页面,其他的大小都是page_size 大小。
      all_pages[i].size = page_size;
      // 初始化随机访问的索引。
      all_pages[i].my_index = i;
      // 由于没有用过,所以大小都是0
      all_pages[i].end = 0;
    }

    // 设置最后一个页面的大小。
    if ( (last_size = total_size % num_pages) != 0 )
      all_pages[i].size = last_size;
  }

  下面看看垃圾回收站怎么设计:

  int **garbage_station;
  void init_garbage_station( int num_pages, int page_size )
  {
    int i;
    garbage_station = (int**) calloc( num_pages, sizeof( int* ) );
    for ( i = 0; i < num_pages; i ++)
    {
      // 这里用unsigned short的高8位来储存首相对地址,低8位来储存长度。
      garbage_station[i] = (int*) calloc( page_size, sizeof( unsigned short ));
      memset( garbage_station[i], 0, sizeof( garbage_station[i] ));
    }
  }

  也许这样的贴代码会让大家觉得很不明白,嗯,我的代码水平确实不怎么样,那么下面我来用文字方式来叙说一下大体的概念吧。对于段页式内存管理,首先分成N个页面,这个是固定的,而对于每个页面内的段则是动态的,段的大小事先是不知道的,那么我们需要回收的不仅仅是页面的内存,还包括段的内存,那么我们就需要一个二维数组来保存是哪个页面的那块段的地址被释放了。然后对于申请内存的时候,则首先检查需要申请内存的大小,如果不够一个页面大小的话,则在垃圾回收站里面寻找可用的段空间分配,如果找不到,则申请一个新的页面空间。
  这样用内存池的方法来管理整个游戏世界的内存可以有效的减少内存碎片,一定程度的提高游戏运行的稳定性和效率。

4.2、游戏中物件的寻址

  第一个问题,我们为什么要寻址?加入了脚本语言的概念之后,游戏中的一些逻辑物件,比如说NPC,某个ITEM之类的都是由脚本语言在游戏运行的过程中动态生成的,那么我们通过什么样的方法来对这些物件进行索引呢?说得简单一点,就是如何找到他们呢?有个很简单的方法,全部遍历一次。当然,这是个简单而有效的方法,但是效率上的消耗是任何一台服务器都吃不消的,特别是在游戏的规模比较大之后。

  那么,我们怎么来在游戏世界中很快的寻找这些物件呢?我想在谈这个之前,说一下Hash Table这个数据结构,它叫哈希表,也有人叫它散列表,其工作原理是不是顺序访问,也不是随机访问,而是通过一个散列函数对其key进行计算,算出在内存中这个key对应的value的地址,而对其进行访问。好处是不管面对多大的数据,只需要一次计算就能找到其地址,非常的快捷,那么弊端是什么呢?当两个key通过散列函数计算出来的地址是同一个地址的时候,麻烦就来了,会产生碰撞,其的解决方法非常的麻烦,这里就不详细谈其解决方法了,否则估计再写个四,五章也未必谈得清楚,不过如果大家对其感兴趣的话,欢迎讨论。

  嗯,我们将用散列表来对游戏中的物件进行索引,具体怎么做呢?首先,在内存池中申请一块两倍大于游戏中物件总数的内存,为什么是两倍大呢?防止散列表碰撞。然后我们选用物件的名称作为散列表的索引key,然后就可以开始设计散列函数了。下面来看个例子:

  static int T[] =
  {
    1, 87, 49, 12, 176, 178, 102, 166, 121, 193, 6, 84, 249, 230, 44, 163,
    14, 197, 213, 181, 161, 85, 218, 80, 64, 239, 24, 226, 236, 142, 38, 200,
    110, 177, 104, 103, 141, 253, 255, 50, 77, 101, 81, 18, 45, 96, 31, 222,
    25, 107, 190, 70, 86, 237, 240, 34, 72, 242, 20, 214, 244, 227, 149, 235,
    97, 234, 57, 22, 60, 250, 82, 175, 208, 5, 127, 199, 111, 62, 135, 248,
    174, 169, 211, 58, 66, 154, 106, 195, 245, 171, 17, 187, 182, 179, 0, 243,
    132, 56, 148, 75, 128, 133, 158, 100, 130, 126, 91, 13, 153, 246, 216, 219,
    119, 68, 223, 78, 83, 88, 201, 99, 122, 11, 92, 32, 136, 114, 52, 10,
    138, 30, 48, 183, 156, 35, 61, 26, 143, 74, 251, 94, 129, 162, 63, 152,
    170, 7, 115, 167, 241, 206, 3, 150, 55, 59, 151, 220, 90, 53, 23, 131,
    125, 173, 15, 238, 79, 95, 89, 16, 105, 137, 225, 224, 217, 160, 37, 123,
    118, 73, 2, 157, 46, 116, 9, 145, 134, 228, 207, 212, 202, 215, 69, 229,
    27, 188, 67, 124, 168, 252, 42, 4, 29, 108, 21, 247, 19, 205, 39, 203,
    233, 40, 186, 147, 198, 192, 155, 33, 164, 191, 98, 204, 165, 180, 117, 76,
    140, 36, 210, 172, 41, 54, 159, 8, 185, 232, 113, 196, 231, 47, 146, 120,
    51, 65, 28, 144, 254, 221, 93, 189, 194, 139, 112, 43, 71, 109, 184, 209,
  };

  // s是需要进行索引的字符串指针,maxn是字符串可能的最大长度,返回值是相对地址。
  inline int whashstr(char *s, int maxn)
  {
    register unsigned char oh, h;
    register unsigned char *p;
    register int i;

    if (!*s)
      return 0;
    p = (unsigned char *) s;
    oh = T[*p]; h = (*(p++) + 1) & 0xff;
    for (i = maxn - 1; *p && --i >= 0; )
    {
      oh = T[oh ^ *p]; h = T[h ^ *(p++)];
    }
    return (oh << 8) + h;
  }

  具体的算法就不说了,上面的那一大段东西不要问我为什么,这个算法的出处是CACM 33-6中的一个叫Peter K.Pearson的鬼子写的论文中介绍的算法,据说速度非常的快。有了这个散列函数,我们就可以通过它来对世界里面的任意物件进行非常快的寻址了。

你可能感兴趣的:(云计算,游戏开发,技术架构)