1. Overview
JGroups is a Java API for reliable messages exchange. It features a simple interface that provides:
- a flexible protocol stack, including TCP and UDP
- fragmentation and reassembly of large messages
- reliable unicast and multicast
- failure detection
- flow control
As well as many other features.
In this tutorial, we’ll create a simple application for exchanging String messages between applications and supplying shared state to new applications as they join the network.
2. Setup
2.1. Maven Dependency
We need to add a single dependency to our pom.xml:
<dependency> <groupId>org.jgroups</groupId> <artifactId>jgroups</artifactId> <version>4.0.10.Final</version> </dependency>
The latest version of the library can be checked on Maven Central.
2.2. Networking
JGroups will try to use IPV6 by default. Depending on our system configuration, this may result in applications not being able to communicate.
To avoid this, we’ll set the java.net.preferIPv4Stack to true property when running our applications here:
java -Djava.net.preferIPv4Stack=true com.baeldung.jgroups.JGroupsMessenger
3. JChannels
Our connection to a JGroups network is a JChannel. The channel joins a cluster and sends and receives messages, as well as information about the state of the network.
3.1. Creating a Channel
We create a JChannel with a path to a configuration file. If we omit the file name, it will look for udp.xml in the current working directory.
We’ll create a channel with an explicitly named configuration file:
JChannel channel = new JChannel("src/main/resources/udp.xml");
JGroups configuration can be very complicated, but the default UDP and TCP configurations are sufficient for most applications. We’ve included the file for UDP in our code and will use it for this tutorial.
For more information on configuring the transport see the JGroups manual here.
3.2. Connecting a Channel
After we’ve created our channel, we need to join a cluster. A cluster is a group of nodes that exchange messages.
Joining a cluster requires a cluster name:
channel.connect("Baeldung");
The first node that attempts to join a cluster will create it if it doesn’t exist. We’ll see this process in action below.
3.3. Naming a Channel
Nodes are identified by a name so that peers can send directed messages and receive notifications about who is entering and leaving the cluster. JGroups will assign a name automatically, or we can set our own:
channel.name("user1");
We’ll use these names below, to track when nodes enter and leave the cluster.
3.4. Closing a Channel
Channel cleanup is essential if we want peers to receive timely notification that we have exited.
We close a JChannel with its close method:
channel.close()
4. Cluster View Changes
With a JChannel created we’re now ready to see the state of peers in the cluster and exchange messages with them.
JGroups maintains cluster state inside the View class. Each channel has a single View of the network. When the view changes, it’s delivered via the viewAccepted() callback.
For this tutorial, we’ll extend the ReceiverAdaptor API class that implements all of the interface methods required for an application.
It’s the recommended way to implement callbacks.
Let’s add viewAccepted to our application:
public void viewAccepted(View newView) { private View lastView; if (lastView == null) { System.out.println("Received initial view:"); newView.forEach(System.out::println); } else { System.out.println("Received new view."); List<Address> newMembers = View.newMembers(lastView, newView); System.out.println("New members: "); newMembers.forEach(System.out::println); List<Address> exMembers = View.leftMembers(lastView, newView); System.out.println("Exited members:"); exMembers.forEach(System.out::println); } lastView = newView; }
Each View contains a List of Address objects, representing each member of the cluster. JGroups offers convenience methods for comparing one view to another, which we use to detect new or exited members of the cluster.
5. Sending Messages
Message handling in JGroups is straightforward. A Message contains a byte array and Address objects corresponding to the sender and the receiver.
For this tutorial we’re using Strings read from the command line, but it’s easy to see how an application could exchange other data types.
5.1. Broadcast Messages
A Message is created with a destination and a byte array; JChannel sets the sender for us. If the target is null, the entire cluster will receive the message.
We’ll accept text from the command line and send it to the cluster:
System.out.print("Enter a message: "); String line = in.readLine().toLowerCase(); Message message = new Message(null, line.getBytes()); channel.send(message);
If we run multiple instances of our program and send this message (after we implement the receive() method below), all of them would receive it, including the sender.
5.2. Blocking Our Messages
If we don’t want to see our messages, we can set a property for that:
channel.setDiscardOwnMessages(true);
When we run the previous test, the message sender does not receive its broadcast message.
5.3. Direct Messages
Sending a direct message requires a valid Address. If we’re referring to nodes by name, we need a way to look up an Address. Fortunately, we have the View for that.
The current View is always available from the JChannel:
private Optional<address> getAddress(String name) { View view = channel.view(); return view.getMembers().stream() .filter(address -> name.equals(address.toString())) .findAny(); }
Address names are available via the class toString() method, so we merely search the List of cluster members for the name we want.
So we can accept a name on from the console, find the associated destination, and send a direct message:
Address destination = null; System.out.print("Enter a destination: "); String destinationName = in.readLine().toLowerCase(); destination = getAddress(destinationName) .orElseThrow(() -> new Exception("Destination not found"); Message message = new Message(destination, "Hi there!"); channel.send(message);
6. Receiving Messages
We can send messages, now let’s add try to receive them now.
Let’s override ReceiverAdaptor’s empty receive method:
public void receive(Message message) { String line = Message received from: " + message.getSrc() + " to: " + message.getDest() + " -> " + message.getObject(); System.out.println(line); }
Since we know the message contains a String, we can safely pass getObject() to System.out.
7. State Exchange
When a node enters the network, it may need to retrieve state information about the cluster. JGroups provides a state transfer mechanism for this.
When a node joins the cluster, it simply calls getState(). The cluster usually retrieves the state from the oldest member in the group – the coordinator.
Let’s add a broadcast message count to our application. We’ll add a new member variable and increment it inside receive():
private Integer messageCount = 0; public void receive(Message message) { String line = "Message received from: " + message.getSrc() + " to: " + message.getDest() + " -> " + message.getObject(); System.out.println(line); if (message.getDest() == null) { messageCount++; System.out.println("Message count: " + messageCount); } }
We check for a null destination because if we count direct messages, each node will have a different number.
Next, we override two more methods in ReceiverAdaptor:
public void setState(InputStream input) { try { messageCount = Util.objectFromStream(new DataInputStream(input)); } catch (Exception e) { System.out.println("Error deserialing state!"); } System.out.println(messageCount + " is the current messagecount."); } public void getState(OutputStream output) throws Exception { Util.objectToStream(messageCount, new DataOutputStream(output)); }
Similar to messages, JGroups transfers state as an array of bytes.
JGroups supplies an InputStream to the coordinator to write the state to, and an OutputStream for the new node to read. The API provides convenience classes for serializing and deserializing the data.
Note that in production code access to state information must be thread-safe.
Finally, we add the call to getState() to our startup, after we connect to the cluster:
channel.connect(clusterName); channel.getState(null, 0);
getState() accepts a destination from which to request the state and a timeout in milliseconds. A null destination indicates the coordinator and 0 means do not timeout.
When we run this app with a pair of nodes and exchange broadcast messages, we see the message count increment.
Then if we add a third client or stop and start one of them, we’ll see the newly connected node print the correct message count.
8. Conclusion
In this tutorial, we used JGroups to create an application for exchanging messages. We used the API to monitor which nodes connected to and left the cluster and also to transfer cluster state to a new node when it joined.
Code samples, as always, can be found over on GitHub.