Remote Message Invocation
This page shows you how to invoke remote objects served by other drasyl nodes. This feature is similar to Java Remote Message Invocation but uses drasyl as the transport rather than TCP.
To use this feature, you have to use the bootstrapping interface, where you have to customize the server channel's ChannelInitializer.
Add Dependencyβ
Maven:
<dependency>
<groupId>org.drasyl</groupId>
<artifactId>drasyl-extras</artifactId>
<version>0.10.0</version>
</dependency>
Other dependency managers:
Gradle : compile "org.drasyl:drasyl-extras:0.10.0" // build.gradle
Ivy : <dependency org="org.drasyl" name="drasyl-extras" rev="0.10.0" conf="build" /> // ivy.xml
SBT : libraryDependencies += "org.drasyl" % "drasyl-extras" % "0.10.0" // build.sbt
Creating the Serverβ
There are two steps needed to create a remote message invocation (RMI) server:
- Create an interface defining the client/server contract.
- Create an implementation of that interface.
Defining the Contractβ
First of all, let's create the interface for the object want to invoke remotely.
As drasyl is asynchronous, each method declared in the interface must have the return
type Future
or void
.
import io.netty.util.concurrent.Future;
public interface MessengerService {
Future<String> sendMessage(final String clientMessage);
}
Note, though, that drasyl supports the full Java specification for method signatures, as long as the Java types are serializable by Jackson. We'll see in future sections how both the client and the server will use this interface. For the server, we'll create the implementation, often referred to as the Remote Object. For the client, the we will dynamically create an implementation called a Stub.
Implementationβ
Furthermore, let's implement the remote interface, again called the Remote Object:
import io.netty.util.concurrent.*;
public class MessengerServiceImpl implements MessengerService {
@Override
public Future<String> sendMessage(final String clientMessage) {
final String result = "Client Message".equals(clientMessage) ? "Server Message" : null;
return new SucceededFuture<>(ImmediateEventExecutor.INSTANCE, result);
}
public String unexposedMethod() { /* code */ }
}
Notice that any additional methods defined in the remote object, but not in the interface, remain invisible for the client.
Registering the Serviceβ
Once we created the remote implementation, we need to bind the remote object to a RMI server.
Creating a RMI Serverβ
First, we need to create a drasyl node that contains a RMI server serving our remote object:
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import org.drasyl.channel.*;
import org.drasyl.handler.rmi.*;
import org.drasyl.identity.Identity;
public class RmiServer {
public static void main(final String[] args) {
final Identity identity = /* code */;
// create server
final RmiServerHandler server = new RmiServerHandler();
// bootstrap node with server added to the pipeline
final ServerBootstrap b = new ServerBootstrap()
.group(new DefaultEventLoopGroup())
.channel(DrasylServerChannel.class)
.handler(new TraversingDrasylServerChannelInitializer(identity, new NioEventLoopGroup(1), 22527) {
@Override
protected void initChannel(final DrasylServerChannel ch) {
super.initChannel(ch);
final ChannelPipeline p = ch.pipeline();
p.addLast(new RmiCodec());
p.addLast(server);
}
})
.childHandler(/* code */);
}
}
Binding the Remote Objectβ
We can now create and bind our remote object to the RMI server. Each binding is identified by a unique key.
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import org.drasyl.channel.*;
import org.drasyl.handler.rmi.*;
import org.drasyl.identity.Identity;
public class RmiServer {
public static void main(final String[] args) {
/* code */
// create remote object
final MessengerService service = new MessengerServiceImpl();
// bind to server
server.bind("MessengerService", service);
}
}
Creating the Clientβ
Finally, let's write the client to invoke the remote object's methods.
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import org.drasyl.channel.*;
import org.drasyl.handler.rmi.*;
import org.drasyl.identity.Identity;
public class RmiClient {
public static void main(final String[] args) {
final Identity identity = /* code */;
// create client
final RmiClientHandler client = new RmiClientHandler();
// bootstrap node with client added to the pipeline
final ServerBootstrap b = new ServerBootstrap()
.group(new DefaultEventLoopGroup())
.channel(DrasylServerChannel.class)
.handler(new TraversingDrasylServerChannelInitializer(identity,new NioEventLoopGroup(1), 0) {
@Override
protected void initChannel(final DrasylServerChannel ch) {
super.initChannel(ch);
final ChannelPipeline p = ch.pipeline();
p.addLast(new RmiCodec());
p.addLast(client);
}
})
.childHandler(/* code */);
}
}
Lookup for the Remote Objectβ
We can now look up the remote object using the bounded unique key and the address of the RMI server's node.
And finally, we'll invoke the sendMessage
method:
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.util.concurrent.FutureListener;
import org.drasyl.channel.*;
import org.drasyl.handler.rmi.*;
import org.drasyl.identity.*;
public class RmiClient {
public static void main(final String[] args) {
/* code */
// lookup
final DrasylAddress serverAddress = /* code */;
final MessengerService service = client.lookup("MessengerService", MessengerService.class, serverAddress);
// invoke
service.sendMessage("Client Message").addListener((FutureListener<String>) future -> {
if (future.isSuccess()) {
System.out.println("Succeeded: " + future.getNow());
}
else {
System.err.println("Errored:");
future.cause().printStackTrace();
}
});
}
}
Exampleβ
A fully working example can be found here.
Further Readingβ
Who Called Me?β
You may be interested in getting to know who called you. For this, you must add a field of
type DrasylAddress
and annotate it with RmiCaller
. drasyl will then inject the current caller to
this variable before every invocation.
import io.netty.util.concurrent.*;
import org.drasyl.handler.rmi.annotation.RmiCaller;
import org.drasyl.identity.DrasylAddress;
public class MessengerServiceImpl implements MessengerService {
@RmiCaller
private DrasylAddress caller;
@Override
public Future<String> sendMessage(final String clientMessage) {
System.out.println("Called by: " + caller);
/* code */
}
}
Note to save the caller
value when doing asynchronous operations. Otherwise, it might be possible
that a subsequent invocation has already changed the caller
field.
Adjust Timeoutβ
By default, all invocations will timeout after 60 seconds by completing the future exceptionally with a RmiException
.
But you can customize this value per class and method.
To do so, just add the annotation RmiTimeout
to your implementation class or method.
A class annotation will override the default value, while a method annotation will override any class annotation.
import io.netty.util.concurrent.Future;
import org.drasyl.handler.rmi.annotation.RmiTimeout;
public class MessengerServiceImpl implements MessengerService {
@RmiTimeout(5_000L)
@Override
public Future<String> sendMessage(final String clientMessage) {
/* code */
}
}
This page is an adapted version of the Java RMI tutorial by Baeldung.