5.4 服务器开发
-
5.4.5 服务器简单验证
这里先不介绍客户端实现,而用HTML5方法,通过chrome,直观体验服务器开发的效果。
-
简单准备服务器组件
eclipse导入项目auany
启动limax.auany.Main
注意,第一次启动auany前请在auany当前目录下手工创建zdb目录。
eclipse导入项目switcher
启动limax.switcher.Main
-
xmlgen参数
加入 -script 参数生成服务器脚本支持代码
加入 -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作为token platflag : '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; import limax.provider.ProviderListener; import limax.provider.ProviderTransport; import limax.util.Trace; public class SessionManager implements ProviderListener { private ServerManager manager; @Override public 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(); } } @Override public void onManagerUninitialized(Manager manager) { } @Override public void onTransportAdded(Transport transport) throws Exception { long sessionid = ((ProviderTransport) transport).getSessionID(); if (Trace.isInfoEnabled()) Trace.info("SessionManager.onTransportAdded " + transport + " sessionid = " + sessionid); } @Override public void onTransportRemoved(Transport transport) throws Exception { long sessionid = ((ProviderTransport) transport).getSessionID(); if (Trace.isInfoEnabled()) Trace.info("SessionManager.onTransportRemoved " + transport + " sessionid = " + sessionid); } @Override public void onTransportDuplicate(Transport transport) throws Exception { manager.close(transport); } }
服务器初始化的时候触发onManagerInitialized, 这里应该首先加载服务器运行所需的应用资源,加载完成以后调用openListen(), openListen()首先初始化所有全局View,接下来告知Switcher准备就绪, 允许客户端连接。
服务器停止的时候触发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
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解除了绑定,意味着服务器停止。
-
实验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 here long 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字段在客户端新创建出来了,其中bind0字段内var0 = 100。
再次刷新chrome页面,内容不会变化。
修改example.html中的login.username,换成一个没有使用过的用户名。
刷新chrome页面:
注意到sessionid = 53248,这说明sessionid区分了用户。
-
实验4
private MySessionView(SessionView.CreateParameter param) { super(param); // bind here long 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.".
第四行,同前一个实验,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);
第二行,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。
-