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.xmluseGlobalId="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。


上一页 下一页