Sunday, August 16, 2015

Write your own Chat service with Vert.x 3.0 - Part 1

Introduction

This is a multi-part tutorial on how to write a chat service with Vert.x 3.0.
Bring some knowledge about Java, JavaScript and maven (or gradle) and tolerance for overly long sentences and let’s have some fun!

What is Vert.x 3 again?

Vert.x is a reactive, actor-based, event-bus driven, asynchronous, polyglot tool-kit for the JVM. (Wow, that’s a mouthful.) Let’s look at what this means in detail:
Event-Bus Driven
At the heart of Vert.x lies an event bus with a very simple address scheme that supports publishing and sending messages, along with replying to messages. Payload is usually JSON or plain-text.
Reactive
Vert.x supports the popular RxJava library that allows you to write observers that react to sequences of events.
Actor-based
Most code in Vert.x is written as a so-called “Verticle”. In its simplest form a Verticle is a piece of single-threaded code that acts on events from the event bus.
Asynchronous
Any I/O in Vert.x is performed asynchronously. Instead of blocking the current thread and waiting for data to arrive, you usually specify a callback-handler that is called when data arrives. You can still run blocking code though (in separate worker threads).
Polyglot
Vert.x’s API is available in several programming languages. Verticles deployed in Java, JavaScript, Groovy, Python or Ruby can seamlessly communicate via the event bus. You are free to use – say – JavaScript to write your REST API and do the heavy lifting in some Java-based Verticles.
You can also think of Vert.x as the ‘multi-threaded Node.js of the Java world’. Kinda.
A few useful links
Vert.x documentation
Vert.x download

Another Chat service? Noooooo.

I understand. But a chat service is a good showcase for the capabilities of Vert.x. One of those being the event bus, which can be extended all the way to the browser.
Let’s quickly summarize some features we would like to have for our initial prototype:
  • Create a chat alias
  • See a list of channels
  • Join or create a chat channel
  • Send a message
  • Leave a chat channel
We’ll cover more features, such as presence and typing indicators, in later parts of this tutorial.
We’ll also be writing some funky HTML later on using RiotJS. Because, why not?
It looks like our list above already has identified some actors we need. For this exercise we’ll turn them into Vert.x actors and see what we can do with them.
We have a User using an alias, we have a Channel and there are Messages exchanged between Users of a Channel.
That seems easy enough.
Let’s get coding!
The source code for this tutorial is on github. Each part will be tagged as a release.

New Project! New Project! New Project!

I’m using Intellij here to set up a new Maven-based project. Feel free to use your favorite IDE.
Create a new Maven project called Chatty, pick any group ID, and add this to your pom.xml:
<properties>
    <vertx-version>3.0.0</vertx-version>
</properties>

<dependencies>
    <dependency>
        <groupId>io.vertx</groupId>
        <artifactId>vertx-core</artifactId>
        <version>${vertx-version}</version>
    </dependency>

    <dependency>
        <groupId>io.vertx</groupId>
        <artifactId>vertx-web</artifactId>
        <version>${vertx-version}</version>
    </dependency>

    <dependency>
        <groupId>io.vertx</groupId>
        <artifactId>vertx-lang-js</artifactId>
        <version>${vertx-version}</version>
    </dependency>

</dependencies>
This will give us the core Vert.x libraries, the freshly baked web libraries for our REST API, and the JavaScript language binding.
We also need to tell Maven that we are living in the future and want to use Java 8 for compilation and where our JavaScript code will be hiding (see <resources> tag)
<build>
<resources>
        <resource><directory>src/main/js</directory></resource>
    </resources>
<pluginManagement>
    <plugins>
        <plugin>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.1</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
            </configuration>
        </plugin>
    </plugins>
</pluginManagement>
</build>
All set.
Let’s give it a spin.
If you are tired of copying and pasting stuff, you can get the code like this:
git clone https://github.com/beders/chatty.git
cd chatty
git checkout part1

Hello Chatty

Let’s start with a simple HTTP service written in JavaScript.
If you haven’t cleverly anticipated it yet, create a js directory in src/main and add a HelloChatty.js file to it. Here’s our first working HTTP server written with Vert.x!
var html = '<link rel="stylesheet" href="https://goo.gl/fUS2lx">'
    + '<h1 style="color:navy;position:relative;top:50%;transform:translateY(-50%);"'
    + 'align="center" class="animated infinite rubberBand">Hello Chatty</h1>';

var Router = require("vertx-web-js/router");
var router = Router.router(vertx);
router.route('/chatty').handler(function (routingContext) {
    routingContext.response().putHeader("Content-Type", "text/html")
        .end(html);
});
vertx.createHttpServer().requestHandler(router.accept).listen(8888);
print('Go to http://localhost:8888/chatty');
You can run this via command line from your project’s root directory (if you have downloaded and unpacked the full version of Vert.x 3.0):
$ ~/Downloads/vert.x-3.0.0/bin/vertx run src/main/js/HelloChatty.js 
Or you can run it from your IDE using io.vertx.core.Starter as main class and adding run HelloChatty.js as arguments. (In IDEA, use an Application configuration)
Go to http://localhost:8888/chatty and admire the bad color choice and obnoxious CSS animation!
Wait! What? A full web-server in 4 lines of code?
Let’s look at the details.
Ignoring the bad HTML in the first few lines, let’s look at this Router thingie more closely:
var Router = require("vertx-web-js/router");
This loads the router module from the web module (a full list is available here)
var router = Router.router(vertx);
The JavaScript environment contains a pre-defined vertx object which is used to access the vertx core classes and functions. Here we create a new router and tell it to use this vertx object. We’ll see how to do this with Java later on.
What is a router anyway?
A router allows you to attach functions to the individual parts of an HTTP request! In the simplest case, each part of an URL can be matched to one or more routes.
A handler function can then be attached to a route to handle the HTTP request.
In short, a router sits between HTTP requests and your code and handles mapping the request method, path, header and parameters to your functions.
In our example, we define a route and attach a handler for the /chatty part of the URL:
router.route('/chatty').handler(function (routingContext) {
    routingContext.response().putHeader("Content-type", "text/html")
        .end(html);
});
This in essence means, whenever any kind of HTTP request is received that starts with /chatty, call the handler function.
The function takes a single parameter routingContext which gives you access to the HTTP request and response.
In our simple example, we are declaring the response to be proper HTML and then use the end(...) to end the response with a piece of not-really-HTML-conformant-garbage-a-browser-will-still-display.
vertx.createHttpServer().requestHandler(router.accept).listen(8888);
Last but not least we set up the actual HttpServer listening to port 8888. The requestHandler function expects a handler for all incoming HTTP requests which our router provides.

Very Verticle!

Let’s write a Java verticle now to illustrate how they are in essence single-threaded actors which use the event bus to communicate.
For a real chat service, we need some function that provides a list of channels and returns it. A perfect excuse for a Verticle.
A Java verticle is simply an instance of a subclass of AbstractVerticle.
Vert.x gives a Verticle a few excellent guarantees:
  • once deployed, its code will always run in the same event-loop thread which makes synchronizing on instance data unnecessary
  • messages will be received in order
But it also demands a few things from your code:
  • don’t block! No Thread.sleep, no synchronous I/O etc. in your Verticle. There are still ways to do it (in worker threads), but we usually can just rely on the Vert.x API
  • don’t call other verticles directly (use the event bus instead). This is important for scalability issues as the event bus will find an available verticle for you (if you deployed more than one of the same kind)

Channel Manager - does almost nothing

That said, let’s look at our Verticle ‘ChannelManager’:
 public class ChannelManager extends AbstractVerticle {
    Map<String, ChannelInfo> channelMap = new HashMap<>();

    @Override
    public void start() throws Exception {
        new ChannelInfo("#general").registerWith(channelMap);
        vertx.eventBus().consumer("channels.list", msg -> {
            msg.reply(new JsonObject().put("channels", new JsonArray(channelMap.values().stream().map(ci -> ci.toJson()).collect(Collectors.toList()))));
        });
        vertx.eventBus().consumer("channels.join", (Message<JsonObject> msg) -> {
            // sanity checks for the values
            ChannelInfo channel = channelMap.computeIfAbsent(msg.body().getString("name"), name -> new ChannelInfo(name));
            msg.reply(channel.toJson());
        });
    }

    static class ChannelInfo {
        String name, address;

        ChannelInfo(String name) {
            this.name = name;
            this.address = "channel." + name; // we could use UUID.getRandom().toString here as well
        }
        JsonObject toJson() {
            return new JsonObject().put("address", address).put("name", name);
        }

        void registerWith(Map<String, ChannelInfo> channelMap) {
            channelMap.put(name, this);
        }
    }
}
We use a simple Map<String,ChannelInfo> to store information about all existing channels.
In start(), stuff is getting more interesting.
First we add a standard channel (so we have something to send).
Then we register with the even bus to consume any messages arriving on address ‘channels.list’. The message handler is a simple lambda expression that gets passed a Message<?> object.
We can use that object to reply to the sender of the message! In this case, we use some stream() magic to assemble a JSON object with the list of channels.

Test, Decker-Unit!

Let’s get fancy and write a unit test for this. We will be using bog-standard JUnit 4 for this:
public class ChannelManagerTest {
    private static Vertx vertx;

    @BeforeClass
    public static void setUp() throws Exception {
        CountDownLatch latch = new CountDownLatch(1);
        vertx = Vertx.vertx();
        vertx.deployVerticle(new ChannelManager(), res -> {
            latch.countDown();
        });
        latch.await();
    }

    @Test
    public void listChannels() throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(1);
        vertx.eventBus().send("channels.list", null, reply -> {
            assertNotNull(reply.result().body());
            System.out.println(reply.result().body());
            latch.countDown();
        });
        if (!latch.await(5, TimeUnit.SECONDS)) {
            fail();
        }
    }
}
@BeforeClass is where we set up our test environment and where we deploy a single instance of the ChannelManager. Note that deployment is asynchronous in nature, we are registering a callback that then counts down on the latch to allow the JUnit thread to finish set up.
We’ll be using this little trick a few more times (There are a few extensions for JUnit out there that deal with the asynch nature of things. We’ll try to keep in simple by relying on a CountDownLatch.
Test method listChannels uses the Vert.x event bus to send an (empty) message to the address bus address ‘“channels.list”’. Luckily, that is exactly the address the ChannelManager is listening on!
Note that listChannels has no idea about the existence of ChannelManager. It just knows that when it sends something to "channels.list", it will get a reply.
We could exchange our implementation of ChannelManager with a Ruby one and as long as we keep the message format the same, we are good to go!
Our test code also has a reply handler with a lambda expression:
It expects to get a reply with a list of channels.
Run the test and hopefully it will complete and give you this output:
{"channels":[{"name":"#general","address":"channel.#general"}]}
Neat!
We have a list of channel objects with a name and an event bus address we will use later.

Listing Channels the traditional way

Getting the list of channels via the event bus works now, but we also want to expose it as a REST resource.
Open HelloChatty.js and add the following on line 5 or so:
vertx.deployVerticle('us.monoid.vertx.chatty.ChannelManager');
This deploys a single instance of the ChannelManager verticle.
And we need to add a simple REST API to get a list of channels.
router.route('GET', '/chatty/channels').handler(function(routingContext) {
    vertx.eventBus().send('channels.list', null, function(reply, err) {
        if (err == null) {
            var channelList = reply.body();
            routingContext.response().putHeader("Content-Type", "application/json").end(JSON.stringify(channelList));
        }
    });
});
If a client does a GET request on /chatty/channels, we will send a message to the event bus, register a function as reply handler and - if we get a valid reply -(which is automatically converted into a JS object), stringify the channel list and send it as payload.
Now, run HelloChatty again.
If your IDE is clever, the classpath is already set up.
If you run vert.x from the command line, try this:
~/Downloads/vert.x-3.0.0/bin/vertx run src/main/js/HelloChatty.js -cp target/classes
Then point your browser to http://localhost:8888/chatty/channels
Working? Not working?
Let me know in the comments.

Sending a chat message

With a list of channels in hand we have enough information to actually chat directly via the event bus (in later parts we might re-visit that decision).
vertx.eventBus().publish(channel.address, {message:'Hello World', alias: 'beders' });
This snippet above will send a message to ALL listeners of a chat channel, which is what we will be using in part 2, where we’ll have a first prototype ready to go that can actually - you know - send chat messages between users.

No comments :

Post a Comment