limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web...

228
Limax 项目 用户手册 1

Transcript of limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web...

Page 1: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

Limax 项目用户手册

1

Page 2: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

目录Limax 项目........................................................................................................................................1Limax Project 是什么........................................................................................................................4Limax 最大的特点............................................................................................................................4View 基础.........................................................................................................................................5设计目标..................................................................................................................................5基本概念..................................................................................................................................5实现中涉及的概念..................................................................................................................6

体系结构..........................................................................................................................................7服务器框架..............................................................................................................................7客户端框架..............................................................................................................................8

应用开发..........................................................................................................................................9基本概念..................................................................................................................................9模型创建................................................................................................................................10类型映射................................................................................................................................18服务器开发............................................................................................................................19客户端开发............................................................................................................................44高级特性................................................................................................................................82极简模式客户端....................................................................................................................86Provider间数据交换..............................................................................................................89Limax Http...............................................................................................................................96

运行管理......................................................................................................................................101配置......................................................................................................................................101部署......................................................................................................................................109运行状态监视......................................................................................................................110维护......................................................................................................................................115

附录..............................................................................................................................................122支付框架..............................................................................................................................122账号系统..............................................................................................................................125应用配置..............................................................................................................................129JSON 支持.............................................................................................................................133CLR/Lua.................................................................................................................................142CLR/Javascript(SpiderMonkey).............................................................................................149脚本语言访问 C#规范.........................................................................................................159Node.js兼容框架.................................................................................................................164PKIX支持..............................................................................................................................182Key分发系统........................................................................................................................193外部数据..............................................................................................................................200ProviderLogin........................................................................................................................201

2

Page 3: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

LimaxKey...............................................................................................................................204动态 XBean...........................................................................................................................208QR Code................................................................................................................................212JDBC连接池.........................................................................................................................216

3

Page 4: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

Limax Project 是什么

一个运营体系一个应用服务器框架一个数据同步框架

Limax 最大的特点

突破传统协议方式的束缚,使用 View这样一个概念,应用服务器处理请求逻辑生成表现数据;客户端根据数据进行表现,大大降低逻辑处理复杂性。协议模式的应用设计——服务器处理逻辑功能,产生输出数据,根据功能定义协议,

根据协议打包数据,数据发送到客户端,客户端解码协议,根据解码数据处理后续逻辑或者直接表现数据。服务器打包代码,通讯代码,客户端解码代码都可以根据协议描述自动生成,这看起

来已经解决了很多问题。不足在哪里呢?协议本身这个概念没有任何问题,ISO还下个 7层的定义,网络上各种协议满天飞。正

确设计协议才是最大的麻烦,往往是个权衡利弊不断妥协的结果。设计上最大的麻烦有两个:其一,粒度;其二,时序。粒度——实现一个复杂应用,服务器可能有各种各样的数据需要传送,设计多少协议

合适?少了,某些协议就带有冗余数据,客户端不得不根据具体上下文决定哪些数据是需要的,哪些是冗余,这就成了接收方必须处理的逻辑功能之一,这些逻辑功能与具体项目功能无甚关系,完全是额外劳动;设计多了,几十上百条协议如何维护?协议重用吧,数量看似可以减小,牵一发动全身的问题出来了,维护可能更难。时序——实现某一功能,可能需要发送多条协议。服务器先发谁,后发谁,客户端先

处理谁,后处理谁,都是需要仔细权衡的问题。为了正确处理顺序,在服务器逻辑功能的4

Page 5: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

设计上有两种考虑。其一,处理过程中该发就发,问题在于,如果需要实现的是一个事务逻辑,也许后续操作导致了回滚,前面的发送显然就不应该了。其二,逻辑功能处理完成以后发送,那么就必须根据结果数据,组织一系列协议的发送,这种组织静态的还好,一旦是个动态的,处理起来就非常麻烦。服务器实现既然那么麻烦,干脆把一个功能的数据打包到一条协议好了,这下好了,同一协议的字段按何种顺序解释变成了客户端需要解决的问题,逻辑处理被推给了客户端;更进一步,服务器要求先关灯,后开灯,打包到一条协议,客户端糊涂了,到底解释成,关然后开,或者开然后关?天哪,这个责任推还推不干净。粒度,时序,两个问题综合到一起考虑——按简化时序设计,协议粒度变粗,冗余出

现了,客户端必须识别冗余,解释时序;按精细粒度设计,协议维护代价大大增加,服务器时序设计变得更加困难——原来,这是一对矛盾。协议模式搞设计,这对矛盾不可解决,只能调和。客户端,服务器都很难避免与最终

需求无关的逻辑处理,处理起来不但成本高昂,而且往往最容易出现 bug。调和出来的设计,在需求发生变动的时候,非常难以维护,从一种调和方式转向另一种调和方式,也许就是伤筋动骨——最终需求无关的逻辑处理都必须挖掘出来,仔细调整,而且这些逻辑处理往往就在某一个程序员的脑子里——项目很容易被绑架。

拒绝协议模式的应用设计也就避免了调和带来的不确定性,Limax通过 View这一概念,解释了数据意义上的MVC,回避了粒度问题——哪些数据改变了就发送哪些;明确了时序问题——客户端完全重现服务器对 View的修改顺序。

View 基础

设计目标完全消除为了维护协议体系添加的结构性逻辑代码——上文提及的那些与最终需求无

关的,难以维护的逻辑代码——实现的精力集中到最终需求上来。服务器的精力集中到功能性数据上,客户端的精力集中到数据表现上。

基本概念View,被命名的一个结构描述,名字的定义应该反映具体功能。这个数据结构包含了

5

Page 6: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

一系列的字段,服务器 View与客户端 View一一对应,服务器 View的字段发生改变同步到客 户 端 对 应 View 的 对 应 字 段 。 这 里 的 改 变 有 4 种 类 型 ,NEW,REPLACE,TOUCH,DELETE。NEW——服务器 View 初始化了这个字段,字段数据从无到有;REPLACE——服务器设置了这个字段,字段数据不同于前一个;TOUCH——服务器设置了这个字段,但是字段数据没有变化;DELETE——服务器删除了这个字段,字段数据从有到无。更进一步,以 View为单位的时序是严格保证的,也就是说,客户端可以重现服务器设置同一 View上的字段的顺序;不同 View之间的时序不作保证。

Control,定义在 View 名字空间下,客户端给服务器发送的控制命令。View,Control两个概念合到一起看,就是客户端给服务器发送控制命令,服务器设置

View的状态,这个状态被反映到客户端。当然了,Control与 View状态改变的因果关系不是强制的,即便没有 Control,服务器也可以改变 View 从而实现推送;Control 尽管定义在View 名字空间下,也不意味着这个 Control的实现只能改变当前这个 View。

实现中涉及的概念SessionId,客户端登录服务器,相应的会话建立起来的时候系统给客户端分配的 ID,

作为客户端的唯一标识。三种 View的类型,GlobalView,SessionView,TemporaryView。GlobalView,具有服务器生命周期的 View。View的改变不能自动同步到客户端,必须

手工调用同步方法,把整个 View或者 View的某个字段的最新版本数据同步到一个或者多个客户端。

SessionView,具有会话生命周期的 View。客户端登录服务器建立会话时自动创建,依据 SessionId分类。View的字段发生变化时,字段数据自动同步到对应客户端。

TemporaryView,具有临时生命周期的 View,服务器应用执行过程中手工创建,手工删 除。临时 View 是一种最灵 活的,使用上也最复杂的 View。通过 Membership 维护SessionId 集合实现成员管理,提供广播特性,也就是说 View 字段发生变化时,字段数据自动广 播 给 所有 Membership 成员。临时 View 还拥有更强大的能力—— 订 阅成员的SessionView 字段,某一成员的被订阅 SessionView 字段发生改变,这一改变自动广播给所有Membership成员。

ViewContext,View上下文,服务器,客户端,拥有各自的 ViewContext,具有服务器或者客户端生命周期,通过它可以访问 View,多数情况下被 Limax生成代码使用。

ViewChangedListener,客户端的关键应用接口,接收服务器同步过来的 View 字段改变事件,主要提供 View 改变的 5个信息,哪一个 View,哪一个字段,哪一个 SessionId,字段

6

Page 7: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

当前值,此次改变的类型,NEW,REPLACE,TOUCH,DELETE。Control,客户端发送控制命令给服务器,有两种类型,Control,Message

Control,定义在 View 名字空间下,一个名字空间下可以定义多个。类型化客户端框架支持该类型 Control。

Message,系统实现中用Message描述,客户端可以通过 Message 机制向服务器发送字符串消息,每个 View 名字空间下隐含有一个。类型化客户端,脚本客户端都支持。实现一个应用,需要同时支持类型化客户端和脚本化客户端,只能使用Message。

体系结构

服务器框架

基本概念Environment,整个框架实现为一个运营体系Application,一个运营体系里可以运行多个应用Provider,一个应用可以由多个 Provider 组成PVID,一个 Provider 由一个 PVID 唯一决定,整个 Environment可以分配 2^24个 PVID

服务器组件Switcher,接受客户端连接,其它服务器组件的汇集点,根据 PVID完成信息交换,一

个运营体系里可以部署多个 Switcher,典型的情况下,一台服务器运行一个。GlobalId,全局 Id 分配服务,一个应用可能需要运行在多台服务器上,可以使用

GlobalId服务提供全局 Id;同一应用隔离运行的情况下,为了未来的合并方便,也可以使用 GlobalId服务提供全局 Id。根据具体需求在运营体系下部署。

7

Page 8: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

Auany,统一认证服务,可以提供运营体系自己的认证服务,也可以通过插件方式支持其它运营体系的第 三方认 证服务。认 证成功后,给客户端提 供 当 前运营体系内的SessionId,标识该用户。一个运营体系下可以部署一个或者多个运营体系共用一个。Auany同时提供支付框架的支持,可以通过插件方式扩展第三方支付系统。

Provider,提 供具体应用的服务,应用开发的主体,开发应用就是开发应用的Provider,严格意义上讲,上文描述 View时涉及到的服务器特指 Provider,没有特别说明,后文的服务器开发特指 Provider开发。服务器开发接口服务器开发纯 java实现,关键的两个包,provider,zdb

limax.provider,提供了 View 相关的基础类,主要由生成的框架代码引用,应用开发应该在生成的代码基础上完成;支持对 GlobalId服务的访问。

limax.zdb,数据库支持,使用键值对的方式访问的事务化内存数据库,支持 ACI 除了 D。底层通过配置,使用 limax自带的 edb 键值对数据库,或者mysql。存储过程是事务化的,支持 level2,level3两种事务隔离度,是 zdb的关键,实现一个应用的大部分代码都可以在存储过程中实现,保证事务完整性。

Zdb 对 View的扩充,可以在 View描述中使用 bind特性,将一张表上一个特定 key 对应的 value,或者 value中的某些字段 bind到 View上,事务成功提交后,发现相应 value或者value中相应字段在数据库操作时发生改变,value或者 value中相应字段的数据作为字段数据自动设置到 View上——如果是 SessionView或者 TemporaryView这些数据也就自动同步到客户端。View与表的 bind实现为多对多关系。特别要注意的是,同一 View上 bind 字段的改变时序不作假设,这些改变具有事务原子性。服务器管理

框架提供的基本服务都可以通过 JMX 控制,并且提供一系列运行状态数据。应用可以定义自己的数据监视集合,在运行阶段执行数据采集。

客户端框架

基本概念类型客户端,JAVA,C#,C++实现的客户端

8

Page 9: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

脚本客户端,通过嵌入 LUA,Javascript 等脚本语言实现的客户端Endpoint,端点,类型化客户端框架总入口EndpointConfig,端点配置,提供登陆服务器所需要的用户名,密码,请求的认证平台

请求的 PVID 集合等信息。EndpointListener,基础网络事件接收接口,报告连接进度,链路测试结果,发生的网

络错误等信息。EndpointManager,端点管理器,EndpointConfig与 EndpointListener,共同决定一个

EndpointManager。EndpointManager代表了一次网络会话,一个客户端应用允许存在多个,通过 SessionId 区分。EndpointManager 将所有的 View 汇集到以 PVID分类的 ViewContext中。

Java客户端开发接口limax.endpoint,提供 Endpoint基础支持limax.endpoint.script,提供嵌入脚本语言的接口,鉴于 OracleJDK 内嵌了 javascript脚本

引擎,示例性实现了 javascript脚本支持limax.endpoint.variant,动态类型化的 View访问支持,这种模式下开发的客户端无需生

成静态的 View代码。C#客户端开发接口与 Java 版本基本一致,如果要求实现脚本支持,需要按照脚本语言接口规范,集成需

要的脚本引擎。C++客户端开发接口

除了没有实现嵌入脚本语言的接口,与 Java 版一致提供了可直接嵌入 Lua的版本

脚本开发接口limax.lua,Lua 版本,完整的 View支持,所有的 View数据按照 View定义的名字空间,

完全映射到脚本名字空间,只需要按照规范定义各个 view的 onXXX方法接收 View 改变消9

Page 10: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

息,驱动客户端表现limax.js,Javascript 版本,实现上与 limax.lua完全一致

HTML5开发接口直接使用 limax.js,limax.js已经内建了WebSocket访问

应用开发

基本概念Limax 项目的开发遵循如下步骤:通过 xml文件描述应用模型,包括数据库表的描述,View的描述,服务描述等等通过 xmlgen生成需要的服务器,客户端框架代码,模板代码,运行配置模板等等编辑生成的代码加入应用逻辑

模型创建

Xml 基本节点project

应用的根namespace,state,service,zdb 节点均可以定义在 project 节点下,命名了名字空间的

根<project name="limax" xmlns:xi="http://www.w3.org/2001/XInclude"></project>

10

Page 11: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

定义项目 limax, 启用 XInclude支持state

定义在 project 节点下,被 manager 节点引用网络服务器可以运行在多个状态下,state 节点定义了该状态下网络服务器允许接受的

protocol,rpc——尽管强烈推荐使用 View,但是 protocol,rpc 也可以支持,毕竟 View本身在系统内部也是通过 protocol实现

<state name="XXX"><namespace ref="a" /><protocol ref="b.a" /><rpc ref=”b.c”/>

</state>定义状态 XXX,引入了 a 名字空间下的所有 protocol,rpc,b 名字空间下的 a协议,b

名字空间下的 c协议。名字空间允许嵌套,为了避免混淆,使用 state 引用名字空间时,只允许引用最外层。

namespace

定义在 project,namespace 节点下,被 state 节点引用namespace, bean, protocol, rpc, view 均可以定义在 namespace下<namespace name="gs" pvid="12"></namespace>定义了名字空间名 gs,设定该名字空间的 pvid为 12。名字空间嵌套的情况下,最外层

pvid有效,内层 pvid 被忽略。bean

定义在 project,namespace下,作为结构化类型被用来定义变量enum,variable可定义在 bean下<bean name="b1">

<enum name="e0" value="1" /><variable name="v0" type="int" /><variable name="v1" type="binary" />

</bean><bean name="b2">

<variable name="a0" type="b1"><variable name="a1" type="string"/><variable name="a2" type="list" value="float"/>

11

Page 12: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

<variable name="a3" type="vector" value="b1"/><variable name="a4" type="set" value="b1"/><variable name="a5" type="map" key="int" value="b1"/>

</bean>在这里,bean b2 引用了 b1,具体的类型映射后文描述。

protocol

定义在 namespace下,被 state 引用protocol下可定义 enum,variable,与 bean类似<protocol name="CHandShake" type="101" maxsize="1030">

<variable name="dh_group" type="byte" /><variable name="dh_data" type="binary" />

</protocol>定义了 101号协议 CHandShake,该协议允许的最大尺寸为 1030。一个 pvid——最外层

名字空间决定——下的协议编号不可重复,最大尺寸定义为 0表示不限制,实际运行环境中服务器存在一个约束所有协议尺寸的硬限制,可以通过虚拟机运行参数调整, java -D limax.net.Engine.limitProtocolSize=xxx …,默认为 1M。rpc

定义在 namespace下,被 state 引用<bean name="CheckProviderKeyArg">

<variable name="pvid" type="int" /><variable name="pvkey" type="string" />

</bean><bean name="CheckProviderKeyRes">

<variable name="errorcodes" type="int" /></bean><rpc name="CheckProviderKey" type="305" argument="CheckProviderKeyArg"

result="CheckProviderKeyRes" maxsize="1024" timeout="10000" />定义了 305号 rpc, CheckProviderKey,允许最大尺寸 1024,10000毫秒超时,rpc 参数

和结果必须是 bean。rpc与 protocol在同一空间下编号,互相不可重复,最大尺寸单独限制参数和结果,应该取两者的最大值。特别注意,rpc仅在 java 环境下支持,提供给 limax框架自身使用, java之外的客户端

版本均不提供支持,不建议使用。view

定义在 namespace下,被 state 引用。不包含 ref 节点的 bind 节点,不可引用包含 any类型 xbean的 table;包含 ref 节点的

12

Page 13: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

bind 节点,ref 指向的 xbean 字段不能包含 any类型。<view name="globalview" lifecycle="global">

<variable name="var4" type="list" value="MyBeanAll" /><bind name="bindfirst" table="first">

<ref name="s" /><ref name="sets" />

</bind><bind name="bindsecond" table="second" /><control name="control1">

<variable name="var1" type="int" /><variable name="var2" type="MyBean" />

</control></view>定义了全局 globalview,包含字段 var4,bindfirst,bindsecond,控制 control1。var4为

一般字段,通过在 view 对象上 set来改变;bindfirst为 bind 字段,first表的 value结构中s,sets两字段任何一个发生变动,bindfirst 被设置成 value.s,value.sets 组织起来的结构;bindsecond为 bind 字段,second表的 value发生变动,bindsecond 被设置为 value;contol1为 control,参数为 var1,var2 组织起来的结构。

<namespace name="gs" pvid="12"><namespace name="for_session">

<view name="firstview" lifecycle="session"><variable name="var1" type="int" /><bind name="bindsecond" table="second" />

</view></namespace><namespace name="for_temp">

<view name="TestTempView" lifecycle="temporary"><variable name="var1" type="int" /><subscribe name="_var1" ref="gs.for_session.firstview.var1" /><subscribe name="_bindsecond" ref="gs.for_session.firstview.bindsecond" /></view>

</namespace></namespace>在这里,临时 View TestTempView的字段 var1发生变动,所有 TestTempView成员都将

收到这一变动。 TestTempView 的订 阅 字 段 _var1, _bindsecond 分别 订 阅了会 话 View firstview的 var1,bindsecond 字段,TestTempView成员中,某一成员的 firstview的 var1或者 bindsecond 字段发生变动,该变动通过_var1或者_bindsecond 被广播给所有成员。实际上_var1,_bindsecond 被作为 map实现,key为 SessionId。特别要注意的是,成员用户的firstview.var1 发 生 变 动 , 该 用 户 将 收 到 两 个 变 动 , firstview.var1 和TestTempView._var1[SessionId]。

13

Page 14: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

service

定义在 project下,描述了该项目支持的服务,一个项目下允许多个<service name="switcher">

<manager name="SwitcherServer" type="server" initstate="EndpointKeyExchange"port="10000"><state ref="EndpointSessionLogin" /><state ref="EndpointClient" />

</manager><manager name="ProviderServer" type="server" initstate="ForProvider"

port="10100" /><manager name="AuanyClient" type="client" initstate="AuanyClient"

port="10200" /></service>这是 limax Switcher服务器组件的定义。一个服务器由一系列网络服务构成,manager定义了网络服务。SwitcherServer 作为网

络服务器运行在 10000端口上,接受客户端的连接请求;ProviderServer 作为网络服务器运行在 10100端口上,接受 Provider的连接请求;AuanyClient 作为 Auany服务器组件的客户端,连接 Auany的端口 10200。一个网络服务可以运行在一个或多个状态下,通过 state 引用了该状态下允许的

protocol,rpc,view,初始状态由属性 initstate 指定,其他状态定义在manager 节点内。<state name="GsProvider">

<namespace ref="gs" /></state><service name="demogsd" useGlobalId="true" useZdb="true">

<manager name="Provider" type="provider" initstate="GsProvider"port="10100" /></service>这里定义了服务 demogsd,注意 manager下的 type为 provider,意味着生成 provider

需要的代码,前面提到实现 limax应用一般就是实现 provider,这是最常用的。事实上provider是 Switcher服务器组件的客户端,所以 port 10100决定了该 provider连接 Switcher服务器的 10100端口。同时 provider又是应用客户端的服务器,应用客户端与 provider之间通过 Switcher 转发 protocol,rpc,view数据。

useGlobalId, 在服务配置文件中生成使用 GlobalId服务组件需要的配置。useZdb,在服务配置文件中生成使用 Zdb的启动配置模板。特别要注意,provider模式下的 manager仅支持一个状态,定义多个没有意义,并且

该状态只能引用一个 namespace,namespace的 PVID 被作为该 provider的 PVID。zdb

描述了应用使用的数据库的全部信息,一个应用只允许使用一个 zdb数据库,project下最多定义一个。

14

Page 15: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

cbean

定义在 project,namespace,zdb下。结构化类型作为表的 key,map的 key,set的 value时,必须定义为 cbean,与 bean的

定义类似,字段成员的类型可以是除 binary 和 any以外的基本类型以及 cbean类型,不能使用容器类型。特别的,cbean可以被 bean,protocol,rpc,view,control 引用。<cbean name="xcompare">

<variable name="b" type="boolean" /><variable name="s" type="short" /><variable name="i" type="int" /><variable name="l" type="long" /><variable name="text" type="string" />

</cbean><cbean name="xcompare2">

<enum name="eX" value="1" /><variable name="xc1" type="xcompare" />

</cbean>

xbean

定义在 zdb下结构化类型作为表的 value时,必须定义为 xbean,与 bean的定义类似,字段成员类

型可以是各种基本类型,容器类型,如果需要嵌套结构化类型可以使用 cbean或者 xbean,既然 cbean是常量 bean,意味着该嵌套类型的对象没有修改的机会。xbean支持一种特殊类型 any,any类型不可持久化,使用了 any类型的 xbean 作为表的 value时,该表只能是内存表。特别的,不包含 any类型的 xbean可以被 bean,protocol,rpc,view,view,control通

过 variable 节点引用。bind不能完整引用值为包含 any类型的 xbean的 table,但是 bind部分引用非 any成员是允许的。

<xbean name="BasePlayerInfo"><variable name="name" type="string" /><variable name="level" type="int" /><variable name="sid" type="long" />

</xbean>

<xbean name="WaitingPlayerInfo"><variable name="baseinfo" type="BasePlayerInfo" /><variable name="ready" type="boolean" />

</xbean>

<xbean name="ObservePlayerInfo"><variable name="baseinfo" type="BasePlayerInfo" /><variable name="obpos" type="int" />

15

Page 16: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

</xbean>

<xbean name="WaitingTableInfo"><enum name="POS_EAST" value="0" /><enum name="POS_SOUTH" value="1" /><enum name="POS_WEST" value="2" /><enum name="POS_NORTH" value="3" /><enum name="POS_COUNT" value="4" /><enum name="POS_OBSERVE" value="4" /><variable name="tableid" type="int" /><variable name="players" type="vector" value="WaitingPlayerInfo" /><variable name="observe" type="vector" value="ObservePlayerInfo" /><variable name="playing" type="boolean" />

</xbean>

<xbean name="RoomInfo"><variable name="name" type="string" capacity="32" /><variable name="hallid" type="long" /><variable name="players" type="set" value="long" /><variable name="tables" type="vector" value="WaitingTableInfo" />

</xbean>这里定义了一个比较复杂的 xbean结构。<xbean name="Any">

<variable name="any" type="any:Object" capacity="32" /><variable name="anyset" type="set" value="any:Object"/><variable name="boolv" type="boolean" />

</xbean>这里定义了包含 any类型的 xbean,Object是 java.lang.Object,可以引用任何对象。any

冒号之后可以引用所有合法的 java 类型以及描述中出现的普通 bean, view,因为bean,view本身在生成代码以后也会生成对应的 java类。

<view name=”Info” lifecycle=”session”><variable name=”info” type=”BasePlayerInfo” /><variable name=”testany” type=”Any” />

</view>这个 view的变量字段引用了 xbean,字段 testany是 any类型的,是错误的,代码生成

过程中会抛出异常指出 Info.testany 存在 any类型依赖。any 依赖是递归检测的。<table name=”anytable” key=”int” value=”testany” persistence="MEMORY"/><view name=”Info” lifecycle=”session”>

<variable name=”info” type=”BasePlayerInfo” /><bind name=”bindtestany” table=”anytable”>

16

Page 17: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

<ref name=”boolv”/></bind>

</view>这里的 bind 绑定了 anytable,anytable的值类型是 xbean Any,尽管这个 xbean包含了

Any类型,但是这里只引用了 boolean类型的字段,所以上面的描述是正确的。table

定义在 zdb下zdb数据库中表的描述<table autoIncrement="true" cacheCapacity="4096" key="long" name="roominfos"persistence="MEMORY" value="RoomInfo" />roominfos表,key类型为 long,value类型为前面定义的 xbean RoomInfo,long类型的

key在配置了 autoIncrement="true"的情况下支持自动增量,persistence="MEMORY"决定了这张表是内存表,cacheCapacity="4096"决定了这张表在内存中最多 cache 4096条记录,对于内存表而言,记录数一旦超过 cacheCapacity,某些记录将丢失。

<table name="keyisxcompare2" key="xcompare2" value="string" cacheCapacity="4096" />keyiscompare2表,key为前面定义的 cbean xcompare2,value为字符串,没有指明

persistence,这是张磁盘表。<table name="tany" key="int" value="Any" cacheCapacity="4096"persistence="MEMORY" />tany表,value的类型是前面的定义的 xbean Any,xbean包含了 any类型字段,该表只

能是内存表。<table autoIncrement="true" cacheCapacity="4096" key="long" name="testtype"

value="TestType" lock="abc" /><table name="secondaryindex" key="long" value="SecondaryIndex" cacheCapacity="2000"

lock="abc" />这里多了 lock这一属性,zdb的锁都是行锁,基本锁定方式是 lock(table, key),具有相

同 lock属性的表共用锁。在这里意味着锁定了表 testtype 的 key行,同时也就锁定了secondaryindex表的 key行,反之亦然。同时锁定 testtype与 secondaryindex表的 key行也不存在问题。适当规划 lock可以简化锁。monitorset

定义在 project,namespace下,为应用提供相关的运行数据监视能力。<monitorset name="TransactionMonitor" supportTransaction="false">

<monitor name="runned" type="counter" /><monitor name="false" type="counter" /><monitor name="exception" type="counter" /><key name="procedureName" type="string" />

</monitorset>

17

Page 18: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

这是 limax.xml中的 zdb 事务监视器集合定义,key为存储过程名,分别为存储过程定义了 3个计数信息,执行次数,执行返回 false的次数,执行抛出异常的次数。其中:

supportTransaction 指出了这一监视器集合需不需要事务特性,简单的说如果为 true,那么在事务环境下进行的计数,只有事务成功提交才真正执行,默认为支持。

monitor的 type分为 counter 和 gauge两种。counter在生成代码中提供 increament方法,gauge 提供 set方法。一个monitorset 允许多个 key,作更细致的划分,key的类型,只能使用除了 binary与

any之外的简单类型。Xml描述规范

Limax使用 xml描述项目模型的构建,定义了命名节点和引用节点两类 xml 节点。存在同时是命名节点和引用节点的这种节点。xml 内部名字的解析使用内部名字空间规范。命名节点

命名节点包含 name属性,生成代码按照特定规范连接这些 name,组织项目名字空间。zdb中 table的 name必须全部小写,其它 name不限。引用节点凡是使用了 ref属性的节点属于引用节点,ref属性的值,必须是命名节点的引用。临

时 View 节点下的 subscribe 节点,既是引用节点——引用了会话 View的字段,又是命名节点——作为当前 View的字段被命名。各种节点下的 variable 节点,必须指定 type属性,当 type 指向了 xml 内部描述的结构,

bean,cbean,xbean时,可以理解为引用节点。variable 节点作为上层节点的字段节点被命名,因此也是命名节点。内部名字空间规范同一 xml 节点下的命名节点不可重名。父节点名 “.” 子节点名,构成节点路径,节点路径允许逐层向外扩展,直到根节点,

根节点开始的节点路径为全路径。节点名,节点路径,都可以合法地引用节点自身。代码生成过程中,报告二义性节点引用时,应该使用更长的节点路径解决二义性。例

如,名字空间 A下定义 bean X,名字空间 B下定义 bean X。某一 variable定义字段时指定了type为 X,代码生成过程报告二义性错误,这种情况下,必须正确选择 type为 A.X或者B.X,消除二义性。特殊情况讨论

zdb 节点也是命名节点,它的名字不会应用到生成代码中,所以允许不进行命名,这种情况下默认名为空串。为了名字空间解析方便,需要的情况下也可以命名。

cbean 作为特殊的常量类型 bean,具有全局性,不受名字空间约束,整个项目内,任何地方定义的 cbean 均不可重名。

table的 name要求必须小写基于此原因:使用文件数据库时,该 name 被用来直接命18

Page 19: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

名表文件。如果操作系统使用了大小写不敏感的文件系统,比如 windows,应用运行在这类操作系统上时,为了规避可能产生的冲突,全部小写是最好的解决方案。

bind 节点下的 ref 节点,看起来是命名节点,但是应该解释成,与 bind 节点的 table属性结合到一起,引用了 xbean的字段。这样设计的原因:描述简单清晰,否则就应该在bind 节点中加入 ref属性,xbean的字段名逗号分隔。

类型映射正确处理类型是数据库,服务器,客户端,正常工作的关键

简单类型Xml描述 Java C# C++ Javascript Luaboolean boolean bool bool Boolean Booleanbyte byte byte int8_t Number Numbershort short short int16_t Number Numberint int int int32_t Number Numberlong long long int64_t Number Numberfloat float float float Number Numberdouble double double double Number Numberstring string string std::string String Stringbinary Octets Octets Octets Array(Number) Table(Number)any:[type] type 不支持 不支持 不支持 不支持

除了 c#的 byte为无符号类型之外,其它的所有数值类型均为有符号类型,可能导致处理上的差异,需要特别小心。使用脚本客户端的情况下需要小心使用 long类型,脚本客户端的 Number在内部表示

为 double,long的精度达不到 64 位,根据 IEEE754标准,javascript能提供 52 位有效位,不知道为什么 Lua5.1更少,基于这样的现实考虑,在脚本数据编码时作了一个特殊处理,绝对值小于等于 0x3FFFFFFFFFFFL的 long 按照数值编码,否则编码为串,解码之后至少能确保表达的一致性。服务器的字符串均以 UTF8 格式编码,客户端在必要情况下需要自己转换。没有编码解

码库的情况下,可以考虑使用 binary替换,自己解释。any类型,不能直接或间接地被任何 bean,protocol,rpc,view 引用。

容器类型Xml描述 Java C# C++ Javascript LuaList LinkedList LinkedList std::list Array TableVector ArrayList List std::vector Array Table

19

Page 20: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

Set HashSet HashSet std::unordered_set Array TableMap HashMap Dictionary std::unordered_map Map Table

类型化语言中所有容器均使用泛型类型。Javascript规范本身不包含 Map,各种浏览器上的 javascript 环境都有各自的实现版本;

OracleJDK自带的 javascript 引擎实现不包含 Map,使用 limax 内置的简单实现——map.js。构造类型Xml描述 Java C# C++ Javascript LuaBean Class Class Class Map TableCbean Class Class Class Map TableXbean Class Class Class Map Table

类型化语言中各种 bean都定义成类,另外 protocol,rpc,view,control,引用部分xbean 字段的 bind,均生成类代码。

允许的引用关系的粗略描述(直接或者通过容器间接引用):bean := bean* | cbean* | xbean*,bean不允许最终引用到 Any类型上。cbean := cbean*xbean := cbean* | xbean*

脚本语言中,没有类的概念,运行过程中,对象数据直接生成到以字段名为 key的关联容器中。

服务器开发

代码生成——xmlgen

操作limax.jar包集成了代码生成工具。java –jar limax.jar xmlgen –java server.xml按照 server.xml描述的模型在当前路径下生成代码,详细使用帮助可以执行 java –jar

limax.jar xmlgen获得。一般来说,构建一个项目的 xml描述使用类似如下的组织方式:

example.share.xml

<?xml version="1.0" encoding="utf-8"?><namespace name="share" pvid="100">

20

Page 21: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

<!—服务器客户端需要支持的 bean,protocol,rpc,view描述rpc不适合应用项目使用,下面以 bean,protocol,view 作为例子

--><bean name="MyBean">

<enum name="e0" value="0"/><enum name="e1" value="1"/><variable name="var0" type="int"/>

</bean><protocol name="MyProtocol" type="101" maxsize="4">

<variable name="var0" type="MyBean"/></protocol><view name="MyGlobalView" lifecycle="global">

<variable name="var0" type="MyCbean"/></view><view name="MySessionView" lifecycle="session">

<variable name="var0" type="example..MyXbean"/><bind name="bind0" table="mytable"/><bind name="bind1" table="mytable">

<ref name="var0"/></bind><control name="control">

<variable name="var0" type="int"/></control>

</view><view name="MyTemporaryView" lifecycle="temporary">

<variable name="var0" type="int"/><subscribe name="_var0" ref="MySessionView.var0" />

</view></namespace>

example.zdb.xml

<?xml version="1.0" encoding="utf-8"?><zdb><!—

zdb使用的 cbean,xbean,table描述-->

<cbean name="MyCbean"><variable name="var0" type="int"/>

</cbean><xbean name="MyXbean">

<variable name="var0" type="int"/>

21

Page 22: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

<variable name="slist" type="vector" value="string" /></xbean><table name="mytable" key="long" value="MyXbean" autoIncrement="true"/>

</zdb>

example.server.xml

<?xml version="1.0" encoding="utf-8"?><project name=”example” xmlns:xi="http://www.w3.org/2001/XInclude">

<xi:include href="example.share.xml" /><xi:include href="example.zdb.xml" /><state name="Server">

<namespace ref="share" /></state><service name="ExampleServer" useGlobalId="true" useZdb="true">

<manager name="ExampleServer" type="provider" initstate="Server" port="10100"/>

</service></project>

example.client.xml

<?xml version="1.0" encoding="utf-8"?><project name="example" xmlns:xi="http://www.w3.org/2001/XInclude">

<xi:include href="example.share.xml" /><xi:include href="example.zdb.xml" /><state name="Client">

<namespace ref="share" /></state><service name="ExampleClient">

<manager name="ExampleClient" type="client" initstate="Client"port="10000"/>

</service></project>

通过 xml的 XInclude 组织描述文件,划分出服务器,客户端的共享部分和 zdb部分。项目比较复杂的情况下,应该考虑从功能模块的角度使用 XInclude 作更合理的细分。必要的时候,服务器 xml与客户端 xml 允许合并成一个,xmlgen 加入-service 参数可以

指定只生成某一个 service的代码。不推荐这样使用。脚本环境的客户端不需要依据客户端 xml生成代码,应该通过指定 xmlgen 参数在生成

服务器的同时生成对应脚本语言的代码模板。按照 xml描述生成的服务器源代码,存放在当前目录下的两个子目录内,src目录与

22

Page 23: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

gen目录。src目录为源码目录,下面的文件按照应用需求修改编辑,这个目录应该提交到版本控制系统内;gen目录不需要作版本控制,每次执行 xmlgen都会重建,任何修改都会丢失。另外,当前目录下生成了 service-XXX.xml文件,XXX为 xml描述中的 service name,运行服务器需要的配置生成在这里,部署到具体运行环境的时候可以按照运营需求适当调整。一个描述文件下有多个 service的情况下,service-XXX.xml生成多个。

新建 eclipse Java项目,Location 指定当前目录。Package Explorer中,右键编辑项目属性Java Builder Path – Source下,加入 src与 gen目录Java Builder Path – Projects 下,加入对 limax的引用,或者 Java Builder Path – Libraries下,加入 limax.jar。这时就能清晰看见项目组织结构。

名字空间映射把 xml描述中的名字空间映射到 java 名字空间下。protocol, rpc, view 命名规则:项目名,service 名, (protocol, rpc, view)所在的名字空间名, (protocol, rpc, view)名。例如,上面定义的MyGlobalView 被命名为 example.ExampleServer.share.MyGlobalView特别的,gen目录下能看到 example.ExampleServer.states 这样一个名字空间,这个名字

空间下放置了 service通过 manager 节点引用的所有 state 节点相关的生成代码。为了避免混乱,项目的最外层名字空间不允许命名为 states,否则生成阶段报错。

bean,monitorset 命名规则:项目名,(bean,monitorset)所在的名字空间名,(bean,monitorset)名。例如,上面定义的MyBean,被命名为 example.share.MyBean。对于 bean 而言,bean与 protocol, rpc, view一同定义在 xml的 namespace 节点下,还是

为之采用不同命名规则的原因在于 bean可以被多个服务中的 protocol,rpc,view 引用,这种方案可以减少重复代码的生成。

对于 monitorset 而言,这种命名方式容易映射到期望的 jmx domain。cbean,xbean,table 命名规则:cbean放置在 cbean 名字空间下。xbean放置在 xbean 名字空间下。table放置在 table 名字空间下,table的名字被修订为首字母大写。使用 cbean, xbean 与 table 时,不要 import 这些名 字 空间,应该直接使用

xbean.MyXbean,table.Mytable,这样就能清晰看出代码使用了 zdb的某个元件,便于维护。

23

Page 24: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

View

代码生成以后,只需要编辑 src目录下的几个文件,MyProtocol不作讨论。观察MyGlobalView,MySessionView,MyTemporaryView 三个段代码。三 段 代 码 分 别 继 承 自 gen 目 录 下 的 _MyGlobalView, _MySessionView,

_MyTemporaryView,父类里的公有方法,即包含了操作 view 所需要的代码。View 对象的获得与维护获取全局 ViewMyGlobalView gview = MyGlobalView.getInstance();全局 View全局一个,获取不需要参数。手工同步全局 Viewgview.syncToClient(sessionid)同步全局 view到客户端gview.syncToClient(sessionid, “var0”);同步全局 View的 var0 字段到客户端gview.syncToClient(Arrays.asList(sessionid1, sessionid2));广播全局 view到指定的客户端集。gview.syncToClient(Arrays.asList(sessionid1, sessionid2), “var0”);广播全局 view得 var0 字段到指定的客户端集获取会话 ViewMySessionView sview = MySessionView.getInstance(sessionid);这里需要 sessionid 参数,因为会话 View在会话建立时以 sessionid为 key分类自动创建。创建临时 View MyTemporaryView tview = MyTemporaryView.createInstance();

销毁临时 Viewtview.destroyInstance();

获取临时 ViewMyTemporaryView tview = MyTemporaryView.getInstance(sessionid, instanceid);每个临时 View维护了一个Membership,该 Membership中的 sessionid,才能用作这里

的 sessionid 参数,临时 View创建以后 tview.getInstanceIndex(),可以获取第二个参数所需的 instanceid。

加入临时 Viewtview.getMembership().add(sessionid);执行成功以后临时 View代码中的 onAttached方法被调用,见MyTemporaryView的生

24

Page 25: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

成代码,这里应该填写加入成功后应该执行的动作。执行失败以后临时 View代码中的 onAttachAbort方法被调用,见 MyTemporaryView的

生成代码,AbortReason 指出了失败原因,比如加入 sessionid 过程中,sessionid 对应用户离线了。

离开临时 Viewtview.getMembership().remove(sessionid, reason); 执行成功以后临时 View代码中的 onDetached方法被调用,见 MyTemporaryView的生

成代码,reason 将传递给 onDetached; reason必须为非负值,负值内部使用,指出系统原因,比如整个 view关闭,导致了用户 detach。

执行失败以后临时 View代码中的 onDetachAbort方法被调用。这里要明确,onDetachAbort是对 Membership.remove动作失败的响应,销毁临时 View

导 致的离开,系统以负值 调用 onDetached。如果销毁临时 View 的动作出现在Membership.remove之前,会出现这样的状况,首先 onDetached以负值 reason 被调用,然后 onDetachAbort以 VIEWCLOSED为 reason 被调用,如果需要精确处理离开的行为,这种情况应该要考虑到。View 对象上的操作

View 对象上 3种操作,分别与 xml描述中 view 节点下三个子节点 variable, bind, control一一对应。

variable上的操作<variable name="var0" type="int"/>生成 setVar0(Integer),_setVar0(Integer)提供两个版本 set方法与事务支持相关。事务环境下,使用 set 版本,事务成功后 set

动作被真正执行,事务失败 set动作不执行,_set 版本 set动作立即执行与事务成功失败无关;非事务环境下,两个版本行为一致。以 null为参数执行 set,意味着删除该字段。

bind上的操作<table name="mytable" key="long" value="MyXbean" /><bind name="bind0" table="mytable"/>生成 bindMytable(long) 方法,这里的参数类型 long 即是mytable的 key的类型 long。例如,view.bindMytable(100);执行以后,一旦 mytable表的 key=100这一行的数据,也

就是MyXbean类型的 xbean在事务成功以后检测到改变,改变的数据自动设置到 view的bind0 字段上。

control上的操作<control name="control">

<variable name="var0" type="int"/></control>生成如下方法protected static final class control implements limax.codec.Marshal, Comparable<control> {

25

Page 26: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

public int var0;……

}protected void onControl(_MySessionView.control param, long sessionid) {}需要在 onControl 内填写 control的响应代码。特别的,每个 View的生成代码中,均有protected void onMessage(String message, long sessionid) {}填写消息处理代码,脚本客户端的情况下,脚本系统难以有效构造类型化数据,只能

使用默认的 onMessage传递字符串消息,应用应该定义自己的串数据格式规范,服务器端按规范解析message 字符串。Zdb

存储过程zdb通过存储过程实现数据库的事务支持,需要使用的包是 limax.zdb.Procedure,过程

返回 true 事务提交,返回 false 事务回滚。有 3种基本执行方式,execute,submit,call。execute:异步执行存储过程Procedure.execute(()->{

过程代码,返回 true或者 false});上面的方法不关心返回结果,是最简单常用的方法。Procedure.execute(()->{

过程代码,返回 true或者 false}, (procedure, result)->{

if (!result.isSuccess()) {System.err.println("Procedure " + procedure + " fail");if(result.getException() != null)

result.getException().printStackTrace(System.err);else

System.err.println(“procedure return false;”);

}});上面的方法通过第二个 lambda表达式参数获得过程的返回值,返回值提供了成功失败

的信息,如果失败可以检测是否由异常导致。submit:异步执行存储过程,返回 Future<Procedure.Result>Future<Procedure.Result>future = Procedure.submit(()->{

26

Page 27: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

过程代码,返回 true或者 false});可以通过 future.get(),等待过程结束,通过 get可以获得执行结果。call: 同步执行存储过程,返回 Procedure.ResultResult result = Procedure.call(()->{

过程代码,返回 true或者 false});

这里需要特别注意,submit不允许在存储过程内调用,否则抛出 IllegalStateException异常。存储过程嵌套调用,必须使用 call,存储过程的嵌套导致事务嵌套,内层事务失败回滚不影响外层事务;存储过程内部使用 execute 将启动新的过程,不能实现嵌套。异常规范

Zdb框架划分两类异常:用户异常,派生自 java.lang.Exception框架异常 XError,派生自 java.lang.Error,由框架和生成代码使用。用户在实现存储过程时允许抛出或者捕获用户异常。禁止捕获 Error,Throwable。存储过程执行时,抛出用户异常,导致当前事务回滚,效果如同在过程中 return

false,并且将异常设置到过程返回结果中。存储过程执行时,抛出 XError,导致事务回滚到最外层,直接设置最外层过程的返回

结果。submit方式下,如果存储过程执行时抛出异常至最外层,应该捕获 java.util.concurrent.

ExecutionException e,e.getCause(),获取该异常。正确设置过程的日志级别,可以将框架捕获到的异常记录下来。

标准表操作table的 value有两种类型,一是 xbean类型,二是常量类型(cbean以及除了 binary之

外的简单类型),这两种类的操作稍有区别,分别介绍。value 为 xbean类型的表操作:xbean = table.Tablename.insert(key);根据 key 插入一条记录,记录存在插入失败,返回 null,否则返回 xbean,这之后可以

填写 xbean 内容,提交以后内容被加入数据库。key = table.Tablename.newKey();

27

Page 28: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

允许自增量的表,可以用这个操作获取新的 key,再作插入操作。pair = table. Tablename.insert();允 许 自 增 量 的 表 支 持 该 操 作 , 功 能 相 当 于 table.

Tablename.insert(table.Tablename.newKey()); , 返 回 的 pair 为 limax.util.Pair 类 型 ,pair.getKey(),获取 key,pair.getValue()获取 xbean,接下来填写 xbean 内容。除非自增量配置在使用以后,又进行了错误的重配置,返回的 xbean不可能为 null。

xbean = table.Tablename.update(key);获取与 key关联的 xbean记录,获得的 xbean进行修改以后,事务提交后更新到数据库。xbean = table.Tablename.select(key);获取与 key关联的 xbean记录,获得的 xbean 只能读,一旦修改将抛出异常导致事务失

败。result = table.Tablename.delete(key);删除与 key关联的记录,返回类型为 boolean,成功 true,失败 false。value为常量类型的表操作:(以 cbean为例)cbean = table.Tablename.insert(key, cbean);除非记录存在,返回原来的 cbean,否则 null。pair = table.Tablename.insert(cbean);除了多个 cbean 参数与 value为 xbean的自增量表类似。update 操作返回常量本身,但是无法修改。与 select的差别在于 update获取了写锁,

而 select 只能获取读锁。select,remove 操作与 value为 xbean的情况相同。

非标准表操作table.Tablename.get().getCache().walk((key, value) -> {

//readonly action});遍历表 cache,遍历过程逐个读锁定记录,提供的记录只读。既可用于磁盘表也可用于

内存表。table.Tablename.get().walk((key, value) -> {

//readonly action});遍历低层数据库内容,内存数据库中未 checkpoint的记录不会访问到,zdb 周期性将提

28

Page 29: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

交数据 checkpoint到低层。虚拟机参数 limax.zdb.Checkpoint.SCHED_PERIOD 控制 checkpoint检测的最小周期,默认 100ms,具体的 Checkpoint 周期由运行时的 xml 参数配置。

因为看到的低层数据库内容,所以与 zdb锁无关系。这种walk 只能用于磁盘表。对于这 2种walk,结果的顺序不保证,不需要在事务上下文中执行。

事务隔离度默认隔离度为 level2,非常必要的情况下可以使用 level3,这种情况下事务被串行执行,

效率低。Transaction.setIsolationLevel(Transaction.Isolation.LEVEL3);将当前线程事务隔离度设置为 level3,也就是说,凡是通过当前线程启动的事务,均

拥有 level3 隔离度。Transaction.setIsolationLevel(Transaction.Isolation.LEVEL2);隔离度修改回 level2。

锁zdb的锁为行锁,默认支持隔离度 level2,实现 holdlock,确保可重复读,直到事务结

束。select 操作获得读锁,insert,update,delete 操作获得写锁。执行 select之后,如果在同一 key上 update,先前的读锁被释放,再进行写锁定,这意味着读锁被升级为写锁;反过来锁不会被降级,原因在于:修改记录后如果把锁降级为读锁,那么别的事务就有可能读取到刚修改过的数据,事务隔离度就降级为 level0了,发生了脏读,往往容易造成逻辑错误。这意味着,

xbean = table.Tablename.select(key);table.Tablename.update(key);

这一序列执行完成以后,xbean 允许修改,即便再次 table.Tablename.select(key),该 xbean还是能够修改。经验上,判断一个 key不存在再插入这一典型事务与关系数据上的操作非常类似:

if (table.Tablename.select(key) == null)table.Tablename.insert(key);

往往被认为是一个不好的操作,因为有可能在 select读锁释放以后 insert写锁定之前,另外的事务用这个 key 执行了 insert,导致这里的 insert失败,直接违反了上面语句序列的初衷。合理的写法应该是:

xbean = table.Tablename.update(key);if(xbean == null)

table.Tablename.insert(key);复杂的事务可能导致死锁,zdb支持死锁检测。死锁检测周期,重试次数,重试退避

的最大时间均可配置。死锁检测周期至少 1秒。死锁被作为异常记录到过程返回结果中,可以通过日志配置记录下来。乐观模式下的设计,不需要考虑锁问题。需要解决的情况下检查日志,发现死锁热点,再考虑使用显式的预先锁定解决。通过预先一次性锁定事务将涉及到的表的行,可以有效解决死锁问题。需要使用的包

是 limax.zdb.Transaction.LockContext。29

Page 30: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

例如:同时写锁定表 ta,tb的行 row1,row2,可以写为:Transaction.getLockContext().wAdd(row1, row2, table.Ta.get(), table.Tb.get()).lock();同时写锁定表 ta,tb的行 row1,row2以及读锁定表 tc的行 row3,可以写为:Transaction.getLockContext().wAdd(row1, row2, table.Ta.get(), table.Tb.get()).rAdd(row3,

table.Tc.get()).lock();一个 add 操作请求锁定,参数中所有列举出来的表的所有列举出来的行,有非常大的

灵活性,可以一一列出,也可以通过容器给出,顺序也没有限制。例如:wAdd(table.Ta.get(),table.Tb.get(), row1, row2);wAdd(new Object[]{row1, row2}, new Object[]{table.Ta.get(),table.Tb.get()});wAdd(new Object[]{table.Ta.get(),table.Tb.get(), row1, row2});wAdd(Arrays.asList(table.Ta.get(), row1), Arrays.asList(table.Tb.get(), row2));均是等价的。实际上,容器(Collection)类型或者数组类型的参数值,被全部递归解析出来,区分出

表类型对象构造锁定的表集,非表类型对象构造行集。xbean访问

Xbean只能在事务环境下访问,确保访问时拥有相应的锁,避免并发冲突,生成不正确的序列化数据,使得错误蔓延到下次数据库读取,影响服务正常运行。实现上, Zdb运行配置中,属性 zdbVerify,控制相应的锁检测,默认 zdbVerify=true,每次 Xbean访问均检测锁有效性,如果访问缺少锁,则抛出 limax.zdb.XLockLackedError。除非服务器经过严格的覆盖测试,不要为了少量的性能提升将 zdbVerify设置为 false,特别是闭包的使用很容易将 Xbean带出锁范围之外。只读操作获得的 xbean比如通过 select,walk表 cache,只能进行读访问,写访问将抛出 limax.zdb.XLockLackedError。需要定期检查服务器运行日志,一旦发现 limax.zdb.XLockLackedError,则根据对应的栈信息修改代码,确保并发安全性。

<xbean name="MyXbean"><variable name="var0" type="int"/><variable name="slist" type="vector" value="string" />

</xbean>

MyXbean xbean = new xbean.MyXbean ();

xbean 对象使用 get,set 存取简单类型数据。int x = xbean.getVar0();xbean.setVar0(100);

xbean 对象访问容器类型时,直接 get获得容器进行修改xbean.getSlist().add(“abc”);xbean.getSlist().remove(0);

30

Page 31: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

Xbean在组织结构上,把记录看作根节点,层层连接起来,形成一棵树。作为一棵树上节点的 xbean不能连接到另一棵树上,例如:

Xbean1 x1 = table.Table1.select(key1)Xbean2 x2 = table.Table2.update(key2);x2.getArray().add(x1); //这里假设 Xbean2的 array 字段定义为 Xbean1的 vector是禁止的,这种情况下将抛出异常报告 Xbean管理错误,结束事务。上面的例子应该

通过拷贝数据解决问题,例如:x2.getArray().add(new Xbean1(x1));

Monitor

这里简单解释 Monitor的使用。limax.xml定义了 TransactionMonitor,于是生成了代码 limax.zdb.TransactionMonitor,提

供了 6个方法:public static void increment_runned(String procedureName);public static void increment_runned(String procedureName, long _delta_);public static void increment_false(String procedureName);public static void increment_false(String procedureName, long _delta_);public static void increment_exception(String procedureName);public static void increment_exception(String procedureName, long _delta_);供应用开发使用。两个方法:public static String buildObjectNameQueryString(String procedureName);public interface Collector;提供给运行环境下的采集应用使用。

服务器简单验证这里先不介绍客户端实现,而用 HTML5方法,通过 chrome,直观体验服务器开发的

效果。

简单准备服务器组件eclipse 导入项目 auany启动 limax.auany.Main注意,第一次启动 auany 前请在 auany 当前目录下手工创建 zdb目录。eclipse 导入项目 switcher启动 limax.switcher.Main

xmlgen 参数加入-script 参数生成服务器脚本支持代码

31

Page 32: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

加入-jsTemplate 参数生成 javascript模板代码例如:java –jar <path to limax.jar> xmlgen –script –jsTemplate example.server.xml执行以后,在当前目录下,能看到一个名为 template.js的文件

制作实验用 html

拷贝 limax.js到当前目录example.html

<!DOCTYPE html><html><script type="text/javascript" src="limax.js"></script><body><script>

在这里拷入 template.js的全部内容</script></body></html>

在其中修改:var login = {

scheme : 'ws',host : '127.0.0.1:10001', // 配置服务器地址username : 'XXX', //用户名随意token : '123456', //必须用 123456 作为 tokenplatflag : 'test', //使用 auany的 test 认证模块pvids : [100], //PVID=100

}这个配置与 auany的 test模块相关,配置对应关系,详见 service-auany.xml,及 test模

块的实现。添加 Session管理器类

SessionManager.java

import java.io.IOException;

import limax.net.Config;import limax.net.Manager;import limax.net.ServerManager;import limax.net.Transport;

32

Page 33: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

import limax.provider.ProviderListener;import limax.provider.ProviderTransport;import limax.util.Trace;

public class SessionManager implements ProviderListener {private ServerManager manager;@Overridepublic void onManagerInitialized(Manager manager, Config config) {

try {this.manager = (ServerManager)manager;this.manager.openListen();

} catch (IOException e) {if (Trace.isErrorEnabled())

Trace.error("SessionManager.onManagerInitialized", e);this.manager.close();

}}@Overridepublic void onManagerUninitialized(Manager manager) {}@Overridepublic void onTransportAdded(Transport transport) throws Exception {

long sessionid = ((ProviderTransport) transport).getSessionId();if (Trace.isInfoEnabled())

Trace.info("SessionManager.onTransportAdded " + transport+ " sessionid = " + sessionid);

}@Overridepublic void onTransportRemoved(Transport transport) throws Exception {

long sessionid = ((ProviderTransport) transport).getSessionId();if (Trace.isInfoEnabled())

Trace.info("SessionManager.onTransportRemoved " + transport+ " sessionid = " + sessionid);

}@Overridepublic void onTransportDuplicate(Transport transport) throws Exception {

manager.close(transport);}

}

服务器初始化的时候触发 onManagerInitialized,这里应该首先加载服务器运行所需的应用资源,加载完成以后调用 openListen(),openListen()首先初始化所有全局 View,接下来告知 Switcher准备就绪,允许客户端连接。

33

Page 34: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

服务器停止的时候触发 onManagerUninitialized()这时候可以清理所有应用资源。客户端连接上服务器后触发 onTransportAdded 消息,提供机会准备相应用户的 View系统之外的资源,在这之后用户的 SessionView 被创建出来。客户端从服务器断开触发 onTransportRemoved 消息,提供机会释放相应用户的 View之外的资源,多数情况下,记录日志即可。客户端重复登录触发 onTransportDuplicate 消息,这里关闭 transport,前一个用户Session 被 Kick掉;这里什么都不做,后一个用户被禁止登录。

修改 service-ServerExample.xml

Provider 节点下加入属性 className =”SessionManager”,这样服务器启动的时候才能创建 SessionManager通告消息。

Trace 节点下,修改属性 level =”INFO”,设置 log级别,可以看到更多信息,比如上面SessionManager.java中的日志记录就是记录在 INFO级别。不使用 GlobalId 服务组 件的情 况下可以注 释掉 GlobalId 节点,同时去掉

example.server.xml中 useGlobalId=”true”这个属性,重新生成,否则下一次代码生成又会生成新的 GlobalId 节点。服务器启动主函数

Main.java

import limax.xmlconfig.Service;public class Main {public static void main(String[] args) throws Exception {

Service.run("service-ExampleServer.xml");}

}

src目录加入服务器运行主函数,并且在当前目录建立 zdb目录。运行Main,eclipse 控制台返回2015-03-19 21:10:08.322 INFO <main> ServiceConf load service-ExampleServer.xml

2015-03-19 21:10:08.323 INFO <main> ServiceConf runTaskBeforeEngineStart

2015-03-19 21:10:08.354 FATAL <main> zdb start begin

2015-03-19 21:10:08.410 FATAL <main> zdb start end

2015-03-19 21:10:08.410 INFO <main> ServiceConf startNetEngine

2015-03-19 21:10:08.433 INFO <main> ServiceConf runTaskAfterEngineStart

34

Page 35: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

2015-03-19 21:10:08.465 INFO <main> ProviderManager SessionManager@15327b79 opened!

2015-03-19 21:10:08.471 INFO <limax.net.io.NetModel.processPool.17> provider client

manager limax.net.StateTransportImpl (/127.0.0.1:11705-/127.0.0.1:10100)

setInputSecurityCodec key = compress = false

2015-03-19 21:10:08.472 INFO <limax.net.io.NetModel.processPool.17> provider client

manager limax.net.StateTransportImpl (/127.0.0.1:11705-/127.0.0.1:10100)

setOutputSecurityCodec key = compress = false

2015-03-19 21:10:08.475 INFO <ProviderConnectorExecutor.ExampleServer.18>

SessionManager@15327b79 onTransportAdded limax.net.StateTransportImpl

(/127.0.0.1:11705-/127.0.0.1:10100)

2015-03-19 21:10:08.480 INFO <limax.net.Engine.protocolScheduler.21> provider had bind

success! pvid = 100

看到最后一行,pvid = 100,这就是 example.share.xml中指定的那个 PVID,一切OK,可以开始进行实验了。实验 1

啥代码也不写,打开 chrome,F12打开调试窗口,将前面准备好的 example.html拖入chrome。观察服务器日志:2015-03-20 16:37:56.679 INFO <limax.net.Engine.applicationExecutor.24>

SessionManager.onTransportAdded limax.provider.ProviderTransportImpl (49152 -

/127.0.0.1:40505) sessionid = 49152

这一行就是前 面的 ViewManager 代码记录下来的,客户端连接上来了,用户的sessionid为 49152。观察 chrome 控制台:约一分钟后,chrome 控制台上显示了 keepalive,这指出 limax.js 向服务器定时发送了

keepalive 消息。停止服务器,chrome 控制台:

拷贝过来的模板代码中的 ctx.onerror 报告了异常,连接随即关闭,ctx.onclose 报告了关闭原因——错误码 17。错误码存在的情况下,不用去关心上面的异常,直接查找 limax框架描述文件中的 defines.beans.xml,然后看到这一行:

<enum name="SWITCHER_PROVIDER_UNBIND" value="17" />这个意思大致就是 switcher 报告 provider解除了绑定,意味着服务器停止。

35

Page 36: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

实验 2

启动服务器,刷新 chrome页面。这又回到了刚才的初始状态。启动一个新的 chrome实例,拖入 exmaple.html。chrome 控制台:

这里看到错误码 3008,对应了:<enum name="PROVIDER_KICK_SESSION" value="3008" />这意味着 SessionManager.onTransportDuplicate正常工作了,前一个用户的会话被 Kick

掉。实验 3

一般来说,一个应用至少应该有一个 SessionView,用户的应用级信息,应该持久化。所以:

private MySessionView(SessionView.CreateParameter param) {super(param);// bind herelong sessionid = param.getSessionId();bindMytable(sessionid);Procedure.execute(() -> {

xbean.MyXbean xb = table.Mytable.insert(sessionid);if (xb != null)

xb.setVar0(100);return true;

});}这段代码首先将 view的 bind0 字段关联到当前的 sessionid上,接下来使用一个存储过

程判断Mytable中该 sessionid的记录是否存在,不存在则创建一条新记录,记录中的 var0字段初始化为 100。先 bind,然后再使用存储过程决定是否初始化记录,是比较常用的方式。运行服务器,刷新 chrome页面:

这里清楚看到,sessionid = 49152这一用户的 bind0 字段在客户端新创建出来了,其中36

Page 37: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

bind0 字段内 var0 = 100。再次刷新 chrome页面,内容不会变化。修改 example.html中的 login.username,换成一个没有使用过的用户名。刷新 chrome页面:

注意到 sessionid = 53248,这说明 sessionid 区分了用户。实验 4

private MySessionView(SessionView.CreateParameter param) {super(param);// bind herelong sessionid = param.getSessionId();bindMytable(sessionid);Procedure.execute(() -> {

xbean.MyXbean xb = table.Mytable.insert(sessionid);if (xb != null)

xb.setVar0(100);setVar0("oh, my god. it works.");setVar0("the sequence is right.");return true;

});setVar0("hello world");

}注意到,这里补充了 3个 View上的 setVar0。运行服务器,刷新 chrome。

第一行,view的 var0,首先被创建为”hello world”。尽管这一 set 调用在函数最后一行,但是前面的存储过程是在别的线程异步执行的,所以它被首先执行并不意外。

第二行,view的 var0,被替换为”oh, my god.it works.”。第三行,view的 var0,被替换为”the sequence is right.”

37

Page 38: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

第四行,同前一个实验,view的 bind0的字段被初始化。这里的 bind0实际上是由 bindMytable 初始化的,在另外一个存储过程中被设置,我们

并不知道它应该什么时候发生。所以,修改 example.html,再换一个没有使用过的用户名。

现在已经能够确保 insert发生在两次 setVar0之前了,为什么 bind0的设置还是在最后?这就要需要进一步理解本手册前面的叙述了:“特别要注意的是,同一 View上 bind字段的改变时序不作假设,这些改变具有事务原

子性。”而 view的 var0的设置表现上也可以看出:“客户端可以重现服务器设置同一 View上的字段的顺序”variable的设置顺序是有保证的,bind没有,其实道理也很简单,存储过程中,在同一

xbean上反复修改完全有可能,从事务的角度看,只有最终那一次修改结果才被提交。将 setVar0("the sequence is right.");修改为_setVar0("the sequence is right.");

在这里,bind0的又放到了第二行,前面已经讲清楚了,它出现的位置不需要关心。第三第四行,”the sequence is right.”跑到了前面。这里体现了_set 版本的意义:使用

_set版本,字段数据被立即设置到 view上,无需等到事务成功。setVar0("hello world");之后增加两行 setVar0("hello world");和 setVar0(null);

38

Page 39: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

第二行,view的 var0 被新创建为”hello world”第三行,view的 var0还是”hello world”,状态是 TOUCH第四行,view的 var0 “hello world”,被删除,状态为 DELETE第五行,view的 var0又被新创建为”the sequence is right.”实际上 TOUCH实现了网络流量的优化,服务器发现字段数据没有变动,就简单告知客

户端字段被 TOUCH了。要删除 view的字段,就设置成 null。此外,bind的记录在表中删除后,bind字段被设

置成 null。实验 5

private MySessionView(SessionView.CreateParameter param) {super(param);// bind herelong sessionid = param.getSessionId();bindMytable(sessionid);Procedure.execute(() -> {

xbean.MyXbean xb = table.Mytable.insert(sessionid);if (xb != null)

xb.setVar0(100);return true;

});MyGlobalView gview = MyGlobalView.getInstance();gview.setVar0(new xbean.MyCbean(123));

}运行服务器,刷新 chrome页面,结果与实验 3的输出完全一样。函数最后加入一行 gview.syncToClient(sessionid);

第一行可以看到,MyGlobalView的 var0 字段设置的 123 被发送到客户端了。使用39

Page 40: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

GlobalView需要手工同步。函数最后再加入两行 gview.syncToClient(sessionid);

注意到第二行和第三行,状态都是 REPLACE,明明数据没有改变,怎么不是 TOUCH?原因很简单,GlobalView维护数据的最新版本,通过手动刷新将最新版本的数据同步

到一个或者多个客户端,并不会为个别客户端记录发送历史,所以 GlobalView上永远不会通告 TOUCH状态。同样的,如果删 除了字 段数据,字 段数据的状态就是不存在,既 然不存在,

syncToClient时也就不会同步到客户端,这就是说,GlobalView上永远不会通告 DELETE状态。为了客户端实现方便,避免使用 GlobalView字段的删除语义。

实验 6

private MySessionView(SessionView.CreateParameter param) {super(param);// bind herelong sessionid = param.getSessionId();bindMytable(sessionid);Procedure.execute(() -> {

xbean.MyXbean xb = table.Mytable.insert(sessionid);if (xb != null)

xb.setVar0(100);return true;

});MyTemporaryView tview = MyTemporaryView.createInstance();tview.getMembership().add(sessionid);

}

MyTemporaryView.java中:@Overrideprotected void onAttached(long sessionid) {

MySessionView.getInstance(sessionid).setVar0("onAttached");}

运行服务器,刷新 chrome页面。40

Page 41: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

第二行,TemporaryView的 onopen 被调用,于是:第三行,SessionView上的 var0 被创建为”onAttached”。注意到 xml的描述。

<subscribe name="_var0" ref="MySessionView.var0" />在这里,临时 view 订阅了MySessionView.var0,命名为_var0,于是有了:第四行,TemporaryView的_var0同样设置为”onAttached”。在这里把 View 对象的内容展开看,其实那个_var0,挂在了 57344这个节点下,这个

57344实际上就是当前的 sessionid。TemporaryView的订阅效果在客户端表现就是:每个成员用户的被订阅信息挂在以成员用户 sessionid为 key 节点下。

修改 example.html,在 v100.share.MyTemporaryView.onchange的方法之后添加一行。ctx.send(v100.share.MyGlobalView, e.view.__i__);在MyGlobalView.java中实现 onMessage方法:@Overrideprotected void onMessage(String message, long sessionid) {

MyTemporaryView.getInstance(sessionid, Integer.parseInt(message)).getMembership().remove(sessionid, (byte) 33);

}

重新运行服务器,刷新 chrome页面

41

Page 42: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

在这里多出了第 5行。应该这样解释:第 4行输出以后,TemporaryView的 instanceid,被作为消息发送给 GlobalViewGlobalView实现了 onMessage方法,解析出 instanceid获得该 TemporaryView,将用户

从 Membership中移除,导致客户端结束了该 TemporaryView。这里可以看出,Control,Message具有全局意义,这就是本手册前面叙述的:“Control尽管定义在 View名字空间下,也不意味着这个 Control的实现只能改变当前

这个 View。”实验 7

该实验详细解释 TemporaryView的行为,行为比较复杂。清理掉前面所有修改。拷贝一份 example.html到 example1.html,example1.html中更换一个 username

private static Object lock = new Object();private static MyTemporaryView tview;private MySessionView(SessionView.CreateParameter param) {

super(param);// bind herelong sessionid = param.getSessionId();setVar0("hello " + sessionid);synchronized (lock) {

if (tview == null)tview = MyTemporaryView.createInstance();

tview.getMembership().add(sessionid);}

}

MyTemporaryView.java中@Overrideprotected void onAttached(long sessionid) {

MySessionView.getInstance(sessionid).setVar0("onAttached " + sessionid);

42

Page 43: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

}

运行服务器,刷新 chrome页面。另外启动一个 chrome,F12开启调试,拖入 example1.html。图 1,example.html 调试窗口

图 2,example1.html 调试窗口

43

Page 44: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

图 1,前 5行按前一个解释即可。图 2,第一行,当前用户加入了前一个用户创建的 TemporaryView,onopen 后面 [57344,

53248]可以看到现在有 2个成员。第二行,”_var0 onAttached 57344”,这是 57344成员被订阅的 var0的最新信息第三行,”_var0 hello 53428”,这是当前用户被订阅的 var0最新信息第四行,当前用户的MySessionView.var0 被设置为“hello 53428”这里,出现一个疑问,第三行,第四行搞反了吧?其实,这就是本手册前面叙述的: “客户端可以重现服务器设置同一 View上的字段的顺序;不同 View之间的时序不作保

证。”回到图 1,第六行,onattach 53428,这表示第二个成员 53428 加入进来了第七行,53428成员被订阅的 var0 信息”hello 53428”被送过来了回到图 2,第五行,MySessionView.var0在 onAttached时被设置为”onAttached 53428”第六行,被订阅的信息也送给自己回到图 1,第八行,53428成员被订阅的信息”onAttached 53428”被送过来了。比较,图 1 第七行,图 2 第八行展开的信息,完全一样,这里可以看见两个客户端的

TemporaryView 内容上完全同步的。这个比较复杂,简单总结一下行为。用户加入 Membership时:

44

Page 45: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

1. 收集新用户的所有被订阅信息,作为 attach 消息广播给其它用户。2. View信息(variable, bind),连同其它用户的所有被订阅信息被发送给新用户。3. 框架以新用户 sessionid为参数调用服务器端 View实例的 onAttached 方法。离开 view的实验建议自己做了,否则截图太多,这个相对简单,有两种情况。Membership.remove移除用户:1. 发送 close 消息给离开用户。2. 发送 detach 消息(连同Membership.remove的 reason 参数)给其它用户。3. 框架以离开用户 sessionid 与 reason为参数调用服务器端 View实例的 onDetached

方法。用户离线:1. 发送 detach 消息(以-1为 reason)给其它用户。2. 框架以断 线用户 sessionid 以及 reason = -1 为参数调用服务器端 View 实例的

onDetached 方法。最后,临时 View关闭时1. 发送 close 消息给所有用户。2. 框架遍历所有用户 sessionid,连同 reason = -1为参数逐一调用服务器端 View实例的 onDetached 方法。

总结上面几个实验,简单示例了 View的使用。类比协议模式可以看出,协议模式的设计可

以映射到 View系统里面来:站在服务器端的角度看,相当于创建一个唯一 SessionView,只使用 variable 节点,用协议名命名节点,类型为按照协议字段组织的 Bean。所以,View提供了远比协议模式强大的网络应用编程能力,而又完全不用关心任何网络开发细节。

客户端开发

Javascript客户端前面的几个实验已经涉及到了 javascript开发,这里只需要明确客户端名字空间组织方

式即可。对照 template.js可以看出,客户端所有数据全部挂接在以 ctx为根的一棵树上。在上面

的实验中适当位置 console.log(ctx);即可输出这棵树,一层一层讨论。

45

Page 46: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

第一层

100 即为当前请求服务的 PVID,如果有一个客户端请求多个,则会并列出现多个。f: -1, auany返回的状态标记,和认证模块相关。i: 57344,用户的 SessionIdonclose,onerror,onopen,template.js中设定的网络消息 handle。register,这个方法是系统注入的,如果 view的字段太多,需要单独监听个别字段的变

化,可以使用 register。三个参数,r为 view 对象;v为需要监听的字段名;f为监听器,参数解释与 onchange一样。

send,这个方法是系统注入的,用于向服务器发送控制消息。两个参数, r为 view 对象,s为需要发送的消息字符串。第二层

这里可以看见按服务器 xml描述中的名字空间组织起来的三种 View。观察 example.share.xml:<namespace name="share" pvid="100">这里可以看到 share 和 pvid=100是并列的,为什么会专门制造一个层次出来?原因在于,客户端框架允许连接同一运营体系下多个服务,换言之,即是连接多组

xml定义的项目,所以无法保证两个项目没有使用一样的最外层名字空间名,只有 pvid能做出正确的区分。

46

Page 47: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

第三层

这一层需要分两组讨论,GlobalView,SessionView一组,TemporaryView一组。全局 View 和会话 View:对待 SessionView的观点,与服务器不同,一个客户端的 Session自然只能有 1个,所

以从客户角度来看与 GlobalView应该同等对待,这里用MySessionView解释:__c__: 1 这个是生成服务器代码时提供给各个 View的唯一编号。__n__: “share.MySessionView” 很明确,View的名字。__p__: pvid,既然 View上没有类似__parent__的字段引用回上层,记录在这里就行了。onchange: template.js 注入的这个 View的监听器。var0: 这就是这个 View定义的字段。这里需要注意,xml描述里面定义了的字段这里可能没有,要么是没有初始化,要么

就是删除了。临时 View:1: Object,这个 1是临时 View的 instanceid,一个临时 View 允许有多个实例,在这里

进行区分。__c__, __n__, __p__,同上。onchange: template.js 注入的这个 View的监听器模板onopen: 服务器通告临时 View创建时调用,参数一为临时 View的 instanceid,参数二

为当前临时 View的成员 sessionid数组,这里将 onchange方法的引用拷贝给了创建出来的临时 View实例。

onattach: 新成员加入临时 View时调用,参数一为临时 View的 instanceid,参数二为新成员的 sessionid。

ondetach: 成员离开临时 View时调用,参数一为 View的 instanceid,参数二为离开成员的 sessionid,参数三为离开原因。

47

Page 48: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

onclose: 服务器关闭临时 View时调用,参数为临时 View的 instanceid。

第四层

展开临时 View实例,进入第四层57344: Object,这是 sessionid=57344的用户的被订阅信息,如果临时 View有多个用户

加入,会并列多个。上文实验 7的图 2,能清楚看到这一点。__i__: 1 代表了 instanceid = 1__c__, __n__, __p__ 同上。onchange: 之前 onopen的时候拷贝过来的那个 onchange。临时 View定义的 variable,bind 字段也应该在这一层,这里可以看出服务器没有创建

出来。第五层

展开临时 View的 sessionid=57344用户的订阅信息这里看到订阅字段_var0 对于 sessionid=57344的用户而言是”onAttached 57344”

几点说明1. 分析 template.js代码可得出结论,全局 View 和会话 View,在 ctx.open 被执行之前就已经创建出来,等待服务器的数据改变通告;临时 View,在 ctx.open 被执行之前仅仅创建了一个模板,只有等到模板上的 onopen 被调用前才真正创建出临时View实例,按 instanceid 区分挂接在模板上。

2. onchange 消息 e中 e.sessionid比较特殊,除了通告临时 View 订阅字段变化时,使用发生变化的那个成员的 sessionid外,使用当前用户的 sessionid。仔细观察上文实验 7的图二的第二行,可以看到这一情况。

3. 全局 View,会话 View的字段可以标识为:ctx[pvid].path_to_view.fieldname

临时 View的 variable,bind 字段可以标识为:ctx[pvid].path_to_view[instanceid].fieldname

临时 View的 subscribe 字段可以标识为:ctx[pvid].path_to_view[instanceid][sessionid].fieldname

在这里,path_to_view解释为 xml描述中 service 节点下的manager 节点通过 state

48

Page 49: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

节点引用的名字空间到 View的路径名。4. 使用 javascript脚本客户端,定义 View的字段时避免使用 onXXX,__XXX__命名,防止命名冲突。

最简单服务器为了实验后面各种客户端实现,这里首先提供一个足以说明问题的最简单服务器,以

及 chrome演示结果用以对比。清理掉前面实验中的所有修改,然后:MySessionView.java中:private MySessionView(SessionView.CreateParameter param) {

super(param);// bind herelong sessionid = param.getSessionId();setVar0("Hello " + sessionid);MyTemporaryView.createInstance().getMembership().add(sessionid);

}

protected void onControl(_MySessionView.control param, long sessionid) {onMessage(Integer.toString(param.var0), sessionid);

}protected void onMessage(String message, long sessionid) {

setVar0(message);}

example.html中:v100.share.MyTemporaryView.onopen = function(instanceid, memberids) {this[instanceid].onchange = this.onchange;console.log("v100.share.MyTemporaryView.onopen", this[instanceid], instanceid,

memberids);ctx.send(v100.share.MySessionView, "99999");

}注意加入的最后一句 send,临时 View onopen的时候发送一个控制触发一次改变动作。

49

Page 50: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

Java版客户端java 版本客户端支持 3种工作模式,静态模式,Variant模式,脚本模式,分别介绍。

静态模式静态模式需要使用 xmlgen生成客户端代码,是最不容易出错的模式。java -jar <path to limax.jar> xmlgen -java –noServiceXML example.client.xml–noServiceXML 参数,申明不需要生成 service-ExampleClient.xml这样的启动配置文件。

不同于服务器,多数情况下,生成的客户端框架只是应用的一部分,应用提供主函数,不会通过这个配置文件启动。接下来将会介绍如何使用手工启动。创建 eclipse项目,生成的 src,gen目录设置为源码目录,建立 limax项目的依赖关系。创建一个Main.java,逐条解释。public class Main {private final static int providerId = 100;private static void start() throws Exception {

Endpoint.openEngine();EndpointConfig config = Endpoint

.createEndpointConfigBuilder("127.0.0.1", 10000LoginConfig.plainLogin("testabc","123456", "test"))

.endpointState(example.ExampleClient.states.ExampleClient

.getDefaultState(providerId)).staticViewClasses(

example.ExampleClient.share.ViewManager.createtInstance(providerId)).build();

Endpoint.start(config, new MyListener());}

50

Page 51: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

private static void stop() {Runnable done = new Runnable() {

@Overridepublic synchronized void run() {

notify();}

};synchronized (done) {

Endpoint.closeEngine(done);try {

done.wait();} catch (InterruptedException e) {}

}}

public static void main(String args[]) throws Exception {start();Thread.sleep(2000);stop();

}}

1. 首先注意到main函数,Thread.sleep(2000);一般情况下在这个地方进入应用主循环,sleep仅仅是个示例。之前的 start()启动 Endpoint,之后的 stop结束。

2. start函数首先启动 Endpoint 引擎,然后创建配置,使用配置与一个 Listener 启动Endpoint连接服务器。a. createEndpointConfigBuilder,创建了服务器登录所需配置,参数分别是服务器

ip,端口,LoginConfig.plainLogin包装的用户名,用户 token,auany 认证模块名。

b. endpointState,直接使用依据 xml描述中声明的那个客户端 service 节点下的manager 节点引用的 state 节点生成的 service代码提供的方法即可。如果需要支持多个 PVID 提供的服务,参数逗号分隔列出各段生成代码中的相应方法。实际上,这个方法提供了对协议的支持,如果完全不使用协议,这个方法无需调用。

c. staticViewClasses,设置了静态模式下 view的管理类实例,这个管理类在生成代码时已经自动生成出来了。如果需要支持多个 PVID 提供的服务,参数逗号分隔列出各段生成代码中的相应方法。

3. stop函数用同步方式结束 Endpoint 引擎。使用 EndpointListener接收来自 Endpoint的网络交互信息。class MyListener implements EndpointListener {

public MyListener() {51

Page 52: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

}@Overridepublic void onAbort(Transport transport) throws Exception {

Throwable e = transport.getCloseReason();System.out.println("onAbort " + transport + " " + e);

}@Overridepublic void onManagerInitialized(Manager manager, Config config) {

System.out.println("onManagerInitialized "+ config.getClass().getName() + " " + manager);

}@Overridepublic void onManagerUninitialized(Manager manager) {

System.out.println("onManagerUninitialized " + manager);}@Overridepublic void onTransportAdded(Transport transport) throws Exception {

System.out.println("onTransportAdded " + transport);}@Overridepublic void onTransportRemoved(Transport transport) throws Exception {

Throwable e = transport.getCloseReason();System.out.println("onTransportRemoved " + transport + " " + e);

}@Overridepublic void onSocketConnected() {

System.out.println("onSocketConnected");}@Overridepublic void onKeyExchangeDone() {

System.out.println("onKeyExchangeDone");}@Overridepublic void onKeepAlived(int ms) {

System.out.println("onKeepAlived " + ms);}@Overridepublic void onErrorOccured(int source, int code, Throwable exception) {

System.out.println("onErrorOccured " + source + " " + code + “ “ + exception);}

}

启动服务器,运行客户端以后大致能得到这样的信息:52

Page 53: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

onManagerInitialized limax.endpoint.EndpointConfigBuilderImpl$2 limax.endpoint.EndpointManagerImplonSocketConnectedonKeyExchangeDoneonTransportAdded limax.net.StateTransportImpl (/127.0.0.1:26240-/127.0.0.1:10000)onKeepAlived 8onTransportRemoved limax.net.StateTransportImpl (/127.0.0.1:26240-/127.0.0.1:10000) java.io.IOException: channel closed manuallyonManagerUninitialized limax.endpoint.EndpointManagerImpl对照代码,逐一解释:1. Endpoint.start以后,首先调用 onManagerInitialized,这时候可以准备与网络相关的应用初始资源。对应的,最后结束的时候 onManagerUninitialized 被调用,这里可以释放那些初始资源。

2. onSocketConnected,这是一个进度指示,告知 socket连接完成。如果连接过程中失败,则 onAbort 被调用,通过 transport.getCloseReason();可以获知 abort 原因。

3. onKeyExchangeDone,这是另一个进度指示,告知与服务器的登录握手动作已经完成。如果登录过程失败,则 onErrorOccured 被调用,通过 source,code,可以获知失败原因。source,code的定义详见 limax源码中 defines.beans.xml。这之后如果发生了其它错误,比如用户被踢下线,onErrorOccured 也将被调用。

4. onErrorOccured,source为 ErrorSource.ENDPOINT时,code必为 0,exception为框架内部操作时产生的异常。

5. onTransportAdded,所有 Endpoint层面的连接初始化动作完成以后,该方法被调用,可以在 transport上收发信息了。对于 View 而言,这里是注册全局 View与会话View的 Listener的合适的地方。onTransportRemoved与之对应,连接结束的时候被调用。

6. onKeepAlived,客户端定时向服务器发送 Keepalive 消息,服务器响应以后被调用,这个方法粗略提供客户端服务器端的以毫秒为单位的往返时间。

使用 View 的关 键就 是注 册需 要的 Listener 获取 改 变 信 息 ,接 下来 修 改onTransportAddedpublic void onTransportAdded(Transport transport) throws Exception {

System.out.println("onTransportAdded " + transport);MySessionView.getInstance().registerListener(e -> System.out.println(e));

}

修改 MyTemporaryView.javaprotected void onOpen(java.util.Collection<Long> sessionids) {

// register listener hereSystem.out.println(this + " onOpen " + sessionids);registerListener(e -> System.out.println(e));try {

MySessionView.getInstance().control(99999);} catch (Exception e) {

53

Page 54: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

}}protected void onClose() {

System.out.println(this + " onClose ");}运行程序,获得类似如下的结果:onManagerInitialized limax.endpoint.EndpointConfigBuilderImpl$2 limax.endpoint.EndpointManagerImplonSocketConnectedonKeyExchangeDoneonTransportAdded limax.net.StateTransportImpl (/127.0.0.1:33440-/127.0.0.1:10000)onKeepAlived 11[class = example.ExampleClient.share.MyTemporaryView ProviderId = 100 classindex = 2 instanceindex = 1] onOpen [61440][class = example.ExampleClient.share.MyTemporaryView ProviderId = 100 classindex = 2 instanceindex = 1] 61440 _var0 Hello 61440 NEW[class = example.ExampleClient.share.MySessionView ProviderId = 100 classindex = 1] 61440 var0 Hello 61440 NEW[class = example.ExampleClient.share.MySessionView ProviderId = 100 classindex = 1] 61440 var0 99999 REPLACE[class = example.ExampleClient.share.MyTemporaryView ProviderId = 100 classindex = 2 instanceindex = 1] 61440 _var0 99999 REPLACEonTransportRemoved limax.net.StateTransportImpl (/127.0.0.1:33440-/127.0.0.1:10000) java.io.IOException: channel closed manually[class = example.ExampleClient.share.MyTemporaryView ProviderId = 100 classindex = 2 instanceindex = 1] onClose onManagerUninitialized limax.endpoint.EndpointManagerImpl

对照前面的 chrome输出可见,数据改变的通告是一致的。倒数第二行需要注意一下,chrome的输出结果里没有这一行。这不是错误,原因在于:javascript 版本严格按照服务器的通告触发消息。 java 版本中的这个 onClose 并不是来自服务器的通告,而是Endpoint关闭的时候,在所有的临时 View上调用了这个方法,目的在于与前面的onOpen 对应,提供给应用一个释放资源的机会。几个基本注意事项:1. 消息处理过程中抛出任何异常都将导致网络连接关闭,通过 EndpointListener中的

onManagerUninitialized可以获知 Endpoint结束,所有相关资源完全释放,在这之后可以重启 Endpoint。

2. 框架保证 EndpointListener中 onManagerInitialized 和 onManagerUninitialized 消息成对通告,除非 onManagerInitialized抛出异常。

3. 框架保证 EndpointListener中 onTransportAdded 和 onTransportRemoved 消息成对通告,除非 onTransportAdded抛出异常。

4. onTransportAdded,报告了网络活动的开始,各种后续数据可能已经到来,即便在54

Page 55: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

这里抛出异常终止连接,连接终止前投递的数据依然会正常通告,建议不要在这里抛异常。

5. onManagerUninitialized 消息被触发才表示所有服务器数据已经处理完毕。框架严格保证不丢失任何数据。正如上一条提到的 onTransportAdded抛出异常可能带来的问题,严格保证不丢失数据并不意味着广泛的逻辑功能适应性,某一消息的处理导致了错误,应用有责任自己忽略后续可能的无意义的数据,避免产生连带错误。

6. 临时 View的 onOpen无论是否抛出异常,onClose最终必然调用。7. 静态方式的代码也可以使用字符串 Message 作为控制发送给服务器,在这个例子中MySessionView.getInstance().control(99999); (这里的 control函数名与参数定义源于 example.share.xml中的 <control name="control">)换成:MySessionView.getInstance().sendMessage("99999");将获得一样的结果。如果需要同时支持脚本客户端,建议直接使用字符串方式,不用定义自己的 control,服务器实现起来会更加统一。

8. View 对象上提供两个版本 registerListener方法,可以注册 ViewChangedListener监测整个 View 对象的字段变化,或者个别字段的变化。 registerListener方法返回Runnable 对象,运行该对象的 run方法即可撤销注册,上面的代码都没有取消注册,原因在于 View 对象释放的时候所有的注册自动取消。

9. ViewChangedListener.onViewChanged 通告发生在框架内部的线程中,通告中的Value是 View上相应字段的引用,把 Value传递给另外的线程,另外的线程可能得不到及时的数据,原因很简单,有可能另外的线程在通过 Value访问 View的对应字段时,已经发生了另一轮通告,更新了 View。有两种方法解决这个问题。其一,如果要把 Value传递给别的线程,拷贝一份。其二,让使用数据的线程直接调度通告,方法是:创建 EndpointConfig的时候,使用 executor方法,指定一个自己的线程调度器。以 Android为例,如果多数 View数据都是 UI线程使用,那么就用 UI线程执行通告:EndpointConfig config = Endpoint

.createEndpointConfigBuilder("127.0.0.1", 10000, LoginConfig.plainLogin("testabc", "123456", "test"))

.executor(r -> runOnUiThread(r))

.endpointState(example.ExampleClient.states.ExampleClient

.getDefaultState(providerId)).staticViewClasses(

example.ExampleClient.share.ViewManager.createInstance(providerId)).build();

10. 如果用 executor 指定了自己的调度线程,例如 UI线程,那么必须理解如下几个设计要点:A. Endpoint.start必然启动一个新线程创建 Endpoint,因为不可确认当前是否是由

UI线程执行 Engine.start,如果是,在通告 onManagerInitialized 消息时将产生饥饿,因为启动过程必须严格等待 onManagerInitialized完成。(UI线程等待onManagerInitialized完成,而执行 onManagerInitialized又必须是 UI线程)

55

Page 56: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

B. 在某个 Manager上调用 close方法时,将启动新线程作异步关闭,所以通过onManagerUninitialized确认 Endpoint 活动结束是必要的,见第 5条。

C. Endpoint.closeEngine 将启动新线程,通过异步方式停止引擎,原因类似 A。通过传入一个 Runnable done,可以获知关闭完成消息,前面的例子将异步方式转换为同步方式执行,如果 done为 null,则得不到任何结束消息。使用 UI系统的环境里,可以实现一个自己的 done,设置一个标记,UI线程轮询标记获知是否完成关闭。

11. 生成的所有 View的实现代码都提供 getInstance方法获取 View 对象实例,在 View对象实例上使用 ViewVisitor 调用 visitXXX可以线程安全地访问字段数据。对于非订阅字段,如果数据不存在,ViewVisitor不会被调用;对于订阅字段,始终提供一个Map 给 ViewVisitor,Map包含了有效的 SessionId及其对应数据。

12. 不论是通告给出的 View字段数据,还是在 View对象上调用 visit 方法获得的字段数据,必须认为是只读数据,不应该修改。

Variant模式Variant模式无需生成客户端代码,但是必须明确理解 xml描述的内容,xml 调整后必

须仔细检查代码,否则容易造成错误(静态模式修改以后,重新生成客户端代码,编辑器编译器都有机会报告错误)。优点在于可以使得客户端代码相对较小。生成服务器代码的时候需要加入-variant 参数,服务器端将生成相关的支持代码,例如:java –jar <path to limax.jar> xmlgen –variant example.server.xml为了比较,直接用静态模式代码修改:首先,创建 Variant模式需要使用这样的启动配置:int providerId = 100;EndpointConfig config = Endpoint.createEndpointConfigBuilder("127.0.0.1", 10000,

LoginConfig.plainLogin("testabc",123456","test")).variantProviderIds(providerId).build();注意到,endpointState,staticViewClasses,这两个依赖客户端生成代码的方法不需要

了,多了一个 variantProviderIds,指定 PVID,如果需要支持多个 pvid,参数逗号分隔。executor这样的方法在这里也可以使用。接着修改 Listener的 onTransportAdded方法。public void onTransportAdded(Transport transport) throws Exception {

System.out.println("onTransportAdded " + transport);VariantManager manager = VariantManager.getInstance(

(EndpointManager) transport.getManager(), providerId);VariantView mySessionView = manager

.getSessionOrGlobalView("share.MySessionView");mySessionView.registerListener(e -> System.out.println(e));manager.setTemporaryViewHandler("share.MyTemporaryView",

new TemporaryViewHandler() {@Overridepublic void onOpen(VariantView view,

56

Page 57: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

Collection<Long> sessionids) {System.out.println(this + " onOpen " + sessionids);view.registerListener(e -> System.out.println(e));try {

Variant param = Variant.createStruct();param.setValue("var0", 99999);mySessionView.sendControl("control", param);

} catch (Exception e) {}

}@Overridepublic void onClose(VariantView view) {

System.out.println(view + " onClose ");}@Overridepublic void onAttach(VariantView view, long sessionid) {}@Overridepublic void onDetach(VariantView view, long sessionid,

int reason) {}

});}运行程序,获得如下结果:onManagerInitialized limax.endpoint.EndpointConfigBuilderImpl$2 limax.endpoint.EndpointManagerImplonSocketConnectedonKeyExchangeDoneonTransportAdded limax.net.StateTransportImpl (/127.0.0.1:47922-/127.0.0.1:10000)[view = share.MyTemporaryView ProviderId = 100 classindex = 2 instanceindex = 2] onOpen [61440][view = share.MyTemporaryView ProviderId = 100 classindex = 2 instanceindex = 2] 61440 _var0 Hello 61440 NEW[view = share.MySessionView ProviderId = 100 classindex = 1] 61440 var0 Hello 61440 NEWonKeepAlived 11[view = share.MySessionView ProviderId = 100 classindex = 1] 61440 var0 99999 REPLACE[view = share.MyTemporaryView ProviderId = 100 classindex = 2 instanceindex = 2] 61440 _var0 99999 REPLACEonTransportRemoved limax.net.StateTransportImpl (/127.0.0.1:47922-/127.0.0.1:10000) java.io.IOException: channel closed manually[view = share.MyTemporaryView ProviderId = 100 classindex = 2 instanceindex = 2] onClose onManagerUninitialized limax.endpoint.EndpointManagerImpl与静态模式的结果输出比较,结果一致。

57

Page 58: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

参考 onTransportAdded代码,使用 Variant模式需要理解这几些关键点:1. 首先需要获取 VariantManager,ViewManager 由 EndpointManager,PVID 共同决定。一个 EndpointManager 对应了一个网络连接,可以从 Transport 上获取,这个EndpointManager 与 onManagerInitialized 通 告 的 manager 是 同 一 个 , 但 是VariantManager本身必须在 onTransportAdded 阶段获取,原因在于,Variant模式下,所有 View的结构信息通过服务器传送过来,onTransportAdded 阶段些信息才准备完毕。前面提到过,同一连接上允许使用多个 PVID请求多个服务,从 VariantManager的获取方式可见,一个 VariantManager 对应了一个服务,管理这个服务下所有 View。

2. ViewManager 上可以直接获取全局 View 与会 话 View,可以在临时 View 上设置Handler,获取临时 View动作的通告。在 View上注册 Listener与静态方式下一致。

3. VariantView使用 Variant类型来表示所有数据类型,可以创建所有基本类型,容器类型,结构类型的 Variant表示,sendControl之前便创建了一个结构类型,作为 control 参数。事实上 Variant 提供了一系列 createXXX,getXXX,setXXX来维护数据结构,更细节的使用可以参考 javadoc文档。

4. Variant 模 式 下 同 样 可 以 向 服 务 器 对 应 View 发 送 字 符 串 消 息 , 例 如 ,mySessionView.sendMessage("99999");

5. 类似静态模式下的 View通过 visitXXX访问字段数据,在 Variant模式下可以使用字段名和 ViewVisitor 调用 VariantView的 visitField方法。

6. 特 别 需 要 注 意 代 码 中 的 字 符 串 , 必 须 与 xml 描 述 严 格 对 应 , 比如”share.MyTemporaryView”,使用了从最外层名字空间开始的 View的全名,不能简写为”MyTemporaryView”,sendControl使用的 bean的参数”var0”,对应 xml描述中的<variable name="var0" type="int"/>,也不能拼错。

7. 其它注意事项与静态 View的注意事项相同。脚本模式脚本模式支持也不需要生成代码,OracleJDK 提供了 javascript 引擎,在这里示例脚本模

式的使用。生成服务器代码的时候需要加入-script,-jsTemplate 参数,服务器端将生成相关的支持

代码,例如:java –jar <path to limax.jar> xmlgen –script -jsTemplate example.server.xml将前面的 example.html文件中的 var providers,var limax两个变量的定义拷贝出来,贴

到 example.js 文件中,将 console.log, console.err 全部替换为 print,因为 OracleJDK 的javascript 引擎没有 console这样的全局对象。最后将 example.js拷贝到 bin目录与Main.class放在同一层。

前面的MyListener代码中的方法都清理掉,留下 System.out.println,显示进度即可。使用这样的启动配置:EndpointConfig config = Endpoint

.createEndpointConfigBuilder("127.0.0.1", 10000, LoginConfig.plainLogin("testabc", "123456", "test"))

58

Page 59: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

.scriptEngineHandle(new JavaScriptHandle(new ScriptEngineManager()

.getEngineByName("javascript"),new InputStreamReader(Main.class

.getResourceAsStream("example.js")))).build();

在这里,使用了 scriptEngineHandle方法指定需要使用的脚本引擎 Handler,一个配置只能指定一个。 JavaScriptHandle 由框架示 例性提 供,参数为脚本引 擎 对象与脚本的Reader,读取脚本内容。运行程序,获得如下结果:onManagerInitialized limax.endpoint.EndpointConfigBuilderImpl$2 limax.endpoint.EndpointManagerImplonSocketConnectedonKeyExchangeDoneonTransportAdded limax.net.StateTransportImpl (/127.0.0.1:53610-/127.0.0.1:10000)v100.share.MyTemporaryView.onopen [object Object] 9 61440v100.share.MyTemporaryView.onchange [object Object] 61440 _var0 Hello 61440 NEWv100.share.MySessionView.onchange [object Object] 61440 var0 Hello 61440 NEWonKeepAlived 151v100.share.MySessionView.onchange [object Object] 61440 var0 99999 REPLACEv100.share.MyTemporaryView.onchange [object Object] 61440 _var0 99999 REPLACEonTransportRemoved limax.net.StateTransportImpl (/127.0.0.1:53610-/127.0.0.1:10000) java.io.IOException: channel closed manuallylimax close nullonManagerUninitialized limax.endpoint.EndpointManagerImpl

这个输出结果与前面几个一致。使用脚本模式,需要明确:1. 如果需要使用 lua脚本,那么就应该参照 JavaScriptHandle,包装第三方的 java 版

lua 引 擎 实 现 ScriptEngineHandle 接 口 。 实 际 上 , ScriptEngineHandle 与limax.js,limax.lua的操作模式相对应。

2. 其它注意事项与静态 View的注意事项相同。三种模式的总结

1.正确选择服务器代码生成参数,Variant模式使用”-variant”,脚本模式使用”-script”。

2.客户端的配置决定使用哪种模式,静态模式使用 staticViewClasses;Variant模式使用 variantProviderIds;脚本模式使用 scriptEngineHandle。如果服务器不支持客户端 选择的模式,将导致协商错误,通过 EndpointListener的消息 onErrorOccured 报告错误 PROVIDER_UNSUPPORTED_VARINAT与 PROVIDER_UNSUPPORTED_SCRIPT。

59

Page 60: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

3.静态模式使用 endpointState支持协议,Variant模式,脚本模式不支持协议。4.服务器可以同时运行在 3种模式下,同一应用,可以有不同的客户端实现。5.客户端框架出于完备性考虑,同一应用可以同时运行在 3种模式下,这样使用没有实际意义,根据需求选择一种即可。

C#客户端C#客户端同样支持静态模式,Variant模式和脚本模式。这里介绍静态模式和 Variant模

式。以下代码以 VS2013 控制台项目举例。

静态模式1. 首先应该创建 C#解决方案和应用项目,确定应用项目的源码目录 sdir,执行如下生成命令:

2. java -jar <path to limax.jar> xmlgen –c# –noServiceXML –outputPath sdir example.client.xml

3. 源码目录里面将看见两个新建目录 xmlsrc与 xmlgen。xmlgen无需提交到版本控制系统。

4. 按照生成目录结构,在项目内建立同样的目录结构,然后使用添加现有项操作手工添加所有生成文件,以后的新的生成文件也必须手工添加。

5. 将 C#版本的 limax项目添加到解决方案中。6. 为应用项目添加引用,引用到 limax项目。最后形成大致如下的解决方案树结构。

60

Page 61: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

class Program{

private const int providerId = 100; private static void start() { Endpoint.openEngine(); EndpointConfig config = Endpoint.createEndpointConfigBuilder(

"127.0.0.1", 10000,LoginConfig.plainLogin("testabc", "123456", "test")).staticViewClasses(

example.ExampleClient.share.ViewManager.createInstance(providerId)).build();

Endpoint.start(config, new MyListener()); } private static void stop() { object obj = new object(); Action done = () => { lock (obj) { Monitor.Pulse(obj); } }; lock (obj) {

61

Page 62: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

Endpoint.closeEngine(done); Monitor.Wait(obj); } } static void Main(string[] args) { start(); Thread.Sleep(2000); stop(); }

}

这个主函数代码看起来和 java 版本无区别。 class MyListener : EndpointListener { public MyListener() { } public void onAbort(Transport transport) { Exception e = transport.getCloseReason(); Console.WriteLine("onAbort " + transport + " " + e); } public void onManagerInitialized(Manager manager, Config config) { Console.WriteLine("onManagerInitialized " + config.GetType().Name + " " + manager); } public void onManagerUninitialized(Manager manager) { Console.WriteLine("onManagerUninitialized " + manager); } public void onTransportAdded(Transport transport) { Console.WriteLine("onTransportAdded " + transport); MySessionView.getInstance().registerListener(e => Console.WriteLine(e)); } public void onTransportRemoved(Transport transport) { Exception e = transport.getCloseReason(); Console.WriteLine("onTransportRemoved " + transport + " " + e); } public void onSocketConnected()

62

Page 63: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

{ Console.WriteLine("onSocketConnected"); } public void onKeyExchangeDone() { Console.WriteLine("onKeyExchangeDone"); } public void onKeepAlived(int ms) { Console.WriteLine("onKeepAlived " + ms); } public void onErrorOccured(int source, int code, Exception exception) { Console.WriteLine("onErrorOccured " + source + " " + code + “ “ + exception); }

}除了 ViewChangedEvent,内部用属性的方法实现访问,其它方面也无区别。MyTemporaryView.java 内部实现:override protected void onOpen(ICollection<long> sessionids) {

Console.WriteLine(this + " onOpen " + sessionids);registerListener(e => Console.WriteLine(e));MySessionView.getInstance().control(99999);

}override protected void onClose() { Console.WriteLine(this + " onClose"); }

编译运行程序,结果:onManagerInitialized DefaultEndpointConfig EndpointManagerImplonSocketConnectedonKeyExchangeDoneonTransportAdded limax.net.StateTransportImpl[class = MyTemporaryView ProviderId = 100 classindex = 2 instanceindex = 6] onOpen

System.Collections.Generic.HashSet`1[System.Int64][class = MyTemporaryView ProviderId = 100 classindex = 2 instanceindex = 6] 61440 _var0

Hello 61440 NEW[class = MySessionView ProviderId = 100 classindex = 1] 61440 var0 Hello 61440 NEWonKeepAlived 47[class = MySessionView ProviderId = 100 classindex = 1] 61440 var0 99999 REPLACE[class = MyTemporaryView ProviderId = 100 classindex = 2 instanceindex = 6] 61440 _var0

99999 REPLACEonTransportRemoved limax.net.StateTransportImpl System.Exception: channel closed

manually[class = MyTemporaryView ProviderId = 100 classindex = 2 instanceindex = 6] onClose

63

Page 64: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

onManagerUninitialized EndpointManagerImpl

跟前面各版本均一致。Variant模式

修改 EndpointConfig的定义为:EndpointConfig config = Endpoint.createEndpointConfigBuilder(

"127.0.0.1", 10000,LoginConfig.plainLogin("testabc", "123456", "test")).variantProviderIds(100).build();

使用 variantProviderIds 提供 PVID 参数。修改 onTransportAdded为:private class MyTemporaryViewHandler : TemporaryViewHandler

{ private readonly VariantView mySessionView; public MyTemporaryViewHandler(VariantView v) { mySessionView = v; } public void onOpen(VariantView view, ICollection<long> sessionids) { Console.WriteLine(view + " onOpen " + sessionids); view.registerListener(e => Console.WriteLine(e)); Variant param = Variant.createStruct(); param.setValue("var0", 99999); mySessionView.sendControl("control", param); } public void onClose(VariantView view) { Console.WriteLine(view + " onClose"); } public void onAttach(VariantView view, long sessionid) { } public void onDetach(VariantView view, long sessionid, int reason) { } } public void onTransportAdded(Transport transport) { Console.WriteLine("onTransportAdded " + transport); VariantManager manager = VariantManager.getInstance((EndpointManager)transport.getManager(), providerId); VariantView mySessionView = manager.getSessionOrGlobalView("share.MySessionView"); mySessionView.registerListener(e => Console.WriteLine(e)); manager.setTemporaryViewHandler("share.MyTemporaryView", new MyTemporaryViewHandler(mySessionView)); }

和 Java 版本比起来,除了 C#不支持通过匿名类直接创建对象,不得不定义MyTemporaryViewHandler外,没有任何区别。

64

Page 65: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

编译运行程序,结果:onManagerInitialized DefaultEndpointConfig EndpointManagerImplonSocketConnectedonKeyExchangeDoneonTransportAdded limax.net.StateTransportImpl[view = share.MyTemporaryView ProviderId = 100 classindex = 2 instanceindex = 8] onOpen System.Collections.Generic.HashSet`1[System.Int64][view = share.MyTemporaryView ProviderId = 100 classindex = 2 instanceindex = 8] 61440 _var0 Hello 61440 NEW[view = share.MySessionView ProviderId = 100 classindex = 1] 61440 var0 Hello 61440 NEWonKeepAlived 38[view = share.MySessionView ProviderId = 100 classindex = 1] 61440 var0 99999 REPLACE[view = share.MyTemporaryView ProviderId = 100 classindex = 2 instanceindex = 8] 61440 _var0 99999 REPLACEonTransportRemoved limax.net.StateTransportImpl System.Exception: channel closed manually[view = share.MyTemporaryView ProviderId = 100 classindex = 2 instanceindex = 8] onCloseonManagerUninitialized EndpointManagerImpl跟前面各版本均一致。

C#版本总结C#版本客户端除了少数语言特性带来的差异外,使用方法与注意事项均与 Java 版本一

致。框架提供的类库结构,直接参考 javadoc文档即可。C#异常规范不够完备,处理各种 EndpointListener 消息时,建议不要抛出任何异常。框架使用了.NET3.5的 C#语言特性,由于网络认证过程使用了 Diffie-Hellman协议作密

钥交换,需要 BigInteger 支持。如果只能使用 .NET3.5,就应该另找一个 .NET4.0以上的BigInteger 库引用进来。

另外,limax.util包下提供了两个辅助方法,runOnUiThread与 uiThreadSchedule,便于应用在自己的 UI 线程中调度 View 消 息通告。创建配置的时候 .executor(r => runOnUiThread),然后在自己的 UI线程空闲时调用 uiThreadSchedule 即可。C++客户端

C++客户端支持静态模式,Variant模式。脚本模式通过与 Lua 库集成实现 Lua支持,这个单独介绍。

C++客户端使用 C++11标准实现,最大限度保持和 java 版本,C#版本一致。C++库在各常用平台上提供支持(g++,ndk,clang),这里以 VS2013 控制台项目举例。静态模式

1. 首先应该创建 C++解决方案和应用项目,确定应用项目的源码目录 sdir,执行如下生成命令:

2. java -jar <path to limax.jar> xmlgen –c++ –noServiceXML –outputPath sdir example.client.xml

65

Page 66: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

3. 源码目录里面将看见两个新建目录 xmlgeninc与 xmlgensrc。xmlgeninc无需提交到版本控制系统。

4. 按照生成目录结构,在项目内建立同样的目录结构,然后使用添加现有项操作手工添加所有生成的.cpp文件,以后的新生成的.cpp文件也必须手工添加。为了编辑器查看方便建议.h 也添加进来。

5. 将 C++版本的 limax项目添加到解决方案中。6. 为应用项目添加引用,引用到 limax项目。7. 编辑项目属性,将 limax项目的 include目录作为附加包含目录。

主函数代码:#include "stdafx.h"#include <iostream>#include "limax.h"#include "xmlgeninc/xmlgen.h"using namespace limax;class MyApp : public EndpointListener{

const int providerId = 100;public:

MyApp(){

Endpoint::openEngine();auto config = Endpoint::createEndpointConfigBuilder(

"127.0.0.1", 10000, LoginConfig::plainLogin("testabc", "123456", "test"))->staticViewCreatorManagers(

{ example::getShareViewCreatorManager(providerId) })->endpointState({ example::getExampleClientStateClient(providerId) })->build();

Endpoint::start(config, this);}~MyApp(){

std::mutex mutex;std::condition_variable_any cond;std::lock_guard<std::mutex> l(mutex);Endpoint::closeEngine([&](){

std::lock_guard<std::mutex> l(mutex);cond.notify_one();

});cond.wait(mutex);

}void run(){ Sleep(2000); }void onManagerInitialized(EndpointManager*,EndpointConfig*) { std::cout <<

"onManagerInitialized" << std::endl; }66

Page 67: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

void onManagerUninitialized(EndpointManager*) { std::cout << "onManagerUninitialized" << std::endl; }

void onTransportAdded(Transport*) {std::cout << "onTransportAdded" << std::endl;View* mysessionview = example::ExampleClient::share::MySessionView::getInstance();mysessionview->registerListener([](const ViewChangedEvent &e){std::cout <<

e.toString() << std::endl; });}void onTransportRemoved(Transport*){ std::cout << "onTransportRemoved" << std::endl; }void onAbort(Transport*) { std::cout << "onAbort" << std::endl; }void onSocketConnected() { std::cout << "onSocketConnected" << std::endl; }void onKeyExchangeDone() { std::cout << "onKeyExchangeDone" << std::endl; }void onKeepAlived(int ping) { std::cout << "onKeepAlived " << ping << std::endl; }void onErrorOccured(int source, int code, const std::string& info) { std::cout <<

"onErrorOccured " << source << " " << code << " " << info << std::endl; }void destroy() {}

};

int _tmain(int argc, _TCHAR* argv[]){

MyApp().run();return 0;

}与 java 版本 C#版本比起来除语言特性外无太大差异。onErrorOccured稍有差别,当

source为 SOURCE_ENDPOINT时,code 对应那些 SYSTEM类型错误,info 报告了具体错误内容,source/code的定义参见 endpoint.h头文件。另外MyApp类中多了一个方法 destroy。具体原因下面介绍。share.MyTemporaryView.cpp:#include "share.MyTemporaryView.h"#include "../xmlgeninc/views/share.MySessionView.h"

#include <iostream>using namespace limax;namespace example { namespace ExampleClient { namespace share {

void MyTemporaryView::onOpen(const std::vector<int64_t>& sessionids) {std::cout << toString() << " onOpen";for (auto &l : sessionids)

std::cout << " " << l;std::cout << std::endl;

67

Page 68: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

registerListener([](const ViewChangedEvent &e){std::cout << e.toString() << std::endl; });

MySessionView::getInstance()->control(99999);}void MyTemporaryView::onAttach(int64_t sessionid) {}void MyTemporaryView::onDetach(int64_t sessionid, int reason){

if (reason >= 0){

//Application Reason}else{

//Connection abort Reason}

}void MyTemporaryView::onClose() {

std::cout << toString() << " onClose " << std::endl;}

} } }

这个看起来差异也不大,注意一下,因为使用了 MySessionView,所以需要的头文件share.MySessionView.h 必须自己 手 工 include。另外,同一目录下生成的头文件share.MyTemporaryView.h中,可以添加需要的成员变量,成员方法。运行结果:onManagerInitializedonSocketConnectedonKeyExchangeDoneonTransportAdded[class = example.ExampleClient.share.MyTemporaryView ProviderId = 100 classindex = 2 instanceindex = 78] onOpen 61440[class = example.ExampleClient.share.MyTemporaryView ProviderId = 100 classindex = 2 instanceindex = 78] 61440 _var0 00A3BCBC NEWonKeepAlived 15[class = example.ExampleClient.share.MySessionView ProviderId = 100 classindex = 1] 61440 var0 0088585C NEW[class = example.ExampleClient.share.MySessionView ProviderId = 100 classindex = 1] 61440 var0 0088585C REPLACE[class = example.ExampleClient.share.MyTemporaryView ProviderId = 100 classindex = 2 instanceindex = 78] 61440 _var0 00A3BCBC REPLACE

68

Page 69: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

onTransportRemoved[class = example.ExampleClient.share.MyTemporaryView ProviderId = 100 classindex = 2 instanceindex = 78] onCloseonManagerUninitialized

这个结果看起来与前面各版本均一致,只不过 View的值被显示为了一个地址了,这是因为消息通告 ViewChangedEvent的定义中,value的类型只能是 void*。Variant模式修改构造函数,创建 Variant模式下的配置

auto config = Endpoint::createEndpointConfigBuilder("127.0.0.1", 10000, LoginConfig::plainLogin("testabc", "123456", "test"))

->variantProviderIds({ providerId })->build();

修改 onTransportAdded方法:class MyTemporaryViewHandler : public TemporaryViewHandler{

VariantView* mySessionView;public:

MyTemporaryViewHandler(VariantView* v) : mySessionView(v) {}virtual void onOpen(VariantView* view, const std::vector<int64_t>& sessionids){

std::cout << view->toString() << " onOpen";for (auto &l : sessionids)

std::cout << " " << l;std::cout << std::endl;view->registerListener([](const VariantViewChangedEvent &e){std::cout <<

e.toString() << std::endl; });Variant param = Variant::createStruct();param.setValue("var0", 99999);mySessionView->sendControl("control", param);

}virtual void onClose(VariantView* view){

std::cout << view->toString() << " onClose " << std::endl;}virtual void onAttach(VariantView* view, int64_t sessionid) {}virtual void onDetach(VariantView* view, int64_t sessionid, int reason) {}virtual void destroy() { delete this; }

};void onTransportAdded(Transport*transport) {

std::cout << "onTransportAdded" << std::endl;

69

Page 70: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

VariantManager* manager = VariantManager::getInstance(transport->getManager(), providerId);

VariantView* mySessionView = manager->getSessionOrGlobalView("share.MySessionView");

mySessionView->registerListener([](const VariantViewChangedEvent &e){std::cout << e.toString() << std::endl; });

manager->setTemporaryViewHandler("share.MyTemporaryView", new MyTemporaryViewHandler(mySessionView));

}这 段 代 码 看 起 来 和 C# 版 本 非 常 类 似 , 除 了 语 言 特 性 差 异 。 另 外

MyTemporaryViewHandler类中多了一个方法 destroy。具体原因下面介绍。运行结果:onManagerInitializedonSocketConnectedonKeyExchangeDoneonTransportAdded[view = share.MyTemporaryView ProviderId = 100 classindex = 2 instanceindex = 79] onOpen 61440[view = share.MyTemporaryView ProviderId = 100 classindex = 2 instanceindex = 79] 61440 _var0 Hello 61440 NEWonKeepAlived 16[view = share.MySessionView ProviderId = 100 classindex = 1] 61440 var0 Hello 61440 NEW[view = share.MySessionView ProviderId = 100 classindex = 1] 61440 var0 99999 REPLACE[view = share.MyTemporaryView ProviderId = 100 classindex = 2 instanceindex = 79] 61440 _var0 99999 REPLACEonTransportRemoved[view = share.MyTemporaryView ProviderId = 100 classindex = 2 instanceindex = 79] onCloseonManagerUninitialized

这里的结果就能看到实际的 value了,因为 Variant知道一些类型信息。各种注意事项1. 主函数代码必须包含头文件 limax.h,使用静态模式需要额外包含生成的头文件

xmlgeninc/xmlgen.h。修改生成代码时,如果需要使用别的生成代码,需要哪些包含哪些。参见前面对 share.MyTemporaryView.cpp的修改。

2. 为了简单起见,limax 库代码只使用 limax 名字空间,不再作进一步的划分,所以 using namespace limax;

3. 创建配置时,staticViewCreatorManagers,endpointState,variantProviderIds的参数使用 C++11 特性的初 始 化 列表作为参数,可以支持多个。同 C#一样,同样提 供runOnUiThread与 uiThreadSchedule两个辅助方法方便使用,->executor([](Runnable r){ runOnUiThread(r); })。

70

Page 71: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

4. 由库提供出来的各种指针 EndpointManager*,Trasnport*,以及各种形式的 View*,用户可以直接使用不允 许 delete。EndpointManager 的寿命到 onManagerUninitialized 为止;Transport以及全局 View,会话 View的寿命到 onTransportRemoved为止;临时 View的寿命到临时 View自身 onClose为止,如果一定需要持有这些指针,必须严格注意寿命问题,尤其是通过 lambda表达式的闭包引用更要小心。

5. 需要应用提供对象的情况下,对象应该由用户自己创建出来提供指针交由库使用,并且提供自己的 destroy方法,前面的MyApp::destroy 和 MyTemporaryViewHandler::destroy就是典型情况。这是由于库内部使用的 new/delete与应用本身的 new/delete可能不配对。

6. 如果使用 Objective-C++,生成静态模式代码时使用-oc 参数代替-c++参数,确保生成需要的.mm文件。

Lua/C++客户端1. 生成服务器代码的时候,通过-luaTemplate生成 lua模板代码 example.lua,java –jar

<path to limax.jar> xmlgen –script –luaTemplate example.server.xml2. 客户端首先创建 C++解决方案和应用项目3. 解决方案中加入 Limax源码中 cpp目录下的 limax项目,lua目录下的 limax.lua项目,

liblua项目。4. 为应用项目添加引用,引用上述 3个项目。5. 编辑应用项目属性,将 limax源码目录,lualib头文件目录,lualib源码目录,3个目录作为附加包含目录添加进来。

主函数代码:#include "stdafx.h"#include <limax.h>#include <iostream>#include <lua.hpp>#include <limax.lua.h>using namespace limax;class MyLuaApp : public EndpointListener{

lua_State* L;public:

MyLuaApp(){

L = luaL_newstate();luaL_openlibs(L);int e = luaL_dofile(L, "callback.lua");if (e != LUA_OK){

std::cout << "lua load 'callback.lua' failed! " << lua_tostring(L, -1) << std::endl;

71

Page 72: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

exit(-1);}auto sehptr = LuaCreator::createScriptEngineHandle(L, -1, false, [this](int s, int e, const

std::string& m){ onErrorOccured(s, e, m); });if (!sehptr)

exit(-1);lua_pop(L, 1);Endpoint::openEngine();auto config = Endpoint::createEndpointConfigBuilder(

"127.0.0.1", 10000, LoginConfig::plainLogin("testabc", "123456", "test"))->scriptEngineHandle(sehptr)->build();

Endpoint::start(config, this);}~MyLuaApp(){

std::mutex mutex;std::condition_variable_any cond;std::lock_guard<std::mutex> l(mutex);Endpoint::closeEngine([&](){

std::lock_guard<std::mutex> l(mutex);cond.notify_one();

});cond.wait(mutex);

}void run(){ Sleep(2000); }void onManagerInitialized(EndpointManager*, EndpointConfig*) { std::cout <<

"onManagerInitialized" << std::endl; }void onManagerUninitialized(EndpointManager*) { std::cout << "onManagerUninitialized"

<< std::endl; }void onTransportAdded(Transport*) { std::cout << "onTransportAdded" << std::endl; }void onTransportRemoved(Transport*){ std::cout << "onTransportRemoved" << std::endl; }void onAbort(Transport*) { std::cout << "onAbort" << std::endl; }void onSocketConnected() { std::cout << "onSocketConnected" << std::endl; }void onKeyExchangeDone() { std::cout << "onKeyExchangeDone" << std::endl; }void onKeepAlived(int ping) { std::cout << "onKeepAlived " << ping << std::endl; }void onErrorOccured(int errorsource, int errorvalue, const std::string& info) { std::cout <<

"onErrorOccured " << errorsource << " " << errorvalue << " " << info << std::endl; }void destroy() {}

};int _tmain(int argc, _TCHAR* argv[]){

MyLuaApp().run();return 0;

72

Page 73: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

}

EndpointListener 消息代码与其它 C++版本无区别。主要差别就在构造函数初始化引擎之前初始化了 Lua 虚拟机,在虚拟机上装载了脚本代码callback.lua,创建了脚本引擎的 handler,在创建配置的时候设置进来。析构函数在结束引擎之后结束 Lua 虚拟机。callback.lua:callback.lua来自生成服务器时生成的 example.lua代码,为了实现例子的功能,添加了ctx.send一行。 v100.share.MyTemporaryView.onopen = function(this, instanceid, memberids) this[instanceid].onchange = this.onchange print('v100.share.MyTemporaryView.onopen', this[instanceid], instanceid, memberids) ctx.send(v100.share.MySessionView, "99999") end

这里可以看出来,使用上除了语言特性与 javascript 版本没有差异。这里需要注意一下 callback.lua的放置路径。如果在控制台中运行,应该放置在 exe 所在目录;如果在 VS2013中运行,放置在项目目录下。

启动服务器,运行客户端,获得结果:onManagerInitializedonSocketConnectedonKeyExchangeDoneonTransportAddedv100.share.MyTemporaryView.onopen table: 00674C50 3 table: 00674C00v100.share.MyTemporaryView.onchange table: 00674C50 61440 _var0 Hello 61440 NEWonKeepAlived 16v100.share.MySessionView.onchange table: 00674840 61440 var0 Hello 61440 NEWv100.share.MySessionView.onchange table: 00674840 61440 var0 99999 REPLACEv100.share.MyTemporaryView.onchange table: 00674C50 61440 _var0 99999 REPLACEonTransportRemovedlimax closeonManagerUninitialized

这个结果看起来和前面版本均一致,除了 table: XXXXXXXX,lua的对象均为 table。这里print没有解析出来。各种注意事项:1. 头文件顺序,头文件 limax.lua.h必须包含在 lua.hpp之后。2. 这里为了示例在 openEngine之前创建了 lua 虚拟机。具体项目应用中,应该直接使用应用提供的虚拟机,保证脚本中 View的 onXXX中实现的控制动作能够直接驱动应用逻

73

Page 74: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

辑。3. 创建配置时,一般需要调用.executor方法指定应用期望的线程。Lua/C#客户端1. 生成服务器代码的时候,通过 -luaTemplate生成 lua模板代码 example.lua, java -jar <path to limax.jar> xmlgen -script -luaTemplate example.server.xml2. 客户端首先创建 C#解决方案和应用项目3. 解决方案中加入 Limax源码中 csharp目录下的 limax项目, lua目录下的 luacs项目clrlua项目,liblua_s项目4. 为应用项目添加项目引用,引用 limax项目,luacs项目,clrlua项目。5. 项目属性中将当前项目的目标平台由 AnyCPU 改为 x86,与其它几个保持一致,均为x86。程序代码using System;using System.Text;using System.IO;using System.Threading;using limax.net;using limax.script;using limax.endpoint;using limax.endpoint.script;

namespace ConsoleApplicationLuaCS{ class MyListener : EndpointListener { public void onAbort(Transport transport) { Exception e = transport.getCloseReason(); Console.WriteLine("onAbort " + transport + " " + e); } public void onManagerInitialized(Manager manager, Config config) { Console.WriteLine("onManagerInitialized " + config.GetType().Name + " " + manager); } public void onManagerUninitialized(Manager manager) { Console.WriteLine("onManagerUninitialized " + manager); } public void onTransportAdded(Transport transport)

74

Page 75: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

{ Console.WriteLine("onTransportAdded " + transport); } public void onTransportRemoved(Transport transport) { Exception e = transport.getCloseReason(); Console.WriteLine("onTransportRemoved " + transport + " " + e); } public void onSocketConnected() { Console.WriteLine("onSocketConnected"); } public void onKeyExchangeDone() { Console.WriteLine("onKeyExchangeDone"); } public void onKeepAlived(int ms) { Console.WriteLine("onKeepAlived " + ms); } public void onErrorOccured(int source, int code, Exception exception) { Console.WriteLine("onErrorOccured " + source + " " + code); } }

class Program { private static void start() { string callback = File.ReadAllText("callback.lua", Encoding.UTF8); Endpoint.openEngine(); EndpointConfig config = Endpoint.createEndpointConfigBuilder(

"127.0.0.1", 10000,LoginConfig.plainLogin("testabc", "123456", "test")).scriptEngineHandle(

new LuaScriptHandle(new Lua((string msg)=>Console.WriteLine(msg)), callback))

.build(); Endpoint.start(config, new MyListener()); } private static void stop() { object obj = new object();

75

Page 76: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

Action done = () => { lock (obj) { Monitor.Pulse(obj); } }; lock (obj) { Endpoint.closeEngine(done); Monitor.Wait(obj); } } static void Main(string[] args) { start(); Thread.Sleep(2000); stop(); } }}

实际上,这个代码与 C#其它 版本代码并无太大差别,除了创建配置时使用了LuaScriptHandle,这一点也可以与 Java 版本的脚本模式作比较。callback.lua:callback.lua来自生成服务器时生成的 example.lua代码,为了实现例子的功能,添加了ctx.send一行。 v100.share.MyTemporaryView.onopen = function(this, instanceid, memberids) this[instanceid].onchange = this.onchange print('v100.share.MyTemporaryView.onopen', this[instanceid], instanceid, memberids) ctx.send(v100.share.MySessionView, "99999") end

这里可以看出来,这个 callback.lua与 Lua/C++版本完全相同。这里需要注意一下 callback.lua的放置路径。应该放置在 exe 所在目录。启动服务器,运行客户端,获得结果:onManagerInitialized DefaultEndpointConfig EndpointManagerImplonSocketConnectedonKeyExchangeDoneonTransportAdded limax.net.StateTransportImplv100.share.MyTemporaryView.onopen table: 048E98F0 31 table: 048E9940v100.share.MyTemporaryView.onchange table: 048E98F0 36864 _var0 Hello 36864 NEWv100.share.MySessionView.onchange table: 048E6820 36864 var0 Hello 36864 NEWonKeepAlived 10v100.share.MySessionView.onchange table: 048E6820 36864 var0 99999 REPLACEv100.share.MyTemporaryView.onchange table: 048E98F0 36864 _var0 99999 REPLACEonTransportRemoved limax.net.StateTransportImpl System.Exception: channel closed manually

76

Page 77: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

limax close nilonManagerUninitialized EndpointManagerImpl

这个结果看起来和前面版本均一致。Javascript/C++客户端 (SpiderMonkey)

准备 javascript 引擎库1. https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Releases/45,下载 javascript 引擎源码包。

2. hBuild_Documentation,按照该文档描述的流程创建二进制包。3. 源 码 目 录 中 编 辑 js/public/CharacterEncoding.h 文 件 , 查 找

UTF8CharsToNewTwoByteCharsZ 方法,将 前缀的 extern TwoByteCharsZ,修 改为JS_PUBLIC_API(TwoByteCharsZ),这个方法在 limax 提 供的粘合代码中是必要的。SpiderMonkey API 提供了 javascript 串到 UTF8 串的转换,除了这个方法没有任何从UTF8 串转换回 javascript 串的方法,应该是个缺陷。

4. 之后的例子需要使用参数 --target=x86_64-pc-mingw32 --host=x86_64-pc-mingw32创建x64 版本的包,同时需要添加参数--disable-jemalloc,否则 vs2013 编译出来的版本在程序退出时出错,如果当前的 vs2013 版本编译时 cl 报告内部错误,则需要将 vs2013升级到最新版本。此外,注意源码包展开之后的 modules目录,如果内部包含的目录名为 src,必须修改成 zlib,至少,45.0.2 版本存在这个 bug。

5. 二进制包创建完成之后,将操作步骤中建立的 build_OPT.OBJ目录,连同目录名整个拷贝到 limax源码目录中的 javascript/SpiderMonkey/mozjs45目录下。

6. 二进制包编译成 DEBUG 版本,应用必须编译成 DEBUG 版本;二进制包编译成 RELEASE版本,应用也必须编译成 RELEASE 版本,否则应用可能在运行时出现 CRT 错误。

开发示例(跟之前的例子一样,还是使用 vs2013)1. 将前面的 example.html文件中的 var providers,var limax两个变量的定义拷贝出来,贴到 example.js文件中,将 console.log,console.err全部替换为 print。

2. 创建 C++解决方案和控制台应用项目,应用项目的平台配置为 x64。3. 解决方案中加入 Limax源码中 cpp目录下的 limax项目,javascript/SpiderMonkey目录下的 limax.js项目。

4. 添加当前项目的引用,引用 limax,limax.js两个项目。5. 正确配置当前应用项目的头文件查找路径,包括 javascript 引擎的头文件查找路径(参

考 limax.js 的 配 置 ) , Limax 源 码 目 录 下 的 cpp/limax/include 以 及 javascript/SpiderMonkey/include

6. 正确配置库查找路径和依赖库,(参考 limax.js的配置)程序代码#include "stdafx.h"#include <limax.h>

77

Page 78: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

#include <iostream>#include <jsapi.h>#include <js/Conversions.h>#include <limax.js.h>using namespace limax;

class MyJsApp : public EndpointListener{

std::shared_ptr<JsEngine> engine;public:

MyJsApp(){

engine = JsEngine::create(1048576);engine->execute([&](JSRuntime* rt, JSContext* cx, JS::HandleObject global){

JS_SetErrorReporter(rt, [](JSContext *cx, const char *message, JSErrorReport *report){

runOnUiThread([message, report](){fprintf(stderr, "%s:%u:%s\n", report->filename ? report->filename : "[no

filename]", (unsigned int)report->lineno, message);});

});JS_DefineFunction(cx, global, "print", [](JSContext *cx, unsigned argc, JS::Value

*vp){JS::CallArgs args = JS::CallArgsFromVp(argc, vp);std::string s;for (unsigned i = 0; i < argc; i++){

char *p = JS_EncodeString(cx, JS::RootedString(cx, JS::ToString(cx, args[i])));

s += p;s += ' ';JS_free(cx, p);

}if (s.length() > 0)

s.pop_back();runOnUiThread([s](){ puts(s.c_str()); });args.rval().setUndefined();return true;

}, 0, JSPROP_READONLY | JSPROP_PERMANENT);});auto sehptr = JsCreator::createScriptEngineHandle(engine, "example.js");Endpoint::openEngine();

78

Page 79: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

auto config = Endpoint::createEndpointConfigBuilder("127.0.0.1", 10000, LoginConfig::plainLogin("testabc", "123456", "test"))

->scriptEngineHandle(sehptr)->build();

Endpoint::start(config, this);}~MyJsApp(){

std::mutex mutex;std::condition_variable_any cond;std::lock_guard<std::mutex> l(mutex);Endpoint::closeEngine([&](){

std::lock_guard<std::mutex> l(mutex);cond.notify_one();

});cond.wait(mutex);

}void run() { for (int i = 0; i < 200; i++) { Sleep(10); uiThreadSchedule(); } }void onManagerInitialized(EndpointManager*, EndpointConfig*) { std::cout <<

"onManagerInitialized" << std::endl; }void onManagerUninitialized(EndpointManager*) { std::cout << "onManagerUninitialized"

<< std::endl; }void onTransportAdded(Transport*) { std::cout << "onTransportAdded" << std::endl; }void onTransportRemoved(Transport*){ std::cout << "onTransportRemoved" << std::endl; }void onAbort(Transport*) { std::cout << "onAbort" << std::endl; }void onSocketConnected() { std::cout << "onSocketConnected" << std::endl; }void onKeyExchangeDone() { std::cout << "onKeyExchangeDone" << std::endl; }void onKeepAlived(int ping) { std::cout << "onKeepAlived " << ping << std::endl; }void onErrorOccured(int errorsource, int errorvalue, const std::string& info) { std::cout <<

"onErrorOccured " << errorsource << " " << errorvalue << " " << info << std::endl; }void destroy() {}

};

int _tmain(int argc, _TCHAR* argv[]){

{MyJsApp().run();

}uiThreadSchedule();return 0;

}

如果通过 vs2013运行程序,需要将之前创建的 example.js放置到项目目录下,如果在控制台中运行,应该放置到 exe 所在目录。

79

Page 80: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

启动服务器,运行客户端,获得结果:onManagerInitializedonSocketConnectedonKeyExchangeDoneonTransportAddedonKeepAlived 1v100.share.MyTemporaryView.onopen [object Object] 3 36864v100.share.MyTemporaryView.onchange [object Object] 36864 _var0 Hello 36864 NEWv100.share.MySessionView.onchange [object Object] 36864 var0 Hello 36864 NEWv100.share.MySessionView.onchange [object Object] 36864 var0 99999 REPLACEv100.share.MyTemporaryView.onchange [object Object] 36864 _var0 99999 REPLACEonTransportRemovedonManagerUninitializedlimax close

这个运行结果与之前的版本均一致。代码说明1. JsEngine::create(1048576)创建了 javascript 引擎,使用 1M 内存,必须根据应用规模估计合适的内存大小,内存不足可能引发 javascript 虚拟机 OutOfMemory。

2. JsEngine通过线程服务器的方式包装了 SpiderMonkey 引擎,提供两个方法。execute 和wait。execute 提交 javascript任务给 JsEngine 立即返回,wait 提交给 javascript任务给JsEngine,等待该任务执行完成后返回。JsEngine 释放前,保证所有通过 execute 提交的任务全部执行完毕。

3. JsEngine创建之后立即安装了 javascript 引擎的 ErrorHandle;在全局空间中添加了 print方法,print方法在 example.js中用来输出结果。

4. 注 意代码中的几处 runOnUIThread 调用,显 示结果通过 UI 线程输出,而不是在JsEngine线程中输出。

5. 主线程被作为 UI线程使用,注意 MyJsApp.run方法,这和前面的代码稍有区别,不是直接 Sleep 2000ms,而是每 10ms就通过 uiThreadSchedule()调度一次,保证 JsEngine线程提交过来的输出方法被执行。_tmain函数的实现和之前的代码也稍有区别,这样的实现保证了"limax close"这一行能够正确输出,原因在于,MyJsApp.run 执行完毕,MyJsApp 对象析构时 Endpoint.closeEngine,导致 example.js中的 ctx.close()被执行,UI线程任务队列中被安排了 print任务,最后需要再调度一次 UI线程,完成这些任务。

80

Page 81: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

Javascript/C#客户端 (SpiderMonkey)

参考前一节准备 javascript 引擎库开发示例1. 将前面的 example.html文件中的 var providers,var limax两个变量的定义拷贝出来,贴到 example.js文件中,将 console.log,console.err全部替换为 print。

2. 创建 C#解决方案和控制台应用项目,应用项目的平台配置为 x64。3. 解决方案中加入 Limax源码中 csharp目录下的 limax项目,javascript/SpiderMonkey目录下的 jscs,clrjs,nativejs项目。

4. 添加当前项目的引用,引用 limax,jscs,clrjs 三个项目。程序代码using System;using System.IO;using System.Text;using System.Threading;using limax.net;using limax.script;using limax.endpoint;using limax.endpoint.script;

namespace ConsoleApplicationJsCS{ class MyListener : EndpointListener { public void onAbort(Transport transport) { Exception e = transport.getCloseReason(); Console.WriteLine("onAbort " + transport + " " + e); } public void onManagerInitialized(Manager manager, Config config) { Console.WriteLine("onManagerInitialized " + config.GetType().Name + " " + manager); } public void onManagerUninitialized(Manager manager) { Console.WriteLine("onManagerUninitialized " + manager); } public void onTransportAdded(Transport transport) {

81

Page 82: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

Console.WriteLine("onTransportAdded " + transport); } public void onTransportRemoved(Transport transport) { Exception e = transport.getCloseReason(); Console.WriteLine("onTransportRemoved " + transport + " " + e); } public void onSocketConnected() { Console.WriteLine("onSocketConnected"); } public void onKeyExchangeDone() { Console.WriteLine("onKeyExchangeDone"); } public void onKeepAlived(int ms) { Console.WriteLine("onKeepAlived " + ms); } public void onErrorOccured(int source, int code, Exception exception) { Console.WriteLine("onErrorOccured " + source + " " + code); } }

class Program { private static void start(JsContext jsc) { string init = File.ReadAllText("example.js", Encoding.UTF8); Endpoint.openEngine(); EndpointConfig config = Endpoint.createEndpointConfigBuilder(

"127.0.0.1", 10000, LoginConfig.plainLogin("testabc", "123456", "test")).scriptEngineHandle(new JavaScriptHandle(jsc, init)).build();

Endpoint.start(config, new MyListener()); } private static void stop() { object obj = new object(); Action done = () => { lock (obj) { Monitor.Pulse(obj); } }; lock (obj) {

82

Page 83: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

Endpoint.closeEngine(done); Monitor.Wait(obj); } } static void Main(string[] args) { JsContext jsc = new JsContext(); start(jsc); Thread.Sleep(2000); stop(); jsc.shutdown(); } }}

将之前创建的 example.js 和 javascript/SpiderMonkey/mozjs45/build_OPT.OBJ/dist/bin/下的所有 dll放置到 exe 所在目录。启动服务器,运行客户端,获得结果:onManagerInitialized DefaultEndpointConfig EndpointManagerImplonSocketConnectedonKeyExchangeDoneonTransportAdded limax.net.StateTransportImplv100.share.MySessionView.onchange [object Object] 36864 var0 Hello 36864 NEWv100.share.MyTemporaryView.onopen [object Object] 190 36864v100.share.MyTemporaryView.onchange [object Object] 36864 _var0 Hello 36864 NEWonKeepAlived 12v100.share.MySessionView.onchange [object Object] 36864 var0 99999 REPLACEv100.share.MyTemporaryView.onchange [object Object] 36864 _var0 99999 REPLACEonTransportRemoved limax.net.StateTransportImpl System.Exception: channel closed manuallylimax close nullonManagerUninitialized EndpointManagerImpl这个运行结果与之前的版本均一致。代码说明1. 受限于 SpiderMonkey的线程模型,不得不提供一个 JsContext包装类,JsContext创建了一个线程与 Js 操作对象关联,Js 操作对象支持 C#与 Javascript交互,详见附录 CLR/Javascript一节。所有操作完成以后,需要 shutdown JsContext 对象。

客户端开发总结1. Limax客户端库覆盖了大多数常见开发环境。

83

Page 84: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

2. 线程的使用上,多数情况下需要在创建配置时调用 .executor设置应用自己的消息通告线程,所有 EndpointListener 消息,View 消息均通告给该线程。

3. 服务器端 View的改变,可以在各种形式的客户端上驱动 Listener,报告 View数据变动这里要注意一个问题,同一 View的同一字段上如果注册多个 Listener,Listener的通告顺序与注册顺序之间关系没有保证。

4. 再次强调,静态模式,Variant模式,脚本模式,在各种类型化语言实现中都能混合使用。混合使用的实际意义不大,而且可能导致开发上的混乱,除非有非常必要的理由,不建议如此使用。

高级特性为了提升应用性能,View 提供了一系列高级特性,扩展了 View的概念,定义了字段

采集的概念,实现了数据裁剪。实际应用环境下,通过裁剪数据可以大大节省网络带宽,防止网络拥塞。是否能够裁剪,取决于逻辑功能的设计,比如,使用了数据裁剪,时序问题自然就失去了讨论的意义,所以严格时序的应用,不应该进行裁剪。基本概念数据采集

View的发送数据时机到达时进行的字段信息收集。过程采集

采集的结果是从上一次采集结束算起到当前时间点为止的所有更新历史,两次采集时间范围内,同一字段更新了多少次,就有多少个结果,如果两次 update的数据没有变化,第二次被简化为一个 Touch。集合采集(Limax基本方式)

集合采集只应用于过程采集,集合采集将过程采集器聚合起来,内部过程采集器每一次收集的数据与过程采集器本身的作为一个二元组,被顺序记录下来,所以集合采集可以保证采集字段之间的更新顺序。状态采集(Limax 扩展方式)

采集的结果是最近的一次更新结果,之前的数据全部废弃掉,状态采集的字段独立于其它字段,不存在时序上的概念,也不生成 Touch。逻辑功能许可的前提下,比起过程采集,可以过滤掉大量数据,节省了网络带宽。Immutable

非易变属性,View描述中的 Variable元素对应了具有非易变属性的字段,Bind 引用的84

Page 85: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

Xbean中定义的map,set类型之外的字段也具有非易变属性,这样的字段不能修改,只能被原子的替换。非易变的字段,只能使用非易变采集器采集数据。Mutable

易变属性,View描述中的 Bind元素对应了具有易变属性的字段,Bind 引用中的 Xbean中定义的map,set类型的字段也具有易变属性,这样的字段可以被修改,例如,map可以在上面直接增删,而 map 对象本身的引用并没有改变。易变的字段,多数情况下使用易变采集器采集数据,特殊情况下也可以使用非易变采集器数据,例如,map 尽管可以直接增删内容,但是整体的替换也是允许的。易变属性的使用使得数据的局部更新成为可能,只发送改变的数据,有利于节省网络带宽。Immutable,Mutable 由系统根据配置自动选择,无需用户干预。Tick

字段数据采集的周期,对于集合采集,一个周期内的所有变化,连同顺序被采集出来;对于状态采集,最后一个状态被采集出来。xml描述扩展

<view name="TestTempView" lifecycle="temporary" tick="20"><variable name="var1" type="int" clip="true" /><bind name="bindfirst" table="roles" clip="true" snapshot="true" /><subscribe name="_var1" ref="gs.for_session.firstview.var1" snapshot=”true”/>

</view>View元素的 tick属性,对于全局 View无意义。View元素内,variable,bind两个元素

上,提供了两个属性,clip 和 snapshot;临时 View的 subscribe 字段只提供 snapshot属性。tick属性配置 SessionView 和 TemporaryView生成代码的 tick,上面设置为 20,默认为 10ms,程

序内能够通过 setTick 修改。clip属性定义了 clip 属性, gen 目录下 _<ViewName>.java 中将生成一个 protected boolean

permitXXX(Type p)函数,默认返回 true。src目录的<ViewName>.java文件里,可以重载该函数,根据应用功能要求决定返回

true或者 false。如果返回 false,XXX 字段的修改将被忽略掉。比如,通过 View 提供一个可丢弃的数据源,可以在这个点上应用令牌桶之类的算法进

行流量控制。与其它的 View方法不同的是,permitXXX方法不一定在 View 对应的线程中调用,所以

使用该方法需要注意线程安全性,传入的参数可以为过滤提供参考,但是不可修改,不可存储,除非另外拷贝一份。

85

Page 86: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

3种 View的描述中都可以应用 clip。特别的,Bind元素如果定义了 clip属性,生成的代码将选择 Immutable 采集方式,这

是因为,工作在Mutable方式下,可能进行数据的部分更新,这样的更新如果被丢弃,将破坏服务器,客户端的数据一致性。例如,一个 Xbean包含两个字段 A,B,第一次更新A,第二次更新 B,如果第一次更新被丢弃,第二次更新发送给客户端,这种情况下,服务器,客户端看到的 A 字段必然不一致。然而,只要用 Immutable方式对待这个 Xbean,每次都发送全部数据,这种情况下,即使第一次更新被丢弃,第二次更新也包含了之前的全部修改。snapshot属性声明该字段为一个状态采集字段,关联一个状态采集器进行管理。对于全局 View,snapshot的设定没有意义,全局 View的数据发送时间点由用户自己决

定,发送的都是最新一个版本的数据,所以全局 View的字段都使用状态采集器。更进一步,Mutable 采集决定的局部更新对于全局 View的字段也没有意义,最终,全局 View的所有字段都选择 Immutable状态采集器。

SessionView的字段采集方式与对应的临时 View的订阅字段的采集方式不相干,例如,TV._a 订阅了 SV.a,SV.a 采用过程采集,TV._a 采用状态采集没有任何问题,实现上同一份更新数据被提交给两个 View各自的字段采集器上,最终输出结果取决于采集器类型。没有定义 snapshot属性的字段集合被提交给一个集合采集器管理。

采集器选择1. 对于 SessionView,临时 View:

Variable Bind Subscribe Subscribe/snapshot="trueVariable Bind Variable Bind

PCS PCS PCS PCS ISC MSCnapshot="true" ISC MSC PCS PCS ISC MSCclip="true" PCS PCS PCS PCS ISC ISCsnapshot="true",clip="true" ISC ISC PCS PCS ISC ISC

第一列表示 Variable/Bind上定义的属性,最后两个 Subscribe 列只对临时 View有意义,用是否定义 snapshot属性区分两种情况,临时 View上 Subscribe关联的采集器,每个成员一份。PCS:集合采集器,每次添加的数据被排队。ISC:Immutable状态采集器,每次添加数据,使用新值简单替换前一个值。MSC:Mutable状采集器,每次添加数据,新值归并到前一个值中。实际采集过程为:首先读取 PCS,然后逐个读取 ISC/MSC,对于临时 View的订阅字段,为本 tick 内进行过订阅字段的更新的成员,逐个执行上述过程。2. 全局 View 所有字段,全部使用 ISC。

86

Page 87: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

大数据集合广播全局 View 扩展

提供一个保护方法 protected void onUpdate(String varname);重载以后可以获知某个字段已经发生了变化,这个地方提供一个 syncToClient的机会,将数据发送给用户。不过,需要注意的是,异步触发方式发送出去的数据可能不是当前更新的数据,而是

最新的数据。这是因为全局 View上的操作,都统一调度到到全局 View 对象自身的线程上,例如,同一字段上连续更新同一字段两次,通过 onUpdate触发发送,setA之后 setB,setA触发的 syncToClientA 将放到 setB之后,所以发送的数据为 B,B触发的 syncToClientB还是发送 B。这个例子从另一个角度说明了全局 View的字段属于状态采集字段。松散临时 View

如果临时 View的定义中没有包含任何订阅,生成代码中可以看见 createLooseInstance()方法,该方法可以创建松散临时 View。松散临时 View 将标准临时 View的Membership同步功能裁剪掉,在操作大规模Membership的情况下可以提高广播性能,减小网络开销。

MyTemporaryView tview = MyTemporaryView.createLooseInstance();服务器端看,松散临时 View与标准临时 View在使用上没有差别。从客户端看,Membership的变动被裁减掉。所以,onOpen 被调用时传入空 sessionid

列表;onAttach,onDetach方法永远不会被调用。如上面的例子,异步方式的全局 View 广播,需要仔细控制时序,否则可能导致不合预

期的结果,如果是同步方式,不使用 onUpdate,则不存在这样的问题,比如执行序列setA,syncToClientA,setB,syncToClientB,4个任务会正确排序到全局 View线程上。异步方式,又必须保证按正确的序列发送,相应发送字段采用了过程采集方式的松散临时 View是一个比较好的选择。状态采集方式下,全局 View的用户可控程度更高,Membership 集合非常大的情况下,

可以将 Membership分割成多个片段,每个片段之间插入一些延迟逐个发送,可以有效防止网络拥塞。设计上必须注意,大数据集广播的频率一定不能太高,否则数据总量决定了网络拥塞

还是难以避免。分区临时 View

生 成 代 码 中 可 以 看 见 createInstance(int partition); 方 法 , 如 果 存 在createLooseInstance(),createLooseInstance(int partition);也会同时存在。用分区的方式对 Membership进行划分,总是选择包含较少成员的分区接受新成员,

所以分区成员数量相对均衡。每个分区维护一个私有的数据投递队列。每次 Tick,进行一次数据采集,往队列追加数据,刷新当前分区队列,最后将当前分

区切换成下一个分区。对于集合采集,采集的数据被追加到所有分区的队列中,所有的数据将会发送给所有

用户,只不过时机不同。对于状态采集,采集数据仅追加到当前分区队列,每次 Tick获取的状态数据,只发送

87

Page 88: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

给当前分区的这一部分用户。普通临时 View是只包含 1个分区的分区临时 View的特例。对于任何一个用户,等效延迟为 tick * partition,如果设计决定单个用户最长接受 TICK

延迟,创建分区临时 View之后,应该 view.setTick(TICK/partition);从原理可以推论,使用分区临时 View,可以减少突发性的网络拥塞。

比较1. 大数据集广播,需要通告成员信息的情况下,从网络负担的角度看,分区临时 View 总是好于普通临时 View,不过为了进行分区,CPU,内存开销会更大。

2. 大数据集广播,不需要通告成员信息的情况下,可以按下表判断。 全局 View 松散临时 View 松散分区临时 View

过程采集 差,根本不适合 中 好状态采集 很好,可控性高 中 好,自动控制

总结1. 使用 snapshot/clip属性定义能够有效裁减数据传输,减少带宽占用2. 过程采集需要保留所有历史数据,tick到达时发送,所以 tick延迟越长,内存开销越大。3. Mutable状态采集需要保留所有历史修改信息,在 tick到达时归并发送,所以 tick延迟越长内存开销越大,并且突发的 cpu占用也越大。

4. Immutable状态采集是最节省内存和 cpu的方式——直接丢弃之前的数据,也不存在归并需求。

5. 对于结构简单,数据量较小的的 Xbean,有时候可以利用 clip的副作用,将本来属于Mutable 采集的 Bind 字段强制改变成 Immutable 采集,减少一些 cpu 和内存的开销。

6. 一般情况下有效 tick(也就是 tick*partition)决定了内存占用,有效 tick越大,内存占用越多;有效 tick 内,分区越多,cpu占用越多——需要进行 partition 次处理。

7. 大多数情 况下,只要确认了某个字 段可以接受状态数据,那么修 改 xml,设置snapshot属性,适当调整 tick,无需修改任何代码,即可有效提升性能。

极简模式客户端Limax 扩展了Websocket协议,支持极简模式客户端的实现。极简模式客户端可以看作

是脚本模式客户端将 原生协议替换为扩 展 Websocket 协议的结果,简化掉EndpointListener,同时又进行了相应的增强,概念上更贴近 HTML5浏览器。Limax 提供的Websocket服务器在同一 websocket配置端口上,同时支持极简模式客户端与 HTML5浏览器。

88

Page 89: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

客户端示例这里延续客户端开发一节中的示例。C++,C#,Java 版本的实现在概念上完全相同,这

里仅示例 C++版本实现。#include "stdafx.h"#include <limax.h>#include <iostream>#include <lua.hpp>#include <limax.lua.h>#include <set>#include <octets.h>using namespace limax;class MyLuaApp{

lua_State* L;public:

MyLuaApp(){

L = luaL_newstate();luaL_openlibs(L);int e = luaL_dofile(L, "callback.lua");if (e != LUA_OK){

std::cout << "lua load 'callback.lua' failed! " << lua_tostring(L, -1) << std::endl;exit(-1);

}auto sehptr = LuaCreator::createScriptEngineHandle(L, -1, false, [this](int s, int e, const

std::string& m){ onErrorOccured(s, e, m); });if (!sehptr)

exit(-1);lua_pop(L, 1);Endpoint::openEngine();Endpoint::start("127.0.0.1", 10001, "testabc", "123456", "test", sehptr);

}~MyLuaApp(){

std::mutex mutex;std::condition_variable_any cond;std::lock_guard<std::mutex> l(mutex);Endpoint::closeEngine([&](){

std::lock_guard<std::mutex> l(mutex);cond.notify_one();

89

Page 90: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

});cond.wait(mutex);

}void run() { Sleep(2000); }void onErrorOccured(int errorsource, int errorvalue, const std::string& info) { std::cout <<

"onErrorOccured " << errorsource << " " << errorvalue << " " << info << std::endl; }};

int _tmain(int argc, _TCHAR* argv[]){

MyLuaApp().run();return 0;

}

可以类比客户端开发->Lua/C++客户端一节,MyLuaApp无需再实现 EndpointListener接口,此外,直接使用方法 Endpoint.start的极简模式客户端版本启动连接,无需再创建相应EndpointConfig。启动服务器,运行客户端,获得结果v100.share.MyTemporaryView.onopen table: 00403000 2 table: 00403028v100.share.MyTemporaryView.onchange table: 00403000 36864 _var0 Hello 36864 NEWv100.share.MySessionView.onchange table: 003D5360 36864 var0 Hello 36864 NEWv100.share.MyTemporaryView.onchange table: 00403000 36864 _var0 99999 REPLACEv100.share.MySessionView.onchange table: 003D5360 36864 var0 99999 REPLACElimax close 0

这个结果与之前的各个版本均一致。实现流程启动 Endpoint 引擎Java

void Endpoint.openEngine()

C# void Endpoint.openEngine()C++ void Endpoint::openEngine()

使用应用脚本创建 ScriptEngineHandle

Java

类 limax.endpoint.script.JavaScriptHandle

C# 类 limax.endpoint.script.LuaScriptHandle类 limax.endpoint.script.JavaScriptHandle

90

Page 91: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

C++ 4个版本的 limax::LuaCreator::createScriptEngineHandle方法4个版本的 limax::JsCreator::createScriptEngineHandle方法

上表中列举了 Limax 当前支持的 ScriptEngineHandle实现。启动 Endpoint连接Java

limax.util.Closeable Endpoint.start(String host, int port, String username, String token, String platflag, ScriptEngineHandle handle, Executor executor)

C# limax.util.Closeable Endpoint.start(string host, int port, string username, string token, string platflag, ScriptEngineHandle handle, Executor executor)

C++ std::shared_ptr<limax::util::Closeable> Endpoint::start(const std::string& host, short port, const std::string& username, const std::string& token, const std::string& platflag, ScriptEngineHandlePtr handle)

1. host, port, username, token, platflag, 指定了连接的 Switcher服务器与登录参数。2. handle为上一步创建的 ScriptEngineHandle3. 脚本在 executor 指定的执行器中执行,C++版本中,脚本必定在 UI线程中执行,所以无需该参数。

4. 返回的 Closeable 对象上执行 close,效果等同于浏览器上点击 STOP 按钮,立刻关闭连接,脚本 context上安装的 onclose方法被调用。onclose获得的消息串格式为,“[宿主语言错误信息描述]<空格><错误码>”。

5. 网络连接在任何情况下断开,脚本 context上安装的 onclose方法被调用。在这之后调用返回的 Closeable 对象上的 close方法,没有任何效果,事实上,内部的连接对象已经被释放。这种实现与浏览器一致,完成装载,或者装载失败的页面上,点击 STOP 按钮,没有任何效果。

6. 应该注意到,所有这些启动方法均不提供连接超时的支持,如果需要支持短于操作系统默认的连接超时,必须应用实现自己的 timer,超时以后在返回的 Closeable 对象上执行 close 即可。这种实现当然也与浏览器一致,浏览器上找不到任何连接超时这样的底层配置。

结束 Endpoint 引擎Java

void Endpoint.closeEngine(Runnable done)

C# void Endpoint.closeEngine(Action done)C++ void Endpoint::openEngine(limax::Runnable done)

引擎内所有活动的 Endpoint连接将自动被关闭。

与脚本模式客户端,HTML5客户端的比较极简模式客户端 脚本模式客户端 HTML5客户端

承载协议 Websocket/Ext Limax Native Protocol Websocket

安全性 支持 通过运行时配置支持 支持/https,不支持/http

91

Page 92: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

压缩 支持 通过运行时配置支持 不支持脚本支持 ScriptEngineHandle ScriptEngineHandle javascript

Provider间数据交换Limax通过客户端隧道方式支持信任 Provider间安全数据交换,具体的数据内容由

Provider自己解释。基本框架

客户端隧道通常的服务器间数据交换需要在服务器间建立网络连接,与客户端隧道方式有很大不同,这里作一比较。

客户端隧道 服务器直连大量数据 不适合 适合服务器防火墙 不需要 服务器网络拓扑越复杂,防火墙越

复杂,现实运营环境下,网络与服务器分属不同部门管理,防火墙配置随服务器配置调整通常不现实,更不用说跨组织机构的协同了。

服务器信任关系 通过证书实现 也可以通过证书,没有实现的版本客户数据传输 简单。客户端连通两端服务器

后可以执行即时传输。客户端也可以暂存数据,执行非即时传输。

复杂。服务器间连接采用传输时连接,还是持久连接是两难问题。客户端终究需要介入传输过 程的协调,需要复杂流程。

92

Page 93: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

数据安全1. 通过证书方式划定信任域,信任域内的服务器可以安全交换数据。详见附录《PKIX支持》《Key分发系统》

2. 通过隧道的数据通过外部数据方式编码。详见附录《外部数据》数据标签1. 隧道数据的交换过程是一个异步过程2. 客户端服务器(可能是所有参与服务器)首先需要执行协商过程,协商结果产生了隧道交换 Session。完整的理解,label就是一次隧道交换过程的 SessionId。

3. 实际上,绝大多数应用,不需要那么复杂的交换行为,可以重新解释 label,简化应用开发。例如,标记应用数据的类型,直接决定目标 ProviderID,等等。

数据交换基本流程1. 客户端服务器协商,决定信任域,决定数据标签,在服务器端打包应用数据。这一步

骤取决于应用实现。2. 服务器发送数据标签和隧道数据。3. 客户端收到数据标签和保护过的隧道数据,根据协商结果,转发数据或者暂存之后转发。

4. 接收数据的服务器用数据标签与解码后的应用数据执行异步回调。回调过程取决于应用实现。

服务器开发隧道异常隧道操作时可能出现各种异常,用 limax.provider.TunnelException表示,这些异常分为 4种类型,使用枚举 limax.provider.TunnelException.Type描述,分别是1. NETWORK,网络操作时出现异常。2. CODEC,编解码异常,通常是数据被篡改。3. EXPIRE,隧道数据过期,源服务器将过期时间编码到被保护数据中,目的服务器用自

己的当前时间校验该过期时间,失败抛出这一异常。4. LABEL,标签异常,源服务器明文传送标签时,同时将标签编码到被保护数据中,如果客户端执行转发时修改了标签,目的服务器校验时将抛出这一异常。

TunnelException.getType() 可以获取异常的类型。对于 NETWORK,CODEC类型,可以通过TunnelException.getException()获取导致 TunnelException的异常。发送数据完成客户端服务器的协商,准备好信任域、数据标签、应用数据之后,通过 4个可选方法发送数据。1. limax.provider.View.tunnel(long sessionid, java.net.URI group, int label, Octets data) throws

93

Page 94: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

TunnelException;2. limax.provider.View.tunnel(long sessionid, int label, Octets data) throws TunnelException;3. limax.provider.SessionView.tunnel(java.net.URI group, int label, Octets data) throws

TunnelException;4. limax.provider.SessionView.tunnel(int label, Octets data) throws TunnelException;第一个方法为基础版本,sessionid决定了参与隧道的客户端,group为信任域(见附录《Key分发系统》),label为数据标签,data为应用数据。SessionView上保存有客户端的sessionid,所以 SessionView上的方法省略了 sessionid 参数。调用缺省 group的方法时使用服务器配置的 defaultGroup 作为信任域,详见运营配置。隧道与 View 没有任何联系,之所以把隧道方法放在 View 上,完全是为了编 程方便。Provider 编程时,所有操作都应该在 View上完成,包括协商过程。在特定的 View上完成协商,准备好参数之后,直接在该 View上执行 tunnel 即可。接收数据Provider实现 limax.provider.ProviderListener时,并列实现 limax.provider.TunnelSupport接口。public interface TunnelSupport {

void onTunnel(long sessionid, int label, Octets data) throws Exception;default void onException(long sessionid, int label, TunnelException exception) throws

Exception {throw exception;

}}实现 onTunnel 方法,即可收到客户端转发过来的标签与应用数据。 onTunnel 方法在sessionid 对应的线程上调度,调用同一用户相关的方法是线程安全的。解码数据的过程可能抛出 CODEC,EXPIRE,LABEL类型的 TunnelException,这样的异常可以通过实现 onException方法截获。默认的 onException方法重新抛出这个异常,将导致 Provider记录错误日志,并且使用错误码 ErrorCodes.PROVIDER_TUNNEL_EXCEPTION关闭与客户端之间的连接。注意事项不支持 TunnelSupport的 Provider 也可以发送隧道数据,这样的 Provider接收到隧道数据后记录错误日志,使用错误码 ErrorCodes.PROVIDER_TUNNEL_EXCEPTION关闭与客户端之间的连接。客户端开发所有类型客户端均支持隧道数据转发。C++/C#/Java 客户端,以 Java 版本为例实现 limax.endpoint.EndpointListener的同时,并列实现 limax.endpoint.TunnelSupport接口。public interface TunnelSupport {

94

Page 95: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

void onTunnel(int providerid, int label, Octets data) throws Exception;void registerTunnelSender(TunnelSender sender);

}public interface TunnelSender {

void send(int providerid, int label, Octets data)throws InstantiationException, SizePolicyException, CodecException,

ClassCastException;}实现 onTunnel,即可收到服务器发送过来的隧道数据。Limax框架决定了,一个 Endpoint可以同时连接多个 Provider,这里的 providerid进行了区分。onTunnel可以实现为即时转发,也可以暂存数据(暂存时长不可超过配置的有效期),以后转发。Endpoint 初始化时,将回调 registerTunnelSender,必须实现 registerTunnelSender 将当前Endpoint的 tunnelSender 保存下来,之后在该 tunnelSender上转发隧道数据。通常 情 况下,客户端需要使用两个 Endpoint, EA, EB 分别连接隧道两边的Provider,EA.onTunnel收到隧道数据时,通过 EB.tunnelSender 转发。这 里 必 须 当 心 , TunnelSupport.onTunnel 中 的 providerid 是 源 Provider 的 id , TunnelSender.send中的 providerid是目的 Provider的 id,不能像 label,data一样直接转发。不支持 TunnelSupport的 Endpoint,忽略接收到的隧道数据。脚本客户端,以 Javascript 版本为例脚本客户端在创建 Limax实例时,将一个函数作为第三个参数传入,即可支持隧道数据接收。var limax = Limax(function(ctx) {ctx.onerror = function(e) {

print('limax error', e);}ctx.onclose = function(e) {

print('limax close', e);}ctx.onopen = function() {

.........................}}, cache, function(pvid, label, data) {});limax实例本身就是一个函数,直接执行 limax(pvid, label, data); 即可发送隧道数据。使用了 ScriptEngineHandle的混合客户端(Java/javscript,C#/javascript,C#/Lua,C++/

javascript,C++/Lua,各种形式的极简模式客户端),以 Java/javascript为例使用带有 TunnelReceiver 参数的构造函数创建相应的 ScriptHandle,即可实现隧道数据的接收。new JavascriptHandle(new ScriptEngineManager().getEngineByName("javascript"),

new InputStreamReader(Main.class.getResourceAsStream("example.js")),

95

Page 96: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

null, null,new TunnelReceiver() {

@Overridepublic void onTunnel(int providerid, int label, String data) {

System.out.println(providerid + " " + label + " " + data);}

});这里的 data来自脚本系统所以类型为 String,实际上,这是 Octets类型隧道数据的 Base64编码结果。注意观察 ScriptEngineHandle接口的定义public interface ScriptEngineHandle {

.......................void tunnel(int providerid, int label, String data) throws Exception;void tunnel(int providerid, int label, Octets data) throws Exception;

}ScriptEngineHandle上直接调用 tunnel方法即可发送隧道数据。来自于脚本系统的隧道数据使用带有 String 参数的方法发送,来自 TunnelSupport的隧道数据使用带有 Octets 参数的方法发送。注意:1. 如 果 客 户 端 的 EndpointListener 实 现 了 TunnelSupport 接 口 , 又 使 用 了 支 持

TunnelReceiver的 ScriptEngineHandle,同一份隧道数据,两处都能接收到。2. 如果客户端的 EndpointListener实现 TunnelSupport时记录了 TunnelSender,又使用了

ScriptEngineHandle,两处都可以用来发送隧道数据。运行管理配置文件Provider配置文件中,Provider 节点内,需要添加一个 Tunnel 节点。<Provider ...> <Tunnel compressor="ZIP" keyProtector="HMACSHA256" defaultExpire="86400000" defaultGroup="prjA"> <PKIX location="pkcs12:/work/keyalloc.p12" passphrase="123456" revocationCheckerOptions="SOFT_FAIL"/> <SharedKey group="prjA" key="123456" /> <SharedKey group="prjB" key="123456" /> <Expire group="prjA" value="3600"/> </Tunnel> <Manager .../> <Manager .../> ...

96

Page 97: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

</Provider>Tunnel 节点 4个属性compressor:源数据压缩方法,可选值 NONE,RFC2118,ZIP,默认为 NONE。参见附录《外部数据》keyProtector : 源 数 据 保 护 算 法 , 可 选 值HMACSHA224,HMACSHA256,HMACSHA384,HMACSHA512,TripleDESCBC,AESCBC128,默认为 HMACSHA256。参见附录《外部数据》defaultExpire:隧道数据默认过期时间,单位毫秒,默认 3600000。过期时间的配置必须小于 key寿命,参见附录《Key分发系统》defaultGroup:默认信任域。Tunnel 节点下支持 2种节点,PKIX与 SharedKey,PKIX 节点具有高优先级,配置了 PKIX 节点SharedKey 节点被忽略。配置了多个 PKIX 节点,第一个节点有效,PKIX 节点配置了 Key分发系统客户端,请求 Key分发网络分配 Key 保护隧道数据。PKIX 节点 5个属性。location:provider 证书包的 location。passphrase:location的私钥启用密码,实际运营时,不应该填写该属性,而应该在服务器启动时输入。trustsPath:信任证书路径, location中已经包含了派生该证书的 ROOTCA, 如果要信任其它ROOTCA(或者当前 ROOTCA 即将过期,应该将新的 ROOTCA放置在这里) , 则需要配置trustsPath, trustsPath可以是一个文件, 也可以是一个目录, 如果是目录, 遍历一层, 获取文件, 文件允许任何证书格式, 包括 CER, DER, PKCS7, PKCS12, KeyStore, 从中获取 ROOTCA,忽略所有错误。revocationCheckerOptions:服务器检测对方证书回收状态的选项,可选的值为 DISABLE, NO_FALLBACK, ONLY_END_ENTITY, PREFER_CRLS, SOFT_FAIL, 大小写不敏感,后 4个参数的解释见 java.security.cert.PKIXRevocationChecker.Option,DISABLE具有最高优先级,如果配置了DISABLE,其它配置无意义。httpsHost:指定 key分发网络中特定的入口服务器 IP或者域名,不配置该属性,使用provider 证书中提供的域名。PKIX 节点配置初始化之后,从 Provider 证书中提取支持的信任域集合校验 defaultGroup配置,如果配置了 defaultGroup,defaultGroup必须在集合中存在,否则抛出运行时异常;如果没有配置 defaultGroup,在集合中挑选一个作为 defaultGroup。不存在 PKIX 节点的情况下,使用 SharedKey 节点的配置,SharedKey 节点允许多个,静态配置了信任域和相应的 key。SharedKey配置方式不使用 Key分发系统,用于调试环境,或者极其简单的应用场景。SharedKey 节点 2个属性group:信任域名称key:信任域 key所有 SharedKey 配置初 始 化之后,生成了静态的信任域集合,如果配置了

97

Page 98: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

defaultGroup,defaultGroup必须在集合中存在,否则抛出运行时异常;如果没有配置defaultGroup,在集合中挑选一个作为 defaultGroup。Expire 节点 2个属性group:信任域名称value:过期时间Expire 节点允许精细配置特定信任域的隧道数据过期时间。运行时注意事项1. 运行 ntp服务,同步 provider时钟2. 配置正确的 dns服务。3. 按照 CA的要求,配置 trustsPath,添加 CA 提供的额外 ROOTCA。4. 开启防火墙,许可 443目的端口,确保与 CAServer,Key分发网络的连通性。高级话题应用数据组织1. Provider间数据交换框架不考虑具体的应用数据,专注于转发 Octets2. 信任域内 Provider,应该设计私有的可交换数据结构,使用 xml描述生成交换用的公共

bean是理想方式。3. 如果存在多种公共 bean,应该设计类型字段予以区分,简单的情况下,可以考虑利用

label 区分类型。4. 使用 JSON表示用户数据也是一种可行的选择,JSON 串上可以统一使用 UTF8 编码

getBytes,然后wrap为 Octets。5. 应用数据尺寸可能较大的情况,配置压缩。隧道数据尺寸1. 隧道数据通过特定协议发送。2. 出于服务器的抗攻击考虑,虚拟机参数 limax.net.Engine.limitProtocolSize,限制了最大协议大小 1M,隧道数据尺寸不可超出这一约束,否则,目的服务器解析协议时将出错。

3. 客户端信任服务器,所以客户端不控制数据尺寸。4. 对于大量数据,应该考虑多次发送的方式。隧道谁的数据1. 仔细规划应用自己的协商过程,决定隧道谁的数据。2. 多数情况,通过隧道转发用户自己的数据。3. 特定情况下,可以通过没有利害冲突的第三方传送。这时,真实的 sessionid打包在应用数据中,解码获得该 sessionid 之后如果需要线程安全地处理,应该使用View.schedule(sessionid, task)将后续任务调度到 sessionid 对应线程上。

98

Page 99: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

4. 出于规避防火墙的考虑,可以设计专用客户端,部署在连通性良好的网络中,转发 Provider间数据。

Limax Http

绝大多数服务器应用或多或少需要提供一些 HTTP服务,Limax服务框架也不例外。sun从 JDK6开始就提供了一个简单的服务器开发包 com.sun.net.httpserver。实际使用会发现这个包的功能太弱,所以提供开发包 limax.http 作为替代。

limax.http支持 HTTP/1.1(兼容 HTTP/1.0),支持 HTTP/2,支持WebSocket(从 HTTP/1.1 upgrade,HTTP/2隧道两种方式均支持),支持 Server-Sent Events。提供一个比 Servlet更加简单有效的服务架构(Servlet已经经历了 4个版本,越来越复杂)。服务实现无需关心当前到底运行在 HTTP/1.1 环境下还是 HTTP/2 环境下,如果条件具备,

自动选择 HTTP/2。HTTP服务架构public interface HttpHandler extends Handler {

DataSupplier handle(HttpExchange exchange) throws Exception;default void censor(HttpExchange exchange) throws Exception {

throw new UnsupportedOperationException();}

}

1. 客户端请求完成之后 HttpHandler的 handle方法被调度,服务代码在 handle中编写。2. HttpHandler的 censor方法用于审查上传进度,对于 GET请求,该方法决不会被调用。审查失败,该方法抛出异常,立即终止网络连接。默认实现禁止 POST请求。

3. POST请求至少调度一次 censor,第一次调度可以获得完整的请求头,上传数据导致后续调度。POST请求的审查详见高级话题。

4. exchange.getFormData()获得 FormData 对象,FormData的 getData()方法可以获取解析之后的 query数据,即便是 GET请求的 query 也被解析出来。返回类型为 Map<String, List<Object>>,其中 Object可能是 String,可能是 List<ByteBuffer>。 String 对应了串查询参数,List<ByteBuffer>对应了上传的文件数据,这里之所以用 List<ByteBuffer>是因为ByteBuffer 尺寸上有 Integer.MAX_INTEGER的限制。

5. FormData的 postLimit(long postLimit)方法用来限制 POST数据尺寸,超出这个尺寸,直接终止连接。FormData的 useTempFile(int threshold)方法提供一个上传文件的尺寸阈值,阈值之下文件内容存放在堆 ByteBuffer中,否则将文件内容存放在一个临时文件中,ByteBuffer 通过文件的内存映射获得,如果要支持上传文件应该设置一个合适的threshold。在不支持文件上传的 POST中使用了 useTempFile,内部将产生异常终止连接。这是合理的,上传文件通常需要设置较大的 postLimit,不支持上传文件的 POST数据全部存储在内存中,过大的 postLimit 将给恶意客户端提供攻击服务器的机会。具体使用方法可以参考示例。

99

Page 100: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

6. FormData的 getRaw()方法返回一个 Octets包含了 POST发送的原始数据(不包括上传文件的情况,这种情况下 Octets是空的)支持处理一些非字符串格式的查询,比如OCSP查询。

public interface limax.http.DataSupplier {interface Done {

void done(HttpExchange exchange) throws Exception;}java.nio.ByteBuffer get(); default void done(HttpExchange exchange) throws Exception;static DataSupplier from(byte[] data) ; static DataSupplier from(java.nio.ByteBuffer data); static DataSupplier from(java.nio.ByteBuffer[] datas); static DataSupplier from(java.io.File file) throws IOException; static DataSupplier from(java.nio.channels.FileChannel fc,long begin,long end) throws

IOException;static DataSupplier from(java.io.InputStream in,int buffersize); static DataSupplier from(java.nio.file.Path path) throws IOException; static DataSupplier from(java.nio.channels.ReadableByteChannel ch,int buffersize); static DataSupplier from(java.lang.String text,java.nio.charset.Charset charset);static DataSupplier from(DataSupplier supplier, DataSupplier.Done done);static DataSupplier from(HttpExchange exchange, java.util.function.BiConsumer<String,

ServerSentEvents> consumer, Runnable onSendReady, Runnable onClose);static DataSupplier async();

}7. 从 DataSupplier接口定义可以看到,DataSupplier 提供的静态方法足以覆盖绝大多数应用场景。如果需要释放资源可以使用带 DataSupplier.Done 参数的方法 修饰DataSupplier , 在 done 操 作 中 执 行 释 放 操 作 。 最 后 一 个 方 法 用 来 支 持ServerSentEvents,详见下文。

8. 极少情况需要实现自己的 DataSupplier,如果自己实现,每次调用 get方法应该返回一个 ByteBuffer,最后一次 get返回 null,表示完成。最终 done方法被调用,可以在done方法中释放资源。特别的,done方法又传入一次 exchange,提供一个设置 trailer头的机会。(HTTP协议定义了 Trailer头,不知道哪些浏览器支持)

9. handle返回 DataSupplier.async()指明 handle 并没有完成响应,用于进一步的异步处理。这种情况下,异步任务完成之后必须执行 exchange.async(HttpHandler handler)完成响应。费时的数据准备工作,可以采用这种方式完成,提高服务效率。(参见示例)

10. handle 允许抛出任何异常。除了 limax.http.HttpException外,返回 InternalServerError,包含异常栈。HTTP 错误码可以通过 limax.http.HttpException抛出,构造 HttpException也允许提供一个 HttpHandler,用这个特殊 HttpHandler再进行一次处理。forceClose 参数,决定这次错误返回之后需不需要关闭连接,特别的,对于 HTTP/2,可以通过设置服务器参数禁止 forceClose。更进一步,可以抛出一个无参数 HttpException,立即无条件关闭连接。

11. 应用也可以通过 exchange.getResponseHeaders().set(“:status”, code);的方式来设置错误码,应该注意到这里的”:status”是 HTTP/2的描述。

100

Page 101: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

12. WebSocketExchange 提供 promise(URI uri)方法支持 HTTP/2的服务器推送,promise应该在处理服务逻辑的过程中调用。通常情况下,推送的资源应该被后续页面引用,客户端发现页面引用的资源之前已经开始推送了,就不需要再次请求该资源了,节省了一次请求过程,所以这是一个优化措施,如果没有推送最终处理结果应该一样。promise调用在 HTTP/1.1下是个空操作,换句话说,如果需要可以在 HttpHandler中实现推送逻辑,无需关心当前运行环境到底是 HTTP/1.1还是 HTTP/2。值得注意的是,服务器推送在某些情况下并不一定是一个好选择,比如客户端早就 cache了推送的资源,再执行推送反而浪费带宽。

WebSocket服务架构public interface WebSocketHandler extends Handler {

void handle(WebSocketEvent event) throws Exception;}

1. WebSocketHandler仅需要实现一个 handle方法,接收到客户端数据或者 websocket关闭时,handle 被调度,handle的调度是串行的。

2. event.type() 返 回 事 件 类 型 。 event.getWebSocketExchange() 可 以 获 取WebSocketExchange,用于发送数据,以及存取当前关联的 SessionObject。

3. event.type()==OPEN时可以设置应用自己的 SessionObject关联当前连接。4. WebSocketExchange上的 ping()方法可以用来测量 RTT,ping()方法返回一个 id,收到

pong 事件后可以获取对应的 id与 RTT 值。5. WebSocketExchange上的 send(byte[] binary),send(String text)方法用于发送WebSocket的二进制数据和字符串数据。event.type()==SENDREADY 意味着之前的数据发送完成,可以发送新的数据,用于支持流控。

6. WebSocketExchange上的 sendFinal()方法发送 CloseFrame之后立即关闭当前 websocket连接。sendFinal(long timeout)方法发送 CloseFrame之后半关连接,在 timeout时限内等待对方的握手 CloseFrame确保 RFC6455关闭语义。实际上 timeout 对于 https连接上的WebSocket没有意义,因为WebSocket关闭握手之后还会执行 SSL关闭握手,这就确保了WebSocket关闭握手一定完成。同样的,timeout 对于 RFC8441定义的 HTTP/2隧道方式的WebSocket关闭也没有意义,因为WebSocket关闭只对应 Stream关闭。

7. WebSocketExchange上的 resetAlarm可以设置超时,如果超时限度没有再次 resetAlarm,自动关闭WebSocket连接。

8. 关闭 event是最后一个 event。收到关闭 event,即可释放相应的资源,通常情况下关闭 code,reason五花八门,不要过于在意。

9. WebSocket 存在 2种运行模式,最初是从 HTTP/1.1升级,Mozillia 提出的 RFC8441 允许通过 HTTP/2的流隧道WebSocket流量,目前已知只有某些版本 FireFox支持这种模式。频繁使用WebSocket完成某些小任务时这种模式有很大优势,如果使用WebSocket 执行复杂的持久化任务,这种模式未必合适。如果要禁用,可以在 HttpServer 对象上设置 参 数httpServer.set(HttpServer.Parameter.HTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL, 0),禁用这种模式。

101

Page 102: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

Server-Sent Events

1. HttpHandle.handle 返 回 时 调 用 DataSupplier.from(HttpExchange exchange, BiConsumer<String, ServerSentEvents> consumer, Runnable onSendReady, Runnable onClose)方法,可以获取一个 ServerSentEvents 对象。其中,BiConsumer的 String为Last-Event-Id,onSent用来获取发送完成消息,用于支持流控。onClose用来通告网络连接已经被终止,或者 done 操作已经完成,后续的 emit已经没有意义了,应该释放ServerSentEvents 对象。

public interface ServerSentEvents {void emit(String event,String id,String data); void emit(String data); void emit(long milliseconds);void done();

}2. 3个参数的 emit用于向浏览器推送含有 event,id,data的数据,其中 event,id 允许为 null。单 String 参数的 emit 相当于 event,id都为 null的情况。long 参数的 emit用于向浏览器发送 retry 命令,单位毫秒。如果推送完成,应该调用 done方法。done方法调用前如果在 exchange上设置了 trailer, trailer能够被发送出去。(http语义允许,尽管不知道有何用处,什么浏览器支持)。

3. 浏览器对应的 EventSource收到 onerror 消息,最好立即关掉 EventSource,否则浏览器将不停重连服务器,这几乎就是一种攻击,去掉示例代码中 sse.html中的 s.close 即能观察到这种情况。

4. Server-Sent Events本质就是利用 HTTP/1.1的 chunked传输编码实现的一个简易设计,能力太弱,最好还是使用WebSocket代替。

HttpServer类1. HttpServer 的 create 方法用来在一个主机端口上创建一个 Http 服务器,调用包含

SSLContext 参 数 的 方 法 即 可 以 创 建 一 个 Https 服 务 器 。 对 于 应 用 而 言 ,HttpExchange,WebSocketExchange上通过调用 getSSLSession方法即可知道自己到底运行在 http 环境还是 https 环境。

2. HttpServer的 createHost方法使用一个域名创建虚拟主机,HttpServer本身就是默认虚拟主机,同一域名上第二次 createHost 等同于 getHost。

3. 虚 拟 主 机 的 createContext 方 法 在 一 个 path 上 安 装 HttpHandler 或 者WebSocketHandler,removeContext移除 path上对应的 Handler。

4. HttpServer的 get,set方法用来存取 HttpServer.Parameter定义的配置参数,配置参数非常多,类型也比较复杂,通常情况下默认参数应该够用了,如果修改可以参考源码。

5. HttpServer 的 start , stop 方 法 用 于 启 停 服 务 器 , 重 复 启 停 将 导 致UnsupportedOperationException异常。

6. HttpServer的 register方法可以注册一个 Closeable,stop服务器的时候被调用。7. HttpServer的 createFileSystemHandler方法用于创建一个静态文件服务器 handler,效果基本等同于常见的静态文件服务器。参数比较多,htdocs 指定文件服务器根目录;textCharset 指定文本文件字符集;mmapThreshold 指定使用文件映射的尺寸阈值,小

102

Page 103: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

于阈值的文件数据直接读到内存,否则使用文件映射;compressThreshold 指定文件数据压缩阈值,压缩尺寸除以原始尺寸小于阈值的说明压缩有效使用压缩版本,否则使用非压缩版本,文件是否允许压缩由 limax.http.ContentType枚举类型决定,通常来说文字内容允许压缩,图片不压缩;indexes数组指定缺省情况下默认的 index文件,通常应该配置为 index.html 和 index.htm;如果碰到目录访问,目录下不存在 indexes 指定的缺省文件的情 况下, browseDir 决定了是否允 许浏览该目录,如果不允 许以403FORBIDDEN响应;browseDirExceptions 指定作为例外的那些目录。

示例1. 源码目录下,demo/httpserver 提供一个示例,可以根据具体环境适当调整。2. 如果要试验 https,可以先安装根证书 ca.p7b,然后在代码中给出 localhost.p12的正确

路径,运行服务器之后,浏览器通过 https://localhost/访问即可。(也可以参考附录PKIX支持,建立自己的证书体系)

3. 示 例 提 供一个静态 http 服务器,可以试验创建静态网站。访问 /upload.html 和 /upload2.html可以试验文件上传,了解 FormData的特性。访问/websocket.html可以试验 websocket。访问/sse.html可以试验 Server-Sent Events。访问/exception可以试验handle抛出异常的情况。访问/async可以试验异步响应。

4. 服务器支持从 HTTP/1.1 upgrade到 HTTP/2,但是当前似乎没有任何客户端支持这一能力。

5. 如果要试验不加密的 HTTP/2,只能搜索安装 nghttp这样一个命令行程序,当前没有任何浏览器支持不加密的 HTTP/2。

6. 如果 https配置完成,使用 JDK8运行服务器,也不可能支持 HTTP/2,因为 JDK8的SSLEngine不支持 ALPN。

7. JDK8以上的 JDK运行服务器,即可支持 HTTP/2,但是当前的 JDK11的 SSLEngine 存在bug,如果使用 firefox访问,然后频繁刷新,SSL的握手方法就会陷入死循环。JDK13能够通过测试。

高级话题服务器模型1. 服务器采用完全的异步模型(HTTP/2这样的协议一个连接上就能并发大量请求,如果一个请求对应一个线程执行服务,少量的用户连接就能耗费大量服务器线程),通过计算请求的 hash 值选择线程执行服务。服务 handle在实现上尽量不要阻塞,如果存在数据库 IO 尽可能使用 cache,重负荷情况下应该设置更大的服务线程池规模。

2. 为了跟 limax 框架其它服务统一,使用 Engine.open 启动网络引 擎。服务器参数NETSERVER_WORKMODE为 true选择异步网络模型,false选择 poll网络模型。

3. Engine.open的参数 nProcessors决定了网络线程池规模,网络线程池负责所有请求数据的解析,关闭消息的投递。参数 protocolSchedulers配置的调度器仅被 HTTP/2的定时器使用,只需要 HTTP服务的情况下通常不需要配置太大。参数 applicationExecutors决

103

Page 104: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

定了服务线程池规模,服务线程池用于 handle请求,执行流控。数据发送1. HTTP/1.1,HTTP2 均支持流控,FLOWCONTROL_WINDOW_SIZE配置了流控窗口大小。2. WebSocket上如果需要发送大量数据,需要根据 event.type()==SENDREADY决定后续发送确保正确的流控,防止数据堆积,交互式应用通常不必严格进行流控。

3. Server-Sent Events同WebSocket类似,使用 onSendReady实现流控。4. CONGESTION_TIMEOUT时限内发送缓冲区没有任何消耗,被看成是严重拥塞,连接随

即关闭。数据上传1. HTTP协议通过 POST方法执行上传,但是 POST本身的设计过于简单,实际应用中如果不小心处理 POST,恶意客户端很容易利用 POST 消耗服务器资源。最大的问题在于POST的数据尺寸。

2. 不涉及文件上传的 POST,通常用来发送一些尺寸较大的查询,这种情况约定最大尺寸,使用 FormData的 postLimit方法简单限制即可。

3. 涉及文件上传的 POST就要复杂得多了,文件可能几 K,几M,几 G都有可能,也可能一次上传多个文件,postLimit很难确定。(为了避免过大的内存开销,FormData的useTempFile方法对于通常的文件上传还是必要的)

4. 示例中的/upload2.html,第一个输入框应该填写一个估计的上传尺寸(上传文件大小加上浏览器添加的MIME头信息的大小)尺寸过小则上传失败。

5. 实际应用中,应该在具体文件上传之前,协商上传细节,例如文件数量,文件大小,从而估算总尺寸。协商结果在协商服务与上传服务间共享,客户端交换协商结果的时效性 key,保证上传服务根据协商结果正确配置。

6. HttpHandler的 censor(HttpExchange exchange)方法由网络线程执行,只要抛出异常就可以结束连接。如果必要,可以在其中完成更多的 FormData审查(FormData可以获取已经传完的部分信息)。FormData 也提供了 getBytesCount(),getCreateTime()这样的方法,可以用来完成上传速率管理一类的任务,例如,直接关闭速率过慢的连接。(如果上传停顿,HTTP11_REQUEST_TIMEOUT,HTTP2_IDLE_TIMEOUT这一类的配置参数会起作用)

运行管理

Limax 框架,既是一套服务器 /客户端开发环 境,又是一个运行环 境。提 供了Switcher,GlobalId,Auany这些服务器组件,这些服务器组件与用户提供的 Provider进行交互,最终达成完整的系统功能。正确配置,调优服务器运行参数;规划服务器互联关系;监视服务器运行状况;以及版本升级,数据迁移,故障恢复,是运行管理阶段必须完成的

104

Page 105: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

工作。

配置Limax框架下各种服务器配置大多数通过 xml描述实现。源码中基本服务器组件的配置

可以作为参照进行修改,应用服务器生成的时候也生成了相应配置根据具体运行环境进行调整。部分不常用配置表现为 java 虚拟机参数形式,可以在启动时调整参数。xml基本参数Properties

只有一个属性:file,可以指定一个符合 java Properties规范的文件,提供给后续解析使用,影响后续部分属性字符串的解析。具体工作方式如下:如果属性字 符 串为 $key:value$ 格式,例如 serveri-switcher.xml 中解析 remoteIp 属

性"$auany.ipaddr:127.0.0.1$"时,按如下步骤进行:1. 分离出 key=auany.ipaddr,用于查找2. 查找系统 Property,如果存在则使用找到的值作为 remoteIp,否则3. 在 Properties中提供的属性文件中查找,如果存在则使用找到的值作为 remoteIp,否则

4. 设置 remoteIp为 127.0.0.1

Trace

系统日志配置,包含如下属性:outDir:日志输出目录,默认为”./trace”console:是否允许输出到控制台,默认为 truerotateHourOfDay,rotateMinute:rotate日志的时间,默认为每天早上 6点,这里要注

意,系统必须在运行中跨越这一时间点才会执行 rotaterotatePeriod:rotate日志的周期,单位毫秒,默认 86400000,即 1天。level:日志级别,有 5种,DEBUG, INFO, WARN, ERROR, FATAL,默认为 warn,这个属

性字符串大小写不敏感。Limit

数量限制配置,当前用于控制 ServerManager的接入数量,允许多个 ServerManager 引用同一限制。

name:Limit的命名maxSize:最大数量

JmxServer

必选三个属性 host, serverPort, rmiPort,提 供 给 JMX 管理应用使用。启动 url

105

Page 106: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

为"service:jmx:rmi://<host>:<serverPort>/jndi/rmi://<host>:<rmiPort>/jmxrmi"。通常情况下配置不常用的端口,只要管理应用能够访问即可。属性 username 和 password可选。通常情况下,如果启动 JmxServer,应该设置防火墙阻止来自 Internet的访问。

ThreadPoolSize

网络服务器线程池参数,包含如下属性。nioCpus:执行网络 Poll的 cpu数量,超过系统 cpu数量没有意义,默认 1。netProcessors:网络数据收发线程数量,默认 4。protocolSchedulers:协议处理线程数量,默认 4。applicationExecutors:应用线程数量,默认 16。

Manager

与应用 xml描述中的 type=”server”,type=”client”两种Manager 对应,描述了一个网络端点。包含以下属性。

type:”client”或者”server”

客户端,服务器共有属性parserCreatorClass:将属性的解析工作转交给该属性指定的类的对象。className:服务器或者客户端 Listener类,用以处理网络消息,不存在则使用默认

Listener。classSingleton:存在 className属性的情况下,如果设置了这参数,这个参数必须为类

定义中一个静态方法名,获取对象单件;如果没有这个参数,则直接创建类的对象。defaultStateClass:如果存在,则启动时调用该类的 getDefaultState 静态方法获取

Manager 初始状态;如果不存在则以 Listener 对象为参数,向 className定义的对象查询Manager的初始状态。以上 4个属性,与开发过程生成代码相关,一般情况下运营维护过程不用关心。enable:允许启动该 manager,默认为 true;某些运营场景下可以设置为 disable,暂时

禁止启动manager。name:端点名称,字符串。inputBufferSize:输入缓冲区大小,默认 16384,除非网络吞吐量很大否则无需修改。outputBufferSize:输出缓冲区大小,默认 16384,除非网络吞吐量很大否则无需修改。checkOutputBuffer:是否检查输出缓冲区大小,如果允许检查,当网络拥塞时堆积数

据超出 outputBufferSize,记录警告,关闭连接。默认为 falseinputSecurity:初始的网络输入流密钥,需要 16 字节串表示十六进制数,默认没有。outputSecurity:初始的网络输出流密钥,需要 16 字节串表示十六进制数,默认没有。inputCompress:初始状态下是否允许输入流压缩,默认 falseoutputCompress:初始状态下是否允许输出流压缩,默认 falseasynchronous:该 Manager 工作模式为异步模式或者 poll模式,默认 false,使用 poll

模式

106

Page 107: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

客户端独有属性:remoteIp:服务器 ip地址remotePort:服务器端口号connectTimeout:连接超时时间,默认 5秒autoReconnect:是否允许连接失败后自动重连,允许的情况下,第一次延迟 1秒后重

连,以后逐次加倍退避,直到 3分钟,3分钟为最长退避时间。默认 false

服务器独有属性:localIp:服务器 Ip地址,默认 0.0.0.0localPort:服务器端口号backlog:服务器 Listen 参数limit:引用 Limit配置名,控制服务器最大承载连接数量,超出数量后,记录日志,关

闭后续接入用户连接,拒绝后续用户接入。缺省情况下,配置名解释为空串,共同引用一个maxSize=Long.MAX_VALUE的 Limit。

autoStartListen:启动后是否允许服务器自动开始监听端口,默认为 truewebSocketEnabled:是否启动为WebSocket服务器,支持 HTML5兼容客户端,默认为

false。如果启动为WebSocket服务器,则可以通过如下属性支持 https:keyStore:pkcs12 格式包装的服务器证书包路径password:keyStore 密码

NodeService

node.js服务组件配置,一个属性:module:node模块路径一系列有序子 xml 节点集,对应模块需要的参数。<parameter value=”p0”/><parameter value=”p1”/>

Switcher

switcher服务器组件专用,提供与 Endpoint之间的连接配置参数。五个属性:cacheGroup:成功登录的情况下,switcher通过该组播地址在服务器间同步登录响应,switcher与 auany断开连接的情况下,试图使用该 cache 信息进行认证。默认为空串,禁止该 cache。cacheCapacity:成功登录的 login 信息 cache容量,默认 10000needcompress.s2c:是否压缩 Switcher到 Endpoint数据流,默认为 trueneedcompress.c2s:是否压缩 Endpoint到 Switcher数据流,默认为 truekey:swicher 向 auany发起验证使用的 key,具体描述参见附录 3(应用配置)。一系列子 xml 节点集:<dh group="1"/>配置服务器允许的 Diffie-Hellman 组。RFC2049,RFC3526

107

Page 108: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

隐含支持 dhgroup = 1配置支持 dhgroup = 2,5,14,15,16,17,18<native id=”1”/><ws id=”2”/><wss id=”3”>配置该服务器支持的 switcher id,具体描述参见附录 3(应用配置)。

Auany

Auany服务器组件专用,包括三种类型子节点,plat,pay,appstore:plat 节点:plat 节点具有 2个主要属性 name与 className。name大小写不敏感,为认

证平台名,与 Endpoint 登录配置中的 platflag 相对应,相应的认证请求交由 className 指定的平台支持类进行处理。

平台支持类需要实现 limax.auany.PlatProcess接口,提供两个方法:init:参数为对应 plat 节点,可以在 plat 节点内扩充平台需要的其它配置,在这里解析check:具体处理方法可以参考,limax.auany.plats.Test的实现,Test模块允许任何用户

名,但是密码必须为 123456。limax.aunay.local.Authenticator为本地认证方式,比较复杂,支持 3种常见的认证系统,radius,ldap,sql数据库,实际使用按照例子修改配置,选择某一种即可。定义在该 plat 节点下的认证系统通过 RR方式支持负载均衡及容错,这种情况下请放置多个不同条目,条目越多,timeout配置应该越小,因为实现上,如果某一认证系统的查询不能正确返回结果——包括超时与失败——这种情况下,轮换使用下一认证系统。

pay 节点:pay 节点具有 2个主要属性 gateway与 className。gateway是系统为第三方支付网关分配的数字 id,与 Endpoint.AuanyService.Pay中的 gateway 相对应。className 指明了第三方支付网关消息处理类,该类必须实现 limax.auany.PayGateway接口。

appstore 节点:appstore发票处理的基础配置。此外, Auany 节点内 <xi:include href="appconfig.xml"/> 通过 include 方式引用了

appconfig.xml配置文件,提供应用配置,具体描述在附录(应用配置)中详细介绍。GlobalId

Provider 服务专用,配置类似于 type=”client”的 Manager。一般来说应该配置autoReconnect=”true”,以及正确的 GlobalId服务器 ip地址,端口号。另外,可以配置属性timeout , 指 定 GlobalId 请 求 超 时 , 效 果 等 同 于 执 行 方 法limax.provider.GlobalId.setTimeout(long timeout),默认为 2000ms。Provider

与应用 xml描述中的 type=”provider”类型Manager 对应,提供了 Provider的网络端点配置。

Provider 内置至少一个客户端类型Manager 节点,描述了 Provider连接 Switcher服务器组 件 的 配 置 , 这 些 Manager 节 点 除 了parserCreatorClass,className,classSingleton,defaultStateClass属性无意义外,其它与客户端Manager 节点相同。一个 Provider 内置Manager 节点允许有多个,连接多个 Switcher服务器组件,需要调整接入规模的时候,这是最常用的。

Provider 节点有如下属性:108

Page 109: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

className: ProviderListener 类,用以处理 Provider 消 息,不存在则使用默 认ProviderListener

classSingleton:存在 className属性的情况下,如果设置了这参数,这个参数必须为类定义中一个静态方法名,获取对象单件;如果没有这个参数,则直接创建类的对象。

viewManagerClass:View的管理类。setAsOnlines:决定是否 Provider 启动时立刻打开数据服务,默认 true以上 4个属性,与开发过程生成代码相关,一般情况下运营维护过程不用关心。name:Provider 名称,字符串pvid:Provider的 PVID 参数,决定了系统内服务号key:Provider 密钥串,用于 Auany服务器验证 Provider合法性。默认为空串。Auany有

验证需求时,应同步修改该串。useVariant:Provider是否支持 Variant模式 View,这与生成代码相关,但是必要的情况

下可以通过设置该配置为 false关闭这一特性。useScript:Provider是否支持脚本模式 View,这与生成代码相关,但是必要的情况下

可以通过设置该配置为 false关闭这一特性。paySupportClass:如果 Provider需要支持支付,则应该提供该支付处理类,支付处理类

必须实现 limax.provider.PaySupport接口。Zdb

Zdb配置支持大量属性:dbhome:数据库 home,如果前缀为 jdbc:mysql,表示使用 MYSQL数据库,解释为

jdbcUrl,否则解释为 EDB数据库的文件系统路径。preload:表 cache预装载路径。zdb正常停止,最后一次 checkpoint之后,所有表的

cache都是干净的。此时,将表 cache 内容保存到本地磁盘,下次启动,通过保存的内容初始化 cache,后端为mysql的情况下,可以有效减轻启动负荷。装载过程中出现任何异常,立刻停止装载,所以表结构的变动不会造成任何影响。装载完成,清空整个目录。该属性默认不配置。

edbCacheSize:使用 EDB数据库的情况下,该参数指定了 EDB的 cache页面最大数量。EDB数据页尺寸为 8K。数据吞吐量过大的情况下,可能出现内存使用过载,一旦超出这个数量 EDB 将自动 checkpoint,释放干净页面。

edbLoggerPages:使用 EDB数据库的情况下,该参数指定了 EDB的日志文件能够存储的页面的最大数量,checkpoint之前检查当前日志文件的页面数量是否超出这一限制,如果超出,则创建新的日志文件,如果启用了增量备份,前一个日志文件被复制到备份目录里,如果没有,前一个日志文件被简单删除。

jdbcPoolSize:使用MYSQL数据库的情况下,jdbcPoolSize决定了连接池大小,否则无意义。

defaultTableCache:选择表 cache 类,当前支持 limax.zdb.TTableCacheConcurrentMap, limax.zdb.TTableCacheLRU,默认为 limax.zdb.TTableCacheLRU。

zdbVerify:运行中检查程序锁的使用是否合规,测试运行阶段可以设置为 true,正式运行设置为 false,提高性能。

autoKeyInitValue,autoKeyStep:自增量 key 初始值和自增量 key 增长步长,如果预期到109

Page 110: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

以后有合并数据库的需求,那么多个服务器可以考虑使用同样步长,不同初始值。这样使用自增量 key的表就可以在维护时直接合并,不需要考虑重设 id。

corePoolSize,procPoolSize,schedPoolSize:分别配置了 zdb核心线程池大小;存储过程线程池大小;调度线程池大小。

checkpointPeriod,marshalPeriod,marshalN,snapshotFatalTime:checkpointPeriod决定了将被改变的数据存储到底层数据库的频率,marshalPeriod决定了存储到底层数据库之前预打包未加锁的被改变数据的频率,预打包未加锁的被改变数据可以显著调高 zdb吞吐量。可以几次预打包存储一次,marshalN决定了在最终存储之前预打包未加锁的被改变数据次数。两个频率的最小值由虚拟机参数 limax.zdb.Checkpoint.SCHED_PERIOD 控制,默认100ms。如果在最终存储之前,预打包与完全打包消耗的总时间大于 snapshotFatalTime,则记录一条故障日志,多数情况表明系统负荷过重。

dealockDetectPeriod:数据库死锁检测周期。Zdb 节点下可以有一个 Procedure 节点,配置存储过程相关参数,Procedure 节点有 4

个属性:maxExecutionTime:存储过程执行的最大时间,执行超出该时间将报告过程执行超时,

常见的 OLTP应用,存储过程执行时间过长往往不合理。retryTimes,retryDelay:这两个参数决定了死锁之后过程重试次数,以及退避时间。trace:设定了存储过程相关日志记录级别。需要注意,所有 Zdb配置的属性都没有提及默认值,因为这些默认值,全部来源于应

用描述 xml,存储在生成的程序文件中。应用启动时,这里的配置可以覆盖程序文件中的相应配置,一把来说只需要根据运营需求作少许修改。Switcher

Switcher 配 置 可 由 上 述Properties,Trace,JmxServer,ThreadPoolSize,Manager,Switcher 节点进行组合,可参考Limax源码提供的 service-switcher.xml进行调整,其中:

Switcher 节点必选Manager 节点中

name=”ProviderServer” 节点定义了该 Switcher 作为 Provider服务器的网络特性name=”AuanyClient” 定义了该 Switcher 作为 Auany客户端的网络特性name=”SwitcherServer” 定义了该 Switcher 作为 Endpoint客户端的网络特性name=”SwitcherServerWebSocket” 定义了该 Switcher 作为WebSocket服务器的网络

特性。例如,需要接入多个 ISP,使用了多个网卡,可以克隆多份名为 SwitcherServer 的

Manager 节点,调整对应的 name,配置各自的 localIp,绑定不同的 ISP的 IP地址。Auany

Auany 配 置 可 由 上 述Properties,Trace,JmxServer,ThreadPoolSize,Manager,Auany,Zdb 节点进行组合,可

110

Page 111: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

参考 Limax源码提供的 service-auany.xml进行调整。Zdb在这里用于存储整个运营环境下的用户 SessionId分配关系。应用于实际的运营环境时,应该在 Auany框架下,创建更多的 limax.auany.PlatProcess

接口实现,提供更多的三方平台支持。GlobalId

GlobalId配置可由上述 Properties,Trace,JmxServer,ThreadPoolSize,Manager, Zdb节点进行组合,可参考 Limax源码提供的 service-globalid.xml进行调整。

Zdb在这里用于存储分配的全局 ID 信息。Provider

Provider 配置可由上述 Properties,Trace, JmxServer, ThreadPoolSize,Manager, Zdb,GlobalId,ProviderId 节点进行组合,可参考生成的服务器代码中的 service-XXX.xml进行调整。一个 Provider必须有且只有一个 Provider 节点。如果 Provider 提供的服务使用了 GlobalId服务,则必须配置 GlobalId 节点,否则不用。如果 Provider 提供的服务使用了 Zdb,则必须配置 Zdb 节点,否则不用。

java 虚拟机参数limax.net.io.NetModel.delayPoolSize:网络层触发各种超时动作使用的调度线程池大小,

默认为 1,一般无需调整。limax.util.ConcurrentEnvironment.timeoutSchedulerSize:可超时执行器的调度线程池尺

寸,可超时执行器用于 Zdb的存储过程超时。默认认为 3,一般无需调整。limax.zdb.Checkpoint.SCHED_PERIOD: zdb 的 checkpoint 检测最小周 期,默 认为

100ms,一般无需调整。limax.zdb.Lockeys.bucketShift:zdb 内部锁 hash桶尺寸移位参数。默认为 10,表示桶尺

寸为 2<<10 == 1024。一般无需调整。limax.zdb.Zdb.useFixedThreadPool:boolean类型,如果设置为 true,表示 zdb的核心线

程池、过程线程池使用固定线程池,这种情况下使用 LinkedBlockingQueue排队任务,防止丢失。除非存储过程生成过多,导致瞬时重负荷,严重影响系统响应,不应该设置该参数如果使用这种方式应该考虑 zdb配置里面加大相应 pool的配置。设置该参数应该被作为临时手段,修改设计才是合理考虑,必须意识到使用 LinkedBlockingQueue排队任务,在任务间存在依赖关系的情况下,可能导致难以被检查到的饥饿。(正在执行的任务依赖了还在排队的任务)。

limax.net.Engine.limitProtocolSize:协议尺寸硬限制,限制了所有协议以及 View协议的最大尺寸,超出尺寸连接中断,记录日志,默认 1048576。一般来说对于交互性应用 View的被改变字段尺寸,一次改变的字段的数量不应该过大,过大了将导致过多的网络传输,不是好的设计。对于数据传输应用,可以考虑在 Provider,Switcher服务器上增大该参数。

limax.net.Engine.intranetKeepAliveTimeout:某些云环境下,内网可靠性不能保证,可以通过该参数配置服务器间 keepalive检测的超时时长,一旦超时,关闭连接;作为服务器的服务器(通常是 Switcher)清除相应的状态的信息,恢复到初始状态;作为客户端的服

111

Page 112: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

务器(通常是 Provider),配置了断线重连的情况下,通过自动重连,试图恢复到正确的状态。默认为 0ms,不作检测。

limax.switcher.SwitcherListener.handShakeTimeout: 从 Switcher接受 Endpoint连接,到处理 Endpoint握手请求之间允许的时间窗口,默认 1000ms。除非 Switcher服务器负荷过重或者客户端性能过低该限制才不能满足。服务器硬件性能不成问题的情况下可以在Switcher服务器上适当减小该参数,有利于更好抵抗攻击。

limax.net.WebSocketServer.handShakeTimeout:上述参数对于 WebSocket方式运行的服务器无意义,因为WebSocket协议的设计决定了第一个 HTTP请求分析处理完毕才切换进入WebSocket数据交换状态,这时所需的认证参数已经齐备。对于 WebSocket方式下这个参数与上述参数具有同等意义。

limax.net.WebSocketServer.maxMessageSize:WebSocket服务器允许的最大消息大小,默认 65536,如果需要通过 WebSocket交互大尺寸消息,可以在 Switcher服务器上增大该参数。过大的设置不利于 Switcher抵抗流量攻击。

limax.net.WebSocketServer.keyExchangeTimeout:扩展的 websocket方式下,密钥交换超时,默认 3000ms。

limax.net.WebSocketServer.dhGroupMax:扩展的 websocket方式下,密钥交换允许使用的最大 DHGroup,默认 2。

limax.net.secureIp:如果 Switcher处于 DNAT 环境下,通过该参数指定指定外网 ip,确保 native方式或者扩展的websocket方式下的密钥协商能够正确进行。

limax.switcher.SwitcherListener.sessionLoginTimeout:Switcher与 Endpoint握手成功直到正常上线允许的最长时间,默认 20000ms。超过这个时长一般意味着 Switcher与 Endpoint或者 Auany之间的通讯出现问题。一般无需调整。

limax.switcher.SwitcherListener.keepAliveTimeout:Endpoint 周期性向 Switcher发送 Ping协议,这个参数决定了 Switcher收到 Ping协议的最长允许间隔,超出这个间隔,Switcher关闭网络连接。默认为 60000ms。

limax.switcher.SwitcherListener.pingProtect:Switcher的 Ping 保护周期,在保护周期内如果 Switcher收到一个以上的 Ping协议,服务器关闭网络连接。默认为 30000ms。上述两个参数与 Endpoint 相关,发布版本的所有 Endpoint使用 50000ms的 ping 周期,

在上述两个参数决定的窗口之内。如果要调整,需要保留一定宽容度。limax.node.js.EventLoop.corePoolSize:node.js框架事件循环线程池的最小线程数量,所

有 Cluster 共享同一个线程池,默认 64。limax.node.js.modules.Dns.corePoolSize:node.js框架中,dns模块的 DirContext池容量,

默认 16,除非应用需要太多并发 dns查询,一般不需要调整。limax.node.js.module.Net.TLSExchange.concurrency:node.js框架中,net模块的 socket

对象上启动 TLS支持的情况下,使用的 SSLEngine的并发数量,默认 32,重负荷 TLS服务器,可以试验性增加该值。

limax.node.js.module.Sql.ConnectionFadeout:node.js框架中,sql模块的连接池中的连接淡出超时,默认 60000ms,一般无需调整。

112

Page 113: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

部署

单机Auany,GlobalId,Switcher以及应用提供的 Provider可以运行在同一台机器上,一般来

说开发环境即使如此。小规模

Auany,GlobalId可以合并在同一机器上运行。应用负荷不太重,连接数量不太多的情 况下可以将 Switcher 配置文件中的

Manager, Switcher 节点直接拷贝到 Provider 配置文件中,并 且 将 Provider 节点下的Manager 节点的 remoteIp设置为 127.0.0.1。这种情况下直接启动 Provider时,Switcher服务将被启动到同一 java 虚拟机中,Switcher服务与 Provider服务之间除控制协议外的数据传输将被旁路,提高运行效率。大规模分立运行 Auany。运行多个 Switcher服务器。配置 Provider,连接多个 Switcher服务器,支持大量 Endpoint接入。

注意事项正确分配 PVID

同一运营环境下 PVID必须正确分配。一个 Provider 对应一个唯一的 PVID。整个环境允许分配 2^24个 PVID。

Endpoint的设计决定了一个完整的应用允许由多个 Provider 提供服务,大规模应用可以使用垂直划分的设计,解决负载问题。框架决定了,运行环境可以设计并提供公共的 Provider解决方案,交由各应用集成。

建议这样的解决方案提供 Variant模式或者脚本模式的实现,便于集成。GlobalId

GlobalId 原则上应该一个应用一个,如果多个应用之间可以约定一个组名的分配规范,那么可以共享一个。

113

Page 114: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

底层数据库如果一个应用的所有 Provider中,只需要一个 Provider 提供 zdb服务,使用 edb 底层数

据库是合适的,可以提供比较高的存储效率。如果一个应用需要在多个 Provider上提供 zdb服务,应该考虑各个 zdb连接同一mysql服务器,或者mysql 集群,便于维护。日志系统

Trace用于记录系统日志,不适合重负荷日志记录,建议应用使用 log4j记录应用自己的日志。

运行状态监视系统使用 JMX 提供运行状态监视的能力。其中分为独立服务的监视,与服务器组数据搜集。

独立服务监视以 switcher服务器为例:启动 switcher服务器运行 jdk的 bin目录内的 jconsole 程序,新建连接,大致出现如下内容:

连接 limax.swticher.Main服务,切换到MBean TAB页可以看到这样一系列 limax 相关的mbean 信息在这里,可以查看运行中的各类相关数据。

114

Page 115: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

这里建立的是本地连接,如果需要使用远程连接方式,需要配置相应服务器的JmxServer 参数,制定 serverPort 和 rmiPort。switcher的服务配置中:

<JmxServer rmiPort="10002" serverPort="10003"/>对应了使用连接 url:service:jmx:rmi://localhost:10003/jndi/rmi://localhost:10002/jmxrmi

Jconsole使用图形 UI直观表现了mbean数据,但是不利于记录分析。如果需要,可以通过 limax.jar 提 供的简单 工具来获取数据文本。例如,为了获取前一个图中最后项threadpoolsize的数据,可以运行命令:

java -jar limax.jar jmxtool attrs -c "service:jmx:rmi://localhost:10003/jndi/rmi://localhost:10002/jmxrmi" -b "limax.xmlconfig:type=XmlConfigs,name=threadpoolsize"

将获得如下结果:4 ProtocolSchedulers16 ApplicationExecutors1 NioCpus4 NetProcessors

这与 Jconsole中查看到的数据一致:115

Page 116: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

java –jar limax.jar jmxtool 的查询子命令domains:获取注册到服务器的 JMX Domainmbeans:查询注册到服务器的所有mbean Nameattrs:查询mbean属性,见上面的例子

通用服务器组数据收集使用 jmxtool的 monitor 子命令,提供一个实现了 limax.util.monitor.Collector的接口的

类作为参数,monitor运行时,创建该类的对象,通过 limax.util.monitor.CollectorController接口与之交互,实现收集。示例

demo目录下提供了一个 testmonitor应用作为例子,AuanyCheckPointMonitor.java收集auany服务器的 Zdb的 Checkpoint 相关数据,首先运行 auany服务器,ant run 即可运行收集。例子作周期为 30s的收集,收集 1分钟,大致结果如下:run: [java] Sat May 09 02:59:24 CST 2015 localauany limax.zdb:type=Zdb,name=Checkpoint [java] CountMarshalN 174 [java] TimeOfNextFlush 2015-05-09 00:04:40.691 [java] TotalTimeFlush 7391194 [java] PeriodCheckpoint 60000 [java] TotalTimeMarshalN 16099374 [java] TotalTimeCheckpoint 0 [java] TimeOfNextCheckpoint 2015-05-09 02:59:49.095 [java] CountMarshal0 0 [java] CountSnapshot 0 [java] CountFlush 0 [java] CountCheckpoint 174 [java] TotalTimeSnapshot 14960553 [java] Sat May 09 02:59:54 CST 2015 localauany limax.zdb:type=Zdb,name=Checkpoint [java] CountMarshalN 175 [java] TimeOfNextFlush 2015-05-09 00:04:40.691 [java] TotalTimeFlush 7408422 [java] PeriodCheckpoint 60000 [java] TotalTimeMarshalN 16151422 [java] TotalTimeCheckpoint 0 [java] TimeOfNextCheckpoint 2015-05-09 03:00:49.145

116

Page 117: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

[java] CountMarshal0 0 [java] CountSnapshot 0 [java] CountFlush 0 [java] CountCheckpoint 175 [java] TotalTimeSnapshot 14986577 [java] Sat May 09 03:00:24 CST 2015 localauany limax.zdb:type=Zdb,name=Checkpoint [java] CountMarshalN 175 [java] TimeOfNextFlush 2015-05-09 00:04:40.691 [java] TotalTimeFlush 7408422 [java] PeriodCheckpoint 60000 [java] TotalTimeMarshalN 16151422 [java] TotalTimeCheckpoint 0 [java] TimeOfNextCheckpoint 2015-05-09 03:00:49.145 [java] CountMarshal0 0 [java] CountSnapshot 0 [java] CountFlush 0 [java] CountCheckpoint 175 [java] TotalTimeSnapshot 14986577

BUILD SUCCESSFULTotal time: 1 minute 1 second

开发接口Collector接口:(收集应用必须实现该接口)1. void onController(CollectorController controller) throws Exception;收集对象被创建出来之后,该方法被调用,收集对象获得收集控制器,controller的使用见下文。

2. void onRecord(String host, ObjectName objname, Map<String, Object> item);Monitor收集到 Jmx数据后调用该方法,区分主机名,Mbean 和 Mbean下的属性数据。该方法是收集的关键方法,数据内容可以存储到数据库,提 供给后续分析使用。Monitor收集执行过程中,一个主机的一轮收集被分配给一个线程,该方法可能被并行调用,收集应用应该自行考虑同步问题。

3. default void onException(String host, Exception e){}某一主机的收集过程中如果出现异常,则该方法被调用,将异常通告给收集应用。注意,这种情况下收集不会停止,例如出现了网络故障,导致某一主机的一次收集失败,故障恢复之后,后续的收集任务仍将正常进行。该方法可能被并行调用。

CollectorController接口:(收集应用用来管理收集行为)1. Runnable addHost(String host, String url, String username, String password) throws

MalformedURLException;添加主机配置,username为 null表示无须认证。返回的 Runnable 对象上调用 run,该主机配置被删除,该主机上的所有收集被取消。

117

Page 118: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

2. Runnable addCollector(String host, String pattern, long period) throws MalformedObjectNameException;为一个主机添加一个收集,调用该方法之前必须确保 host已经通过 addHost成功加入,并且没有被取消, pattern 按照 ObjectName的模板格式,使用通配符,匹配多个实际的 ObjectName,period 单位毫秒,决定了收集频率。返回的 Runnable 对象上调用run,该收集配置被取消。

3. void stop();同步停止整个收集过程,返回之后 Collector接口的任何方法都不再被调用,可以安全清理收集器。

应用相关的服务器组数据收集应用相关收集是通用收集的特化形式,与应用描述 xml中定义的 monitorset 相联系,

生成了相应的收集代码,可以更加精确地进行收集编程。limax.util.monitor.MonitorCollecotor,实现了前面提及的 CollectorController接口,内建

Collector 接口 对象,将 Collector.onRecord 采 集到的 Jmx 相关数据,分发到经由MonitorCollector.addCollectorInstance 添加到收集器内的收集对象。通过 addCollectorInstance 加入的对象,必须实现应用 xml定义的 monitorset生成代码

中的 Collector接口。例如:auany.xml 定义了名为 AuthProvider 的 monitorset,在生成代码中可以找到

AuthProvider.Collector 这样一个接口定义,该接口定义的 onRecord 按 照收集 主 机 名,monitoset定义的 key,monitorset定义的 counter的顺序,精确还原了参数类型。通过mc.addCollectorInstance((AuthProvider.Collector) (host, platflag, pvid,

_newaccount, _auth) -> {System.out.println("host = " + host + " platflag = " + platflag

+ " pvid = " + pvid + " newaccount = " + _newaccount + " auth = " + _auth);});的方式,加入收集对象实例,即可实现类型化的收集。具体使用,可以参考,testmonitor中的例子 AuanyAuthMonitorAPI.java。

收集数据直接入库某些运行环境下,需要将收集数据直接入库,事后分析。通过 limax.util.monitor.MonitorCollector.addSQLExecutor方法支持。具体使用可以参考

testmonitor中的例子 AuanyAuthMonitorDB.java。构造带有 onexception 参数的MonitorCollector,即可通过 onexception收集采集过程中

发生的所有异常。查看生成的 AuthProvider.Collector 代码,注 意到接口 内部提 供了两个静态方法

getCreateTableString 和 createInsertStatement,采集应用启动后,首次插入数据前会根据getCreateTableString返回的 SQL语句在数据库中试图创建表格,忽略创建错误。这就是说,如果有必要可以根据特定需求预先建表,表的名 字 和基本字 段描述出现在

118

Page 119: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

createInsertStatement方法中,用户定义的表格只要确保上述表名和基本描述字段存在即可,这些名字除了 host,其它均来源于 monitorset的描述,名字前面全部加上下划线,避免与数据库保留字冲突。此外,getCreateTableString返回了符合Mysql规范的 CREATE TABLE语句,如果使用别的数据库,也请仿照样式预先建表,createInsertStatement使用的 INSERT语句是标准的,不存在数据库兼容性问题。

维护

停机Switcher这类服务可以用外部结束进程的方式关闭。集成了 Zdb服务的应用,比如 Auany,GlobalId,Provider之类的服务,有两种关闭方

法。1. 结束 Switcher后,等待一个以上的 Zdb checkpoint 周期,再结束进程,确保数据完全刷新到底层数据库。

2. 正确配置服务的 JmxServer 参数,然后使用 jmxtool的 Stop 子命令,确保 Zdb数据刷新到底层数据库,服务按正确顺序停止。例如,结束 auany服务,可以执行命令:java -jar limax.jar jmxtool stop -c "service:jmx:rmi://localhost:10202/jndi/rmi://localhost:10201/jmxrmi"该命令允许一个额外的-d delay 参数,delay为毫秒,指令服务器在 delay时限达到之后停机。

备份与恢复备份与恢复的方式依赖于使用的底层数据库引擎。

使用 EDB 引擎的备份使用 jmxtool的 backup 子命令实现备份功能,支持全备份,增量备份两种方式。backup 子命令参数:1. –d <backupdirectory> 指定备份目录2. –i <true or false>,true表示执行增量备份,false表示不进行增量备份。这里备份与 sqlserver之类的备份方式稍有区别,sqlserver类数据库,要求增量备份之前必须存在一个全备份。所以要做增量备份必须按 照 dump database, (dump transaction)+ 的方式执行。limax上实现为只要执行了 backup 子命令作增量备份,首先执行全备份,logrotate的时候自动复制数据库日志到备份目录。这种方式可以简化备份规则设计,例如,要求以一天为周期作增量备份,只需要在每天特定时间点执行一次增量备份即可。

119

Page 120: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

例如:增量备份 auany的 zdb数据库,执行如下命令即可java -jar limax.jar jmxtool backup -c

"service:jmx:rmi://localhost:10202/jndi/rmi://localhost:10201/jmxrmi" -d c:\temp\backup –i true

使用 EDB 引擎的恢复如果恢复的是全备份,直接将备份目录拷贝成 zdb目录即可。如果在增量备份上恢复,则按下述步骤进行:1. zdb目录重命名成 zdb.old2. 备份目录拷贝成 zdb目录3. 将 zdb.old/log目录下的文件覆盖到 zdb/log中。这是由于 zdb.old/log目录下的日志文件中可能存在最新的还没有提交到备份目录的 checkpoint 信息。

如果要按选择的时间点恢复,上面的拷贝完成之后在 zdb目录上用命令行方式执行EDB的交互工具 edbtool:

java -jar limax.jar edbtool#helprescue <src dbpath> <dst dbpath>list checkpoint <dbpath>recover checkpoint <dbpath> <recordNumber>out <filename> <charset> #default System.out UTF-8exitquit#list checkpoint c:\temp\backup0 : 2015-04-18 16:15:36.210#

关键命令有两条,list checkpoint 命令,列出当前数据库所有 checkpoint时间点,从 0开始编号,在列表中选择期望的时间点编号执行 recover checkpoint 命令,数据库数据量较大,时间点较多的情况下,命令可能执行较长时间,命令完成后,数据库恢复到选择的那个时间点的状态。使用Mysql 引擎的备份与恢复:直接使用Mysql的备份与恢复策略。

数据格式转换应用升级以后,可能需要转换 Zdb中存储的数据格式,需要实现类似 sql数据库的

ALTER TABLE功能。这项工作的大部分任务应该交由应用的开发者完成,提供相应的转换程序,交由运营者执行生产环境上的转换。

120

Page 121: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

Limax框架提供转换所需的相应的支持,这里以提供一个以前面的服务器开发章节example 提供的 zdb描述为例,介绍转换方法。转换实例1. zdb描述

<xbean name="MyXbean"><variable name="var0" type="int" /><variable name="slist" type="vector" value="string" />

</xbean><table name="mytable" key="long" value="MyXbean" autoIncrement="true"/>

2. 添加一条记录import limax.util.Pair;import limax.util.Trace;import limax.zdb.DBC;import limax.zdb.Procedure;import limax.zdb.Zdb;import limax.zdb.tool.DataWalker;

public final class ConvertTest {public static void main(String[] args) throws Exception {

new java.io.File("zdb").mkdir();Trace.set(Trace.ERROR);limax.xmlgen.Zdb meta = limax.xmlgen.Zdb.loadFromClass();meta.setDbHome("zdb");Zdb.getInstance().start(meta);Procedure.call(() -> {

Pair<Long, xbean.MyXbean> pair = table.Mytable.insert();pair.getValue().setVar0((short) 123);pair.getValue().getSlist().add("name123");return true;

});Zdb.getInstance().stop();DBC.start();DBC dbc = DBC.open(meta);DataWalker.walk(dbc.openTable("mytable"), kv -> {

System.out.println(kv.getKey() + ", " + kv.getValue());return true;

});DBC.stop();

}}

121

Page 122: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

运行这段代码,返回如下结果4096, {var0:123, slist:["name123", ], }

3. 更新 zdb的 xml描述<xbean name="MyXbean">

<variable name="var0" type="short" /><variable name="slist" type="vector" value="string" />

</xbean><table name="mytable" key="long" value="MyXbean" autoIncrement="true"/>

注意到,MyXbean.var0 类型从 int 改变为 short

4. 重新生成代码,刷新 eclipse,这时发现代码出现错误,将pair.getValue().setVar0(123); 改为 pair.getValue().setVar0((short)123);

5. 再次运行这时,运行报告错误Exception in thread "main" limax.zdb.XError: convert needed: {mytable=MANUAL}运行结果指明了,当前版本应用与前一版本应用的 Zdb数据库不兼容,mytable这张表需要手动转换才能兼容。

6. 运行转换工具生成转换代码在应用的当前目录下创建 zdbcov目录,执行命令java -cp <path to limax.jar>;bin limax.zdb.tool.DBTool -e "convert zdb zdbcov"注意,这里指明了 2个 classpath,一个是 limax.jar,另一个是当前应用的 bin目录。这时,获得如下输出:mytable MANUAL-----COV.class not found, generate-----make dir [cov]make dir [cov\convert]generating cov\convert\Mytable.javagenerating cov\COV.java

这个输出,指明了mytable表需要手动转换,创建了一 cov目录,里面放置了转换用框架代码。刷新 eclipse,配置项目属性,将 cov目录设置为源码目录。在Mytable.java里面寻找//TODO这一行,这里就是填写手工转换代码的地方。 // TODO var0 = s.var0;假设我们将这行修改为:

var0 = (short) -s.var0;122

Page 123: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

7. 再次运行代码转换工具java -cp <path to limax.jar>;bin limax.zdb.tool.DBTool -e "convert zdb zdbcov"获得如下结果2015-05-12 17:56:14.283 INFO <main> limax.zdb.DBC start ...mytable MANUAL-----COV.class found, manual convert start-----copying... _sys_converting... mytable2015-05-12 17:56:14.390 INFO <main> limax.zdb.DBC stop begin2015-05-12 17:56:14.397 INFO <main> limax.zdb.DBC stop end-----manual convert end-----

在这里,能够看到创建了新目录 zdbcov 存放转换后的数据库,mytable表上执行了转换,不需要转换的_sys_表被直接拷贝。

8. 验证转换效果前一版本的 zdb目录重命名为 zdb.old,zdbcov目录重命名为 zdb重新执行 ConvertTest.java,获得如下结果:4096, {var0:-123, slist:["name123", ], }8192, {var0:123, slist:["name123", ], }

注意到,key=4096行,var0 变为了-123,这正是转换代码实现的功能,转换成功。

转换相关的细节前面的例子 mytable的转换类型报告为MANUAL,_sys_的转换类型报告为 SAME,事实

上 系 统 中 提 供 4 种 转 换 类 型 , 定 义 在 limax.zdb.tool.ConvertType 中 , 分 别 为SAME,AUTO,MAYBE_AUTO,MANUAL。

转换类型含义如下:SAME

相同。这样的表在转换时直接拷贝。AUTO

转换不会损失精度,例如整数从短到长的转换;一个 bean,去掉了某些字段,并且该 bean又没有作为任何map的 key 存在。这类转换可以自动进行,无需用户干预。MAYBE_AUTO

可能损失精度的转换,例如整数转换为浮点;一个 bean,添加了某些字段,需要初始化添加的字段。这类转换可以自动进行,用户愿意干预也可以进行干预。MANUAL

123

Page 124: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

其他一切情况,必须用户干预才能进行的转换。如果新版应用启动失败,需要进行转换,首先应该使用命令:java -cp <path to limax.jar>;bin limax.zdb.tool.DBTool -e "convert zdb zdbcov"获取转换类型,如果存在MANUAL或者MAYBE_AUTO类型的转换,则该命令会生成框

架代码,填写相应的 TODO,编译完成后再次运行代码转换工具即可。对于在上述的结果中只有MAYBE_AUTO,没有MANUAL的情况下,如果允许执行损失

精度的转换,则可以删除先前生成的 cov目录,直接使用命令:java -cp <path to limax.jar>;bin limax.zdb.tool.DBTool -e "convert zdb zdbcov true"例如,将上例中的 var0类型改变为 float,运行上述命令获得如下结果:mytable MAYBE_AUTO-----no need generate!, auto convert start-----2015-05-12 23:40:52.532 INFO <main> limax.zdb.DBC start ...mytable MAYBE_AUTOcopying... _sys_auto converting... mytable2015-05-12 23:40:52.623 INFO <main> limax.zdb.DBC stop begin2015-05-12 23:40:52.630 INFO <main> limax.zdb.DBC stop end-----auto convert end-----

这里可以看到,MAYBE_AUTO的表也被自动转换了。事实上,convert 命令的参数格式如下:convert [fromDB [toDB [autoConvertWhenMaybeAuto [generateSolver]]]]

fromDB 指定了转换源,默认为 zdbtoDB 指定了转换目标,默认为 zdbcovautoConvertWhenMaybeAuto 指示是否直接转换MAYBE_AUTO类型的表,默认 falsegenerateSolver 指定了是否生成合并代码,默认 false。数据库的合并后面介绍。

转换总结1. 对于应用开发者而言:完成新版本开发之后,应该在上一个版本的 zdb数据库上运行应用,如果报告错误提

示数据转换,则应该在当前项目目录创建 zdbcov目录并运行:java -cp <path to limax.jar>;bin limax.zdb.tool.DBTool -e "convert zdb zdbcov"如果生成了转换代码,根据需要提供自己的记录转换实现,打包新版本应用,集成转

换工具,测试通过后,提交给运营环境在生产系统中执行。2. 对于运营环境而言:获取新版本应用以后,如果获知需要转换,则执行转换java -cp limax.jar;application.jar limax.zdb.tool.DBTool -e "convert zdb zdbcov"

124

Page 125: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

执行完成以后,备份原始 zdb目录,将 zdbcov目录改名为 zdb。最后,启动新版应用。

3. convert的 fromDB,toDB 参数如果是 MYSQL url,则需要在 java –cp 参数之后,追加mysql/Java连接器的 jar包。

4. 转换目标 zdbcov,必须手工创建,对于 EDB 引擎创建一个目录,对于 MYSQL 引擎创建相应数据库。如果使用了 MYSQL数据库,转换之后,可以将应用配置文件中 dbhome直接指向新的数据库,减少数据库拷贝。

注意事项对于关系数据库而言,大型表格的 ALTER TABLE,非常耗时,同理,转换大型 zdb数据

库,也非常耗时。实际使用中,如果涉及到大型 zdb数据库的转换,建议首先使用备份数据进行转换时长测试,确认停机时间上限,如果停机时间不可接受,则只能特别设计应用在运行过程中逐步转换。数据库合并

存在这样的应用,开始阶段分立运营,一段时间之后可能出现合并数据库的需求。Limax通过一系列手段支持这样的应用。合并的支持1. 使用 GlobalId服务,同一 GlobalId域之内的应用,通过 GloalId服务提供唯一 id,相应的

id 作为表的 key,这样的表可以安全合并,而不会发生 key冲突。2. Zdb的自增量 key的配置,autoKeyInitValue,autoKeyStep,分立运营的同种应用,一开

始配置相同的 autoKeyStep,不同的 autoKeyInitValue。这样,凡是使用自增量 key的表可以安全合并,不会发生 key冲突。

3. 数据库合并,事实上是一种特殊类型的格式转换,与前面提到的格式转换相比,普通的格式转换目标数据库为空,执行合并时,目标数据库存在。对于同名表,如果在源数据库与目标数据库中,发现了相同的 key,则认为发生冲突,这种情况下,可以生成相应代码框架,提示用户解决冲突。

合并的操作1. 备份目标数据库,zdb -> zdb.bak2. 准备源数据库,假设为 zdbsrc3. 执行合并

java -cp limax.jar;application.jar limax.zdb.tool.DBTool -e "convert zdbsrc zdb"4. 如果没有冲突,合并完成,如果报告冲突,执行下面步骤。

java -cp limax.jar;application.jar limax.zdb.tool.DBTool -e "convert zdbsrc zdb false true"convert的最后一个参数 true 指明了需要生成冲突处理代码,将生成的 cov目录提交给应用

5. 应用将 cov目录作为源码目录,可以看到比起前面的数据格式转换,cov目录中多出一个 solver包,里面存在所有数据库表的相关冲突解决代码。找到代码中的方法,

125

Page 126: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

public OctetsStream solve(OctetsStream sourceValue, OctetsStream targetValue, OctetsStream key)填写需要的 TODO代码,重新打包应用。

6. 提交应用,重新执行第 3步。注意事项1. 建议尽量使用 GlobalId,和正确配置 Zdb自增量 key,避免出现需要解决冲突的情况。2. 对于可能出现的冲突,设计上应该有预见。事实上,出现上一节提到的在运营阶段出现合并冲突,生成冲突解决代码,解决冲突的过程是不应该出现的。出现这一情况,应该理解为设计缺陷。

3. 如果应用运行在MYSQL 引擎上,并且能够确认合并操作决不会发生冲突,则可以直接在MYSQL上用 SQL 命令合并_meta_,_sys_之外的所有表,对于这两张表,保留目标数据库的版本即可。

4. convert可以在 EDB数据库和 MYSQL数据库之间相互转换。5. convert可以同时生成格式转换代码和合并时的冲突解决代码,也就是说,格式转换与合并可以同时进行。为了避免混乱,建议按照先转换,再合并的顺序,分步操作。

附录

支付框架Limax 提供完整的支付框架,可以接入任意第三方支付系统。同时该支付框架隔离了第

三方支付系统与应用服务器实现,便于应用服务器的开发调试。A类支付流程1. Provider 向 Endpoint发布商品信息(可选步骤)2. Endpoint首先向 Auany下订单,获得订单 id,使用该订单 id 向第三方支付系统发起支付

3. 第三方支付系统向 Auany 反馈支付结果,如果支付失败,流程结束。4. Auany 向应用服务器投递支付数据B类支付流程1. Endpoint 向支付服务器购买,获得发票

126

Page 127: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

2. Endpoint 向 Auany 提交发票直到 Auany接受发票3. Auany 向服务器验证发票,如果失败,流程结束4. Auany 向应用服务器投递支付数据客户端limax.Endpoint.AuanyService.pay(int gateway, int payid, int product, int price, int quantity, String receipt, long timeout,Result onresult);

该方法向 auany发起支付gateway:接下来使用的第三方支付网关在系统内分配的 idpayid:接受支付的 ProviderId,第三方支付网关成功支付以后,Auany使用该 ProviderId进行投递。product:商品号price:单价quantity:数量receipt:发票timeout:请求超时,单位毫秒onresult:接收返回结果。public interface Result {

void accept(int errorSource, int errorCode, String result);}

注意,对于 A类支付流程,首先调用 pay,验证 errorCode == 0,获得 result,随即使用result进行第三方支付;对于 B类支付流程,获得发票后,本地存储发票,使用发票调用pay,返回 errorCode==0后可删除本地存储的发票,否则应该持续重试。Provider

实现public interface PaySupport {

void onPay(long serial, long sessionid, int product, int price, int quantity, Runnable ack);void onPayConfirm(long serial, Runnable ack);

}

Provider实现 ProviderListener的同时,与之并列实现 PaySupport接口首先需要提供一张表格记录 serial.onPay的实现1. 查表格中是否存在 serial,如果存在转 42. 对于 A类支付检查 product与 price是否与系统定义的商品信息匹配,如果不匹配说明

Endpoint 向 Auany发起支付时作假;对于 B类支付 price=-1,仅需要检查 product。发生127

Page 128: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

了作假的情况可以根据应用设计要求处理这笔收入然后转 4。3. 提供 quantity个 product 给 sessionid 对应用户,记录 serial。这两个操作建议在同一事务中完成。

4. 调用 ack();

onPayConfirm的实现1. 删除表格记录的 serial2. 调用 ack();

注意,这两个方法必须在操作的最后调用 ack,如果没有调用,Auany 将周期性向 Provider发起请求。记录支付日志时,通过转换 Long.toString(serial, Character.MAX_RADIX)获得订单号,该订单号与 Auany的支付日志相对应,便于在必要的时候进行对账。配置Provider无需任何配置Auany上的配置Auany配置节点属性payEnable:开启运营环境的支付支持,默认为 truepayLoggerClass:支付日志实现类,默认为 limax.auany.PayLoggerSimpleFile,可以定义运营环境自己的支付日志类,该类必须实现 limax.auany.PayLogger接口。payLoggerSimpleFileHome:PayLoggerSimpleFile记录支付日志使用的目录,默认为 paylogs。orderExpire:订单过期时间,默认 3600000ms 即 1小时。orderQueueConcurrencyBits:订单处理队列并发参数,默认为 3,建立 1<<3 == 8个处理队列。orderQueueHome:订单处理队列目录,默认为 queue。deliveryExpire:订单投递超时,默认为 604800000ms,即 7天,投递超时可能有几种原因。其一,下订单时提供的 payid 错误,其二,超时周期内 payid 对应的 Provider 从来没有启动过,其三,应用实现 limax.Provider.PaySupport时,忘记调用 ack()。订单投递超时以后PayLogger.logDead 被调用。deliveryQueueCheckPeriod:投递队列检查周期,默认为 60000ms,即 1分钟。deliveryQueueBackoffMax:投递失败退避 参数,默 认为 5,定义了退避 周 期序列1,2,3,4,5,5,5…,这里的 1,2,3,4,5用来乘以 deliveryQueueCheckPeriod 即为下一次检查延迟。deliveryQueueConcurrencyBits:投递队列并发参数,默认为 3,建立 1<<3 == 8个处理队列。deliveryQueueHome:投递队列处理目录,默认为 queue。pay配置节点属性gateway:运营系统为第三方支付网关分配的 idclassName:第三方支付网关消息处理类,必须实现 limax.auany.PayGateway接口。

128

Page 129: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

第三方支付网关扩展public interface PayGateway {

void initialize(Element e, Map<String, HttpHandler> httphandlers) throws Exception;void unInitialize();void onPay(long sessionid, int gateway, int payid, int product, int price, int quantity, String

receipt,Result onresult) throws Exception;}

initialize 初始化第三方支付网关配置,一般来说第三方支付网关使用 http投递支付消息,可以在 httphandlers中设置自己的 httpContext与 httpHandler的关联。onPay实现具体支付操作。

A类支付示例系统提供了 limax.auany.paygws.Simulation模拟支付网关,可以仿照该实现扩展更多的第三方支付网关支持,该模拟支付网关也可以用来进行应用调试,默认配置如下:<pay className="limax.auany.paygws.Simulation" gateway="0" httpContext="/pay/simulation" maxAmount="999999" maxDeliveryRandomDelay="30000"/>

有两种调试方式:1. 通过 http,访问 http://auanyserver:8181/pay/simulation?<order> ,其中 order为客户 pay

操作返回的订单号,这种方式只支持成功支付。2. maxDeliveryRandomDelay > 0的情况下,系统在 maxDeliveryRandomDelay规定值的范围

内随机一个延迟,然后进行结果投递。如果 price * quantity 在{0, maxAmount}范围内支付成功,否则失败。maxDeliveryRandomDelay == 0,则只支持 http方式。

B类支付示例系统提供了 limax.auany.paygws.AppStore,支持 AppStore的支付,默认配置如下。<appstore connectTimeout="15000" home="appstore" readTimeout="15000" receiptExpire="604800000" receiptReplayProtectorConcurrentBits="3" receiptVerifyScheduler="4" retryDelay="300000"/><pay className="limax.auany.paygws.AppStore" gateway="1" productPattern="[^\d]+([\d]+)$" url="https://buy.itunes.apple.com/verifyReceipt"/>

其中 appstore 节点为 appstore服务的全局配置。connectTimeout:连接发票验证服务超时,默认 15000msreadTimeout:发票验证服务器返回结果超时,默认 15000msretryDelay:访问发票验证服务器失败以后,下一次调度延迟,默认 300000ms,即 5分钟

129

Page 130: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

receiptVerifyScheduler:发票验证调度器线程数,默认为 4home:appstore 相关文件的根目录,默认为当前目录下的 appstore目录receiptExpire:发票超时,系统拒绝接受超出该限制的旧发票,从发票的支付之间算起。默认 604800000ms,即 7天receiptReplayProtectorConcurrentBits:发票重放保护器并发参数,默认为 3,即平均允许1<<3==8个线程同时验证重放注意,所有 B类支付方式都存在发票重放的问题,尽管实现要求客户端在验证发票前进行本地存储,服务器发货完成,通知客户端之后,删除本地存储的发票,但是这种解决方案本身并不可靠,不能避免某些恶意程序在删除本地发票时进行欺骗,从而进行发票重放。为了解决重放问题,发票超时是必要的,这个时间之内进行重放保护,这个时间之外立刻失败,这样重放保护的存储数量才是有界的。pay 节点中,除了 gateway, className标准属性外:productPattern:指定一个正则表达式匹配发票中的 product-id,如果不能匹配系统直接拒绝,解析出匹配结果的 group(1)对应的数字,作为本框架中的 product 参数,进行投递。url:发票验证服务器 url

账号系统limax通过 Auany 提供一个支持子账号的凭证式账号系统,分配 sessionid。基本概念1. credential,字符串表示的由 auany的 key签名的凭证,当前系统提供 Session凭证和

Temporary凭证。2. authcode,用户授权码,参与凭证签名。3. serial,凭证序列号,凭证更新之后,序列号加一,系统通过序列号确认凭证有效性,

拒绝过期凭证。4. loginConfig,登录配置对象5. mainid,主账号的 sessionid6. uid,系统内用户标识,为 username@platflag 转换为小写的字符串。7. appid,应用标识,aunay下的每一个应用分配一个 appid,同一 uid下,不同 appid映射了不同的mainid。

8. derive,创建未绑定主账号,或者在主帐号下派生子账号。9. bind,创建绑定主账号,或者进行帐号绑定,或者更新凭证序列号。Session 凭证为了支持子账号,下面的方法可能返回一个形为 credential[,subid]+的字符串,该字符串为完全的 Session凭证表示,其中 subid 即是子账号。

130

Page 131: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

非绑定账号的创建AuanyService.derive(String httpHost, int httpPort, int appid, String authcode, long timeout, Result onresult);在这里 httpHost, httpPort为 auany 提供的 http服务器地址,appid 指明了请求的应用。如果 onresult.errorSource == ErrorSource.LIMAX && onresult.errorCode == SUCCEED ,则onresult.result为返回的初始 Session凭证。绑定账号的创建AuanyService.bind(String httpHost, int httpPort, int appid, String authcode, LoginConfig loginConfig, long timeout, Result onresult);与非绑定账号的创建相比,这里多出了 loginConfig 参数,使用这个参数执行认证,认证成功之后创建一个已经绑定好的 Session凭证。特别的,如果 username,token,platflag 认 证之后映射的 uid 下 appid 已经关联上一个mainid,则递增相应的 serial之后返回完全的 Session凭证表示。这个功能可以用于账号找回。派生子账号AuanyService.derive(String credential, String authcode, long timeout, Result onresult);该方法如果执行成功,则 onresult.result返回完全的 Session凭证表示,如果之前有 N个subid,则返回之后包含 N+1个 subid,最后一个 subid 即为派生出来的新的子账号。派生子账号上限由应用的配置参数maxSubordinates决定。详见,service-auany.xml。账号的绑定AuanyService.bind(String credential, String authcode, LoginConfig loginConfig, long timeout, Result onresult);该方法通过 loginConfig 认证之后进行绑定,返回 Session凭证的完全表示。该方法除了对未绑定账号(凭证)进行绑定外,也可以使用与绑定时相同的 loginConfig进行重复绑定,目的是返回 Session凭证的完全表示。Provider的影响ProviderTransport 接口提供 2个方法

long getMainId();String getUid();

其中,通过 getMainId()返回 mainid与当前的 sessionid比较,可以得知当前的登录是在主帐号下还是子账号下。Session凭证的登录登录参数中 username 指定为 Session 凭证, token 指定为 authcode, platflag 指定为 ” credential” , 如 果 登 录 子 账 号 , platflag 后 缀 ” :subid” 。 尽 可 能 使 用LoginConfig.credentialLogin方法。

131

Page 132: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

移动客户端的考虑凭证方式适合移动客户端实现,满足有需要时再进行帐号绑定的需求。返回的 Session凭证应该进行本地存储,便于下一次访问时直接登录。如果支持子账号,多数情况下需要考虑与相应 UI资源的关联。这里要注意的一个问题就是未绑定账号在登录后执行绑定时的可能出现的意外情况——发起 bind 操作之后,网络断开——这种情况下不能确定是否绑定成功,所以下一次登录时应该首先是用原来的凭证进行登录,如果不成功,则使用非凭证方式进行登录,登录完成后再次进行 bind,直到获得 Session凭证的完全表示,完成本地存储。pc,web客户端的的考虑这两种客户端通常不会在本地存储凭证,可以使用普通方式进行登录,如果有子账号需求可以通过 bind方式获取 Session凭证的完全表示,再派生子账号;或者登录主账号,使用空凭证,空 authcode,调用 derive,这种情况下 onresult.result仅返回新的子账号串。子账号的考虑使用子账号功能的系统内,Provider通常会维护子账号,主账号的关系。这里要注意 2个问题。1. 需要验证 ProviderTransport 提供的mainid, sessionid关系,如果是子账号登录,则验证

从属关系是否改变,如果改变应该进行相应的修改,因为有可能发生了子账号迁移。2. 不能信任客户端提供的子账号 id,例如客户端派生了子账号,不应该提供给 Provider要求在其上创建对应记录。正确的解决办法应该是客户端派生了子账号以后,使用子账号进行登录,Provider发现对应 sessionid的相关记录没有创建,则创建之。

Temporary 凭证Temporary凭证,在 Session凭证的基础上添加了时效性,过期则失效;添加 subid属性,可以直接标识子账号;添加用法属性,支持两种用法——临时登录,账号传送。创建 Temporary凭证1. AuanyService.temporary(String credential, String authcode, String authcode2, long

millisecond, byte usage, String subid, long timeout, Result onresult);2. AuanyService.temporary(String httpHost, int httpPort, int appid, String credential, String

authcode, String authcode2, long millisecond, byte usage, String subid, long timeout, Result onresult);

3. AuanyService.temporary(LoginConfig loginConfig, int appid, String authcode,long millisecond, byte usage, String subid, long timeout, Result onresult);

4. AuanyService.temporary(String httpHost, int httpPort, int appid, LoginConfig loginConfig, String authcode, long millisecond, byte usage, String subid, long timeout, Result onresult);

方法 1,2,通过 Session凭证创建临时凭证,方法 2无需在登录状态下执行。方法 3,4,通过登录账号方式创建临时凭证,方法 4无需在登录状态下执行。

132

Page 133: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

方法 1,3的 authcode2 参数与方法 2,4的 authcode 参数,指定了临时凭证的 authcode。millisecond 参数指明了临时凭证自创建开始计算的有效时间,单位毫秒。usage 参数指 明了临时凭证用法, USAGE_LOGIN = 1,指 示 该 临时凭证用于 登录,USAGE_TRANSFER = 2,指示该临时凭证用于账号传送。subid 参数指明了子账号标识串,空串代表临时凭证作用于主账号。执行成功后 onresult.result返回了临时凭证。Temporary凭证的登录类似 Session凭证的登录,不同的是 platflag中的子账号后缀无意义,因为临时凭证本身包含了 subid。账号传送AuanyService.transfer(String httpHost, int httpPort, int appid, LoginConfig loginConfig, String authcode, String temp, String authtemp, long timeout, Result onresult);在这里 httpHost, httpPort为 auany 提供的 http服务器地址,appid 指明了请求的应用。loginConfig,指明了接收账号,authcode为账号接收之后生成的 Session凭证的授权码。temp, authtemp,为临时凭证及其授权码。执行成功之后 onresult.result返回接收账号的 Session凭证的完全表示。这里需要注意:1. 必须通过一个有效账号来接收传递账号。2. 临时凭证指定的账号被传递,可以是主账号——这种情况连同所有子账号一起被传递,接收账号必须没有绑定过该应用,否则报告 appid冲突;可以是子账号,这种情况下如果接收账号没有绑定过该应用,则首先建立主账号,再添加该子账号,如果绑定过,在不违反 maxSubordinates 限制的前提下添加子账号。

3. 如果应用提供了子账号传送支持,则 Provider实现时必须验证 mainid,sessionid的关系。

注意事项1. 以 httpHost,httpPort,appid 三个参数开头的方法不需要在账号登录的状态下执行,这些方法为同步方法,onresult 被触发之后返回;其它方法均为异步方法。

2. 只要账号进行了绑定,任何时候都可以使用绑定时的账号参数调用 bind方法获取Session凭证的完全表示。

3. 通过方法 EndpointManager.getAccountFlags(),ProviderTransport.getAccountFlags()的调用Provider 与 Endpoint 均可获取当 前 登录帐号的标记,该标记与 SessionFlags. FLAG_ACCOUNT_BOUND = 1 和 SessionFlags. FLAG_TEMPORARY_LOGIN = 2进行掩码运算即可获知当前登录帐号是否绑定,是否临时登录帐号,从而采取特定应对。

4. 临时凭证主要提供给他人使用,如果有效期之内反悔,可以使用绑定时的账号参数调用 bind方法,递增 serial,使得之前生成的临时凭证全部失效。没有绑定过的账号没有机会反悔,原因在于没有绑定过的账号禁止递增 serial,一旦递增 serial,新凭证因为网络原因丢失,老凭证又无法登录,就意味着账号的丢失。

133

Page 134: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

应用配置一个 Limax运营环境由一个 auany服务器支撑,一个运营环境下可以运行多个应用下的服务,每个应用可以运行多个并列服务,一个服务可以由多个 Provider构成。对于一个特定的应用,允许 Provider发布公共信息提供给客户端使用。Provider信息发布Provider的框架周期性向应用 Provider请求 JSON 对象,该对象最终通过 auany的 http服务发布出去。实现public interface JSONPublisher {

JSONSerializable getJSON();long getDelay();

}

Provider实现 ProviderListener的同时,与之并列实现 JSONPublisher接口,其中:getJSON:返回一个 JSON 对象。getDelay:系统下一次获取 JSON 对象的延迟,单位毫秒。配置Provider无需额外配置,auany的应用配置中存在一个 jsonPublishDelayMin属性(之后介绍 ) , 该 属 性 约 束 了 delay 的 最 小 值 , 也 就 是 说 , max(jsonPublishDelayMin, JSONPublisher.getDelay())决定了实际延迟,这个配置属性可以防止 JSONPublisher的错误实现导致 Provider攻击 auany。Endpoint应用信息获取endpoint 向 auany 提供的 http服务器请求应用配置信息,解析为对应语言的描述。实现上即是访问 http://auanyserver/app?{servicetype}=appid [&additionalQuery] ,获取一个 JSON 对象 进 行 解 析 , 其 中 servicetype : { native, ws, wss } , 对 于 非 WebSocket 应 用 ,servicetype=native。additionalQuery为 http代理服务器约定的查询参数,用以过滤 auany返回的结果集。实现List<ServiceInfo> Endpoint.loadServiceInfos(String httpHost, int httpPort, int appid, long timeout, int maxsize, File cacheDir, boolean staleEnable);List<ServiceInfo> Endpoint.loadServiceInfos(String httpHost, int httpPort, int appid, String

134

Page 135: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

additionalQuery, long timeout, int maxsize, File cacheDir, boolean staleEnable);

其中:httpHost,httpPort:auany http服务地址appid:运营框架为该应用分配的 idadditionalQuery:http代理服务器约定的查询参数timeout, maxsize, cachedir, staleEnable: http 服务请求参数, timeout 决定了请求超时,maxsize 约束了返回信息的最大尺寸,cacheDir 提供了本地缓存目录,staleEnable决定了请求失败后是否使用本地缓存中的陈旧数据。同一应用可能运行多个并列服务,所以返回 List,其中的 ServiceInfo,提供 5个方法:1. int[] getPvids();获取该服务引用的 pvid,顺序由 auany配置决定。

2. int[] getPayids();获取该服务的支付 id,一般来说一个服务应该只有一个。

3. JSON[] getUserJSONs()这里的 JSON 对象就是通过前面提到的 JSONPublisher 提供的,因为一个服务可能由多个provider 提供,provider是否支持 JSON通告是可选能力,所以这里返回 JSON数组,数组尺寸与 getPvids()返回的尺寸未必一致。

4. boolean isRunning()该服务是否正在运行。

5. String getOptional()这个方法与运营环境相关,返回运营环境为服务设定的特殊信息,例如,维护信息。

Auany配置appconfig.xml 提供了 auany上的应用配置,该配置文件在系统运行过程中可以通过 merge方式添加新的信息,而不需要停机维护。switcher 相关配置<switcher host="127.0.0.1" id="1" key="abc" port="10000" type="native"/><switcher host="127.0.0.1" id="2" key="abc" port="10001" type="ws"/>auany 内部的 switcher与 switcher服务的配置文件相对应,其中:type:switcher类型,包括 native,ws,wss 三种,native 对应 limax网络服务,ws,wss 对应WebSocket服务,其中wss支持 ssl。id:SwitcherId,在整个运营环境中唯一分配key:与 switcher服务器配置的 key 对应,用来认证 switcher服务器。host,port:switcher服务器对外服务的地址。对照 switcher服务器配置文件 service-switcher.xml中 Switcher条目:<Switcher key="abc" …… >

……………………………..

135

Page 136: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

<native id="1"/> <ws id="2"/></Switcher>

Switcher 启动时,通过构造 { key, native-id[], ws-id[], wss-id[] } 信息向 auany发起通告,如果不能匹配 auany中的配置信息,auany 将命令 switcher退出,同时在 switcher日志中报告错误原因。注意:1. 不同 Switcher服务器应该配置不同 key,避免 id配置错误导致冲突。2. 同一类型的 Switcher服务可能存在多个,对应绑定不同的网络接口或者端口。3. Switcher配置中的是否应该存在某种类型取决于接入服务的Manager配置。4. auany配置中的 host无法与 Switcher配置中接入服务Manager下的 localIp属性对应,因为无法确定 Switcher之前是否存在 NAT,这里的 host必须填写真实的对外 IP。

共享 provider 相关配置<provider id="1" jsonPublishDelayMin="0" key=""/><provider id="12" jsonPublishDelayMin="0" key=""/>

id:ProviderId,运营环境内部全局唯一。key:provider的 key,用以验证 provider服务合法性,与 Provider配置中的 key 对应。jsonPublishDelayMin:该 provider的两次 JSON 对象请求之间的最小时间间隔,为 0表示禁止该 provider发布 JSON 信息。共享 provider 被应用配置中的 app条目,或者之下的 service条目引用。没有被任何 app或者 service 引用的 provider为可选的共享 provider,客户端登录时可以请求这样的 provider 进行服务,也可以不请求。其中 provider id=1 被 Auany 保留,提 供AuanyService。应用配置<app id="1" jsonPublishDelayMin="15000" maxSubordinates="0" providerMatchBidirectionally="true" shareProvider=""> <service id="1" shareProvider="" switcher="1,2,3"> <provider id="100" key=""/> </service></app><app id="2" jsonPublishDelayMin="5000" maxSubordinates="8" providerMatchBidirectionally="true" shareProvider="12"> <service id="1" shareProvider="" switcher="1,2,3"> <provider id="200" key=""/> </service> <service id="2" shareProvider="" switcher="1,2,3">

136

Page 137: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

<provider id="300" key=""/> </service></app>

app 节点:<app id="1" jsonPublishDelayMin="15000" maxSubordinates="0" providerMatchBidirectionally="true" shareProvider="">id:应用 id,运营环境内部全局唯一。jsonPublishDelayMin:该应用中使用的公共 Provider之外的所有私有 Provider的 JSON 对象发布时间间隔。maxSubordinates:该应用支持的最大子账号数,为 0表示禁止使用子账号。providerMatchBidirectionally:provider 列表双向匹配,如果为 true,请求的 provider 列表与配置的 provider 列表双向匹配;如果为 false,允许请求配置的 provider 列表的非空子集。shareProvider:逗号分隔的共享 ProviderId 列表,禁止引用 Auany服务的 id(=1)。service 节点:<service id="1" shareProvider="" switcher="1,2,3">id:服务 id,一个应用配置下保证唯一。shareProvider:逗号分隔的共享 ProviderId,禁止引用 Auany服务的 id(=1)。switcher:逗号分隔的 SwitcherId 列表。私有 provider 节点:<provider id="200" key=""/>类比共享 provider 节点,这里缺少 jsonPublishDelayMin属性,该属性继承自 app 节点。注意:1. 共享 provider,私有 provider在同一分配域中分配 id,也就是说共享 provider,私有

provider的 id之间也不允许冲突。2. 客户端向服务器发起应用信息请求,最重要的信息就是下一步需要连接的 switcher地址,auany 只通告已经启动的 switcher,如果配置文件中配置了,但是实际的 Switcher服务没有启动,对应的配置地址不会通告。如果 service中相应 type的所有 Switcher服务都没有启动,则通告的服务运行状态一定为 false。

3. 服务信息中 pvid通告顺序由私有 provider 列表,service 引用的共享 Provider 列表,app引用的共享 provider 列表决定。

4. 如果一个服务引用的 provider没有全部启动,则通告的服务运行状态为 false。添加新的服务配置auany配置文件中,Auany 节点中两个属性appConfigPatch:服务 patch文件名,默认为 appnew.xmlappConfigPatchCheckPeriod:服务 patch文件检测周期,默认 30000,即 30秒具体操作:

137

Page 138: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

1. 建立一个格式与 appconfig.xml 相同的配置文件 appnew.xml。其中,可以创建新的switcher,新的共享 provider,新的 app,已经存在 app中新的 service,只要不存在 id冲突即可。

2. 将 appnew.xml拷贝到 appconfig.xml 相同目录下,最迟 30秒,appnew.xml 被改名为appnew.xml.result,文件尾部追加了merge结果信息。

3. 如果成功,新的配置被装载进 auany服务,appnew.xml 被 merge到 appconfig.xml中,原来的 appconfig.xml备份为 appconfig.xml.bak。

4. 如果失败,追加的 merge结果信息指出了错误原因,比如操作异常,id冲突,这种情况下正在运行的服务不受影响,appconfig.xml没有改变。

更新具体服务的 Optional 信息。public interface AppManagerMXBean {

void setServiceOptional(int appid, int serviceid, String optional);}通过访问 Auany 的 JMX 服务,以 appid, serviceid 为 key, optional 为 value,调用setServiceOptional方法。之后,相应的 Endpoint上通过 ServiceInfo.getOptional()即可取得该信息。注意事项1. 应用应该通过自己的 JSON通告提供服务名之类的信息,不要过度依赖 auany通告的服务信息,一种典型的实现是创建一个 map,提取 JSON通告的应用服务信息作为 key去索引通告返回的 ServiceInfo。

2. Endpoint 提供的各种与 switcher ip,switcher port 相关的请求接口都提供了对应的ServiceInfo 版本,使用 ServiceInfo 前,必须判断服务是否处于运行状态,避免产生运行时错误。

3. 大运营环境下,为了降低 auany http服务负荷,可以部署 http代理服务器进行加速。

JSON 支持Limax 提供完整的 JSON支持(Java,C#,C++,Lua 版本均提供与 Javascript一致的 JSON支持,使用上最大限度保证与 javascript 相同),便于同其它支持 JSON的第三方系统进行交互。编码Javascript JSON.stringify(obj)Java String limax.codec.JSON.stringify(Object obj);C# string limax.codec.JSON.stringify(object obj);C++ std::string limax::JSON::stringify(const T& object);

std::string limax::JSON::stringify(const char *obj);

138

Page 139: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

std::string limax::JSON::stringify(std::shared_ptr<limax::JSON> obj);Lua JSON.stringify(obj)

C++,Lua 版本中的串应该使用 UTF8 编码。类型映射Javascript Java C# C++ LuaNumber byte

BytesbyteSByte

int8_t Number

byteByte

uint8_t

shortShort

shortInt16

int16_t

ushortUInt16

uint16_t

intIntegerAtomicInteger

intInt32

int32_t

uintUInt32

uint32_t

longLongAtomicLong

longInt64

int64_t

ulongUInt64

uint64_t

floatFloat

floatFloat

float

doubleDouble

doubleDouble

double

boolean booleanBooleanAtomicBoolean

boolbool

bool boolean

string

charCharacter

String

charCharstringString

charconst char*const std::string&

string

Array java.reflect.ArrayCollection

IEnumerable std::list<T>std::vector<T>

table

139

Page 140: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

std::deque<T>std::unordered_set<T>std::set<T>

object JSONSerializableJSONMarshalMap

JSONSerializableJSONMarshalIDictionary

JSONMarshalstd::unordered_map<K,V>set::map<K,V>

table

null null null

JSON JSON

const JSON&std::shared_ptr<JSON>

注意, JSON 规范对应的类型 object,在之后文 档中写作 JSON/Object,避免与Java,C#,C++实现的 JSON类构造出来的对象 JSON Object 混淆。数组就写作 JSON/Array,以此类推。这些通称 JSON 元件。1. Java的 java.reflect.Array表示支持原生数组,Collection 对应了各种集合容器。2. C#所有容器,原生数组均派生自 IEnumerable,所以编码时优先 IDictionary检测,区分出是否需要按 JSON/Object 编码。

3. C++版本不支持任何类型的原生数组,或者说不支持指针方式指向的对象,const char*的支持只是一种语法糖,这种参数进入编码器前,立刻被转换成 std::string。既然不支持指针方式指向的对象,也就意味着不会编码生成 null,除非在 JSONBuilder上使用null()方法硬编码一个 null,或者进行中继,见后。

4. Lua中 table的使用乱七八糟,编码器通过 #table>0的方法区分 JSON/Array与 JSON/Object,如果需要正确编码,不要在一个 table上混用数组操作和对象操作。Lua的 nil解释为 null。

5. null不利于各语言版本互操作,为了减少不必要的麻烦,尽可能避免使用。6. 关联容器Map,IDictionary,std::unordered_map<K,V>,std::map<K,V>,table 被编码为

JSON/Object时,其中的 key 被强制转换为字符串,按 JSON/String 编码。如果容器定义了非 string类型的 key,应该小心验证强制转换的结果是否符合预期。

7. 如果编码结果提交给 Javascript处理,必须注意到 Javascript的 Number最多支持 53 位整型。

8. 表格的最后一行没有 Javascript的对应,Java,C#,C++,执行 JSON中继任务时使用,见后。

代码生成实现(Java,C#,C++)Java,C#,C++三种类型化语言,使用 JSONMarshal 与 JSONBuilder 进行交互完成 JSON/Object的编码。limax 环境下,具体对象的 JSONMarshal.marshal方法可以通过代码生成实现。只要在 bean,cbean,xbean,protocol的 xml描述中正确使用 json属性即可,例如:

<bean name="JChild" json="true">

140

Page 141: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

<variable name="val" type="string"/></bean>

<bean name="JBean" json="true"><variable name="intset" type="set" value="int"/><variable name="child" type="JChild"/><variable name="ignore" type="binary" json="false"/>

</bean>使用生成的 JBean类型的对象,调用 JSON类的静态方法 stringify,即可获得类似如下的输出结果:

{"intset":[1,2],"child":{"val":"it's child"}}

bean,cbean,xbean,protocol 元素上设定属性 json="true",允许为该类型生成 JSON/Object 编码代码,同时默认下属 variable元素的 json属性为 true,除非为 variable 单独设置属性 json="false",关闭该元素对应字段的代码成。上例中 JBean.ignore 字段,在编码时被忽略。代码生成时,将严格检查 xml描述,确保生成有效的 JSON/Object 编码代码,否则抛出异常,停止生成。具体来说,3种情况:1. 使用了 binary类型2. 使用了 any类型3. 引用的 bean没有开启 json="true"

反射方式实现(Java,C#)Java,C#支持反射,只需要在定义类的时候实现 JSONSerializable标签接口即可,支持该标签的对象在编码时被直接解释为 JSON/Object,字段名即是对象的 key,例如定义两个 Java类:

public class JChild implements JSONSerializable {private String val = "it's child";

}

public class JBean implements JSONSerializable {private int intset[] = new int[] { 1, 2 };private JChild child = new JChild();private transient Octets ignore;

}

System.out.println(JSON.stringify(new JBean())) 即可输出与上面例子相同的结果。在这里 Java关键字 transient,阻止了 ignore 字段的编码,C#没有类似特性,必须注意。特别的,C#类中定义的属性字段在反射时将获得一个 C#内部编码的字段名,需要 JSON 编码的类尽量避免使用属性字段。

141

Page 142: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

编码器执行反射时,只 考 虑 对象所属类本身,不考 虑基类,即 便基类同样实现了JSONSerializable接口。如果被反射的字段类型没有在类型映射一节的表格中列出,编码时将抛出 JSONException异常,编码失败。详见后节——异常规范。关联容器方式实现(Java,C#,C++)直接对一个关联容器编码即可。例如 Java代码:

Map<String, Object> jchild = new HashMap<>();jchild.put("val", "it's child");Map<String, Object> jbean = new HashMap<>();jbean.put("intset", new int[]{1,2});jbean.put("child", jchild);System.out.println(JSON.stringify(jbean));

可以输出与上面例子相同的结果。C++中基本类型缺少一个公共基类,所以上述结果无法实现。

std::unordered_map<std::string, std::string> jchild;jchild.insert(std::make_pair("val", "it's child"));std::unordered_map<std::string, std::unordered_map<std::string, std::string>> jbean;jbean.insert(std::make_pair("child", jchild));printf("%s\n", JSON::stringify(jbean).c_str());

只能拼凑出规整的结果:{"child":{"val":"it's child"}}

由上可见:1. 对于 C++,多数情况下只有使用代码生成方式,或者参考生成的代码,手工实现。2. 关联容器方式比生成代码方式,反射方式可读性差很多,非特殊情况不值得使用。解码Javascript JSON.parse(string)Java JSON JSON.parse(String s);C# JSON JSON.parse(string s);C++ std::shared_ptr<JSON> JSON::parse(std::string s);Lua JSON.parse(string)

实现(Java,C#,C++)1. JSON 对象上可以通过 isNull(),isBoolean(),isNumber(),isString(),isArray(),isObject()方法测试 JSON元件类型。

142

Page 143: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

2. isObject()返回 true,指出该 JSON 对象描述的是 JSON/Object,允许在该对象上执行下面的方法,否则抛出 JSONException

Java JSON get(String key);C# JSON get(string key);C++ std::shared_ptr<JSON> get(const std::string& key) const;

上述方法,以 key为字段名查询 JSON/Object,返回相应字段的 JSON表示。如果当前JSON/Object 对应的 key不存在,同样返回一个 JSON 对象,在该对象上调用 isUndefined()方法,将返回 true。Java Set<String> keySet();C# ICollection<string> keySet();C++ std::vector<std::string> keySet() const;

上述方法,返回 JSON/Object 所有字段名3. isArray()返回 true,指出该 JSON 对象描述的是 JSON/Array,允许在该对象上执行下面的方法,否则抛出 JSONException

Java JSON get(int index);C# JSON get(int index);C++ std::shared_ptr<JSON> get(size_t index) const;

上述方法,以 index为下标,查询 JSON/Array,返回数组元素的 JSON表示。如果 index超出范围,同样返回一个 JSON 对象,在该对象上调用 isUndefined()方法,将返回 true。Java JSON[] toArray();C# JSON[] ToArray();C++ std::vector<std::shared_ptr<JSON>> toArray() const;

上述方法,返回 JSON/Array 所有元素构成的 JSON 对象数组。4. booleanValue()方法,与 Javascript完全一致:如果 JSON 对象描述的是 JSON/true,返回 true。如果 JSON 对象描述的是 JSON/false,返回 false。如果 JSON 对象描述的是 JSON/null,或者 isUndefined()测试为 true,返回 false如果 JSON 对象描述的是 JSON/Number,值为 0,返回 false如果 JSON 对象描述的是 JSON/String,长度为 0,返回 false其余情况返回 true

5. intValue(),longValue(),doubleValue()方法如果 JSON 对象描述的是 JSON/Number,执行相应的类型转换然后返回。如果 JSON 对象描述的是 JSON/String,并且该 string能够转换为目标类型(没有数字格

式错误),转换之后返回其余情况抛出 JSONException

6. toString()/ToString()方法如果 JSON 对象描述的是 JSON/String,返回相应的串值如果 JSON 对象描述的是 JSON/Number,转换为数值串返回如果 JSON 对象描述的是 JSON/true,返回串表示的 true如果 JSON 对象描述的是 JSON/false,返回串表示的 false如果 JSON 对象描述的是 JSON/null,返回串表示的 null如果 JSON 对象 isUndefined()测试为 true,返回串表示的 undefined

143

Page 144: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

其余情况,不同语言有不同返回,与各自的 toString/ToString 相关示例若有串 {"x":[10,"abc", 2], "y": null, "z": true}表示的 JSON 对象,返回 x 字段的首个元素可

以有如下实现:Javascript JSON.parse("{\"x\":[10,\"abc\", 2], \"y\":null,\"z\":true}").x[0]Java JSON.parse("{\"x\":[10,\"abc\", 2], \"y\":null, \"z\": true}").get("x").get(0).intValue();C# JSON.parse("{\"x\":[10,\"abc\", 2], \"y\":null, \"z\": true}").get("x").get(0).intValue();C++ JSON.parse("{\"x\":[10,\"abc\", 2], \"y\":null,\"z\":true}")->get("x")->get(0)->intValue();Lua JSON.parse("{\"x\":[10,\"abc\", 2], \"y\":null,\"z\":true}").x[1]

与 Javascript 版本比较,其它版本的实现抛开语言差异看,几乎完全一致。多数情况下,都只是简单使用已知格式的 JSON元件描述,像上面的例子一样,无需作更多的测试,直接连写即可。中继某些情况下,需要实现这样一种系统——接收来自上游系统的 JSON 串表示,解码该串执行设计要求的处理之后,将整个 JSON元件或者 JSON元件的一部分,作为本地系统的数据,重新打包为 JSON 串,传递给下游系统——JSON中继系统。实现(Javascript,Lua)脚本语言执行 parse之后在名字空间内生成相应的对象层次结构。如果有中继需求,创建本地对象,按照相应语言的名字空间规范,引用或者部分引用解码结果,最后执行 stringify即可。实现(Java,C#,C++)参见之前的类型映射表格最后一行可知, JSON 编码器的输入参数,支持 JSON解码器返回类型。也就是说 parse/stringify可逆。parse/stringify可逆,即是实现 JSON中继的关键,唯一需要注意的是,在 JSON/Object或者JSON/Array上执行相应的 get 操作之后,返回的 JSON 对象 isUndefined()测试为 true的情况下,该返回对象不可中继,因为该对象并非来自上游系统,不可能存在一个正确表示,如果在这样的对象上执行 stringify 将抛出 JSONException。异常规范为了确保程序健壮性,任何语言实现执行 JSON 编码解码操作,都必须考虑异常。编码异常1. 遭遇不可解释的字段类型,不可编码2. 遭遇 isUndefined()测试为 true的 JSON 对象

144

Page 145: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

3. 对象环引用4. 各种运行时异常,例如并发访问异常,字段读取失败解码异常1. JSON的串表示语法错误解码数据获取异常1. 不正确地访问 JSON元件2. 数据类型转换失败3. 各种运行时异常,例如并发访问异常。Javascript的考虑1. 编码过程遭遇环,抛出异常;类型映射表不支持的类型被忽略,例如 function2. 解码过程遭遇语法错误抛出异常3. 解码结果访问过 程中,访问对象不存在的字 段,访问数组越界,返回

undefined,undefined上执行除 bool 测试,算术运算之外的访问抛出异常。4. 应该用 try {} catch(e) {} 方式捕获处理异常Lua的考虑1. 编码过 程遭遇环,抛出异常;类型映射表不支持的类型被 忽 略,例如

function,userdata2. 解码过程遭遇语法错误抛出异常3. 解码结果访问过程中,访问对象不存在的字段,访问数组越界,返回 nil,nil上执行除

bool 测试之外的访问抛出异常。4. 应该使用 pcall(function, args)方式捕获处理异常Java,C#的考虑1. 编码过 程遭遇环,抛出异常;遭遇类型映射表不支持的类型抛出异常;遭遇

isUndefined()测试为 true的 JSON 对象,抛出异常;并发访问异常。特别的,服务器端XBean如果支持 JSONMarshal,与正常访问 XBean规则一样,不能离开相应的锁,编码过程必须在事务环境中完成,否则抛出异常。

2. 解码过程遭遇语法错误抛出异常3. 解码结果访问过程中,访问对象不存在的字段,访问数组越界,返回 isUndefined()测

试为 true的 JSON 对象,这样的对象上除了 booleanValue(), isXXX()测试,toString()/ToString() 操 作外,抛出异常;非 JSON/Object 上 get(string),keySet()抛出异常,非JSON/Array 上 get(int) , toArray()/ToArrray() 抛 出 异 常 ;intValue(),longValue(),doubleValue()转换失败时抛出异常。

4. 各种类型的异常最终被 JSONException包裹,所以应该使用 try {} catch(JSONException e){}的方式捕获处理。

145

Page 146: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

C++的考虑1. C++不支持编码指针指向的对象,不可能出现环;遭遇类型映射表不支持的类型时,编译阶段就会报错;不考虑并发;所以仅在遭遇 isUndefined()测试为 true的 JSON 对象时抛出异常。

2. 解码过程遭遇语法错误抛出异常3. 解码结果访问过程中,访问对象不存在的字段,访问数组越界,返回 isUndefined()测

试为 true的 JSON 对象,这样的对象上除了 booleanValue(),isXXX()测试,toString()操作外,抛出异常;非 JSON/Object上 get(std::string),keySet()抛出异常,非 JSON/Array上 get(size_t),toArray()抛出异常;intValue(),longValue(),doubleValue()转换失败时抛出异常。

4. JSON 操 作抛出 JSONException,应该使用 try{} cache(JSONException e){} 方式捕获,e.message可获取抛出异常代码的行号,供 debug使用。

字符集编码问题理论上看,JSON 编码解码过程只对应字符串的输出输入,传输过程中这些字符串如何进行字节流的编码解码,不是 JSON需要考虑的问题。然而,实际应用的设计很可能涉及到底层问题。理想情况发送端接收端使用兼容的字节流编码解码器,两边的 JSON 编码解码器均能获得正确的字符串表示,例如,limax通过 Websocket与浏览器交互,两端均使用 UTF8 编码解码,JSON的使用不存在任何问题。较好情况(Java,C#)Java的 java.nio.charset包,C#的 System.Text.Encoding类,提供了完善的编解码器,如果有特殊需求,可以灵活适配对端系统。糟糕情况(C++, Lua)只支持 UTF8,\uHHHH 格式编码的 Unicode 字符在解码阶段被转换为 UTF8。高级功能非标准 JSON 串(Java,C#,C++,Lua)某些陈旧系统的编码器,将 JSON/Object 串描述中的冒号写作等号,分割 JSON元件的逗号写作分号,解码器可以容忍这样的输入。另外,true,false,null 三个 JSON元件,解码器按大小写不敏感处理。

146

Page 147: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

编码流(Java,C#)public static void encode(Object obj, Appendable a);JSONEncoder 提供上面的方法,允许编码器与 limax.codec其它流处理模块协同,直接向目的输出。OutputStream os = ...;JSONEncoder.encode(json, new CharSource(StandardCharsets.UTF_8, new SinkStream(os)));例如,上面的代码,将 json 对象编码结果,看作一个字符流源,通过 UTF8 编码器,编码成字节流,再发送到输出流。这样的过程,注意捕获异常,流中处理过程中所有异常最后都会包裹到 JSONException中然后抛出。流解码(Java,C#,C++)Java,C#,C++三个版本内部通过流方式解码,输入为字节流,而不是简单的串输入。1. JSONDecoder通过 void accept(char c);方法获取字符输入,该方法可能抛出异常。C++

版本抛出 JSONException,Java 版本抛出 RuntimeException,C#版本抛出 Exception。2. 通过 JSONDecoder()方法构造的解码对象只能解码一个 JSON元件,解码完毕后通过

JSON get() 方法获取 JSON 对象3. JSONDecoder(JSONConsumer consumer)方法允许提供一个 consumer,获取解码后的对象(连续解码 JSON元件)

4. Java, C# 版本的 JSONDecoder 实现了 limax.codec.CharConsumer 接口,允 许与limax.codec其它流模块协同解码。

例一 (Endpoint.java 中解码来自 Auany的服务配置信息的方法)public static List<ServiceInfo> loadServiceInfos(String httpHost, int httpPort, int appid, long timeout, int maxsize, File cacheDir, boolean staleEnable) throws Exception {

JSONDecoder decoder = new JSONDecoder();new HttpClient("http://" + httpHost + ":" + httpPort + "/app?native=" + appid, timeout,

maxsize, cacheDir,staleEnable).transfer(new CharSink(decoder));List<ServiceInfo> services = new ArrayList<ServiceInfo>();for (JSON json : decoder.get().get("services").toArray())

services.add(new ServiceInfo(appid, json));return services;

}在这里 HttpClient下载指定 url的内容,通过 CharSink适配,将字节流传送给解码器,之后在解码器上执行 get(),获取 JSON 对象。解码器套上适配器后,异常处理只需要考虑最外层,在这里就是 HttpClient.transfer 抛出的 IOException。之后的 JSON 访问可能抛出JSONException,整个方法简单处理,指示抛出 Exception。例二(C++实现的多 JSON解码)JSONDecoder jd([](std::shared_ptr<JSON> json) {printf("%s\n", json->toString().c_str()); });for (auto c : std::string("{\"a\":10}[2,3]5[12]"))

147

Page 148: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

jd.accept(c);这个例子会输出如下 4行:<Object><Array>5<Array>输出结果刚好反映了 JSON输入串的结构。需要注意的是,如果在这个串的最后添加一个数字,那么输出结果还是上面 4行。流方式处理时,数字的解码比较特殊,不参考下一个字符,不可能决定该数字是否结束,不像串使用双引号结束。所以 JSON.parse 通过JSONDecoder实现的解码代码,在最后会调用一次 JSONDecoder的内部方法 flush,flush简单地 accept一个空格,JSON规范规定了 JSON元件之间的白空格均被忽略,用最后加入空格的方法正好解决数字的问题。另外,还必须注意 JSON.parse 只能解码一个 JSON元件,如果用上面代码中的串调用 JSON.parse,解码器处理完毕"{\"a\":10}",遭遇到第一个非白空格,也就是之后的"[",即会抛出异常,用 Javascript,Lua 版本执行这样的 parse同样是这个效果,各语言版本语义一致。

CLR/Lua

Limax 提供一个 clrlua项目,粘合 C#与 Lua,支持 C#与 Lua代码之间的互操作,用于实现 C#脚本模式框架下的 LuaScriptHandle。这一章介绍 clrlua 提供的功能。编程接口limax.script.Lua

创建该对象也就创建了一个 lua 虚拟机,使用该对象即可与 lua 虚拟机交互。构造函数: Lua(limax.script.Lua.ErrorHandle eh)

ErrorHandle 定义为委托 delegate void ErrorHandle(string msg); 用以接收 lua 错误信息。成员函数: object eval(string code)

执行 code表示的代码 chunk,返回结果。 object eval(string codepattern, params object[] parameters)用变长参数列表填充 codepattern中的占位符<0>,<1>......<n>,然后执行 chunk,返回结果。占位符通过与之对应创建的上下文变量进行关联,在模板代码中应该当作变量名使用,而不能用引号括起来,否则将替换结果将是一个上下文变量名串。返回的结果可能是任何类型,特别的可能返回 LuaObject,LuaTable,LuaFunction 三两个类型。

Lua name(string chunkname)为后续 eval 命名,如果运行时错误发生在被命名的 eval的代码块中,错误信息中会前缀该名字,便于调试。该名字内部使用 UTF8 编码,建议不要使用非 ASCII 字符。

148

Page 149: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

limax.script.LuaObject

无法转换为 C#类型的 Lua 对象,哪些无法转换在类型映射一节介绍,该对象可以被 C#持有,该对象上不提供任何操作,多数情况下需要传回 lua 虚拟机使用。limax.script.LuaTable

该对象派生自 LuaObject,实现了 System.Collections.IDictionary接口,关联到 lua 虚拟机中的 table上。访问该对象,也就访问了虚拟机中对应的 table,该对象上执行修改操作将体现到虚拟机中对应的 table上,反之亦然。limax.script.LuaFunction

一个变长参数委托,包装了一个派生自 LuaObject,关联到 lua 虚拟机中的 function上的对象。在该委托上进行调用,相当于调用了虚拟机中对应的 function。注意,通过 limax.script.Lua创建的对象,在之后的文字中命名为 Lua 操作对象,区别于LuaObject。C#与 Lua的互操作举例首先定义 Lua lua = new Lua((string msg)=>Console.WriteLine(msg));

C#操纵 Lua

C#操纵 Lua,通过 eval方法调用实现。 hello world

lua.eval("print('hello world')");lua.eval("print(<0>)", "hello world");Console.WriteLine(lua.eval("return 'hello world'"));Console.WriteLine(lua.eval("return <0>", "hello world"));输出:hello worldhello worldhello worldhello world其中,第一行代码无需解释,第二行代码说明了占位符的使用,第三行代码说明了eval可以返回 lua 虚拟机中持有的值,第四行代码说明了一个值可以在 C#,Lua间反复传递。

好好学习,天天向上lua.eval("print('好好学习,天天向上')");lua.eval("print(<0>)", "好好学习,天天向上");Console.WriteLine(lua.eval("return '好好学习,天天向上'"));Console.WriteLine(lua.eval("return <0>", "好好学习,天天向上"));输出:

149

Page 150: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

濂藉ソ瀛︿範锛屽ぉ澶╁悜涓濂藉ソ瀛︿範锛屽ぉ澶╁悜涓好好学习,天天向上好好学习,天天向上这个例子说明了,C#中的 Unicode 字符串传递给 Lua的时候使用 UTF8 编码,从 Lua传递回来使用 UTF8解码。所以对于 Unicode 字符,Lua 虚拟机看来是乱码,回到 C#将变得正确。

执行代码片断返回多值object[] a = (object[])lua.eval("return 1,2,3");foreach (object o in a)

Console.WriteLine(o);输出:123这个例子说明了,如果 Lua代码返回多值,这些值被放置在 object[]中返回。

执行代码片段返回 tableIDictionary dict = (IDictionary)lua.eval("t = { a = 'A', b ='B' }\n return t ");foreach (DictionaryEntry e in dict) Console.WriteLine(e.Key + ":" + e.Value);输出:b:Ba:A

继续执行:dict.Remove("a");dict.Add("c", "C");lua.eval("print (t.a)");lua.eval("print (t.c)");输出:nilC这里可以看见,C#中的修改影响到了 lua 内部的 table

接续执行:lua.eval("t.d = 100");foreach (DictionaryEntry e in dict) Console.WriteLine(e.Key + ":" + e.Value);输出:d:100c:Cb:B这里可以看出,lua 内部对 table的修改,也反映到 C#一边。

150

Page 151: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

执行代码片断返回 functionLuaFunction func = (LuaFunction)lua.eval("return function(a,b) return a + b; end");Console.WriteLine(func (10, 20));输出:30这里可以看出,返回一个 lua函数在 C#中使用非常容易。

Lua 操纵 C#

首先定义一个 C#实验类。public class TestLua{

public TestLua() { Console.WriteLine("Construct TestLua"); acc = add; }public TestLua(int x) { Console.WriteLine("Construct TestLua with " + x); acc = add; }public int add(int a, int b) { return a + b; }public string mystr;public int Prop { get; set; }public int this[int x]{

get { return x + 1; }set { Console.WriteLine("Indexed Property set key = " + x + " value = " + value); }

}public delegate int ACC(int a, int b);public ACC acc;

}这里应该看到,需要通过 Lua访问的对象构造函数,方法,字段,属性,委托,必须声明为 public。 构造对象

lua.eval("t0 = <0>()", typeof(TestLua));lua.eval("t1 = <0>(100)", typeof(TestLua));Console.WriteLine(lua.eval("return t0") is TestLua);输出:Construct TestLuaConstruct TestLua with 100True在这里,类 TestLua 被传递进 Lua 虚拟机,在类上直接进行方法调用,也就创建了对象,通过参数个数的控制可以选择不同的方法进行调用。第三行把 Lua中创建的变量 t0传回 C#进行验证,果然是 TestLua 对象。

方法调用lua.eval("print (t0.add(1,2))");输出:3

151

Page 152: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

如果是类的静态方法,同样可以调用。 字段访问

lua.eval("print(t0.mystr)");lua.eval("t0.mystr='hello world'");Console.WriteLine(lua.eval("return t0.mystr"));Console.WriteLine(((TestLua)lua.eval("return t0")).mystr);输出nilhelloworldhelloworld第一行代码,输出 mystr中的内容,对象构造时没有初始化该字段 C#中为 null,对应了 Lua中的 nil,第二行设置 mystr的字段内容,所以第三第四行访问均正确返回了设置内容。

普通属性访问lua.eval("print(t0.Prop)");lua.eval("t0.Prop = t0.Prop + 10");Console.WriteLine(lua.eval("return t0.Prop"));输出:010第一行代码输出 Prop的默认初始化值 0,第二行相当于读取该属性,再设置该属性,属性上加上 10,所以第三行输出 10。

索引化属性访问lua.eval("print(t0[100])");lua.eval("t0[200] = 300");输出:101Indexed Property set key = 200 value = 300这里可以看到索引化属性的两个方法均被正确调用。

委托访问lua.eval("print(t0.acc(100,200))");输出:300对照前面的 C#代码,这里的 acc实际上是一个委托,指向了 add方法,所以实际上执行的是 add。

Lua访问 C#的具体细节参见章节《脚本语言访问 C#规范》

152

Page 153: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

复杂交互首先定义委托 FNpublic delegate long FN(object f, long a);然后执行下面的代码FN fn = (object f, long a) => (long)lua.eval("if<1><2 then return<1>else return<0>(<0>,<1>-1)+<0>(<0>,<1>-2)end", f, a);lua.eval("print(<0>(<0>,<1>))", fn, 9);输出:34事实上,这段程序在 C#和 Lua之间间接递归,计算了斐波那契数列的第 9项。说明了

C#和 Lua进行复杂交互的可能性。类型映射C#,Lua两种语言的类型系统没有兼容性可言,所以类型映射无法用一张明确的表格进行描述。分 3种规则讨论。规则 1:C#向 Lua传递(C#通过 eval 向 Lua传递;Lua访问 C#时,构造返回对象,方法返回值,委托返回值,读取字段,读取属性)这种情况下,C#类型信息可以明确获知。 null,使用 lua_pushnil传递。 bool以及装箱类型 Boolean,使用 lua_pushboolean传递。 byte , sbyte , short , ushort , int , uint , long , ulong , 以 及 装 箱 类 型

Byte,SByte,Int16,UInt16,Int32,UInt32,Int64,UInt64,使用 lua_pushinteger传递

float,double,decimal以及装箱类型 Single,Double,Decimal,使用 lua_pushnumber传递

char,string以及装箱类型 Char,String,UTF8 编码后使用 lua_pushstring传递。需要注意,字符串传递给 Lua之后,已经变成 Lua 字符串,不可能调用 C#的字符串方法进行访问。之前的字段访问例子中,如果执行 lua.eval("print(t0.mystr.Length)"),将输出nil,改为 lua.eval("print(t0.mystr:len())"),才能获得正确结果。语法上比较怪异,小心。

LuaObject,LuaTable,LuaFunction之外的类型使用 lua_newuserdata创建一个用户数据类型与之关联,特别的,如果类型为委托类型,则获取委托对象的 Invoke方法作为实际需要关联的对象,这样委托对象就能被正确调用了。

LuaObject,LuaTable,LuaFunction是之前返回给 C#的类型,这时取回之前关联的 Lua对象。

特别的,C#方法的返回 void,或者委托返回 void,或者非可读属性的读取,根本不会在栈上 push任何值,详见章节《脚本语言访问 C#规范》中的实验。

规则 2:Lua 向 C#返回值(eval的返回)这种情况下,只能根据 Lua 当前已知的信息返回,C#获取这样的返回值以后,必须严格检

153

Page 154: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

查类型然后使用,不小心就可能导致类型转换异常,System.InvalidCastException。 LUA_TNIL,返回 null LUA_TBOOLEAN,返回为 Boolean LUA_TSTRING,UTF8解码后返回为 String LUA_TNUMBER,根据 lua_isinteger进行判断,返回为 Int64,或者 Double。这一点非常特殊,并不存在 LUA_TINTEGER这一类型,估计是 Lua在保证兼容性的前提上为了支持Int64 加入了一个补丁——在类型信息之外,内部增加了一个 tag进行区分,无需影响原有的类型规范。

LUA_TUSERDATA,该对象是 C#之前传入的非数值对象,直接向 C#返回该对象,特别的,如果该对象是委托的包装,返回委托本身。

LUA_TTABLE,该对象是一个 Lua表,创建并返回一个与之关联的 LuaTable 对象,使得C#能够通过 LuaTable的 IDictionary接口直接访问该表。LuaTable 对象按照 Lua规范访问,而不是 IDictionary规范。例如,IDictionary重复 Add抛异常,Lua的重复 Add 执行替换;Lua中 Add nil 值表示删除,所以 LuaTable.Add(key,null)等同于 LuaTable.Remve(key)

LUA_TFUNCTION,该对象是一个 Lua函数,创建一个与之关联的 LuaObject派生对象,这个对象再使用 LuaFunction委托进行包装,使得 C#能够通过 LuaFunction委托,将函数调用转发给 Lua 虚拟机,调用相应的 Lua函数。

特别的,对于无返回值的情况,返回 DBNull.Value;对于多返回值的情况,将各个返回值按照上面的方式转换为相应的 C#对象之后,再包装成 C#对象数组返回。

注意,(int)lua.eval("return 1"); 这样的转换必然导致 System.InvalidCastException,原因在于 lua_Integer是 64 位的,对应 long,返回类型为 System.Int64,这样的装箱类型无法像简单类型 long一样直接转换为 int,正确的写法应该是(int)(long)lua.eval("return 1");一旦出现类型转换异常,可以在返回对象上调用 getType()方法检查实际类型,判断是否符合需求,进而实现正确的转换。

规则 3:Lua 向 C#传递(构造参数,方法调用参数,委托调用参数,字段设置,属性设置)首先按照规则 2,转换为 C#对象,然后根据期待的参数类型在 C#中进行转换,细节参见章节《脚本语言访问 C#规范》

异常规范 Lua代码的运行时异常,由 Lua 虚拟机捕获,通过 Lua 操作对象构造时传入的委托传递

给 C#。 Lua 调用 C#构造函数或者成员函数或者委托时,如果抛出异常,这样的异常将被内部捕获,转换为 C#异常的串格式,再后缀 Lua栈描述,生成错误信息,提交给 Lua 虚拟机,最终传递给 C#。这里需要强调一个问题,C#和 Lua 互相调用,间接递归的情形下,异常实际上到达不了整个递归栈的最上层,而是打断当前 eval的执行,传送出异常串,最后 eval 返回 DBNull.Value。如果有回滚整个递归栈的需求,可以这样设计:逻辑上避免 eval 返回 DBNull.Value,一旦 eval 返回 DBNull.Value,在 C#内抛出异常,带上最

154

Page 155: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

后一条错误信息。注意事项1. C#中的泛型对象不可往 Lua 虚 拟 机中传递,否则将抛出异常。原 因在于 ,System::Runtime::InteropServices::Marshal::GetNativeVariantForObject,System::Runtime::InteropServices::Marshal::GetObjectForNativeVariant这两个关键方法的泛型版本从.NET4.5.1之后才开始提供,为了兼容较老的.NET 版本,该项目使用.NET4.0开发(.NET3.5应该也可以使用)。2. LuaObject返回给 C#持有时,将被 C#的 gc 控制,gc线程不一定是操作 Lua 虚拟机的线程,所以该项目必须按线程安全的方式设计。实现上,通过对 Lua 操作对象加锁保证安全,为了在 LuaObject析构时进行锁定,需要将 Lua 操作对象的引用传递给 LuaObject构造器,这样的传递只能通过 lua_State结构实现,问题是 lua_State结构缺少一个用户自定义指针的字段,所以实现上挪用了 lua_State.hook 指针,这相当于扔掉了 Lua的 hook能力,全局空间中的 debug.sethook,debug.gethook两个方法也被删除掉,避免用户使用,导致错误。如果需要保留 hook能力,则只能修订 Lua源码,在 lua_State结构上添加一个用户定义指针,对应修改 clrlua.cpp,重新编译项目。3. 可以创建多个 Lua 操作对象,从一个 Lua 操作对象获取的 LuaObject,LuaTable,不能够传入另一个 Lua 操作对象,这个显而易见;其它的 C#对象传入多个 Lua 操作对象,没有限制,如果不同线程使用不同的 Lua 操作对象,这些传入的 C#对象的线程安全性必须应用自己保证。多个 Lua 操作对象,如果确有数据交换需求,可以通过 JSON实现,Limax自带json.lua,限制是对象中不能存在环。4. 多线程的情况下通过 LuaTable访问 Lua 虚拟机中的 table时需要注意,使用 foreach方式遍历 table 前,必须在 lua 操作对象或者 LuaTable.SyncRoot上加锁,这两者实际上指向同一对象。其它访问方法,已经在内部通过加锁保证安全了。5. json.lua 集成在项目中,全局名字空间中已经存在 JSON.stringify,JSON.parse,可以直接使用。

CLR/Javascript(SpiderMonkey)

Limax 提供一个 clrjs项目,粘合 C#与 Javascript,支持 C#与 Javascript代码之间的互操作,用于实现 C#脚本模式框架下的 JavaScriptHandle。这一章介绍 clrjs 提供的功能。编程接口limax.script.Js

创建该对象也就创建了一个 javascript 虚拟机,使用该对象即可与 javascript 虚拟机交互。构造函数: Js(uint maxbytes)

155

Page 156: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

maxbytes决定了 javascript 虚拟机使用的最大内存字节数。 Js()

调用上一个构造函数,默认 maxbytes = 8388608成员函数: object eval(string code)

执行 code表示的代码 chunk,返回结果。 object eval(string codepattern, params object[] parameters)用变长参数列表填充 codepattern中的占位符<0>,<1>......<n>,然后执行 chunk,返回结果。占位符通过与之对应创建的上下文变量进行关联,在模板代码中应该当作变量名使用,而不能用引号括起来,否则将替换结果将是一个上下文变量名串。返回的结果可能是任何类型,特别的可能返回 JsObject, JsArray, JsFunction类型。

Js name(string chunkname)为后续 eval 命名,如果运行时错误发生在被命名的 eval的代码块中,错误信息中会前缀该名字,便于调试。该名字内部使用 UTF8 编码,建议不要使用非 ASCII 字符。

异常: ScriptException见异常规范一节。

ThreadContextException见线程安全一节。

limax.script.JsObject

无法转换为 C#类型的 javascript 对象,哪些无法转换在类型映射一节介绍,该对象实现了System.Collections.IDictionary接口,便于在 C#代码中操纵 javascript 对象的属性。limax.script.JsArray

派生自 JsObject,对应了 javascript中的 Array,实现了 IList接口,便于在 C#代码中操纵javascript的 Array 对象。limax.script.JsFunction

一个变长参数委托,包装了一个派生自 JsObject,关联到 javascript 虚拟机中的 Function上的对象。在该委托上进行调用,相当于调用了虚拟机中对应的 Function。注意,通过 limax.script.Js创建的对象,在之后的文字中命名为 javascript 操作对象,区别于JsObject。C#与 Javascript的互操作举例首先定义 Js js = new Js();

C#操纵 Javascript

C#操纵 Javascript,通过 eval方法调用实现。156

Page 157: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

hello worldjs.eval("print('hello world')");js.eval("print(<0>)", "hello world");Console.WriteLine(js.eval("'hello world'"));Console.WriteLine(js.eval("<0>", "hello world"));输出:hello worldhello worldhello worldhello world其中,第一行代码无需解释,第二行代码说明了占位符的使用,第三行代码说明了eval 可以返回 javascript 虚 拟 机中持有的值,第四行代码说 明了一个值可以在C#,Javascript间反复传递。

null与 undefinedConsole.WriteLine(js.eval("null") == null);js.eval("print(<0> === null)", null);Console.WriteLine(js.eval("undefined") == DBNull.Value);js.eval("print(<0> === undefined)", DBNull.Value);输出TruetrueTruetrue这就证明了 javascript 的 null 与 C#的 null 完全等价, javascript 的 undefined 与 C#的DBNull.Value完全等价。

Unicodejs.eval("var 好好学习=<0>", "天天向上");js.eval("print(好好学习)");Console.WriteLine(js.eval("好好学习"));输出:天天向上天天向上这里可以看出,Unicode可以被正确支持。

执行代码片段返回 JsObjectJsObject obj = (JsObject)js.eval("var t = { a: 'A', b :'B' }; t ");foreach (DictionaryEntry e in obj) Console.WriteLine(e.Key + ":" + e.Value);输出:a:Ab:B

继续执行:157

Page 158: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

obj.Remove("a");obj.Add("c", "C");js.eval("print (t.a)");js.eval("print (t.c)");输出:undefinedC这里可以看见,C#中的修改影响到了 javascript 对象的属性继续执行:js.eval("t.d = 100");foreach (DictionaryEntry e in obj) Console.WriteLine(e.Key + ":" + e.Value);输出:b:Bc:Cd:100这里可以看出,javascript 内部修改对象属性,也反映到 C#一边。

执行代码片断返回 JsArrayJsArray a = (JsArray)js.eval("var o=[1,2]; o");foreach (var v in a) Console.WriteLine(v);输出:12

继续执行:a.Add(3);js.eval("print(o)");输出:1,2,3这里可以看见,C#中的修改影响到了 javascript数组的内容继续执行:js.eval("o.shift()");foreach (var v in a) Console.WriteLine(v);输出23这里可以看出,javascript 内部数组操作,也反映到 C#一边。继续执行:Console.WriteLine(a.Count);a[5] = 100;

158

Page 159: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

js.eval("print(o)");输出:22,3,,,,100这里可以看出,JsArray数组上的操作,符合 javascript数组访问规范,不符合 C#数组访问规范,不会抛出 System.ArgumentOutOfRangeException异常。继续执行:a.Remove(DBNull.Value);js.eval("print(o)");输出:2,3,100这里再次明确,C#的 DBNull.Value 等同于 javascript 的 undefined,不存在的元素在javascript数组中表示为 undefined。继续执行:JsObject o = a;o['c'] = 1000;Console.WriteLine(a.Count);Console.WriteLine(o.Count);输出:34这里需要特别注意,对于 javascript 而言,Array也是 Object,Array上也可以操作属性,通过 JsObject或者 JsArray 两种方式引用同一对象,导致了不同行为。非特殊情况下,为了避免混乱,数组只用 JsArray操作更容易理解。

执行代码片断返回 JsFunctionJsFunction f = (JsFunction)js.eval("var func = function(a,b){ return a + b;}; func");Console.WriteLine(f(10, 20));输出:30这里可以看出,返回一个 javascript函数在 C#中使用非常容易。

Javascript 对象操作JsObject obj1 = (JsObject)js.eval("function CLS(n){this.value = n; this.dump = function(){print(this.value);}}; new CLS(200)");((JsFunction)obj1["dump"])();输出:200这里可以看出,javascript代码用函数 CLS创建了一个对象,交由 obj1持有,obj1上查询到函数属性 dump进行调用,输出创建出的对象 value 值 200。C#的语法无法写成obj1.dump,只好用索引化方式访问。

159

Page 160: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

继续执行:JsFunction cls = (JsFunction)js.eval("CLS");JsObject obj2 = JsObject.create(cls, 400);((JsFunction)obj2["dump"])();输出:400第一行返回了前面创建的函数 CLS,第二行使用 JsObject上的静态方法 create创建对象obj2,比较之前的代码,可见 JsObject.create 等效于 javascript代码中的 new。

Javascript 操纵 C#

首先定义一个 C#实验类。public class TestJs{

public TestJs() { Console.WriteLine("Construct TestJs"); acc = add; }public TestJs(int x) { Console.WriteLine("Construct TestJs with " + x); acc = add; }public int add(int a, int b) { return a + b; }public string mystr;public int Prop { get; set; }public int this[int x]{

get { return x + 1; }set { Console.WriteLine("Indexed Property set key = " + x + " value = " + value); }

}public delegate int ACC(int a, int b);public ACC acc;

}这里应该看到,需要通过 Javascript访问的对象成员必须声明为 public。 构造对象

js.eval("var t0 = new<0>()", typeof(TestJs));js.eval("var t1 = new<0>(100)", typeof(TestJs));Console.WriteLine(js.eval("t0") is TestJs);输出:Construct TestJsConstruct TestJs with 100True在这里,类 TestJs 被传递进 Javascript 虚拟机,在类上执行 new 操作,也就创建了对象,通过参数个数的控制可以选择不同的构造函数进行调用。第三行把 Javascript中创建的变量 t0传回 C#进行验证,果然是 TestJs 对象。

instanceof 测试js.eval("print(t0 instanceof <0>)", typeof(object));

160

Page 161: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

js.eval("print(t0 instanceof <0>)", typeof(TestJs));输出:truetrue在这里,t0是通过 C#类 TestJs构造的对象,所以 instanceof 操作转发给 C#执行,两行代码都输出 true。继续执行:js.eval("print('abc' instanceof <0>)", typeof(string));js.eval("print(<0> instanceof <1>)", 'a', typeof(string));js.eval("print(100 instanceof <0>)", typeof(int));js.eval("print(<0> instanceof <1>)", 100, typeof(int));js.eval("print(<0> instanceof <1>)", 100L, typeof(int));js.eval("print(<0> instanceof <1>)", 100L, typeof(double));js.eval("print(<0> instanceof <1>)", UInt32.MaxValue, typeof(uint));js.eval("print(<0> instanceof <1>)", UInt32.MaxValue, typeof(double));js.eval("print(<0> instanceof <1>)", (uint)10, typeof(uint));输出:truetruetruetruefalsetruefalsetruefalse实际上 instanceof 执行时,用第二个参数作为类型来测试第一个参数,所以第一条语句输出 true;第二条语句,字符型进入 javascript 转换为串类型,所以测试为 true;第三,第四条语句输出 true 显而易见;第五,第六条语句,javascript 整数仅支持 int,所以 long 被 转换为 double;第七,第八条语,既 然 uint 为无符号类型,那么大于0x7FFFFFFF的 uint 只能转换为 double,转换为 int 将变为负数,完全损失了无符号的意义;第九条语句,尽管第一个参数类型为 uint,但是 javascript 内部并不存在这个类型,所以输出 false,实际上,javascript的数值只有 int,double两种类型,用之外的 C#类型进行测试永远 false。

方法调用js.eval("print (t0.add(1,2))");输出:3如果是类的静态方法,同样可以调用。

字段访问161

Page 162: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

js.eval("print(t0.mystr)");js.eval("t0.mystr='hello world'");Console.WriteLine(js.eval("t0.mystr"));Console.WriteLine(((TestJs)js.eval("t0")).mystr);输出nullhelloworldhelloworld第一行代码,输出mystr中的内容,对象构造时没有初始化该字段,C#中为 null,对应了 Javascript中的 null,第二行设置 mystr的字段内容,所以第三第四行访问均正确返回了设置内容。

普通属性访问js.eval("print(t0.Prop)");js.eval("t0.Prop = t0.Prop + 10");Console.WriteLine(js.eval("t0.Prop"));输出:010第一行代码输出 Prop的默认初始化值 0,第二行相当于读取该属性,再设置该属性,属性上加上 10,所以第三行输出 10。

索引化属性访问js.eval("print(t0[100])");js.eval("t0[200] = 300");输出:101Indexed Property set key = 200 value = 300这里可以看到索引化属性的两个方法均被正确调用。

委托访问js.eval("print(t0.acc(100,200))");输出:300对照前面的 C#代码,这里的 acc实际上是一个委托,指向了 add方法,所以实际上执行的是 add。

Javascript访问 C#的具体细节参见章节《脚本语言访问 C#规范》

复杂交互首先定义委托 FNpublic delegate int FN(object f, int a);

162

Page 163: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

然后执行下面的代码FN fn = (object f, int a) => (int)js.eval("<1><2?<1>:<0>(<0>,<1>-1)+<0>(<0>,<1>-2);", f, a);js.eval("print(<0>(<0>,<1>))", fn, 9);输出:34事实上,这段程序在 C#和 Javascript之间间接递归,计算了斐波那契数列的第 9项。说

明了 C#和 Javascript进行复杂交互的可能性。类型映射C#,Javascript两种语言的类型系统没有兼容性可言,所以类型映射无法用一张明确的表格进行描述。分 3种规则讨论。规则 1:C#向 Javascript传递(C#通过 eval 向 Javascript传递;Javascript访问 C#时,构造返回对象,方法返回值,委托返回值,读取字段,读取属性)这种情况下,C#类型信息可以明确获知。 null,解释为 javascript的 null DBNull.Value,解释为 javascript的 undefined bool以及装箱类型 Boolean,解释为 javascript的 boolean类型 byte,sbyte,short,ushort,int,以及装箱类型 Byte,SByte,Int16,UInt16,Int32解

释为 javascript的 int类型。 float,double,decimal以及装箱类型 Single,Double,Decimal,解释为 javascript的

double类型。 uint以及装箱类型 UInt32,如果值小于 0x80000000解释为 javascript的 int类型,否则解释为 javascript的 double类型。

char,string以及装箱类型 Char,String解释为 javascript的 string。需要注意,字符串传递给 javascript之后,已经变成 javascript 字符串,不可能调用 C#的字符串方法进行访 问 。 如 果 执 行 js.eval("print(<0>.Length)", "abc"); , 将 输 出 undefined , 改 为js.eval("print(<0>.length)", "abc");,才能获得正确结果 3。语法上比较怪异,小心。

JsObject,JsArray,JsFunction之外类型的 C#对象,在 javascript中创建一个特殊对象与之关联,通过 SetPrivate 引用该 C#对象,防止在 C#虚拟机中被 GC,特别的,如果类型为委托类型,则获取委托对象的 Invoke方法作为实际需要关联的对象,这样委托对象就能被正确调用了。

JsObject,JsArray,JsFunction是之前返回给 C#的类型,这时取回之前关联的 Javascript对象。

规则 2:Javascript 向 C#返回值(eval的返回)这种情况下,只能根据 Javascript 当前已知的信息返回,C#获取这样的返回值以后,必须严格检查类型然后使用,不小心就可能导致类型转换异常,System.InvalidCastException。 null,返回 null undefined,返回 DBNull.Value

163

Page 164: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

Boolean,返回 Boolean string,返回 String int,返回 Int32 double,返回 Double 通过 SetPrivate 引用了 C#对象的特殊 javascript 对象,返回引用的 C#对象。 其它 javascript 对象,创建一个 JsObject 对象引用它,防止在 javascript 虚拟机中被

GC,该 JsObject 被返回。 除数组和函数外的 Javascript 对象使用 JsObject返回,JsObject实现了 IDictionary接口,通过该接口可以访问对应 javascript 对象的属性。JsObject的按照 Javascript规范访问,而不是 IDictionary规范。例如,IDictionary重复 Add抛异常,Javascript重复 Add 执行替换;JsObject上 Remove属性,跟在属性上 Add一个 DBNull.Value 效果完全一样。

Javascript数组使用 JsArray返回,JsArray继承自 JsObject,实现了 IList接口,通过该接口可以访问对应的 javascript数组的成员。JsArray 按照 Javascript规范访问,而不是 IList规范,简单说来,不会生成 System.ArgumentOutOfRangeException异常,即不存在数组越界的问题。JsArray实现的 IList接口与继承自 JsObject 对象中的 ICollection接口,存在属性和方法同名的问题,使用了 new规则 override,所以按照 JsArray方式与按照JsObject方式可能导致不同结果,可以回顾之前的例子。

Javascript函数使用,使用 JsFunction委托返回,该委托包装了一个继承自 JsObject的对象操作该 Javascript函数。

注意,(int)js.eval("3.14"); 这样的转换必然导致 System.InvalidCastException,原因在于返回的 javascript 值是 double类型,返回类型为 System.Double,这样的装箱类型无法像原生类型 double一样直接转换为 int,正确的写法应该是(int)(double)js.eval("3.14");一旦出现类型转换异常,可以在返回对象上调用 getType()方法检查实际类型,判断是否符合需求,进而实现正确的转换。

Javascript规范不断更新中,为对象属性规定了很多特定访问控制属性,比如设置为readonly 的属性,修改不可能成功。例如,执行代码, js.eval("print=100; print('readonly')");将输出 readonly。这种情况需要小心,不过一般来说,不是系统内建的一些对象,方法,类型,不会存在这类问题。

规则 3:Javacript 向 C#传递(构造参数,方法调用参数,委托调用参数,字段设置,属性设置)首先按照规则 2,转换为 C#对象,然后根据期待的参数类型在 C#中进行转换,细节参见章节《脚本语言访问 C#规范》异常规范 C#与 Javascript 均支持异常框架,两种语言都可以进行异常捕捉。 C#调用 Javascript,Javascript中抛出了异常,没有捕捉,异常抛出到 C#中。 Javascript 调用 C#,C#中抛出了异常,没有捕捉,异常抛出到 Javascript中。 C#与 Javascript可以互相嵌套,没有捕捉,异常可以抛出到嵌套的最外层。 异常每进入一次 C#,则用 limax.script.Js.ScriptException包装一次。 ScriptException 将 Javascript异常串作为异常 message,C#异常作为内部异常,进行包

164

Page 165: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

装。 Javascript异常串由文件位置信息,异常 message 和 StackTrace 共同拼接而成。对于

Javascript 而言抛出的 Error才能包含位置信息,和 StackTrace。 Javascript中,捕获的异常对象上 toString,可以获得完整信息,包括来自 C#的异常信

息。线程安全 SpiderMonkey 库不允许虚拟机跨线程运行。javascript 操作对象上执行的任何操作,包

括 eval,以及 eval 返回的 JsObject, JsArray, JsFunction 上的操 作,必须在创建javascript 操 作 对 象 的 线 程 中 执 行 , 否 则 抛 出 异 常limax.script.Js.ThreadContextException。

注意事项1. C#中的泛型对象不可往 javascript 虚 拟 机中传递,否则将抛出异常。原 因在于,System::Runtime::InteropServices::Marshal::GetNativeVariantForObject,System::Runtime::InteropServices::Marshal::GetObjectForNativeVariant这两个关键方法的泛型版本从.NET4.5.1之后才开始提供,为了兼容较老的.NET 版本,该项目使用.NET4.0开发(.NET3.5应该也可以使用)。2. 可 以 创 建 多 个 Javascript 操 作 对 象 , 从 一 个 Javascript 操 作 对 象 获 取 的JsObject,JsArray,JsFunction不能够传入另一个 Javascript 操作对象,这个显而易见;其它的 C#对象传入多个 Javascript 操作对象,没有限制。多个 Javascript 操作对象,如果确有数据交换需求,可以通过 JSON实现,Javascript本身即提供了完整支持,限制是对象中不能存在环。

脚本语言访问 C#规范Limax 提供 limax.util.ReflectionCache类,为脚本语言访问 C#对象提供尽量完整的支持。当前由 CLR/Lua,CLR/Javascript使用。编程接口构造函数: ReflectionCache(ToStringCast toString)

toString, 定义为委托 delegate object ToStringCast(object obj);指令脚本系统执行相应的ToString 操作,某些脚本系统对象,可能只是具体实现的包装,该方法提供了解包装再ToString的机会。

成员函数:165

Page 166: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

object GetValue(object obj, object name)obj,被访问的 C#对象name,被访问的 C#对象的成员该方法返回了 C#对象成员的值,成员可以是字段,属性,方法。如果返回的是方法,使用 Invokable接口包装,便于下一步被脚本语言调用。name的类型为 object,而不是string,这是因为脚本语言访问对象一般两种形式 obj.member,obj[member],第一种形式,member必然是 string类型,第二种形式则不然,第二种形式与 C#的索引化属性访问方式类似,所以可以被利用来支持单参数索引属性的访问,这时 name的含义不再是字段,属性,或者方法,而是索引参数。

void SetValue(object obj, object name, object value)obj,被访问的 C#对象name,被访问的 C#对象的成员value,期望给成员设置的值该方法为 C#对象成员设置一个值,成员可以是字段,属性,如果 name 指定了方法成员,SetValue的执行没有任何效果。类似 GetValue,该方法同样被索引化属性利用。

object Construct(object obj, object[] parameters)obj,类型对象parameters,构造函数参数该方法构造并返回类型对象 obj的一个实例,obj必须是 Type派生类,否则调用抛出System.InvalidCastException。

接口成员: interface Invokable

该接口用于包装对象方法,便于被脚本语言调用,包含两个成员:object Invoke(object[] parameters);使用参数 parameters 调用包装的对象方法object GetTarget();见正确处理委托一节。

System.DBNull.Value:不存在的值在 C#中用 DBNull.Value表示。脚本语言在不存在的值的理解上存在差异,

比如 Javascript用 undefined表示值不存在;而 Lua中存在二义性,有时候不存在就是完全没有,占位都不行;有时候又认定为 nil,可以在 Lua 控制台程序中做一个实验说明:定义函数:function undefined() end执行:print(undefined())输出是空白,从语句的描述来看,print有一个参数,那就是 undefined()函数的返回值

实际上,返回值根本没有,所以 print 执行的时候发现参数数量为 0,输出空白。要证明这个结论,可以用 CLR/Lua做一个实验:

Action a = () => Console.WriteLine("empty");lua.eval("function undefined() end\n<0>(undefined())", a);输出 empty回到 Lua 控制台程序,继续执行:

value = undefined()print(value)

输出竟然是 nil !!!166

Page 167: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

可以解释为,赋值语句必需执行,但是没有值,只好用 nil代替。对于这种解释,又必需提出一个问题,为什么不可以解释成赋值根本没有执行,毕竟 value还没有初始化过?所以还得继续实验:再执行:value = 100

print(value)输出 100再执行:value = undefined()

print(value)输出 nil上面的实验见证了 Lua不支持不存在的值导致的二义性。CLR/Lua在实现上保证和 Lua

的特性完全兼容,这些问题同样存在,必须小心。访问范围: 需要提供给脚本语言访问的对象字段,属性,方法,限定为 public 对象本身的字段,属性,方法可以被访问,不包括继承的基类成员。 以上两点限制很容易突破,不过,从编程规范化的角度看,不建议。对于第一条,C#本身代码都被限制访问的成员,通过脚本语言反而能访问完全不存在合理性;对于第二条,尽量多使用接口,少使用类继承,是更好的编程方式,没有理由往不好的习惯上引导。

构造对象调用 Construct方法,即可构造对象,具体执行流程与方法调用相同,之后介绍。字段访问读字段GetValue时,name 参数为 string类型,值与当前对象的字段名匹配,执行字段读取。写字段SetValue时,name 参数为 string类型,值与当前对象的字段名匹配,用 value写字段,脚本语言提供的 value类型可能与字段实际类型不匹配,所以必须先执行类型转换,类型转换有失败抛出异常的可能。属性访问读属性GetValue时,name 参数为 string类型,值与当前对象的属性名匹配,执行属性读取,如果属性不可 get,返回 DBNull.Value。

167

Page 168: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

写属性SetValue时,name 参数为 string类型,值与当前对象的属性名匹配,用 value写属性,脚本语言提供的 value类型可能与字段实际类型不匹配,所以必须先执行类型转换,类型转换有失败抛出异常的可能。如果属性不可 set,操作被忽略。方法访问读方法GetValue 时,name 参数为 string 类型,值与当 前 对象的方法 名匹配,使用一个实现Invokable接口的内部对象包装方法集合(方法重载允许同名方法的存在)并返回。写方法SetValue时,name 参数为 string类型,值与当前对象的方法名匹配,该操作被忽略。索引化属性访问脚本语言的语法限制了只能访问单参数索引化属性。读索引化属性GetValue时,如果 name 参数为 string类型,并且 name的值不匹配任何可访问的字段名,属性名,方法名,则把 name 假设为索引参数,执行后续访问,这样的访问存在二义性。name 参数为其它类型,则直接认定为索引参数,遍历所有单参数可读索引属性,匹配类型进行访问,没有可匹配的情况下返回 DBNull.Value。访问方式与方法调用相同,之后介绍,可能抛出异常。写索引化属性SetValue时,如果 name 参数为 string类型,并且 name的值不匹配任何可访问字段名,属性名,方法名,则把 name 假设为索引参数,执行后续访问,这样的访问存在二义性。name 参数为其它类型,则直接认定为索引参数,遍历所有单参数可写索引属性,匹配类型进行访问,没有可匹配的情况下操作被忽略。访问方式与方法调用相同,之后介绍,可能抛出异常。事件访问常见脚本语言没有相应的语法对应,所以不支持。方法调用流程读方法获得的 Invokable接口对象上执行 Invoke,构造对象,读索引属性,写索引属性,这

168

Page 169: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

4种情况下执行方法调用流程。参数匹配遍历所有同名方法,首先根据形参实参列表长度进行匹配,然后逐参数匹配,获得适合的方法列表。参数匹配使用一个加权评价算法,得分最高的那些方法被选中,评价值为Int32.MinValue的方法被直接抛弃,如果全部都是 Int32.MinValue,全部抛弃。方法评价值的计算:1. 遍历形参列表1.1. 使用用形参对应位置的实参与形参类型,计算参数评价值1.1.1. 形参类型等于实参类型,表示完全匹配,返回评价值 11.1.2. 实参是形参类型的实例,或者实参为 null 并且形参类型为非值类型,表示兼容的转

换,返回评价值 01.1.3. 如果形参类型为 bool或者 string,返回评价值-1,这是为了兼容脚本语言特性,可

以进行安全的转换。1.1.4. 如果形参实参都是 IConvertible类型,说明还有机会使用 System.Convert.ChangeType

进行转换,数值类型都是 IConvertible,转换损失精度的情况下会抛出异常,这就是接下来参数绑定过程忽略失败转换的主要原因,这种情况下返回评价值-2。

1.1.5. 其它情况不存在有效的转换,返回 Int32.MinValue。1.2. 如果参数评价值为 Int32.MinValue,则方法的评价值直接返回 Int32.MinValue,否则累

加到方法评价值中。2. 如果所有实参都被匹配过了,返回方法评价值参数绑定遍历匹配后获得的方法列表,执行参数类型转换,忽略失败的转换,记录成功的转换,如果转换成功一次以上,抛出异常 System.Reflection.AmbiguousMatchException。遍历形参列表执行参数绑定,实际上就是按照形参类型对实参进行转换,获得新的参数列表对象。转换方法:1. 实参是形参类型的实例,或者实参为 null 并且形参类型为非值类型,直接返回该实参,

实际上对应了评价值为 1,0的两种情况。2. 形参类型为 bool的情况下(评价值-1),按照脚本语言的惯例,转换实参:2.1. 实参为 null或者 DBNull.Value,转换为 false2.2. 实参为 char类型,等于'\0'为 false,否则 true2.3. 实参为 string类型,长度为 0为 false,否则 true2.4. 实参为各种数值类型,交给 System.Convert.ChangeType 执行转换,这些转换不会抛

出异常,可以获得符合脚本语言惯例的结果(当然了,这不是 Lua的惯例,对 Lua 而言,if 0 then print('true') else print('false') end 输出 true,这一点跟 Lua不兼容)。

2.5. 其它情况一律转换为 true3. 形参类型为 string的情况下(评价值-1),使用构造 ReflectionCache时提供的 toString

委托,将实参提交给脚本系统进行转换。4. 其它情况(评价值-2)交给 System.Convert.ChangeType 执行转换,可能抛出异常,指

示形参列表对应的方法不能被调用,应该忽略。

169

Page 170: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

方法调用如果存在成功的转换,使用转换结果执行对应的方法;如果没有,对于 Invokable 对象上的Invoke,抛出异常指示没有合适的方法可用,构造对象的情况下,抛出异常指示没有合适构造函数可用,读索引属性的情况下返回 DBNull.Value,写索引属性忽略该操作。Invokable对象上的 Invoke的方法如果返回类型为 void,返回 DBNull.Value;

注意事项1. 不支持包含可选参数的方法,也就是说调用可选参数方法时,可选的那些参数不能省

略。可选参数方法并不常用,支持可选参数的版本性能会将大大降低,得不偿失。2. 参数匹配实际上是为了减少参数绑定失败次数,提高性能,而作的预处理。3. 实现上,首先按照参数长度进行匹配,如果选择出唯一的方法,则省略参数匹配过程,

可以提高性能,所以减少重载的使用才是最优的方法。4. 参数评价算法中,为了减少参数绑定失败概率,向 string 转换的评价值高于数值间转

换,某些情况下可能违反脚本使用习惯。 例如,定义方法 int add(int a, int b);与 string add(string a, string b);分别执行数值加法与串连接。脚本语言调用 add(1,2),如果是javascript来调用返回 3,如果是 Lua来调用,返回字符串 12,原因在于,Javascript的整数是 int,可以严格匹配,评价值为 2 和-2;Lua的整数是 long,评价值为-4与-2,这类情况需要小心。

5. 参数绑定是个完全动态的过程,比如脚本语言中使用一个变量作为参数调用方法func(short x);如果变量的值在 short范围内,该方法被调用,否则报告找不到合适的方法。

正确处理委托C#对象进入脚本环境,需要检测该对象是否为委托对象,如果是,则需要进一步在该

对象上 GetValue获取 Invoke方法的 Invokable包装作为实际被脚本引用的对象,在脚本环境看来,这样的对象与读取对象方法获得的 Invokable包装没有任何区别。Invokable包装重新回到 C#就必须进一步判断被包装的对象是否是委托对象,如果是,返回委托对象,否则返回包装本身,这样才能做到进出一致,这也就是 Invokable.GetTarget()方法需要完成的功能。委托对象必须被包装,原因在于,.Net实现上,提供了一个 Delegate基类,delegate

关键字定义的信息,被用来生成一个类,该类继承自 Delegate,生成的 Invoke方法的参数列表正好就是 delegate定义的参数列表,有了它才能进行正确的类型转换,然后调用。如果不包装,直接调用 Delegate.DynamicInvoke则缺少类型转换过程,不能保证正确性。

Node.js 兼容框架Limax 集成原生 java 版本 node.js兼容框架,使用 JDK8 提供的 Nashorn 引擎,依据

node.js 手册的描述实现,并做了适当扩展,以利于集成到 Limax框架中,为连接 Limax外的系统提供便利。

Nashorn支持 ES5.1标准,对于某些 ES6标准的 js文件,应该首先使用 6to5 工具转换,170

Page 171: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

现有 Node.js 手册示例都是 ES6代码,应该转换以后实验。以下的描述,符合 node.js 6文档的顺序,便于对比。

核心模块除了 Buffer 模块比较特殊,放在包 limax.node.js 内,其它核心模块均放置在包

limax.node.js.modules 内,一个 .java文件一个 .js文件配对,按照 java习惯,首字母大写,require时全小写。Assert(断言)功能与标准 node.js一致。AssertionError类做了一些信息伪造。实际上,js中,Error类

无法被有效继承,特别是在 stack的问题上。Buffer

不同于其它核心模块的对象,Buffer不是 js 对象,直接导出了 limax.node.js.Buffer 对象。原因在于,node.js文档描述了[]操作,但是 ES5.1标准不支持索引化属性的访问,所以使用了 Nashorn 的一个怪异特性,实现了 List 接口的对象可以用 []方式问,带来的问题是BoundCheck在 Nashorn中完成,数组越界会直接抛出异常,这一行为与 js不一致。最好避免使用[]访问 Buffer。Java 插件不同于 node.js用 C++实现,使用 C++创建插件。Limax的 Node 插件用 Java创建,一个

插件就是一个 jar,一个插件 jar 内部只能有一个 js文件,同时保证存在一个相应名字的java类,更多的细节可以参见 limax.node.js.modules包。例如:MyTest.javapackage testnodejsmodule;

import limax.node.js.EventLoop;import limax.node.js.Module;

public final class MyTest implements Module {public MyTest(EventLoop eventLoop) {}

public Number add(Number x, Number y) {return x.doubleValue() + y.doubleValue();

}}

171

Page 172: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

MyTest.jsexports.add = function(x, y) {

return java.add(x, y);}

这样两个程序用 eclipse export成一个 jar,就可以作为插件被引用。将插件命名为 test.jar 放置在主模块目录下,执行主模块console.log(require('./test.jar').add(1,2))即可输出结果 3。具体细节详见模块一节。Child Processes(子进程)使用 java.lang.ProcessBuilder 操控子进程,支持进程管道操作。1. 不支持 child_process.fork方法,Limax使用线程模型,不需要这个方法,详见模块一节。2. 类 ChildProcess仅支持 exit 事件(其它事件都与 IPC 相关,不需要),在进程结束后触发。支持 child.kill 方法, java.lang.Process 向进程发送的信号仅支持 SIGTERM 与SIGKILL , 非 SIGKILL 的 情 形 , 一 律 默 认 SIGTERM 。 支 持child.stdin,child.stdout,child.stderr,child.stdio。

3. 所有方法的 options 参数中,支持 options.cwd,options.env(Node.js 手册中声明 spawn方法默认 process.env,exec方法默认 null,实际测试 exec 默认值也是 process.env,所以 默 认process.env),options.shell,options.detached,options.input,options.timeout,options.encoding,options.maxBuffer,options.killSignal(SIGTERM|SIGKILL),options.stdio(只支持 3个标准 IO 对象)。

4. child_process.spawnSync方法返回的对象,没有 pid成员,因为 java.lang.Process不提供。Cluster(集群)Node.js使用进程模型,一个进程对应一个 Javascript 虚拟机,为了使用 cpu的多核特性,所以提出了集群概念, fork 出更多的虚拟机,使用 IPC 进行协调。Limax 中,通过扩充require,允许 require传递参数,允许 require创建新的虚拟机实例,完全可以达成同样的目的,而且更节约资源。具体细节详见模块一节。CLI(命令行)可以在 shell中执行,这种情况下 java –jar limax.jar node <module> [args]可以作为 Limax服务加载,在服务 xml配置中添加:<NodeService module=module_path>

<parameter value=”p0” />按参数数量,顺序排列

</NodeService>

172

Page 173: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

一条 NodeService element 启动一个 javascript 虚拟机线程执行module。Console(控制台)1. console.dir 作为 console.log的别名,不支持显示颜色之类的选项。2. 内部使用 java.lang.String.format方法格式化输出字符串,格式参数与 node.js使用的

sprintf 格式参数稍有差异,应该注意。如果格式化失败,按照普通字符串对待。3. console.timeEnd,用WeakHashMap管理,不删除定时器也不会有泄漏。Crypto(加密)1. node.js 使用 openssl 实现, limax 中主要使用 jce 实现,支持的算法集合稍有差异。

getCiphers(),getCurves(),getHashes(),三个方法可以用来获取具体支持的算法,进行比较。

2. Cipher类没有提供 AEAD的支持,因为 AEAD的使用流程尚未确定。3. limax.util.OpenSSLCompat 提供了需要的 openssl兼容性支持。jce 只支持自己的 keystore与 PKCS11,PKCS12,这里通过使用 limax.codec.asn1包,提供 PEM 格式的支持,这两处代码,其它地方可能有用。

Debugger(调试器)Nashorn根本不会生成 js 字节码,而是直接生成 java 字节码,深度调试应该使用 java的调试器。DNS(域名服务器)node.js使用 libresolve实现,limax使用 jndi实现,所以不提供 libresolve规格的错误码。特别注意,lookup方法的返回结果,node.js文档描述与实现不一致,文档描述是错误的。Domain(域)node.js文档申明该模块是废弃的,所以不提供。Error(错误)错误类由 Narshorn 提供,不需要提供。需要注意的是,Limax 环境下,所有通过 callback 报告的错误,可能是 js的 Error,也可能是java的 Exception,并没有统一成 Error,因为 Exception可以提供更丰富的栈信息,例如:

function(err) {if (err) {

if (err instanceof Error) {console.log(err.message, err.stack);

} else {err.printStackTrace();

}return;

}

173

Page 174: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

}这样的处理,可以输出最完整的错误信息。Events(事件)提供了完全的 java实现,js 只是一个简单包装。setMaxListeners 方法实际上被 忽 略,不限 制 listener 数量, getMaxListeners 永远返回Integer.MAX_VALUE。node.js 手册的提法是,限制 listener数量有助于寻找内存泄露,java 环境下无需考虑这个问题。File System(文件系统)使用 java.nio.file包实现,与 node.js实现有稍许差异。1. fs.Stats 差异,java能够获取的信息与 struct stat有差异,可以用如下代码 dump具体信

息进行比较。fs.stat(file, console.log);

2. java不提供硬连接能力,所以 fs.link,fs.linkSync不提供。3. fs.watch,改为通过 绝 对 路径调用 callback,便 于使用单一 listener 监听多个目录

options.recursive无效,java不提供支持,node.js 手册对 options.encoding的描述上下文矛盾。

4. fs.readFile,fs.readFileSync,fs.writeFile,fs.writeFileSync,fs.appendFile,fs.appendFileSync,这6个方法的 options 参数不支持对象,只作为 encoding 串进行 Buffer.isEncoding 测试,测试失败直接默认为 UTF8。否则逻辑上存在大量矛盾,比如执行 writeFile,提供flag=’r’,纯属自找麻烦。

5. fs.mkdtemp,fs.mkdtempSync,这两个方法的 options 参数看不出任何意义,忽略;prefix要求一个不带路径分隔符的串,生成用户临时目录下的文件名,避免创建临时文件出现权限问题。

6. fs.constants实际上是一 java 对象,只有 R_OK,W_OK,X_OK,F_OK这 4个值。7. java.nio.file 并不支持文件描述符,所以 open返回的文件描述符实际上是内部伪造的用来索引 FileChannel的整数。

Global(全局变量)node.js 手册描述的全局 变量全部支持, require 相关成员做了扩 充,增 加了parameters,java,两个模块级全局变量,详见模块一节。HTTP

1. http.Agent构造参数 options中 options.keepAlive 被忽略,Agent 始终支持 HTTP/1.1的keep-alive。options.maxSockets没有明显意义被忽略。options.keepAliveMsecs 被重新解释为连接 idle超时,超时以后连接被关闭,默认为 60000ms,多数 NAT 环境 TCP IDLE默认 5分钟,不会存在问题,node.js文档中默认的 tcp-keepalive=1000ms的配置,linux上通过设置 tcp选项 TCP_KEEPINTVAL才能实现,不具有通用性。

2. http.Request中,http.setSocketKeepAlive 第二个参数 initialDelay,被忽略,jdk不支持这174

Page 175: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

个参数,也不知道哪个协议栈能支持这样的行为。HTTPS

Node.js的设计方式过于冗余,Limax 版本直接在 HTTP模块中实现(实际上不存在 https 这 个模块),只需要简单修订 2个方法。(TLS的相关内容详见 TLS/SSL一节)1. http.createServer([requestListener])扩展为 http.createServer([requestListener], [tls]),其中,

tls为 TLS配置信息,只要提供了 TLS配置,则服务器启动为 HTTPS服务器,如果配置要求客户端认 证, http.IncomingMessage 上可以读取属性 socket.tlsPeerPrincipal 与socket.tlsPeerCertificateChain,分别获得客户端证书的主题与证书链。

2. http.ClientRequest的构造参数 options中,增加 options.tls属性,如果设置了该属性,使用 HTTPS 发送客户端请求,如果没有提 供 该 字 段,检查 options.protocol 是否为’https:’如果是,使用 HTTPS发送客户端请求,这种情况下,使用 JDK 提供的 cacerts中的受信任根证书验证服务器,指定 SNIHostName为连接的服务器名,不支持客户端认证,不检查证书撤销状态。

示例:最简单的 https客户端:var http = require(‘http’)var req = http.get('https://www.alipay.com', function(res) {

res.pipe(process.stdout);});req.once('socket', function(socket) {

socket.once('tlsHandshaked', function() {for (var i = 0; i < arguments.length; i++)

console.log(arguments[i].toString());});

})最后一条语句,通过在 socket上监听 tlsHandshaked 消息可以获得服务器证书的主题以及证书链。Module(模块)在实现 node.js文档描述功能之外,添加了更多功能。1. require(module[, parameter1[, parameter2,…]])

允许向模块传递启动参数,因为模块默认被 cache了,所以第一次传入的启动参数才有意义。

2. require.reload(module[, parameter1[, parameter2,…]])重新加载模块,不存在 C++插件可能导致的麻烦,所有模块都允许重新加载。

3. require.launch(module[, parameter1[, parameter2,…]])在新线程中启动新的 Nashorn 虚拟机,加载模块,被加载的模块是 main模块,这个方法可以实现 Cluster描述的行为。

4. 使用不包含路径前缀(./,../,/)的名字加载模块,按核心模块名搜索失败之后,按Node模块加载之前,插入一个 java模块加载步骤,如果成功,直接返回该模块。例如,当前 ClassLoader 内存在一个模块类 app.Jmodule,require(‘app.Jmodule’),即可加载该

175

Page 176: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

模块,优先于 Node模块加载。5. 全局目录加载位置不包括$PREIFX/lib/node,因为没有合适地方配置 node_prefix。6. 加载模块时的添加的外层wrapper,添加了 parameters,java两个参数。

(function (exports, require, module, __filename, __dirname, parameters[, java]) {// your module code

});parameters为 require传入的参数 Array。实现 java 插件时,java 变量为关联的 java 对象实例,js通过该变量访问对应的 java 对象;其他模块类型,java 变量为 undefined。

7. require.extensions,尽管被 node.js文档废弃了,实际上用户可以用来扩展更强的模块支持能力,所以保留,该 对象中 key 为文件 扩 展 名, value 为 function(path, parameters);这里的 path为模块文件绝对路径,parameters为 require传入的参数。废弃这样一个扩展性,不知道设计者怎么考虑的。

8. node.js经典示例的 Cluster 版本实现:example.jsif (--parameters[0] > 0)

require.launch(__filename, parameters[0]);var http = require('http');var hostname = '127.0.0.1';var port = 3000;var Thread = Java.type('java.lang.Thread');var server = http.createServer(function(req, res) {

res.statusCode = 200;res.setHeader('Content-Type', 'text/plain');res.end('Hello World ' + Thread.currentThread() + '\n');

});server.listen(port, hostname, function() {

console.log('server launch');});

用 java –jar limax.jar node example.js 3 启动服务器,可以看见启动了 3个服务器实例,关键是前两行,require.launch与 require 参数配合即可提供 Cluster支持。用浏览器访问http://127.0.0.1:3000/可以看见服务线程名,刷新浏览器,服务线程名没有改变,体现了Keep-Alive特性,重启浏览器访问,可以看见线程名发生了改变,体现了 Cluster特性。8. 虚拟机线程间通讯,是实现 Cluster的基础,可以参考 limax.node.js.module.Net的实现,基本方法就是,实现自己的插件,通过类静态成员进行数据交换,正确使用同步即可。

Net(网络)该模块使用 java.nio.channels中的异步 IO支持类实现,与 js的异步特性正好对应。1. java本身只支持 TCP服务,不支持 LocalDomain服务。2. server.listen端口绑定失败会直接报告异常,node.js不会,反复重试,应该是为了支持

Cluster达成的妥协。3. node.js文档要求 server.listen可以执行多次,应该也是为了支持 Cluster达成的一种妥协,这里不允许。

176

Page 177: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

4. node.js文档中提及 server.maxConnections在 Cluster 环境下没有意义, limax实现重新找回了这个成员的意义。ServerChannel实际上全局管理的,accept获得的 channel 向各个虚拟机轮转投递,某个虚拟机中的 maxConnections 限制超出以后,就向下一个虚拟机投递,所有虚拟机都拒绝接受该 channel才关闭该 channel,所以配置这个选项和Cluster数量配合,可以实现更合理的负载均衡。

5. socket.KeepAlive方法的第二个参数 initialDelay,仍不明确。6. node.js文档中提供的 ECHO服务器与客户端,尽管多数时候都能正确运行,实际上并不合理,任何时候都必须明确 TCP是流,不应该假设一个报文就能承载所有数据,即便数据量很小。

Os(操作系统)理论上,既然应用运行在 java 虚拟机上,就不应该再考虑操作系统层面的问题,该模块仅提 供 java 虚 拟 机 能 提 供 的 信 息 , 包 括os.arch(),os.endianess(),os.homedir(),os.hostname(),os.networkInterfaces(),os.platform(),os.release(),os.tmpdir(),os.type(),os.userInfo(),某些方法返回的信息与 node.js文档提到的可能有少许差异,比如 x64,在这里可能是 amd64。Path(路径)与 node.js文档基本一致,除了 path.win32 等同于 path.posix,应该说都等同于 path.java,不需要过多考虑系统相关问题,java 虚拟机层面解释即可。Process(进程)java 虚 拟 机 中 不 需 要 太 多 进 程 相 关 特 性 , 所 以 这 个 模 块 仅 提 供process.chdir(),process.cwd(),process.env,process.exit(),process.exitCode,process.hrtime(),process.memoryUsage(),process.nextTick(),process.platform,process.stderr,process.stdin,process.stdout,其中 process.memoryUsage()返回的内容是 Java 虚拟机的内存使用情况,与node.js描述完全不同,process.nextTick()完全用 setImmediate()实现。Punycode

既然 node.js模块自己都废弃了,不实现。Query String(查询字符串)

纯 js代码,完整实现。Readline(逐行读取)太多的终端相关行为,用得不多,暂不实现。

177

Page 178: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

REPL(交互式解释器)用得不多,暂不实现。Stream(流)核心中的核心,纯 js代码,完整实现,如同 node.js文档所说,应该尽量使用 flow模型。String Decoder(字符串解码器)使用 java.nio.charset.CharsetDecoder实现。Timer(定时器)用处很多的工具,完整实现。TLS/SSL

Node.js的设计方式过于冗余,Limax 版本直接在 Net模块中实现。(实际上不存在 tls 这个 模块),Limax 版本的实现不但支持 Node.js的 TLSSocket方式,也支持某些实际网络协议使用的 STARTTLS模式,允许在非安全连接上启停 TLS。Net模块的扩充1. net.Server类1.1. 构造参数 options中增加了 options.tls属性,如果设置了该属性,服务器按 TLSSocket

服务器方式启动。(http模块的使用的方式)2. net.Socket类2.1. 构造参数 options中增加了 options.tls属性,如果设置了该属性,socket.connect时客

户端按照 TLSSocket客户端方式启动。(http模块使用的方式)2.2. 方法 socket.starttls(tls),在非安全连接上使用 tls配置信息启动 TLS。starttls之后,允

许立即发送数据,实际上数据将在 tls握手完成以后被发送。2.3. 方法 socket.stoptls([function()]),结束 TLS 会话,回到非安全会话状态。如果传入一个

回调函数,该函数在 tlsDown 消息触发时被调用,详见 tlsDown 消息的说明。一般来说,如果需要设计 STARTTLS类应用,必须正确设计 STOPTLS协商机制,TLS结束握手完成以后再执行非安全连接状态下的会话。Limax实现上,保证不丢失任何数据,由于一些异步特性,如果 stoptls之后立刻发送数据,这些数据可能在安全会话中发送,也可能在安全会话结束以后发送。

2.4. 方法 socket.tlsrenegotiate(),启动 TLS重协商,该方法仅设置一个标记,指示在下一次 socket 活动时重新握手,所以不会返回任何错误。

2.5. 消息 tlsHandshaked,握手完成后,该消息被触发,参数模式为 function(principal, cert0, cert1 …) , 也 可 以 在 触 发 后 直 接 读 取 属 性 socket.tlsPeerPrincipal 与socket.tlsPeerCertificateChain , 获 得 对 方 的 证 书 主 题 与 证 书 链 , 其 中socket.tlsPeerPrincipal 为 javax.security.auth.x500.X500Principal 类 型 ,socket.tlsPeerCertificateChain 为 一 js 数 组 , 成 员 类 型 为java.security.cert.X509Certificate。

2.6. 消息 tlsDown,TLS 会话结束后该消息被触发,参数模式为 function(err),if(err)成立,178

Page 179: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

表示会话由某些异常导致,err.printStackTrace()可以获取异常信息,发生异常以后,用户应该自己 socket.destroy(err),关闭网络连接。(可以参考 http模块的实现)

3. net.createTLS(function(c)),创建 TLS配置,参数为一个 callback函数,c为一个 java 对象,类型为 limax.node.js.modules.tls.TLSConfig,所有的配置必须在该 callback中调用 c上的相应方法执行,这样可以最小化异常传播。

3.1. 信任检测类配置,客户端必须配置,服务器如果需要验证客户端,也必须配置。3.1.1. c.addAllCA(),将 jdk的 cacerts文件中的受信任证书全部加入。3.1.2. c.addTrustCertificate(data),加 入 信任证书, data 的类型可以是 String,可以是

Buffer,内容可以是文件路径,也可以是实际内容,实际内容的格式可以是 PEM,可以是 DER,可以有一个证书,也可以有多个证书,也可以是 PKCS7 证书。

3.1.3. c.addCRL(data),加入 CRL 列表,data的解释同上。3.1.4. c.setRevocationEnabled(revocationEnabled), revocationEnabled 为 boolean 类型,该

方法配置是否允许 CRL 测试,如果不允许,通过上面的 c.addCRL 加入的 CRL 列表也没 有 意 义 。 如 果 允 许 , 则 必 须 在 启 动 虚 拟 机 时 加 入 虚 拟 机 参 数com.sun.security.enableCRLDP=true,否则需要通过网络进行的测试(比如 OCSP)将会失败,CRL 测试多数情况下极其费时,默认为 false。

3.1.5. c.setTrustChecker(function(chain, exception)), chain 为 java 数组表示的证书链,exception为使用上面的配置检测失败以后抛出的异常,该方法返回 true,表示接受证书链,TLS握手过程可以继续。通常,浏览器访问 https网站时,发现某些证书异常,提示用户是否继续,就是使用这样的方式实现。复杂的证书链检测,应该将chain 和 exception 转发给用户自己实现的 java模块。

3.1.6. c.setPositiveTrustChecker(function(chain, exception)),执行积极的证书链检测,参数解释同上,使用该方法配置,将忽略上面配置的检测,直接使用设置的检测器,上面配置的检测能力不符合要求的情况下可以使用该方法。另外,使用该方法设置永远返回 true的检测器,可以辅助进行一些证书配置上调试。

3.2. 服务器证书私钥配置,需要客户端验证的情况下,客户证书私钥的配置。3.2.1. c.addPKCS12(data, pass),加入 PKCS12 证书包,data的类型可以是 String,可以是

Buffer,内容可以是文件路径,可以是 PKCS12 证书包的实际内容;pass的类型可以是 String,可以是 Buffer,内容为 PKCS12 证书包的密码。

3.2.2. c.addPrivateKeyAndCertificatePack(pkey, cert, pass),加入私钥和相应的证书链,实际上就是生成 PKCS12 证书包需要的信息。所有参数都允许是 String或者 Buffer,特别的,pkey关联的实际内容要求 PEM 格式表示的各种私钥格式,RSA,DSA,EC或者PKCS8,pass 提供了私钥密码,如果私钥没有设置密码,pass 被忽略,cert的要求与前面的 c.addTrustCertificate(data)相同。

3.3. TLS 引擎配置3.3.1. c.setProtocol(protocol) , protocol 为 String 类 型 , 允 许

SSLv3,TLSv1,TLSv1.1,TLSv1.2,默认 TLSv1.2。3.3.2. c.setNeedClientAuth(enable),enable为 boolean类型,默认为 false,设置为 true,

将强制客户端提供证书进行验证。3.4. 虚拟服务器相关配置3.4.1. c.setSNIHostName(hostname),hostname为 String类型或者 Buffer类型,客户端调用

该方法,设置请求的虚拟服务器名。3.4.2. c.addSNIServerName(type, name),type为 int类型,name为 Buffer类型,客户端调

179

Page 180: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

用该方法,按类型添加请求的虚拟服务器名,c.setSNIHostName,实际上提供了type=0,所有这些 type不能重复,否则抛出异常。

3.4.3. 服务器没有使用 JDK 提供的 SNIMatcher 机制实现,采用的方法是,如果服务器端配置加入了多个私钥证书包,则启动虚拟服务器方式,SNIServerName使用证书Subject中 CN设定的域名模板和 SubjectAlternativeNames中的 dNSName类型的域名模板进行模式匹配,选择正确的证书链和私钥,如果所有的匹配都不成功,由 JDK的 pkixKeyManager选择一对证书链和私钥。之所以这样实现,是因为 JSSE 提供的方 案 并 不 完 备 , 存 在 逻 辑bug。javax.net.ssl.ExtendedSSLSession.getRequestedServerNames()方法可以获取对方请求的服务器名,问题在于握手阶段选择证书时,该方法返回空集,直到握手完成,才返回客户端请求的证书。

示例:服务器 testtlsserver.jsvar net = require('net')var tls = net.createTLS(function(c) {

c.addPKCS12('/work/js.test/testtls.p12', '123456');c.addTrustCertificate('/work/js.test/testtls.cer');c.setNeedClientAuth(true);

});var server = net.createServer(function(c) {

c.on('error', function(e){console.log('error', e.stack)e.printStackTrace();

});c.once('tlsHandshaked', function() {

console.log('tlsHandshaked:');for (var i = 0; i < arguments.length; i++)

console.log(arguments[i].toString());});c.once('tlsDown', function(err) {

console.log('tlsDown'); if (err)

client.destroy(err)})c.on('end', function() {

console.log('client disconnected');});c.starttls(tls);c.write('hello\r\n');c.pipe(c);c.pipe(process.stdout)

});server.on('error', function(err) {

180

Page 181: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

throw err;});server.listen(8124, function() {

console.log('server bound');});

客户端 testtlsclient.jsvar net = require('net')var tls = net.createTLS(function(c) {

c.addPKCS12('/work/js.test/testtls.p12', '123456');c.addTrustCertificate('/work/js.test/testtls.cer');

});var client = net.createConnection({

port : 8124}, function() {

client.starttls(tls);console.log('connected to server!');client.write('world!\r\n', function() {

print('send done')client.stoptls(function() {

print('stopped');client.write('wwww?');

});});

});client.on('tlsHandshaked', function() {

console.log('tlsHandshaked:');for (var i = 0; i < arguments.length; i++)

console.log(arguments[i].toString());});client.on('tlsDown', function(err) {

console.log('tlsDown')if (err)

client.destroy(err)});client.on('data', function(data) {

data = data.toString();console.log(data);if (data[data.length - 1] == '?')

client.end();});client.on('close', function() {

console.log('disconnected from server');181

Page 182: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

});client.on('error', function(e) {

console.log('error', e.stack)e.printStackTrace();

})

1. 该例子通过对 ECHO功能的服务器客户端进行 TLS 改造而来,要求客户端提供认证,客户端服务器简单使用同样的私钥和证书,testtls.p12,testtls.cer,可以按 Node.js文档示例的方式制作。

2. 客户端 STARTTLS后,立即向服务器发送”world\r\n”,可以明确该字符串在 TLS 会话中发送,发送完成后立即 STOPTLS,停止完成后,发送字符串”wwww?”,这里可以明确该字符串在非安全会话中发送,这两个串最终都会被服务器 ECHO 回来。

3. 服务器 accept客户端连接以后,立即在该连接上 STARTTLS,随后向客户端发送’hello\r\n’,可以明确该字符串在 TLS 会话中发送,收到”world\r\n”,向客户端 ECHO ”world\r\n”,这里注 意到,服务器并不需要关心客户端是否 STOPTLS,切换由 底层完成,”wwww?”的 ECHO,肯定在非安全会话中完成。逻辑上讲,尽管”world\r\n”在TLS 会话中收到,ECHO 回去的数据是否在 TLS 会话中发送,不应该作假设,毕竟客户端发送数据之后立即 STOPTLS。这里可以看到设计 STARTTLS类协议,应用层面必须仔细考虑 STOPTLS的握手机制。

4. 运行例子可以观察 tlsHandshaked,tlsDown两个消息的效果。TTY(终端)终端使用很少,不实现。UDP/Datagram

1. UDP服务器理论上不能提供 Cluster能力,如果用 Cluster方式启动,后续服务器 bind时会产生地址已经使用异常。目前 UDP最大的用途还是局域网环境内进行组播实现服务器间协同,不需要承载太重的负载。重负荷的情况可以设计为一个虚拟机收发报文,通过线程间通讯将任务分发给服务 Cluster。

2. socket.addMembership(multicastAddress[, multicastInterface])的实现与 node.js描述有差异,Limax在实现上,缺省multicastInterface的行为是查找所有已经 UP 并且有组播能力的接口进行绑定。不同系统接口名差异太大,选择正确的名字都是问题,很难保证正确性;现在的计算机网络接口太多,让操作系统自己选择一个接口也是不可靠行为,谁知道能选中一个自己需要的?所以 java.nio.channels.MulticastChannel接口也不提供自动选择能力。

3. socket.setTTL(ttl)与 socket.setMulticastTTL(ttl) 等同,因为 DatagramChannel上仅提供一个 TTL设置方法。

URL

完整实现,仔细研究 node.js文档可以发现问题,HTTP模块会用到这个 URL解析器,这个解析器返回的 urlObject.host 并不是 HTTP模块需要的 host,HTTP模块文档需要的 host实际上是这里的 urlObject.hostname,基本数据字典都没有统一,不知道怎么规划的?

182

Page 183: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

Util(实用工具)提 供 util.format() , util.inherits() , util.inspect() 三个 方法 。其 中, util.format() 使用java.lang.String.format 格式化参数,格式参数与 sprintf 格式参数有稍许差异;inspect用于递归展示对象数据,不支持显示颜色之类的特性。实际上 console.log输出的串就是 inspect 展开的。V8

Nashorn,没 V8啥事。VM(虚拟机)1. 所有方法的 options,都是 V8 相关的,忽略。2. DebugContext是 V8特性,所以 vm.runInDebugContext(),作为 vm.runInNewContext()的

别名实现。3. 不建议使用已有 sandbox创建 Context,最好 sandbox = vm.createContext(),然后初始化

sandbox,有利于提高性能。这是 Nashorn的限制,非全局对象设置到 Context上访问,会在该对象上添加一个 nashorn.global成员作为真正的全局对象执行访问,为了保证正确性,执行完成之后必须执行一次拷贝。

ZLIB(压缩)node.js使用 zlib实现,limax使用 java.util.zip实现1. java.util.zip没有提供太多配置用常量,多数情况不需要使用 options,默认实现即可。2. node.js文档中时间推送服务器的例子,flush的使用与文档本身定义的原型不符,原型中 callback 参数必选,例子没有。

3. 实 验 时 间 推 送 服 务 器 例 子 , 最 好 把 gzip 算 法 改 为deflate,java.util.zip.GZIPOutputStream的 cache太大,直接的结果是感觉服务器长时间不响应。

增强模块长期以来,node.js 被诟病最多问题的就是缺乏持久化能力与管理能力。Java能够轻松

解决这些问题,所以扩充两个模块 sql 和 edb,分别提供关系数据库,键值对数据库访问能力,扩充一个简单 JMX查询模块monitor。Sql

sql.Connection类 ‘close’ 事件 connection.execute(statement [,parameter1[,parameter2…]], callback) connection.destroy()

sql.createConnection(url)

Sql.Connection类183

Page 184: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

提供数据库连接,内部实现为连接池,不活动的连接 1分钟淡出。var url = 'jdbc:mysql://192.168.1.3:3306/test?user=root&password=admin&autoReconnect=true'var sql = require("sql");var conn = sql.createConnection(url);conn.once('close', function() {

console.log('closed')});conn.execute("select * from a where id > ?", 1, function(err) {

if (err) {console.log(err);return;

}var s = '!';for (var i = 1; i < arguments.length; i++)

s += arguments[i] + ' ';console.log(s)

});conn.destroy();

‘close’ 事件执行 destroy,关闭数据库连接,但是关闭时可能还有活动连接存在,所有的活动连接结束以后触发该事件,表示数据库连接彻底关闭。connection.execute(statement [,parameter1[,parameter2…]], callback)statement <String> 符合 jdbc规范的 sql语句,用?占位parameter <Object> 该参数的个数必须和 statement中的占位符个数相等callback <Function> 结果回调parameter通过 PreparedStatement.setObject()方法设置到查询请求中,能接受的类型取决于应用数据库的 JDBC连接库,一般来说 int类型 string类型没有问题,其它类型必须通过实际测试决定,小心 long,js的 long 只有 53 位有效位。最坏情况下,可以重新设计 sql语句,支持 string 参数,使用数据库自身的 convert函数将 string 转换为目标类型。callback 第一个参数是 err,这一点与 node.js习惯一致。SELECT使用的 callback 参数数量取决于查询语句本身返回的列数,查询语句返回 N 列,那么使用 N+1个参数进行回调。查询语句可能返回空集,所以为了表示查询结束,最后一次 callback传入 0个参数,可以通过arguments.length = 0检测查询结束,所以上面的例子,最后一行输出总是单字符”!”。返回结果的类型思考与 paramter一致,如果返回了不可识别的类型,考虑修改 sql语句使用数据库自身的 convert 转换为 string。UPDATE,DELETE,INSERT,这一类操作用 2个参数调用 callback一次,第二个参数为影响的行数。

184

Page 185: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

connection.destroy()结束数据库连接,destroy之后的 execute 调用直接用连接已经关闭的错误回调。sql.createConnection(url)url <String> 数据库连接 url

创建数据库连接的便捷方法。Edb

edb.Instance类 ‘close’ 事件 instance.addTable(table1, [,table2 [,table3 …]], callback) instance.removeTable(table1, [,table2 [,table3 …]], callback) instance.insert(table, key, value, callback) instance.replace(table, key, value, callback) instance.remove(table, key, callback) instance.find(table, key, callback) instance.exist(table, key, callback) instance.walk(table, [,from], callback) instance.destroy()

edb.createEdb(path)

edb.Instance类edb 对象实例,提供键值对数据库访问操作。首先,必须创建目录’/work/js.test/dbhome’,Edb不会自动创建。var edb = require('edb').createEdb('/work/js.test/dbhome');edb.once('close', function() { console.log('close')});edb.addTable('a', function() {

var n = 3;function insert3() {

if (--n > 0)return;

edb.find('a', '1', console.log);edb.walk('a', '0', console.log);edb.destroy();

}edb.insert('a', '0', 'AA', insert3);edb.insert('a', '1', 'b', insert3);edb.insert('a', '3','',insert3);

});注意一下这段代码的并行特性,3个 insert 操作并行,完成以后 find与 walk 操作并行,之

185

Page 186: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

后 destroy关闭数据库,禁止后续查询操作。‘close’ 事件执行 destroy,关闭 Edb实例,但是关闭时可能还有操作正在执行,所有的操作执行结束以后触发该事件,表示 Edb实例彻底关闭。instance.addTable(name1, [,name2 [,name3 …]] , callback)name <String> 往数据库中添加的表名callback <Function>

一次可以添加多个表,执行完成 callback 被调用,如果有错误第一个参数为 Exception 信息。数据库中已有该表,操作忽略。instance.removeTable(name1, [,name2 [,name3 …]] , callback)name <String> 从数据库中删除的表名callback <Function>

一次可以删除多个表,执行完成 callback 被调用,如果有错误第一个参数为 Exception 信息。数据库中该表不存在,操作忽略。instance.insert(table, key, value, callback)table <String> 表名key <Buffer|String>value <Buffer|String>callback <Function>

插入一个键值对,key或者 value如果为 String,在内部用 utf8 编码为 Buffer后执行。callback(err, succeed);如果 key在表中已经存在 succeed == false,插入失败。instance.replace(table, key, value, callback)table <String> 表名key <Buffer|String>value <Buffer|String>callback <Function>

替换一个键值对,key或者 value如果为 String,在内部用 utf8 编码为 Buffer后执行。callback(err)。key在表中不存在执行插入,存在则替换。instance.remove(table, key, callback)table <String> 表名key <Buffer|String>callback <Function>

186

Page 187: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

删除一个键值对,key如果为 String,在内部用 utf8 编码为 Buffer后执行。callback(err)。key在表中不存在,操作忽略,不作为错误。instance.find(table, key, callback)table <String> 表名key <Buffer|String>callback <Function>

执行查找,key如果为 String,在内部用 utf8 编码为 Buffer后执行。callback(err, value)。key在表中不存在,value == null,value为 Buffer类型instance.exist(table, key, callback)table <String> 表名key <Buffer|String>callback <Function>

key如果为 String,在内部用 utf8 编码为 Buffer后执行。callback(err, value)。key在表中不存在,value == false

instance.walk(table, [,from], callback)table <String> 表名from <Buffer|String>callback <Function>

from如果为 String,在内部用 utf8 编码为 Buffer后执行。遍历表,如果有 from 参数,从 from的下一个键值对开始遍历。callback(err, key, value)。key, value 均为 Buffer类型instance.destroy()关闭 Edb实例连接,destroy之后执行的操作直接用实例已经关闭的错误回调。edb.createEdb(path)创建 Edb实例的便捷方法,应用必须保证 path目录存在,否则会抛出异常,结束当前代码片段的执行。Monitor

monitor.Host类 ‘close’ 事件 host.query(objectName, callback) host.destroy() host.ref() host.unref()

187

Page 188: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

monitor.connect (host, serverPort, rmiPort[, username, password])例如,启动 switcher服务器(配置了本机 jmx服务,见 service-switcher.xml)

var host = require('monitor').connect('localhost', 10003, 10002);setInterval(function(){

host.query('java.lang:type=Threading', console.log);}, 10000);

运行该程序,即可 10秒间隔,查询线程状态。mointor.Host类描述了一个 JMX连接‘close’事件执行 destroy,关闭 Host 对象,但是关闭时可能还有操作正在执行,所有的操作执行结束以后触发该事件,表示 Host彻底关闭。host.query(objectName, callback)objectName<String> 需要查询的 jmx 对象名callback <Function>

查询完成使用 callback(err, obj) 回调。host.destroy()关闭 Host 对象连接,destroy之后执行的操作直接用实例已经关闭的错误回调。host.ref()同 unref 相反,如果之前在 host 对象上调用过 unref,然后调用该方法,禁止程序退出,多次调用无效。调用该方法返回对象本身。host.unref()如果这个 host 对象是事件系统内唯一对象,在该对象上调用 unref,允许程序退出,多次调用无效。调用该方法返回对象本身。monitor.connect(host, serverPort, rmiPort[, username, password])host<String>serverPort<Number> IntegerrmiPort<Number> Integerusername<String>password<String>

提供连接参数,创建 Host 对象。

188

Page 189: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

PKIX 支持为了建立跨服务器、跨项目、跨组织机构的信任关系,Limax 集成 PKIX模块,提供创建CA,签署,回收 X509 证书的能力,为系统间安全互操作提供基础支持。同时,提供便利的管理工具,在确保安全的基础上,最大限度简化 CA系统管理负担。证书以及 CRL的相关概念可以参考 RFC5280,http://www.ietf.org/rfc/rfc5280.txtOCSP服务相关概念可以参考 RFC2560,http://www.ietf.org/rfc/rfc2560.txt

设计原则ROOTCA 离线管理,使用命令行工具操作,规避网络安全风险。CA服务实现为网络服务,集成 3个服务器。1 签署,回收证书采用近线方式,实现为 HTTP服务,绑定在服务器的 localhost:8080接

口上,如果需要远程操作,必须架设自己的反向代理,所有的操作必须通过 TOTP方式提供授权码。最大限度限制未授权访问。

2 通过 HTTPS方式向外提供自动 Renew服务,允许签署短期证书,证书超出 80%寿命之后,向服务器发起 Renew请求,更换证书。使用短期证书,可以有效减少回收列表长度,降低回收管理负担;一旦证书不再使用,快速过期,减少盗用风险,降低管理负担。

3 通过 HTTP方式对外提供 OCSP服务,同时自动发布 CRL。CA 存在过期的问题,所以提供相应的解决方案,确保 Renew的新证书由新 CA签署,避免重新线下申请。通过重新解释证书策略的方式,以 CPS进行名字空间命名,为多个 ROOTCA间互操作,提供基础支持。允许使用支持 PKCS11的智能卡设备,为 Key安全提供进一步的保障。应用端模块,自动维护私钥、证书链,执行 Renew 操作,隐藏实现复杂性,提供简单的接口,简化开发。Location

Location 对应了 java.security.KeyStore.PrivateKeyEntry,是使用指定私钥及其证书链的基础。Location用 opaque URI方式描述,scheme:[alias@]path[#algorithm]

189

Page 190: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

scheme

支持的 scheme为 FILE,PKCS11,PKCS12,JKS,JCEKS。约定了私钥、证书链的存放位置。alias

alias与 KeyStore定义的 alias的含义相同。一个私钥、证书链容器,允许存放多份私钥、证书链条目,用 alias索引。除了 FILE,PKCS11不允许缺省 alias外,其它 scheme 均可缺省,缺省的情况下通过 KeyStore.aliases()方法查找第一个 alias,作为隐含 alias。注意:JDK在 alias的管理上没有明确的规范进行约定,实现上非常混乱。例如,PKCS12可以使用空串 alias,但是 javax.net.ssl.KeyManagerFactory,却搜索不出这样的 alias,行为与JKS,JCEKS不一致。同样,PKCS11类型的 KeyStore,可以通过 KeyStore.aliases()搜索出这样的 alias,KeyStore.loadKey()的时候却又失败。所以,尽量避免缺省 alias。path

path是一个文件系统路径,在不同的 scheme下有不同的解释。1. FILE的情况下,私钥、证书链分别存储为.key文件与.p7b文件。path是一个目录名。

例如:” file:abc@/work”,指定了私钥文件 /work/abc.key,证书链文件 /work/abc.p7b。

2. PKCS11的情况下,path 指定了 pkcs11配置文件,例如,对于 epass2003这样一个支持PKCS11的 TOKEN,可以找到 PKCS11 驱动,建立配置文件:/work/epass2003-pkcs11.cfg:name = epass2003library = c:/work/eps2003csp11.dll那么,”abc@/work/epass2003-pkcs11.cfg” 这样一个 location,指明了 pkcs11容器中abc alias 对应的私钥、证书链。

3. PKCS12、JKS、JCEKS,的情况下,path为相应 KeyStore文件的路径。例如”pkcs12:/work/client.p12”,指明了使用 client.p12文件里面的第一个 alias 对应的私钥、证书链。特别的,因为 CA可以签发 PKCS12 私钥证书包,这样的证书包只放置一对私钥、证书链,这样的证书包默认使用空串 alias,也只有这种情况下,使用缺省 alias是合理的。

algorithm

证书相关算法描述,构造为 PublicKeyAlgorithm/PublicKeyLength[/SignatureLength]PublicKeyAlgorithm可取的值为 RSA,DSA,EC。PublicKeyLength,与相应的算法相关。SignatureLength,可取的值为 256,384,512。除了初始化 ROOTCA,签署 CA 证书之外,algorithm几乎都可以缺省,之后的命令行操作详细介绍。开发工具limax.pkix 包提供了 Key管理工具,证书服务开发工具,证书服务客户端工具。

190

Page 191: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

Key管理工具limax.pkix.KeyInfo所有相关工具的基础,解释 Location,获取,保存相应的私钥、证书链,同时提供私钥、证书链的更新支持,更新过程保证事务完整性,PKCS11的情况下,有稍许安全性降低。更新的私钥、证书链首先会暂存到本地存储(当然,私钥是被启动时提供的密码加密的),然后执行一个拷贝过程,最后删除本地暂存数据。其它 scheme下,必须保证 Location 指向的位置可写。另外,直接支持在 KeyInfo的基础上创建 SSLContext。证书服务开发工具limax.pkix.CAService通过实现 X509*Parameter系列接口,描述 ROOTCA 证书,CA 证书,EndEntity 证书,CRL请求,Renew请求,执行相应的签署操作。具有相同 Subject的 CAService 允许进行组合,使用过期时间最迟的 CA 证书签署新证书;使用被回收证书的发布 CA签署 CRL。这种方式可以确保提供可持续服务。证书服务客户端工具limax.pkix.SSLContextAllocator使用 Location 指定的私钥、证书链,分配对应的 SSLContext,提供给应用作进一步开发。短连接应用的情况下,每次获取 SSLContext直接使用即可,长连接的情况下,可以注册一个Listener获取 renew 消息,适时更换新的 SSLContext。管理命令limax.pkix.tool包,提供了 pkix 相关管理工具的实现。一共 9条命令。以下命令,如果涉及到私钥访问的,都会提示输入私钥使用密码,涉及到私钥创建的,会 2 次确认私钥创建密码。java -jar limax.jar pkix algo

显示支持的算法列表。java -jar limax.jar pkix keygen <algo> <PathPrefix>

请求证书签名有两种方式,一是客户端生成私钥,提交公钥给 CA,签署证书,生成证书链;二是 CA直接生成公钥私钥对,签署证书,将私钥、证书链打包为 PKCS12。第一种形式,私钥不会在网络上传输,具有更高的安全性。该命令为第一种方式提供支持。例 如 , 执 行 java –jar limax.jar pkix keygen “rsa/1024” “/work/client” 生 成 私 钥 文件/work/client.key,公钥文件 /work/client.pub。将 client.pub,提交给 CA签署证书。这种情况下,CA发还一个.p7b 证书链文件,可以命名为 client.p7b,放置在/work目录下,之后即可通过 Location: ”file:client@/work”使用。

191

Page 192: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

java -jar limax.jar pkix copy <locationSRC> <locationDST>

Location 转换工具,scheme之间进行拷贝,可能覆盖 locationDST 相应容器中的 alias条目。例如,java –jar limax.jar pkix copy “file:client@/work” “pkcs12:client@/work/client.p12” 将 file方式描述的私钥、证书链转换为 PKCS12方式。注意,PKCS11不可能作为拷贝的源,因为 PKCS11规范要求,私钥不可导出,这样的操作必然失败。java -jar limax.jar pkix initroot <locationROOT> <subject> <yyyyMMdd> <yyyyMMdd>

初始化 ROOTCA,例如:java -jar limax.jar pkix initroot "file:ca@/work/pkix/root#rsa/2048/256" "dc=limax-project,dc=org" "20100101" "20500101"以"dc=limax-project,dc=org"为 subject签署有效期从 2010-01-01到 2050-01-01的自签名证书,使用算法 rsa/2048,256 位签名。在这里算法参数不可缺省。java -jar limax.jar pkix initca <locationROOT> (<locationCA>|<keygen/.pub>) <subject>

<yyyyMMdd> <yyyyMMdd> <OcspDomainOfRoot>

使用 ROOTCA 证书,签署一个 CA 证书,例如:java -jar limax.jar pkix initca "file:ca@/work/pkix/root" "pkcs12:/work/pkix/ca/ca0.p12#ec/256" "dc=ca,dc=limax-project,dc=org" "20150101" "20200101" "root.limax-project.org"使用上面签署的 ROOTCA 证书,签署 subject为"dc=ca,dc=limax-project,dc=org"的 CA 证书。在这里, locationCA使用 ec/256算法。允许缺省算法参数,缺省的情况下,直接继承ROOTCA的算法参数。使用 ROOTCA时也可以使用”file:ca@/work/pkix/root#rsa/1024/512”这样的描述形式,在这里因为证书已经签署,所以 rsa/1024没有意义,被忽略,512 指定了签名长度,覆盖了 CA 证书签署时使用的 256长度。“root.limax-project.org” 指定 ROOTCA关联的 OcspServer的域名。证书签署方法使用从该域名的派生的 OCSP服务信息与 CRL分发点信息填写证书,PKIX应用系统需要访问这些 URL获得证书回收状态。java -jar limax.jar pkix initocsp <location> <locationOcsp> <subject> <yyyyMMdd>

<yyyyMMdd>

签署一份 OCSP服务专用证书,通常情况下这里的 location就是 locationROOT,ROOTCA规划为离线访问,需要运行一个关联的 OcspServer,提供下级 CA的回收状态,该证书即是提供给 OcspServer用来签名状态响应的。java -jar limax.jar pkix gencrl <location> <crlfile> <nextUpdate(yyyyMMdd)> [[-](CertDir|

CertFile)]

签署 CRL,通常 情 况下这里的 location 就是 locationROOT,ROOTCA 规划为离线访问,ROOTCA的下级 CA 相关 CRL必须离线生成,然后提供相应下载。nextUpdate,指出下一次 CRL更新的预期时间。CertDir|CertFile 参数缺省时,如果 crlfile文件不存在,生成一份包含空列表的 crlfile;如果

192

Page 193: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

存在则更新 crlfile的 nextUpdate 信息。CertFile,可以是 X509 证书格式,也可以是 PKCS7 格式,所有由 location 指定的 CA签署的证书被提取出来,回收掉。CertDir目录扫描一层,扫描内部所有 CertFile 执行回收。如果 CertFile或者 CertDir之前前缀了”-”,表示执行 recall 操作,从 CRL 列表中移除相应证书。java -jar limax.jar pkix ca [path to caserver.xml]

根据 caserver.xml的配置运行 CA服务器。后文详细介绍。java -jar limax.jar pkix ocsp [path to ocspserver.xml]

根据 ocspserver.xml的配置运行独立 OcspServer,一般提供给 ROOTCA使用。后文详细介绍。CAServer

参考 caserver.xml,进行说明。CAServer顶层元素<CAServer archive="/work/pkix/ca/certs" authCode="123456" domain="ca.limax-project.org">…………………………………….</CAServer>3个属性:domain:指定了该 CAServer运行的服务器域名,由该 CAServer签发的所有证书的 OCSP服务信息,CRL分发点信息,均通过该域名派生出来。archive:该 CAServer签发的所有证书的存档目录,包括 Renew 过程签发的证书。目录内文件以”certSerialNumber”.cer 方式命名,certSerialNumber为证书序列号,文件格式为 DER。authCode:授权码种子,实际运营时,不应该填写该属性,而应该在服务器启动时输入。这个种子用来生成 TOTP授权码程序,签署证书时使用该程序生成当时的授权码,填写证书服务页面的授权码字段。CAServer 运行后,当前目录下将生成 authcode.jar,需要填写授权码时,可以 java –jar authcode.jar运行该程序。

在这个界面下,输入这一次运行 CAServer 提供的授权码种子,然后回车,切换为如下界面。

193

Page 194: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

点击该界面的 authCode 按钮,生成的 TOTP 密钥的将被生成到系统剪贴板,直接粘贴到相应页面即可。注意,每次启动 CAServer 都会生成新的 authcode.jar,之前的失效,即便使用了同样的授权码种子。CAService元素<CAService location="pkcs12:/work/pkix/ca/ca0.p12" passphrase="123456"/>2个属性:location:ROOTCA为 CA签发的私钥,证书链 location。passphrase:location的私钥启用密码,实际运营时,不应该填写该属性,而应该在服务器启动时输入。该元素允许多条,所有 location 指向的 CA 证书必须具有相同的 Subject,有效期不同,整体快要过期时(由最后过期的 CA 证书的过期时间减去该 CAServer签发证书的最长有效期决定),应该向 ROOTCA发起申请,添加配置。已经过期的 CA,可以从配置中剔除。CertServer元素<CertServer port="8080" publicKeyAlgorithmForGeneratePKCS12="rsa/1024">………………………………</CertServer>两个属性:port:CertServer的 HTTP服务运行端口 8080。CertServer服务绑定在 localhost接口上。publicKeyAlgorithmForGeneratePKCS12:CAServer 支持 CertServer 帮助生成私钥,打包成PKCS12文件发送给申请者的形式,该属性配置了密钥对生成算法。CertServer 内部元素,与页面元素的配置选项相对应,可以根据具体需求调整。特别的,subject输入必须是合法的 X500 Distinguished Name。若有进一步的约定,可以配置 Subject元素的 pattern 相关属性,使用 java.util.regex.Pattern支持的模式,校验 subject合法性,template属性提供 subject 字段初始模板,简化页面输入。NotAfter 参数 period系列属性单位为天。其它元素内 mandatory属性,指定相应页面元素是否 disable。非法输入在相应位置用红框提示。访问 http://localhost:8080/可以看到如下页面:

194

Page 195: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

该页面使用申请方提供的公钥(java –jar limax.jar genkey 生成的.pub文件)生成.p7b 证书链文件。点击 publicKey,页面切换为:

由 CAServer生成私钥,输出.p12文件,这里设定的 PKCS12Passphrase(至少 6 位),必须选择独立渠道提供给申请方。

195

Page 196: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

该页面用来回收证书,输入内容可以是独立的证书文件,也可以是之前签发的.p7b文件。

该页面用来撤销回收,输入内容同上。OcspServer元素7个属性:port:指定 OCSPServer的 HTTP服务运行端口 80,该服务绑定在所有网络接口上。nextUpdateDelay:下一次更新延迟的天数,该属性既用来设置 Ocsp响应中的 nextUpdate信息,也决定了 CRL 列表生成周期。certificateAlgorithm:CAServer自动签署 Ocsp签名证书时使用的公钥算法。certificateLifetime:CAServer自动签署的 Ocsp签名证书的以天为单位的寿命。signatureBits:Ocsp响应签名位数,合法值为 256,384,512。responseCacheCapacity:每一个 Ocsp响应都需要使用 Ocsp 证书签名,为了提高性能,必须提供 cache,cache设计为 32个分区的 LruCache,该参数配置了响应 cache 总容量。ocspStore:配置 ocsp 存储目录路径。ocspStore管理方式:1. ocspStore目录下,按 CA的 serialNumber 区分 CA 子目录,每个子目录内的证书命名方

196

Page 197: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

式为:”certSerialNumber”.”certNotAfter”.”revocationTime”.cer,其中,certSerialNumber为被回收证书序列号,certNotAfter为被回收证书过期时间,revocationTime为回收时间。

2. 过期的被回收证书自动从相应的 CA 子目录内删除,CA 过期,整个子目录都被自动删除。

3. ocspStore目录本身被监控,如果目录中发现了新文件,则该文件被当作需要回收的证书文件进行识别,识别成功执行回收,然后删除,否则直接删除。需要注意,期望被回收的证书文件首先应该在同一文件系统内其它位置创建出来,然后 MOVE 到ocspStore,而不要 COPY,COPY 存在一个写入过程,文件创建出来的时候立刻被监视到,识别操作执行时写入过程可能没有完成,导致识别失败,直接删除,回收操作被忽略。

4. ocspStore目录下的 CA 子目录被监控,如果某个回收的证书文件被删除,则自动执行Recall 操作。

5. 监控目录的管理方式主要提供给独立 OcspServer使用,在 CAServer 内,还是应该通过管理页面执行 Revoke,Recall 操作。

CAServer 启动时,自动为该服务器签署 Ocsp签名证书,证书即将到期时,自动更新。CertUpdateServer元素4个属性:port:指定 CertUpdateServer的 HTTPS服务运行端口 443,该服务绑定在所有网络接口上。renewLifespanPercent:证书寿命过去了该百分比才执行 Renew 操作,避免客户端频繁renew 增加服务器负荷。certificateAlgorithm:CAServer自动签署 HTTPS服务器证书时使用的公钥算法。certificateLifetime:CAServer自动签署的 HTTPS服务器证书的以天为单位的寿命。CAServer 启动时,自动为该服务器签署 HTTPS服务器证书,证书即将到期时,自动更新、重启服务。该服务器仅接受来自同一 CAServer签署的证书的 Renew请求。被 Revoke的证书,通不过验证,无法 Renew。Trace元素与其它 Limax服务配置方式相同,见手册正文,运行管理配置一节。OcspServer

独立运行的 OcspServer。ROOTCA 离线运行,所以必须启用独立的 OcspServer发布由该 ROOTCA签发的 CA状态。OcspServer顶层元素9个属性:domain:指定该 OcspServer运行的服务器域名。location:Ocsp签名证书的 location

197

Page 198: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

passphrase:location的私钥启用密码,实际运营时,不应该填写该属性,而应该在服务器启动时输入。cRLFile:ROOTCA签发的 CRL文件的路径。port,nextUpdateDelay,signatureBits,responseCacheCapacity,ocspStore:这 5个属性的描述见 CAServer一节中 OcspServer元素的说明。Trace元素与其它 Limax服务配置方式相同,见手册正文,运行管理配置一节。与测试配置相对应的命令举例所有密码提示下都输入 123456。创建并运行 ROOTCA

1 签署 ROOTCAjava -jar limax.jar pkix initroot "file:ca@/work/pkix/root#rsa/2048/256" "dc=limax-

project,dc=org" "20100101" "20500101"2 签署 ROOTCA的 Ocsp签名证书

java -jar limax.jar pkix initocsp "file:ca@/work/pkix/root" "pkcs12:/work/pkix/root/ocsp.p12#rsa/2048" "cn=OCSP Responder,dc=limax-project,dc=org" "20170101" "20200101"3 初始化 ROOTCA的 CRL

java -jar limax.jar pkix gencrl "file:ca@/work/pkix/root" /work/pkix/root/ca.crl 201710014 配置域名服务器,将 root.limax-project.org解析到运行独立 OcspServer的机器的 IP上,

然后运行 OcspServer。java -jar limax.jar pkix ocsp ocspserver.xml

签署并运行 CAServer(可以签署第一个 CA使用几次以后,签署第二个 CA)1. 使用 ROOTCA签署第一个 CA

java -jar limax.jar pkix initca "file:ca@/work/pkix/root" "pkcs12:/work/pkix/ca/ca0.p12#ec/256" "dc=ca,dc=limax-project,dc=org" "20150101" "20200101" "root.limax-project.org"2. 使用 ROOTCA签署第二个 CA(subject 相同)

java -jar limax.jar pkix initca "file:ca@/work/pkix/root" "pkcs12:/work/pkix/ca/ca1.p12#rsa/2048" "dc=ca,dc=limax-project,dc=org" "20170101" "20300101" "root.limax-project.org"3. 配置域名服务器,将 ca.limax-project.org解析到运行 CAServer的机器的 IP上。

java –jar limax.jar pkix ca caserver.xml4. 拷贝出当前目录下生成的 authcode.jar,在证书管理页面中使用。

198

Page 199: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

现实应用的考虑规划的考虑1. 公钥系统仅仅是技术规范,一个基础设施。2. 如果要达成系统间相互信任的目标,必须在系统间达成相应的协议,所有参与者必须

遵从协议的约束。3. 通常情况下,设计审计规范,监督协议的执行是必要的。4. 例如,公共域 CA签发一份网站证书,向用户证明网站的真实性。一旦这样的 CA为域

名劫持者签发同样的证书,用户必然被欺骗。所以,公共域 CA必须是可信第三方,麻烦在于可信第三方不可自证,所有的公共域 CA都提供审计,剩下的问题只能扔给法律。

5. 可以这样理解,公钥系统提供的信任,是现实中的信任向电子化系统的延伸,现实中的信任才是真正的基础。

6. 确定现实信任关系的系统之间,可以自己约定证书签署字段的模式,自己解释相应的字段。Limax重新解释证书策略,提供枚举 limax.pkix.X509CertificatePolicyCPS,需要的情况下可以继承该枚举,为私有的信任系统提供一个最基本的划分。

运营的考虑1 ROOTCA

1.1 有效期内私钥必须安全保存,可以选择各种线下方式,PKCS11智能卡可以作为一个选择,但不一定是最好的,硬件失效也应该考虑

1.2 注意 Ocsp签名证书过期问题,一旦 Ocsp签名证书过期,OcspServer自动退出。1.3 Ocsp签名证书的实际过期时间由证书过期时间减去 nextUpdateDelay决定,因为

签名证书没有理由为自己有效期范围外的行为背书。1.4 ROOTCA 如果要回收 CA 证书,应该 执行 2 步操 作,首先将 CA 证书上传到

OcspServer,移动到 ocspStore 目录;接下来使用 gencrl 签署 CRL,上传到OcspServer cRLFile 指定位置。

1.5 ROOTCA有责任根据上一次签署 CRL时设置的 nextUpdate日期更新 CRL 并上传到OcspServer服务器主机。

2 CAServer2.1 安全的服务器主机(网络安全,物理安全都必须考虑),毕竟 CA私钥存储在上

面,可以考虑使用 PKCS11智能卡。2.2 证书管理服务绑定在 localhost上,实际上,操作者不应该直接登录 CAServer 操

作管理证书,必须架设反向代理,这里故意增加了一个可审计环节。2.3 启动 CAServer生成的 authcode.jar,虽然可以随意分发,但也只是在 authCode种

子足够安全的前提下。如果怀疑出现了泄密问题,可以重新启动 CAServer,重新设置种子生成新的 authcode.jar。

2.4 执行证书管理服务的机器,防木马是必须的。2.5 注意 CA 证书过期问题,提前向 ROOTCA发出新的申请。2.6 设计定时执行脚本,收集存档目录内的证书,根据设计规划管理所有已签发证

书,收集之后清理存档目录。3 服务器内存许可的前提下,尽量配置大的 Ocsp响应 cache。

199

Page 200: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

4 对外提供的服务,使用 HTTP,HTTPS协议,可以架设相应的正向代理。5 运行 ntp服务,同步时钟。6 选择抗攻击,安全的域名服务。7 为了达成抗攻击、负载均衡的目标,OcspServer与 CAServer 均可以部署多份。这种情

况下,签署证书使用任意一台 CAServer 即可;REVOKE,RECALL 操作可以考虑在服务器端定时执行简单脚本,到特定位置获取操作数据,在 ocspStore目录内直接操作。

开发的考虑1 规划阶段设计的证书字段、策略解释方式必须在开发的时候正确执行。2 CAServer签发的证书链,自带了签发 CAServer的 ROOTCA 证书,简单情况下的验证是

足够的。如果 ROOTCA本身过期更新了,或者需要验证别的 ROOTCA派生出来的证书,就 必 须 把 更 新 的 、 别 的 ROOTCA 加 入 信 任 , 例 如 ,limax.pkix.SSLContextAllocator.addTrust(),提供了这样的方法。

3 ROOTCA 证书必须通过安全渠道分发,现实中,Windows更新,JDK更新,都会更新公共域上的 ROOTCA 证书,这一类通过网络的更新,事实上都没有办法保证避免欺骗,也许假设欺骗不会持续发生这一前提,有机会提供某些事后策略。

4 事实上 Limax 提供的工具并没有支持 DSA算法,DSA算法设计之初就是搭配 SHA1用来签名,2017以后 SHA1逐渐废弃。DSA1024/SHA256,DSA2048/SHA256,这些算法 JDK能够支持,但是Windows 并不能支持,为了兼容性,Limax 工具仅提供了测试证明兼容的算法。EC算法使用的一些特定曲线,由权威机构发布,兼容性没有问题,怀疑论者担心植入后门,RSA算法还是常用选择。

Key 分发系统为了建立跨服务器、跨项目、跨组织机构的信任关系,Limax 集成 Key分发系统,在 PKIX框架的基础上,实现安全可靠的 Key分发。获得的 Key可以用来签名,加密,确保信任系统间安全共享信息。设计原则基本原理1. 服务器维护一个定时生成的随机数masterKey的数组,使用 timestamp索引。2. 客户端生成 nonce,连同信任域名字 group,发送给服务器。3. 服务器执行 key = Hash(masterKey, nonce, group)。timestamp 和 key发还客户端。4. 客户端组合 timestamp,nonce,group形成 keyIdent。5. 客户端使用 key签名或者加密消息,连同 keyIdent发送给接收客户端。6. 接收客户端使用 keyIdent 向服务器请求对应的 key,用来验证或者解密消息。

200

Page 201: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

信任关系设计1. 通过约定证书签署方式,建立信任关系。2. 用 KDSName 约定 Key分发系统名,必须使用有效的域名。3. 服务器证书的 Subject中的 RDN CommonName 指定了 KDSName,其它 RDN不作约定。4. 同一 Key分发系统中的所有服务器证书必须由同一 CA签署。5. 客户端证书的 SubjectAltName中必须有一个 DNS条目,指定 KDSName。6. 客户端证书的 SubjectAltName中可以有多个 URI条目,指定 group7. 同一 CA签名的具有相同 KDSName,group的客户端证书,在 Key分发系统中相互信任。

例如,同一 CA为系统 A,B,C,签署客户端证书,使用相同 KDSNAme,其中 A包含group G0,G1,B包含 group G0,G1,C包含 group G0。在 G0 内,A,B,C可以互相通讯,G1 内 A,B可以互相通讯。

8. 签发客户端的 CA不一定需要与签发服务器的 CA 相同。不同 CA签署的具有相同KDSName,group的客户端证书没有信任关系(实现上生成 Key的时候客户端证书Issuer同时参与 HASH计算)。

服务器设计要点1. 同一 CA签署的具有同一 KDSName的服务器证书,唯一决定了一个 Key服务网络。2. 定期生成新的masterKey,为消息发送客户端分配 key,老的masterKey 保留一定时间,

保证消息接收客户端能够用 keyIdent还原之前的 key。3. Key服务网络设计成基于 DHT的 P2P网络,masterKey数组在服务网络中分发。P2P方式下服务网络规模不受限制,最大限度保证系统健壮性。

客户端设计要点1. nonce用当前时间生成,有机会 cache服务器响应,最大限度降低服务器负荷,减少客户端阻塞的机会。

2. nonce的生成精度可调,可以在签署证书时直接约定,group使用 URI表示,利用 URI中 fragment 部分。例如,G0#10s,约定了使用 group G0 时,以 10s 为单 位生成nonce,许可的单位为 s,m,h,分别为秒,分,小时,如果不带单位,默认为毫秒。fragment不存在或者解析失败使用系统默认参数,fragment不作为 group一部分。

3. 客户端发起请求前,预先根据自己的证书检查 group合法性,避免增加不必要的网络开销。当然,这不妨碍服务器执行检查。

201

Page 202: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

服务器运营签署证书

这样的签署的服务器证书约定了 Key分发系统 key.limax-project.org

服务器配置(keyserver.xml)顶层 KeyServer元素,8个属性location:服务器证书的 locationpassphrase:location的私钥启用密码,实际运营时,不应该填写该属性,而应该在服务器启动时输入。trustsPath:信任证书路径,location中已经包含了派生该证书的 ROOTCA,如果要信任其它ROOTCA(或者当前 ROOTCA 即将过期,应该将新的 ROOTCA放置在这里),则需要配置trustsPath,trustsPath可以是一个文件,也可以是一个目录,如果是目录,遍历一层,获取文件,文件允许任何证书格式,包括 CER,DER,PKCS7,PKCS12,KeyStore,从中获取ROOTCA,忽略所有错误。revocationCheckerOptions:服务器检测对方证书回收状态的选项,可选的值为 DISABLE, NO_FALLBACK, ONLY_END_ENTITY, PREFER_CRLS, SOFT_FAIL,大小写不敏感,后 4个参数的解释见 java.security.cert.PKIXRevocationChecker.Option,DISABLE具有最高优先级,如果配置了DISABLE,其它配置无意义。algorithm:指定了 HASH算法,HASH算法决定了服务器返回客户端的 Key长度,例如 SHA1返回 160 位 Key,SHA-256返回 256 位 Key。master:指定了 P2P网络的入口服务器列表,逗号分隔,可以是域名,可以是 IP。keyLifespan:masterKey生存周期,单位为天,超出生存周期的masterKey 被服务器删除。publishPeriod:masterKey发布周期,单位为天,一个新的周期开始时,服务器向网络中发布一个新的masterKey。没有配置该属性的服务器,不参与发布。理论上,一个服务网络内部只能有一个发布者,现实中可以配置多个保证冗余性,详见高级话题。服务器运行java –jar limax.jar keyserver [path to keyserver.xml]keyserver.xml目录下会生成一个 keyserver.dht文件,该文件定时更新,保存了 P2P网络中

202

Page 203: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

DHT邻居信息,有助于重启服务器时,快速建立邻居关系,不要删除。客户端开发签署证书

这样签署的客户端证书约定了客户端使用 Key分发系统 key.limax-project.org,具有两个分组prjA 和 prjB,其中 prjA使用 10秒的 nonce生成精度。编程接口1 limax.key.KeyDesc,Key描述类,提供 2个关键方法。

1.1 byte[] getIdent();,获取 Key标识。发送端将标识,合并到签名或者加密的消息中发送。接收端使用 Key标识向服务器请求对应的 Key。

1.2 byte[] getKey(); 获取 Key。发送端用 Key签名或者加密消息,接收端使用 Key验证或者解密。

2 KeyException,使用 KeyException.Type getType()返回相关异常。2.1 SubjectAltNameWithoutDNSName,证书签署错误,SubjectAltName中没有包含

DNS类型条目。2.2 SubjectAltNameWithoutURI,证书签署错误,SubjectAltName中没有包含 URI条

目。2.3 MalformedKeyIdent,key标识解码异常,通常是传输过程中被篡改,消息不可信。2.4 UnsupportedURI,当前客户端证书不支持指定的 group。2.5 ServerRekeyed,服务器无法使用指定 timestamp生成 Key。通常是 timestamp 对

应的masterKey 过期了,这种情况下,信息获取方应该向发送方重新请求消息。3 limax.key.KeyAllocator,Key分配器类

3.1 KeyAllocator(SSLContextAllocator sslContextAllocator),使用 sslContextAllocator 构造 一 个 Key 分 配 器 , cache 容 量 1024 。 可 能 抛 出 异 常 类 型SubjectAltNameWithoutDNSName,SubjectAltNameWithoutURI。

3.2 KeyAllocator(SSLContextAllocator sslContextAllocator, int cacheCapacity) , 使 用sslContextAllocator构造一个 Key分配器,指定 cache容量。可能抛出异常类型

203

Page 204: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

SubjectAltNameWithoutDNSName,SubjectAltNameWithoutURI。3.3 KeyDesc createKeyDesc(java.net.URI uri),信息发送方,请求一个 KeyDesc,使用

uri 指定的 group, uri 的 fragment 部分被 忽 略。可能抛出异常类型UnsupportedURI。如果抛出 KeyException之外的异常,通常是网络错误,可以重试。

3.4 KeyDesc createKeyDesc(byte[] ident),信息接收方,使用收到的 Key标识,请求KeyDesc 。 可 能 抛 出 异 常 类 型MalformedKeyIdent,UnsupportedURI,ServerRekeyed。如果抛出 KeyException之外的异常,通常是网络错误,可以重试。

3.5 String getDNSName(),获取客户端证书中签署的 DNSName。3.6 Map<URI,Long> getURIs(),获取客户端证书中签署的 group 信息,Map的 Value

为 nonce 精度,单位为毫秒。3.7 void setHost(java.lang.String httpsHost),指定访问的 Key服务网络中的部分 Key服

务器的域名或者 IP地址,如果没有使用该方法设置服务器地址,则默认使用getDNSName()返回的域名。如果域名解析出多个 IP地址,这些 IP地址被一一访问,直到没有发生网络错误,或者全部发生错误,抛出网络异常。

3.8 String getHost(),返回当前正在使用的 Key服务器域名或者 IP。4 系统属性

4.1 limax.key.KeyAllocator.DEFAULT_PRECISION , nonce 精度,单 位毫秒,默 认3600000。

4.2 limax.key.KeyAllocator.TIMEOUT,网络访问超时,单位毫秒,默认 3000。应用实现要点1 如果服务提供者提供了新的 ROOTCA,通过 SSLContextAllocator.addTrust 加入。2 根 据 服 务 提 供 者 的 建 议 决 定 是 否 需 要

SSLContextAllocator.setRecovationCheckerOptions。3 根据服务提供者的提供的配置决定 KeyAllocator.setHost。4 KeyAllocator.createKeyDesc,正确进行异常分类,KeyException类型属于不可恢复异常,

可能需要检查证书配置,或者请求消息发送方重发。其它 Exception属于超时之类的网络异常(这样的异常应该非常少,详见高级话题),应该优先考虑重试操作,唯一例外是证书过期导致连接被 reset,需要确认日志中的“Certificate renew fail”警告。

5 应该为消息设计自己的 expire 机制,masterKey寿命仅仅应该理解为 expire的最大值。高级话题时钟同步的考虑既然服务本身依赖了时钟,不论服务器,客户端,都应该使用 ntp同步时钟。这里讨论时钟误差的容忍限度。1 服务器时钟不同步,理论上,masterKey寿命变为配置寿命减去机器间时钟差异。也就

是机器间masterKey寿命区间的交集。非常长的配置寿命有助于容忍时钟的不同步。2 客 户 端 时 钟 不 同 步 , nonce 由 系 统 时 间 除 以 配 置 精 度 取 整 获 得 。

204

Page 205: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

(timestamp,nonce,group)组合而成的 keyIdent 作为客户端 cache的 key,不同步的结果即是 cache中(timestamp,nonce)的项变多,换言之,客户端时钟不同步将导致客户端cache使用量变大,网络请求变多。

masterKey的分发masterKey在 P2P网络中分发。因为应用环境的不同,所以采用修改过的 Kademlia算法。因为没有 Kademlia 参数的现实评估结果,所以这里涉及到许多可以配置的系统参数。1 使用 HTTPS代替 UDP,保证服务器间的严格认证。2 DHT查找过程中的协议交互,附带交换服务器间 masterKey。查找发起服务器发送自己

节点信息的同时发送已知的 timestamp 集合。接收服务器返回邻居信息的同时,通过diff 操作,附带返回对方没有的 masterKey 集合,以及自己缺少的 timestamp。查找发起服务器解析返回的邻居信息,merge返回的masterKey 集合,向对方 upload缺少的mastreKey 集合。

3 Node地址长度通过 limax.p2p.DHTAddress.HASH决定,默认配置 SHA-2564 单个 k-Bucket 尺寸通过 limax.p2p.Neighbors.BUCKET_SIZE决定,参考 BT,默认配置为

8。5 k-Bucket更新方法不同于 Kademlia,以距离优先为标准,因为服务器不需要过多考虑

偶尔在线的问题。新加入 k-Bucket的 node,设置 PING定时器,如果 node已经存在,cancel之前的定时器。k-Bucket溢出,删除最后一个条目,cancel 对应的定时器。PING采 用 连 接 HTTPS 端 口 的 方 式 实 现 , PING 定 时 器 周 期 由limax.p2p.Neighbors.ENTRY_AGE_MAX 决 定 , 默 认 20 分 钟 。 PING 超 时 由limax.key.KeyServer.NETWORK_TIMEOUT决定,默认 3秒。

6 Refresh 做 2 次 查 找 , 查 找 自 己 , 查 找 一 个 随 机 地 址 。 Refresh 周 期 由limax.key.KeyServer.NEIGHBORS_REFRESH_PERIOD决定,默认配置 20分钟。

7 查找动作使用的本地初始集合大小由 limax.key.P2pHandler.BASE_LIMIT决定,默认 8。被查找的服务器返回集合大小由 limax.key.P2pHandler.REPORT_SIZE决定,默认 16。查找并行度由 limax.key.P2pHandler.CONCURRENCY_LEVEL决定,默认 64。期望的查找结果集最大尺寸由 limax.key.P2pHandler.ANTICIPANTION决定,默认 8。

8 如果查找结果数量无法满足预期,则从配置文件中 master属性配置的服务器开始再执行一轮查找。这些配置指定服务器类似于 BT 环境中的超级种子,随着邻居表逐渐增大,可以预期这些超级种子被访问的频率逐渐降低。

9 发布服务器向邻居表中所有地址,所有master配置属性指定的地址分发masterKey。10 既然 refresh频率为 20分钟,每次 refresh在本地选择 8个最近 node,也就是不到 4层,

算作 3层(Kademlia使用基于地址前缀的二叉树划分地址空间),地址算法使用 SHA-256,总共 256层,256 / 3 * 20 / 60 / 24 = 1.19天,作最保守的估计 masterKey同步到最远端需要 2天,配置文件中 publishPeriod属性配置为 3天。正因为如此,服务器优先使用第二新的masterKey 提供服务,严格来说,整个网络的启动至少需要提供 2天的初始静默期。实际运营,通过指定合适的master配置,这个问题就不再重要了。

11 正 确 处 理 NAT 。 实 现 上 DHTAddress 关 联 InetSocketAddress 表 示 为NetworkID,InetSocketAddress 作为查找时的 secondaryKey。NAT 内部服务器如果没有分配外部 IP,应该适当提高 Refresh频率,配置较多的master。

205

Page 206: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

多个masterKey分发服务器1 理论上分发服务器只能有一个。2 实现上,分发服务器启动时,向邻居服务器同步所有masterKey,记录最新 masterKey

的 timestamp,然后启动分发定时器。定时器到期时检查当前最新 timestamp是否与之前的记录相 等,如果相 等,执行分发;不相 等,不执行,最后依据当 前最新timestamp 启动下一轮定时。

3 从实现来看,启用多分发服务器需要满足两个条件。其一,分发服务器间网络通讯良好,分发服务器集共享 master配置,包括所有的分发服务器地址,这样就保证了最新生成的masterKey能立即同步。其二,分发服务器的启动设定一个时间差,保证其中一台服务器的分发动作,发送到其它服务器,设置它们的最新 timestamp之前,它们的分发定时器没有到期。

4 实际上,timestamp的精度为毫秒,就算两台服务器同时分发,生成的 timestamp碰撞的概率也非常小,顶多让所有服务器多记录一个masterKey。

不使用 P2P的资源存取模型1 理论上,可以通过 timestamp在 P2P网络中搜索对应的masterKey,而不必等待一个较

长的发布周期。事实上这不可行。2 搜索是一个高开销行为,恶意客户端可以伪造 timestamp 向服务器发起请求,如果提

供搜索就给这些恶意客户端提供了攻击 P2P网络的机会。3 如果要避免攻击,就只能给 timestamp签名,服务器在发起搜索操作之前作一次过滤,

这样做需要一个签名 Key,整个服务网络本身就是为了提供 Key 而存在,这个问题就变成了一个循环。

孤立服务器,孤立网络1 脱离 P2P网络的服务器称为孤立服务器,与分发服务器网络断开的网络称为孤立网络。2 孤立服务器,或者孤立网络中的服务器可以为客户端生成 key,但有可能不能还原客

户端访问非孤立网络中服务器后生成的 keyIdent,原因很简单,新的 timestamp关联的masterKey没有收到。

3 孤立服务器可以通过日志确认,“Neighbors save NNN entries”记录条目中 NNN = 0,应该怀疑服务器已经孤立。

4 现实环境中,证书不能及时 renew,过期之后可能导致服务器孤立。事实上就算签署30天短期服务器证书,80%寿命开始 renew,也就是说有 6天的时间可以 renew,每小时重试一次,一共可以重试 144 次,过期的情况几乎不可能发生,除非 CA 6天不在线。

5 服务器证书被 CA 回收,这种情况下,应该关闭服务器。6 孤立网络可以通过日志确认,“MasterKeyContainer CurrentDigester”记录了

masterKey更新事件,两条这样的日志之间的时间差大致等于发布周期,如果该超出 2倍发布周期的该记录还没有出现,意味着该服务器及其邻居都处于孤立网络内。

7 现实环境中,除非大范围网络故障,孤立网络不会存在,配置文件中使用比较大的publishPeriod可以增加网络故障耐受容限。

206

Page 207: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

客户端的高可用性1 较大的 nonce设置可以有效利用 cache,大大减少网络访问。2 尽管客户端不加入服务器的 P2P网络,但是也提供同样级别的高可用实现,有效减少

网络失败。3 设定网络访问优先级。证书提供的 DNSName地址具有低优先级;KeyAllocator.setHost

设置的地址具有中优先级;每次向服务器发起 Key请求,服务器会连带返回一个随机的 服 务 器 地 址 列 表 , 该 列 表 具 有 高 优 先 级 , 客 户 端 测 量 这 些 地 址 的RTT(limax.key.KeyAllocator.TIMEOUT决定的超时限度内,超过该限度的服务器直接放弃)。后续 Key请求按 RTT排序的高优先级列表,中优先级列表,低优先级列表,逐一访问,直到成功,失败的 IP 从高优先级列表中删除。

4 证书提供的 DNSName与 KeyAllocator.setHost 提供的域名,映射的 IP地址可能改变,客户端定期解析这些地址,limax.key.ServerEvaluate.DOMAIN_UPDATE设置了解析频率,默认 5分钟。

5 服务器返回的地址列表,每次都可能更新,实现上选择 RTT最小的 8个 IP 提供使用,可以通过系统属性 limax.key.ServerEvaluate.DYNAMIC_SERVERS 修改。

6 服务器提供给客户端的最大随机地址列表个数由 limax.key.P2pHandler.ANTICIPANTION决定,默认 8个。

7 limax.key.KeyAllocator.ISOLATED_SERVER_THRESHOLD 默认为 0,该属性提供了孤立服务器过滤能力,如果服务器返回的随机列表个数小于该值,则认为该服务器不可靠,继续访问下一 IP。小心配置该值,绝不能大于 limax.key.P2pHandler.ANTICIPANTION。

安全考虑1 服务器使用 HTTPS交互,接收请求一方验证对方的证书的 Subject中 CN是否与自己证

书相同,同时验证 Issuer 名字是否相同。(这里不能比较 CA 证书,因为 CA自身可能renew,CA的 renew规则要求 Subject不能改变,见上一章《PKIX支持》)

2 每一台接入网络的服务器都必须保证私钥安全,任何一台服务器私钥被盗都可能导致masterKey 被盗,危害整个网络,可以考虑采用硬件方式,足够的冗余保证了硬件失效可接受。

3 配置文件中 revocationCheckerOptions属性不要选择 DISABLE,CA正常运转的情况下,至少考虑使用 SOFT_FAIL。

运营维护1 Key分发网络应该看作是与 CA同级别的基础设施,直接由 CA维护是最合理的选择。2 配置文件中,master属性可以配置成层次结构或者网状结构(环不是问题),这样做

的实质是把 P2P网络从动态网络,局部调整为静态,可以进一步提高健壮性。(见《多个masterKey分发服务器》一节)

3 关注服务器孤立,网络孤立的情况。Key分发系统本质是 key的离线协商1 Key协商是系统间安全通讯的基础。2 常见安全协议例如 SSL,必须在通讯双方同时在线的情况下完成 key协商。

207

Page 208: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

3 在线协商不可能覆盖所有应用场景。4 Limax 提供的 Key分发系统通过提供一个能够保证持续在线的第三方机构来实现客户之

间的离线协商。5 从建立信任的角度看,可信第三方是保障安全的前提,这一点和 CA完全一致。

外部数据来自其它系统的数据就是相对于当前系统的外部数据,简称外部数据。通常情况下,外部数据需要完整性或者机密性的保证。信任系统间的数据交换就是外部数据的交换。Key分发系统建立了系统间信任关系,分配的 key,可以用来签名或者加密应用数据。这里提供一个外部数据表示模块,完成这样的任务。编程接口limax.key.ed.Transformer

数据转换对象,编码解码外部数据。构造函数: Transformer(KeyAllocator keyAllocator)

Key分配器的基础上创建 Transformer 对象。 Transformer(Map<URI, byte[]> keyAllocator)

将 Map 对象 keyAllocator看作是一个 Key分配器,创建 Transformer 对象。提供这样的方法可以简化调试,也可以满足极其简单的环境下的应用需求。这里的 Map 对象不作拷贝,直接引用,所以创建 Transformer 对象之后,也可以修改 Map,这种情况下建议使用 ConcurrentHashMap,避免并发异常。特别的,这里的 URI 允许设置 fragment部分配置外部数据超时,例如,G0#10s,约定了使用 group G0 编码的外部数据 10s超时,许可的单位为 s,m,h,分别为秒,分,小时,如果不带单位,默认为毫秒。 fragment不存在或者解析失败则不作超时检测fragment 不 作 为 group 一 部 分 。 解 码 外 部 数 据 时 检 测 到 超 时 抛 出limax.key.KeyException,类型为 ServerRekeyed。

成员函数: ByteArrayCodecFunction getEncoder(URI uri, KeyProtector keyProtector, Compressor

compressor)获得一个编码函数对象,使用这个对象执行编码操作。编码原始数据 data,首先使用compressor 指定的算法执行压缩,然后使用 uri 指定的 group 分配 key,最后使用keyProtector 指定的算法 加 密或者签名。执行过 程中的任何异常都转换为CodecException抛出。

ByteArrayCodecFunction getDecoder()获得一个解码函数对象,使用这个对象执行解码操作。uri,keyProtector,compressor相关信息在编码过程中已经打包在编码结果中了,所以获取解码器不需要任何参数。

208

Page 209: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

执行过程中的任何异常都转换为 CodecException抛出。

limax.key.ed.Compressor

枚举类型,指定压缩方法。如果压缩之后的数据尺寸没有减小,自动输出为不压缩的格式。 NONE不压缩

RFC2118使用 limax.codec.RFC2118Encode,limax.codec.RFC2118Decode,执行压缩解压。

ZIP使用 java.util.zip.Deflater,limax.util.zip.Inflater,执行压缩解压。

limax.key.ed.KeyProtector

枚举类型,指定签名或者加密方法。 HMACSHA224 HMACSHA256 HMACSHA384 HMACSHA512 TripleDESCBC AESCBC128解码时,如果检测到签名失败,抛出 CodecException,其中包裹了 SignatureException。应用考虑1 多数系统共享数据时,需要标识数据来源。按照安全系统的设计惯例可以将证书链打

包传输,提供来源标识。基于下述原因,这里并不提供证书链打包支持。1.1 一份数据一份证书链,空间开销过大。1.2 即便提供证书链也无法保证来源真实性,假如信任系统内,存在一个恶意系统,

这个恶意系统至少可以将任何的数据来源替换为自己。(如果简单附带证书链,可以替换为任意证书链,如果设计上能够提供一个随机串的 Challenge 机制,使用私钥签名随机串,则只能替换为自己)

1.3 从上一条叙述可以看出,实际上证书链提供的标识,在仅有 2个参与者的信任系统中才是不可伪造的。

2 信任系统应该根据具体需求,仔细定义交换的数据格式,提供诸如来源标识,时戳,有效期之类的信息。

3 为了简单,KeyProtector中,签名与加密实现为二选一。现实应用,例如 IPSEC,尽管AH/ESP支持同时使用,绝大多数时候没人这样使用。

209

Page 210: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

ProviderLogin

Limax框架中,Provider为应用提供服务。某些应用场景,存在 Endpoint切换 Provider的需求,从源 Provider携带特定的 Session数据,传送到目的 Provider,本质就是 Session迁移。使用 ProviderLogin 机制,源 Session数据在目的 Provider创建 Session之前安全传送到目的Provider,可以大大简化 Session迁移类应用的开发。基本原理流程考虑通常的登录流程为1. Endpoint连接 Switcher,发起登录请求,登录请求包含所有需要访问的 Provider。2. Switcher 向 Auany 转发登录请求。3. Switcher获得 Auany的授权之后,向请求的所有 Provider发起上线通告。4. 收到上线通告的 Provider,成功初始化 Session之后,向 Switcher发出 OK应答。5. Switcher收集 Provider发来的应答,如果全部 OK,向 Endpoint发送上线通告。ProviderLogin 机制扩展了登录流程的第 3步。Switcher获得 Auany的授权之后,逐一判断请求的 Provider 是否需要 LoginData,如果需要,向 Endpoint 查询 LoginData,接下来向Provider发起上线通告,附带 LoginData。安全考虑存在两种形式的 LoginData,安全的和不安全的。1. 来自同一信任域的 Provider发送的隧道数据可以用来创建安全 LoginData,包含了 label与 data,详见《Provider间数据交换》。

2. 应用使用其它方式指定的数据只能创建不安全的 LoginData。服务器开发Provider 实 现 limax.provider.ProviderListener 时 , 并 列 实 现limax.provider.LoginDataSupport接口。public interface LoginDataSupport {}LoginData 接口仅为一标签接口,不需要实现任何方法。实现该接口的 Provider 连接Switcher时,将报告 Switcher,该 Provider需要 LoginData。ProviderListener.onTransportAdded方法的实现代码中,可从 Transport中获取 LoginData。@Overridepublic void onTransportAdded(Transport transport) throws Exception {

210

Page 211: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

ProviderTransport pt = (ProviderTransport) transport;LoginData loginData = pt.getLoginData();String message;if (loginData != null) {

if (loginData.isSafe())message = "SAFE LoginData " + loginData.getLabel() + " " +

Helper.toHexString(loginData.getData().getBytes());else

message = "UNSAFE LoginData " + Helper.toHexString(loginData.getData().getBytes());

} elsemessage = "NO LoginData";

System.out.println(message);}这里可以看到,如果 Provider需要 LoginData,客户端没有提供,那么返回的 loginData为null,是否作为错误,应用自己决定,抛出异常即可关闭客户端的网络连接。需要注意,onTransportAdded之后,Transport上存储的 LoginData即被清除,这是考虑到 LoginData数据量可能较大,如果应用忘记清除,浪费内存空间。客户端开发HTML5之外的所有客户端使用 limax.endpoint.ProviderLoginDataManager 组织 LoginData,提供 3个 add方法。void add(int pvid, Octets unsafedata);void add(int pvid, int label, Octets data);void add(int pvid, int label, String data);其中,不使用 label的方法,表明提供的是不安全 LoginData,带 String 参数的 add方法用于构造 LoginData的隧道数据来自脚本系统的情况。如果期望使用字符串作为不安全 LoginData,可以编码成 UTF8 格式的 Octets再 add,同时服务器端使用 UTF8解码,有助于获得最好的兼容性。LoginConfig中选用带 ProviderLoginDataManager 参数的静态方法,创建登录信息。HTML5客户端按照如下模式定义 login 参数对象var login = {

scheme : 'ws',host : '127.0.0.1:10001',username : 'testabc',token : '123456',platflag : 'test',

211

Page 212: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

pvids : [100],logindatas : {

100 : {data : 'unsafedata'

},101 : {

label : 0,data : <base64tunneldatastring>

}}

}var connector = WebSocketConnector(limax, login);

LimaxKey

LimaxKey简称 Lmk,支持证书方式的客户端登录。客户端登录过程使用约定方式签署的私钥证书包。证书内容中包含了签署者信息,可以非常容易地追溯用户来源,事实上Lmk 提供了去中心化的第三方登录机制。设计要点1. 为了简化客户端实现,提供 Lmk私钥证书包格式 LmkBundle,LmkBundle 当前只支持

RSA。提供 LmkBundle 格式和 PKCS12 等常用格式的互相转换工具。2. 提供 Lmk签署服务的实现。支持大型第三方机构,允许 ROOTCA签署特定第三方 CA 证书 LmkCA,LmkCA 只能为自己的用户签署 LmkBundle。支持小微第三方机构,允许通用 CA为任何第三方机构签署属于自己的用户的 LmkBundle。

3. Switcher,Auany帮助客户端更新证书。客户端登录之后,如果 Switcher,Auany检测到客户端使用 Lmk方式登录,并且达到证书更新时限,Switcher,Auany后台请求相应的 Lmk签署服务获得新的 LmkBundle 存储在 Auany 上,客户端下一次登录时,将LmkBundle 推送到客户端保存。(当然,客户端也可以定期到自己的签署机构获取新的 LmkBundle)

4. 特定情况下,允许通过配置,忽略证书过期检测。5. LmkBundle可以用来绑定登录凭证,这样的凭证使用证书更新时限决定有效期。凭证

过 期,将 导 致客户端使用 Lmk 方式重新 绑定凭证,这时 Auany 帮助客户端更新LmkBundle,下一次使用凭证登录的时候,更新的 LmkBundle 被推送到客户端保存。

6. 纯 HTML5方式暂不支持 Lmk 登录,原因在于各种浏览器,HTTPS客户端证书密码提示的方式不能兼容。保存自动推送私钥证书包也要受到不同操作系统的安装限制。

212

Page 213: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

客户端开发(以 java客户端为例)相关类与接口1. limax.endpoint.LmkBundle

LmkBundle LmkBundle.createInstance(Octets lmkdata, Octets passphrase);创建 LmkBundle,lmkdata为保存在客户端的第三方机构为用户签发的私钥证书包文件数据,passphrase为文件密码。Octets LmkBundle.save(Octets passphrase);允许客户端自己更改 LmkBundle 密码,返回新的文件数据。

2. limax.endpoint.LmkUpdater (C#中为委托,C++中为函数对象)void LmkUpdater.update(Octets lmkdata, Runnable done) throws Exception;实现该接口,保存 Switcher 推送回来的更新的 LmkBundle数据。lmkdata 存储完成后执行 done。

3. limax.endpoint.LoginConfig其中,4个版本的 LoginConfig.lmkLogin方法使用 LmkBundle创建登录对象。LoginConfig.setLmkUpdater(LmkUpdater lmkUpdater);安装 LmkUpdater 对象。

客户端实现样式代码LoginConfig loginConfig = LoginConfig.lmkLogin(LmkBundle.createInstance(

Octets.wrap(Files.readAllBytes(Paths.get("/work/xxx.lmk"))),Octets.wrap("123456".getBytes())));

loginConfig.setLmkUpdater((lmkdata, done) -> { Files.write(Paths.get("/work/xxx.lmk"), lmkdata.getBytes()); done.run();});

注意事项1. 如果使用 Lmk方式创建的凭证,那么通过 LoginConfig.credentialLogin创建登录对象之后,同样需要执行 loginConfig.setLmkUpdater安装 LmkUpdater 对象。

2. 从安全角度看,LmkUpdater应该设计为 LmkUpdater.update(LmkBundle lmkBundle),用户存储 LmkBundle时自己设置一个密码。这种方式,需要使用 LmkBundle.save重新设置密码,加密 LmkBundle后再执行存储,用户通常难以接受。实际实现时,passphrase在登录过程中被上传到服务器,如果需要更新 LmkBundle,即用该 passphrase 加密,推送回客户端。所以 LmkBundle.save方法应该少用,避免冲突。

3. 服务器实现上保证了 LmkBundle更新事务的完整性,整体完整性的保证必须有客户端的参与。具体实现上,如果登录过 程中检测到 Auany 已经存 储了更新的LmkBundle , Switcher 返 回 给 Endpoint 的 上 线 通 告 中 就 提 供 了 这 个

213

Page 214: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

LmkBundle,Endpoint收到上线通告立即触发 LmkUpdater,lmkdata 存储完成后执行done,通告 Auany清除存储的 LmkBundle。

4. 参见《PKIX支持》,Location一节,其中 scheme 增加了 LMK类型,使用上与 PKCS12类似,可以通过 java -jar limax.jar pkix copy <locationSRC> <locationDST>执行格式转换,非 RSA的情况下执行失败,另外,为了节省空间,Lmk签署服务发行的 LmkBundle中不包含根证书,向其它格式转换也会失败。

签署 LmkBundle

设计原则1. Limax框架提供了 Lmk签署服务,支持签署与回收 LmkBundle。2. 有能力部署 Lmk签署服务的大型三方机构,可以使用自己的 LmkCA 证书运行签署服务,为自己的用户发行 LmkBundle。

3. 没有能力部署 Lmk签署服务的小微三方机构,可以请求自己的客户端证书,访问通用CA 证书上运行的签署服务,为自己的用户发行 LmkBundle。

创建 LmkCA

java -jar limax.jar pkix initlmkca <locationROOT> (<locationCA>|<keygen/.pub>) <subject> <domain> <yyyyMMdd> <yyyyMMdd> <OcspDomainOfRoot>使用 ROOTCA 证书,签署一个三方机构的 LmkCA,与签署通用 CA 证书(参见《PKIX支持》)的区别在于这里多了一个 domain 参数,该参数与 Auany配置中平台名等价,允许和平台名重名,这意味着这个第三方机构,既支持通常的三方登录方式,也可以支持 Lmk方式。例如:java -jar limax.jar pkix initlmkca "file:ca@/work/pkix/root" "file:lmkca@/work/pkix/ca/#rsa/2048" "dc=lmkca,dc=limax-project,dc=org" "lmkca.limax-project.org" "20170101" "20300101" "root.limax-project.org"

签署 Lmk服务请求证书

注意,这里的 dNSName,即是 Auany中对应的平台名。214

Page 215: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

运行 Lmk签署服务java -jar limax.jar pkix lmk [path to lmkserver.xml]参考 lmkserver.xml,进行配置说明LmkServer顶层元素7个属性:certificateAlgorithm:LmkServer自动签署 HTTPS服务器证书时使用的公钥算法。certificateLifetime:LmkServer自动签署的 HTTPS服务器证书以天为单位的寿命。constraintNameLength:用户名长度限制。domain:LmkServer的 HTTPS服务器域名。port:HTTPS服务器运行端口,默认 443。revocationCheckerOptions,trustsPath 参见《Key分发系统》服务器运营一节。LmkBundle元素2个属性:certificateLifetime:签署的 LmkBundle以天为单位的寿命。rsaBits:LmkBundle的 RSA私钥长度。其它元素的配置与 caserver.xml 相同,参见《PKIX支持》其中 CAService 的 location 可以指 向 LmkCA,也可以指 向通用 CA,具体的区 别在请求LmkBundle时体现。请求 LmkBundle

使用 HTTP/GET方法请求需要客户端认证的 HTTPS服务,使用 JSON 组织查询参数,JSON 编码之后再 URL 编码即可。JSON请求样式为:{ ”u”:”username”, “p”:”passphrase” }其中:u为用户名,必须为非空串,长度小于等于 Lmk签署服务配置的 constraintNameLength,用户名转换为小写签署 LmkBundle。p为 LmkBundle的加密密码。Limax框架提供了 java实现:limax.pkix. LmkRequest.createReqeustor(SSLContextAllocator sslContextAllocator, String host);初始化一个 requestor之后,即可 requestor.fetch(“user”); 为后续用户请求 LmkBundle了。更新 LmkBundle

通常情况下使用 Switcher , Auany 帮助客户端更新 LmkBundle 即可。 Limax框架提供了 java实现,直接向 Lmk签署服务请求更新:limax.pkix.LmkBundleUtils.renew(Path path, char[] passphrase);LmkBundle更新实现流程与请求 LmkBundle稍有不同,其中:JSON请求样式为:{“p”:”passphrase”},只需要提供密码。使用 LmkBundle自己的证书执行 HTTPS客户端认证,如果证书有效期未过半,更新失败。

215

Page 216: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

注意事项1. Switcher,Auany 也会访问 Lmk签署服务更新证书,服务端口必须对 Switcher,Auany打开。

2. Lmk签署服务器,必须运行 NTP服务,保证时钟同步,避免签署无效的 LmkBundle。Limax服务运营配置Switcher 免配置,所有配置信息由 Auany 推送。Auany配置中增加一个 lmk元素。5个属性:location:CA为当前 limax运营环境签署的证书的 location。passphrase:location的私钥启用密码,实际运营时,不应该填写该属性,而应该在服务器启动时输入。revocationCheckerOptions,trustsPath 参见《Key分发系统》服务器运营一节。validateDate:配置了当前运营环境是否需要检测 LmkBundle 过期状态。虚拟机参数limax.auany.LmkManager.defaultLifetime,如果当前运营环境关闭了 LmkBundle 过期状态检测,通过 LmkBundle创建的凭证使用该参数决定过期时间,默认为 8640000000,即 100天。只要使用了 LmkBundle创建凭证,必须设置过期时间,无论当前环境是否检测过期。因为关闭了的过期状态也可能被重新打开。limax.auany.LmkManager.renewConcurrency,该参数决定了单个的 Auany,Switcher服务器发起 LmkBundle更新请求使用的线程数量,默认 16。注意事项1. 如果当前运营环境配置为不检测 LmkBundle 过期状态,那么也不会帮助客户端执行证书更新,同一 LmkBundle用于多个 Limax运营环境时必须注意到这个问题。

2. 启用了过期检测的环境,Switcher,Auany必须运行 NTP服务,保证时钟同步。

动态 XBean

支持动态 XBean的 Zdb数据库,允许增删,修改 XBean 字段的定义,无须在原先的Zdb数据库上执行数据格式转换,减少维护代价。详见《运行管理》数据转换一节。基本原理1. 动态 XBean 由静态字段和动态字段组成,静态字段的调整需要执行数据格式转换,动态字段的调整则不需要。

216

Page 217: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

2. 不论是静态字段,还是动态字段,在编程访问的方式上没有任何区别。3. 动态 XBean 将所有动态字段组织为一个Map,这个Map定义为动态数据,在编码上进行了扩展。编码规则为,首先按定义顺序编码所有静态字段,然后编码动态数据数量,随后编码动态数据条目。其中,动态数据条目的 key为的 serial,value为动态字段单独编码后获得的字节数组。

4. serial是实现动态 XBean的关键,serial在 XBean上进行分配,任何一次修改调整必须递增 serial。serial 反映了 XBean的修改历史。

5. XBean中读出了当前运行代码不知道的 serial,意味着该 serial 对应的字段需要删除,这样的 serial 对应的数据在解码过程中即被忽略。

6. 新增动态字段,必然分配了所有已存储 XBean都不知道的最新的 serial,解码过程获得的 XBean动态数据不可能更新这一字段的值,所以用户看到的就是构造 XBean时初始化的默认值(除非字段描述中定义了 script,详见后面的讨论)。

7. 同一动态字段,如果需要修改定义,那么最新的定义具有最新的 serial,之前的定义必须保留,这些定义反映了当前数据中的历史。设计上,允许提供脚本描述,定义历史serial 向新 serial的转换,解码过程中如果解码出历史 serial,则调用转换过程,将字段转换为最新的版本,更新 XBean 对应字段,提供给用户访问,编码存储 XBean时最新版本被写入数据库。

动态 XBean的描述描述样式<?xml version="1.0" encoding="utf-8"?><zdb dynamic="true">

<xbean name="MyXbean" nextserial="1"><variable name="var0" type="int" /><variable name="var1" dynamic="true">

<variable serial="0" type="int"/> </variable>

</xbean><table name="mytable" key="long" value="MyXbean" autoIncrement="true" />

</zdb>

1. zdb元素 dynamic属性决定了该 zdb中允许动态 XBean。一旦这样定义 zdb,该 zdb中所有的 XBean都是动态的,不论是否定义了动态字段。

2. xbean元素的 nextserial属性,决定了下一次修订动态字段定义时应该使用的 serial,这样的 serial 被使用之后,需要将 nextserial 加一。该属性用于代码生成时的检查,避免错误分配 serial,导致运行时出现错误。nextserial缺省为 0,没有定义动态字段 XBean允许缺省。

3. 字段 var0为静态字段。4. 字段 var1为动态字段,当前的 serial为 0,类型为 int。

217

Page 218: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

增加动态字段例如,增加一个 string类型的字段 var2,添加一个描述片段即可

<variable name="var2" dynamic="true"> <variable serial="1" type="string"/>

</variable>这里使用了 xbean属性 nextserial 指定的 serial 1,所以应该将 nextserial 修改为 2。删除动态字段例如,删除 var2,删除上述描述片段即可,xbean属性的 nextserial不修改。修改动态字段例如,修改动态字段 var1,新类型为 double,值为之前的值乘以 3.14。

<variable name="var1" dynamic="true"> <variable serial="0" type="int"/> <variable serial="2" type="double" script="$0 * 3.14"/>

</variable>这里定义了 var1的更新类型,serial为 2,记得将 xbean的 nextserial更新为 3。动态字段定义中,具有最大 serial的元素描述的信息为该字段的当前描述信息,其它元素提供了历史描述信息。script属性可以填写一个表达式,这里的$0表示需要依赖 serial="0"的历史值,动态字段的解码基本过程为:1. 如果最新版本 serial 对应的数据已经存在,直接解码即可。2. 如果不存在,根据 script描述的依赖关系递归解析依赖值,计算结果作为当前动态字

段的值。3. 递归解析过程中任何一个依赖值不存在,则使用字段构造时初始化的默认值。从当前的例子来看,如果读取的 XBean动态数据中,存在 serial="2"的值,直接解码,初始化 var1,否则检查 serial="0"的值,如果存在,则按 int类型解码该值,乘以 3.14,获得的结果初始化 var1,如果 serial="0"的值不存在,使用 var1构造时的默认值,即 0.0。历史描述信息没有被 XBean定义范围内的任何 script 引用,意味着历史值可能被丢弃,生成代码时将产生警告。在这里,如果 serial="2"对应的 script没有定义,则会警告 serial=”0”没有使用。script的设计原则为:1. 为了灵活性,允许使用$引用 XBean中的任何字段,不限于当前动态字段的历史描述。2. $number 的形式引用了历史动态字段,其中 number必须小于自己的 serial,并且存在

于当前 XBean定义中,否则生成过程中报错。3. $word的形式引用了静态字段,word必须为有效的静态字段名,否则生成过程中报错。4. 生成代码只检查$引用的合法性,并不执行语法检查,所以生成之后应该检查生成代码是否有错,如果有错,修订 script描述,重新生成。

218

Page 219: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

5. script 只允许填写一个合法表达式,如果需要执行复杂操作,可以首先定义一个生成的XBean可见的静态函数,执行转换。例如 script=”mypkg.MyApp.transform($0)”

6. 特别注意,一个动态字段最小 serial 元素中定义的 script表达式计算结果并不能理解为该字段初始值。该值仅仅是解码 XBean时使用的默认值,并不是构造 XBean的默认值这一点需要特别注意,通常最小 serial 元素中最好不要定义 script属性,这样就能保证解码默认值与构造默认值一致。

7. 修改动态字段,设计 script的时候,正确区分解码默认值与构造默认值对于 insert 新记录时,如何正确填写相应字段的值非常重要。

数据格式转换使用动态 XBean可以减少修改 Zdb描述带来的数据格式转换代价。至少有两种情况需

要转换,其一,非动态 XBean系统向动态 XBean系统的转换;其二,Zdb描述修订太多次,开发人员感觉难以维护。数据格式转换的操作与《运行管理》数据转换一节,完全相同,应该首先熟悉操作过

程。静态转换到动态<?xml version="1.0" encoding="utf-8"?><zdb>

<xbean name="MyXbean"><variable name="var0" type="int" />

</xbean><table name="mytable" key="long" value="MyXbean" autoIncrement="true" />

</zdb>假设原本的 Zdb这样的描述,已经运行一段时间了。现在需要修订为前面例子定义的形式,加入动态字段 var1。修订 Zdb描述之后生成代码,运行程序。则会报告错误” convert needed to cast to dynamic”

java -cp ../limax/bin/limax.jar;bin limax.zdb.tool.DBTool -e "convert zdb zdbcov"当前开发环境下,参考上面的命令执行,生成转换代码。编译代码之后,再执行一次上述命令,(注意创建 zdbcov目录),数据被转换到 zdbcov中。备份 zdb目录,zdbcov重命名为 zdb,程序即可正常执行。特别注意,动态不可重新转换回静态。维护性转换Zdb描述中动态字段修订太多次,开发人员感觉难以维护,可以考虑生成代码执行维护性转换,维护性转换的实质就是将所有 XBean读取存储一次,更新到最新版本,清除所有历史数据。继而可以清理 Zdb描述,删除所有动态字段的历史描述,只保留 serial最大的元素。java -cp ../limax/bin/limax.jar;bin limax.zdb.tool.DBTool -e "convert zdb zdbcov"执行维护性转换同样需要运行这样的命令,结果是为所有以 XBean为 value的 table生成转

219

Page 220: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

换代码,编译代码后重复运行上述命令执行转换。这里需要注意,即便 XBean没有描述任何动态字段,也会生成转换代码,因为转换工具不能确定动态字段从来都不存在,还是仅仅在当前的描述中被删除了。所以,开发人员可以根据实际情况清理相应的生成代码,基本原则有两条,其一,确实从来没有定义动态字段其二,动态字段一直只有一条定义,从来没有修改过,能够确信数据库中没有历史信息。最简单的策略是根本不清理,转换时间长一些而已。如果静态字段的定义被修改了,运行程序自然会报告需要转换,这种情况参考《运行管理》数据转换一节即可。动态还是静态的讨论基本比较

动态 静态性能 编解码过程较复杂,性能较差 好修改描述的影响 影响小,动态字段的修订,无需

执行数据转换影响大,每次修改都需要转换

安全性 低,动态字段的修订必须非常小心,数据库上执行错误的代码将会造成不可挽回的影响。

高,既然每次修改都需要转换,转换的源可以作为备份,一旦出现问题容易挽回。

修改描述的讨论1. 数据库设计必须仔细斟酌,不到万不得已不应该修改。2. 动态方式不是随意修改设计的理由,通常动态系统都会教坏设计者。3. 通常的修改都是需求变化带来的,讨论好需求,预期一些可能的变化,事先准备,是比较好的方式。

推荐的方式1. 项目开始投入使用,推荐使用静态方式。2. 维护时,使用转换工具转换数据库,备份原始库,是非常安全的方式。3. 随着记录数量的增加,转换时间快要超出容忍限度的时候,可以考虑转换为动态方式,降低之后的转换频率。

4. 一旦使用动态方式需要特别小心,应该在备份的数据上,执行严格的测试,再应用于实际系统,避免造成运营事故。

QR Code

Limax 参考 ISO/IEC 18004-2006 提供 QR Code 编解码支持(Micro QR Code 除外)。220

Page 221: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

所有语言版本都实现在单个文件中,没有任何语言标准库之外的依赖,便于引用。编码器原型Java QrCode limax.codec.QrCode.encode(byte[] data, int ecl)C# QrCode limax.codec.QrCode.encode(byte[] data, int ecl)C99 QrCode qr_encode(void *data, int size, int ecl)Javascript QrCode encode(data, ecl)

1 参数1.1 data,字节数组,特别的,对于 Javascript,提供方法 QrCode.toUTF8,可以将浏

览器中输入的字符串转换为 UTF8 字节数组。(可以参考 javascript目录下的qrcode.html)

1.2 ecl,各语言定义的常数 ECL_L,ECL_M,ECL_Q,ECL_H决定纠错级别。2 返回值

2.1 QrCode 对象,输入数据过长以至于 VERSION 40的 QR Code都不能表示,返回null。特别的,对于 C99,QrCode 对象需要通过 qrcode->release(qrcode);的方式释放。

2.2 所有 QrCode 对象都提供方法 toSvgXML方法,返回 SvgXML 字符串,输出到文件便于浏览器直接显示。

2.3 Java, C#的 QrCode 对象提 供方法 getModules(),返回表示 QrCode 模块的boolean数组,数组长度开平方取整,即可获得尺寸,C99中直接访问 QrCode的成员 modules,size 即可,通过模块数组可以方便地生成其它形式的表示,比如图片(可以参考 limax.executable.QrCodeTool.java的实现)

ISO/IEC 18004-2006 相容性1 只支持单一的数字模式,字母数字模式,字节模式编码,不使用混合模式。输入数据

全部符合数字模式选择数字模式,全部符合字母数字模式选择字母数字模式,否则使用字节模式。

2 不支持 ECI,不支持 FNC1,不支持 KANJI,不支持结构链。应用讨论1 通常输入数据应该考虑使用 UTF8 编码。2 通过 QR Code交换数据,如果要求生成尽量小的 QR Code,使用纯数字最有效,数字

模式生成短编码,加入大写字母,字母数字模式生成较短编码。避免使用小写字母。3 如果QR Code中间需要嵌入图片,选择较高纠错级别,比如,ECL_Q,ECL_H。

221

Page 222: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

解码器(解码一张黑白图片中的QR Code)原型Java

QrCode.Info limax.codec.QrCode.decode(byte[] image_1bit, int width, int height, int sample_granularity);

C99 QrCodeInfo qr_decode(char *image_1bit, int width, int height, int sample_granularity);

1 参数1.1 image_1bit,输入应该为 1bit黑白图片,一个字节表示一个像素,0为黑。1.2 width,图片宽度1.3 height,图片高度1.4 sample_granularity,采样粒度,1为逐点采样,较大的图片选择较大采样粒度可

以获得更好的性能,但是可能导致解码失败。2 返回值

2.1 无论解码成功失败,都返回 QrCode.Info,或者 QrCodeInfo 对象,C99中的返回对象 info应该通过 info->release(info)方式释放。成功失败由其中的 status 字段表示。C99中对象字段直接访问即可,Java中通过 get方式获取。

2.2 status,状态字 段。 Java 中的 enum QrCode.Info.Status, C99 中的, enum scan_status。有多种返回状态值。

2.2.1 SCAN_OK,扫描成功,可以获取正确的解码数据。2.2.2 ERR_POSITIONING,图片中的QR Code定位失败。2.2.3 ERR_VERSION_INFO,获取版本信息失败。2.2.4 ERR_ALIGNMENTS,定位 QR Code中的 ALIGNMENT PATTERNS失败。2.2.5 ERR_FORMAT_INFO,获取格式信息(纠错级别和掩码)失败。2.2.6 ERR_UNRECOVERABLE_CODEWORDS,码字错误数量超过纠错容量。2.2.7 UNSUPPORTED_ENCODE_MODE,不支持的编码模式,这种情况下从 Info中

获取的数据是纠错完成的未解码数据,应用如果有能力应该自己解码。2.3 data,length,返回数据的字节数组表示,C99中不可理解为 NULL 终止串。2.4 reverse,图片是否是反转的。2.5 mirror,图片是否是镜像的。2.6 version,版本2.7 ecl,纠错级别2.8 mask,掩码号

ISO/IEC 18004-2006 相容性1 不支持Micro QR Code解码2 支持QR Code的反转和镜像3 支持混合模式,数字模式,字母数字模式,字节模式,KANJI模式(KANJI模式编码的

日文字符转换为 UTF8输出)4 支持 ECI 模式,两种 FNC1 模式,使用 ISO/IEC 15424 指定的符号标识方式输出。

(ECI,FNC1能否交错,如何交错,两个标准都没有提及,实现为允许随意交错)222

Page 223: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

5 不支持结构链模式示例工具Javascript 版本javascript目录下的 qrcode.html,可以根据输入串,生成纠错级别为 H的QR Code。Java 版本java -jar limax.jar qrcode encode <filename> <(L|M|Q|H)> <text>为 text文字串生成 QR Code,输出到 filename 指定的文件中。文件内容由文件名后缀决定,如果是 svg,生成 SvgXML,除此之外生成图形文件,文件名后缀必须为 ImageIO.write方法支持的文件格式。java -jar limax.jar qrcode decode <filename> [sample_granularity=1] [bwthreshold=64] [meanfilter=2]解码图片,可以使用一张包含 QR Code的图片执行解码。sample_granularity为采样粒度,bwthreshold为图片转换为灰度图之后进一步转换为黑白图的阈值(这里存在一个比较奇怪的问题, jdk的灰度转换结果不符合 Y = 0.299R + 0.587G + 0.114B的灰度转换公式,明显偏暗,不得已选择了 bwthreshold=64),meanfilter决定了均值过滤器尺寸,使用周边(N*2+1)^2个像素参与中心像素均值计算,均值过滤器过滤掉可能影响解码的图像噪点。默认参数比较适合解码照片,如果要解码屏幕截图,这种参数选择可能导致失败。

java –jar limax.jar qrcode decode capture.jpg 1 128 0也许是合适的选择,屏幕分辨率通常比照片小得多,meanfilter=2有可能把截图中的 QR Code模块破坏掉,另外,屏幕截图通常不存在噪点问题,0才是更合适的选择。高级话题解码性能1. 显而易见,越小的图片解码性能越高,图片越小细节越少,解码高版本 QR Code可能

存在问题,缩小原始图像或者截取部分原始图像属于工程手段,不作重点讨论。2. 解码操作属于内存密集型操作, java 版本性能终归比不上 C99 版本,对性能有要求的应用建议使用 C99 版本。

3. 彩色图像到黑白图像的预处理过程比解码操作本身开销更大,可以考虑使用能够充分利用 GPU能力的图像处理库,demo中使用了 opencv。pc上实测,比 java 版本可以有10倍以上提升。

4. 上述手段都用上了,还不能满足要求,可以选择较大的 sample_granularity解码参数。5. 更加极端的需求,就只能对解码器进行并行优化了,并行优化的运行环境相关性太大,这里不提供,源码中 scanFinder方法就是最大的性能瓶颈,可以重点考虑。

223

Page 224: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

图像质量1. 图像质量是能否成功解码的先决条件,受各种因素影响。2. ISO/IEC 18004-2006 对 QR Code图像的规格,印刷规则有明确指导。现实中确有很多不合规 QR Code 存在,例如,Border 明确要求至少 4倍模块宽度,却有不少 QR Code在Border范围内印刷文字。

3. QR Code使用 RS纠错算法,对于 QR Code 内图像污染有一定的抵抗能力,但是无法解决三个角上的 FinderPattern的污染。此外,3个 FinderPattern 并不足以进行透视矫正,当前实现通过扫描底边和右边确定 QR Code的右下角,所以对底部空白,右边空白有一定的要求,对于扫描范围内的污染没有太好的抵抗能力。

4. 版本越高的 QR Code 对于对焦的要求越高,对焦不好,边缘模糊的 QR Code图像滤波之后黑模块与白模块的宽度比例会出现明显偏差,FinderPattern不能确定下来,直接导致解码失败。

5. 照度不足将导致相机选择更高感光度,产生更多图像噪点,图像滤波可以解决一定的问题。如何选择更好的滤波方案只能根据具体应用环境实测。

6. 不支持同一图片上多个 QR Code,多 QR Code可能导致 FinderPattern选择错误。PC屏幕上某些选中的 Radio Button具有明显的 FinderPattern特征,实际环境中同样可能存在这类干扰。当前实现,以面积最大为标准选择 3个 FinderPattern,不作方差过滤(较重的透视变形使得远端 FinderPattern 明 显偏小,方差过滤可能滤掉这样的FinderPattern)。面积最大的标准,决定了 QR Code 面积在图片中占比越大,解码成功率越高,扫描明显很小QR Code可能需要进行图像截取,或者局部放大。

7. 不可能完全支持非标准的创意 QR Code,某些创意 QR Code贴合特定解码器的参数条件进行设计,为这样 QR Code 调试参数没有太大意义,除非把兼容这些 QR Code 作为应用的设计目标。

Demo

1. demo/qrdecode目录下提供了 pc与 android两个版本的 demo,编译这些版本均需要下载 opencv 库,进行相应配置。

2. pc 版本与 java 版本功能相同,用来解码照片中的QR Code,有极大的性能提升。3. pc 版本与 android 版本的关键代码完全相同,可以辅助设计特定场景下的 android应用,

试验和选择图像滤波算法以及参数。4. android 版本利用相机直接实现一个通用的实时解码器,显示 QR Code 信息。5. http://www.limax-project.org/download/QR-Demo.apk 提供下载。

JDBC 连接池常见的 c3p0之类的连接池,直接向应用分配自己管理的连接。绝大多数 JDBC方法,都会抛出 SQLException,要求每一处 SQL 操作都能正确处理连接意外并不现实。典型的,数据库重启之后,初次 SQL 操作往往会失败,接下来连接池才会恢复所有连接。Mysql的Connector/J,抛出 CommunicationsException的问题并不少见。所以 Limax 提供一个简单的连接池实现,通过区分 SQLException决定是否重启操作,避免把可以通过重启操作解决的

224

Page 225: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

问题推给应用。基本原理1. 连接通过 consumer的方式提供给应用, SQL 操作集合在一个特定范围内完成,这样所有的 SQLException才有可能被连接池者监管,consumer才有机会被重启。这里提供两个范围支持,函数范围和线程范围(跨线程转移连接不支持,这也不必要,即便使用常规连接池,线程也是从连接池请求新连接,而不至于私下交换)。

2. 通过重启 consumer 也顺便解决死锁,超时之类的异常。编程接口接口 limax.sql.SQLConnectionConsumer

应用实现这个接口打包 SQL 操作集合接口 limax.sql.SQLExecutor

定义方法 void execute(SQLConnectionConsumer consumer) throws Exception 定义常量 COMMIT 和 ROLLBACK,专用于线程范围连接池。execute(SQLExecutor.COMMIT) 提交事务。execute(SQLExecutor.ROLLBACK)回滚事务。类 limax.sql.SQLPooledExecutor

构造函数:SQLPooledExecutor(String url, int size, boolean threadAffinity, Consumer<Exception> logger)url为数据库连接 url,size决定了连接池尺寸,threadAffinity决定了该连接池是函数范围还是线程范围,logger用于记录导致 consumer重启的那些异常,便于应用排查重启原因,改进设计。SQLPooledExecutor(String url, int size, Consumer<java.lang.Exception> logger) SQLPooledExecutor(String url, int size, boolean threadAffinity) SQLPooledExecutor(String url, int size)简化版本的构造函数,其中不含 threadAffinity 参数的构造函数,默认 threadAffinity 为false。构造函数返回时,size个连接已经建立,这样可以确保数据库认可连接 url,并且允许建立size个连接,如果构造函数迟迟不返回,应该检查数据库服务器是否启动,这种情况下logger 也会不断输出导致重连的异常。成员函数:void execute(SQLConnectionConsumer consumer) throws Exception执行 consumer 中应用提 供的 SQL 操 作 集合, consumer 不必 catch 异常,特别是

225

Page 226: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

SQLException,应该在 execute外 catch异常,保证连接池能够检查所有异常决定是否重启,导致重启的异常通过 logger记录,不会导致的重启的异常通过 execute往外抛。。void shutdown()关闭连接池,拒绝新的执行请求。类 limax.sql.RestartTransactionException

线程范围连接池使用这个异常来指明事务需要被重启。函数范围连接池一次 execute就能完成整个事务,使用函数范围连接池。这种类型的连接池,绝大多数情况够用了。特性1. 连接初始化为 auto-commit方式。2. 一次 execute之后无论成败,连接重新设置为 auto-commit方式。3. 执行多语句事务,首先应该将 auto-commit设置为 false。示例public final class MysqlTest {

public static void main(String[] args) throws Exception {SQLPooledExecutor executor = new SQLPooledExecutor(

"jdbc:mysql://192.168.1.3:3306/test?user=root&password=admin&characterEncoding=utf8", 3, e -> e.printStackTrace());

Thread.sleep(10000);executor.execute(conn -> {

conn.setAutoCommit(false);try (PreparedStatement ps = conn.prepareStatement("INSERT INTO test(name) VALUES(?)")) {

ps.setString(1, "A");ps.executeUpdate();ps.setString(1, "B");ps.executeUpdate();

}});executor.shutdown();

}}在 sleep的 10秒期间,重启数据库,观察输出,执行结束后两条记录均被插入数据库。

226

Page 227: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

线程范围连接池线程范围连接池跨越多个 execute 操作,除非复杂应用,这种类型的连接池很少用到,最典型的应用是 limax.zdb.LoggerMysql.java中的writer连接池,一次 checkpoint 操作发起的全部修改动作打包为一个事务。特性1. 连接初始化为非 auto-commit方式,consumer禁止设置 auto-commit。2. 线程首次调用 execute,连接分配给线程,意味着事务的开始。3. 任何一次 execute抛出异常或者以 SQLExecutor.COMMIT,SQLExecutor.ROLLBACK 作为

consumer 调用 execute,连接将归还连接池,事务结束。4. 任何一次 execute抛出异常,事务回滚并结束。其中,RestartTransactionException 指示

应用可以重启事务。示例public final class MysqlTest {

private static SQLPooledExecutor executor = new SQLPooledExecutor("jdbc:mysql://192.168.1.3:3306/test?

user=root&password=admin&characterEncoding=utf8", 3, true);

private static void insert(String s) throws Exception {System.out.println("insert " + s);executor.execute(conn -> {

try (PreparedStatement ps = conn.prepareStatement("INSERT INTO test(name) VALUES(?)")) {

ps.setString(1, s);ps.executeUpdate();

}});

}

public static void main(String[] args) throws Exception {while (true) {

try {insert("A");Thread.sleep(5000);insert("B");Thread.sleep(5000);insert("C");executor.execute(SQLExecutor.COMMIT);Thread.sleep(5000);

227

Page 228: limax-project.orglimax-project.org/download/Limax Project.zh.docx · Web viewLimax使用xml描述项目模型的构建,定义了命名节点和引用节点两类xml节点。存在同时是命名节点和引用节点的这种节点。xml内部名字的解析使用内部名字空间规范。命名节点

} catch (RestartTransactionException e) {System.out.println("restart");

}}

}}

程序启动之后,任何时间点重启数据库,可以看到类似如下的结果。insert Ainsert Binsert Crestartinsert Arestartinsert Arestartinsert Ainsert B发生了 3 次 restart,原因在于,连接池 size = 3,轮转分配,首次 restart之后的两次 insert A,触发了后续连接的恢复。可以看出,无论以任何方式重启数据库,ABC 插入数据库的事务完整性总能得到保证。特别注意,executor.execute(SQLExecutor.COMMIT);跟其它 SQL 操作在同一个 try块内,千万不能习惯性放到 finally块,这个操作同样有可能抛出异常。高级话题1. SQLConnectionConsumer 约束了应用实现,同时也就约束了更好的事务完整性。2. 既然操作可能重启,那么必须设计可重启的代码,比如某些参数多次执行不可改变。3. 一些大型结果集的 SELECT 操作,也许设计允许失败,这种情况下,应该判断操作是否

重启,作出相应决策。4. 如果数据库服务器长时间维护,应用也许会表现为不响应,碰到这种情况应该检查

logger输出,检查连接状态。5. RestartTransactionException 从 RuntimeException派生,而不是 SQLException,这是应用

复杂性决定的,也 许 调用栈内大多数方法都跟 SQL 毫无关系,可以参 考limax.zdb.Checkpoint.java中的使用。

228