Skip to content
thomasjammet edited this page Sep 24, 2013 · 62 revisions

CumulusServer includes a powerfull script-engine (using LUA langage) to create your own server applications. It allows to extend Cumulus behavior to add your specific needs in a flexible scripting-way.

Create a server application

On start, Cumulus creates a www folder in its root directory if it doesn't exist already. In this space, you can create subfolders, each one of them describes one server application. When a client connects to CumulusServer, the URL path relates the application to use. For example, the address rtmfp://host:port/myApplication search its corresponding application in CumulusServer/www/myApplication folder. First time CumulusServer builds and executes the file main.lua presents in this folder. Then, on new comer, the application already built is executed, unless main.lua file has been modified since the last time. Indeed when you edit main.lua file, the corresponding application is rebuilt in a dynamic way, without having to restart the server.

rtmfp://host:port/                   ->     CumulusServer/www/main.lua (root application)
rtmfp://host:port/myApplication      ->     CumulusServer/www/myApplication/main.lua
rtmfp://host:port/Games/myGame       ->     CumulusServer/www/Games/myGame/main.lua

Note The root application is built and started on CumulusServer start, whereas other server applications are started on first client connection.

Each application is identified by its path, which is exactly the path part of the RTMFP URL connection. In the example above, root application has an empty string for path, then the both others have respectively /myApplication and /Games/myGame for path values.

Here a very simple first server application:

	function onStart(path)
		print("Server application '"..path.."' started")
	end
	function onStop(path)
		print("Server application '"..path.."' stopped")
	end

Global configurations

CumulusServer.ini file allows to give some global configuration values on CumulusServer start-up (see Installation page). To access for these values from script, use cumulus.configs property (see cumulus object description in Server Application, API page to get more details). This system allows to create your own global configuration values and access for them in scripts.

	; CumulusServer.ini file
	port=19350 ; existing config
	; New custom config
	[myGroup]
	myConfig=test
	-- A script file
	print(cumulus.configs.port) -- displays "19350"
	print(cumulus.configs.myGroup.myConfig) -- displays "test"

Communication between server applications

One application can access (read or write) to a variable of one other application, and can call one function of one other application. But it goes much further than that, applications can be specialized, and inherit them, exactly like inheritance of classes.

Application inheritance

Principle is simple, for example the /Games/myGame application extends the /Games application, and so all functions and variables available in /Games/main.lua are available in /Games/myGame/main.lua.

	-- /Games script application
	test = "I am Games"
	-- /Games/myGame script application
	print(test) -- displays "I am Games"

You can overload an inherited variable or an inherited function, and even dynamically remove the overload if need in putting value to nil.

	-- /Games/myGame script application
	print(test)          -- displays "I am Games"
	test = "I am myGame" -- overloads test variable
	print(test)          -- displays "I am myGame"
	test = nil           -- remove overloading test variable
	print(test)          -- displays "I am Games"

On variable overloading (or function overloading), you can always access for the parent version in prefixing with the parent application name.

	-- /Games/myGame script application
	print(test)          -- displays "I am Games"
	test = "I am myGame" -- overloads test variable
	print(test)          -- displays "I am myGame"
	print(Games.test)    -- displays "I am Games"

Note The root server application has for path an empty string, and is reached by the name www.

	-- '/' script application (the root server application)
	function hello()
		print("I am the root application")
	end
	-- /Games script application
	function hello()
		print("I am /Games application")
	end
	hello() -- displays "I am /Games application"
	www:hello() -- displays "I am the root application"

Warning Events are functions called by the system (see Events part of Server Application, API page), if an application don't definites onConnection event for example, on new client connection for this application, it's the parent application which will receive the event. To avoid it, you have to overload the event in child application, and you can call also the parent version if need.

	-- /Games script application
	function onConnection(client,...)
		return www:onConnection(client,...)
	end

Note The keyword super is supported to refer to the the father application:

	-- /Games script application
	function onConnection(client,...)
		return super:onConnection(client,...)
	end

You can use client.path property to check if it's a client connected for this application or for one child application (see Server Application, API page for more details on client object description).

Exchange between unrelated server applications

In class inheritance, parent class has no knowledge of its children. However, here a parent server application can access for an child variable or function in checking before its existence. For example if /Games application would like to call a load function in /Games/myGame application, it have to check myGame existence, if myGame returns nil, it means that myGame doesn't exist or is not yet started.

	-- /Games script application
	if myGame then myGame:load() end
	-- /Games/myGame script application
	function load() end
		...
	end

By the same way, any applications can do the same thing with any other applications, even without hierarchical relationship.

	-- /myApplication script application
	if Games then
		if Games.myGame then Games.myGame:load() end
	end
	-- /Games/myGame script application
	function load() end
		...
	end

Communication between server and client

Pull data, Remote Call Procedure

You have to define your RPC functions as a member of client object gotten on connection, its signature will be exactly same on client and server side. It can take multiple parameters, and it can return one result.

	function onConnection(client,...)
		function client:test(name,firstname)
			return "Hello "..firstname.." "..name
		end
	end
	_netConnection.client = this
	_netConnection.call("test",new Responder(onResult,onStatus),"Client","Test")

	function close():void { _netConnection.close() }
	function onStatus(status:Object):void {
		trace(status.description)
	}
	function onResult(response:Object):void {
		trace(response) // displays "Hello Test Client"
	}

Warning When you change default client of NetConnection, the new client should have a close() method which closes the connection, because a RTMFP Server can call this function in some special cases

Note that returned result of the scripting function is a writing shortcut for:

	function client:test(name,firstname)
		client.writer:writeAMFResult("Hello "..firstname.." "..name)
	end

The both make exactly the same thing.

If the function is not available on the client object, it returns a NetConnection.Call.Failed status event with Method 'test' not found in description field. But you can also customize your own error event:

	function client:test(name,firstname)
		if not firstname then error("test function takes two arguments") end
		return "Hello "..firstname.." "..name
	end
	_netConnection.client = this
	_netConnection.call("test",new Responder(onResult,onStatus),"Client");

	function close():void { _netConnection.close() }
	function onStatus(status:Object):void {
		trace(status.description) // displays "..main.lua:3: test function takes two arguments"
	}
	function onResult(response:Object):void {
		trace(response)
	}

Push data

Push data mechanism is also possible in using client.writer object.

	client.writer:writeAMFMessage("onPushData","Application","Test")
	function onPushData(name:String,firstName:String):void {
	}

Here an example of push data every two seconds (see Events part of Server Application, API page for onManage event description):

	writers = {}
	function onConnection(client,...)
		writers[client] = client.writer
	end
	function onDisconnection(client)
		writers[client] = nil
	end
	function onManage()
		for client,writer in pairs(writers) do
			writer:writeAMFMessage("refresh")
		end
	end
	function refresh():void {...}

client.writer returns the main flowWriter of this client. A FlowWriter is an unidirectional communication pipe, which allows to write message in a fifo for the client. Each flowWriter has some statistic exchange informations. When you want push a constant flow with a large amount of data, or if you want to get independant exchange statistics without disrupting the main flowWriter of one client, you can create your own flowWriter channel to push data:

	writers = {}
	function onConnection(client,...)
		writers[client] = client.writer:newFlowWriter()
	end
	function onDisconnection(client)
		writers[client] = nil
	end
	function onManage()
		for client,writer in pairs(writers) do
			writer:writeAMFMessage("refresh")
		end
	end
	function refresh():void {...}

When you create your own flowWriter, you can overload its onManage function, allowing you to write the same thing in a more elegant way, which avoid here writers table usage, and make the code really more short (see Objects part of Server Application, API page for more details).

	function onConnection(client,...)
		writer = client.writer:newFlowWriter()
		function writer:onManage()
			self:writeAMFMessage("refresh")
		end
	end
	function refresh():void {...}

If you have need of pushing rate greater than two seconds, use onRealTime event of root application (see Events part of Server Application, API page for more details).

AMF and LUA types conversion

A conversion between AMF and LUA types allows to read AMF data from client, and write AMF data to client.

Complex types

Primitive conversion types are easy and intuitive (Number, Boolean, String). Except these primitive types, in LUA all is table. Concerning AMF complex type conversions, things go as following:

	-- LUA table formatted in Object          // AMF Object 
	{x=10,y=10,width=100,height=100}          {x:10,y:10,width:100,height:100}          

	-- LUA table formatted in Array           // AMF Array
	{10,10,100,100}                           new Array(10,10,100,100)

	-- LUA table mixed                       // AMF Array associative
	{x=10,y=10,100,100}                      var mixed:Array = new Array(10,10,100,100);
						 mixed["x"] = 10;  mixed["y"] = 10;

	-- LUA table formatted in Dictionary     // AMF Dictionary
	{10="test","test"=10,__size=2}           var dic:Dictionary = new Dictionary();
						 dic[10] = "test";  dic["test"] = 10;

	-- LUA table formatted in ByteArray      // AMF ByteArray
	{__raw="rawdata"}                        var data:ByteArray = new ByteArray();
						 data.writeUTFBytes("rawdata");

	-- LUA Table formatted in date           // AMF Date
	{year=1998,month=9,day=16,yday=259,      new Date(905989690435)
	wday=4,hour=23,min=48,sec=10,msec=435,
	isdst=false,__time=905989690435}       

On a LUA to AMF conversion, priortiy conversion order works as following:

  1. If the LUA table given contains the property __raw, it's converted to a ByteArray AMF object.
  2. If the LUA table given contains the property __size, it's converted to a Dictionary AMF object.
  3. If the LUA table given contains the property __time, it's converted to a Date AMF object.
  4. Otherwise it chooses the more adapted conversion (Object, Array, or Array associative).

About __time property on a date object, it's the the number of milliseconds elapsed since midnight UTC of January 1 1970 (Unix time).

About Dictionary object, LUA table supports an weak keys table feature, and it's used in AMF conversion with the weakKeys contructor argument of Dictionary AMF type. It means that if you build an AMF Dictionary object with weakKeys equals true and send it to CumulusServer, CumulusServer converts it in a LUA table with weak keys, and vice versa.

Note Actually Cumulus supports all AMF0 and AMF3 format, excepts Vector and XML types.

Custom types

You can custom your object in using typed object feature. Indeed, when a typed object is unserializing, a onTypedObject application event is called.

On client side, the AS3 class flag RemoteClass have to be added:

	[RemoteClass(alias="Cat")]
	public class Cat {
		public function Cat () {
		}
		public function meow() {
			trace("meow")
		}
	}

On reception of this type on script-server side, it will call our onTypedObject function, and you can custom your object:

	function onTypedObject(type,object)
		if type=="Cat" then
			function object:meow()
				print("meow")
			end
		end
	end

object second argument contains a __type property, here equals to "Cat" (also equals to the first argument of typeFactory function). It means that if you want create a typed object from script-side, and send it to client, you have just to add a __type property.

	function onConnection(client,...)
		response:write({__type="Cat"})
	end

Cient will try to cast it in a Cat class.

You can go more further on this principle, and custom the AMF unserialization and serialization in adding __readExternal and __writeExternal function on the concerned object, it relates AS3 object which implements IExternalizable on client side (see IExternalizable). For example, ArrayCollection is an externalizable type, and it's not supported by default by the conversion system, you can add its support in adding this script code:

	function onTypedObject(type,object)
		if type=="flex.messaging.io.ArrayCollection" then
			function object:__readExternal(reader)
				self.source = reader:readAMF(1)
			end
			function object:__writeExternal(writer)
				writer:writeAMF(self.source)
			end
		end
	end

reader and writer arguments are equivalent of IDataOutput and IDataInput AS3 class (see Objects part of Server Application, API page for more details).

LUA extensions and files inclusion

LUA can be extended easly, it exists LUA extensions for all needs and on all operating system. Add some new CumulusServer abilities as SQL, TCP sockets, or others is a common thing make possible by LUA. Install your LUA extension library, and add a require line for your script. LUA will search the extension in some common location related with LUA folder installation.

Now if you have need to organize your code in different files for your server application, you can use absolutePath(path) helpful functions on cumulus object (see Objects part of Server Application, API page for more details), in addition of dofile or loadfile LUA functions (see this LUA page). It allows to convert path application for an absolute version.

	function onStart(path)
		dofile(cumulus:absolutePath(path).."start.lua")
	end
	function onConnection(client,...)
		dofile(cumulus:absolutePath(path).."connection.lua")
	end

Warning If you edit a included file (like start.lua or connection.lua here), change are not taken as far as dofile is not called again or main.lua of this server application is not updated again.

LOGS

LUA print function writes text on the output console of CumulusServer in a non-formatted way. Also, in service or daemon usage, nothing is printed of course. Solution is to use logs macro make available in scripts.

  • ERROR, an error.
  • WARN, a warning.
  • NOTE, an important information, displayed by default in Cumulus log files.
  • INFO, an information, displayed by default in Cumulus log files.
  • DEBUG, displayed only if you start CumulusServer with a more high level log (see /help or --help on command-line startup)
  • TRACE, displayed only if you start CumulusServer with a more high level log (see /help or --help on command-line startup)
	function onStart(path)
		NOTE("Application "..path.." started")
	end
	function onStop(path)
		NOTE("Application "..path.." stoped")
	end

API

Complete API is available on Server Application, API page.