K.O.(KeyFC Open Translation Toolset) 是目前正在开发的“开放版本”汉化工具的代号。
其中的汉化部分工具试验性的采用了代号为Chobits Application Server(CAS)的架构。
这个结构的特点为,程序全部由基于消息传递的模块动态组合而成。
上回谈了一下CAS的内部结构,今天谈谈CAS如何工作——CAS消息、传递和应用。
--- CAS消息 ---
前面讲到了,因为CAS消息传递是处于进程内的,因此可以将“消息”抽象为指针。
那么一个指针能传递什么样的信息呢?
我的回答是:“给我一个指针,我就能传递整个世界” (听起来有点耳熟 :D)
想想,在程序的世界里面,有什么是指针不能表达的呢?
当然,要做有用的事情,自然需要按照一定的方法去做,不然就和自我崩溃没有区别了...
CAS的消息同样需要遵循一定的格式,但是为了能够兼容各种不同的应用模块和编程习惯,又不能对消息的格式作过多的限制。
软件工程学帮助CAS实现了这一目的:可扩展格式而又不限制实现的结构——接口(Interface)
(其实,我已经不是第一次提到接口了。上回谈到将CAS队列管理和消息传递模块化的时候,就提到模块和消息总线之间的交互就是接口提供的。)
将一个CAS消息定义为一个指向“CAS基本消息接口"(BaseMSG Interface)的指针,这样总线传递的消息和消息的实例也就分离开来。
消息总线接受任何实现了这个“基本消息接口”的消息,因此一切疑难杂症也就迎刃而解。
“CAS基本消息接口"(BaseMSG Interface) 规定了一些传递消息必须的方法,比如:给出消息的接受方的编号,消息传递时的一些参数,以及一个可以自定义意义的整数(通常用来区分消息的种类)。
在编写应用模块的时候,编程者可以在“基本消息接口”的基础上任意定义自己想要传递的消息的接口,以及用任何方法实现这些接口,而不用担心与总线的兼容性问题。
作为参考,在CAS SDK(开发套件)中提供了一个基本的实现。通过继承这个对象,就不用自己实现“CAS基本消息接口"的功能了。
(图1) 注: 消息实例中蓝色的部分表示实现“基本消息接口"的方法。
--- 消息的传递 ---
发送一条消息的基本流程非常简单,同时也具有很强的可调控性:
0. 首先需要知道接受方的标识(消息接口编号)。
这很简单,只需要通过消息接口提交给总线对方的名称和GUID就行了。
1. 生成一个消息实例,填充好自定义的信息,然后填上接受方的标识,以及一些参数(下面详细解释)。
2. 调用消息接口的发送方法,完事。
根据选择的发送方法(上回里介绍过),这条消息可能会先存放在总线的某个消息队列,然后被转发到接受方的消息队列里;或者直接放入接受方的消息队列里。
编程人员不仅可以选择发送方法,而且可以通过消息的参数详细的控制消息传输的过程:
一般情况下,消息会一路顺畅的到达接受方的队列,不论什么参数都一样,没什么有趣的事情发生。
但是当总线发生拥堵时,配置的参数就会产生效果了:
* 默认的配置下,当消息在一个队列阻塞一段(预设定的)时间还不能被送达,这个消息将被队列抛弃;
(如果模块选择亲自送达,那么将会得知这一事件;如果是总线转交,当总线队列阻塞,发送方会得知,但如果接受方队列阻塞,消息则会被"安静"的抛弃)
* 如果消息的适时性很高,发送方可以配置消息完全不等待,一旦阻塞,立刻抛弃。(处理结果的通知情况同前)
* 如果消息是具有时间性的,超过一定时间就没有意义了(比如音/视频帧),发送方可以为消息增加一个(高精度)作废时间戳。
发送的过程会一直尝试投递消息直到截止时间之后,如果消息还未送达就会被抛弃;同时如果截止时间前消息没有被接受方提取,此消息也会被抛弃。
(这种配置的消息不能交给总线转发,只能亲自送达,因为过长的等待时间将会影响转发效率。如果消息在发送过程里被抛弃,发送方会得知;如果在提取的时候被抛弃,那么是"安静"的)
* 如果消息无论如何都要被送达,发送方也可以选择相应配置,这样在消息送进对方的队列之前,传送的过程不能返回(除非异常发生: 比如接受方的队列被释放)。
(同前一种配置一样,这种配置的消息不能交给总线转发,只能亲自送达;如果异常发生,消息被抛弃,发送方会得知)
另外,消息的接口化带来了另外一个(可以算)有用的特性,就是消息是被引用计数(Reference Counted)的,因此,发送的消息将一直存在到其没有被引用为止。
当消息经过接口进入总线内部后,总线就"获取"一份此消息的引用;当消息被提取时这一份引用也就传递到了接受方;
当消息被总线"抛弃"时,总线的操作仅仅是将其引用计数减去一份;同理,接受模块处理完消息之后,也仅仅需要减去一份引用。
这也就意味着,如果发送方在发送消息之前就保留了一份或者更多的引用的话,消息将会一直存在,而且发送方也一直可以访问(不过需要注意同步问题)。
这带来了一些值得注意的应用方法:
1. 发送方如果希望在投递失败的时候对企图发送的消息进行其它处理,则应该预先保留一份引用;否则等发送失败的结果返回的时候,消息已经被自动释放(不过有时这种处理方法更方便,根据个人的编程风格和应用目的而定);
2. 通过保留引用,发送模块也可以达到与接受模块的线程直接交互的效果。一些问题通过纯粹消息传递模式实现效率低下,这种方法则可以越过CAS消息传递,回到多线程交替操作的经典模式(当然,就需要自己解决同步问题了);
3. 通过保留引用,还可以高效的实现"流水线"处理。例如,加密一块数据时,一个挂接了数据的消息可以依次被各个模块处理,其间不用频繁的被释放、复制(当然,需要统一每个模块接受的消息的接口)。
--- 消息的应用 ---
基于接口的CAS消息具有开放、灵活的特征,因此,可以被方便的扩展,赋予多种多样的应用。
仅支持"基本消息接口"的消息只能达到有限的目的,但是CAS 开发套件中提供了丰富的扩展消息接口和参考实现:
1. 可重定向消息。
这个是很基本的扩展。默认的消息接口仅对传递的信息提供"读取"的方法。
这对于防止编程者误操作(意外的向接受到的消息里面写一些东西,然后又忘记更改了消息,导致接下来处理过程出错)有用,但也意味着消息是单向的,处理后只能抛弃。
但是有一些的时候消息需要被回复。因此可重定向消息提供了更改消息接受方、参数、标志数的方法。
2. 跨模块调用(Inter-Module Call, IMC)消息。
通过使用这个消息,可以实现类似于"远过程调用"(RPC)的功能,只不过RPC是跨进程的,IMC只是跨线程的。
具体实现很简单,将一个"事件"(Event)和一系列辅助数据(如返回值)、方法(如"等待"、"通知")捆绑到一个基本消息上。
消息发送方发送消息,然后调用"等待"方法;接受方收到消息并处理完成后,调用"通知"方法;这时发送方将会从"等待"方法返回,并获得一个返回值,就像刚调用了一个函数一样。
IMC消息受到总线的支持,当消息不能送达,被抛弃的时候,等待中的发送方将会返回并获得一个错误返回值。(目前这种支持是内部特定的,但是我会通过事件抽象化将其改写为通用的)
事实上,除了直接继承,接口还提供了另一种很实用的扩展方法——代理(delegation)。
CAS 开发套件中,IMC的实现是独立于消息的;因此任何消息实例,包括自定义的,都可以通过代理的方法将"绑定"IMC接口,成为一个支持IMC操作的消息,仅需要写几行代码。
3. 可复制消息
这种消息接口定义了一个“复制”方法,只要调用,就可获得一份"几乎"一样的复制品(每份消息都有一个"复制编号"表明其是第几份复制品)。
这种消息是为了实现消息“广播”(Boradcast)和“组播”(Unicast)而定制的。(目前尚未实现)
在我的设计中,“广播”和“组播”不是总线原生功能,而是以服务模块的方式实现的(遵循最大模块化宗旨 :P):
该模块中可动态定义多个"组",每个组接受各个模块的"订阅";发送给一个组的消息(当然,必须提供"可复制消息"接口),订阅的所有模块都会收到该消息的副本。
4. 可序列化消息
这种消息接口定义了"序列化"和"反序列化"方法。
顾名思义,序列化方法将一个消息的实例转换为一串数据,可以被记录在任何媒体上;反序列化则将数据实例化。
这种消息为将来CAS消息总线的跨进程、跨平台通讯奠定了基础。
(当然,这个功能的实现应该是另外N年以后的事情了。)
(不过想象一下,向远程机器的一个CAS模块发送一个请求,就像同本地模块通讯一样,是一件多么美好的事情啊... =v=)
这些仅仅是抛砖引玉了,在实际应用中相信有更多种类复杂的功能要求;而且我相信CAS的开放、灵活的消息接口应该能够胜任这些要求。
------
当然任何东西都不可能完美,修订和改错是必定的。
当CAS总线 / 消息接口被更改后,有可能会和旧的模块不再兼容,需要它们被重新编译(或者,更坏的情况下,修改)。
这就涉及到下一次将要谈的问题: CAS模块的结构、封装和应用程序的组装。