Recording

Commit volatile memory to persistent append-only log

0%

每个全局功能(需要和其他玩家交互的功能,例如聊天,公会,副本和场景等)都作为一个独立服务来做,方便扩展。这个服务可以是逻辑服务器内嵌的(如果没有与其他服务器交互的需求),也可以是一个单独的服务器,甚至一个服务器集群。

在分区游戏中,很多时候某些全局功能都作为一个功能模块内嵌到逻辑上去了,这样在某些需要服务器间玩家交互的功能时,就会很难做。例如: 如果聊天/好友服务内嵌到逻辑服务器上时,玩家如果需要与其他服务器上的好友(在跨服功能中加为好友的)通信时就会很麻烦。

大多可能有很大压力的全局功能都采用服务器集群的做法,集群中的成员:

  • 一个或多个(分层的或平行的)管理服务器,职责:
    • 根据某种策略(id 哈希/映射,服务器压力统计等)路由外部请求到具体的工作服务器;
    • 工作服务器的新增(外部请求或根据工作强度向其他服务请求启动新的工作服务器);
    • 工作服务器的停机
  • 多个工作服务器,职责:
    • 根据请求 id,处理对应的模块实体;
    • 向管理服务器汇报服务器压力

实现:

gate

  • 多个 gate 管理服务器,外网接入:
    • 由 DNS 根据线路等指向或者固定 ip,client 接入时,会根据玩家的线路和 gate 的压力情况选择适合的 gate ip:port 返回给玩家;
    • gate 接入,外网 ip:port 注册
  • 多个 gate ,外网接入:
    • 玩家接入,认证等等;
    • 向 gate 管理服务器发送在线压力情况;
    • 和在线统计服务器和逻辑服务器合作,负责玩家的登入及之后的消息中转

登入

  • 一个或多个在线统计服务器:

    • 如果不存在玩家记录,根据策略(id 哈希,服务器压力等)选择一个逻辑服务器供玩家登录 [player: zzz, id: xxx, ref: yyy];
    • 如果存在玩家记录,直接返回其所在逻辑服务器 [player: zzz, id: xxx, ref: yyy+1](引用加1,防止逻辑服务器的错位删除)
    • 监控逻辑服务器的状态,逻辑服务器宕机后,标记其状态为宕机,避免玩家登入时再次接入已经宕机的逻辑服务器。
    • 做成无状态的服务,所有数据都存 Redis(这些数据都是在线玩家和所在逻辑服务器的映射,只需存在内存中),避免过多的逻辑,以及宕机后的自动恢复。 (服务与存储分开的关键是,数据能够原子的更新, Redis 的 Lua Script 都可以做到这点。当然,也可以写一个定制的内存服务程序来解决这点。)
  • 多个逻辑服务器:

    • 玩家个体逻辑的大部分功能都由该服务器实现 (如果玩家逻辑功能比较复杂,可以用动态语言+虚拟机方式实现,这样当某个玩家或某个功能出现异常时,缩小影响范围);
    • 在玩家登出后的有限时间内清空玩家数据,并向在线服务器(或 Redis)发送清空玩家对该服务器的引用[player: zzz, ref: yyy]

client 在 gate 完成登录认证,由 gate 向在线统计服务器发送请求选择逻辑服务器 [id: xxx, ref: yyy] , gate 向逻辑服务器发送玩家的登入请求 [player: zzz, ref: yyy] ,玩家进入游戏。

Read more »

Lua coroutine

Lua coroutine 是协作式多任务的一种实现。coroutine 经常被 C/C++ 这类没用原生轻量级多任务实现的宿主语言通过异步方式实现 Lua 层的同步调用。

这种同步调用的流程:

val, err = synchronous_call_in_lua(...)    -- function call in lua coroutine
    ==> coroutine.yield
        ==> C/C++ 发起异步请求会话
        ==| (time consumed)
        ==| C/C++ 异步会话完成(消息/回调)
    <== coroutine.resume
val, err = ... -- <== return from function
do_someting(...)

这个流程很像阻塞式的系统调用:

n = read(fd, buf, len)        // system call in userland
    ==> syscallenter(...)     // in kernel mode
        |=> sys_read(...)     // in system call
        |== kernel activity   // asynchronous wait if data is not available yet
        |==
    <== syscallret(...)       // return from syscall
n = ...                       // return from read in userland

C# async/await

async/await 是 C# 5 引入的,用来简化异步编程的设施,其本质是由编译器实施的 CPS 变换。

C# 5.0 introduces the async and await keywords. These keywords let you write asynchronous code that has the same structure and simplicity as synchronous code, as well as eliminating the “plumbing” of asynchronous programming. The await keyword simplifies the attaching of continuations.

即将如下代码:

Read more »

C++11 的 auto 和 decltype 可以做 “返回值类型推断”。

我写了个两参数版本的 std::accumulate

1
2
3
4
5
6
7
8
9
10
11
#include <numeric>
#include <type_traits>
namespace std {

template <typename InputIt>
auto accumulate(InputIt begin, InputIt end) -> typename std::remove_reference<decltype(*begin)>::type {
typedef typename std::remove_reference<decltype(*begin)>::type ValueType;
return accumulate(begin, end, ValueType());
}

}

配上点测试代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <vector>
#include <utility>
#include <iostream>
#include <iterator>
#include <initializer_list>

template <typename ValueType>
void test(std::vector<ValueType> const& v) {
std::cout << "=========================n";
std::copy(std::begin(v), std::end(v), std::ostream_iterator<ValueType>(std::cout, ", "));
std::cout << "n";

auto sum = std::accumulate(std::begin(v), std::end(v));
std::cout << "sum = " << sum << "n";
std::cout << "=========================nn";
}

template <typename ValueType>
void test(std::initializer_list<ValueType>&& values) {
std::vector<ValueType> v(std::forward<std::initializer_list<ValueType>>(values));
test(v);
}

int
main() {
test<int>(std::vector<int>{});
test<int>({1, 2, 3, 4});
test<int>(std::vector<int>{2,2,3});
test<double>({1.1, 1.2, 1.3});
test<std::string>({"aa", "bb", "cc", "dd"});
std::cout << "cpp : " << __cplusplus << std::endl;
}

EOF

Read more »

游戏中各种怪物都会有自己的掉落物品列表。

最原始的掉落列表设计可能就是,掉落金钱,掉落经验,掉落物品1(id + 数量),掉落物品2 …… 。我觉得这样的掉落表设计不够灵活,不能很好的对不同种类(分类?)的物品做不同的处理。

一种更好的掉落表可能是这样的:掉落分类1,掉落 id 1,掉落数量1,掉落分类2,掉落 id 2,掉落数量2,……

这样程序可以对不同的掉落分类进行不同的处理,并且 id 的含义可以局限于分类之下,例如:可以将“分类1”视为虚拟物品,“分类2”视为普通物品,“分类3”视为某种特殊物品,等等。

Read more »

C 语言中具有静态生存周期的变量的初始化器必须是常量表达式。当初始化器不是常量表达式时,gcc 和 clang 的报错信息不尽相同。

gcc 的报错信息是 error: initializer element is not constant clang 的报错信息是 error: initializer element is not a compile-time constant

clang 用的是“编译时常量”(compile-time constant),实际上 C99 标准里没有“编译时常量”这个概念,只有“常量表达式”。

A constant expression can be evaluated during translation rather than runtime, and accordingly may be used in any place that a constant may be.

C99 规定了初始化器中可用的常量表达式:

More latitude is permitted for constant expressions in initializers. Such a constant expression shall be, or evaluate to, one of the following:

  • — an arithmetic constant expression,
  • — a null pointer constant,
  • — an address constant, or
  • — an address constant for an object type plus or minus an integer constant expression.

An address constant is a null pointer, a pointer to an lvalue designating an object of static storage duration, or a pointer to a function designator; it shall be created explicitly using the unary & operator or an integer constant cast to pointer type, or implicitly by the use of an expression of array or function type. The array-subscript [] and member-access . and -> operators, the address & and indirection * unary operators, and pointer casts may be used in the creation of an address constant, but the value of an object shall not be accessed by use of these operators.

也就是说,你可以在初始化器中使用变量、变量的成员和函数的地址,也可以在地址的基础上加上个偏移量。

“address constant” 不能算作 “compile-time constant”。 尽管编译器可能在生成 ELF 文件之时,就决定了变量的虚拟地址,但在作为共享库 (shared library) 被加载时或其他情况下,这些变量的虚拟地址最终还需要加载器 (loader) 的调整。对于这些变量地址的引用,需要加载器的重定位功能。

c99 标准对于 “address constant” 的运算只规定了可以加减整数常量表达式。

1
2
3
#include <stdint.h>
#include <stdlib.h>
uintptr_t addr = (uintptr_t)malloc + (uintptr_t)free + 0x2345;
Read more »