À la découverte de Vert.x


 

Un serveur web innovant

Créé en 2011, Vert.x est un framework événementiel pour la JVM qui est inspiré de Node.js. Réputé pour sa performance et sa montée en charge rapide, ce framework permet de créer des serveurs Web des plus efficaces. Asynchrone, polyglotte et scalable, Vert.x s’appuie sur un concept-clé que nous détaillerons dans cet article : le verticle.

Multilangage

Vert.x peut être utilisé en Java, Groovy, JavaScript, Coffeescript, Ruby et Python. En ce qui concerne Java, cette application est construite autour du framework asynchrone Netty. Cette diversité de langage permet à Vert.x de se positionner sur différents fronts.

Multiprotocole

Vert.x peut communiquer en utilisant les protocoles les plus courants comme TCP, HTTP et Websockets, y compris à travers un chiffrement SSL. Il peut aussi communiquer avec des applications JavaScript en utilisant SockJS.

Scalable

Vert.x est extrêmement scalable. Il peut être distribué entre plusieurs machines. Cela sera transparent pour le code de l’application. En effet, comme nous le verrons à travers la notion de verticle, la mécanique de communication se définit de la même manière localement au sein d’une machine qu’entre différentes machines. Clairement, étendre son application dans ces conditions devient un jeu d’enfant.

Vert.x garantit également que les envois de messages des verticles seront bien reçus dans l’ordre dans lequel ils ont été émis, sans pour autant pouvoir dire que la méthode send est synchrone. Hormis quelques rares exceptions comme les workers (décrit un peu plus loin dans l’article) Vert.x travaille de manière non bloquante.

La réponse est quant à elle attendue de manière asynchrone et sans ordre. Cette mécanique est une des grandes forces de Vert.x. C’est en partie grâce à elle que le framework peut afficher de si bonnes performances, notamment en élasticité.

Qu’est-ce qu’un verticle ?

Les verticles sont des blocs de code qui sont déployés et exécutés par Vert.x. Une instance Vert.x maintient N threads de boucles d’événements par défaut. Les verticles peuvent être écrits dans n’importe quelle langue que Vert.x prend en charge et une seule application peut inclure des verticles écrits dans plusieurs langages.
À certains égards, un verticle est similaire à une servlet ou à un EJB piloté par message en Java EE.

Une application est généralement composée de nombreuses instances verticles s’exécutant parallèlement dans la même instance Vert.x. Les différentes instances verticles communiquent entre elles en envoyant des messages sur le bus d’événement.

Vehicle et Thread

Chaque instance de verticle s’exécute toujours dans le même thread et dispatche ensuite la réponse. Vert.x est capable de gérer plusieurs events loops à la demande, mettant à profit le nombre de processeurs à disposition. Un event loop est une méthode pour vérifier s’il existe un nouvel événement dans la boucle infinie et appeler un event handler lorsqu’un événement a été reçu. Pour utiliser la méthode event loop, toutes les E / S doivent être gérées comme des événements.

Vert.x_Event Loop

Chacune de ces boucles est nommée verticle.

Ce fonctionnement permet qu’aucune action ne soit bloquante dans le thread principal, ce qui empêche le risque de ralentissement dû à trop de demandes simultanées.

Working vehicle

Parfois, il est nécessaire d’avoir des actions bloquantes. Par exemple, persister des données requiert une action dont on doit attendre la réponse. C’est un working verticle qui permet d’exécuter des actions bloquantes et un handler traitera la réponse.

Ainsi, pour accéder à un module de persistance, il faut que l’action soit bloquante. Dans ce cas-là, un message est publié dans un bus d’événements, ce message sera traité par un worker, géré dans un pool de threads dédié.

Pré-requis

Pour utiliser Vert.x, vous pouvez exploiter un IDE avec l’outil de build le plus adapté pour vous. Pour cet article, j’utilise un exemple en Java avec Maven.

Ajouter la dépendance vers Vert.x et les test unitaires :

<dependency>
	<groupId>io.vertx</groupId>
	<artifactId>vertx-core</artifactId>
	<version>3.0.0</version>
</dependency>
	
<dependency>
	<groupId>junit</groupId>
	<artifactId>junit</artifactId>
	<version>4.12</version>
	<scope>test</scope>
</dependency>

<dependency>
  	<groupId>io.vertx</groupId>
  	<artifactId>vertx-unit</artifactId>
  	<version>3.0.0</version>
 	<scope>test</scope>
</dependency>

Running

Pour lancer les verticles, utilisez la classe “io.vertx.core.Launcher” et indiquez votre class “run com.monpackage.vertx.maclass” comme argument de programme.

Example

Côté serveur

Voici un petit exemple de serveur affichant juste “helloworld”.

public class HelloWorld extends AbstractVerticle {

	public void start(final Future<Void> startFuture) throws Exception {
		vertx.createHttpServer()
	    	.requestHandler(req -> req.response()
	        	.putHeader("content-type", "text/html")
	        	.end("<html><body><h1>Hello World</h1></body></html>"))
	    	.listen(8080, res -> {
	        	if (res.succeeded()) {
	            	startFuture.complete();
	        	} else {
	            	startFuture.fail(res.cause());
	        	}
	    	});
	}
}

Test Unitaire

Ce test vérifie juste que le body du HTML contient Hello

@RunWith(VertxUnitRunner.class)
public class MyFirstVerticleTest {
	
  private Vertx vertx;
	
  @Before
  public void setUp(TestContext context) {
    vertx = Vertx.vertx();
    vertx.deployVerticle(MyFirstVerticle.class.getName(),
        context.asyncAssertSuccess());
  }
	
  @After
  public void tearDown(TestContext context) {
    vertx.close(context.asyncAssertSuccess());
  }
	
  @Test
  public void testMyApplication(TestContext context) {
    final Async async = context.async();
	
    vertx.createHttpClient().getNow(8080, "localhost", "/",
     response -> {
      response.handler(body -> {
        context.assertTrue(body.toString().contains("Hello"));
        async.complete();
      });
    });
  }
}

Envoi de message par bus d’événement

Voici un exemple pour envoyer des messages par un bus event

public class ConsumerVerticle extends AbstractVerticle{
    @Override
    public void start() throws Exception {
        System.out.println("Start of Consumer Verticle");
        
        final String address = "HELLO";
        
        final EventBus bus = vertx.eventBus();
        final MessageConsumer<String> consumer = bus.consumer(address);
        consumer.handler(message -> {
            System.out.println("incoming message: " + message.body());
        });
    }
}


public class SenderVerticle extends AbstractVerticle{
    @Override
    public void start() throws Exception {
        System.out.println("Start of Sender Verticle");
        
        final String address = "HELLO";
	 final String message = "I am ready";
        
        final EventBus bus = vertx.eventBus();
		bus.send(address, message);
    }
}

Si on veut envoyer des objets dans les bus events, il faut passer par des bus en mode cluster.
Ajoutons d’abord la dépendance :

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

Ajouter dans SenderVerticle l’objet à envoyer avec son codec. Si on n’a pas de codec, on va avoir une exception, expliquant qu’aucun codec n’a été indiqué pour cet objet du modèle.

final PersonneMessageCodec personneCodec = new PersonneMessageCodec();
bus.registerCodec(personneCodec);
        
final DeliveryOptions options = new DeliveryOptions().setCodecName(personneCodec.name());
	
vertx.setPeriodic(3000, (l) -> {
            final Personne pers = new Personne();
            pers.setName("test");
	     ...
            bus.send("object.address", pers, options);
        });
	
Ajouter dans ConsumerVerticle pour récupérer l’objet
bus.<Personne> consumer("object.address", message -> {
    final Personne pers = message.body();
    final String name = pers.getName();
    System.out.println("Name object: " + name);
});

Créer le codec de l’objet Personne :

public class PersonneMessageCodec implements MessageCodec<Personne, Persone> {
	
    @Override
    public void encodeToWire(Buffer buffer, Personne pers) {
        // Easiest ways is using JSON object
        final JsonObject jsonToEncode = new JsonObject();
        jsonToEncode.put("name", pers.getName());
	
        // Encode object to string
        final String jsonToStr = jsonToEncode.encode();
	
        // Length of JSON: is NOT characters count
        final int length = jsonToStr.getBytes().length;
	
        // Write data into given buffer
        buffer.appendInt(length);
        buffer.appendString(jsonToStr);
    }
	
    @Override
    public Personne decodeFromWire(int position, Buffer buffer) {
        // My custom message starting from this *position* of buffer
        int _pos = position;
	
        // Length of JSON
        final int length = buffer.getInt(_pos);
	
        // Get JSON string by it`s length
        // Jump 4 because getInt() == 4 bytes
        final String jsonStr = buffer.getString(_pos += 4, _pos += length);
        final JsonObject contentJson = new JsonObject(jsonStr);
	
        // Get fields
        final String name = contentJson.getString("name");
	
        // We can finally create the pers object
        final Personne pers = new Personne();
        pers.setName(name);
	
        return pers;
    }
	
    @Override
    public Personne transform(Personne pers) {
        return pers;
    }
	
    @Override
    public String name() {
        // Each codec must have a unique name.
      // This is used to identify a codec when sending a message and for unregistering codecs.
        return this.getClass().getSimpleName();
    }
	
    @Override
    public byte systemCodecID() {
        // Always -1
        return -1;
    }
}

Lancement du Consumer

public class ConsumerLauncher {
	
    public static void main(String[] args) {
        System.out.println("Start of Consumer Launcher");
        
        final ClusterManager mgr = new HazelcastClusterManager();
        final VertxOptions options = new VertxOptions().setClusterManager(mgr);
        Vertx.clusteredVertx(options, res -> {
            if (res.succeeded()) {
                System.out.println("res ok consume");
                final Vertx vertx = res.result();
                vertx.deployVerticle(ShopVerticle.class.getName());
            } else {
                System.out.println("FAIL !!!");
            }
        });
	
        System.out.println("End of Consumer Launcher");
    }
}

Lancement du Sender

public class SenderLauncher {
	
    public static void main(String[] args) throws InterruptedException {
        System.out.println("Start of Sender Launcher");
        
        final ClusterManager mgr = new HazelcastClusterManager();
	
        final VertxOptions options = new VertxOptions().setClusterManager(mgr);
        Vertx.clusteredVertx(options, res -> {
            if (res.succeeded()) {
                System.out.println("res ok send");
                final Vertx vertx = res.result();
                vertx.deployVerticle(SenderVerticle.class.getName());
            } else {
                System.out.println("FAIL !!!");
            }
        });        
	
        System.out.println("End of Sender Launcher");
    }
}

Avec tout cela, on peut maintenant envoyer et recevoir des objets.

Exemple d’un Worker

Création d’un worker en mode bloquant

WorkerExecutor executor = vertx.createSharedWorkerExecutor("my-worker-pool");
executor.executeBlocking(future -> {
  // Execute cette tâche en mode bloquant
  String result = someAPI.blockingMethod("hello");
  future.complete(result);
}, res -> {
  System.out.println("The result is: " + res.result());
});

Un framework à garder à l’oeil

Les points forts de Vert.x sont :

  • Sa capacité à encaisser beaucoup de requêtes en parallèle.
  • Sa possibilité de scaler en fonction du nombre de processeurs et de scaler sur plusieurs serveurs. On peut passer par des bus en mode cluster.
  • Sa très bonne productivité surtout si on s’appuie sur un langage dynamique comme python.

Comparaison du nombre de requêtes avec Node.js :

Vert.x_Comparaison1
Comparaison de la performance lorsque seulement la réponse 200 / OK a été retournée

Vert.x_Comparaison2
Comparaison des performances lorsqu’un fichier HTML statique de 72 octets est renvoyé

Vert.x possède une vraie richesse de fonctionnalités. Rien n’est nouveau dans ce framework, chaque partie est une réutilisation de frameworks ou de concepts déjà existants. Mais mis bout à bout, Vert.x propose un outil puissant et très productif. J’ai pu constater lors de la présentation de ce framework au Voxxed Day qu’il pouvait supporter une grande charge de connexions sans pour autant perdre en efficacité, tout en étant géré par un seul thread. Les changements effectués au niveau du code sont modifiés sur l’application presque instantanément et ne nécessitent pas un redémarrage du serveur. Ce framework est encore jeune, mais pourrait dans un avenir proche devenir très intéressant à utiliser.

Built With :

  • Vert.x – The web framework used
  • Maven – Dependency Management

Vous aimerez aussi...