Lesson 11 - Java Server - Local Area Network Propagation (Part 1)
In the previous lesson, Java Server - Plugin System, we focused on the plugin system of our Java server.
Today we're going to make the server visible in the local network.
TCP vs. UDP
Before we start programming, let's talk a little bit about the theory behind the TCP and UDP protocols.
TCP
TCP stands for Transmission Control Protocol. It's a reliable and connected protocol. Reliable means that the data a user sends arrives to the destination successfully and in the correct order. Connected means that the connection must be established before the communication begins and it's maintained for the whole time. This protocol is mainly used where reliability is preferred over speed.
UDP
UDP stands for Universal/User Datagram Protocol. UDP is the exact opposite of TCP. The protocol is unreliable and unconnected. Individual datagrams can come in different order. The protocol doesn't guarantee that the data will be transferred successfully - it may be lost on the way. It's used where it's necessary to transfer data efficiently and quickly such in games or videos.
Multicast Sender
In the previous lessons we designed a communication protocol above TCP, so we're guaranteed that our data always arrives right. We'll use UDP to make the server visible. We'll create a new class to send a datagram to all machines in the local network in an infinite loop at a defined interval. A machine that doesn't know how to process the message will discard it. We'll program receiving these datagrams in the client.
In the core
package we'll create a new multicaster
package, in which we'll implement the functionality mentioned above.
Designing Interfaces
We'll create a simple IMulticastSender
markup interface that
doesn't have any method:
public interface IMulticastSender extends IThreadControl {}
We'll also add an IMulticastSenderFactory
interface with the
getMulticastSender()
method:
public interface IMulticastSenderFactory { IMulticastSender getMulticastSender(ServerInfoProvider serverInfoProvider); }
In the getMulticastSender()
method, we used a
ServerInfoProvider
interface, which doesn't exist yet. It'll be
used to obtain information about the current server state (identifier,
occupancy, address, name, ...). Let's add it:
public interface ServerInfoProvider { IMessage getServerStatusMessage(); }
The interface contains a single method,
getServerStatusMessage()
, which returns a server status
message.
Modifying Existing Interfaces
Now we'll add new methods to the existing interfaces that we're going to need
today. We'll add a parameterless getParameters()
method to the
IParameterFactory
interface:
public interface IParameterFactory { IParameterProvider getParameters(); // the new method IParameterProvider getParameters(String[] args); }
To the IConnectionManager
interface we'll add a
getConnectedClientCount()
method to get the number of connected
clients and a getMaxClients()
method to return the maximum number
of connected clients:
public interface IConnectionManager { void addClient(Socket socket) throws IOException; void onServerStart(); void onServerStop(); int getConnectedClientCount(); // new method int getMaxClients(); // new method }
We'll extend the IMessage
interface of a new
toByteArray()
default method to create a serializable data package
from the class:
default byte[] toByteArray() throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(this); oos.writeByte(0); final byte[] bytes = baos.toByteArray(); assert bytes.length < 1024; return bytes; }
In the method, we create a ByteArrayOutputStream
class instance,
which we pass as a parameter when creating an ObjectOutputStream
instance. Using the writeObject()
method, we serialize our message
and write the data to the stream. We must add a zero byte, otherwise the stream
on the other side wouldn't recognize where the data ends. The
toByteArray()
method returns the resulting data package. I've also
added a check to make sure the data doesn't exceed the length of
1024
. That's because when we'll implement the client, the buffer
into which we'll read the data to will be 1024
bytes.
Finally, we'll modify the IServerThread
interface to inherit
from the ServerInfoProvider
interface:
public interface IServerThread extends IThreadControl, ServerInfoProvider {}
Implementing the Interfaces
Once we've created and modified the necessary interfaces, let's implement
them. First, we'll create an IMulticastSender
implementation using
a MulticastSender
class:
class MulticastSender extends Thread implements IMulticastSender { }
We'll add three constants to the class:
private static final long SLEEP_TIME = 2000L; private static final String DEFAULT_MULTICAST_ADDRESS = "224.0.2.50"; private static final int DEFAULT_MULTICAST_PORT = 56489;
Let's take a minute to discuss the default multicast address. The address
224.0.2.50
falls within the multicast range 224.0.2.0
- 224.0.255.255
. Packets sent within this address range will travel
across the entire local network.
There will be two instance constants:
private final IParameterFactory parameterFactory; private final ServerInfoProvider serverInfoProvider;
And four instance variables:
private DatagramSocket socket; private InetAddress broadcastAddress; private int port; private boolean interrupt = false;
DatagramSocket
represents a socket using which we'll send datagram packets.InetAddress
contains the broadcast address to which our packet will be sent.- The
interrupt
variable has the same meaning as in the previous chapters.
The class constructor won't be public, so available only within the package,
and will accept two parameters of the IParameterFactory
and
ServerInfoProvider
types:
MulticastSender(IParameterFactory parameterFactory, ServerInfoProvider serverInfoProvider) { super("MulticastSender"); this.parameterFactory = parameterFactory; this.serverInfoProvider = serverInfoProvider; }
In the constructor, we set the thread name first so that we can distinguish it in the future easily. Then the instance constants are initialized.
Next, we'll create a private method to initialize the address and socket.
We'll name the method init()
:
private void init() { final IParameterProvider parameterProvider = parameterFactory.getParameters(); try { this.broadcastAddress = InetAddress.getByName(parameterProvider .getString(CmdParser.MULTICAST_ADDRESS, DEFAULT_MULTICAST_ADDRESS)); this.port = parameterProvider.getInteger(CmdParser.MULTICAST_PORT, DEFAULT_MULTICAST_PORT); this.socket = new DatagramSocket(); } catch (IOException e) { throw new RuntimeException(e); } }
First, we get an IParameterProvider
instance from the factory
using the getParameters()
method. From the parameters we get the
broadcast address value. If the value isn't available, we use the default one.
Please add two new attributes to the CmdParser
class:
MULTICAST_ADDRESS
and MULTICAST_PORT
with these custom
values:
// The address to broadcast multicast packets to public static final String MULTICAST_ADDRESS = "multicast_address"; // The port to broadcast multicast packets to public static final String MULTICAST_PORT = "multicast_port";
Now we're going to implement or override the methods defined by the
IThreadControl
interface or the Thread
class. We'll
override the start()
method and call the init()
method
in it:
@Override public synchronized void start() { init(); super.start(); }
The shutdown()
method will have the same body as in many other
cases:
@Override public void shutdown() { interrupt = true; try { join(); } catch (InterruptedException ignored) { } }
I kept the most important run()
method for the end:
public void run() { if (socket == null || broadcastAddress == null) { interrupt = true; } while(!interrupt) { try { final IMessage serverStatusMessage = serverInfoProvider .getServerStatusMessage(); final byte[] data = serverStatusMessage.toByteArray(); final DatagramPacket datagramPacket = new DatagramPacket( data, data.length, broadcastAddress, port); this.socket.send(datagramPacket); } catch (IOException e) { e.printStackTrace(); break; } try { Thread.sleep(SLEEP_TIME); } catch (InterruptedException ignored) {} } }
At the beginning of the method we check whether the socket and address
initialization was successful. If one of the variables is null
, the
interrupt
variable is set to true
to ensure that the
thread terminates. In an infinite loop, a server information message is obtained
and converted to a data packet. This data package is inserted into a datagram
and sent to the world through the socket. The thread is then put to sleep for
the time specified by the SLEEP_TIME
constant. This infinite loop
ensures that our server is visible across the entire local network.
We're almost at the end of this lesson, but we can still make a factory. So
we'll create a MulticastSenderFactory
class implementing the
IMulticastSenderFactory
interface. The interface requires the class
to contain a single getMulticastSender()
method:
@Singleton public class MulticastSenderFactory implements IMulticastSenderFactory { private final IParameterFactory parameterFactory; @Inject public MulticastSenderFactory(IParameterFactory parameterFactory) { this.parameterFactory = parameterFactory; } @Override public IMulticastSender getMulticastSender(ServerInfoProvider serverInfoProvider) { return new MulticastSender(parameterFactory, serverInfoProvider); } }
Finally, we'll register the factory in ServerModule
:
bind(IMulticastSenderFactory.class).to(MulticastSenderFactory.class);
That'd be all for the first part of today's tutorial.
In the second part, Java Server - Local Area Network Propagation (Part 2), we'll implement the rest of the functionality of the server part.