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模式, 脚本模式, 在各种类型化语言实现中都能混合使用。混合使用的实际意义不大, 而且可能导致开发上的混乱, 除非有非常必要的理由, 不建议如此使用。