What I mean by “comfortable” is this:
- Define your communication protocol in terms of a service interface (plain Java interface).
- A server, implementing the service interface, capable to serve multiple clients at the same time.
- Clients can simply connect using host, port and service interface, and get a proxy with that interface to call methods of the server through it.
Sounds great, right? What we do not want is this:
- A separate service, such as a naming service or RMI repository, to receive the actual location of our server, because we already know it and we are not planning to cluster it.
- A separate language to declare our service interface and another build step to turn those into Java code.
- Any quirky annotations, to describe shit further than we already do with an interface.
So, Java comes with two fully fledged middleware implementations (CORBA and RMI), which are ruled out by the requirements above. But Java also comes with all the essential tools to build this yourself in a few lines of code (399 to be precise).
What we essentially need to communicate with a remote JVM through a Java interface is this:
- Marshalling/Unmarshalling: A tool which converts Java objects into byte-streams and vice versa.
- Low-level Protocol: A set of messages to transfer method invocation parameters to the server and return values and exceptions back to the client.
- TCP Server Subsystem: A subsystem, which opens a wellknown port, waits for incoming connection requests, manages connected clients, and dispatches threads to deal with incoming service requests of those clients.
- Service Delivery: A way to turn incoming requests into method calls at the service implementation (Java object) and return the results.
- Service Proxying: A means to attach an interface to a proxy, which translates local method calls into remote method calls.
Let’s go!
Marshalling/Unmarshalling
- java.io.Serializable
- java.io.ObjectInputStream
- java.io.ObjectOutputStream
Java always had this wonderfully simplistic capability to serialize and deserialize objects. You declare your class to be serializable simply like this:
class MyMessage implements Serializable {
int value_to_be_transfered;
transient boolean value_NOT_to_be_transfered;
}
Done!
All the basic Java types and most of the standard data structures such as lists and hash tables, are already serializable. And you can use the keyword transient
, for attributes, which are not supposed to be serialized.
Now you can serialize/deserialize objects using ObjectOutputStream
/ObjectInputStream
.
For example to write your messages:
ObjectOutputStream oout = new ObjectOutputStream(socket.getOutputStream());
oout.writeObject(serializableObject);
Low-level Protocol
We would like to call methods on our service object. So we basically need its signature (return type and exceptions are not significant for the signature):
class RemoteInvocation {
String methodName;
Object[] parameters;
}
Now, to differentiate between regular return values and exceptions on the way back (btw. we do not consider so-called inout parameters), we need two more message types.
class RemoteException {
Throwable e;
}
class RemoteResult {
Object returnValue;
}
Also done!
It is necessary to explicitly differntiate between exceptions and return values, because you could also use an exception as return value.
TCP Server Subsystem
- java.net.ServerSocket
- java.net.Socket
- java.lang.Thread
Next up is our server sub-system. First, we need an open wellknown port and accept incoming connections in a loop.
wellknown = new ServerSocket(port);
while (running) {
try {
Socket clientSocket = wellknown.accept();
serveClient(clientSocket);
} catch (IOException e) {
log.error("While trying to connect a new client: ", e);
}
}
Here the method serverClient(clientSocket)
just stores the new connection and instantiates a thread to deal with incoming requests on that connection. The request handler method, which is executed by the thread, can look like this:
while (active) {
in = new ObjectInputStream(socket.getInputStream());
Object request = in.readObject();
Object reply = handle(request);
out = new ObjectOutputStream(socket.getOutputStream());
out.writeObject(reply);
}
Done!
Obviously, you have to deal with exceptions. Most importantly EOFException
, which indicates, that the client just closed or lost the connection. And obviously you have to properly manage the connections so you do not end up wasting your memory with closed connections and have a method to orderly shutdown the server. But these things aren’t interesting.
Service Delivery
- java.lang.reflect.Method
This is basically what happens in the method handle(request)
. Here we use Java reflection. We’ve got our service interface, which contains the public methods of our service, and we have got a service object, implementing the methods. So we just need to identify the method using the message we got, call this method, and turn the result (exception or return value) into a reply message.
protected Object handle(Object message) {
RemoteInvocation invokation = (RemoteInvocation) message;
Object reply = null;
try {
Method method = getMethod(invokation.method, invokation.args);
Object result = method.invoke(service, invokation.args);
reply = new RemoteInvocationResult(result);
} catch (InvocationTargetException e) {
// This is an application level exception
reply = new RemoteInvocationException(e);
} catch (Throwable t) {
// This is not an application level exception
log.error("while trying to send a reply", t);
reply = new RemoteInvocationException(t);
}
return reply;
}
Done!
InvocationTargetException is just a wrapper class around an exception thrown by the method. This is used by Java reflections to differentiate between exceptions of the application and those thrown by the reflection API, such as when the method does not exist or cannot be called due to a security manager saying “No!”. We will transfer this wrapper with the application exception to do the differntiation on client side.
Service Proxying
- java.lang.Socket
- java.lang.reflect.Proxy
First we need a TCP connection to our server. This is basically just this:
Socket socket = new Socket(host, port)
Now we get to the really cool part. The reflection API also provides a way to create a proxy, which is wired to a given interface, and forwards method calls at this interface to an InvocationHandler
.
Thus, we create a proxy like this:
public static <T> T connect(Class<T> serviceInterface, String host, int port) throws ... {
return (T) Proxy.newProxyInstance(serviceInterface.getClassLoader(),
new Class<?>[] { serviceInterface },
new MyInvocationHandler(host, port));
}
This invocation handler implements a method called invoke
, which can in our case turn the method call into a remote method call just like this:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// send request
RemoteInvocation request = new RemoteInvocation(method.getName(), args);
ObjectOutputStream stream = new ObjectOutputStream(socket.getOutputStream());
stream.writeObject(request);
// receive reply
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
Object reply = in.readObject();
// classify reply type
if (reply instanceof RemoteInvocationException) {
Throwable e = ((RemoteInvocationException)reply).exeption;
if (e instanceof InvocationTargetException) {
e = e.getCause(); // unwrap application level exception
}
throw e;
} else if (reply instanceof RemoteInvocationResult){
// application level method result -> return it
return ((RemoteInvocationResult)reply).result;
} else {
// error, unknown message type
}
}
This is it!
Of course, error handling needs to be done, but that’s basically the essential part of it.
Application Level
This is an example of what the application level part can look like. It starts by declaring the service interface:
public interface HelloService {
int DEFAULT_PORT = 12345;
String hello(String name);
}
Now we can use this interface to create a proxy on client side and call the service like this:
HelloService service = ServiceConnector.connect(HelloService.class, host, HelloService.DEFAULT_PORT);
String reply = service.hello("Agnes");
And, if the server sub-system is implemented in a class called Service, the service implementation can look like this:
public class HelloServiceImpl extends Service<HelloService> implements HelloService {
public HelloServiceImpl() throws IOException {
super(HelloService.class, HelloService.DEFAULT_PORT);
}
@Override
public String hello(String name) {
return "Hello " + name + "!";
}
public static void main(String[] args) throws IOException {
HelloServiceImpl server = new HelloServiceImpl();
server.run();
}
}
This is how simple it can be with just a few tools provided by the Java runtime library. I wrote this example in one day and thought it might be interesting to others. It’s not the most efficient way to setup communication, especially because Java serialization puts the entire type information in the stream and reflection API isn’t the fastest way to call methods. But the ability to have plain Java interfaces and put whatever communication protocol behind it, is just great and allows to improve the protocol further in later stages of the development.
Hope you liked it!
- homac