Create a UDP Multicast message system


Android Java Multicast UDP Library

Some days ago I thought about an application which is able to comunicate inside a LAN with all the others connected to the same network. Let's say a "local chat", or even better an *automatic* local chat: just connect to the right WiFi and chat! Well, the idea is not just that one, but I won't tell you more before an advanced stage of my application :)

I've studied computer networks at the University, I know what UDP and TCP, Unicast, Multicast and Broadcast are, network shaping strategies... but all in theory! Yesterday I decided to soil my hands creating this simple UDP Multicast messenger library.


Attention: this work is under the Creative Commons Attribution 3.0 Unported licence


Warning: you may flood your LAN with UDP packets with this project, use it responsibly!


Note: some public networks just disable UDP to prevent the previous warning. In that case, this library won't work.


Source code and brief description: UDP Messenger


These are the methods of the class we're going to create:

These are the class variables we're gonna create:

Let's start!

This is the complete list of variables:

:::Java
protected static String DEBUG_TAG = "UDPMessenger"; // to log out things
protected static final Integer BUFFER_SIZE = 4096; // size of the reading buffer

protected String TAG; // chat TAG
protected int MULTICAST_PORT; // chat port

private boolean receiveMessages = false; // variable to know if we have to listen for incoming packets

protected Context context; // the application's context, used to get network state info etc.
private DatagramSocket socket; // the socket used to send the messages

protected abstract Runnable getIncomingMessageAnalyseRunnable(); // the abstract runnable which will analyse the incoming packets
private final Handler incomingMessageHandler; // the handler which will start the previous one
protected Message incomingMessage; // the n-th incoming message to be analysed - Message is a custom class
private Thread receiverThread; // the thread used to receive the multicast

It may appear confusing, but it will soon be clearer when you'll see what each variable will be used for :)

Class constructor (UDPMessenger)

:::Java
/**
 * Class constructor
 * @param context the application's context
 * @param tag a valid string, used to filter the UDP broadcast messages (in and out). It can't be null or 0-characters long.
 * @param multicastPort the port to multicast to. Must be between 1025 and 49151 (inclusive)
 * @param connectionPort the port to get the connection back. Must be between 1025 and 49151
 */
public UDPMessenger(Context context, String tag, int multicastPort) throws IllegalArgumentException {
    if(context == null || tag == null || tag.length() == 0 ||
        multicastPort <= 1024 || multicastPort > 49151)
        throw new IllegalArgumentException();

    this.context = context.getApplicationContext();
    TAG = tag;
    MULTICAST_PORT = multicastPort;

    incomingMessageHandler = new Handler(Looper.getMainLooper());
}

You could ask yourself why 1024 <= multicast port < 49151? The answer is pretty much simply: from 0 to 1023 are system reserved and you'd need root privileges to use them. Also, you may interfere with system services. The ports after 49150 are reserved, too (well, you've got a pool of ~48k ports to choose from!). The constructor is straight forward.

The only thing I'd like to let you notice is the handler. Handlers in Android are used to comunicate between threads. More info at the Android Developers website. The important thing to notice is that I'm binding the handler to the main looper, i.e. the handler will run the runnable (which we'll see later) in the main thread (UI), allowing the user to modify UI elements (or run another thread, whatever he/she likes to do).


Before proceeding with the other main methods, I need to explain my "ip-to-string" utility one.

First of all, the term "IP" is just generic: we're gonna use IPv4 because it's the most supported protocol (opposed to IPv6). IPv4 is represented by 4 numbers between 0 and (2^8)-1 (255), divided by dots (for example 192.168.0.1). All zeros and all 255 are protected addresses and can't be used to identify a machine. Every machine in a network owns an IP (which is given by a DHCP daemon or manually set by the machine, depending on the network itself). But in the end, the IP is just a 32 bits integer. And this is why we need to transform into a string, in order to proceed using the InetAddress class.

Example (skip it if you want)

If you want to send a message to a single machine, you need to write its IP in the packet. What about when you want to send a message to ALL the computers in the network? This is called "broadcast" and can be achieved simply by substituting the last number with 255. You can achieve different ranges of broadcast by using more 255s. In our case, being a local area network, we'll need to send messages to our subnet only.

Just as an example, let's think about sending a message to all the computers of a network. Using unicast, you'd send multiple copies of the messages inside different packages with as destination all the IPs of the machines. That's a lot of flood! What about using multicast? Just send ONE packet to all the partecipants. That's pretty amazing: less time, less flood.

If you'd like to learn more about IP addresses or network in general, I'd like to suggest you a book on which I've studied on: Computer Networking: A Top-Down Approach Featuring the Internet by Kurose, James F.; Ross, Keith W. published by Pearson Education, Inc.

TL;DR: my method translates the integer representation of the IP into a readable string, which will be then used to setup an InetAddress variable.

Utility method ipToString

:::Java
public static String ipToString(int ip, boolean broadcast) {
    String result = new String();

    Integer[] address = new Integer[4];
    for(int i = 0; i < 4; i++)
        address[i] = (ip >> 8*i) & 0xFF;
    for(int i = 0; i < 4; i++) {
        if(i != 3)
            result = result.concat(address[i]+".");
        else result = result.concat("255.");
    }
    return result.substring(0, result.length() - 2);
}

It may scare you those strange >> and & symbols, but it's pretty much forward. The "readable" numbers are formed by 8bit hexadecimal values, so in the first part we simply convert them with 0xFF (which is 255 in hexadecimal), taking 1 byte per time (8*i). The single & is the bitwise logic AND operator and >> is the shift right one. Now we've got the IPv4 address divided in an integer array, and we concatenate it with dots to form the actual IP address. If we want to broadcast, we substitute the last number with "255". We add a dot in the end in order to make logics simplier, as in the other case we add it in any case. Eventually we delete the extra dot (alternatively we'd check wherever the string ends with a dot, making a string comparison).


sendMessage method

:::Java
/**
 * Sends a broadcast message (TAG EPOCH_TIME message). Opens a new socket in case it's closed.
 * @param message the message to send (multicast). It can't be null or 0-characters long.
 * @return
 * @throws IllegalArgumentException
 */
public boolean sendMessage(String message) throws IllegalArgumentException {
    if(message == null || message.length() == 0)
        throw new IllegalArgumentException();

    // Check for WiFi connectivity
    ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo mWifi = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);

    if(mWifi == null || !mWifi.isConnected())
    {
        Log.d(DEBUG_TAG, "Sorry! You need to be in a WiFi network in order to send UDP multicast packets. Aborting.");
        return false;
    }

    // Check for IP address
    WifiManager wim = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
    int ip = wim.getConnectionInfo().getIpAddress();

    // Create the send socket
    if(socket == null) {
        try {
            socket = new DatagramSocket();
        } catch (SocketException e) {
            Log.d(DEBUG_TAG, "There was a problem creating the sending socket. Aborting.");
            e.printStackTrace();
            return false;
        }
    }

    // Build the packet
    DatagramPacket packet;
    Message msg = new Message(TAG, message);
    byte data[] = msg.toString().getBytes();

    try {
        packet = new DatagramPacket(data, data.length, InetAddress.getByName(ipToString(ip, true)), MULTICAST_PORT);
    } catch (UnknownHostException e) {
        Log.d(DEBUG_TAG, "It seems that " + ipToString(ip, true) + " is not a valid ip! Aborting.");
        e.printStackTrace();
        return false;
    }

    try {
        socket.send(packet);
    } catch (IOException e) {
        Log.d(DEBUG_TAG, "There was an error sending the UDP packet. Aborted.");
        e.printStackTrace();
        return false;
    }

    return true;
}

I don't think there is anything I'd add to the comments. We first check whether we're in a WiFi network or not: we don't want to flood internet with our broadcast packets, nor we can! Then we try to create a new UDP socket, bound to one of the available ports.

If we want, we can add an argument to DatagramSocket (UDP socket) specifying the port, i.e. DatagramSocket(MULTICAST_PORT+1), in this way we have more control on the port used (for example for further unicast response messages).

If all the previous steps thrown no errors, we proceed creating the DatagramPacket (UDP packet) setting the IP destination, the message (in bytes), the message length and the port destination (this is where the broadcast becomes multicast, specifying a port the client must listen on the very same one to get it).

Eventually send the packet over the previously setup socket.


startMessageReceiver method

:::Java
public void startMessageReceiver() {
    Runnable receiver = new Runnable() {

        @Override
        public void run() {
            WifiManager wim = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
            if(wim != null) {
                MulticastLock mcLock = wim.createMulticastLock(TAG);
                mcLock.acquire();
            }

            byte[] buffer = new byte[BUFFER_SIZE];
            DatagramPacket rPacket = new DatagramPacket(buffer, buffer.length);
            MulticastSocket rSocket;

            try {
                rSocket = new MulticastSocket(MULTICAST_PORT);
            } catch (IOException e) {
                Log.d(DEBUG_TAG, "Impossible to create a new MulticastSocket on port " + MULTICAST_PORT);
                e.printStackTrace();
                return;
            }

            while(receiveMessages) {
                try {
                    rSocket.receive(rPacket);
                } catch (IOException e1) {
                    Log.d(DEBUG_TAG, "There was a problem receiving the incoming message.");
                    e1.printStackTrace();
                    continue;
                }

                if(!receiveMessages)
                    break;

                byte data[] = rPacket.getData();
                int i;
                for(i = 0; i < data.length; i++)
                {
                    if(data[i] == '\0')
                        break;
                }

                String messageText;

                try {
                    messageText = new String(data, 0, i, "UTF-8");
                } catch (UnsupportedEncodingException e) {
                    Log.d(DEBUG_TAG, "UTF-8 encoding is not supported. Can't receive the incoming message.");
                    e.printStackTrace();
                    continue;
                }

                try {
                    incomingMessage = new Message(messageText, rPacket.getAddress());
                } catch (IllegalArgumentException ex) {
                    Log.d(DEBUG_TAG, "There was a problem processing the message: " + messageText);
                    ex.printStackTrace();
                    continue;
                }

                incomingMessageHandler.post(getIncomingMessageAnalyseRunnable());
            }
        }

    };

    receiveMessages = true;
    if(receiverThread == null)
        receiverThread = new Thread(receiver);

    if(!receiverThread.isAlive())
        receiverThread.start();
}

This not-so-short method allows us to keep listening for incoming packets. The first thing we notice is the runnable variable. I've built the method with a runnable because we need to run it on another thread. The reason why we need to do so is because otherwise we'd lock the main thread (see the potential-infinite while loop?). In any case Android won't allow us to run network things on the main thread. Remember that Android's main thread is the UI one, this is why the OS will crash the application in case, in order to avoid choppy experience.

Android allows to listen for multicast messages over WiFi only. So we check whether we can access to it and eventually add a multicast lock. This is Android-specific stuff, as by default the OS will filter those messages out. The lock enables the application to listen for those packets, but we require a special permission (see the permissions paragraph at the end of the article).

We create a buffer and bind it to an empty DatagramPacket, which will be filled with our multicast messages (one per time). We also create a MulticastSocket which, in this case, will listen for multicast messages (it's still a UDP socket, not a TCP one!). Eventually we bind the new listening socket to the multicast port (which is the same we've used to address the multicast packets in the sender method).

If we don't run the rest of the runnable within a while cycle, it'd stop at the first incoming message. The receive method of the multicast socket is a blocking one, which means it will freeze the thread's execution untill a message arrives. This is the reason why we need to run in another thread, otherwise we'd freeze the entire application!

When a packet arrives, it's been stored in our rSocket. Because we know the incoming message (i.e. the data contained in the packet) is a string, we can treat it as it. If we decide to create a binary files messenger, we'd change this portion of the code. In any case, we simply read the bytes untill there's an end string character, eventually we transfer the string data into a String object, not reading the rest of the (oversized) buffer.

Hurray! We now have the message! But what do we do? Well, we'd call the message analyser abstract method (which will implemented by the developer), for this we packet the message into a Message class container and set it as the class' incomingMessage variable (read by the analyser). Eventually, we call the handler (which runs in the main looper) asking it to execute our analyse method.

In the meanwhile, outside or big Runnable object... we simply create a new thread (if not already present), setting its runnable object our runnable one and, if it's not already started by a previous start command without stopping it, start it.

stopMessageReceiver method

:::Java
public void stopMessageReceiver() {
    receiveMessages = false;
}

Trivial method, but with a point to highlight: being the MulticastSocket.receive() method a blocking one, the stop command won't affect the thread directly. We could stop the thread instead of setting a variable, but that may lead to inconsistence and unpredictable state (and this is why that thread's method has been deprecated). The best solution is the one provided, i.e. inserting a receiveMessages check after the blocking method in the receiving runnable. In this case, it will still receive the last message, but it won't continue nor execute the analyser. The con is that the thread won't shut down immediately, but who really cares, if it doesn't stress the CPU?

Android permissions

In order to make this library work, we need the following permissions (see Manifest file):

- 23rd June 2013

> back