5.5 客户端开发


  • 5.5.6 Lua/C++客户端

    1. 生成服务器代码的时候,通过-luaTemplate生成lua模板代码example.lua

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

    2. 客户端首先创建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;
                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;
    }    
    

    EndpointListener消息代码与其它C++版本无区别。

    主要差别就在构造函数初始化引擎之前初始化了Lua虚拟机, 在虚拟机上装载了脚本代码callback.lua, 创建了脚本引擎的handler, 在创建配置的时候设置进来。 析构函数在结束引擎之后结束Lua虚拟机。

    callback.lua:

    The 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中运行,放置在项目目录下。


    启动服务器,运行客户端,获得结果:

    onManagerInitialized

    onSocketConnected

    onKeyExchangeDone

    onTransportAdded

    v100.share.MyTemporaryView.onopen table: 00674C50 3 table: 00674C00

    v100.share.MyTemporaryView.onchange table: 00674C50 61440 _var0 Hello 61440 NEW

    onKeepAlived 16

    v100.share.MySessionView.onchange table: 00674840 61440 var0 Hello 61440 NEW

    v100.share.MySessionView.onchange table: 00674840 61440 var0 99999 REPLACE

    v100.share.MyTemporaryView.onchange table: 00674C50 61440 _var0 99999 REPLACE

    onTransportRemoved

    limax close

    onManagerUninitialized

    这个结果看起来和前面版本均一致,除了table: XXXXXXXX,lua的对象均为table。这里print没有解析出来。


    各种注意事项:

    1. 头文件顺序,头文件limax.lua.h必须包含在lua.hpp之后。

    2. 这里为了示例在openEngine之前创建了lua虚拟机。具体项目应用中,应该直接使用应用提供的虚拟机,保证脚本中View的onXXX中实现的控制动作能够直接驱动应用逻辑。

    3. 创建配置时,一般需要调用.executor方法指定应用期望的线程。


  • 5.5.7 Lua/C#客户端

    1. 生成服务器代码的时候,通过-luaTemplate生成lua模板代码example.lua

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

    2. 客户端首先创建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)
            {
                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();
                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 EndpointManagerImpl

    onSocketConnected

    onKeyExchangeDone

    onTransportAdded limax.net.StateTransportImpl

    v100.share.MyTemporaryView.onopen table: 048E98F0 31 table: 048E9940

    v100.share.MyTemporaryView.onchange table: 048E98F0 36864 _var0 Hello 36864 NEW

    v100.share.MySessionView.onchange table: 048E6820 36864 var0 Hello 36864 NEW

    onKeepAlived 10

    v100.share.MySessionView.onchange table: 048E6820 36864 var0 99999 REPLACE

    v100.share.MyTemporaryView.onchange table: 048E98F0 36864 _var0 99999 REPLACE

    onTransportRemoved limax.net.StateTransportImpl System.Exception: channel closed manually

    limax close nil

    onManagerUninitialized EndpointManagerImpl

    这个结果看起来和前面版本均一致。


  • 5.5.8 Javascript/C++客户端 (SpiderMonkey)

    • 准备javascript引擎库

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

      2. https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Build_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>
      #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();
              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所在目录。


      启动服务器,运行客户端,获得结果:

      onManagerInitialized

      onSocketConnected

      onKeyExchangeDone

      onTransportAdded

      onKeepAlived 1

      v100.share.MyTemporaryView.onopen [object Object] 3 36864

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

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

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

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

      onTransportRemoved

      onManagerUninitialized

      limax 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线程,完成这些任务。


  • 5.5.9 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)
              {
                  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)
                  {
                      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 EndpointManagerImpl

      onSocketConnected

      onKeyExchangeDone

      onTransportAdded limax.net.StateTransportImpl

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

      v100.share.MyTemporaryView.onopen [object Object] 190 36864

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

      onKeepAlived 12

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

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

      onTransportRemoved limax.net.StateTransportImpl System.Exception: channel closed manually

      limax close null

      onManagerUninitialized EndpointManagerImpl

      这个运行结果与之前的版本均一致。


    • 代码说明

      1. 受限于SpiderMonkey的线程模型,不得不提供一个JsContext包装类,JsContext创建了一个线程与Js操作对象关联,Js操作对象支持C#与Javascript交互,详见附录CLR/Javascript一节。所有操作完成以后,需要shutdown JsContext对象。


  • 5.5.10 客户端开发总结

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

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

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

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


上一页 下一页