3. 初次创建iFun引擎游戏服务器

在这一章利用基本项目 hello world 来讲述iFun引擎的功能.

3.1. 创建项目

$ funapi_initiator hello_world

输入上面的命令, 会在当前的目录中产生一个叫 hello_world-source 的目录. 可以认为这个目录是项目的源目录, 如果想要使用 GITSVN 这类的版本管理软件, 把这个目录放在储存库就可以.

3.2. 生成构建环境

Note

在这里只介绍在 terminal终端处怎么使用command line tool. 如果想通过 CLionVisualGDB 来对接 Visual Studio 的内容, 请参考 CLion 利用方法Visual Studio 使用方法.

虽然可以从项目的源目录中直接编译, 但是单独创建build目录可以有助于方便简洁的管理. (如果是很多开发者一起工作, 大家把源目录通过SVN 或 GIT进行共享, 然后各自创建自己的build目录就可以. )

$ hello_world-source/setup_build_environment --type=makefile

用上面的命令在现目录中创建一个叫 hello_world-build 的build目录. 然后会在这里面生成一个叫 debug 的目录和一个叫 release 的目录. 可以分别用来建立debug版本和release版本.

Note

Release建立时应打开 -DNDEBUG preprocessing选项, 编译性能优化选项用 -O3 来设置. 除了编译选项不同以外, 其他内容相同.

3.3. 建立游戏服务器

现在移动到 hello_world-build/debug 目录, 输入 make 命令.

$ cd hell_world-build/debug
$ make

创建成功了吗?如果出现了下面的消息, 就说明创建成功了.

...
[100%] Building CXX object src/CMakeFiles/hello_world.dir/hello_world_server.cc.o
Linking CXX shared module libhello_world.so
[100%] Built target hello_world

现在实际运行一下试试. 在创建之后iFun引擎会生成一个 -local 后缀的脚本和一个 -launcher 后缀的脚本. 前者是开发中的服务器运行时使用, 后者是游戏服务器打包后以作为守护程序运行时使用. (例如用 upstart 或者 systemd 运行时). 打包运行时, 因为文件的路径和生成日志的位置都会发生相应改变, 所以脚本要单独利用. 对于打包的详细内容, 会在下面再次进行说明. 现在运行一下我们正在开发的 hello_world-local 吧!

$ ./hello_world-local

运行好了吗?恭喜你. 那么在下一节中讲解联网部分, 并且详细说明如何添加‘客户端-服务器’数据包类型.

3.4. 添加消息处理程序

目前为止创建的服务器虽然可以编译和运行, 但实际上是一个没能力做任何事情的dummy服务器. 如果想要iFun引擎服务器具体操作, 处理从客户端发来的信息, 需要附加上message handler.

尝试添加处理器, 实现如下功能:如果客户端发来"hello"信息时, 回复"world". 从源目录中打开src/event_handlers. cc文件时, 会看到如下代码.

HandlerRegistry::Register("login", OnAccountLogin, login_msg);
HandlerRegistry::Register("echo", OnEchoMessage);

已经知道是什么了吧?如果从客户端发来 loginecho 信息时, 调出处理程序 OnAccountLoginOnEchoMessage 的代码. 在 echo 部分替换上 hello.

HandlerRegistry::Register("hello", OnHello);

然后定义 OnHello 函数. 这个函数将 world 包回复给客户端.

 void OnHello(const Ptr<Session> &session, const Json &message) {
   Json empty_response;
   session->SendMessage("world", empty_response);
}

哇~ 全部完成. 文件保存后进行调试吧! 如果调试的时候没有问题, 那么运行服务器, 试试发送一个 hello信息吧?

$ make
$ ./hello_world-local

3.5. 测试

如果没有特别说明的, 那么使用iFun引擎设置的默认值为TCP 8012 号和 HTTP 8018 号端口. 无论消息是从TCP进来还是从HTTP进来, iFun引擎对同样的数据包调用相同的Handler. 所以这样一来, 就可以按照需求不同很轻松地转换成TCP/UDP/HTTP等传输协定. 这个设置放置在 hello_world/source/src/MANIFEST.json 里面. 找到 SessionService 部分之后, 会看到如下部分.

"SessionService": {
  "tcp_json_port": 8012,
  // ...
  "http_json_port": 8018,
  // ...
},

TCP 和 HTTP都可以使用默认设置, 这里我们简单的利用HTTP进行测试. 因为我们添加了叫 hello 的数据包, 所以 http://localhost:8018/v1/messages/hello 这个URL来调用HTTP中的``POST``方式. HTTP body传送的是空的JSON对象. 在Chrome或Firefox浏览器下安装调用插件也可以调用RESTful API, (在Chrome浏览器中推荐使用 Postman 插件) 也可以如下所示调用terminal终端.

$ wget -qO- --post-data="{}" http://localhost:8018/v1/messages/hello

现在怎么显示的呢?如果出现了如下信息, 表示全部正常. ^_^

{"_sid":"5452bab6-5927-4353-8978-bb35206135ad", "_msgtype":"world"}

我们在没有Session信息的情况下发送了"hello"该信息, iFun引擎生成了新的session, 然后按照我们指示将"world"结果发送了过来.

 如果来自客户端数据包里没有会话信息, iFun引擎会给出一个新的会话ID.  如果客户端发来的是一个无效的会话信息, 为了防止恶意性的攻击, 此时无视, 无需答复客户端.

 iFun引擎的服务器和客户端之间的信息是通过header 和 body 实现的. Header是和HTTP的header同样的形态使用的. (即, 输入的键-值在一行的形态). 并且在Body中可以使用 JSONGoogle Protobuf. 有关iFun引擎的message format 和 session的详细说明请参考 iFun引擎参考手册. 好的! 到现在为止已经完成了利用iFun引擎来制作简单的服务器.

那么在下一节了解一下衔接数据库的方法吧.

Tip

因为iFun引擎的标题格式和HTTP的标题格式一致, 正文格式使用的也是众所周知的JSON或Protobuf, 所以大家可以方便地直接创建客户端模块. 为了实现更快的游戏开发, iFun引擎提供了可以对应各种客户端引擎的客户端插件. 推荐大家看一下 ifun工厂的Github 帐户.

Important

如果开发者用JSON作为正文文件, 由于 _ 开头的字段名是iFun引擎内部使用的, 所以在正文文件中不可以使用 _.

3.6. 对象关系映射和数据库自动处理

游戏开发过程中常常会更改数据库架构. 然后如果改变了数据库的架构, 数据库服务器和游戏服务器就必须根据需求的不同做适当的更改.

如果开发中常常需要改变数据库架构, 那么这将是一个非常繁琐并且是很容易出现失误的过程. (尤其是因为SQL查询语句在内部程序中是被视为字符串处理的, 不会自动查错更新, 这就导致即使有错误, 编译器也不会找出来, 直到实际运行的时候, 才可以发现漏洞). 在iFun引擎中, 我们便可以通过使用ORM (Object Relational Mapping)来解决这个繁琐的工作. iFun引擎会自动生成适合数据库架构的C++类, 如果改变了数据库架构C++类就会自动进行查错更新. 通常在使用iFun引擎的时候, 应该按下方的顺序进行操作.

  1. 服务器程序员应尽量避免更改数据库, 而是用JSON记录游戏中要使用的对象.

  2. 引擎会根据JSON明细表不同, 使用相应的对象类定义用C++代码创建出来. 这个代码用来在数据库中调出对象, 读取或写入, 也包含更改后的内容重新写入数据库的功能. 然后为了像这样的操作在分布式服务器中也可以运行, 同时也包含了分布式加密的处理代码. 所以服务器程序员不需要直接写入SQL查询语句, 也没必要实现分布加密的处理.

  3. 用引擎驱动游戏服务器时, 系统会连接到数据库, 查看有没有按照JSON明细表生成的表格, 还有确认所需要的列. 万一发现有缺少的信息, 将会重新生成.

Tip

为了安全起见iFun引擎不会触碰原有的列, 而且没有记录到JSON里的列也不会触碰. 以此, 在数据库中生成的类似于index的其他项目, 以及像客服系统这样的外部系统中将要使用的字段或表, 都可以自由的添加. 虽然说了很多, 眼见为实!首先打开 MANIFEST.json 中的数据库设置, 然后输入访问信息. 假设开发的服务器中MySQL或者MariaDB都正常运转时, 一个叫 funapi 的数据库用户有访问名为 funapi 数据库架构的权限.

"Object": {
  // ...
  "enable_database" : true,
  "db_mysql_server_address" : "tcp://127.0.0.1:3306",
  "db_mysql_id" : "funapi",
  "db_mysql_pw" : "funapi",
  "db_mysql_database" : "funapi",
  // ...
},

enable_database 变成 true 就可以了.

Tip

如果没有安装好mysql server, 那么进行如下操作就可以.

$ sudo apt-get install mysql-server

$ mysql -u root -p

现在如果出现了mysql shell, 那么请像下面一样输入. 只输入mysql>后面的就可以.

mysql> create user 'funapi'@'localhost' identified by 'funapi';

mysql> grant all privileges on *.* to 'funapi'@'localhost';

mysql> create database funapi;

现在将发送记录游戏对象的JSON文件. hello_world-source/src/object_model/example.json 里面的内容如下所示.

{
  "User": {
    "Id": "String KEY",
    "MyCharacter": "Character"
  },
  "Character": {
    "Name": "String KEY",
    "Exp": "Integer",
    "Level": "Integer",
    "Hp": "Integer",
    "Mp": "Integer"
  }
}

这个内容是游戏对象的JSON明细表信息. 我们可以发现有 User 类和 Character 类, User 拥有可以键入的字符串的 Id, 也拥有 Character 类的 MyCharacter. 并且拥有 Character 类里面的名字, 经验值, 等级, HP, MP类似的基本信息. 驱动游戏服务器时, 访问 MANIFEST.json 中指定的数据库, 确认是否存在叫 User 表和叫 Character 表, 如果没有就会重新生成. 还有确认生成的数据库表里面, 是否有如前面所述的以字段存在, 如果没有的话重新添加. 此外往回看 hello_world-source/src/hello_world_object.h 文件和 hello_world-source/src/hello_world_object.cc 文件, 可以看到一个 User 和一个 Character 的C++类. 而且还可以发现在每个类中有 Create(...)Fetch(...) 方法存在. 在每个数据库上面创建对象, 是加载创建对象的方法. 此外可以知道, 每个字段是以 GetXXX(), SetXXX() 形态的方法创建出的. 这两个方法是用来读写每个字段的值.

class Character;
class User;


class Character : public ObjectProxy {
  // ...

  // Create
  static Ptr<Character> Create(const string &name);

  // Fetch by object id
  static Ptr<Character> Fetch(
      const Object::Id &id,
      LockType lock_type = kWriteLock);
  static void Fetch(
      const std::vector<Object::Id> &ids,
      std::vector<std::pair<Object::Id, Ptr<Character> > > *result,
      LockType lock_type = kWriteLock);

  // Fetch by Name
  static Ptr<Character> FetchByName(
      const string &value,
      LockType lock_type = kWriteLock);
  static void FetchByName(
      const std::vector<string> &values,
      std::vector<std::pair<string, Ptr<Character> > > *result,
      LockType lock_type = kWriteLock);

  ...

  // Getter/Setter for 'Name' attribute
  string GetName() const;
  void SetName(const string &value);

  // Getter/Setter for 'Exp' attribute
  int64_t GetExp() const;
  void SetExp(const int64_t &value);

  // Getter/Setter for 'Level' attribute
  int64_t GetLevel() const;
  void SetLevel(const int64_t &value);

  // Getter/Setter for 'Hp' attribute
  int64_t GetHp() const;
  void SetHp(const int64_t &value);

  // Getter/Setter for 'Mp' attribute
  int64_t GetMp() const;
  void SetMp(const int64_t &value);

  // ...
};

因为目前已经打开了 MANIFEST.jsonenable_database, 所以要重启游戏服务器.

游戏服务器需要在MySQL DB中生成表格.

$ ./hello_world-local  (运行后用 Ctrl+c终止服务器.)

$ mysql -u funapi -p funapi

mysql> show tables;
+-----------------------+
| Tables_in_funapi      |
+-----------------------+
| tb_Key_Character_Name |
| tb_Key_User_Id        |
| tb_ObjectLock         |
| tb_Object_Character   |
| tb_Object_User        |
+-----------------------+
5 rows in set (0.00 sec)

mysql> desc tb_Object_Character;
+----------------+---------------+------+-----+------------------+-------+
| Field          | Type          | Null | Key | Default          | Extra |
+----------------+---------------+------+-----+------------------+-------+
| col__ObjectId_ | binary(16)    | NO   | PRI |                  |       |
| col_Name       | varchar(4096) | YES  | UNI | NULL             |       |
| col_Exp        | bigint(8)     | YES  |     | NULL             |       |
| col_Level      | bigint(8)     | YES  |     | NULL             |       |
| col_Hp         | bigint(8)     | YES  |     | NULL             |       |
| col_Mp         | bigint(8)     | YES  |     | NULL             |       |
| col__tag       | varchar(4096) | YES  |     | NULL             |       |
+----------------+---------------+------+-----+------------------+-------+
7 rows in set (0.00 sec)

现在可以确认数据库已经自动生成了. 现在让我们在 Character 中尝试着添加 Sp 吧. 通常来讲是手动在数据库中添加表的, 主要是为了让服务器代码也可以使用Sp, 所以必须手动添加.

不过在iFun引擎当中只要更新JSON明细表就可以实现. 如下所示更新 hello_world-source/src/object_model/example.json.

{
  "User": {
    "Id": "String KEY",
    "MyCharacter": "Character"
  },
  "Character": {
    "Name": "String KEY",
    "Exp": "Integer",
    "Level": "Integer",
    "Hp": "Integer",
    "Mp": "Integer",
    "Sp": "Integer"
  }
}

现在重新构建.

$ make

hello_world-source/src/hello_world_object.hCharacter 类中, 可以看到添加的Sp的相关内容.

 class Character : public ObjectProxy {
   // ...
   // Getter/Setter for 'Sp' attribute
   int64_t GetSp() const;
   void SetSp(const int64_t &value);
   // ...
 };

现在重启一次服务器看看怎么样? 需要在数据库的``Character``表格中需要自动添加叫``Sp``列. 让我们来看看.

 $ ./hello_world-local  (运行后用 Ctrl+c终止服务器.)
 $ mysql -u funapi -p funapi

 mysql> desc tb_Object_Character;
 +----------------+---------------+------+-----+------------------+-------+
 | Field          | Type          | Null | Key | Default          | Extra |
 +----------------+---------------+------+-----+------------------+-------+
 | col__ObjectId_ | binary(16)    | NO   | PRI |                  |       |
 | col_Name       | varchar(4096) | YES  | UNI | NULL             |       |
 | col_Exp        | bigint(8)     | YES  |     | NULL             |       |
 | col_Level      | bigint(8)     | YES  |     | NULL             |       |
 | col_Hp         | bigint(8)     | YES  |     | NULL             |       |
 | col_Mp         | bigint(8)     | YES  |     | NULL             |       |
 | col_Sp         | bigint(8)     | YES  |     | NULL             |       |
 | col__tag       | varchar(4096) | YES  |     | NULL             |       |
 +----------------+---------------+------+-----+------------------+-------+
 7 rows in set (0. 00 sec)

看到了吧?即使改变数据库架构, 游戏内的对象类并不需要手动改变, 并且数据库也会自动更新. 最后看一下实际的对象是怎样生成的吧. 插入在前面制作的 OnHello 中生成Character的代码. 即, 如果调用了 hello, 现在只需要创建一个Character实例写入数据库中.

void OnHello(const Ptr<Session> &session, const Json &message) {
  Ptr<Character> ch = Character::Create("ifun");
  if (!ch) {
    LOG(ERROR) << "Already exists";
  } else {
    LOG(INFO) << "Created";
  }

  Json empty_response;
  session->SendMessage("world", empty_response);
}

现在如果接收到 hello, 服务器就会创建一个叫 ifun 的Character. 请注意 Character 的JSON明细表是以 Name 定义的. 在明细表中存在关键字段的话, Create函数会将这些字段全部作为参数. 下面代码是以 ifun 命名的character的创建代码.

重新创建游戏服务器, 然后利用以下命令来确认结果. (也可以使用前面所述的Chrome或Firefox浏览器的RESTful API插件来代替Shell命令.)

$ wget -qO- --post-data="{}" http://localhost:8018/v1/messages/hello

在MySQL中进行确认.

$ mysql -u funapi -p funapi
mysql> select * from tb_Object_Character;
+------------------+----------+---------+-----------+--------+--------+--------+----------+
| col__ObjectId_   | col_Name | col_Exp | col_Level | col_Hp | col_Mp | col_Sp | col__tag |
+------------------+----------+---------+-----------+--------+--------+--------+----------+
| 2bf2da6643904... | ifun     |       0 |         0 |      0 |      0 |      0 |          |
+------------------+----------+---------+-----------+--------+--------+--------+----------+
1 row in set (0. 01 sec)

已经完成人物的制作! 现在已经附加上了可以处理客户端发来的数据包的处理器, 而且数据库也衔接就绪. 像这样, 用服务器来配置的游戏服务器才可以进行运行. 但是现实世界中, 服务器是需要进行扩张的. 在下一章节中将了解这方面的iFun引擎分布式系统的功能.

3.7. 共享游戏服务器Flavor 和代码的同时, 以不同方式运行服务器

很多时候为了配置游戏服务, 会需要很多功能不同的服务器. 例如, 大厅服务器, 游戏服务器, 聊天服务器. 在iFun引擎中像这种功能分类的叫做 服务器 flavor. 不同的Flavor存在不同的 MANIFEST. json, 封装的时候也会把独立的压缩文件或者包文件绑定在一起. 在 hello_world-source/CMakeLists.txt 里面配置flavor. 添加以下内容.

# 添加下行.
set(APP_FLAVORS lobby game)

# ...

# 下列内容是原有就有的内容. 上面的那行需要放在include之前
set(CMAKE_MODULE_PATH "/usr/share/funapi/cmake")
include(Funapi)

现在, 让尝试一个新的构建.

$ make

$ ls *local *launcher
hello_world-launcher* hello_world-local* hello_world.game-launcher* hello_world.game-local* hello_world.lobby-launcher* hello_world.lobby-local*

$ ls ../../hello_world-source/src/MANIFEST.*
../../hello_world-source/src/MANIFEST.game.json ../../hello_world-source/src/MANIFEST.json ../../hello_world-source/src/MANIFEST.lobby.json

在此之前, 仅仅有 hello_world-localhello-world-launch, 但是现在可以知道每个flavor都会生成文件. 同时复制原有的 MANIFEST.json, 可以看到每个flavor都会产生一个MANIFEST.json.

Tip

在游戏服务器中可以根据FLAGS_app_flavor的谷歌标识来确认flavor的种类.

Flavor的详细说明请参考 参考文献的 Flavor 项目.

3.8. 分布式系统功能

不需要在意大厅和游戏浮动到其他服务器上, 因为我们打算把它们放在一个服务器上, 为了不发生冲突, 需要更改MANIFEST. json上绑定的TCP端口. 此外, 还需要打开分布功能. 打开 hello_world-source/src/MANIFEST.game.json 按照如下所示修改端口.

// ...
"tcp_json_port": 9012,

"http_json_port": 9018,

"api_service_port": 9014

"rpc_enabled": true,
"rpc_port": 9015,
// ...

只需要打开 hello_world-source/src/MANIFEST.lobby.json 的分布功能.

// ...
"rpc_enabled": true,
// ...

为了测试在大厅服务器上登录的用户在游戏服务器上也可以得到确认, 需要添加一个叫 signin 的客户端包和 check 客户端包. 在 hello_world-src/src/event_handlers.cc 添加下面内容.

// 下面的函数是叫ifun的用户登录进行处理的.
void OnSignin(const Ptr<Session> &session, const Json &message) {
  bool r = AccountManager::CheckAndSetLoggedIn("ifun", session);
  Json response;
  response["result"] = r;
  session->SendMessage("singin_reply", response);
}

// 下面的函数是查看ifun用户登录的是哪个服务器.
// 如果没有登录服务器, 那么服务器ID会变成0000-00. . . 形态的Null UUID.
void OnCheck(const Ptr<Session> &session, const Json &message) {
  Rpc::PeerId server_id = AccountManager::Locate("ifun");
  Json response;
  response["result"] = boost::lexical_cast<string>(server_id);
  session->SendMessage("check_reply", response);
}


void RegisterEventHandlers() {
  // ...
  HandlerRegistry::Register("signin", OnSignin);
  HandlerRegistry::Register("check", OnCheck);
  // ...
}

最后, iFun引擎是利用Apache Zookeeper来实现分布功能. 如果没有安装Zookeeper, 现在安装Zookeeper吧.

$ sudo apt-get install zookeeper zookeeperd

如果在其他服务器已经安装了, 那么只需要在MANIFEST文件中的 zookeeper_nodes 上输入IP/port就可以了. 现在准备都已就绪. 使用的大厅服务器是8018号端口, 游戏服务器是9018号端口. 各自用script来运行它们.

# 在一个terminal终端上
$ ./hello_world.lobby-local
# 在其他terminal终端上
$ ./hello_world.game-local

现在往8018号大厅服务器传送signin来使ifun实现登录.

$ wget -qO- --post-data="{}" http://localhost:8018/v1/messages/signin
{"result":true, "_sid":"497844e8-1885-40ed-b7d7-29f2931ddac2", "_msgtype":"singin_reply"}

和期待的一样结果是true对吧. 试试重复登录怎么样?

$ wget -qO- --post-data="{}" http://localhost:8018/v1/messages/signin
{"result":false, "_sid":"f0a511c0-ad16-4b1b-917f-f86da7809814", "_msgtype":"singin_reply"}

被视为已经登录了所以结果是false. 如果往9018号游戏服务器发送signin会怎么样呢?

$ wget -qO- --post-data="{}" http://localhost:9018/v1/messages/signin
{"result":false, "_sid":"52a48a4a-58ef-462c-a08b-92371d298382", "_msgtype":"singin_reply"}

和预想一样被视为已经登录对吧. 但是怎么样才能知道各自都登录了哪个服务器吗?尝试往大厅服务器和游戏服务器发送check邀请来确认吧.

$ wget -qO- --post-data="{}" http://localhost:8018/v1/messages/check
{"result":"aa561332-8aad-4f1f-0000-000000000000", "_sid":"622e4ae2-3793-41ac-97ab-708840416a6d", "_msgtype":"check_reply"}

$ wget -qO- --post-data="{}" http://localhost:9018/v1/messages/check
{"result":"aa561332-8aad-4f1f-0000-000000000000", "_sid":"8bff8116-5583-40f7-b363-c18929ac9400", "_msgtype":"check_reply"}

可以知道用户ifun同时存在于两个服务器中. 大厅服务器和游戏服务器之间的信息实现了很好的共享! 我们已经现在学习到了分布式系统服务. 下面的章节中, 尝试添加游戏服务器程序的包管理和管理用到的RESTful API.

3.9. 游戏服务器打包

生成游戏服务器后, 分配binary是一个非常重要的事情. 但是这个过程也会出现很多失误. 大部分的失误是漏掉文件, 或者是不能复制等问题. 因此为了分配服务器, 可以把包都绑定在一起更为可取, 这个最好不要人为绑定, 程序自动绑定的比较好.

在iFun引擎的服务器创建内容中, 运行 make package 会自动绑定包. 并且包的格式有支持Ubuntu的.deb文件格式, 支持Centos的.rpm格式, 还有支持Linux常用的的TGZ格式. 想要的包格式可以在 hello_world-source/CMakeLists.txt 中配置. 因为我们使用的是Ubuntu, 所以配置成以下格式.

# Generate a distribution package in tgz?
set(WANT_TGZ_PACKAGE false)

# Generate a distribution package in Debian DEB?
set(WANT_DEB_PACKAGE true)

# Generate a distribution package in Redhat RPM?
set(WANT_RPM_PACKAGE false)

现在运行make package. 会看到有. deb文件生成.

$ make package
Leveraging ccache. CCACHE=/usr/bin/ccache, CCACHE_DIR=, CCACHE_TEMPDIR=.
/tmp/hello_world-source/etc/upstart/default/hello_world. lobby not found. Skipping.
/tmp/hello_world-source/etc/upstart/init/hello_world. lobby. conf not found. Skipping.
/tmp/hello_world-source/etc/upstart/default/hello_world. game not found. Skipping.
/tmp/hello_world-source/etc/upstart/init/hello_world. game. conf not found. Skipping.
-- Configuring done
-- Generating done
-- Build files have been written to: /tmp/hello_world-build/debug
[ 16%] Built target internal_create_launchers
[ 25%] Built target internal_import_manifest_dirs
[ 33%] Built target internal_import_resource_dirs
[100%] Built target hello_world
Run CPack packaging tool...
CPack: Create package using DEB
CPack: Install projects
CPack: - Run preinstall target for: hello_world
CPack: - Install project: hello_world
CPack: -   Install component: game
CPack: -   Install component: lobby
CPack: Create package
CPackDeb: - Generating dependency list
CPackDeb: - Generating dependency list
CPack: - package: /tmp/hello_world-build/debug/hello-world_0.0.1_install-game.deb generated.
CPack: - package: /tmp/hello_world-build/debug/hello-world_0.0.1_install-lobby.deb generated.

$ ls *.deb
hello-world_0.0.1_install-game.deb  hello-world_0.0.1_install-lobby.deb

Tip

如果没有配置flavor. 只会产生一个. deb文件, 如果配置了flavor, 每个flavor都会生成文件. 现在把这些文件移动到服务器, 然后用Ubuntu的 dpkg 命令便可以安装游戏服务器.

3.10. 游戏服务器包的版本和版本号

在上一章生成的.deb文件们的版本号被配置成 0.0.1. 大家可能会好奇这个版本号是从哪里来的. 这个号使用的是 hello_world-source/VERSION 中记载的. 如果改成0.0.2然后进行make package, 那么就会出现0.0.2版本的文件. 版本一般指的就是伴随游戏的升级而上升的数字. 所以上传时需要手动更新版本文件. 但是根据很多的情况来看, 每当创建的时候都附加上创建序号的话会更加方便. 用iFun引擎管理SVN源和GIT源的时候, SVN修订版号或者GIT提交ID版本的后面都会附加上创建序号.

hello_world-source/CMakeLists.txt 中选择以下变量, 然后修改成true.

# Source managed by git can append a build number from git commit id.
set(PACKAGE_WITH_BUILD_NUMBER_FROM_GIT false)

# Source managed by svn can append a build number from svn info.
set(PACKAGE_WITH_BUILD_NUMBER_FROM_SVN false)

Tip

如果还存在没有提交到源目录的内容, iFun引擎会附加 dirty 字符串. 例如像 0. 0. 1-1234~dirty 一样.

3.11. 为了连接操作工具添加管理API

在做游戏服务的过程中, 常常会有连接监控工具或者其他客户支持工具的情况. 连接这些工具的方法虽然有很多种, 但更简便的方法是用游戏服务器调出RESTful API, 然后用这些工具直接调用这个API. 在iFun引擎中就可以这样简单的可以添加RESTful API. 考虑尝试封锁或者解除用户角色的情况. 首先要在character中添加Blocked字段. hello_world-source/src/object_model/example.json 如下所示进行修改.

{
  "User": {
    "Id": "String KEY",
    "MyCharacter": "Character"
  },
  "Character": {
    "Name": "String KEY",
    "Exp": "Integer",
    "Level": "Integer",
    "Hp": "Integer",
    "Mp": "Integer",
    "Sp": "Integer",
    "Banned": "bool"
  }
}

然后是添加封锁API的URL, 在 hello_world-source/src/event_handlers. cc 中添加如下内容. 通过根据相应的URL传送来的character 和 state的值, 在数据库中储存是否封锁的信息的代码. 注意URL使用的是Perl的正规形态, (?<user> ) 作为其中一种模式匹配结果, 是访问 user 的手段.

void OnBlockCharacter(
    Ptr<http::Response> response,
    const http::Request &request,
    const ApiService::MatchResult &params) {
  string character = params["character"];
  string state = params["state"];
  LOG(INFO) << "character: [" << character << "]";
  LOG(INFO) << "state: [" << state << "]";

  bool result = false;
  Ptr<Character> c = Character::FetchByName(character);
  if (c) {
    c->SetBanned(state == "1");
    result = true;
  }

  response->status_code = http::kOk;
  response->header. insert(std::make_pair("Content-Type", "plain/text"));
  response->body = result;
}


void RegisterEventHandlers() {
  // ...
  ApiService::RegisterHandler(
      http::kPost,
      boost::regex("/v1/cs/ban/(?<character>\\w+)/(?<state>\\w+)"),
      OnBlockCharacter);

  // ...
}

API服务器的端口号可以在MANIFEST中设置. hello_world-source/src/MANIFEST.lobby. json 的默认值是8014.

// ...
"ApiService": {
  "api_service_port": 8014
},
// ...

现在尝试调用RESTful API. 如前面所说, 可以直接使用Chrome或Firefox中的插件, 或者在terminal终端进行如下操作. 登陆API的时候, 注意要用 http::kPost 使用POST方法.

调出大厅服务器.

$ ./hello_world.lobby-local

调用大厅服务器的API.

$ wget --post-data="{}" -qO- http://localhost:8014/v1/cs/ban/ifun/1

服务器端会出现下方的log信息吗?

I0309 16:43:52.674099 17302 api_service.cc:81] "POST /v1/cs/ban/ifun/1" 200

感觉怎么样? 很轻松的实现了停止用户角色的功能! 除此之外还有默认提供的管理API. 如果调用 GET /v1/ 就会发现, 在游戏服务器中可以确认有哪些API可以被调用.

$ wget -qO- http://localhost:8014/v1/
[
    {
        "method": "GET",
        "url": "/v1/"
    },
    {
        "method": "GET",
        "url": "/v1/counters/"
    },
    {
        "method": "GET",
        "url": "/v1/counters/all/"
    },
    {
        "method": "GET",
        "url": "/v1/counters/(?<type>\\w+)/"
    },
    {
        "method": "GET",
        "url": "/v1/counters/(?<type>\\w+)/(?<id>\\w+)/"
    },
    {
        "method": "GET",
        "url": "/v1/counters/(?<type>\\w+)/(?<id>\\w+)/description/"
    },
    {
        "method": "GET",
        "url": "/v1/maintenance/"
    },
    {
        "method": "GET",
        "url": "/v1/configurations/"
    },
    {
        "method": "GET",
        "url": "/v1/configurations/(?<name>\\w+)/"
    },
    {
        "method": "POST",
        "url": "/v1/cs/ban/(?<character>\\w+)/(?<state>\\w+)/"
    },
    {
        "method": "PUT",
        "url": "/v1/maintenance/update/"
    },
    {
        "method": "PUT",
        "url": "/v1/configurations/(?<name>\\w+)/(?<value>\\w+)/"
    }
]

在上述的API当中, 和计数器相关的API部分, 对于监控服务器非常有用. 主机, 操作系统, 引擎状态值都可以曝光给各种计数器, 而且程序员也可以在计数器上做一些添加. 例如, 对于实际当前正在运行的服务器 (不是储存在数据库中的, 而是 run-time中的), 通过计数器接口可以曝光出昂贵物品的数量等. 这个数据虽然不是数据库中储存的值, 那个数值如果急速变化会有助于检测服务器运行中的物品复制情况. 详细内容请参考 ifun引擎参考手册.

$ wget -qO- http://localhost:8014/v1/counters
[
  "process",
  "funapi_object_model",
  "os",
  "funapi"
]
$ wget -qO- http://localhost:8014/v1/counters/funapi/sessions
0