This is the documentation of the Jnag library.
- 1 Introduction
- 2 Workflow
- 3 Library's modules
- 4 Proxies and exposed objects
- 4.1 Static binding
- 4.2 Dynamic binding
- 4.2.1 Example
- 4.2.2 Proxies and exposed object's life cycle
- 5 Network Interface
- 5.1 Local and Remote
- 5.2 HostKind.Client and HostKind.Server
- 5.3 @Log and @NoLog
- 5.4 @CallerObject
- 5.5 @Singleton
- 5.6 @BindToLocalId and @BindToSharedId
- 5.7 @ImplementedBy
- 5.8 View<T>
Introduction
Jnag is a remoting library. Fundamentally, it provides its users an easy way to encode and decode procedure calls (procedures are functions whose return type is void). This functionality is commonly called Remote Procedure Call (RPC). However, Jnag is doing it differently from the other library in many points, which are explained in this documentation.
Workflow
Jnag is operating on a peer-to-peer model. The communication is normally always between 2 entities. The object which handle the peer-to-peer communication on each entity is the "Jnag session".
Note: There is a special way of using Jnag to make it operate in a peer-to-group way. This will be explained later in the documentation.
Jnag is providing Object Oriented Remote Procedure Call (OO-RPC), which means that any call from a host A to a host B is implicitly or explicitly composed of the following informations:
- The identification of an destination object on the host B which will receive the call.
- The identification of the procedure call.
- The procedure's parameters.
Because Jnag is designed to be easy to use, the procedure calls from the host A to the host B are made via proxies. Proxies are objects on the host A that represent the objects on the host B using a 1-to-1 relationship. From the host A's point of view, they have the same set of procedures than their counterpart on the host B. When a user call one of them, the proxy object intercept the call, encode it into a binary stream, and the Jnag session transmits the binary data to an underlying pluggable message transfer layer.
Once the message is delivered to the host B, the Jnag session decodes the binary message and transforms it into a procedure call on the destination object, with the decoded parameters.
Note: The message transfer layer is provided by the user at the setup of a Jnag session. It is ensuring the transfer of the data to the remote host.
Library's modules
Jnag is composed of a set of projects (libraries, tools and plugins):
- Jnag Model
A library containing the annotations used for the network interfaces. - Jnag Annotation Processor
An annotation processor tool which parse the network interfaces and generate the code for the proxies and their corresponding interfaces. - Jnag Maven plugin
A Maven plugin, to integrate the annotation processor tool in a Maven build. - Jnag Jse Common
A library used internally by Jnag, contains some classes that are common between the different Jse implementation of Jnag (i.e. the "Jnag Jse" and "Jnag Red Dwarf" projects). - Jnag Jse
The Jnag library for the Jse platform. - Jnag Red Dwarf
The Jnag library for the Red Dwarf platform. - Jnag utils for Red Dwarf Jse clients
Some optional utility classes for users of the "Jnag Jse" project. - Jnag utils for Red Dwarf servers
Some optional utility classes for users of the "Jnag Red Dwarf" project.
Proxies and exposed objects
When we want a host A to be able to call an object on a host B, we need this object to be "exposed" to the host A, i.e. we want to declare it as callable from the host A. This is done by registering it to the Jnag session on the host B. In fact, when we do it, we also ask Jnag to assign an identifier to the object, so that the host A has a way to specify this object as a destination using that identifier.
The function to expose an object to the remote host and bind it to an identifier is:
public JnagSession {
...
public <T extends Local> void bind(T localObject, Class<T> localInterface);
...
}
The function to unexpose an object to the remote host and unbind it from its identifier is:
public JnagSession {
...
public <T extends Local> void unbind(T localObject);
...
}
On the host A, we also need to create a proxy which will represent a specific exposed object of the host B.
We create a proxy with:
public JnagSession {
...
public <T extends Remote> T createProxy(Class<T> remoteInterface);
...
}
We release a proxy with:
public JnagSession {
...
public <T extends Remote> void releaseProxy(T proxy);
...
}
Static binding
As you can see, the identifier of the exposed object is not given to the user. There is a reason for that: At the establishment of a communication between the host A and the host B, the host A need to know in advance which identifiers the host B will use for it's exposed objects. There is a need for a predefined assignment between objects and identifiers, and this assignment has to be known by both the host A and the host B.
In Jnag, this assignment is defined by an ordered sequence of objects, where the identifier of each object is its sequence number. Thus, the first object bound on the host B will have the identifier 0, the second one will have the identifier 1, etc ... Similarly, on the host A, the first proxy to be created will represent an exposed object of the host B bound to the identifier 0, the second one the identifier 1, etc ...
Dynamic binding
This way to synchronize the proxies of the host A with the exposed object of the host B is only valid when we know the order of the objects in advance. In most of the games, this condition is only valid at the establishment of a connection. This is not valid when the host B start to expose objects in a non-deterministic way. In such situation, the host B has to send the identifier and the type of the newly exposed objects to the host A.
In Jnag, this is simply done by sending to the host A a reference of the newly exposed object 'objectB' on the host B. The user call a procedure on an already-exposed object on the host A and specify the 'objectB' as a parameter. The Jnag session of the host B detects that it the parameter should be passed by reference and encodes its identifier instead of its value. The Jnag session of the host A decodes the procedure call, automatically creates a proxy to represent 'objectB' if it is not already done, and provide it to the user of the library on the host A. The user of the host A now has a reference to a proxy that he can use to call procedures of the 'objectB' on the host B.
Example
On the host A:
public interface A {
public void notifyObjectB1Exposed(B1 b1);
public void notifyObjectB2Exposed(B2 b2);
// In practice, those functions would sound more like:
// "notifyYouAreAttackedBy(Enemy enemy);" or
// "notifySomeoneSpeakToYou(Player player, String message);"
}
void initJnagSession() {
// This is the initialization time.
JnagSession jnagSession = new JnagSession();
...
// This object is created and exposed so that the host B can call it.
AImpl objectA = new AImpl();
// Static binding to an identifier.
// (beware the order of the calls to the bind function)
jnagSession.bind(objectA, A.class);
// We connect to the host B and wait for something to happen,
// here we want the host B to drive the conversation.
...
}
On the host B:
// This is the initialization time. JnagSession jnagSession = new JnagSession(); ... // In this example, we don't expose objects statically. We wait a little // and will expose either an instance of B1 or an instance of B2. // We do know how to talk to the host A via the class A. A objectAProxy = jnagSession.createProxy(A.class);
Later on the host B:
// Now we want to expose an instance of B2 to the host A. objectAProxy.notifyObjectB2Exposed(b2Impl);
Later on the host A:
public class AImpl implements A {
public void notifyObjectB1Exposed(B1 b1) {
...
}
public void notifyObjectB2Exposed(B2 b2) {
// Starting from now, we can use b2 as a proxy and call its procedures.
...
}
}
Proxies and exposed object's life cycle
Proxies created statically and dynamically both need to be explicitly released once they are not used any more. In practice, proxies on the host A should be removed by the user on the host A according to the information passed in the message from the host B. In other words, proxies' life cycle should be directed implicitly or explicitly by the other host, the one who has the corresponding exposed object.
Exposed objects also have to be unbound from the Jnag session when they are not supposed to be exposed anymore. This should be done after informing the other host (which own the proxies) about it*.
*Note: This part of Jnag will need to be improved in the next releases.
Network Interface
When 2 hosts want to communicate, they first have to define a communication protocol, so that 1 host can expose its objects with its procedures (the receiver side) and another host can call them (the caller side). In Jnag, the language used to define this protocol is Java itself.
We call a network interface a Java interface annotated with the annotation @NetworkInterface, which is used to define the format of the procedures of a class of objects on the caller side and the one on the receiver side. Those 2 formats are not necessary the same.
Example of network interface:
@NetworkInterface(HostKind.Server)
public interface EchoServer {
public void requestSaySomethingToEcho(String message);
}
Local and Remote
The network models are not supposed to be used directly in any program. Instead, they serve as an input for a tool (the annotation processor of Jnag) which generates some similar Java interfaces for each of the 2 hosts.
The network interface from the example above will be used to generate one Java interface on each host. The caller side will have a generated interface that will be used to make procedure calls (the proxies will implement it), and the receiver side will have another generated interface that will be implemented by its own exposed objects.
In order to avoid confusions between the two Java interfaces generated for each side, the code generator tag them differently: The one for the caller side inherits the interface Remote, and the one for the receiver side inherits the interface Local. Those are the types used in the declaration of the bind() / unbind() / createProxy() / releaseProxy() functions previously shown.
For instance, this is what is generated for the caller side:
public interface EchoServer extends Remote {
public void requestSaySomethingToEcho(String message);
}
and this is what is generated for the receiver side:
public interface EchoServer extends Local {
public void requestSaySomethingToEcho(String message);
}
HostKind.Client and HostKind.Server
When we annotate a network interface with @NetworkInterface for a communication between a host A and a host B, we have to tell Jnag on whose host the service is. The reason is that when we generate the source code for the networking stuffs, we have specify to the generator for which host we want to generate the code.
In Jnag, we use the 2 tokens HostKind.Client and HostKindServer to differentiate them.
@Log and @NoLog
This tag can be applied to the network interfaces or their methods. When applied on a network interface, the log flag is implicitly applied to all of the method of the network interface.
Methods marked by @Log are treated specially by the Jnag annotation processor, which generate some code to log the method invocation and reception.
- To only log the invocation of a method, use @Log(Side.Invocation).
- To only log the reception of a method, use @Log(Side.Reception).
When the annotation @Log is applied to a network interface, it is possible to prevent a method to be implicitly logged by using @NoLog on that method.
- To only prevent to log the invocation of a method, use @NoLog(Side.Invocation).
- To only prevent to log the reception of a method, use @NoLog(Side.Reception).
@CallerObject
This annotation is used to identify the caller of a function. This is typically useful for all the services that are exposed to more than 1 client.
There is absolutly no restriction on the type of the caller object. The jnag session just inject the object that has been previously provided to it. It could be an object exposed to the client or not.
Example of usage:
Declaration of the caller object in the network interface:
@NetworkInterface(HostKind.Server)
public interface FriendService {
public void requestAddToMyFriendList(@CallerObject ServerPlayer caller, ServerPlayer newFriend);
}
Setup of the caller object at the initialization of the jnag session on the receiver side:
jnagSession.setCallerObject(serverPlayer);
Generated interface on the caller side:
public interface FriendService extends Remote {
public void requestAddToMyFriendList(ServerPlayer newFriend);
}
Generated interface on the receiver side:
public interface FriendService extends Local {
public void requestAddToMyFriendList(ServerPlayer caller, ServerPlayer newFriend);
}
Example of user implementation on the receiver side:
public class FriendServiceImpl implements FriendService {
public void requestAddToMyFriendList(ServerPlayer caller, ServerPlayer newFriend) {
// We add the friend to the friend list of the player.
((ServerPlayerImpl) caller).addFriend(newFriend);
}
}
@Singleton
By default, each message created by Jnag contains an explicit identifier of the object which is the target of a method call. However, if you tell Jnag that there can only be 1 instance of such type of object per session, it won't add this identifier in the message and the target of the method call will be deducted implicitly by Jnag on the receiver side.
To tag a network interface as been a "per session singleton", use the @Singleton annotation.
Example:
@Singleton
@NetworkInterface(HostKind.Server)
public interface ServerPlayerPrivateInventory {
public void requestAddItem(int itemId);
}
@BindToLocalId and @BindToSharedId
By default, Jnag binds the objects exposed to the session using identifier which are local to the communication between 2 hosts. This is also what the @BindToLocalId annotation is doing, when we want to use it explicitly on a network interface.
In a client-server topology, sometimes we don't want this behaviour for some types of objects on the server. For objects that are accessible by all the clients, we don't want the server to keep a mapping object-id per jnag session as it might take too much memory and may affect the scalability of the server.
For this purpose, Jnag handles different spaces of identifiers: one of them is an identifier space which is global to the host. The identifiers from this space are called "shared id", all the clients refer to their associated object via the same identifier. This way, the server only has to keep 1 mapping object-id, independently of the number of clients (i.e. independently of number of jnag sessions).
Use @BindToSharedId on a network interface to declare that all of its instance should be bound to shared ids.
@ImplementedBy
This annotation is designed to help avoid type casting of a generated local or remote interface to its subtype.
In most of the cases, the user only have 1 implementation of a local interface, and when he wants to refer to the implementation from one of the parameters of a method of a network interface, he has to manually cast it to the type of the implementation.
Example where we have to manually cast:
The network interface:
@NetworkInterface(HostKind.Server)
public interface ServerPlayer {
public void requestSendGold(ServerPlayer receiver, float goldAmount);
}
The generated interface on the receiver side:
public interface ServerPlayer extends Local {
public void requestSendGold(ServerPlayer receiver, float goldAmount);
}
The implementation on the receiver side:
public class ServerPlayerImpl implements ServerPlayer {
private float gold;
public void requestSendGold(ServerPlayer receiver, float goldAmount) {
((ServerPlayerImpl) receiver).gold += goldAmount;
}
}
The annotation @ImplementBy avoid a manual cast when the user only has 1 implementation of the interface. It inform the annotation processor that it must use the subtype in any place where it is possible and where it makes sense.
Example where the user doesn't need to cast anymore:
The network interface:
@NetworkInterface(HostKind.Server)
@ImplementedBy("mygame.server.ServerPlayerImpl")
public interface ServerPlayer {
public void requestSendGold(ServerPlayer receiver, float goldAmount);
}
The generated interface on the receiver side:
public interface ServerPlayer extends Local {
public void requestSendGold(ServerPlayerImpl receiver, float goldAmount);
}
The implementation on the receiver side:
public class ServerPlayerImpl implements ServerPlayer {
private float gold;
public void requestSendGold(ServerPlayerImpl receiver, float goldAmount) {
receiver.gold += goldAmount;
}
}
Here, the subtype is not used on the caller side, as the implementation is not supposed to be known there.
View<T>
Provides support for associating an object on an host A (the viewed object) with an object on an host B (the view object).
On the side of the view, Jnag maintains a mapping between the viewed object and its view, and replace the viewed object by its associated view object at the reception of a message. The result is that on the side of the view, the user doesn't have to manually look for the view object from the viewed object, Jnag does the lookup for him, in an bug free automated view.
In a client-server topology, for scalability (and other) reasons, we want to have the view defined on the client side, as the mapping between the view object and the viewed object is stored on the view side.
Example of usage:
@NetworkInterface(HostKind.Server)
public interface ServerMonster {
...
}
@NetworkInterface(HostKind.Client)
public interface ClientMonster extends View<ServerMonster> {
...
}
Definition of an object that is referring to the ServerMonster:
@NetworkInterface(HostKind.Server)
public interface ServerPlayer {
public void requestHitMonster(ServerMonster monster);
}
Now, the wanted result:
Interface generated on the server side, uses the instance of ServerMonster:
public interface ServerPlayer implements Local {
public void requestHitMonster(ServerMonster monster);
}
Interface generated on the client side, uses the instance of ClientMonster:
public interface ServerPlayer implements Remote {
public void requestHitMonster(ClientMonster monster);
}
From the instance of a ClientMonster, we can get a reference to the accosiated instance of a ServerMonster:
ServerMonster viewedObject = clientMonster.getViewedObject();
A sample illustrating this feature will be released soon.





