« June 2010 | Main | August 2010 »

July 28, 2010

mingw 下的 stack backtrace

我们的项目的 Windows 版本是用 MinGW 开发的。当程序在 Windows 下挂掉后,固然可以用 gdb 调试,看到调用栈。但有些时候还是不够方便。

比如说今天,我们写的模型编辑器发到广州美术同事使用时,就出了问题。3d 程序在不同显卡环境下的确容易出故障,异地调试程序非常困难。这个时候,多么想看看调用栈啊。

The GNU C Library 是提供了 Backtraces 的,可惜 MinGW 不支持 :( 。最后打算自己写一个。

获取 stack backtrace 调试信息的基本原理就是利用 windows 的 imagehlp.dll 取得正在运行的 exe 或 dll 被系统加载到内存后的镜像信息(可能被重定位了)。然后利用 libbfd 按偏移量取得源码的相关信息。

在崩溃时获取调用栈信息使用的是 SetUnhandledExceptionFilter ,可以注册一个函数拿到 context 继续处理。

搜索一番后,发现其实有个哥们实现了个 C++ 版本 。不过我还是用我自己写的 C 版本吧。

使用非常简单,只需要把事先编译好的 backtrace.dll 在需要追踪的程序前面加载一下即可,然后在 crash 时就会向 strerr 输出详细的堆栈信息。

有兴趣的同学可以看这里:我把它在 code.google.com 上开源了

July 27, 2010

换了个新手机

我的那个 Palm Treo 650 终于寿终正寝了。去年曾经买了个 HTC Hero 送老爸,玩过几天 Andriod ,还是有点好感,这次打算选一款 Andriod 系统的。

曾经也考虑过 iPhone 或是 Palm Pre ,请教了我公司的手机大神,大神同学说,他买过两款 iPhone ,用一段时间总想换,不推荐长期使用。至于 Palm Pre ,大神同学也买过一个,评价是极其垃圾,完全比不上 Treo 系列。他最近半年买过 Milestone ,Desire ,Nexus One ,X10i ,i9000 等等几款,每个都有不同的缺憾,最终比较下来,给我推荐了三星 i9000 。

我比较信任亲身使用者的感受,尤其是对手机如此挑剔,以至于每一两个月就情不自禁的买新手机的同学的现身说法。没太多犹豫就去 taobao 下了单。

结果…… 等了 8 天还是没寄来,卖家一直说缺货。昨天忍不住退了款,还是很干脆的。再一看,原来是涨价了,从 3450 涨到了 3600 。换了一家重新下单,今天一早就寄到了。


最为头痛的问题是导联系人。我一下找不到 palm 的联机线,只好去找了个 palm 版的 syncML 程序。把联系人同步到 google 。结果悲剧了,那个软件是个老外写的,完全没考虑编码问题。而 palm treo 一直用的国人开发的外挂的中文系统,内码是 GBK 而不是 Unicode 。然后一片乱码。

从 gmail 下载了我的联系人名单,打开仔细分析了一下。google 用的 Unicode 做内码,下载下来为 csv 文件。由于微软的 Office 标准是用 UTF-16 来编码 Unicode 文件,所以这份文件是 UTF-16 格式的。Palm 上的软件在同步的时候,完全没做转换,直接把中文按 GBK 上传。google 的 syncML 服务器在接收的时候,直接把每个汉字拆分为两个独立的字节,转换成 UTF-16 编码后,插入了字节 0 每个汉字变成了 4 字节。

然后,我的联系人名单就变成了两种编码的混合文件。

想了一下,没想到什么工具可以辅助我解决这个问题。只好打开编辑器,花了半个小时写了一个 C 程序来做转换。好在我对问题了解的比较清楚,一次性就把乱码问题搞定了。

接下来的悲剧是 Andriod 2.1 的系统的联系人名单貌似不能按中文拼音分类排序。导致我所有的中文联系人都被排到了 Z 类之后。喜剧的是,原来 Palm 系统也有类似问题,所以我曾经写过一个程序,自动给所有联系人名字前面加上拼音首字母。

两个系统的区别在于,Palm Treo 默认的姓在前,名在后。到了 Andriod 中,默认变成了名在前。我原来加的英文被跟在汉字后面,没能起到作用。修改了设置后,次序就正常了。

btw, 最近碰到过好几起汉字编码问题了。其中还有一次就出在我们内部用的 ftp 系统上。在 Linux 和 Windows 系统间通过 ftp 交换文件时,汉字文件名就出过许多问题。Samba 在这个问题上表现就好的多 :D


第二件大事是 vpn 。我换手机的一大动力就是老的机器不支持 vpn 。结果手机读 google reader 老是被郁闷。换了新手机赶紧的试一下 vpn 。我自己架了个 pptpd 服务在 linode 处买的主机上。 以前用的挺好的。今天死活连不上。跑去看了下 log ,感觉跟防火墙有点关系,不支持 GRE 。

还好有别的选择。三下两下架了个 xl2tpd 服务。试了下 vpn 连接,感觉很好。

btw, 其实,卖 vpn 也能致富吧?嗯,一条只能做不能说的生财之道。

July 19, 2010

游戏多服务器架构的一点想法

把网络游戏服务器分拆成多个进程,分开部署。这种设计的好处是模块自然分离,可以单独设计。分担负荷,可以提高整个系统的承载能力。

缺点在于,网络环境并不那么可靠。跨进程通讯有一定的不可预知性。服务器间通讯往往难以架设调试环境,并很容易把事情搅成一团糨糊。而且正确高效的管理多连接,对程序员来说也是一项挑战。

前些年,我也曾写过好几篇与之相关的设计。这几天在思考一个问题:如果我们要做一个底层通用模块,让后续开发更为方便。到底要解决怎样的需求。这个需求应该是单一且基础的,每个应用都需要的。

正如 TCP 协议解决了互联网上稳定可靠的点对点数据流通讯一样。游戏世界实际需要的是一个稳定可靠的在游戏系统内的点对点通讯需要。

我们可以在一条 TCP 连接之上做到这一点。一旦实现,可以给游戏服务的开发带来极大的方便。

可以把游戏系统内的各项服务,包括并不限于登陆,拍卖,战斗场景,数据服务,等等独立服务看成网络上的若干终端。每个玩家也可以是一个独立终端。它们一起构成一个网络。在这个网络之上,终端之间可以进行可靠的连接和通讯。

实现可以是这样的:每个虚拟终端都在游戏虚拟网络(Game Network)上有一个唯一地址 (Game Network Address , GNA) 。这个地址可以预先设定,也可以动态分配。每个终端都可以通过游戏网络的若干接入点 ( GNAP ) 通过唯一一条 TCP 连接接入网络。接入过程需要通过鉴权。

鉴权过程依赖内部的安全机制,可以包括密码证书,或是特别的接入点区分。(例如,玩家接入网络就需要特定的接入点,这个接入点接入的终端都一定是玩家)

鉴权通过后,网络为终端分配一个固定的游戏域名。例如,玩家进入会分配到 player.12345 这样的域名,数据库接入可能分配到 database 。

游戏网络默认提供一个域名查询服务(这个服务可以通过鉴权的过程注册到网络中),让每个终端都能通过域名查询到对应的地址。

然后,游戏网络里所有合法接入的终端都可以通过其地址相互发起连接并通讯了。整个协议建立在 TCP 协议之上,工作于唯一的这个 TCP 连接上。和直接使用 TCP 连接不同。游戏网络中每个终端之间相互发起连接都是可靠的。不仅玩家可以向某个服务发起连接,反过来也是可以的。玩家之间的直接连接也是可行的(是否允许这样,取决于具体设计)。

由于每个虚拟连接都是建立在单一的 TCP 连接之上。所以减少了互连网上发起 TCP 连接的各种不可靠性。鉴权过程也是一次性唯一的。并且我们提供域名反查服务,我们的游戏服务可以清楚且安全的知道连接过来的是谁。

系统可以设计为,游戏网络上每个终端离网,域名服务将广播这条消息,通知所有人。这种广播服务在互联网上难以做到,但无论是广播还是组播,在这个虚拟游戏网络中都是可行的。

在这种设计上。在逻辑层面,我们可以让玩家直接把聊天信息从玩家客互端发送到聊天服务器,而不需要建立多余的 TCP 连接,也不需要对转发处理聊天消息做多余的处理。聊天服务器可以独立的存在于游戏网络。也可以让广播服务主动向玩家推送消息,由服务器向玩家发起连接,而不是所有连接请求都是由玩家客互端发起。

虚拟游戏网络的构成是一个独立的层次,完全可以撇开具体游戏逻辑来实现,并能够单独去按承载量考虑具体设计方案。非常利于剥离出具体游戏项目来开发并优化。

最终,我们或许需要的一套 C 库,用于游戏网络内的通讯。api 可以和 socket api 类似。额外多两条接入与离开游戏网络即可。

July 12, 2010

C 语言中统一的函数指针

有时候,我们需要把多个模块粘合在一起。而这些模块的接口参数上有少许的不同。在 C 语言中,参数(或是返回值)不同的函数指针属于不同的类型,如果混用,编译器会警告你类型错误。

在 C 语言中,函数定义是可以不写参数的。比如:

void foo();

这个函数定义表示了一个返回 void 的函数,参数未定。也就是说,它是个弱类型,诸如:

void foo(int);

void foo(void *);

这些类型都可以无害的转换成它。正如在 C 语言中,具体的指针类型如 int * ,char * 都可以转换为 void * 一样。

注1:如果要严格定义一个无参数的函数,应该写成 void foo(void);

注2:如果有部分参数固定,而其后的参数可变,则定义看起来是这样: void foo(int , ...); 这表示第一个参数为 int ,从第 2 个参数开始可变。

不过,C 语言的这种语法,实际上不太使用。因为用 C 语言无法主动控制函数调用的参数压栈。我们很难根据程序的上下文来决定如何传入参数去调用某个函数。如果需要逐级传递多个函数的参数,用的更多的是 va_list

比如,你很难对 printf 做封装,通常为了方便做封装,还提供了形为 vprintf 的接口。

C++ 解决此类问题的方案是用类去模拟一个函数,通过重载 () 操作符的方法,让函数调用看起来和普通函数一致(并美其名曰 functor/仿函数)。当然,也有撕破语法糖的伪装,用更直白的类继承的方式来定义出接口。

这里想说的是,C 语言里也还有一种有趣的方案来在保证类型安全的基础上解决类似问题。

在 X-Window 的消息定义中就可以看到这样的手法。

在 Windows 的接口中,Windows 的消息携带的数据通常用两个参数来表示:WPARAM 和 LPARAM ,均为 32bit 整数。我们知道,消息本质上等同于对象的方法。在更早的面向对象语言如 smalltalk 中,调用对象的方法即被看成向对象发送一个消息。Windows 如此把所有消息处理相关函数的接口都以 WPARAM 与 LPARAM 的形式传递参数,正是为了方便统一其接口形式。各种五花八门的参数都蕴涵于这 64 bit 数据中。

Xlib 处理类似的问题,对 C 程序员的亲合力则大的多。至少更为类型安全。

Xlib 定义了一个叫做 XEvent 的结构体(实际是一个 union)。然后把各种可能的消息类型放在这个 union 中。例如,我想取键盘消息,则可以用 event.xkey.keycode 。

一般说来,我们可以把模块的对外接口看成是接收一组输入参数并加以处理。如果需要粘合多个不同的模块,他们需要处理不同的输入参数的话,可以借鉴 XLib 的这个方法。在粘合层定义一个 union ,把所有可能的参数组,每组定义成一个 struct 然后定义在同一个 union 中。这个粘合层的统一接口则为这个 union 指针。有必要的话,所有的参数组 struct 的头部都留下 type 字段。这样比较容易分发消息。

这样做的本质是:把函数调用时由编译生成的、将调用参数逐个压栈的代码,改由程序员主动填写(填写参数结构体)。利用结构的类型安全,保证了函数调用时的参数类型安全。再利用 union 的语法,把不同的参数组联合到一起变成同一类型。

给 api 传递一个 struct 或 union 指针而不是逐个参数传递,是 C 接口设计的一种常见手法。除了 XLib 的设计,还能找到很多耳熟能详的例子。例如,我们在 socket api 上也可以看到类似的东西。例如 connect 的参数中有一 sockaddr 结构,就适用于各种不同的网络底层协议。