5.5 客户端开发


  • 5.5.3 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,逐条解释。

      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", 10000, LoginConfig.plainLogin("testabc","123456", "test"))
                      .endpointState(example.ExampleClient.states.ExampleClient.getDefaultState(providerId))
                      .staticViewClasses(example.ExampleClient.share.ViewManager.createInstance(providerId))
                      .build();
                  Endpoint.start(config, new MyListener());
          }
      
          private static void stop() {
              Runnable done = new Runnable() {
                  @Override
                  public 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() {
          }
          @Override
          public void onAbort(Transport transport) throws Exception {
              Throwable e = transport.getCloseReason();
              System.out.println("onAbort " + transport + " " + e);
          }
          @Override
          public void onManagerInitialized(Manager manager, Config config) {
              System.out.println("onManagerInitialized "
                      + config.getClass().getName() + " " + manager);
          }
          @Override
          public void onManagerUninitialized(Manager manager) {
              System.out.println("onManagerUninitialized " + manager);
          }
          @Override
          public void onTransportAdded(Transport transport) throws Exception {
              System.out.println("onTransportAdded " + transport);
          }
          @Override
          public void onTransportRemoved(Transport transport) throws Exception {
              Throwable e = transport.getCloseReason();
              System.out.println("onTransportRemoved " + transport + " " + e);
          }
          @Override
          public void onSocketConnected() {
              System.out.println("onSocketConnected");
          }
          @Override
          public void onKeyExchangeDone() {
              System.out.println("onKeyExchangeDone");
          }
          @Override
          public void onKeepAlived(int ms) {
              System.out.println("onKeepAlived " + ms);
          }
          @Override
          public void onErrorOccured(int source, int code, Throwable exception) {
              System.out.println("onErrorOccured " + source + " " + code + “ “ + exception);
          }
      }
      

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

      onManagerInitialized limax.endpoint.EndpointConfigBuilderImpl$2 limax.endpoint.EndpointManagerImpl

      onSocketConnected

      onKeyExchangeDone

      onTransportAdded limax.net.StateTransportImpl (/127.0.0.1:26240-/127.0.0.1:10000)

      onKeepAlived 8

      onTransportRemoved limax.net.StateTransportImpl (/127.0.0.1:26240-/127.0.0.1:10000) java.io.IOException: channel closed manually

      onManagerUninitialized 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获取改变信息,接下来修改onTransportAdded


      public void onTransportAdded(Transport transport) throws Exception {
          System.out.println("onTransportAdded " + transport);
          MySessionView.getInstance().registerListener(e -> System.out.println(e)); 
      }
      

      修改MyTemporaryView.java

      MyTemporaryView.java


      protected void onOpen(java.util.Collection<Long> sessionids) {
          // register listener here
          System.out.println(this + " onOpen " + sessionids);
          registerListener(e -> System.out.println(e));
          try {
              MySessionView.getInstance().control(99999);
          } catch (Exception e) {
          }
      }
      protected void onClose() {
          System.out.println(this + " onClose ");
      }
      

      运行程序,获得类似如下的结果:

      onManagerInitialized limax.endpoint.EndpointConfigBuilderImpl$2 limax.endpoint.EndpointManagerImpl

      onSocketConnected

      onKeyExchangeDone

      onTransportAdded 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 REPLACE

      onTransportRemoved 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,报告了网络活动的开始,各种后续数据可能已经到来,即便在这里抛出异常终止连接,连接终止前投递的数据依然会正常通告,建议不要在这里抛异常。

      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线程)

          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() {
              @Override
              public void onOpen(VariantView view, 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) {
                  }
              }
      
              @Override
              public void onClose(VariantView view) {
                  System.out.println(view + " onClose ");
              }
      
              @Override
                  public void onAttach(VariantView view, long sessionid) {
              }
      
              @Override
                  public void onDetach(VariantView view, long sessionid, int reason) {
              }
          });
      }
      

      运行程序,获得如下结果:

      onManagerInitialized limax.endpoint.EndpointConfigBuilderImpl$2 limax.endpoint.EndpointManagerImpl

      onSocketConnected

      onKeyExchangeDone

      onTransportAdded 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 NEW

      onKeepAlived 11

      [view = share.MySessionView ProviderId = 100 classindex = 1] 61440 var0 99999 REPLACE

      [view = share.MyTemporaryView ProviderId = 100 classindex = 2 instanceindex = 2] 61440 _var0 99999 REPLACE

      onTransportRemoved 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

      与静态模式的结果输出比较,结果一致。


      参考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参数,服务器端将生成相关的支持代码,例如:


      java -jar <path to limax.jar> xmlgen -script 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"))
          .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.EndpointManagerImpl

      onSocketConnected

      onKeyExchangeDone

      onTransportAdded limax.net.StateTransportImpl (/127.0.0.1:53610-/127.0.0.1:10000)

      v100.share.MyTemporaryView.onopen [object Object] 9 61440

      v100.share.MyTemporaryView.onchange [object Object] 61440 _var0 Hello 61440 NEW

      v100.share.MySessionView.onchange [object Object] 61440 var0 Hello 61440 NEW

      onKeepAlived 151

      v100.share.MySessionView.onchange [object Object] 61440 var0 99999 REPLACE

      v100.share.MyTemporaryView.onchange [object Object] 61440 _var0 99999 REPLACE

      onTransportRemoved limax.net.StateTransportImpl (/127.0.0.1:53610-/127.0.0.1:10000) java.io.IOException: channel closed manually

      limax close null

      onManagerUninitialized 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。

      3. 静态模式使用endpointState支持协议,Variant模式,脚本模式不支持协议。

      4. 服务器可以同时运行在3种模式下,同一应用,可以有不同的客户端实现。

      5. 客户端框架出于完备性考虑,同一应用可以同时运行在3种模式下,这样使用没有实际意义,根据需求选择一种即可。


上一页 下一页