Chapter 6. Writing protocols

This chapter discusses how to write custom protocols

6.1. Anatomy of a protocol

6.2. Writing user defined headers

Headers are mainly used by protocols, to ship additional information around with a message, without having to place it into the payload buffer, which is often occupied by the application already. However, headers can also be used by an application, e.g. to add information to a message, without having to squeeze it into the payload buffer.

A header has to extend org.jgroups.Header, have an empty public constructor and (currently) implement the Externalizable interface (writeExternal() and readExternal() methods). Note that the latter requirement (Externalizable) will probably go away in 3.0.

A header should also override size(), which returns the total number of bytes taken up in the output stream when an instance is marshalled using Streamable. Streamable is an interface for efficient marshalling with methods void writeTo(DataOutputStream out) throws IOException; and void readFrom(DataInputStream in) throws IOException, IllegalAccessException, InstantiationException;. Method writeTo() needs to write all relevant instance variables to the output stream and readFrom() needs to read them back in. It is important that size() returns the correct number of bytes, because some components such a message bundling in the transport depend on this, as they need to measure the exact number of bytes before sending a message off. If size() returns fewer bytes than what will actually be written to the stream, then it is possible that (if we use UDP with a 65535 bytes maximum) the datagram packet is dropped by UDP !

The final requirement is to add the newly created header class to jg-magic-map.xml (in the ./conf directory), or - if this is not a JGroups internal protocol - to add the class to ClassConfigurator. This can be done with method ClassConfigurator.getInstance().put(1899, MyHeader.class).

The code below shows how an application defines a custom header, MyHeader, and uses it to attach additional information to message sent (to itself):

                public class bla {

                    public static void main(String[] args) throws ChannelException, ClassNotFoundException {
                        JChannel ch=new JChannel();
                        ch.connect("demo");
                        ch.setReceiver(new ReceiverAdapter() {
                            public void receive(Message msg) {
                                MyHeader hdr=(MyHeader)msg.getHeader("x");
                                System.out.println("-- received message " + msg + ", header is " + hdr);
                            }
                        });

                        ClassConfigurator.getInstance().add((short)1900, MyHeader.class);

                        int cnt=1;
                        for(int i=0; i < 5; i++) {
                            Message msg=new Message();
                            msg.putHeader("x", new MyHeader(cnt++));
                            ch.send(msg);
                        }
                        ch.close();
                    }


                    public static class MyHeader extends Header implements Streamable {
                        int counter=0;

                        public MyHeader() {
                        }

                        private MyHeader(int counter) {
                            this.counter=counter;
                        }

                        private static final long serialVersionUID=7726837062616954053L;

                        public void writeExternal(ObjectOutput out) throws IOException {}

                        public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {}

                        public String toString() {
                            return "counter=" + counter;
                        }

                        public int size() {
                            return Global.INT_SIZE;
                        }

                        public void writeTo(DataOutputStream out) throws IOException {
                            out.writeInt(counter);
                        }

                        public void readFrom(DataInputStream in) throws IOException, IllegalAccessException, InstantiationException {
                            counter=in.readInt();
                        }
                    }
                }
            

The MyHeader class has an empty public constructor and implements the writeExternal() and readExternal() methods with no-op implementations.

The state is represented as an integer counter. Method size() returns 4 bytes (Global.INT_SIZE), which is the number of bytes written by writeTo() and read by readFrom().

Before sending messages with instances of MyHeader attached, the program registers the MyHeader class with the ClassConfigurator. The example uses a magic number of 1900, but any number greater than 1024 can be used. If the magic number was already taken, an IllegalAccessException would be thrown.

The final part is adding an instance of MyHeader to a message using Message.putHeader(). The first argument is a name which has to be unique across all headers for a given message. Usually, protocols use the protocol name (e.g. "UDP", "NAKACK"), so these names should not be used by an application. The second argument is an instance of the header.

Getting a header is done through Message.getHeader() which takes the name as argument. This name of course has to be the same as the one used in putHeader().