« October 2010 | Main | December 2010 »

November 18, 2010

Go 语言初步

这几天认真玩起了 Go。所谓认真玩,就是拿 Go 写点程序,前后大约两千行吧。

据说 Go 的最佳开发平台是 Mac OS ,我没有。其次应该是 Linux 。Windows 版还没全部搞定,但是也可以用了。如果你用 google 搜索,很容易去到一个叫 go-windows 的开源项目上。千万别上当,这是个废弃的项目。如果你用这个,很多库都没有,而且语法也是老的。我在 Windows 下甚至不能正确链接自己写的多个 package 。活跃的 Windows 版是 gomingw ,对于 Windows 用户,装一个 mingw32 以后就可以开始玩了。

就三天来实战经历,我喜欢上这门新语言有如下原因:

mix-in 的接口风格。非常接近于我在用 C 时惯用的面向对象风格。有语法上的支持要舒服多了。以平坦的方式编写函数,没有层次。而后用 interface 把需要的功能聚合在一起。没有继承层次,只有组合功能。

强类型系统。使得犯错误的机会大大降低。正确通过编译,几乎就没有什么 bug 了。而编写程序又有点使用 lua 这种动态语言的感觉,总之,写起来很舒服。

内置的 string / slice 类型,以及 gc 。这是我觉得现代编程必须的东西。手工管理未必有更高的效率,但一定有更多的出错机会。至少,我一直主张有一个方便的 string 不变量的基本类型的(参见这一篇)。

defer 是个有趣使用的东西,用它来实现 RAII 比 C++ 利用栈上对象的析构函数的 trick 方案让人塌实多了。go 在语言设计上是很吝啬新的关键字的。但多出一个关键字 defer ,并用内建函数 panic / recover 来解决许多看似应该用 exception 解决的问题要漂亮的多。

zero 初始化。我一直觉得 C++ 的构造函数特别多余。按我用 C 的惯例,一切数据结构都应该用 0 初始化。所以 C 里有 calloc 这个函数。go 把这点贯彻了。不会再有未定义的数据。

包系统特别的好。而且严格定义了包的初始化过程,即 init 函数。在我自己的 C 语言构建的项目中,实现了几乎一样的机制,甚至也叫 init 。但是有语言层面的支持就是好。对,只有 init 没有 exit 。正合我意。

goroutine 是个相当有用的设计。8 年前,我给 C 实现了 coroutine 库,并用在项目里,并坚信,程序就应该这么写。但是没有语言级的支持,用起来还是很麻烦。goroutine 不仅简化了许多业务逻辑的编写,而且天生就是为并发编程而生的。select/chan 可能是唯一正确的并发编程的模型。Erlang 还是太小众了,而 Go 可以延用 Erlang 的模型,却有着纯正的 C 语言血统,我想会被更多人接受的。虽然 Go 依然可以用共享状态加锁的方案,但不推荐使用。chan 用习惯了,还是相当方便的。

{ 要不要独立占一行的信仰之争终于结束了。还记得前段时间有位同学来 email 指责我开源的代码没有章法。程序写的太乱。他的理由就是,我的 { 都没有独占一行。好了,争论可以结束了。在 Go 里,如果你把 { 从 if/for 语言的行末去掉,放在下一行。编译器是不会让你通过的。(除非你再加一个 ; )我很欣慰 ;)

我发现我花了四年时间锤炼自己用 C 语言构建系统的能力,试图找到一个规范,可以更好的编写软件。结果发现只是对 Go 的模仿。缺乏语言层面的支持,只能是一个拙劣的模仿。


对于有 C 基础的同学,比如我,学习 Go 毫不费力。按这篇文章的指引即可。Rob Pike 的三日教程 PPT ,我心急,用了一个下午就看完了,并且做完了练习。

不过实战编写程序还是需要反复查阅文档的。学习一门新语言,就是在学习它的各种惯用法和库。而不是去模拟熟悉的语言。我在编写代码的时候,时刻问自己,在 Go 里,通常用什么手法来处理这个问题。接下来就是不断的查询文档了。从这个意义上讲,学习新东西还是很累的。好在 Go 的各种设计都非常切合我的本意,所以自然是越写越舒服了。

至于把变量类型申明都放在后面,按 Sean 同学的话说,有种真气逆行的感觉。对我来说倒是小问题,几个小时就习惯了。反而 C 语言那种亦前亦后的方式才是奇怪呢。


说一下我的练手项目。我用 Go 重新实现了处理多连接的服务器。当然,现在的设计方案和几年前写 blog 时的方案有略微的不同。

需求是这样的:

这个服务会监停一个端口,允许外部多个连接的接入,并可以把这个连接上的数据包汇总发到后端的一个连接上。简单的说,就是一个 N 对 1 的数据处理器。把 N 个 TCP 数据流合成一个数据流。

一个服务的处理上限是 64K 的连接,使用 2 字节的 id 号区分不同的外部连接。我定义了简单的协议,每个数据片段有 3 字节的数据头。分别是数据长度一字节和 2 字节的连接 id 号。

这个服务仅仅做数据流的合并,而不规定数据逻辑上的分包。对内的数据管道上看起来的数据流就是这样的:

len id_lo id_hi content ... len id_lo id_hi content ...  len id_lo id_hi content ... 

处理合并起来的数据流非常简单,只需要通过一个 IO 管道 (可以是 socket 也可以的 stdio/stdout ,对于 Go 来说,甚至可以是一个 in-memory Pipe )这方便后端的程序不再考虑多连接的问题。

后端服务需要可以控制连接服务器。最基本的功能就是可以强制断开某个外部连接。并且可以获得新的外部连接接入或离开的信号。

更进一步,应该由后端服务器来控制连接服务器对外监听端口的开启与关闭,以及外部连接的上限等。

为了简化设计,我选择在一个特殊的内部连接(0号连接)上收发内部的控制指令。并且使用 \r\n 分割的文本协议。


用 Go 来实现这个服务非常简洁。全部我只使用了 240 行左右的 Go 代码。所有的网络连接都使用独立的 goroutine 来控制。每个都以阻塞方式处理 socket 。主循环仅仅使用一个 select ,这类似 Erlang 的事件驱动模型。

对于控制指令流,创建一个 in-memory Pipe 即可。在对内的数据流上,过滤到 id 为 0 的数据包,转发到这个 Pipe 上。使用 bufio 把 Pipe 转换成一个 bufio.Reader 接口,就可以方便的使用 ReadString 方法去读以回车分割的文本行,进而排发到解析指令的 goroutine 中,把结构化指令利用 chan 发进消息 select 循环。整个只需要不到 10 行代码。

大多数 goroutine 内部都是一个 for 循环,结束条件是和它通讯的 chan 被外部关闭。

需要稍微考虑性能的地方是给外部连接的数据包加上三字节的头转发到内部通道上。这里如果每个包都用 make 创建一个新的 array slice 会有一些内部管理上的开销。我的做法是每次申请 16k 的 array ,再创建一个 slice 去组包,如果这个 16k 的 array 没用完,就会顺着用下去。如 Go 的教程中所言,slice 的创建是很廉价的,想来也是如此。它只是对 array 的部分引用。

Go 的引用和值分得很清楚,这使它更像 C 而不是 Java ,却又提供了 C++ 提供不了的安全性。

用 Go 写网络程序,真是非常舒心。socket 和 file 在 interface 上的统一,暗合 Unix 之道。程序嘛,就是处理输入,产生输出。Reader 和 Writer 接口让人愉快。

November 05, 2010

多进程资源共享及多样化加载

梦幻西游在去年出了个新版本,在这个版本中,采用了 3d 技术渲染人物。我参加过部分的技术讨论。总的来说,对于公司的关键产品,是以稳定性为第一。所以不希望对程序做大改动。最好以独立扩充的模块为主。所以最终采用的技术是用 3d 技术渲染成图片,再依靠旧的程序框架跑起来。

采用 3d 渲染,主要是为了解决人物换装时的图片资源组合爆炸问题。当然还有更绚的特效等。

最近,梦幻西游的项目经理提出,新的版本性能依旧有些问题。当老的版本可以同时打开 5 个客户端时,新的版本只能开两个。允许用户同时开多个客户端,对梦幻西游这款产品非常重要。我最近几天就开始着力解决这个优化问题。


目前的方案是用一个独立进程去渲染图片,然后通过共享内存的方式,把渲染结果提供给游戏客户端。这较好的解决了多个客户端的性能下降问题。 我认为,这也是继续延用 2d engine 做核心的优势。(当然更重要原因是,这个游戏的客户端程序不适合重新制作,否则重制版一旦出现严重问题,对公司影响太大)

我研究了一下现有方案。考虑了几天,把一些细节重新设计了一下。感觉可以压榨出一些性能改善的空间。

3d 渲染本身并不是大的瓶颈,不过渲染结果从显存拷贝出来会是个问题。我个人倾向于使用软件渲染引擎。Pixomatic 是个不错的选择。不过这是个独立模块,倒是无所谓什么时候做。目前已经在用的这一块工作良好,暂时是不用动的。

另一块是从 32bit 平坦位图数据(渲染结果)压缩为 8bit 的 RLE 格式数据,和以前的 2d engine 中的数据格式兼容。这部分目前的代码运行时间大约相当于渲染环节的 1/4 。虽然还有一定的优化空间,不过暂时也可以不动。注:这里的优化,主要是集中在调色盘的计算上,目前用的算法是可以改进的。

在旧的 Engine 中,RLE 压缩后的图片是以行为单位储存。而每行数据,是记录的指针。这点可以修改一下,改记为相对数据块首的偏移量。这样,压缩图片数据就是地址无关的。更适合做进程间共享。这一点,大约花几个小时就能修改过来,而且不需要更改压缩图片数据在磁盘中的格式。


重点可以优化的地方在:我们可以把所有图片加载的环节全部移到单一的进程中。而不仅仅只是用一个独立进程负责实时渲染。这可以降低在多个游戏进程同时工作时的 IO 压力。

其实,现代操作系统,已经会把所有闲置内存用于磁盘 cache 。所以,重复加载文件,只是一些内存拷贝工作。不过这个工作也正是消耗 CPU 时间的工作。减少重复加载的数据量,也可以减轻 CPU 负担。

让多个游戏进程共享资源,也可以减少总的内存开销。这就能极大的改善总体性能。


最终的方案,是用一个独立 128bit ID 来定义每个资源的请求。这个 128bit ID 是由整个请求的参数计算出来的。

游戏进程计算出 ID 后,每次需要资源前,都向资源提供进程提交 ID , 但并不等待回应。而资源进程,按优先次序,依次获取图片资源,并逐个广播地址给每个游戏进程。为了加快响应,可以先返回缺省替代图的地址,然后等资源获取完整后,再刷新一次完整的地址。

游戏进程则维护一张缓存表,每当资源进程通知它新的图片地址后,就更新表中的对应关系。

资源共享使用分页共享内存的方式。按经验数值,一页 8M 。每个游戏进程映射的共享区虚拟地址不一。但由于图片数据是地址无关的,所以不太所谓。整个共享空间可以达到 512M 足够游戏使用。

其实,这个资源管理程序,不限于从磁盘加载数据,或是利用 GPU 渲染图片。更可以从网络下载。只需要把共享推送机制最好就行了。需要解决的只是响应速度问题。

November 04, 2010

关于群服务的实现

既然好多朋友感兴趣,我就继续写写我对 IM 服务的看法。

许多人都认为 QQ 比较粘人的设计是群。我个人是反感“群"这个设定的,以前写 blog 批判过。不过我不否定用户的需求。

我认为,任何支持 XMPP 协议的 IM ,都可以利用 XMPP 的 s2s 服务做出一个独立于所有 IM 提供商的独立的群服务出来。

我想这个形式应该是这样的:

用户只需要加一个叫 群号@groupchat.foobar 的好友。由 groupchat.foobar 服务器提供 XMPP s2s 的协议。服务器指派一个机器人管理这个指定群。然后由这个聊天机器人来负责转发信息就好了。

这有点像 wow 做团队 DKP 工具一样。

可以设计一个人阅读方便的文本协议,不通过特定的 client 来解析,就能模拟出很好的用户体验的群聊服务。如果配上特定的 client ,用户体验会更好。

关键在于,这个服务可以做的很开放,不限制是由什么 IM 来使用。只要你用的 IM 服务支持 XMPP 即可。把 qq 群转移到这个上面也不会是太难的事情。而且这个群聊服务(groupchat.foobar)谁都可以做,并可以共存。技术牛的可以不限制群的人数,承载能力差的可以限制用户。有能力的可以提供 web 界面的聊天记录备查。

IM 协议只有到了互联互通那天,才能更好的激发程序员们的创造力,给用户更好的 IM 体验。

QQ 用户关系的迁移

最近网上最热的话题算是 QQ 和 360 的火并了。我不是 360 的用户,也没有装 QQ 的 client 。偶尔有朋友非要用 QQ 联系我时,我会用 web qq 登陆上去,收个消息,回复一下,然后就下来。我有个 6 位 qq 号码(曾经还有个 5 位的),04 年开始就不怎么用了。

在腾讯,我有几个私交很不错的朋友,对这家公司没什么恶感。唯一的一次是觉得他们不尊重 GPL 协议。所以这次这件事情也没啥心情观看。直到,QQ 居然封掉了 web qq ,用这种自残的方式参战,完全不顾用户的感觉。

我一直认为,做一家大的互联网企业是应该有更高的追求的。所以我喜爱 google ,他们以让信息被更高效的获取为己任。而作为中国 IM 最大提供商的 qq 呢?我想,更方便的让中国互联网用户相互联络应该是他们最高的追求吧。但是,qq 没有这个意识,这是让我最为失望的地方。

腾讯绝对有技术实力,按 XMPP 协议实现个互联互通的 s2s 服务。这能极大的促进互联网用户的在线交流。但是他们没有。这是我不喜欢这家公司的主要地方。而不是因为 qq 是我所在网易公司所开发的不争气的 popo 的竞争对手的缘故。

我一直不懈余力的相我的朋友推广 google 的 IM 软件(以及 google 其它服务):googletalk 。不仅因为它的简洁,更在于它的开放。完整的支持了 XMPP 协议。我写过 blog 谈过为什么我喜欢 google talk 。我觉得这次也算一个契机,可以帮助更多的朋友从 qq 的封闭世界里解脱出来。

大多数人都觉得,摆脱不了 qq 的主要原因是 QQ 上的社交圈。看似无解,但其实也可以部分的做到 QQ 用户关系的迁移。

我们需要做两件事情:

第一,导出 QQ 的好友名单。

完成这一步的方法有三:

  1. 兼容 qq 协议,从服务器获取好友信息。
  2. 使用 web qq 或 qzone 的信息,这相对简单。
  3. 扫描用户本地聊天记录,提取好友信息。

关于其一,qq 是私有协议,逆向工程不符合 QQ 的 EULA 最终用户许可协议。不过世界上已经有许多开源的实现。我不认为 clone QQ 协议是非法的。用户有权用自己的方式合法的使用腾讯授予的服务。

关于其二,web qq 被腾讯自己砍掉了。qzone 倒是个不错的途径。

关于其三,扫描本地文件,比较流氓,不过得到用户授权,比如弹出对话框让用户选择目录应该没问题。只是兼容各种不同的 qq 版本比较繁琐。

如果用兼容 qq 协议的方式来做,出于对用户的方便,可以做成网络服务,让用户提交 qq 用户名密码。但这显得不太安全。没有什么可靠的手段让用户放心。所以,退而求其次,可以开发一个开源的 qq 登陆器。因为开源,可以受到监督,并没有盗取任何好友名单之外的信息。

并且在提交好友信息时,应该以明文方式呈现提交内容,让用户确定提交。

我认为这种形式是可以得到用户的信任的。

第二步要做的是提供一个服务收集用户之间的拓扑关系。用户提交自己另外在使用的 IM 软件的名字和帐号,以及自己的 QQ 好友列表。如果有足够多的用户(只需要是一个群体的)提交,这个服务就可以以各种方式提示你,你的 qq 好友某某正在使用 google talk / msn 等等。你可以方便的加他了。

以 google talk 为例,我们可以制作一个机器人。(因为 gtalk 兼容 XMPP 协议,机器人更方便制作,以跨服务器的方式来通讯,而不需要以 gtalk client 的形式)

在用户提交自己的 qq 好友关系时,我们的机器人就会自动加他为 gtalk 好友。一旦侦测到他有 qq 好友迁移到 gtalk,机器人就可以自动发通知了。


总的看来,这套东西不算难做。这几天比较闲,打算实现出来看看。

btw, 如果有 360 的同学看到这里。如果 360 有推出自己的 IM 软件的打算。我强烈建议使用标准的 XMPP 协议,架设 jabber 服务器就行了。别再搞私有协议了。