Recording

Commit volatile memory to persistent append-only log

0%

FreeBSD 下的 kqueue 监听的单位是 (ident, filter) , Linux 下的 epoll 监听的单位是单个 fd 。在 Linux 下,通常你需要对 epoll 监听的 fd 做一些额外的记录工作,以便下次更改时查询。这里直接用 Redis 的代码做个示例。

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
// FreeBSD kqueue
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata;
struct kevent ke;

if (mask & AE_READABLE) {
EV_SET(&ke, fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
if (kevent(state->kqfd, &ke, 1, NULL, 0, NULL) == -1) return -1;
}
if (mask & AE_WRITABLE) {
EV_SET(&ke, fd, EVFILT_WRITE, EV_ADD, 0, 0, NULL);
if (kevent(state->kqfd, &ke, 1, NULL, 0, NULL) == -1) return -1;
}
return 0;
}

static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata;
struct kevent ke;

if (mask & AE_READABLE) {
EV_SET(&ke, fd, EVFILT_READ, EV_DELETE, 0, 0, NULL);
kevent(state->kqfd, &ke, 1, NULL, 0, NULL);
}
if (mask & AE_WRITABLE) {
EV_SET(&ke, fd, EVFILT_WRITE, EV_DELETE, 0, 0, NULL);
kevent(state->kqfd, &ke, 1, NULL, 0, NULL);
}
}

从上面的代码可以看到,FreeBSD kqueue 在改变 fd 的监听事件时不需要做额外的记录工作。

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
33
34
35
// Linux epoll
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata;
struct epoll_event ee = {0}; /* avoid valgrind warning */
/* If the fd was already monitored for some event, we need a MOD
* operation. Otherwise we need an ADD operation. */
int op = eventLoop->events[fd].mask == AE_NONE ?
EPOLL_CTL_ADD : EPOLL_CTL_MOD;

ee.events = 0;
mask |= eventLoop->events[fd].mask; /* Merge old events */
if (mask & AE_READABLE) ee.events |= EPOLLIN;
if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
ee.data.fd = fd;
if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;
return 0;
}

static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask) {
aeApiState *state = eventLoop->apidata;
struct epoll_event ee = {0}; /* avoid valgrind warning */
int mask = eventLoop->events[fd].mask & (~delmask);

ee.events = 0;
if (mask & AE_READABLE) ee.events |= EPOLLIN;
if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
ee.data.fd = fd;
if (mask != AE_NONE) {
epoll_ctl(state->epfd,EPOLL_CTL_MOD,fd,&ee);
} else {
/* Note, Kernel < 2.6.9 requires a non null event pointer even for
* EPOLL_CTL_DEL. */
epoll_ctl(state->epfd,EPOLL_CTL_DEL,fd,&ee);
}
}

从上面的代码可以看到,Linux epoll 在改变 fd 的监听事件时需要查询之前监听的事件,记录当前监听的事件。
这样 API 的调用者就无需做额外的记录工作,但读写事件需要额外的同步机制去保证线程安全。

Q3 Is the epoll file descriptor itself poll/epoll/selectable?

A3 Yes. If an epoll file descriptor has events waiting, then it will indicate as being readable.

---- epoll(7)

下面的代码利用了 epoll fd 本身在有等待事件时是可读的特性,展示了一种新的在 Linux epoll 下对 fd 读写
事件进行监听的方法。该方法分离了 fd 的读和写,这样我们就可以把 fd 的读和写交给不同的线程去处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
// initialization
int epfd = epoll_create(1);
int readfd = epoll_create(1);
int writefd = epoll_create(1);

struct epoll_event event;
event.events = EPOLLIN;

event.data.fd = readfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, readfd, &event);

event.data.fd = writefd;
epoll_ctl(epfd, EPOLL_CTL_ADD, writefd, &event);
1
2
3
4
5
// wait read event
struct epoll_event ev;
ev.data.fd = fd;
ev.events = EPOLLIN; // with possible other flags.
epoll_ctl(readfd, EPOLL_CTL_ADD, fd, &ev);
1
2
3
4
5
6
// wait write event
struct epoll_event ev;
ev.data.fd = fd;
ev.events = EPOLLOUT; // with possible other flags.
// No bookkeeping needed for read flags.
epoll_ctl(writefd, EPOLL_CTL_ADD, fd, &ev);
Read more »

游戏开发、运营过程中通常会有 “给全服所有玩家发送邮件” 的需求。

我个人的想法是,给每封全服邮件一个版本号,玩家上线时或定期向 “全服邮件中心” 查询更新的全服邮件。下面是 C++ 版本的示例代码:

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
// Global Mail Centra
using version_t = uint64;
using mail_id_t = uint64;

std::map<version_t, mail_id_t> _mails;

std::tuple<version_t, std::vector<mail_id_t>> fetchNewerMail(version_t currentVersion) {
auto newerVersion = currentVersion;
std::vector<mail_id_t> newerMails;
for (auto it = _mails.upper_bound(currentVersion); it != _mails.end(); ++it) {
newerVersion = it->first;
newerMails.push_back(it->second);
}
return std::make_tuple(newerVersion, newerMails);
}


// Global Mail Proxy in Player Module
version_t _currentVersion;

std::vector<mail_id_t> checkGlobalMails() {
version_t newerVersion;
std::vector<mail_id_t> newerMails;
std::tie(newerVersion, newerMails) = fetchNewerMails(_currentVersion);
if (newerVersion != _currentVersion) {
_currentVersion = newerVersion;
// update _currentVersion to backend data storage
}
return newerMails;
}

如果服务器框架有很好的 coroutine 支持,玩家模块可以用一个独立的 coroutine 不停的向 “全服邮件中心” 获取邮件更新;“全服邮件中心” 根据自身状态决定立即或延迟回复以恢复对方的执行流。例如,在 skynet 中,可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
function update_global_mails()
local currentVersion = get_current_version()
while true do
newerVersion, newerMails = skynet.call(".global_mail_centra", "lua", "fetchNewerMails", currentVersion)
if newerVersion != currentVersion then
// synchronized to player's mailbox (other module in same process), blocking call
currentVersion = newerVersion
// update currentVersion to backend data storage
end
end
end

skynet.fork(update_global_mails)

推荐阅读:skynet 中如何实现邮件达到通知服务

Read more »

参与并理解策划需求

能把事情做好的人永远是少数。程序如此,策划也如此。遇到不靠谱的策划,一定好主动沟通,弄清楚它们都在想些什么?!

隔离是必须的

游戏逻辑相关开发要占用游戏服务器开发的绝大部分时间,也是出现 bug 最多的部分。游戏逻辑的各个子系统之间直接的源码级 API 交互,会随着团队成员和子系统的增加,呈现出急剧的复杂度。更好的方案是,子系统通过唯一的中间层 API 以约定的协议与其他子系统进行交互,子系统之间实现源码级的隔离。

这种分案有很多优点:

  • 便于模块分配,降低了源码冲突的可能性,限制了单个子系统 bug 的波及范围。
  • 降低了系统的整体复杂度,便于人类的大脑理解。
  • 子系统的实现变更(重构、重写)的代价(负担)会很小,代码质量会越来越好。
  • 容易定位造成 bug 的子系统。
  • 心情舒坦,利于长寿

代码质量

代码质量是一件严肃的事情。所有改 (zao) 善 (ta) 代码质量的行为,都会获得回 (bao) 报 (ying) 的。

潜在即是必然

一个发生概率为百万分之一的 bug ,在线上运行时是必然会发生的

Read more »

在很早的时候,我就接触到 Algorithms + Data Structures = Programs 这样的概念。

在几年的工作之后的现今,我不认为这个等式可以表达所有的程序,也许只是我所工作的领域无法让我体会到这个等式的价值。

下面是我尝试去总结自己的经历,得到的另一个等式。

模块 + 模块间的通信 = 程序
modules + communications between modules = program

这里的模块不应该狭义的理解为 C/C++ 语言当中的模块化概念,我把 C/C++ 语言当中的模块称之为“源码级模块”。“源码级模块”通过函数调用的方式进行通信,彼此之间是紧耦合的,在多人协作的软件开发活动中会加剧开发人员之间的沟通成本,也没法很好的适应需求变更和扩展。

与“源码级模块”对应的是“运行时模块”,其模块本身有着对应的运行时的实体表示,对外提供服务。“运行时模块”可以表现为服务器内部对其他模块提供服务的运行时实体,也可以表现为提供运行时服务的服务器。很少有语言层面的“运行时模块”的直接支持, Erlang 是其中之一。 Erlang 中的 process 就是一个运行时的实体,我将之视为“运行时模块”。在没有提供“运行时模块”支持的语言中,可以通过将整个程序分层,由底层提供“运行时模块”的抽象。相对于“源码级模块”,“运行时模块”具有天然的强隔离性和可扩展性。这两点可以很好的应对软件开发活动中的多人协作和需求变更。

“运行时模块”通过消息传递的方式进行通信,跨节点的支持需要对消息体进行序列化和反序列化。 Erlang 中的消息可以跨节点进行传输,

对在 C/C++ 中引入“运行时模块”的一个潜在担忧是可能会导致程序性能的下降,我的想法是承认性能的损失,为此换来的是编程模型的提升。

在软件开发中引入的大多数“抽象”和“分层”(如果不是全部)都会引起性能的损失,但是今天的大部分程序仍然是运行在“进程”和“虚拟地址空间”的抽象之上。

Read more »

在 Stack Overflow 上回答了一个问题: How can I use an enum class in a boolean context?,记录下。

C++ 11 的 explicit operator bool() 可以让类类型的值使用在条件语句中,但无法对 enum 类型提供支持。

下面是我使用的一种方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct Error {
enum {
None = 0,
Error1 = 1,
Error2 = 2,
} Value;

/* implicit */ Error(decltype(Value) value = None) : Value(value) {}

explicit operator bool() {
return Value != None;
}
};

inline bool operator==(Error a, Error b) {
return a.Value == b.Value;
}

inline bool operator!=(Error a, Error b) {
return !(a==b);
}

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Error lastError();

if (auto err = lastError()) {
}

switch (lastError().Value) {
case Error::None:
break;
case Error::Error1:
break;
default:
break;
}

if (lastError() == Error::Error1) {
}
Read more »