Get up to 80 % extra points for free! More info:

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.


 

Previous article
Java Server - Plugin System
All articles in this section
Server for Client Applications in Java
Skip article
(not recommended)
Java Server - Local Area Network Propagation (Part 2)
Article has been written for you by Petr Štechmüller
Avatar
User rating:
1 votes
Activities