Skip to content

Getting Started with wAsync

shubhamrai1993 edited this page May 29, 2018 · 22 revisions

Which framework are you using?

The wAsync library can be used with any WebSocket/Asynchronous framework like Atmosphere, Node.js or Vert.x, jsr 356 implementation or any existing WebSocket Server. wAsync is fully asynchronous and non blocking.

Creating the client

The first steps is to decide which Client to use. With version 1.0, three clients are available:

  • Default: Works with all frameworks, no specific framework/protocol information.
  • Atmosphere: Specific client that support the Atmosphere Protocol.
  • Serial: A client that serialize messages so the server receive messages in the order they were send by the client.

The client instance is obtained by doing:

// Default, works with all framework
Client client = ClientFactory.getDefault().newClient();

// Atmosphere
AtmosphereClient client = ClientFactory.getDefault().newClient(AtmosphereClient.class);

// Serial
SerializedClient client = ClientFactory.getDefault().newClient(SerializedClient.class);

If your application doesn't use any framework specific configurations, it is recommended to just do:

// Default, works with all framework
Client client = ClientFactory.getDefault().newClient();

// Atmosphere
Client client = ClientFactory.getDefault().newClient(AtmosphereClient.class);

// Serial
Client client = ClientFactory.getDefault().newClient(SerializedClient.class);

Top

Function

Functions are the central piece of wAsync. Function wil be called by the library when an event or message is received by the library. Function are defined as

public interface Function<T> {
    void on(T t);
}

There are two types of Functions: the one associated with an Event, and the one associated with a message received from a remote server.

Event based Function

You can define Functions for the following Event: Supported event are:

    /**
     * This event is fired when the connection is opened. This event is only fired once.
     */
    OPEN,

    /**
     * This event is fired when the connection gets closed. This event is only fired once.
     */
    CLOSE,

    /**
     * This event is fired every time a new message is received.
     */
    MESSAGE,

    /**
     * This event is fired every time the connection is re-opened
     */
    REOPENED,

    /**
     * This event is fired whenever unexpected error happens. This event is only fired once.
     */
    ERROR,

    /**
     * This event is fired when the response's status is received. This event is only fired once.
     */
    STATUS,

    /**
     * This event is fired when the response's headers is received. This event is only fired once.
     */
    HEADERS,

    /**
     * This event is fired when the response's bytes are received.
     */
    MESSAGE_BYTES,

    /**
     * This event is fired when the connection has been established. The Transport propagated is the one that worked.
     * This event is only fired once.
     */
    TRANSPORT

For example, you can define a Function that will tell you which transport was used:

new Function<Request.TRANSPORT>() {
    @Override
    public void on(Request.TRANSPORT t) {
        ....
    }

Or to get notified when the connection is opened

new Function<String>() {
    @Override
    public void on(String openString) {
        ...;
    }

You will see in the Socket section below how to associate an Event with a Function. Top

Message based Function

Message based Functions are used to handle the server response. If the server is sending String based message, a simple function can be defined as:

new Function<String>() {
    @Override
    public void on(String message) {
        ...;
    }

With the help of Decoder (see section below), you can also have typed Function. For example, if a Decoder has been defined to decode String into instance of StringBuilder, a Function can be defined as:

new Function<StringBuilder>() {
    @Override
    public void on(StringBuilder builder) {
        ...;
    }

As you will see below, Function can be associated with an Event, pre-defined String message and event Object by configuring your own FunctionResolver. Top

Creating the Request

RequestBuilder

Next step is to create the Request object. First, you need to obtain a RequestBuilder

RequestBuilder requestBuilder = client.newRequestBuilder()
                .method(Request.METHOD.GET)
                .uri("http://127.0.0.1:8080")
                .transport(Request.TRANSPORT.WEBSOCKET)
                .transport(Request.TRANSPORT.STREAMING);

If you are using specific Client implementation, extra methods are available. For example:

RequestBuilder requestBuilder = client.newRequestBuilder()
                .method(Request.METHOD.GET)
                .uri("http://127.0.0.1:8080")
                .trackMessageLength(true)
                .transport(Request.TRANSPORT.WEBSOCKET)
                .transport(Request.TRANSPORT.STREAMING);

The above will add support for the message's length feature of the Atmosphere Protocol. Top

Encoder

You may want to encode messages before they are sent to the remote server. An Encoder is defined as:

public interface Encoder<U, T> {
    T encode(U s);
}

For example, the following encoder will encode a Message's instance into a Reader

requestBuilder.encoder(
  new Encoder<Messsage, Reader>() {
     @Override
     public Reader encode(Message s) {
        return writeValueAsReader(s);
     }
}))

Another example is to use the Jackson library for encoding object:

requestBuilder.encoder(
  new Encoder<Message, String>() {
    @Override
    public String encode(Message data) {
        try {
            return mapper.writeValueAsString(data);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
))

You can add more that one Encoder for different classes, and you can also chain Encoders, e.g one Encoder result will be used by another encoder:

requestBuilder.encoder(
   new Encoder<String, POJO>() {
        @Override
        public POJO encode(String s) {
            return new POJO("<-" + s + "->");
        }
    })
    .encoder(new Encoder<POJO, Reader>() {
        @Override
        public Reader encode(POJO s) {
            return new StringReader(s.message);
        }
    })

Top

Decoder

When the library receives a message from a remote server, it is possible to decode the received message into another object. The resulting object will be used by the library to invoke it's associated Function. A Decoder is defined as:

public interface Decoder<U, T> {

    T decode(Event e, U s);

}

The library always invokes Decoders with some information about the underlying connection life cycle. The Event enum can be used to decode messages based on the state of the connection. For example, when the connection is established, you can define a Function that will be called as a result of a Decoder:

Client client = ClientFactory.getDefault().newClient();
RequestBuilder request = client.newRequestBuilder()
        .method(Request.METHOD.GET)
        .uri("http://127.0.0.1:8080")
        .decoder(new Decoder<String, ConnectionOpen>() {
            @Override
            public ConnectionOpen decode(Event e, String s) {
                if (e.equals(Event.OPEN)) {
                   return new ConnectionOpen(s);
                }
            }
        })
        .transport(Request.TRANSPORT.WEBSOCKET);

final Socket socket = client.create();
socket.on(Event.OPEN, new Function<ConnectionOpen>() {
    @Override
    public void on(ConnectionOpen t) {
        // The Connection is opened
        socket.fire("some message");
    }
})

Top

Creating the request

Once Encoders/Decoders has been set, you can create the Request object that will be used by a Socket by doing

Request request = client.newRequestBuilder()
        .method(Request.METHOD.GET)
        .uri("http://127.0.0.1:8080")
        .decoder(new Decoder<String, ConnectionOpen>() {
            @Override
            public ConnectionOpen decode(Event e, String s) {
                if (e.equals(Event.OPEN)) {
                   return new ConnectionOpen(s);
                }
            }
        })
        .transport(Request.TRANSPORT.WEBSOCKET); 

Top

Socket

Creating the Socket

A Socket represents a connection to the server. For WebSocket, it's a single connection where for other transport, it's always two: one which is constantly connected (called the suspended connection), the other one used to push data from the client to the server. To create a Socket, just do:

Socket socket = client.create();

You can also set some Options on the Socket, by doing:

OptionsBuilder builder = client.newOptionsBuilder();
builder.requestTimeoutInSeconds(30);
Socket socket = client.create(builder.build());

You can also set specific framework Options. For example, with Serial Support, you can do:

AtmosphereOptionsBuilder builder = client.newOptionsBuilder();
builder.serializedFireStage(new SerializedFireStage() {...});
Socket socket = client.create(builder.build());

Top

Assigning Function to Socket

Next step is to associate Function with Event of String message. For example, to associate a Function with an Event, you just do:

socket.on(Event.OPEN, new Function<ConnectionOpen>() {
    @Override
    public void on(ConnectionOpen t) {
        // The Connection is opened
        socket.fire("some message");
    }
});

To associate Function with message

socket.on(Event.MESSAGE, new Function<String>() {
    @Override
    public void on(String message t) {
        ....
    }
});

If a Decoder has been defined for a Function's Type:

Request request = client.newRequestBuilder()
        .method(Request.METHOD.GET)
        .uri("http://127.0.0.1:8080")
        .decoder(new Decoder<String, Message>() {
            @Override
            public Message decode(Event e, String s) {
                return new Message(s);
            }
        });

socket.on(Event.MESSAGE, new Function<Message>() {
    @Override
    public void on(Message message t) {
        ....
    }
});

Top

Opening a connection

To open a connection to your server, you just need to pass the previously constructed Request

socket.open(request);

Top

Sending message

To send message

socket.fire("message");

If you have defined Encoder, you can also send Object:

socket.fire(new Message("Hello"));

You can also send multiple messages by doing:

socket.fire(new Message("Hello").fire(new Messsage("World");

Putting all together: a Chat Client

The code below demonstrates how to write a Chat application using the Atmosphere Framework. For the server side component, see the sample

        AtmosphereClient client = ClientFactory.getDefault().newClient(AtmosphereClient.class);
        RequestBuilder request = client.newRequestBuilder()
                .method(Request.METHOD.GET)
                .uri("http//127.0.0.1/chat")
                .trackMessageLength(true)
                .encoder(new Encoder<Message, String>() {
                    @Override
                    public String encode(Message data) {
                        try {
                            return mapper.writeValueAsString(data);
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                    }
                })
                .decoder(new Decoder<String, Message>() {
                    @Override
                    public Message decode(Event type, String data) {

                        data = data.trim();

                        // Padding from Atmosphere, skip
                        if (data.length() == 0) {
                            return null;
                        }

                        if (type.equals(Event.MESSAGE)) {
                            try {
                                return mapper.readValue(data, Message.class);
                            } catch (IOException e) {
                                logger.debug("Invalid message {}", data);
                                return null;
                            }
                        } else {
                            return null;
                        }
                    }
                })
                .transport(Request.TRANSPORT.WEBSOCKET)
                .transport(Request.TRANSPORT.SSE)
                .transport(Request.TRANSPORT.LONG_POLLING);

        Socket socket = client.create();
        socket.on("message", new Function<Message>() {
            @Override
            public void on(Message t) {
                Date d = new Date(t.getTime());
                logger.info("Author {}: {}", t.getAuthor() + "@ " + d.getHours() + ":" + d.getMinutes(), t.getMessage());
            }
        }).on(new Function<Throwable>() {

            @Override
            public void on(Throwable t) {
                t.printStackTrace();
            }

        }).on(Event.CLOSE.name(), new Function<String>() {
            @Override
            public void on(String t) {
                logger.info("Connection closed");
            }
        }).open(request.build());

        logger.info("Choose Name: ");
        String name = null;
        String a = "";
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        while (!(a.equals("quit"))) {
            a = br.readLine();
            if (name == null) {
                name = a;
            }
            socket.fire(new Message(name, a));
        }
        socket.close();

Advanced Topics

Sharing AHC runtime

By default, AHC runtime is not shared between instances of Client. You can turn it on by calling OptionsBuilder.runtimeShared