sidebar_position |
---|
30 |
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.
Maven:
<dependency>
<groupId>org.drasyl</groupId>
<artifactId>drasyl-extras</artifactId>
<version>0.9.0</version>
</dependency>
Other dependency managers:
Gradle : compile "org.drasyl:drasyl-extras:0.9.0" // build.gradle
Ivy : <dependency org="org.drasyl" name="drasyl-extras" rev="0.9.0" conf="build" /> // ivy.xml
SBT : libraryDependencies += "org.drasyl" % "drasyl-extras" % "0.9.0" // build.sbt
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.
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.
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.
Once we created the remote implementation, we need to bind the remote object to 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
// highlight-start
final RmiServerHandler server = new RmiServerHandler();
// highlight-end
// 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();
// highlight-start
p.addLast(new RmiCodec());
p.addLast(server);
// highlight-end
}
})
.childHandler(/* code */);
}
}
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 */
// highlight-start
// create remote object
final MessengerService service = new MessengerServiceImpl();
// bind to server
server.bind("MessengerService", service);
// highlight-end
}
}
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 */;
// highlight-start
// create client
final RmiClientHandler client = new RmiClientHandler();
// highlight-end
// 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();
// highlight-start
p.addLast(new RmiCodec());
p.addLast(client);
// highlight-end
}
})
.childHandler(/* code */);
}
}
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 */
// highlight-start
// 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();
}
});
// highlight-end
}
}
A fully working example can be found here.
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 {
// highlight-start
@RmiCaller
private DrasylAddress caller;
// highlight-end
@Override
public Future<String> sendMessage(final String clientMessage) {
// highlight-start
System.out.println("Called by: " + caller);
// highlight-end
/* 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.
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 {
// highlight-start
@RmiTimeout(5_000L)
// highlight-end
@Override
public Future<String> sendMessage(final String clientMessage) {
/* code */
}
}
This page is an adapted version of the Java RMI tutorial by Baeldung.