go的go protobuf socket这个问题是不是一个bug

 Protobuf通信协议详解:代码演示、详细原理介绍等
我的图书馆
 Protobuf通信协议详解:代码演示、详细原理介绍等
前言http://www.52im.net/thread-323-1-1.html
在移动互联网时代,手机流量、电量是最为有限的资源,而移动端的即时通讯应用无疑必须得直面这两点。
解决流量过大的基本方法就是使用高度压缩的通信协议,而数据压缩后流量减小带来的自然结果也就是省电:因为大数据量的传输必然需要更久的网络操作、数据序列化及反序列化操作,这些都是电量消耗过快的根源。
当前即时通讯应用中最热门的通信协议无疑就是Google的Protobuf了,基于它的优秀表现,微信和手机QQ这样的主流IM应用也早已在使用它。本文将详细介绍Protobuf的使用、原理等。
(更多有关即时通讯应用的通信协议文章,请参见:)
强列建议将Protobuf作为你的即时通讯应用数据传输格式:
如何选择即时通讯应用的数据传输格式:
Protobuf官方主页:
移动端IM开发问题:
即时通讯综合性资料:
即时通讯安全性资料:
实时音视频开发资料:
即时通讯的架构设计:
更多资料精选请查看:
Protobuf简介
什么是 Google Protocol Buffer? 假如您在网上搜索,应该会得到类似这样的文字介绍:
Google Protocol Buffer( 简称 Protobuf) 是 Google 公司内部的混合语言数据标准,目前已经正在使用的有超过 48,162 种报文格式定义和超过 12,183 个 .proto 文件。他们用于 RPC 系统和持续数据存储系统。
Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。目前提供了 C++、Java、Python 三种语言的 API(即时通讯网注:Protobuf官方工程主页上显示的已支持的开发语言多达10种,分别有:C++、Java、Python、Objective-C、C#、JavaNano、JavaScript、Ruby、Go、PHP,基本上主流的语言都已支持,详见工程主页:)。
或许您和我一样,在第一次看完这些介绍后还是不明白 Protobuf 究竟是什么,那么我想一个简单的例子应该比较有助于理解它。
二、一个简单的例子
安装 Google Protocol Buffer
上可以下载 Protobuf 的源代码。然后解压编译安装便可以使用它了。
安装步骤如下所示:
123456tar -xzf protobuf-2.1.0.tar.gz cd protobuf-2.1.0 ./configure --prefix=$INSTALL_DIR makemake check make install
关于简单例子的描述
我打算使用 Protobuf 和 C++ 开发一个十分简单的例子程序。该程序由两部分组成。第一部分被称为 Writer,第二部分叫做 Reader。Writer 负责将一些结构化的数据写入一个磁盘文件,Reader 则负责从该磁盘文件中读取结构化数据并打印到屏幕上。
准备用于演示的结构化数据是 HelloWorld,它包含两个基本数据:
ID,为一个整数类型的数据Str,这是一个字符串
书写 .proto 文件
首先我们需要编写一个 proto 文件,定义我们程序中需要处理的结构化数据,在 protobuf 的术语中,结构化数据被称为 Message。proto 文件非常类似 java 或者 C 语言的数据定义。代码清单 1 显示了例子应用中的 proto 文件内容。
清单 1. proto 文件:
1234567 message helloworld { &&&required int32&&&& id = 1;& // ID &&&required string&&& str = 2;& // str &&&optional int32&&&& opt = 3;& //optional field }
一个比较好的习惯是认真对待 proto 文件的文件名。比如将命名规则定于如下:
1packageName.MessageName.proto
在上例中,package 名字叫做 lm,定义了一个消息 helloworld,该消息有三个成员,类型为 int32 的 id,另一个为类型为 string 的成员 str。opt 是一个可选的成员,即消息中可以不包含该成员。
编译 .proto 文件
写好 proto 文件之后就可以用 Protobuf 编译器将该文件编译成目标语言了。本例中我们将使用 C++。
假设您的 proto 文件存放在 $SRC_DIR 下面,您也想把生成的文件放在同一个目录下,则可以使用如下命令:
1protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto
命令将生成两个文件:
lm.helloworld.pb.h , 定义了 C++ 类的头文件lm.helloworld.pb.cc , C++ 类的实现文件
在生成的头文件中,定义了一个 C++ 类 helloworld,后面的 Writer 和 Reader 将使用这个类来对消息进行操作。诸如对消息的成员进行赋值,将消息序列化等等都有相应的方法。
编写 writer 和 Reader
如前所述,Writer 将把一个结构化数据写入磁盘,以便其他人来读取。假如我们不使用 Protobuf,其实也有许多的选择。一个可能的方法是将数据转换为字符串,然后将字符串写入磁盘。转换为字符串的方法可以使用 sprintf(),这非常简单。数字 123 可以变成字符串”123”。
这样做似乎没有什么不妥,但是仔细考虑一下就会发现,这样的做法对写 Reader 的那个人的要求比较高,Reader 的作者必须了 Writer 的细节。比如”123”可以是单个数字 123,但也可以是三个数字 1,2 和 3,等等。这么说来,我们还必须让 Writer 定义一种分隔符一样的字符,以便 Reader 可以正确读取。但分隔符也许还会引起其他的什么问题。最后我们发现一个简单的 Helloworld 也需要写许多处理消息格式的代码。
如果使用 Protobuf,那么这些细节就可以不需要应用程序来考虑了。
使用 Protobuf,Writer 的工作很简单,需要处理的结构化数据由 .proto 文件描述,经过上一节中的编译过程后,该数据化结构对应了一个 C++ 的类,并定义在 lm.helloworld.pb.h 中。对于本例,类名为 lm::helloworld。Writer 需要 include 该头文件,然后便可以使用这个类了。
现在,在 Writer 代码中,将要存入磁盘的结构化数据由一个 lm::helloworld 类的对象表示,它提供了一系列的 get/set 函数用来修改和读取结构化数据中的数据成员,或者叫 field。当我们需要将该结构化数据保存到磁盘上时,类 lm::helloworld 已经提供相应的方法来把一个复杂的数据变成一个字节序列,我们可以将这个字节序列写入磁盘。
对于想要读取这个数据的程序来说,也只需要使用类 lm::helloworld 的相应反序列化方法来将这个字节序列重新转换会结构化数据。这同我们开始时那个“123”的想法类似,不过 Protobuf 想的远远比我们那个粗糙的字符串转换要全面,因此,我们不如放心将这类事情交给 Protobuf 吧。
程序清单 2 演示了 Writer 的主要代码,您一定会觉得很简单吧?
清单 2. Writer 的主要代码:
01020304050607080910111213141516171819#include "lm.helloworld.pb.h"…&int main(void) &{ &&&&&lm::helloworld msg1; &&msg1.set_id(101); &&msg1.set_str(“hello”); &&&&&&&// Write the new address book back to disk. &&fstream output("./log", ios::out | ios::trunc | ios::binary); &&&&&&&&&&&if (!msg1.SerializeToOstream(&output)) { &&&&&&cerr && "Failed to write msg." && &&&&&&return -1; &&}&&&&&&&& &&return 0; &}
Msg1 是一个 helloworld 类的对象,set_id() 用来设置 id 的值。SerializeToOstream 将对象序列化后写入一个 fstream 流。
代码清单 3 列出了 reader 的主要代码。
清单 3. Reader:
01020304050607080910111213141516171819202122#include "lm.helloworld.pb.h" …&void ListMsg(const lm::helloworld & msg) { &&cout && msg.id() && &&cout && msg.str() && &} &&&int main(int argc, char* argv[]) { &&lm::helloworld msg1; &&&&{ &&&&fstream input("./log", ios::in | ios::binary); &&&&if (!msg1.ParseFromIstream(&input)) { &&&&&&cerr && "Failed to parse address book." && &&&&&&return -1; &&&&} &&} &&&&ListMsg(msg1); &&… &}
同样,Reader 声明类 helloworld 的对象 msg1,然后利用 ParseFromIstream 从一个 fstream 流中读取信息并反序列化。此后,ListMsg 中采用 get 方法读取消息的内部信息,并进行打印输出操作。
运行 Writer 和 Reader 的结果如下:
1234&writer &reader 101 Hello
Reader 读取文件 log 中的序列化信息并打印到屏幕上。本文中所有的例子代码都可以在附件中下载。您可以亲身体验一下。
这个例子本身并无意义,但只要您稍加修改就可以将它变成更加有用的程序。比如将磁盘替换为网络 socket,那么就可以实现基于网络的数据交换任务。而存储和交换正是 Protobuf 最有效的应用领域。
三、和其他类似技术的比较
看完这个简单的例子之后,希望您已经能理解 Protobuf 能做什么了,那么您可能会说,世上还有很多其他的类似技术啊,比如 XML,JSON,Thrift 等等。和他们相比,Protobuf 有什么不同呢?
简单说来 Protobuf 的主要优点就是:简单,快。这有测试为证,项目 thrift-protobuf-compare 比较了这些类似的技术,图 1 显示了该项目的一项测试结果,Total Time.
图 1. 性能测试结果:
Total Time 指一个对象操作的整个时间,包括创建对象,将对象序列化为内存中的字节序列,然后再反序列化的整个过程。从测试结果可以看到 Protobuf 的成绩很好,感兴趣的读者可以自行到网站 上了解更详细的测试结果。
Protobuf 的优点
Protobuf 有如 XML,不过它更小、更快、也更简单。你可以定义自己的数据结构,然后使用代码生成器生成的代码来读写这个数据结构。你甚至可以在无需重新部署程序的情况下更新数据结构。只需使用 Protobuf 对数据结构进行一次描述,即可利用各种不同语言或从各种不同数据流中对你的结构化数据轻松读写。
它有一个非常棒的特性,即“向后”兼容性好,人们不必破坏已部署的、依靠“老”数据格式的程序就可以对数据结构进行升级。这样您的程序就可以不必担心因为消息结构的改变而造成的大规模的代码重构或者迁移的问题。因为添加新的消息中的 field 并不会引起已经发布的程序的任何改变。
Protobuf 语义更清晰,无需类似 XML 解析器的东西(因为 Protobuf 编译器会将 .proto 文件编译生成对应的数据访问类以对 Protobuf 数据进行序列化、反序列化操作)。
使用 Protobuf 无需学习复杂的文档对象模型,Protobuf 的编程模式比较友好,简单易学,同时它拥有良好的文档和示例,对于喜欢简单事物的人们而言,Protobuf 比其他的技术更加有吸引力。
Protobuf 的不足
Protbuf 与 XML 相比也有不足之处。它功能简单,无法用来表示复杂的概念。
XML 已经成为多种行业标准的编写工具,Protobuf 只是 Google 公司内部使用的工具,在通用性上还差很多。
由于文本并不适合用来描述数据结构,所以 Protobuf 也不适合用来对基于文本的标记文档(如 HTML)建模。另外,由于 XML 具有某种程度上的自解释性,它可以被人直接读取编辑,在这一点上 Protobuf 不行,它以二进制的方式存储,除非你有 .proto 定义,否则你没法直接读出 Protobuf 的任何内容【 2 】。
四、Protobuf 的更多细节
人们一直在强调,同 XML 相比, Protobuf 的主要优点在于性能高。它以高效的二进制方式存储,比 XML 小 3 到 10 倍,快 20 到 100 倍。对于这些 “小 3 到 10 倍”,“快 20 到 100 倍”的说法,严肃的程序员需要一个解释。因此在本文的最后,让我们稍微深入 Protobuf 的内部实现吧。
有两项技术保证了采用 Protobuf 的程序能获得相对于 XML 极大的性能提高:
第一点:我们可以考察 Protobuf 序列化后的信息内容。您可以看到 Protocol Buffer 信息的表示非常紧凑,这意味着消息的体积减少,自然需要更少的资源。比如网络上传输的字节数更少,需要的 IO 更少等,从而提高性能。第二点:我们需要理解 Protobuf 封解包的大致过程,从而理解为什么会比 XML 快很多。
Google Protocol Buffer 的 Encoding
Protobuf 序列化后所生成的二进制消息非常紧凑,这得益于 Protobuf 采用的非常巧妙的 Encoding 方法。
考察消息结构之前,让我首先要介绍一个叫做 Varint 的术语。Varint 是一种紧凑的表示数字的方法。它用一个或多个字节来表示一个数字,值越小的数字使用越少的字节数。这能减少用来表示数字的字节数。
比如对于 int32 类型的数字,一般需要 4 个 byte 来表示。但是采用 Varint,对于很小的 int32 类型的数字,则可以用 1 个 byte 来表示。当然凡事都有好的也有不好的一面,采用 Varint 表示法,大的数字则需要 5 个 byte 来表示。从统计的角度来说,一般不会所有的消息中的数字都是大数,因此大多数情况下,采用 Varint 后,可以用更少的字节数来表示数字信息。下面就详细介绍一下 Varint。
Varint 中的每个 byte 的最高位 bit 有特殊的含义,如果该位为 1,表示后续的 byte 也是该数字的一部分,如果该位为 0,则结束。其他的 7 个 bit 都用来表示数字。因此小于 128 的数字都可以用一个 byte 表示。大于 128 的数字,比如 300,会用两个字节来表示:00 0010。下图演示了 Google Protocol Buffer 如何解析两个 bytes。注意到最终计算前将两个 byte 的位置相互交换过一次,这是因为 Google Protocol Buffer 字节序采用 little-endian 的方式。
图 6. Varint 编码:
消息经过序列化后会成为一个二进制数据流,该流中的数据为一系列的 Key-Value 对。如下图所示。
图 7. Message Buffer:
采用这种 Key-Pair 结构无需使用分隔符来分割不同的 Field。对于可选的 Field,如果消息中不存在该 field,那么在最终的 Message Buffer 中就没有该 field,这些特性都有助于节约消息本身的大小。
以代码清单 1 中的消息为例。假设我们生成如下的一个消息 Test1:
12Test1.id = 10; Test1.str = “hello”;
则最终的 Message Buffer 中有两个 Key-Value 对,一个对应消息中的 id;另一个对应 str。
Key 用来标识具体的 field,在解包的时候,Protocol Buffer 根据 Key 就可以知道相应的 Value 应该对应于消息中的哪一个 field。
Key 的定义如下:
1(field_number && 3) | wire_type
可以看到 Key 由两部分组成。第一部分是 field_number,比如消息 lm.helloworld 中 field id 的 field_number 为 1。第二部分为 wire_type。表示 Value 的传输类型。
Wire Type 可能的类型如下表所示:
在我们的例子当中,field id 所采用的数据类型为 int32,因此对应的 wire type 为 0。细心的读者或许会看到在 Type 0 所能表示的数据类型中有 int32 和 sint32 这两个非常类似的数据类型。Google Protocol Buffer 区别它们的主要意图也是为了减少 encoding 后的字节数。
在计算机内,一个负数一般会被表示为一个很大的整数,因为计算机定义负数的符号位为数字的最高位。如果采用 Varint 表示一个负数,那么一定需要 5 个 byte。为此 Google Protocol Buffer 定义了 sint32 这种类型,采用 zigzag 编码。
Zigzag 编码用无符号数来表示有符号数字,正数和负数交错,这就是 zigzag 这个词的含义了。
图 8. ZigZag 编码:
使用 zigzag 编码,绝对值小的数字,无论正负都可以采用较少的 byte 来表示,充分利用了 Varint 这种技术。
其他的数据类型,比如字符串等则采用类似数据库中的 varchar 的表示方法,即用一个 varint 表示长度,然后将其余部分紧跟在这个长度部分之后即可。
通过以上对 protobuf Encoding 方法的介绍,想必您也已经发现 protobuf 消息的内容小,适于网络传输。假如您对那些有关技术细节的描述缺乏耐心和兴趣,那么下面这个简单而直观的比较应该能给您更加深刻的印象。
对于代码清单 1 中的消息,用 Protobuf 序列化后的字节序列为:
108 65 12 06 48 65 6C 6C 6F 77
而如果用 XML,则类似这样:
12331 30 31 3C 2F 69 64 3E 3C 6E 61 6D 65 3E 68 65 6C 6C 6F 3C 2F 6E 61 6D 65 3E 3C 2F 68 65 6C 6C 6F 77 6F 72 6C 64 3E
一共 55 个字节,这些奇怪的数字需要稍微解释一下,其含义用 ASCII 表示如下:
1234&helloworld& &&&&id&101&/id& &&&&name&hello&/name& &/helloworld&
封解包的速度
首先我们来了解一下 XML 的封解包过程。XML 需要从文件中读取出字符串,再转换为 XML 文档对象结构模型。之后,再从 XML 文档对象结构模型中读取指定节点的字符串,最后再将这个字符串转换成指定类型的变量。这个过程非常复杂,其中将 XML 文件转换为文档对象结构模型的过程通常需要完成词法文法分析等大量消耗 CPU 的复杂计算。
反观 Protobuf,它只需要简单地将一个二进制序列,按照指定的格式读取到 C++ 对应的结构类型中就可以了。从上一节的描述可以看到消息的 decoding 过程也可以通过几个位移操作组成的表达式计算即可完成。速度非常快。
为了说明这并不是我拍脑袋随意想出来的说法,下面让我们简单分析一下 Protobuf 解包的代码流程吧。
以代码清单 3 中的 Reader 为例,该程序首先调用 msg1 的 ParseFromIstream 方法,这个方法解析从文件读入的二进制数据流,并将解析出来的数据赋予 helloworld 类的相应数据成员。
该过程可以用下图表示,图 9. 解包流程图:
整个解析过程需要 Protobuf 本身的框架代码和由 Protobuf 编译器生成的代码共同完成。Protobuf 提供了基类 Message 以及 Message_lite 作为通用的 Framework,CodedInputStream 类,WireFormatLite 类等提供了对二进制数据的 decode 功能。
从 5.1 节的分析来看,Protobuf 的解码可以通过几个简单的数学运算完成,无需复杂的词法语法分析,因此 ReadTag() 等方法都非常快。 在这个调用路径上的其他类和方法都非常简单,感兴趣的读者可以自行阅读。
相对于 XML 的解析过程,以上的流程图实在是非常简单吧?这也就是 Protobuf 效率高的第二个原因了。
五、结束语
往往了解越多,人们就会越觉得自己无知。我惶恐地发现自己竟然写了一篇关于序列化的文章,文中必然有许多想当然而自以为是的东西,还希望各位能够去伪存真,更希望真的高手能不吝赐教,给我来信。谢谢。
全站即时通讯技术资料分类
[1] 网络编程基础资料:
[2] 有关IM/推送的通信格式、协议的选择:
[3] 有关IM/推送的心跳保活处理:
[4] 有关WEB端即时通讯开发:
[5] 有关IM架构设计:
[6] 有关IM安全的文章:
[7] 有关实时音视频开发:
[8] IM开发综合文章:
[9] 开源移动端IM技术框架资料:
[10] 有关推送技术的文章:
[11] 更多即时通讯技术好文分类:
(原文链接:)
TA的最新馆藏[转]&
喜欢该文的人也喜欢go的protobuf这个问题是不是一个bug_百度知道
色情、暴力
我们会通过消息、邮箱等方式尽快将举报结果通知您。
go的protobuf这个问题是不是一个bug
我有更好的答案
pp一般是vc上的叫法或者windows上的文件后缀名;Unix下为C++源文件的默认扩展名。其实没区别。 cc是Linux
为您推荐:
其他类似问题
换一换
回答问题,赢新手礼包679被浏览87684分享邀请回答ASP.NET的时候就体验到的,当然跟Erlang的速错不完全一致,那时候也没有那么高大上的一个名字,但是对待异常的理念是一样的。在.NET项目开发的时候,有经验的程序员都应该知道,不能随便re-throw,就是catch错误再抛出,原因是异常的第一现场会被破坏,堆栈跟踪信息会丢失,因为外部最后拿到异常的堆栈跟踪信息,是最后那次throw的异常的堆栈跟踪信息;其次,不能随便try catch,随便catch很容易导出异常暴露不出来,升级为更严重的业务漏洞。到了Erlang时期,大家学到了速错概念,简单来讲就是:让它挂。只有挂了你才会第一时间知道错误,但是Erlang的挂,只是Erlang进程的异常退出,不会导致整个Erlang节点退出,所以它挂的影响层面比较低。在Go语言项目中,虽然有类似Erlang进程的Goroutine,但是Goroutine如果panic了,并且没有recover,那么整个Go进程就会异常退出。所以我们在Go语言项目中要应用速错的设计理念,就要对Goroutine做一定的管理。在我们的游戏服务端项目中,我把Goroutine按挂掉后的结果分为两类:1、挂掉后不影响其他业务或功能的;2、挂掉后业务就无法正常进行的。第一类Goroutine典型的有:处理各个玩家请求的Goroutine,因为每个玩家连接各自有一个Goroutine,所以挂掉了只会影响单个玩家,不会影响整体业务进行。第二类Goroutine典型的有:数据库同步用的Goroutine,如果它挂了,数据就无法同步到数据库,游戏如果继续运行下去只会导致数据回档,还不如让整个游戏都异常退出。这样一分类,就可以比较清楚哪些Goroutine该做recover处理,哪些不该做recover处理了。那么在做recover处理时,要怎样才能尽量保留第一现场来帮组开发者排查问题原因呢?我们项目中通常是会在最外层的recover中把错误和堆栈跟踪信息记进日志,同时把关键的业务信息,比如:用户ID、来源IP、请求数据等也一起记录进去。为此,我们还特地设计了一个库,用来格式化输出堆栈跟踪信息和对象信息,项目地址:通篇写下来发现比我预期的长很多,所以这里我做一下归纳总结,帮组大家理解这篇文章所要表达的:错误和异常需要分类和管理,不能一概而论错误和异常的分类可以以是否终止业务过程作为标准错误是业务过程的一部分,异常不是不要随便捕获异常,更不要随便捕获再重新抛出异常Go语言项目需要把Goroutine分为两类,区别处理异常在捕获到异常时,需要尽可能的保留第一现场的关键数据以上仅为一家之言,抛砖引玉,希望对大家有所帮助。2018 条评论分享收藏感谢收起201 条评论分享收藏感谢收起今天读了一篇
。这是根据 GDC 2017 上的演讲 Overwatch Gameplay Architecture and Netcode 视频翻译而来的,所以并没有原文。由于是个一小时的演讲,不可能讲得面面俱到,所以理解起来有些困难,我反复读了三遍,然后把英文视频找来(订阅 GDC Vault 可以看,有版权)看了一遍,大致理解了 ECS 这个框架。写这篇 Blog 记录一下我对 ECS 的理解,结合我自己这些年做游戏开发的经验,可能并非等价于原演讲中的思想。
Entity Component System (ECS) 是一个 gameplay 层面的框架,它是建立在渲染引擎、物理引擎之上的,主要解决的问题是如何建立一个模型来处理游戏对象 (Game Object) 的更新操作。
传统的很多游戏引擎是基于面向对象来设计的,游戏中的东西都是对象,每个对象有一个叫做 Update 的方法,框架遍历所有的对象,依次调用其 Update 方法。有些引擎甚至定义了多种 Update 方法,在同一帧的不同时机去调用。
这么做其实是有极大的缺陷的,我相信很多做过游戏开发的程序都会有这种体会。因为游戏对象其实是由很多部分聚合而成,引擎的功能模块很多,不同的模块关注的部分往往互不相关。比如渲染模块并不关心网络连接、游戏业务处理不关心玩家的名字、用的什么模型。从自然意义上说,把游戏对象的属性聚合在一起成为一个对象是很自然的事情,对于这个对象的生命期管理也是最合理的方式。但对于不同的业务模块来说,针对聚合在一起的对象做处理,把处理方法绑定在对象身上就不那么自然了。这会导致模块的内聚性很差、模块间也会出现不必要的耦合。
我觉得守望先锋之所以要设计一个新的框架来解决这个问题,是因为他们面对的问题复杂度可能到了一个更高的程度:比如如何用预测技术做更准确的网络同步。网络同步只关心很少的对象属性,没必要在设计同步模块时牵扯过多不必要的东西。为了准确,需要让客户端和服务器跑同一套代码,而服务器并不需要做显示,所以要比较容易的去掉显示系统;客户端和服务器也不完全是同样的逻辑,需要共享一部分系统,而在另一部分上根据分别实现……
问题的起因是 skynet 上的一个
,大概是说 socket 线程陷入了无限循环,有个 fd 不断的产生新的消息,由于这条消息既不是 EPOLLIN 也不是 EPOLLOUT ,导致了 socket 线程不断地调用 epoll_wait 占满了 cpu 。
我在自己的机器上暂时无法重现问题,从分析上看,这个制造问题的 fd 是 0 ,也就是 stdin ,猜想和重定向有关系。
skynet 当初并没有处理 EPOLLERR 的情况(在 kqueue 中似乎没有对应的东西),这个我今天的 patch 补上了,不过应该并不能彻底解决问题。
我做了个简单的测试,如果强行 close fd 0 ,而在 close 前不把 fd 0 从 epoll 中移除,的确会造成一个不再存在的 fd (0) 不断地制造 EPOLLIN 消息(和 issue 中提到的不同,不是 EPOLLERR)。而且我也再也没有机会修复它。因为 fd 0 被关闭,所以无法在出现这种情况后从 epoll 移除,也无法读它(内核中的那个文件对象),消息也就不能停止。
是我设计的一个类 google protocol buffers 的东西。
在很多年前,我在我经手的一些项目中使用 google protocol buffers
。用了好几年,经历了几个项目后,我感觉到它其实是为静态编译型语言设计的协议,其实并没有脱离语言的普适性。在动态语言中,大家都不太愿意使用它(json 更为流行)。一个很大的原因是,protobuffers 是基于代码生成工作的,如果你不使用代码生成,那么它自身的 bootstrap 就非常难实现。
因为它的协议本身是用自身描述的,如果你要解析协议,必须先有解析自己的能力。这是个先有鸡还是先有蛋的矛盾。过去很多动态语言的 binding 都逃不掉引入负责的 C++ 库再加上一部分动态代码生成。我对这点很不爽,后来重头实现了
这个库。虽然它还有一些问题,并且我不再想维护它,这个库加上 lua 的 binding 依然是 lua 中使用 protobuffer 的首选。
这两个月,我的主要工作是跟进公司内一个 MMORPG 项目,做一些代码审查提出改进意见的工作。
在数月前,项目经理反应程序不太稳定,经常出一些错误,虽然马上就可以改好,但是随着开发工作推进,不断有新的 bug 产生。我在浏览了客户端的代码后,希望修改一下客户端的 UI 框架以及消息分发机制等,期望可以减少以后的 bug 出生概率。由于开发工作不可能停下来重构,所以这相当于给飞行中的飞机换引擎,做起来需要非常小心,逐步迭代。
工作做了不少,其中一个小东西我觉得值得拿出来写写。
我希望 UI 部分可以严格遵守 MVC 模式来实现。其实道理都明白,但实际操作的时候,大部分人又会把这块东西实现得不伦不类。撇开各种条条框框,纸上谈兵的各种模式,例如 MVC MVP MVVM 这些玩意,我认为核心问题不在于 M 和 V 大家分不清楚,而是 M 和 V 产生联系的时候,到底应该怎么办。联系它们的是 C 还是 P 或是 VM 都只为解决一个问题:把 M 和 V 解耦。
昨天在 review 我公司一个正在开发的项目客户端代码时,发现了一些坏味道。
客户端框架创建了一个简单的对象系统,用来组织客户端用到的对象。这些对象通常是有层级关系的,顶层对象放在一个全局集里,方便遍历。通常,每帧需要更新这些对象,处理事件等等。
顶层每个对象下,还拥有一些不同类别的子对象,最终成为一个森林结构,森林里每个根对象都是一颗树。对象间有时有一些引用关系,比如,一个对象可以跟随另一个对象移动,这个跟随就不是拥有关系。
这种设计方法或模式,是非常常见的。但是在实现手法上,我闻到了一丝坏味道。
今天在公司群里,Net bug 同学提出了一个问题,围绕这个问题大家展开了一系列讨论。讨论中谈及了 lua 中的一个常见的模式:property table ,我觉得挺有意思,记录一下。
最初的问题是:当一个对象的某些属性并不常用,希望做惰性初始化的话,应该怎么实现。
我认为,property table 是一个很符合这个案例的常见模式。
比如,对象 f 有三个可能的成员 a b c ,我们可以不把 f.a f.b f.c 记录在 f 这个 table 里,而是额外有三张大表,a b c 。利用 metatable ,可以在访问 f.a 的时候,实际访问的是 a[f] 。也就是说,所有同类对象的 a 属性,都是从 a 这张表里访问的。
a 这张表的 key 就是对象,value 是对象对应的 a 属性值。
无论是客户端还是服务器,把 lua 作为嵌入语言使用的时候,都在某种程度上希望把 lua 脚本做多线程使用。也就是你的业务逻辑很可能有多条业务线索,而你希望把它们跑在同一个 lua vm 里。
lua 的 coroutine 可以很好的模拟出线程。事实上,lua 自己也把 coroutine 对象叫做 thread 类型。
最近我在反思 skynet 的 lua 封装时,想到我们的主线程是不可以调用阻塞 api 的限制。即在主干代码中,不可以直接 yield 。我认为可以换一种更好(而且可能更简洁)的封装形式来绕过这个限制,且能简化许多其它部分的代码。
下面介绍一下我的新想法,它不仅可以用于 skynet 也应该能推广到一切 lua 的嵌入式应用(由你自己来编写 host 代码的应用,比如客户端应用):
最近在尝试重新写 skynet 2.0 时,把过去偶尔用到的一个对象生命期管理的手法归纳成一个固定模式。
先来看看目前的做法:
其中,对象在获取其引用传入处理函数中处理时,将对象的引用加一,处理完毕再减一。这就是常见的基于引用计数的对象生命期管理。
常规的做法(包括 C++ 的智能指针)是这样的:对象创建时,引用为 1 (或 0)。每次要传给另一个处地方处理,或保留待以后处理时,就将其引用增加;不再使用时,引用递减。当引用减为
0 (或负数)时,把对象引用的资源回收。
由于此时对象不再被任何东西引用,这个回收销毁过程就可视为安全且及时的。不支持 GC 的语言及用这些语言做出来的框架都用这个方式来管理对象。
这个手法的问题在于,对象的销毁时机不可控。尤其在并发环境下,很容易引发问题。问题很多情况是从性能角度考虑的优化造成的。
加减引用本身是个很小的开销,但所有的引用传递都去加减引用的话,再小的开销也会被累积。这就是为什么大多数支持 GC 的语言采用的是标记扫描的 GC 算法,而不是每次在对象引用传递时都加减引用。
大部分情况下,你能清楚的分辨那些情况需要做引用增减,哪些情况下是不必的。在不需要做引用增减的地方去掉智能指针直接用原始指针就是常见的优化。真正需要的地方都发生在模块边界上,模块内部则不需要做这个处理。但是在 C/C++ 中,你却很难严格界定哪些是边界。只要你不在每个地方都严格的做引用增减,错误就很难杜绝。
使用 id 来取代智能指针的意义在于,对于需要长期持有的对象引用,都用 id 从一个全局 hash 表中索引,避免了人为的错误。(相当于强制从索引到真正对象持有的转换)
id 到对象指针的转换可以无效,而每次转换都意味着对象的直接使用者强制做一个额外的检查。传递 id 是不需要做检查的,也没有增减引用的开销。这样,一个对象被多次引用的情况就只出现在对象同时出现在多个处理流程中,这在并发环境下非常常见。这也是引用计数发挥作用的领域。
而把对象放在一个集合中这种场景,就不再放智能指针了。
长话短说,这个流程是这样的:
将同类对象放在一张 hash 表中,用 id 去索引它们。
所有需要持有对象的位置都持有 id 而不是对象本身。
需要真正操作持有对象的地方,从 hash 表中用 id 索引到真正的对象指针,同时将指针加一,避免对象被销毁,使用完毕后,再将对象引用减一。
前一个步骤有可能再 id 索引对象指针时失败,这是因为对象已经被明确销毁导致的。操作者必须考虑这种情况并做出相应处理。
看,这里销毁对象的行为是明确的。设计系统的人总能明确知道,我要销毁这个对象了。 而不是,如果有人还在使用这个对象,我就不要销毁它。在销毁对象时,同时有人正在使用对象的情况不是没有,并发环境下也几乎不能避免。(无法在销毁那一刻通知所有正在操作对象的使用者,操作本身多半也是不可打断的)但这种情况通常都是短暂的,因为长期引用一个对象都一定是用 id 。
了解了现实后,“当对象的引用为零时就销毁它” 这个机制是不是有点怪怪的了?
明明是:我认为这个对象已经不需要了,应该即使销毁,但销毁不应该破坏当下正在使用它的业务流程。
这次,我使用了另一个稍微有些不同的模式。
每个对象除了在全局 hash 表中保留一个引用计数外,还附加了一个销毁标记。这个标记只在要销毁时设置一次,且不可翻转回来。
现在的流程就变成了,想销毁对象时,设置 hash 表中关联的销毁标记。之后,检查引用计数。只有当引用计数为 0 时,再启动销毁流程。
任何人想使用一个对象,都需要通过 hash 表从 id 索引到对象指针,同时增加引用计数,使用完毕后减少引用。
但,一旦销毁标记设置后,所有从 id 索引到对象指针的请求都会失败。也就是不再有人可以增加对象的引用,引用计数只会单调递减。保证对象在可遇见的时间内可被销毁。
另外,对象的创建和销毁都是低频率操作。尤其是销毁时机在资源充裕的环境下并不那么重要。所以,所有的对象创建和销毁都在同一线程中完成,看起来就是一个合理的约束了。 尤其在 actor 模式下, actor 对象的管理天生就应该这么干。
有了单线程创建销毁对象这个约束,好多实现都可以大大简化。
那个维护对象 id 到指针的全局 hash 表就可以用一个简单的读写锁来实现了。索引操作即对 hash 表的查询操作可遇见是最常见的,加读锁即可。创建及销毁对象时的增删元素才需要对 hash 表上写锁。而因为增删元素是在同一线程中完成的,写锁完全不会并发,对系统来说是非常友好的。
对于只有唯一一个写入者的情况,还存在一个小技巧:可以在增删元素前,复制一份 hash 表,在副本上慢慢做处理。只在最后一个步骤才用写锁把新副本交换过来。由于写操作不会并发,实现起来非常容易。
起因是最近有人在 skynet 邮件列表里贴了段错误 log ,从 log 显示,他在 table.sort 的比较函数里调用了 skynet 的 snax rpc 去获取远程数据。然后被 lua 无情的报了 attempt to yield across a C-call boundary 。
就 table.sort 不能 yieldable 的问题,其实 。老大的说法是,这个 C 实现是递归的,想要在 C 层面保留上下文非常困难,如果勉强实现,也会大大降低正常不需要 yield 的 case 的性能,非常不划算。
通过这件事,我反而觉得 none-yieldable 的限制反而提前阻止了一个错误的实现,其实是应该庆幸的。
是我自己设计, 用在我们新项目中取代过去用到的 google protocol buffers 的东西。
为什么不用 protobuf ? 这个问题我有足够的发言权。在 lua 语言为主的项目中,sproto 更合适。google 官方并没有给 protobuf 加入 lua 支持。现在在网上流传的 protobuf lua 方案,被人用的最多的两种,一个是
的 lua binding ,另一个是
。前者是我在开发维护,并使用了多年;后者是在我过去的项目中,项目中的同事因为需要而开发的。
另外,在我的项目的副产品中,还有开源的 protobuffer 的 as3 库以及 erlang 库,都有许多用户。所以,我相信我对 protobuf
有足够长时间的使用经验以及对它有足够的了解。这也是放弃 protobuf 而转向自己设计的 sproto 的底气所在。
在这一篇 blog 中,不想讨论 protobuf 的优劣,只谈谈 sproto 中如何使用 rpc 的 api 。这是 sproto 的 api 文档中没有写明,而很多想用它的同学问起的问题。
花了两天给
增加了 unordered map 的支持。
问题是这样的:
sproto 支持数组,但很多情况下,业务处理中,我们并不用数组来保存大量的相同类型的结构数据。因为那样不方便检索。
比如你要配置若干地图表、NPC 表等等的信息,固然可以用 sproto 的 array 来保存。但是在运行时,你更希望用定义好的 id 来检索它们。如果 sproto 不支持 unordered map 的话,你就需要在 decode 之后,对 array
table 做一次遍历,用一张新表来建立索引。
google protocal buffers 2 也有这个问题,据说第 3 版要增加 map 用来兼容 json ,这个话题最后再说。
由于有公司很多同事参与 ejoy2d
的开发,所以
这个项目已经转移到 ejoy 的 github 名下。
有更多项目的参与的情况下,原来 ejoy2d 的简单构架慢慢显出一些局限性。主要是不同的项目会根据项目的需要(通常是针对某些特定需求的优化,以及特别的效果需求)修改底层 shader 的部分。最早设计的时候,因为考虑到只是用于 2d 游戏的开发,所以把 shader 模块实现的比较简单。特别是
attribute layout 是固定的,而 uniform 管理也没有留下太多扩展性。
在现代手机的 GPU 架构下,从渲染层渲染层 API 看,其实 2d 和 3d 其实没有本质上的区别。都是基于三角片渲染的。需要把顶点上传到 GPU 中由 vs 处理,在最后对像素做 fs 渲染出来。
而 2d engine 和 3d engine 的区别通常在于 2d engine 的顶点变换很简单。不需要用 projection matrix 和 view matrix 做变换。2d engine 中的对象多半是四边形,数量很多,常见的优化手法是将大量的四边型合并到同一个渲染批次中;所以 world matrix (以平移变换为主)在 CPU 中和顶点计算再提交更常见一些。
2d engine 从应用上说,就是在处理一张张图片。所以对图片(四边型)的处理的变化要多一些。这使得 fs 要多变一点,需要引擎提供一定的可定制性。但很少去处理 3d engine 常见的光照投影这些东西。更多的是为了优化贴图用量等目的而技巧性的去使用一些图片。
突出 2d engine 的专门面对的业务的特性,而简化 GPU 提供的模型,用简短的代码构建 engine 框架,是 ejoy2d 设计的初衷。而且我也相信,简单可以带来更好的性能。所以一开始设计 ejoy2d 的时候,shader 模块的很多东西都被写死了,以最简单的方式达到目的。仅暴露了很少的外部接口,再在这些有限的接口上设计数据结构,做性能优化。
。随着 skynet 的日趋完善,我希望找到一个更为简单易用的方法实现类似的需求。
对于不常更新的数据,我在 skynet 里增加了 sharedata 模块,用于配置数据的共享。每次更新数据,就将全部数据打包成一个只读的树结构,允许多个 lua vm 共享读。改写的时候,重新生成一份,并将老数据设置脏标记,指示读取者去获取新版本。
这个方案有两个缺点,不适合实时的数据更新。其一,更新成本过大;其二,新版本的通告有较长时间的延迟。
我希望再设计一套方案解决这个实时性问题,可以用于频繁的数据交换。(注:在 mmorpg 中,很可能被用于同一地图上的多个对象间的数据交换)
一开始的想法是做一个支持事务的树结构。对于写方,每次对树的修改都同时修改本地的 lua table 以及被修改 patch 累计到一个尽量紧凑的序列化串中。一个事务结束时,调用 commit
将快速 merge patch 。并将整个序列化串共享出去。相当于快速做一个快照。
读取者,每次读取时则对最新版的快照增加一次引用,并要需反序列化它的一部分,变成本地的 lua table 。
我花了一整天实现这个想法,在写了几百行代码后,意识到设计过于复杂了。因为,对于最终在 lua
中操作的数据,实现一个复杂的数据结构,并提供复杂的 C 接口去操作它性能上不会太划算。更好的方法是把数据分成小片断(树的一个分支),按需通过序列化和反序列化做数据交换。
既然序列化过程是必须的,我们就不需要关注数据结构的问题。STM 需要管理的只是序列化后的消息的版本而已。这一部分(尤其是每个版本的生命期管理)虽然也不太容易做对,但结构简单的多。
这个周末,我实现了 ,并重新命名为
在实现过程中,发现了许多编码格式上可以优化的地方,所以一边实现一边做调整,使结构更适合编码和解码,并且更紧凑。
做了如下改动:
由于这个东西主要 binding 到 lua 这样的动态语言中使用,所以我不需要按 Cap'n Proto 那样,直接访问编码后的数据结构(直接把数据结构映射为 C/C++ 对象),所以数据对齐是不必要的。
编码时的 tag 如果要求严格升序也可以更快的处理数据,减少实现的复杂度。数据段也要求按持续排列,且不准复用。这样可以让数据中有更多的 0 方便压缩。
把 boolean 数组按位打包的意义也不太大(会增加实现的复杂度)。
暂时先不实现 64bit id 的类型。(以后再加)
最终的 Wire Protocol 是这样的:
我们一直使用 google protocol buffer 协议做客户端服务器通讯,为此,我还编写了 。
经过近三年的使用,我发现其实我们用不着那么复杂的协议,里面很多东西都可以简化。而另一方面,我们总需要再其上再封装一层 RPC 协议。当我们做这层 RPC 协议封装的时候,这个封装层的复杂度足以比全新设计一套更合乎我们使用的全部协议更复杂了。
由于我们几乎一直在 lua 下使用它,所以可以按需定制,但也不局限于 lua 使用。这两天,我便构思了下面的东西:
为什么大部分网络服务都需要一个数据库在后台支撑整个系统?
这通常是因为大部分系统的一个运行周期都很短,对于传统的网站服务来说,从收到一个 HTTP 请求开始,到终端用户收到这个请求的结果为止,就是一个运行周期。而其间可能处理的数据集是很大的,通常没有时间(甚至没有空间)把所有数据都加载到内存,处理其中涉及的一小部分,然后保存在磁盘上再退出。
当数据量巨大时,任何对数据的操作的算法和数据结构都需要精心设计,这不是随便一个程序员就可以轻松完成的任务。尤其是数据量大到超过内存容量时,很多算法和数据结构对大部分非此领域的程序员来说都是陌生的。本着专业的事情交给专业的人来做的原则,一般系统都会把这部分工作交给独立的数据库来完成。
对数据的操作只有抽象的足够简单,系统才能健壮,这便有了 SQL 语言做一层抽象,让数据管理的工作可以独立出来。甚至于你想牺牲一部分的特性来提高性能,还可以选用近年来流行的各种 NOSQL 数据库。
可在 MMO 游戏服务器领域,事情发生了一点点变化。
数据和业务逻辑是密切相关的,改变非常频繁。MMO 服务器需要持续快速的响应用户的请求。我们几乎不可能把一切数据都放在独立的数据库中,比如玩家在虚拟世界中的位置,以及他所影响的其他玩家的列表;玩家战斗时的各种属性变化,还有和玩家互动的那些 NPC 的状态改变……
最大的矛盾是:MMO 游戏中数据集的改变不再是简单的 SQL 可以表达的东西,不可能交给数据库服务期内部完成。无论什么类型的数据库,都不是为这种应用设计的。如果你硬要套用其它领域的应用模式的话,游戏服务器只能频繁的把各种数据从数据库中读出来,按游戏逻辑做出改变,再写回去。数据库变成了一个很低效的数据中转中心,无论你是否使用内存数据库,都改变不了这个低效的本质。
我听过无数从别的领域转行到游戏领域做开发的程序员设计出来的糟糕系统。他们最终仅仅把数据库当成一个可靠的数据储存点和中转点,认为把所谓重要的数据写进数据库就万事大吉,然后再别扭的从另一个位置把数据从数据库读出来使用。系统中充满了对数据库的奇怪异步回调用来改善系统的反应速度,而系统却始终步履阑珊。能做对已经是极限了,更何况游戏系统不仅仅是输入输出正确就是正确,如果超过了应用的响应时间,一切都是不正确的。
这个国庆假期,我读完了《D程序设计语言》 一书。里面读到了很多有趣的东西,挑一点写出来和大家分享一下。
字符串,数组和关联数组(hash 表)是最重要的三种数据结构,我们几乎可以利用它们模拟出任何更复杂的结构。Lua 就是这么干的,只不过 Lua 把数组和关联数组合并成一个 table 类型了。D 在语言层面对这三种数据结构支持的很好,概念定义非常清晰。这一篇只谈数组和字符串,不涉及 hash 表的部分。
数组可以看成是存放同一类型数据的连续内存。
在 C 语言中,数组和指针虽然是不同的类型,但编译器生成的代码却是相同的,可以说实质上,数组即指针。但将数组隐含有长度信息,即内存的范围。有些数组是固定大小的,在编译器就知道其范围;有些数组需要动态扩展大小,其范围是运行期确定,并可以改变的。无论如何,对数组的随机访问,缺乏边界检查的代码都隐藏着风险。
D 语言是一门期望有高安全性的同时又重视运行性能的语言。它在平衡这个问题上的解决方案很有趣。程序员可以指定一段代码是安全的,还是系统级的,还是是接口安全的。根据不同的标注来插入边界检查代码。在 debug 版中,即使是系统级代码,也会插入类似 assert 的契约检查。
由于 D 语言以 GC 为内存管理核心(且要求所有数据都是位置无关,可移动的),所以管理数组切片 Slice 就变得很简单。不同的Slice 引用同一块内存,不用担心数据生命期问题。扩展数组也可以根据需要重新分配内存,或是在原地扩展。
提到数组扩展,不得不谈一下 D 语言中结构的 postblit 。D 语言中,所有的 class 都是引用语义的,而 struct 是值语义的。C++ 中花了很多年想解决的一个性能问题就是源于 vector 扩展时,数据如何从旧的位置移动新位置的问题。在 stl 的 sgi 实现中,为 POD 结构增加的特化模板来提高复制效率;在 C++11 中又从语言层面增加了右值引用来实现移动语义,来解决反复析构构造对象带来的性能浪费。
而 D 语言中没有那些晦涩的移动构造,拷贝构造概念;它只有 postblit 。也就是数据都应该默认按位复制(blit),然后在 blit 后,再用 postblit 方法去修改新的副本。这种不动源对象,而只在副本上修改的移动钩子技术概念更简单清晰。而且编译器可以自行推导什么时候调用 postblit 才是必要的。这个技术不仅仅用来解决数组的扩展问题,也可以很好的搞定 C++ 中返回值优化问题。
对于固定大小的数组,D (2.0) 是按值类型处理的(动态数组则是引用类型),不同长度的数组是不同的类型,但它们都可以隐式转换(映射)成动态数组。比较短的固定数组做值传递的时候更方便高效,也符合其它基础类型的特征。长数组可以通过 ref 修饰按引用传递。
C 语言缺乏原生的 string 类型的支持,这使得字符串管理非常烦琐。我在 2006 年左右的一个项目中,我根据项目实际情况,简化了 C string 库,把大部分 string 都做了
,并直到进程退出再释放 string interning pool 。
但这种用法毕竟不够通用。
今天读到 facebook 开源的
,里面也实现了一个简单的 string 库。我有些想法。
libphenom 的 string 库核心想针对问题是尽量的减少堆上内存的动态分配。它把大部分临时字符串都放在栈上处理,也提供了用户自定义串空间的方法。我觉得这个方向是不错的,但是其实大可不必提供太多的弹性,只要尽量让临时字符串存在于栈上即可。而另一个很重要的功能,也就是 string interning 我认为更有实用性。
string interning 可以实现 symbol 类型,对于类似 json/xml 的解析来说非常有意义。可以节约许多内存,而且可以加快 symbol 的比较和 hash 速度。不过对所有字符串无差别的做 interning 有可能因为外部输入多变也被攻击。对 interning 的字符串做引用计数也会降低性能。
最近听从同事建议想尝试一下 MongoDB 。
前年,图灵的同学送过我一本《》 ,当时我花了两个晚上看完。我所有的认知就是这本书了。我们最近的合作项目
也是用的 MongoDB ,最近封测阶段,关于数据库部分也出过许多问题。蜗牛同学在帮助成都的同学做调优,做了不少工作。总是能在办公室里听到关于 MongoDB 的话题。
做一个 MongoDB 的 Driver 。
Skynet 默认是用 lua 做开发语言的。那么为什么不直接用
因为 skynet 需要一个异步库,不希望一个 service 在做数据库操作的时候被阻塞住。那么,我们就不可能直接把 luamongo 作为库的形式提供给 lua 使用。
一个简单的方法是 skynet 目前对 redis 做的封装那样(当然,skynet 中的 redis 封装也是非阻塞的),提供一个独立的 service 去访问数据库,然后其它服务器向它发送异步请求。如果我直接使用 luamongo 就会出现一个问题:
我需要先把请求从 lua table 序列化,发送给和 mongoDB 交互的位置,反序列化后再把 lua table 打包成 bson 。获得 MongoDB 的反馈后,又需要逆向这个流程。这是非常低效的事情。如果我们可以直接让请求方生成 bson 对象,这样就可以直接把 bson 对象的指针发过到 交互模块就够了( skynet 是单进程模型,可以在服务内直接交换 C 指针)。这就需要我定制一套 lua moogodb 的 driver 了。
数据结构中的树结构在抽象复杂事物时非常常见,在图形引擎中,多用于场景以及 sprite 的层级管理。在 GUI 相关的模块中也是必备的结构。其它领域,比如对程序源码本身的解释翻译,以及对数据文件的组织管理,都离不开树结构。
我觉得,这是因为一个对象,除了它自身的属性(例如大小、形状、颜色等)之外,还需要一些外部属性(例如位置、层次、方向等)需要逐级继承。每个对象都可以分解成更细的对象组合而构成、这些对象在组成新的对象后,它们的聚合体又体现出和个体的相似性(至少具有类似的外部属性)。这使得采用树状数据结构最容易描述它们。
我最近的一些工作代码做了很多这方面的工作,回想这些年里,我不只一次的思考类似的问题(),而每次最后解决问题的代码的都有些不同,编程风格也有一些变化。总结一下这段时间的思考,今天再写这么一篇 blog 。
树结构的基本操作无非是遍历整棵树、遍历一层分支、添加节点、移动节点、删除节点这些。但在大部分应用环境下,我们最多用到的只是遍历,而非控制树的结构本身。
最近稍微学习了一点 Objective-C ,做笔记和做编码练习都是巩固学习的好方法。整理记录脑子里的新知识有助于理清思路,发现知识盲点以及错误的理解。
Objective-C 和 C++ 同样从兼容 C 语言开始,以给 C 语言增加面向对象为初衷,他们的出现的时间都很类似(1983 年左右)。但面向对象编程的源头却不同:C++ 受 Simula 和 Ada 的影响比较多,而 Objective-C 的相关思想源至 Smalltalk ,最终的结果是他们在对象模型上有不小的差异。
以我这些天粗浅的了解,Objective-C 似乎比 C++ 更强调类型的动态性,而牺牲了一些执行性能。不过这些牺牲,由于模型清晰,可以在今天,由更先进的编译技术来弥补了。
我对 C++ 的认知比 Objective-C 要多的多,所以对 C++ 开发中会遇到的问题的了解也多的多。在学习 Objective-C 的过程中,我发现很多地方都可以填上曾经在 C++ 开发中遇到的问题。当然,Objective-C 一定也有它自己的坑,只是我才刚开始,没有踩到过罢了。
ObjC 的类方法调用的形式,更接近于向对象发送消息。语法写作:
最近特别忙, 每天写程序的时间都不够。有些东西在做完之前不想公开谈,所以只把一些笔记发在公司内部的周报里了。等这段时间过去,再贴到这里来。
不过还是有一些泛泛的心得可以写写的。
前几天遇到一个优化的问题。我想采用定期计算路图的方式优化寻路的算法。而不用每次每个单位在想查找目标的时候都去做一次运算并记录下路径结果。一切都看起来很顺利,算法的正确性很快就被验证了。可是最后实际跑的时候,发现在生成路图的地方会稍微卡一下影响流畅性。
这几天无意中发现一款开源的 3d engine ,名为
。 虽然不多,但写的很漂亮。从源码仓库 clone 了一份,读了几天,感觉设计上有许多可圈可点的地方,颇为有趣。今天简略写一篇 blog 和大家分享。
ps. 在官方主页上,pixel light 是基于 OpenGL 的,但实际上,它将渲染层剥离的很好。如果你取的是源代码,而不是下载的 SDK 的话,会发现它也支持了 Direct3D 。另,从 2013 年开始,这个项目将 License 改为了 MIT ,而不是之前的 LGPL 。对于商业游戏开发来说,GPL 的确不是个很好的选择。
这款引擎开发历史并不短(从 2002 年开始),但公开时间较晚(2010 年),远不如 OGRE 等引擎有名。暂时我也没有看到有什么成熟的游戏项目正在使用。对于没有太多项目推动的引擎项目,可靠性和完备性是存疑的。不推荐马上在商业游戏项目中使用。但是,他的构架设计和源代码绝对有学习价值。
经过一个月, 我基本完成了
的 C 版本的编写。中间又反复重构了几个模块,精简下来的代码并不多:只有六千余行 C 代码,以及一千多 Lua 代码。虽然部分代码写的比较匆促,但我觉得还是基本符合我的质量要求的。Bug 虽不可避免,但这样小篇幅的项目,应该足够清晰方便修正了吧。
花在 Github 上的这个开源项目上的实际开发实现远小于一个月。我的大部分时间花了和过去大半年的 Erlang 框架的兼容,以及移植那些不兼容代码和重写曾经用 Erlang 写的服务模块上面了。这些和我们的实际游戏相关,所以就没有开源了。况且,把多出这个几倍的相关代码堆砌出来,未必能增加这个开源项目的正面意义。感兴趣的同学会迷失在那些并不重要,且有许多接口受限于历史的糟糕设计中。
在整合完我们自己项目的老代码后,确定移植无误,我又动手修改了 skynet 的部分底层设计。在保证安全迁移的基础上,做出了最大限度的改进,避免背上过多历史包袱。这些修改并不容易,但我觉得很有价值。是我最近一段时间仔细思考的结果。今天这一篇 blog ,我将最终定稿的版本设计思路记录下来,备日后查阅。
今天实现了一个 .
我相信这个东西已经被无数 C 程序员实现过了, 但是通过 google 找了许多, 或是接口不让我满意, 或是过于重量.
在 Windows 下, 我们可以通过
来实现 coroutine , 在 posix 下, 有更简单的选择就是
我的需求是这样的:
这几天,安排新来的王同学做数据持久化工作。一开始他是将 sharedb 里的数据序列化为文本储存的。这步工作做完后,开始动手把数据放到 Redis 数据库中。我们的系统主干由 Lua 构建,所以需要一个 Lua 的 Redis 库。google 来的那份,王同学不满意。三下五除二自己重写了一个。据说把代码量减少到了原来的三分之一(开源计划我正在督促)。唯一的问题是,如果直接采用系统的 socket 库,不能很好的嵌入我们的整个通讯框架。我们的 skynet 全部是通过异步 IO 自己调度的,如果这个数据服务单方面阻塞了进程,会使得别的进程获得不了时间片。
蜗牛同学打算改进 skynet 增加异步 IO 的支持。
我今天在考虑现有的 API 时候,对比原有的 timer 接口和打算新增加的异步 IO 接口,发现它们其实是同一类东西。即,都是一个异步事件。由客户准备好一类请求,绑定一个 session id 。当这个事件发生后,skynet 将这个 session id 推送回来,通知这个事件已经发生。
在用户编写的代码的执行序上,异步 IO 和 RPC 调用一样,虽然底层通过消息驱动回调机制转了一大圈,但主干上的逻辑执行次序是连续的。
受历史影响,我之前在封装 Timer 的时候,受到历史经验的影响,简单的做了个 lua 内 callback 的封装。今天仔细考虑后发现,我们整个系统不应该存在任何显式的回调机制。正确的接口应该保持和异步 IO 一致:
过年了,人都走光了,结果一个人活也干不了。所以我便想找点东西玩玩。
今天想试一下 libev 写点代码。原本在我那台 ubuntu 机器上一点问题都没有,可在 windows 机上用 mingw 编译出来的库一个 backend 都没有,基本不可用。然后网上就有同学推荐我试一下 libuv 。
libuv 是 node.js 作者做的一个封装库,在 unix 环境整合的 libev ,而在 windows 下用 IOCP 另实现了一套。看起来挺满足我的玩儿的需求的。所以就试了一下。
其实铁路订票系统面临的技术难点无非就是春运期间可能发生的海量并发业务请求。这个加上一个排队系统就可以轻易解决的。
本来我在 weibo 上闲扯两句,这么简单的方案,本以为大家一看就明白的。没想到还是许多人有疑问。好吧,写篇 blog 来解释一下。
简单说,我们设置几个网关服务器,用动态 DNS 的方式,把并发的订票请求分摊开。类比现实的话,就是把人分流到不同的购票大厅去。每个购票大厅都可以买到所有车次的票。OK ,这一步的负载均衡怎么做我就不详细说了。
每个网关其实最重要的作用就是让订票的用户排队。其实整个系统也只用做排队,关于实际订票怎么操作,就算每个网关后坐一排售票员,在屏幕上看到有人来买票,输入到内部订票系统中出票,然后再把票号敲回去,这个系统都能无压力的正常工作。否则,以前春运是怎么把票卖出去的?
我们来说说排队系统是怎么做的:
很久没有写工作笔记了,如果不在这里写,我连写周报的习惯都没有。所以太长时间不写就会忘记到底做了些啥了。
这半个多月其实做了不少工作,回想起来又因为太琐碎记不太清。干脆最近这几天完成的这部分工作来写写吧。
第四篇谈到了 agent 的处理流程。但实际操作下来还是觉得概念显得复杂。推而广之,对于不是 agent 的服务,我需要一个通用的消息处理框架。
对于每个服务器,可以看成是对一组约定的服务协议进行处理。对于协议分组,之前我有许多想法,可做下来又发现了若干问题。本来我希望定义出一个完整的 session 概念,同一个 session 下,可以分不同的步骤,每个步骤都有一个激活的协议组。协议组之间可以共享状态,同时限制并发。做下来发现,很难定义出完整的事务处理流程并描述清楚。可能需要设计一个 DSL 来解决这个问题更好一些。一开始我也是计划设置这个小语言的。可一是时间紧迫,二是经验不足,很难把 DSL 设计好。
而之前的若干项目证明,其实没有良好的事务描述机制,并不是不可用。实现一个简单的 RPC 机制,一问一答的服务提供方式也能解决问题。程序员只要用足够多经验,是可以用各种土法模拟长流程的事务处理流。只是没有严格约束,容易写出问题罢了。那么这个问题的最小化需求定义就是:可以响应发起请求人的请求,解析协议,匹配到对应的处理函数。所有请求都应该可以并发,这样就可以了。至于并发引起的问题,可以不放在这个层次解决。
我谨慎的选择了 RPC 这种工作方式。实现了一个简单的 RPC 调用。因为大多数服务用 Lua 来实现,利用 coroutine 可以工作的很好。不需要利用 callback 机制。在每条请求/回应的数据流上,都创建了独立的环境让工作串行进行。相比之前,我设计的方案是允许并发的 RPC 调用的。这个修改简化了需求定义,也简化的实现。
举例来说,如果 Client 发起登陆验证请求,那么由给这个 Client 服务的 Agent 首先获知 Client 的需求。然后它把这个请求经过加工,发送到认证服务器等待回应(代码上看起来就是一次函数调用),一直等到认证服务器响应发回结果,才继续跑下面的逻辑。所以处理 Client 登陆请求这单条处理流程上,所有的一切都仅限于串行工作。当然,Agent 同时还可以相应 Client 别的一些请求。
如果用 callback 机制来表达这种处理逻辑,那就是在发起一个 RPC 调用后,不能做任何其它事情,后续流程严格在 callback 函数中写。
每个 RPC 调用看起来是这样的:
最近工作有点感触, 关于如何分工的。
我觉得所谓设计和实现是无论如何都很难分拆出去的。就是说你不实现你设想的结构,永远都很难知道哪里有问题;即使没有问题,换一个人来实现你想的东西,也无法把设计意图全部传达过去。如果可以做到,那么耗费的时间和精力足够你自己来实现了。
这也是为什么我之前说, 。但毕竟,一个人精力有限,项目时间也有限。分工是无奈之举。可这件事情怎样做才对呢?
我最近有所体会的还是那些被嚼过很多年的老道理。就是模块划分清晰,强内聚,低耦合之类。想强调的是,模块的层次一定要适中,同一层次上规模不能太大,有严格输入、输出接口。
这些并不是为了方便测试,检验工作正确性,而是为了拆分工作。
今天吃晚饭的时候想到,我需要一个定制的内存分配器。主要是为了解决
中的字符串池的管理。
这个内存分配器需要是非入侵式的,即不在要分配的内存块中写 cookie 。
而我的需求中,需要被管理的内存块都是很规则的,成 2 的整数次幂的长度。
刚好适用。
算法很简单,就是每次把一个正内存块对半切分,一直切到需要的大小分配出去。回收的时候,如果跟它配对的块也是未被使用的,就合并成一个大的块。标准算法下,分配和释放的时间复杂度都是 O(log N) ,N 不会特别大。算法的优点是碎片率很小。而且很容易做成非入侵式的,不用在被管理的内存上保存 cookie 。只需要额外开辟一个二叉树记录内存使用状态即可。
我吃完饭简单 google 了一下,没有立刻找到满足我要求的现成代码。心里估算了一下,C 代码量应该在 200 行以下,我大概可以在 1 小时内写完。所以就毫不犹豫的实现了一份。
然后,自然是开源了。有兴趣的同学可以。这样就省得到再需要时再造轮子了。嘿嘿。
开始这个话题前,离上篇开发笔记已经有一周多了。我是打算一直把开发笔记写下去的,而开发过程中一定不会一帆风顺,各种技术的抉择,放弃,都可能有反复。公开记录这个历程,即是对思路的持久化,又是一种自我督促。不轻易陷入到技术细节中而丢失了产品开发进度。而且有一天,当我们的项目完成了后,我可以对所有人说,看,我们的东西就是这样一步步做出来的。每个点滴都凝聚了叫得上名字的开发人员这么多个月的心血。
技术方案的争议在我们几个人内部是很激烈的。让自己的想法说服每个人是很困难的。有下面这个话题,是源于我们未来的服务器的数据流到底是怎样的。
我希望数据和逻辑可以分离,有物理上独立的点可以存取数据。并且有单独的 agent 实体为每个外部连接服务。这使得进程间通讯的代价变得很频繁。对于一个及时战斗的游戏,我们又希望对象实体之间的交互速度足够快。所以对于这个看似挺漂亮的方案,可能面临实现出来性能不达要求的结果。这也是争议的焦点之一。
我个人比较有信心解决高性能的进程间数据共享问题。 谈的其实也是这个问题,只是这次更进一步。
核心问题在于,每个 PC (玩家) 以及有可能的话也包括 NPC 相互在不同的实体中(我没有有进程,因为不想被理解成 OS 的进程),他们在互动时,逻辑代码会读写别的对象的数据。最终有一个实体来保有和维护一个对象的所有数据,它提供一个 RPC 接口来操控数据固然是必须的。因为整个虚拟世界会搭建在多台物理机上,所以 RPC 是唯一的途径。这里可以理解成,每个实体是一个数据库,保存了实体的所有数据,开放一个 RPC 接口让外部来读写内部的这些数据。
但是,在高频的热点数据交互时,无论怎么优化协议和实现,可能都很难把性能提升到需要的水平。至少很难达到让这些数据都在一个进程中处理的性能。
这样,除了 RPC 接口,我希望再提供一个更直接的 api 采用共享状态的方式来操控数据。如果我们认为两个实体的数据交互很频繁,就可以想办法把这两个实体的运行流程迁移到同一台物理机上,让同时处理这两个对象的进程可以同时用共享内存的方式读写两者的数据,性能可以做到理论上的上限。
ok, 这就涉及到了,如何让一块带结构的数据被多个进程共享访问的问题。结构化是其中的难点。
方案如下:
我一直不太满意
的默认设计。为每个 message type 生成一大坨 C++ 代码让我很难受。而且官方没有提供 C 版本, 也不让我满意。
这种设计很难让人做动态语言的 binding ,而大多数动态语言往往又没有强类型检查,采用生成代码的方式并没有特别的好处,反而有很大的性能损失(和通常做一个 bingding 库的方式比较)。比如官方的 Python 库,完全可以在运行时,根据协议,把那些函数生成出来,而不必用离线的工具生成代码。
去年的时候我曾经写过一个
。为了独立于官方版本,我甚至还用 lpeg 写了一个 .proto 文件的解析器。用了大约不到 100 行 lua 代码就可以解析出 .proto 文件内的协议内容。可以让 lua 库直接加载文本的协议描述文件。(这个东西这次帮了我大忙)
这次,我重新做项目,又碰到 protobuf 协议解析问题,想从头好好解决一下。上个月一开始,我想用 luajit 好好编写一个纯 lua 版。猜想,利用 luajit 和 ffi 可以达到不错的性能。但是做完以后,发现和 C++ 版本依然有差距 (大约只能达到 C++ 版本的 25% ~ 33% 左右的速度) ,比我去年写的 C + Lua binding 的方式要差。但是,去年写的那一份 C 代码和 Lua 代码结合太多。所以我萌生了重新写一份 C 实现的想法。
做到一半的时候,有网友指出,有个 googler 最近也在做类似的工作。 。这里他写了一大篇东西阐述为什么做这样一份东西,大体上和我的初衷一致。不过他的 api 设计的不太好,我觉得太难用。所以这个项目并不妨碍我完成我自己的这一份。
去年谈过 。我引入了一个 USING 方法来表达一个 C 语言编写的模块对其它模块的依赖关系。用它来正确的处理模块初始化。
现代语言为了可以接近玩乐高积木的那样直接组合现有的模块,都对模块化做了语言级别上的支持。我想这一点在软件工程界也是逐步认识到的。C 语言实在是太老了。而它的晚辈 Go 就提供了 import 和 package 两个新的关键字。这也是我最为认可的方式。之前提到的方案只能说是对其拙劣的模拟。确认语言级的支持,恐怕也只能做到这一步了。
在项目实践中,那个 USING 的方案我用了许多年,还算满意。之前有过更为复杂“精巧”的方法,都被淘汰掉了。为什么?因为每每引入新的概念,都增加了新成员的学习成本。因为几乎每个人都有 C 语言经验,但每个人的项目背景却不同。接受新东西是有成本的。任何不是语言层面上的“必须”,都有值得商榷的地方。总有细节遭到质疑。为什么不这样,或许会更好?这是每个程序员说出或埋在心里的问题。
那个 USING 的方案远不完美,它只是足够简洁,可以让程序员勉强接受而已。但其实还不够简洁。因为从逻辑表达上来说,它是多余的。一个模块使用了另一个模块,代码上已经是自明的。从 C 语言的惯例上来说,只要 #include 了一个相关的 .h 文件,就证明它需要使用关联的模块。光用宏的技巧很难只依靠一次 #include 就搞定正确的模块初始化次序。因为 C 语言并没有明显的模块概念。如果将每个子模块都编译为动态库可能能一定的解决问题(我曾经试过这种方案),但却会引出别的问题。细粒度的动态库局限性太大。
这两天我结合这半年学习 Go 语言的体验,又仔细考虑了一下这个问题。想到另一个解决方案。
Effective C++ 3rd 的评注版要出版了。我在这本书上花了不少心血。编辑约我最后写一篇序。我新码了点文字,用了点。
今天侠少同学说“现在全文看下来还是有些纠结,反对、支持、再反对,再支持,百转千回的小情绪,读者恐怕会犯晕”。嗯,的确很羞愧的。不应该在这本大牛的书前面发牢骚。打算晚上改稿子。旧稿就贴这里存档吧。
这几天认真玩起了 。所谓认真玩,就是拿 Go 写点程序,前后大约两千行吧。
据说 Go 的最佳开发平台是 Mac OS ,我没有。其次应该是 Linux 。Windows 版还没全部搞定,但是也可以用了。如果你用 google 搜索,很容易去到一个叫
的开源项目上。千万别上当,这是个废弃的项目。如果你用这个,很多库都没有,而且语法也是老的。我在 Windows 下甚至不能正确链接自己写的多个 package 。活跃的 Windows 版是
,对于 Windows 用户,装一个 mingw32 以后就可以开始玩了。
就三天来实战经历,我喜欢上这门新语言有如下原因:
mix-in 的接口风格。非常接近于。有语法上的支持要舒服多了。以平坦的方式编写函数,没有层次。而后用 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 的模仿。缺乏语言层面的支持,只能是一个拙劣的模仿。
今天终于把作业作完了(可能还有地方要返工),Effective C++ 第 3 版读完了,写了几万字的评论。如我给编辑交稿的 email 里所写:
我觉得评注这个工作比翻译难做。作者细节上讲的非常清楚,大部分地方都不觉得有必要再加注解。我想跟这本书反复写了 10 年有关。所以很多页我都没留评注,真的不知道可以写啥。
编辑原想每页中英分列排版,我是不建议这样的。除了少部分评注,针对个别代码段,或关键词。大部分我的文字都是独立成段的。跟具体原文句子关系不大,只跟篇章段落主题有些许联系。
前几天跟兄 email 交流,我发了一些稿子给他。他觉得关于本书第一篇 Item 1 : View C++ as a federation of languages ,我的评注还是没有讲透。是的,许多观点还是很难表达清楚。
下面选一段贴出来吧。
最近一个多月的业余时间都耗在了《》这本书上。读的很辛苦,不仅仅是因为这是本英文书。之前答应了博文的编辑帮这本书写评注,将来用于出版。对于要印成白纸黑字的文章,不得不谨慎一些。
我已经有 4 年没有大段时间写 C++ 代码了。中间偶尔有几天写过几千行,其余的 C++ 经验就来至于 google reader 上的阅读。为了读这本书,我又重温了《》的几个章节。不过整个阅读过程还是不太赏心悦目。
可能还是因为我对 C++ 偏见过多,有如前几年对其的推崇备至。总觉得书里讲的太细,太多观点本是好的,只是局限在了 C++ 语言中。明明是 C++ 的缺陷,却让人绞尽心力的回避那些问题,或是以 C++ 独特的方式回避。在别的语言中不该存在的问题,却成了 C++ 程序员必备的知识。人生苦短,何苦制造问题来解决之。
去年介绍过我在项目中实现的一个。
实际上,我为它提供的接口要更多一些,比如删除一个元素。
void array_erase(struct array *, seqi iter);
原来的语义就是删除 iter 引用的元素。但这里引出一个问题:删除后,iter 是否应该保持有效?
从语义上说,iter 应该在调用完毕后变成一个无效引用。但实际应用中,往往需要在迭代 array 的过程中,删除符合条件的元素。让迭代器失效的做法,用起来很不方便。
有时候,我们需要把多个模块粘合在一起。而这些模块的接口参数上有少许的不同。在 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 语言的短文。4000 字之内 。我刚列了个提纲就去了 三千多字。 -_-
现放在这里,接受大家的批评指正。勿转载。
今天晚上继续读 《Masterminds of Programming》,忍不住又翻译了半章关于 Forth 之父的访谈。我以前读过几篇更早时期关于他的访谈,部分了解他的观点。小时候还特别迷 Forth 。这位神叨叨的老头很有意思。
没看过原来的译本,只是自己按自己的理解翻了第 4 章 Forth 的前一半。我也算对 Forth 很有爱的人吧,也还了解 Forth 里诸如
ITC (Indirected-threaded code) 这种术语到底指的什么,不过还是觉得翻译有点吃力。
对 Forth 同样有爱的同学们姑且看之吧。
setjmp 是 C 语言解决 exception 的标准方案。我个人认为,setjmp/longjmp 这组 api 的名字没有取好,导致了许多误解。名字体现的是其行为:跳转,却没能反映其功能:exception 的抛出和捕获。
longjmp 从名字上看,叫做长距离跳转。实际上它能做的事情比名字上看起来的要少得多。跳转并非从静止状态的代码段的某个点跳转到另一个位置(类似在汇编层次的 jmp 指令做的那样),而是在运行态中向前跳转。C 语言的运行控制模型,是一个基于栈结构的指令执行序列。表示出来就是 call / return :调用一个函数,然后用 return 指令从一个函数返回。setjmp/longjmp 实际上是完成的另一种调用返回的模型。setjmp 相当于 call ,longjmp 则是 return 。
重要的区别在于:setjmp 不具备函数调用那样灵活的入口点定义;而 return 不具备 longjmp 那样可以灵活的选择返回点。其次,第一、setjmp 并不负责维护调用栈的数据结构,即,你不必保证运行过程中 setjmp 和 longjmp 层次上配对。如果需要这种层次,则需要程序员自己维护一个调用栈。这个调用栈往往是一个 jmp_buf 的序列;第二、它也不提供调用参数传递的功能,如果你需要,也得自己来实现。
我们设计任何一个模块,都应当对其实现细节尽可能的隐藏。只留下有限的入口和外部通讯。这些入口如何定义是重中之重。大多数情况下,我们都在模仿已有的系统来设计,所以对貌似理所当然的接口定义不以为然,以为天生就应该是那样,而把过多精力放在了如何做更好(更优化)的实现上。对接口设计方面缺乏深度的思考,使得在面对新领域时,或是随心所欲,或是不知所措。
即使是有成熟设计的模块,用户依然可能使用错误。模块的设计者不能要求模块的使用者对其内部实现了然于胸。不能指望自己能写出完善的文档去告诫使用者,当你怎样用的时候会用错。即使写出了完善的文档,也不能指望每个人都仔细的读过。就算读了,也有百密一疏的时候。越是专有的模块,越不能指望文档或是口述的教导,更不能指望程序员去精读源码。人生苦短,如无特别的理由,逐行去读同僚的代码,何不自己重写一遍?你设计的系统若用出了问题,与其怪别人用错,不如怪自己没设计好。MSDN 洋洋洒洒文字以 G 计算,也属无奈之举。依然有无数人犯下那些被人提及的错误。
现举一个一切 C 语言程序员都用过的模块的设计:内存管理模块。
标准 API 为三个:
大多数程序员都以为这理所当然。直到接触到 gc 的方式管理内存,方知另有一片天地。即使在 C 库中引为标准,也并不是所有内存管理器都承认这种简洁的。比如在 Windows 的 API 中,HeapAlloc 系列的内存管理模块的 API 就更复杂一些。
Windows 游戏软件在发布时,通常会把所有数据文件打包。这通常出于两个目的:一是保护数据文件不被最终用户直接查看,二是 Windows 的文件系统一度相对低效。尤其是在处理非常多小文件的时候,无论是安装、分发还是运行时处理都有性能问题。
而游戏软件通常会有大量的资源文件,对数据文件打包的需求更为强烈。一般游戏引擎都会支持至少一种资源打包的形式。
打包数据文件的概念和实现,我最早是对 Allegro 的源码阅读学习来的,算起来也是十多年前的故事了。之后一段时间,我从 Doom/Quake 里又看到了类似的东西。再之后,见过了星际争霸对数据包的处理。大体上,大家都支持一种用法:即可以让数据包和本地文件系统中的数据文件共存。非打包的数据在开发期用起来非常方便,打包的数据用于发行。
数据结构的序列化是个很有用的东西。这几天在修改原来的资源管理模块,碰到从前做的几个数据文件解析的子模块,改得很烦,就重新思考序列化的方案了。
Java 和 .Net 等,由于有完整的数据元信息,语言便提供了完善的序列化解决方案。C++ 对此在语言设计上有所缺陷,所以并没有特别好的,被所有人接受的方案。
现存的 C++ serialization 方案多类似于 MFC 在二十年前的做法。而后,boost 提供了一个看起来更完备的方案( boost.serialization )。所谓更完备,我指的是非侵入。
boost 的解决方案用起来感觉更现代,看起来更漂亮。给人一种“不需要修改已有的 C++ 代码,就能把本不支持 serialize 的类加上这个特性”的心理快感。换句话说,
就是这件事情我能做的,至于真正做的事情会碰到什么,那就不得而知了。
好吧,老实说,我不喜欢用大量苦力(或是高智慧的结晶?)堆积起来的代码。不管是别人出的力,还是我自己出的。另外,我希望有一个 C 的解决方案,而不是 C++ 的。
所以从昨天开始,就抽了点时间来搞定这件事。
当我还在用 C++ 做主要开发语言的最后几年,我已经不大用 protected 了。从箱底翻出曾经钟爱的一本书:《》,中文版 235 页这样记录:
“ ... Mark Linton 顺便到我的办公室来了一下,提出了一个使人印象深刻的请求,要求提供第三个控制层次,以便能支持斯坦福大学正在开发的 Interviews 库中所使用的风格。我们一起揣测,创造出单词 protected 以表示类里的一些成员,...”
“... Mark 是 Interviews 的主要设计师。他的有说服力的争辩是基于实际经验和来自真实代码的实例。...”
“...大约五年之后,Mark 在 Interviews 里禁止了 protected 数据成员,因为它们已经变成许多程序错误的根源...”
我不喜欢 protected ,但是今天,我偶尔用一下 C++ 时,不再有那么多洁癖。反正很难用 C++ 做出稳定的设计,那么,爱怎么用就怎么用吧。关键是别用 C++ 做特别核心的东西就成了。
今天,碰到一个跟 protected 有关的问题,小郁闷了一下。觉得可以写写。这个倒是个基本问题,貌似以前很熟悉。毕竟很多年不碰了,对 C++ 语法有点生疏。
面向对象编程不是银弹。大部分场合,我对面向对象的使用非常谨慎,能不用则不用。相关的讨论就不展开了。
但是,某些场合下,采用面向对象的确是比较好的方案。比如 UI 框架,又比如 3d 渲染引擎中的场景管理。C 语言对面向对象编程并没有原生支持,但没有原生支持并不等于不适合用 C 写面向对象程序。反而,我们对具体实现方式有更多的选择。
大部分用 C 写面向对象程序的程序员受 C++ 影响颇深。企图用宏模拟出一个常见 C++ 编译器已经实现的对象模型。于我愚见,这并不是一个好的方向。C++ 的对象模型,本质上是为了追求实现层的性能,并直接体现出来。就有如在 C++ 中被滥用的 inline ,的确有效,却破坏了分离原则。C++ 的继承是过紧的耦合。
我所理解的面向对象,是让不同的数据元有共同的操作方式,适合成组的处理。根据操作方式的不同,我们会对数据元做不同的分组。一个数据可能出现在这个组里,也可以出现在那个组里。这取决于你从不同的方面提取的共性。这些可供统一操作的共性称之为接口(Interface),接口在 C 语言中,表现为一组函数指针的集合。放在 C++ 中,即为虚表。
我所偏爱的面向对象实现方式(使用 C 语言)是这样的:
这几天白天都在安排面试,其实还是有点累的。晚上就随便写点程序,好久没摸 C++ ,有点生疏。也算是娱乐一下吧。
主要工作其实是在 C 库的基础上做一个 C++ 的中间层。跟在 C 库的基础上做 lua 中间层差不太多。前几天加入了 gc 后,发现了一些有趣的用法。
比如对于构造对象。 C 的 api 中,如果创建一个对象失败,就会返回空指针。但是对于 C++ 就不一样了,new 是不应返回空指针的。书本上的推荐做法是在构造函数里抛异常。但是我又不太想进一步的引入异常机智,怎么办呢?
为这篇 blog 打腹稿的时候,觉得自己很贱,居然玩弄 C++ 起来了。还用了 template 这种很现代、很有品味的东西。写完后一定要检讨。
起因是昨天写的那篇。里面用了虚继承和虚的析构函数。这会导致 ABI 不统一,就是这个原因,COM 就不用这些。
说起 COM ,我脑子里就浮现出各种条条框框。对用 COM 搭建起来的 Windows 这种巨无霸,那可真是高山仰止。套 dingdang 的 popo 签名:虽不能至,心向往之。
好吧,我琢磨了一下如何解决下面的问题,又不把虚继承啦,虚析构函数啦之类的暴露在接口中。
简单说,我有几个接口是一层层继承下来的,唤作 iA iB 。iA 是基类,iB 继承至 iA 。
然后,我写了一个 cA 类,实现了 iA 接口;接下来我希望再写一个 cB 类,实现 iB 接口。但是,iB 接口的基类 iA 部分,希望复用已经写好的 cA 类。我想这并不是一个过分的需求。正如当年手写 COM 组件时,我对手写那些 AddRef Release QueryInterface 深恶痛绝。
用虚继承可以简单的满足这个需求:
最近想把 engine 做一个简单 C++ 封装,结合 QT 使用。engine 本身是用纯 C 实现的,大部分应用基于 lua 开发。对对象生命期管理也依赖 lua 的 gc 系统。关于这部分的设计,可以参考我以前写的一篇
当我们把中间层搬到 C++ 中时,遇到的问题之一就是,C++ 没有原生的 gc 支持。我也曾经。但在特定应用下还不够简洁。这几天过年休息,仔细考虑了一下相关的需求,尝试实现了一个更简单的 gc 框架。不到 200 行代码吧,我直接列在这篇 blog 里。
这些尚是一些玩具代码,我花了一天时间来写。有许多考虑不周的地方,以及不完整的功能。但可以阐明一些基本思路。
我好多年没写 C++ 程序了,读 C++ 代码也是偶尔为之。
今天晚上就碰到这么一个诡异的问题,我觉得是我太久没摸 C++ 了,对那些奇怪的语法细则已经不那么熟悉了。有知道的同学给我解惑一下吧。
事情的起因是,我想安装一个 perl 模块唤作 Syntax::Highlight::Universal 。
本来用 CPAN 安装很方便的,直接 install 即可。
可是在我的机器上,make 死活通不过。我就仔细研究了一下编译出错信息。又读了一下源代码,自己感觉没错。纠结了半天,仔细模仿出错的地方写了一小段程序测试。
今天继续谈模块化的问题。这个想慢慢写成个系列,但是不一定连续写。基本是想起来了,就整理点思路出来。主要还是为以后集中整理做点铺垫。
我们都知道,层次分明的代码最容易维护。你可以轻易的换掉某个层次上的某个模块,而不用担心对整个系统造成很大的副作用。
层次不清的设计中,最糟糕的一种是模块循环依赖。即,分不清两个模块谁在上,谁在下。这个时候,最容易牵扯不清,其结果往往是把两者看做一体去维护算了。这里面还涉及一些初始化次序等繁杂的细节。
其次,就是越层的模块联系。当模块 A 是模块 B 的上层,而模块 B }

我要回帖

更多关于 go protobuf 使用 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信