Extensions
From EsWiki
The server is highly extensible using what are called extensions. An extension is a collection of one or more ActionScript or Java files/classes that are used to add more functionality to the server. These extensions can be used as server-level event handlers, such as the Login Event Handler, as Managed Objects which come in handy for things like database connection pooling, or as Plugins. Plugins are run at the server level or at the room level and are frequently used to execute game logic.
Contents |
Overview
When developing multi-user applications, it's often useful or even necessary to push some logic to the server rather than the client. There are many good reasons for doing this but here are a few of the most important:
- Security – The server is significantly more secure than the client.
- Performance – In most cases, the server will provide much higher performance than the client.
- Manageability – Maintaining code deployed on the server is simpler than maintaining code deployed on many clients.
- Resolving Conflicts – Making important decisions in one central location allows the clients to agree.
- Maintaining State – Keeping track of the application state ensures that new users entering can properly view the state.
With all these reasons, it seems that most logic should be pushed to the server, but this isn't the case at all. If you push everything to the server, then it will need to do that same work for each client. Ultimately, while the server might be faster for a few calls, it will get bogged down with many hundreds of thousands of calls. Because of this, it's critical to know what should and should not be done on the server.
Fortunately, there are some rules of thumb on what should be handled by the server that can be used to make this decision easier:
- Anything that affects the game state directly
- Anything that is critical to game-play
- Any area of an application where clients might disagree on the outcome of an action
- Any area of an application where there are concerns for security
Below are a few examples what should be handled on the server:
- In an RPG, determining if the player was hit by the monster and calculating how much damage and remaining health is left to the player
- Picking up an object in the world if the object is important to other players
- Any place where cheating could be done by changing strength, angle, speed or any other variable in the client
Types of Extension Components
To support adding this custom logic, ElectroServer 4 includes the concept of extensions. An extension is a grouping of code used to extend the server. Extensions only run on the registry or stand-alone server but never on gateways. There are multiple ways to extend the core functionality of ElectroServer, described in the following sections.
Plug-ins
A plug-in is the most generic and flexible way to add functionality to ElectroServer. Simply put, a plug-in is a piece of code that can be tied to the server itself or a room and can then be called via clients to directly ask it to perform an action. As a special case, room-level plug-ins have the ability to listen to many events that occur in a room such as room variable changes or public messages. These are the bread and butter of multi-player game development.
See the installation directory/server/examples folder for multiple examples of plug-ins, and the Plugins article.
Managed Object Factories
A managed object factory is a special case object that can be used to create objects that need to be tracked over time. Any piece of code in an extension can call a managed object factory and request or return an object to it. An excellent use for managed object factories is accessing a database connection pool. In this case, the connection itself is the managed object and the factory is the pool manager.
See the ConnectionPool example in installation folder/examples/ConnectionPool . To use this in your own extension you will need to edit the ConnectionPool's Extension.xml file to provide the correct url, user, and password information for connecting with your database. The database driver will need to go in the extension's lib folder. For example, a mysql database would need both DBPool_v4.8.3.jar and mysql-connector-java-5.0.4-bin.jar in the lib folder, and this section of ConnectionPool's Extension.xml would need to be edited:
<Variable name="poolname" type="string">mysqlpool</Variable>
<Variable name="url" type="string">jdbc:mysql://localhost:3306/db</Variable>
<Variable name="user" type="string">username</Variable>
<Variable name="password" type="string">password</Variable>
Replace the jdbc:mysql://localhost:3306/db, username, and password with the correct information for your specific database. Merge the ManagedObjects section of the Extension.xml with the rest of the parts needed for your extension.
Any plugin or event handler in the same extension will be able to use lines such as these to connect to the database:
EsObject esDB = new EsObject();
esDB.setString("poolname","mysqlpool");
Connection c = (Connection)getApi().acquireManagedObject("ManagedConnectionPoolExample", esDB);
If you rename the handle for the ManagedObject in Extensions.xml, you will need to edit the above line to match.
Event Handlers
Event handlers are used to implement specific code. All of the available event handlers are covered in the following sections.
Login Event Handler
A login event handler is used to provide custom logic during the login process. The login event handler is designed to allow a developer complete control over how a user logs into the server. Multiple login event handlers can be tied to the server at one time. The order in which they are specified dictates their execution order. Each login event handler has the ability to fail the login at any time. A very common use for a login event handler is to look up a username and password in the database before letting the user login to the server.
See LoginEventHandler in the installation directory/server/examples folder for an example. After creating a login event handler, you will need to use the web admin to add it as a server level component of the extension it is in.
Note that during the executeLogin method, the user is not yet logged in, and not yet in a room. If you need to set a user server variable or permission set for this user, you will need to set it in the context variable.
Logout Event Handler
A logout event handler is used to execute code when the user logs out or disconnects from the server. Unlike the login event handler, these do not work in a chain. Each one is fired in the order specified but with no ties to their peer handlers. A common use for a logout event handler is to track the time the user spent logged in by updating a database with the exit time.
User Variable Event Handler
A user variable event handler is used to listen for changes to user variables. It has the ability to perform an action when a user variable is updated, deleted, or created. A good use for this type of handler is to persist the variable to a database table so when a user next logs in, a login event handler can automatically create the user variables for the user.
Buddy List Event Handler
A buddy list event handler is used to execute custom logic when a user adds, removes, or updates a buddy on their buddy list. Like user variable event handlers, these are most commonly used to store the data to the database. Not yet implemented.
Private Message Event Handler
A private message event handler is used to listen for private messages sent between users and is most often used for message logging or custom filtering. Not yet implemented.
Extension Lifecycle Event Handler
An extension life cycle event handler is a special case event handler that is tied to a given extension, not to the server itself. It listens for both the extension start up and shut down notifications. It is generally used to initially load configuration data for the extension and provide that data to other components in the extension.
Audio/Video Event Handler
An audio/video event handler is another multi-purpose event handler. It is used to approve someone attempting to publish or subscribe to a video stream much like a login event handler. It also serves as a notification that a stream has stopped playing or that a user is no longer subscribing.
Extension Structure and Deployment
Extensions follow a common directory structure regardless of what language is used to create them. In fact, different languages can be matched inside of a single extension. The structure looks like this:
<extension name>
<extension name>/config
<extension name>/classes
<extension name>/lib
<extension name>/scripts
<extension name>/Extension.xml
Everything for an extension goes in a single folder which is then placed in the installation directory/server/extensions folder. It's recommended that the extension name for this folder is used, but it isn’t necessary as the Extension.xml file (described below) contains the extension name the server will use. Technically the Extension.xml file is the only thing required to create an extension, but wouldn’t be a very useful extension by itself.
The config folder is optional and is used to contain any configuration files specific to your extension. The contents of the configuration folder will be added to the classloader for this extension. A good use for the config folder is the applicationContext.xml file used for Spring or HBM files used for Hibernate.
The lib folder is optional and contains any libraries you need for your extension. In practice, these will be jars and other dependencies. They will all be added to this extension's classloader instance. If you package your entire extension into a jar, it should be placed here.
The scripts folder is optional and contains all the script files needed by the extension. These files can be in any language the server supports.
The classes folder is optional and is the equivalent to the scripts folder but for Java classes. Any compiled classes will go in this folder as it will be added directly to the classloader. Alternatively, you can jar your classes and place the jar in the lib folder.
Extension.xml
This file defines the contents of the extension and how it should be loaded by the server. The file is broken down into a series of sections that define each component of the extension. The layout looks like this:
<Extension>
<Name>...</Name>
<ManagedObjects>
<ManagedObject>…</ManagedObject>
</ManagedObjects>
<EventHandlers>
<LoginHandlers>
<LoginHandler>…</LoginHandler>
</LoginHandlers>
<LogoutHandlers>
<LogoutHandler>…</LogoutHandler>
</LogoutHandlers>
</EventHandlers>
<Plugins>
<Plugin>…</Plugin>
</Plugins>
</Extension>
Each section, except the Name section, supports multiple entries. For example, many plug-ins could exist inside the Plugins node as long as each is enclosed in its own Plugin node. The actual data definition of the extension component is the same for all components and would look like this for a Java plug-in (if it had all options enabled):
The Name section contains the name of the extension. This can be any string, but it is useful to name it something related to the extension. This name will be shown in the Web based administrator from the Extensions tab.
<Plugin>
<Handle>ExampleJavaPlugin</Handle>
<Type>Java</Type>
<Path>com.electrotank.electroserver4.testextension.SpringTest</Path>
<Synchronized>true</Synchronized>
<Variables>
<Variable name="Variable1Name" type="string">variable 1 value</Variable>
<Variable name="Variable2Name" type="string">variable 2 value</Variable>
</Variables>
</Plugin>
Each node breaks down as indicated below.
- Handle
- This is the "handle" of the component and is used as a name for the component's definition. When creating a new instance of a component, the handle is needed for the server to identify which component is being referenced.
- Type
- The type node defines what language was used to create the component. It currently must be either Java or ActionScript (with other languages soon to follow in future versions).
- Path
- This is the path to the location of the component’s code. In the case of a scripting-language component, this is a relative path from the scripts directory of the extension. In the case of a Java component, this will be the fully-qualified path.
- Synchronized (optional)
- This node is used to determine if the server should automatically synchronize all access to the component. In the case of Java components, this is often a poor approach because the server must synchronize aggressively due to the fact that it doesn’t know the details of the component, only the entry points. Generally a developer can synchronize the component more efficiently on their own. In the case of an ActionScript component, this flag is crucial because ActionScript doesn’t directly support the ability to synchronize threads.
- Variables (optional)
- All components in ElectroServer 4 support a series of life cycle methods that include being initialized and destroyed. The variables node allows configuration information to be passed into the component’s initialize method directly as an EsObject. Each variable entry will become a single variable in the EsObject. The name attribute will be the name of the variable and the type will be the data type used. All data types are supported with the exception of arrays.
As noted before, every component uses the same structure in the Extension.xml file. Here is an example ActionScript login event handler without variables:
<LoginHandler>
<Handle>ExampleActionScriptEventHandler</Handle>
<Type>ActionScript</Type>
<Path>login/AsLoginHandler.as</Path>
<Synchronized>true</Synchronized>
</LoginHandler>
Remote Deployment
To deploy the extension locally, you simply copy the needed files to the installation directory/server/extensions folder as explained above. This is awkward if your server is remote, so the web based administrator allows you to upload an extension as a zip file.
- Build the extension in a folder, anywhere on your hard drive.
- Create a zip file. You want to include subfolders, do not save full path info, and add with wildcards from the top level of the folder (where Extension.xml is). Name the zip file the name of the extension.
- On the web based administrator's Extensions tab, browse to find the zip file, then click the Upload button.
- Notice that the Extension tab now lists "Uploaded Extensions (Pending Reboot)". If you look in the server/extensions folder (remotely) you will see the zip file, still zipped.
- Use the web based administrator to reboot the server. Notice that the Extension tab now shows the extension with its proper name.
- If the extension needs one or more server-level components, add them now then reboot.
Extension Loading and Start-Up/Shut-Down Process
Extensions are sophisticated so it’s important to describe how they are loaded and the order in which various components will execute.
Each extension is loaded with its own classloader as part of the overall classloader hierarchy to ensure separation between extensions and the server. On the surface, this may seem an unnecessary complexity, but it provides the most flexibility and convenience for extension developers. For instance, by using a separate hierarchy we are able to ensure that extensions don’t interfere with each other. This allows the server to run multiple instances of the same extension in parallel or even different versions of the same extension at the same time. This also ensures that if one extension uses a different version of the same library there will be no conflict between them.
The server loads all extensions together at start up but in no specific order. Any initialization process that throws an error will prevent the server from starting the external listeners. This is to ensure that a secure system isn’t brought online in an insecure manner accidentally. All start up errors will be in the log files as well as in the web-based administrator.
In practice, this is a very valuable attribute. For instance, let’s assume a multiplayer game uses a database to authenticate players. A login event handler uses a managed object factory to get access to the database connection. The managed object factory uses its initialization method to create the connection pool initially. On start up, a connection string is incorrect and the factory can’t connect to the database. Rather than starting the server and possibly letting users log in without being authenticated or logging errors every time the login event handler runs, the server simply flags the extension as an error and ensures the external listeners don't start.
The server loads each extension sequentially in an undetermined order. Each extension runs through the following process:
- The extension life cycle event handler is constructed and initializes.
- Managed object factories are constructed and initialized in the order they are defined.
- Server-level plug-ins are constructed and initialized in the order they are defined.
- Login event handlers are constructed and initialized in the order they are defined.
- Logout event handlers are constructed and initialized in the order they are defined.
- Private message events are constructed and initialized in the order they are defined.
- User variable event handlers are constructed and initialized in the order they are defined.
- Audio/video event handlers are constructed and initialized in the order they are defined.
Only when all extensions have started successfully does the server begin opening external listeners and allow users to connect. As stated before, any exceptions while initializing will prevent listeners from coming online and will be displayed in the logs and as start up errors in the web-based administrator.
The extension shut down process works in exact reverse of the start-up process with the exception that while errors are logged, they do not stop the process from continuing. The only two things that can cause an extension to shut down are the server getting shut down (registry or stand-alone) or the extension reloading which is described in the following section.
Extension Reloading Process
Note: This is not working fully yet. A server restart is the only way to guarantee a proper reload today.
Extensions support the ability to dynamically reload if any changes are made to the contents of the extension directory. Reloading an extension is a multi-step process that warrants a more detailed description.
- The server scans the extension directory looking for changes to existing extensions or newly added extensions.
- If a change is found, the server rescans the extension on short intervals waiting for changes to settle out. This is to prevent an extension from being deployed while it is still being copied.
- The extension is copied into the server’s temporary directory. If the extension is zipped up currently, it is extracted during the copy.
- If the extension is a duplicate of an existing extension, the existing extension is shut down via the process described previously. To ensure the integrity of the system and prevent data mismatch problems with the new extension, some users must be disconnected, according to the following rules:
- Any user that has gone through a login event handler associated with this extension is disconnected. This is to ensure that they will be forced to login through the newly deployed extension.
- Any user that has had an associated extension-bound user-server-variable via the extension will be disconnected. This is necessary because these users could have had an object associated with them that may not be compatible with the new extension code base.
- Any user that is in a room and has a room-level plug-in from the extension will be kicked from the room and the room will be destroyed. This is to ensure the room can be re-created with the new code.
- The new extension is started up using the procedure described above.
Server API
ElectroServer 4 Java API is used by developers to implement extensions. Any server-side, custom-code in any langauge supported will have access to all methods defined in this interface. Any specific extension component from plugin to event-handler will be able to access a specific implementor of the ElectroServerApi by calling the getApi() method on the component. The api will be available to the component by the time the lifecycle "init" method is invoked.
See also Server-level Plugin.
