Simple and Comfortable Networking with Java on-board Tools

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:

  1. Marshalling/Unmarshalling: A tool which converts Java objects into byte-streams and vice versa.
  2. Low-level Protocol: A set of messages to transfer method invocation parameters to the server and return values and exceptions back to the client.
  3. 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.
  4. Service Delivery: A way to turn incoming requests into method calls at the service implementation (Java object) and return the results.
  5. 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
2 Likes

Great description and outline.
The idea is similar to Nate’s great project Kryonet:


Like you suggested, the serialisation is cached so it works more quickly.
Cheers,
Keith

Thanks @CommanderKeith. What do you mean by cached serialisation?

Hi Homac,
I believe that the details of the class to be serialised are accessed via reflection and the field accessor objects are cached to improve performance.
But on looking at the code briefly I cannot find where that happens. However, better than that, many default serialisers are provided which would be even faster than using reflection:

Cheers,
Keith

Oh I get what you mean - interesting idea.

My intention was actually to show that it’s not particularely difficult to come to a reasonable middleware implementation of your own with Java.

I like to figure things out for my self to get a deeper understanding what’s going on inside of tools/libraries I use or used in the past. If you have a closer look at this simple example, you may notice, that it is not very far from CORBA or RMI.

There are lots of other examples like Docker, Spring or Quarkus which are Hyped and people use words like “magic” and when you have a closer look at the actual tech behind it, you get very disappointed :smiley:

EDIT: Oh I forgot to mention: There are also cases where the actual tech behind it gets underestimated by users …

2 Likes