Skip to content

Service Discovery

Thorben Kuck edited this page Oct 24, 2018 · 4 revisions

Service Discovery

Service Discovery was added with Version 2.0. It allows you to find and connect to a ServerStart within your local network.

How does it work.

You will need two basic classes to get this going. First, you will require a "ServiceDiscoveryHub".

You can provide it like this:

int port = 8888;
ServiceDiscoverHub hub = ServiceDiscoveryHub.open(port);
try {
    hub.listen();
} catch(SocketException e) {
    e.printStackTrace();
}

This hub will now listen to any UDP-Messages and then tries to match them. Without anything else, you can be found, but no one will know anything about you. To specify this, you have to change the example a bit work:

int port = 8888;
int targetPort = 4444;
ServiceDiscoverHub hub = ServiceDiscoveryHub.open(port, targetPort);
try {
    hub.listen();
} catch(SocketException e) {
    e.printStackTrace();
}

This ServiceDiscoveryHub will now tell anyone, that tries to find it, that whatever he is "hubbing" can be found on the targetPort within the network.

So, how exactly do we now request the ServiceDiscovery?

This can be done with the ServiceDiscoverer interface. This is used like this:

ServiceDiscoverer discoverer = ServiceDiscoverer.open(8888);
try {
    discoverer.findServiceHubs();
} catch (SocketException e) {
    e.printStackTrace();
}

"But Thorben", i hear you ask. "My program only terminates and does nothing, what am i missing?". Don't worry, we will fix this. First, let the current Thread sleep. The method findServiceHubs is working asynchronously and will therefor not block! The second thing we have to do, is to provide a callback, that will actually print out what we found. Let's just change the ServiceDiscoveryHub

ServiceDiscoveryHub hub = ServiceDiscoveryHub.open(8888, 4444);
try {
    discoverer.listen();
} catch (SocketException e) {
    e.printStackTrace();
}

try {
    Thread.currentThread().join();
} catch (InterruptedException e) {
    e.printStackTrace();
}

The call Thread.currentThread().join() will let the thread you are running this on, wait until said Thread is finished; It will never terminate naturally. Since this is an example, i will let it slip through. Now for the ServiceDiscoverer.

ServiceDiscoverer discoverer = ServiceDiscoverer.open(8888);

discoverer.onDiscover(serviceHubLocation -> System.out.println("Found a service hub pointing to " + serviceHubLocation.getAddress() + ":" + serviceHubLocation.getPort()));

try {
    discoverer.findServiceHubs();
} catch (SocketException e) {
    e.printStackTrace();
}

try {
    Thread.sleep(1000);
} catch(InterruptedException e) {
    e.printStackTrace();
}

So, why is this asynchronous?

Upon calling findServiceHubs, a broadcast request is send through the Network. We don't know if an when it will reach any Hub and if and when any response will come back. Therefor we start a Thread, that listens for a request/response, but we do not stop until a response has come back. This Thread will actually run until we kill the JVM, or until we call discoverer.close() or hub.terminate() respectively. If we now run our example, we will get an output, maybe even multiple outputs. The will look something like this:

Found a service hub at /192.168.178.155:4444

Obviously, the IP is just an example. Yours may vary from this, based on your local network configuration.

Using Service Discovery with NetCom2

You can utilize this form of Service Discovery very easily to connect a ClientStart and a ServerStart. We would change the ServiceHub example in the following way:

First, let's introduce a ServerStart to the ServiceDiscoveryHub example.

ServerStart serverStart = ServerStart.at(4444);
serverStart.addClientConnectedHandler(System.out::println);
serverStart.launch();

new Thread(() -> {
    try {
        serverStart.acceptAllNextClients();
    } catch (ClientConnectionFailedException e) {
        e.printStackTrace();
    }
}).start();

ServiceDiscoveryHub hub = ServiceDiscoveryHub.open(8888);
hub.connect(serverStart);
try {
    discoverer.listen();
} catch (SocketException e) {
    e.printStackTrace();
}

try {
    Thread.currentThread().join();
} catch (InterruptedException e) {
    e.printStackTrace();
}

We did a few things here. We created a ServerStart at the previous targetPort (4444) and started a Thread to acceptAllNextClients. We then no longer tell the ServiceDiscoveryHub which is the targetPort. Instead we connect the hub and the ServerStart. To validate, that we connect, i also added a ClientConnectedHandler, that will print out any Client that connect. The idea is, that this Hub will tell every request, where the connected ServerStart can be found and we can see a console println with a Client.

For the ServiceDiscoverer, we can do the following:

ServiceDiscoverer discoverer = ServiceDiscoverer.open(8888);

discoverer.onDiscover(serviceHubLocation -> {
    System.out.println("Found a ServiceHub!");
    ClientStart clientStart = serviceHubLocation.toClientStart();
    try {
        clientStart.launch();
    } catch (StartFailedException e) {
        e.printStackTrace();
    }

    discoverer.close();
});

try {
    discoverer.findServiceHubs();
} catch (SocketException e) {
    e.printStackTrace();
}

try {
    Thread.sleep(1000);
} catch(InterruptedException e) {
    e.printStackTrace();
}

The only thing that changed, is the "onDiscover" callback. In here we request a ClientStart by stating serviceHubLocation.toClientStart() and launch it afterwards. Then we close the discoverer. "Why is that?", you might ask. We are making broadcast attempts though all of our network interfaces. This may result in multiple responses, because we have multiple network interfaces. The result may be something like this:

NativeClient{sessionValue=NativeSession{identifiedValue=false, identifierValue=}}

This tells us, that we successfully connected! Of course in this example we are missing multiple things. Like handling the StartFailedException correctly.

Header Manipulation and Extension

What you cant see, is the exchange of the Hub and the Discoverer. The Discoverer sends out a simple String NET_COM_SERVICE_DISCOVER_REQUEST. The Hub takes this String, checks if it is exactly this String and if, answers with it own String. This might look like this:

NET_COM_SERVICE_DISCOVER_REQUEST;STATUS:200,TARGET:4444,SERVER_NAME:NO_NAME

This String is then processed by the Discoverer and, if everything is okay, the callback will be informed.

You can change this String, to match your needs. For example, let's add the current running time to it and only accept a Server, that run for at least 5 seconds. For the ServiceDiscoveryHub, we have have to provide a header entry like this:

ServerStart serverStart = ServerStart.at(4444);
serverStart.addClientConnectedHandler(System.out::println);
serverStart.launch();

long timeRunning = System.currentTimeMillis();

new Thread(() -> {
    try {
        serverStart.acceptAllNextClients();
    } catch (ClientConnectionFailedException e) {
        e.printStackTrace();
    }
}).start();

ServiceDiscoveryHub hub = ServiceDiscoveryHub.open(8888);
hub.addHeaderEntry(header -> header.addEntry("TIME_RUNNING", Long.toString(System.currentTimeMillis() - startTime)));
hub.connect(serverStart);
try {
    discoverer.listen();
} catch (SocketException e) {
    e.printStackTrace();
}

try {
    Thread.currentThread().join();
} catch (InterruptedException e) {
    e.printStackTrace();
}

All we do is, that we take the currentTimeMillis after launch and then, add the according header entry. On the ServiceDiscoverer, we have to parse this Header entry. We can do it like this:

ServiceDiscoverer discoverer = ServiceDiscoverer.open(8888);

discoverer.addHeaderMapping("TIME_RUNNING", (s, discoveryProcessingRequest) -> {
    String value = s;
    // We can access the header through the discoveryProcessingRequest like this
    // The String s however is exactly the corresponding header entry.
    // String value = discoveryProcessingRequest.header().get("TIME_RUNNING");
    long timeRunning = Long.valueOf(value);
    return TimeUnit.MILLISECONDS.toSeconds(timeRunning) >= 5;
});

discoverer.onDiscover(serviceHubLocation -> {
    System.out.println("Found a ServiceHub!");
    ClientStart clientStart = serviceHubLocation.toClientStart();
    try {
        clientStart.launch();
    } catch (StartFailedException e) {
        e.printStackTrace();
    }

    discoverer.close();
});

try {
    discoverer.findServiceHubs();
} catch (SocketException e) {
    e.printStackTrace();
}

try {
    Thread.sleep(1000);
} catch(InterruptedException e) {
    e.printStackTrace();
}

Here we add the HeaderMapping for our TIME_RUNNING entry. We return true, if the Server has run for more than 5 seconds, else false. If we now connect to the Server faster than in 5 seconds, we get the following output:

[YOUR-TIME] (NetComThread[priority=7, number=1]) : WARN : >>> Header Entry marked as faulty: TIME_RUNNING:2007. This implies no Location found.
[YOUR-TIME] (NetComThread[priority=7, number=1]) : WARN : >>> Unknown broadcast response. Ignoring ..

But if we patiently wait the 5 seconds, the example works as intended.

This is a rather stupid example, i know, but you can easily add anything to it.

Future plans

This will most definitely be relocated into it's own separate Maven project! If you use this and update your version, you should take a look at what maven-module to include. The package will be the same, but you will have to specify the maven-module to use it.

Clone this wiki locally