5.5 Client development


  • 5.5.3 The client of the Java version

    The client of the Java versoin supports three modes: the static mode, the Variant mode, and the script mode, which will be separately introduced.

    • The static mode

      The static mode need to use the xmlgen to generate the client source code, and is the least error-prone mode.


      java -jar <path to limax.jar> xmlgen -java –noServiceXML example.client.xml
      

      The parameter -noServiceXML declares it is no need to generate the launch configuration file like service-ExampleClient.xml. Different from the server, in the most conditions, the generated client framework is only a part of the application. The application provides the main function and does not be launched via this configuration file. Next introuduce how to use the manually launch.

      Create the eclipse project, set the generated src and gen directories as the source code directories, and add the dependency relationship to the Limax project.

      New the Main.java file which will be explained line by line here.

      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. The statement Thread.sleep(2000) in the main function is noted. In the normal cicumstances, the application enters the main loop here, and sleep used here is for the example. The previous start() launches the Endpoint and the next stop() ends.

      2. The start() function starts Endpoint engine first, then creates the configuration and uses the configuration and a Listener to launch the Endpoint connection server.

          a. The createEndpointConfigBuilder() function creates the required configuration when the server logins, and the parameters are the server ip, server port, LoginConfig.plainLogin packed username, user token, and the auany authentication module name.

          b. The endpointState() function directly uses the method provided by the service code generated by the state element which is referenced by the manager element in the service element of the client defined in the xml description. If it needs to support the service provided by the multiple PVID, the parameters separated by the comma list the corresponding methods related to the each generated source code. Actually, this method provides the support to the protocol. If no protocol is used, it is no need to call this function.

          c. The staticViewClasses() function sets the management class instance of the view in the static mode. This management class is automaticly generated when generating the source code. If it needs to support the service provided by the multiple PVID, the parameters separated by the comma list the corresponding methods related to the each generated source code.

      3. The stop() function ends the Endpoint engine using synchronization mode.

      Use EndpointListener to receive the network interaction information from the 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);
          }
      }
      

      Launch the server, run the client and get the informaiton:

      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

      Contrast the source code and explain line by line:

      1. After calling Endpoint.start(), first the onManagerInitialized() function is called to prepare the initial resource for the application related to the network. In the last, the onManagerUninitialized() function is called to release the initial resource.

      2. The onSocketConnected() function is a progress indicator to inform that the socket connection has finished. If fail in the connecting, the onAbort() function is called and the abort reason could be obtained through calling transport.getCloseReason() function.

      3. The onKeyExchangeDone() function is another progress indicator to inform that the handshake operation of the login with the server has finished. If fail in the logining, the onErrorOccured() function will be called and the failure reason could be gotten through source and code. The definition of the source and code is in the defines.beans.xml of the Limax source code.

      4. The onErrorOccured() function, when the source is ErrorSource.ENDPOINT, the code must be 0, and the exception is the exception from the operation in the internal framework.

      5. The onTransportAdded() function is called to trigger the transferring the message on the transport after the initial operation of the connection in the Endpoint layer has finished. For view, it is the proper position to register the Listener of the global view and the session view. The corresponding onTransportRemoved() function will be called when the connection ends.

      6. The onKeepAlived() function which is used to periodly send the Keepalive message to the server from the client, is called after the server responses. This method roughly provides the round-trip time in milliseconds between the client and the server.

      The key of using view is to register the required Listener to get the change information. Then modify the onTransportAdded() function.


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

      Modify the MyTemporaryView.java file.

      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 ");
      }
      

      Run the program and get the following result:

      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

      Contrasting the output of the chrome, the notice of the data change is consistent. The penultimate line should be noted because the output of the chrome has no this line. It is not error and the reason is that the javascript version triggers the message strictly according to the notice of the server. The onClose of the Java version does not come from the notice of the server, but the Endpoint calls it in all temporay view when closing in order to correspond to the previous onOpen and provide the application a chance to release the resource.

      Matters need attention:

      1. Throwing any exception in processing message will lead to the close of the network connection. The framework calls the onManagerUninitialized() function of the EndpointListener to know that the Endpoint ends and all related resouce has been released. After that, it is ok to re-launch the Endpoint.

      2. The framework guarantees the onManagerInitialized() and onManagerUninitialized() messages of the EndpointListener are notified as a pair, except that the onManagerInitialized() throws the exception.

      3. The framework guarantees the onTransportAdded() and onTransportRemoved() messages of the EndpointListener are notified as a pair, except that the onTransportAdded() throws the exception.

      4. The onTransportAdded reports the beginning of the network activity. Various follow-up data might have arrived, so even the exception is thrown here to close the connection, the data deliveried before the termination of the connection are still notified. We suggest that do not throw exception here.

      5. Only that the onManagerUninitialized message is triggered means that all the datum of the server have been handled. The framework strictly guarantees that no data is lost. As memtioned in the previous one that the onTransportAdded might cause the problem when throwing the exception, strict guarantee that no data loss does not mean a wide range logic function adaptability. When the processing of some message causes the error, the application itself is responsible for ignoring the follow-up meaningless data to avoid to generate the associated error.

      6. Regardless of the onOpen() function of the temporary view throwing the exception or not, the onClose() function eventually will be called.

      7. The code of the static mode could use the string Message as the control to send to the server. In this example, if the MySessionView.getInstance().control(99999) is replaced with the MySessionView.getInstance.sendMessage("99999"), the result is the same. (The control function name and parameter definition here come from the <control name="control"> in the example.share.xml.) If it needs to support the script client at the same time, directly using the string mode, but not defining the own control is more unified when the server implements.

      8. The View object provides two versions' registerListener method, which could register the ViewChangedListener to monitor the fileds' change of the whole view object or the particular field' change. The registerListener method returns the Runnable object. Executing this object's run method could unregister. The above code has not unregistered because all the registers will be automatically unregistered when the view object is released.

      9. The ViewChangedListener.onViewChanged notification happens in the thread inside the framework, and the value of the notification is the reference of the corresponding field of the view. The reason that sending the value to another thread but another thread could not get the timely data is simple: there is might that when another thread access the corresponding field of the view via value, there is another notification sent/received and view is changed. There are two solutions to resolve this issue. First, when transferring the view to another thread, copy it. Second, the thread using the data directly schedules the notification, and the method is: when creating the EndpointConfig, use executor method to specify an own thread scheduler.

      Using the Android as the example, if most data of the view are used by the UI thread, the UI thread is used to execute the notification.


      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. If the own shedule thread is specified by using executor, such as UI thread, it is necessary to understand the below design points:

          a. The Endpoint.start certainly start a new thread to create Endpoint, because it is uncertain that whether the UI thread executes the Engine.start currently. If yes, there is hungry when notifying the onManagerInitialized message, because the launch process must strictly wait for the onManagerInitialized to finish. (The UI thread waits for the onManagerInitialized to finish, however, it must be the UI thread to execute the onManagerInitialized.)

          b. When some Manager calls the close function, the new thread will be started as the asynchronous close. So it is necessary to confirm the end of the Endpoint activity through onManagerUnitialized. Please refer to the 5.

          c. The Endpoint.closeEngine will start the new thread to stop the engine via asynchronous mode, and the reason is similar as the "a". Through passing a "Runnable done", the close finished message could be obtained. The previous example converts the asynchronous mode to the synchronous mode to execute. If the done is null, there is no any finished message to obtain. In the environment of using UI system, an own done could be implemented. Setting a flag, UI thread polles the flag to obtain whether the close is finished.

      11. The generated all implemation code of the view provides the getInstance method to get the instance of the view object. Using ViewVisitor as the parameter of the visitXXX accesses the data of the field in the thread-safe manner. To the non-subscribed field, if the data does not exist, the ViewVisitor would not be called; to the subscribed field, a map is always provided to the ViewVistor, and the Map contains the effective SessionId and the corresponding data.

      12. Not only the field data of the view provided by the notification, but also the field data of the view obtained through calling the visit method, MUST be the READ-ONLY data and could not be modified.


    • The Variant mode

      It is no need to generate the source code for the client in the Variant mode, but the content described in the xml must be clearly understood. When the xml is changed, it is necessary to check the code to avoid the error. (After the modification in the static mode, when regenerating the source code for the client, both the editor and the complier have the chance to report the error.) The advantage is that the size of the soure code of the client is relatively smaller.

      When generateing the code for the server, the -variant parameter is added so that the server could generate the corresponding support source code.


      java –jar <path to limax.jar> xmlgen –variant example.server.xml
      

      To contrast, directly use the source code of the static mode to modify:

      Firstly, the launch configuration is needed when creating the variant mode:


      int providerId = 100;
      EndpointConfig config = Endpoint.createEndpointConfigBuilder("127.0.0.1", 10000, 
          LoginConfig.plainLogin("testabc","123456", "test")).variantProviderIds(providerId).build();
      

      It is noted that the endpointState and staticViewClasses which dependents on the client to generate the source code are not required, and the variantProviderIds is added to assign the PVID. If it needs to support multiple PVID, the parameter is separated by the comma. The method like the executor could be used here.

      Next, modify the onTransportAdded function of the Listener.


      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) {
              }
          });
      }
      

      Run the program and get the below result:

      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

      Contrasting with the output of the static mode, the result is consistent.


      Referencing the source code of the onTransportAdded, it is need to understand the blew key points when using the variant mode:

      1. Fistly, obtaining the VariantManager is necessary. The ViewManager is decided by the EndpointManager and PVID together. One EndpointManager corresponds to one network connection and could be gotten from the Transport. The EndpointManager and the manager noticed by the onManagerInitialized are the same one. But the VariantManager MUST be obtained in the onTransportAdded phase, and the reason is that in the Variant mode, all the structure information of the view is transferred via the server and these information are prepared in the onTransportAdded phase. Mentioned in the previous content, the same connection allows to use multiple PVID to request multiple services. Knowledged from the obtainment mode of the VariantManager, a VariantManager corresponds to a service and manage all the views in this service.

      2. The global view and the session view could be directly obtained in the ViewManager, and the Handler could be set in the temporary view to obtain the action's notice of the temporary view. Registering the Listener in the view is consistent with the one in the static mode.

      3. The VariantView uses Variant type to represent all the data type, and could create the Variant presentation of all the primitive types, the container type, and the structure type. A structure type is created as the parameter of the control before sendControl. Actually, the Variant provides a serial functions such as createXXX, getXXX, setXXX to maintain the data structure, and the javadoc document is referenced for the more detailed information.

      4. Send the string message to the view of the server in the Variant mode, such as manager.sendMessage(mySessionView, "99999").

      5. Similar to accessing the field data via visitXXX by the view in the static mode, the field name and the ViewVisitor could be used to call the visitField method of the VariantView in the Variant mode.

      6. It is particularly noted that the string in the code should strictly correspond to the xml description, such as "share.MyTemporaryView", which uses the view's full name begining from the outermost namespace and could not be abbreviated as "MyTemporaryView". The parameter "var0" of the bean used by the sendControl, which corresponds to the <variable name="var0" type="int"/> in the xml description, could not be wrongly spelled.

      7. The other considerations are the same as the ones of the static view.


    • The script mode

      It is no need to generate the source code to support the script mode because the OracleJDK has provides the javascript engine. The below example represents the usage of the script mode.

      The -script parameter need to be used when generating the server source code so that the server could generate the relative supporting code. For example:


      java -jar <path to limax.jar> xmlgen -script example.server.xml    
      

      Copy the definition of the two variables var providers, var limax in the example.html and paste to the example.js. Replace the console.log and console.err with the print because the javascript engine of the OracleJDK has no global object as the console. Finally, copy the example.js to the directory bin and place it in the same layer as the Main.class.

      Clear all the functions of the MyListener and just leave the System.out.println to display the progress.

      The launch configuration:


      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();  
      

      The scriptEngineHandle method is used to assign the required Handler of the script engine, and one configuration only assigns one Handler. The JavaScriptHandle is provided by the framework as example, with the script engine object and the script's Reader as the parameters to read the content of the script.

      Run the program and get the below result:

      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

      This output result is consistent with the previous ones.

      Using the script mode, some considerations need to be clarified:

      1. If the lua script is used, it is need to reference to the JavaScriptHandle, including the ScriptEngineHandle interface implemented by the lua engine of the third party's java version. Actually, the ScriptEngineHandle corresponds to the operation mode of the limax.js and limax.lua.

      2. The other considerations are the same as the ones in the static view mode.


    • The conclusion of the three modes

      1. Correctly choose the parameter for the server to generate the source code, the "-variant" is used in the variant mode, and the "-script" is used in the script mode.

      2. The configuration of the client decides which mode is used. The staticViewClasses is used by the static mode; the variantProviderIds is used by the Variant mode; and the scriptEngineHandle is used by the script mode. If the server does not support the selected mode of the client, there is negotiation error, and the error PROVIDER_UNSUPPORTED_VARINAT and PROVIDER_UNSUPPORTED_SCRIPT are reported by the onErrorOccured of the EndpointListener message.

      3. The static mode uses the endpointState to support the protocol, however, the Variant mode and the script mode do not support the protocol.

      4. The server could run in the three modes at the same time. The same applicaton could have the different client implementation.

      5. The same application could run in the three modes at the same time for the consideratinn of the completeness by the client framework. However, this kind of usage has no actual meaning and choose one to use according to the requirement.


Prev Next