<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-core</artifactId>
<version>4.5.11</version>
</dependency>
Vert.x Core Manual
At the heart of Vert.x is a set of Java APIs that we call Vert.x Core
Vert.x core provides functionality for things like:
-
Writing TCP clients and servers
-
Writing HTTP clients and servers including support for WebSockets
-
The Event bus
-
Shared data - local maps and clustered distributed maps
-
Periodic and delayed actions
-
Deploying and undeploying Verticles
-
Datagram Sockets
-
DNS client
-
File system access
-
Virtual threads
-
High availability
-
Native transports
-
Clustering
The functionality in core is fairly low level - you won’t find stuff like database access, authorisation or high level web functionality here - that kind of stuff you’ll find in Vert.x ext (extensions).
Vert.x core is small and lightweight. You just use the parts you want. It’s also entirely embeddable in your existing applications - we don’t force you to structure your applications in a special way just so you can use Vert.x.
You can use core from any of the other languages that Vert.x supports. But here’s a cool bit - we don’t force you to use the Java API directly from, say, JavaScript or Ruby - after all, different languages have different conventions and idioms, and it would be odd to force Java idioms on Ruby developers (for example). Instead, we automatically generate an idiomatic equivalent of the core Java APIs for each language.
From now on we’ll just use the word core to refer to Vert.x core.
If you are using Maven or Gradle, add the following dependency to the dependencies section of your project descriptor to access the Vert.x Core API:
-
Maven (in your
pom.xml
):
-
Gradle (in your
build.gradle
file):
dependencies {
compile 'io.vertx:vertx-core:4.5.11'
}
Let’s discuss the different concepts and features in core.
In the beginning there was Vert.x
You can’t do much in Vert.x-land unless you can communicate with a Vertx
object!
It’s the control centre of Vert.x and is how you do pretty much everything, including creating clients and servers, getting a reference to the event bus, setting timers, as well as many other things.
So how do you get an instance?
If you’re embedding Vert.x then you simply create an instance as follows:
Vertx vertx = Vertx.vertx();
Most applications will only need a single Vert.x instance, but it’s possible to create multiple Vert.x instances if you require, for example, isolation between the event bus or different groups of servers and clients. |
Specifying options when creating a Vertx object
When creating a Vert.x object you can also specify options if the defaults aren’t right for you:
Vertx vertx = Vertx.vertx(new VertxOptions().setWorkerPoolSize(40));
The VertxOptions
object has many settings and allows you to configure things like clustering, high availability, pool sizes and various other settings.
Creating a clustered Vert.x object
If you’re creating a clustered Vert.x (See the section on the event bus for more information on clustering the event bus), then you will normally use the asynchronous variant to create the Vertx object.
This is because it usually takes some time (maybe a few seconds) for the different Vert.x instances in a cluster to group together. During that time, we don’t want to block the calling thread, so we give the result to you asynchronously.
Are you fluent?
You may have noticed that in the previous examples a fluent API was used.
A fluent API is where multiple methods calls can be chained together. For example:
request.response().putHeader("Content-Type", "text/plain").end("some text");
This is a common pattern throughout Vert.x APIs, so get used to it.
Chaining calls like this allows you to write code that’s a little bit less verbose. Of course, if you don’t like the fluent approach we don’t force you to do it that way, you can happily ignore it if you prefer and write your code like this:
HttpServerResponse response = request.response();
response.putHeader("Content-Type", "text/plain");
response.write("some text");
response.end();
Don’t call us, we’ll call you.
The Vert.x APIs are largely event driven. This means that when things happen in Vert.x that you are interested in, Vert.x will call you by sending you events.
Some example events are:
-
a timer has fired
-
some data has arrived on a socket,
-
some data has been read from disk
-
an exception has occurred
-
an HTTP server has received a request
You handle events by providing handlers to the Vert.x APIs. For example to receive a timer event every second you would do:
vertx.setPeriodic(1000, id -> {
// This handler will get called every second
System.out.println("timer fired!");
});
Or to receive an HTTP request:
server.requestHandler(request -> {
// This handler will be called every time an HTTP request is received at the server
request.response().end("hello world!");
});
Some time later when Vert.x has an event to pass to your handler Vert.x will call it asynchronously.
This leads us to some important concepts in Vert.x:
Don’t block me!
With very few exceptions (i.e. some file system operations ending in 'Sync'), none of the APIs in Vert.x block the calling thread.
If a result can be provided immediately, it will be returned immediately, otherwise you will usually provide a handler to receive events some time later.
Because none of the Vert.x APIs block threads that means you can use Vert.x to handle a lot of concurrency using just a small number of threads.
With a conventional blocking API the calling thread might block when:
-
Reading data from a socket
-
Writing data to disk
-
Sending a message to a recipient and waiting for a reply.
-
… Many other situations
In all the above cases, when your thread is waiting for a result it can’t do anything else - it’s effectively useless.
This means that if you want a lot of concurrency using blocking APIs then you need a lot of threads to prevent your application grinding to a halt.
Threads have overhead in terms of the memory they require (e.g. for their stack) and in context switching.
For the levels of concurrency required in many modern applications, a blocking approach just doesn’t scale.
Reactor and Multi-Reactor
We mentioned before that Vert.x APIs are event driven - Vert.x passes events to handlers when they are available.
In most cases Vert.x calls your handlers using a thread called an event loop.
As nothing in Vert.x or your application blocks, the event loop can merrily run around delivering events to different handlers in succession as they arrive.
Because nothing blocks, an event loop can potentially deliver huge amounts of events in a short amount of time. For example a single event loop can handle many thousands of HTTP requests very quickly.
We call this the Reactor Pattern.
You may have heard of this before - for example Node.js implements this pattern.
In a standard reactor implementation there is a single event loop thread which runs around in a loop delivering all events to all handlers as they arrive.
The trouble with a single thread is it can only run on a single core at any one time, so if you want your single threaded reactor application (e.g. your Node.js application) to scale over your multi-core server you have to start up and manage many different processes.
Vert.x works differently here. Instead of a single event loop, each Vertx instance maintains several event loops. By default we choose the number based on the number of available cores on the machine, but this can be overridden.
This means a single Vertx process can scale across your server, unlike Node.js.
We call this pattern the Multi-Reactor Pattern to distinguish it from the single threaded reactor pattern.
Even though a Vertx instance maintains multiple event loops, any particular handler will never be executed concurrently, and in most cases (with the exception of worker verticles) will always be called using the exact same event loop. |
The Golden Rule - Don’t Block the Event Loop
We already know that the Vert.x APIs are non blocking and won’t block the event loop, but that’s not much help if you block the event loop yourself in a handler.
If you do that, then that event loop will not be able to do anything else while it’s blocked. If you block all of the event loops in Vertx instance then your application will grind to a complete halt!
So don’t do it! You have been warned.
Examples of blocking include:
-
Thread.sleep()
-
Waiting on a lock
-
Waiting on a mutex or monitor (e.g. synchronized section)
-
Doing a long lived database operation and waiting for a result
-
Doing a complex calculation that takes some significant time.
-
Spinning in a loop
If any of the above stop the event loop from doing anything else for a significant amount of time then you should go immediately to the naughty step, and await further instructions.
So… what is a significant amount of time?
How long is a piece of string? It really depends on your application and the amount of concurrency you require.
If you have a single event loop, and you want to handle 10000 http requests per second, then it’s clear that each request can’t take more than 0.1 ms to process, so you can’t block for any more time than that.
The maths is not hard and shall be left as an exercise for the reader.
If your application is not responsive it might be a sign that you are blocking an event loop somewhere. To help you diagnose such issues, Vert.x will automatically log warnings if it detects an event loop hasn’t returned for some time. If you see warnings like these in your logs, then you should investigate.
Thread vertx-eventloop-thread-3 has been blocked for 20458 ms
Vert.x will also provide stack traces to pinpoint exactly where the blocking is occurring.
If you want to turn off these warnings or change the settings, you can do that in the VertxOptions
object before creating the Vertx object.
Future results
Vert.x 4 use futures to represent asynchronous results.
Any asynchronous method returns a Future
object for the result of the call: a success or a failure.
You cannot interact directly with the result of a future, instead you need to set a handler that will be called when the future completes and the result is available, like any other kind of event.
FileSystem fs = vertx.fileSystem();
Future<FileProps> future = fs.props("/my_file.txt");
future.onComplete((AsyncResult<FileProps> ar) -> {
if (ar.succeeded()) {
FileProps props = ar.result();
System.out.println("File size = " + props.size());
} else {
System.out.println("Failure: " + ar.cause().getMessage());
}
});
Vert.x 3 provides a callback-only model. To allow an easy migration to Vert.x 4 we decided that each asynchronous method has also a callback version. The props method above has also a version props with a callback as method argument. |
Do not confuse futures with promises. If futures represent the "read-side" of an asynchronous result, promises are the "write-side". They allow you to defer the action of providing a result. In most cases, you don’t need to create promises yourself in a Vert.x application. Future composition and Future coordination provide you with the tools to transform and merge asynchronous results. However, if, in your codebase, you have legacy methods which use callbacks, you can leverage the fact that a promise extends |
Terminal operations like Consider a future on which 2 callbacks are registered: It is possible that the second callback is invoked before the first one. If you need such guarantee, consider using Future composition with |
Future composition
compose
can be used for chaining futures:
-
when the current future succeeds, apply the given function, that returns a future. When this returned future completes, the composition succeeds.
-
when the current future fails, the composition fails
FileSystem fs = vertx.fileSystem();
Future<Void> future = fs
.createFile("/foo")
.compose(v -> {
// When the file is created (fut1), execute this:
return fs.writeFile("/foo", Buffer.buffer());
})
.compose(v -> {
// When the file is written (fut2), execute this:
return fs.move("/foo", "/bar");
});
In this example, 3 operations are chained together:
-
a file is created
-
data is written in this file
-
the file is moved
When these 3 steps are successful, the final future (future
) will succeed. However, if one of the steps fails, the final future will fail.
Beyond this, Future
offers more: map
, recover
, otherwise
, andThen
and even a flatMap
which is an alias of compose
Future coordination
Coordination of multiple futures can be achieved with Vert.x futures
. It supports concurrent composition (run several async operations in parallel) and sequential composition (chain async operations).
Future.all
takes several futures arguments (up to 6) and returns a future that is succeeded when all the futures are succeeded and failed when at least one of the futures is failed:
Future<HttpServer> httpServerFuture = httpServer.listen();
Future<NetServer> netServerFuture = netServer.listen();
Future.all(httpServerFuture, netServerFuture).onComplete(ar -> {
if (ar.succeeded()) {
// All servers started
} else {
// At least one server failed
}
});
The operations run concurrently, the Handler
attached to the returned future is invoked upon completion of the composition. When one of the operation fails (one of the passed future is marked as a failure), the resulting future is marked as failed too. When all the operations succeed, the resulting future is completed with a success.
On success, the resultAt
method guarantees the results in the same order specified in the call to Future.all
. In the example above, regardless of which item completed first, the httpServer
result can be accessed using resultAt(0)
and the netServer
result can be accessed using resultAt(1)
.
Alternatively, you can pass a list (potentially empty) of futures:
Future.all(Arrays.asList(future1, future2, future3));
While the all
composition waits until all futures are successful (or one fails), the any
composition waits for the first succeeded future. Future.any
takes several futures arguments (up to 6) and returns a future that is succeeded when one of the futures is, and failed when all the futures are failed:
Future.any(future1, future2).onComplete(ar -> {
if (ar.succeeded()) {
// At least one is succeeded
} else {
// All failed
}
});
A list of futures can be used also:
Future.any(Arrays.asList(f1, f2, f3));
The join
composition waits until all futures are completed, either with a success or a failure. Future.join
takes several futures arguments (up to 6) and returns a future that is succeeded when all the futures are succeeded, and failed when all the futures are completed and at least one of them is failed:
Future.join(future1, future2, future3).onComplete(ar -> {
if (ar.succeeded()) {
// All succeeded
} else {
// All completed and at least one failed
}
});
A list of futures can be used also:
Future.join(Arrays.asList(future1, future2, future3));
CompletionStage interoperability
The Vert.x Future
API offers compatibility from and to CompletionStage
which is the JDK interface for composable asynchronous operations.
We can go from a Vert.x Future
to a CompletionStage
using the toCompletionStage
method, as in:
Future<String> future = vertx.createDnsClient().lookup("vertx.io");
future.toCompletionStage().whenComplete((ip, err) -> {
if (err != null) {
System.err.println("Could not resolve vertx.io");
err.printStackTrace();
} else {
System.out.println("vertx.io => " + ip);
}
});
We can conversely go from a CompletionStage
to Vert.x Future
using Future.fromCompletionStage
. There are 2 variants:
-
the first variant takes just a
CompletionStage
and calls theFuture
methods from the thread that resolves theCompletionStage
instance, and -
the second variant takes an extra
Context
parameter to call theFuture
methods on a Vert.x context.
In most cases the variant with a CompletionStage and a Context is the one you will want to use to respect the Vert.x threading model, since Vert.x Future are more likely to be used with Vert.x code, libraries and clients. |
Here is an example of going from a CompletionStage
to a Vert.x Future
and dispatching on a context:
Future.fromCompletionStage(completionStage, vertx.getOrCreateContext())
.flatMap(str -> {
String key = UUID.randomUUID().toString();
return storeInDb(key, str);
})
.onSuccess(str -> {
System.out.println("We have a result: " + str);
})
.onFailure(err -> {
System.err.println("We have a problem");
err.printStackTrace();
});
Verticles
Vert.x comes with a simple, scalable, actor-like deployment and concurrency model out of the box that you can use to save you writing your own.
This model is entirely optional and Vert.x does not force you to create your applications in this way if you don’t want to..
The model does not claim to be a strict actor-model implementation, but it does share similarities especially with respect to concurrency, scaling and deployment.
To use this model, you write your code as set of verticles.
Verticles are chunks of code that get deployed and run by Vert.x. A Vert.x instance maintains N event loop threads (where N by default is core*2) by default. Verticles can be written in any of the languages that Vert.x supports and a single application can include verticles written in multiple languages.
You can think of a verticle as a bit like an actor in the Actor Model.
An application would typically be composed of many verticle instances running in the same Vert.x instance at the same time. The different verticle instances communicate with each other by sending messages on the event bus.
Writing Verticles
Verticle classes must implement the Verticle
interface.
They can implement it directly if you like but usually it’s simpler to extend the abstract class AbstractVerticle
.
Here’s an example verticle:
public class MyVerticle extends AbstractVerticle { // Called when verticle is deployed public void start() { } // Optional - called when verticle is undeployed public void stop() { } }
Normally you would override the start method like in the example above.
When Vert.x deploys the verticle it will call the start method, and when the method has completed the verticle will be considered started.
You can also optionally override the stop method. This will be called by Vert.x when the verticle is undeployed and when the method has completed the verticle will be considered stopped.
Asynchronous Verticle start and stop
Sometimes you want to do something in your verticle start-up which takes some time and you don’t want the verticle to be considered deployed until that happens. For example you might want to start an HTTP server in the start method and propagate the asynchronous result of the server listen
method.
You can’t block waiting for the HTTP server to bind in your start method as that would break the Golden Rule.
So how can you do this?
The way to do it is to implement the asynchronous start method. This version of the method takes a Future as a parameter. When the method returns the verticle will not be considered deployed.
Some time later, after you’ve done everything you need to do (e.g. start the HTTP server), you can call complete on the Future (or fail) to signal that you’re done.
Here’s an example:
public class MyVerticle extends AbstractVerticle { private HttpServer server; @Override public void start(Promise<Void> startPromise) { server = vertx.createHttpServer().requestHandler(req -> { req.response() .putHeader("content-type", "text/plain") .end("Hello from Vert.x!"); }); // Now bind the server: server.listen(8080) .<Void>mapEmpty() .onComplete(startPromise); } }
Similarly, there is an asynchronous version of the stop method too. You use this if you want to do some verticle cleanup that takes some time.
public class MyVerticle extends AbstractVerticle { @Override public void stop(Promise<Void> stopPromise) { doSomethingThatTakesTime() .<Void>mapEmpty() .onComplete(stopPromise); } }
INFO: You don’t need to manually stop the HTTP server started by a verticle, in the verticle’s stop method. Vert.x will automatically stop any running server when the verticle is undeployed.
Verticle Types
There are two different types of verticles:
- Standard Verticles
-
These are the most common and useful type - they are always executed using an event loop thread. We’ll discuss this more in the next section.
- Worker Verticles
-
These run using a thread from the worker pool. An instance is never executed concurrently by more than one thread.
Standard verticles
Standard verticles are assigned an event loop thread when they are created and the start method is called with that event loop. When you call any other methods that takes a handler on a core API from an event loop then Vert.x will guarantee that those handlers, when called, will be executed on the same event loop.
This means we can guarantee that all the code in your verticle instance is always executed on the same event loop (as long as you don’t create your own threads and call it!).
This means you can write all the code in your application as single threaded and let Vert.x worry about the threading and scaling. No more worrying about synchronized and volatile any more, and you also avoid many other cases of race conditions and deadlock so prevalent when doing hand-rolled 'traditional' multi-threaded application development.
Worker verticles
A worker verticle is just like a standard verticle but it’s executed using a thread from the Vert.x worker thread pool, rather than using an event loop.
Worker verticles are designed for calling blocking code, as they won’t block any event loops.
If you don’t want to use a worker verticle to run blocking code, you can also run inline blocking code directly while on an event loop.
If you want to deploy a verticle as a worker verticle you do that with setThreadingModel
.
DeploymentOptions options = new DeploymentOptions().setThreadingModel(ThreadingModel.WORKER);
vertx.deployVerticle("com.mycompany.MyOrderProcessorVerticle", options);
Worker verticle instances are never executed concurrently by Vert.x by more than one thread, but can executed by different threads at different times.
Virtual thread verticles
A virtual thread verticle is just like a standard verticle but it’s executed using virtual threads, rather than using an event loop.
Virtual thread verticles are designed to use an async/await model with Vert.x futures.
If you want to deploy a verticle as a virtual thread verticle you do that with setThreadingModel
.
DeploymentOptions options = new DeploymentOptions().setThreadingModel(ThreadingModel.VIRTUAL_THREAD);
vertx.deployVerticle("com.mycompany.MyOrderProcessorVerticle", options);
this feature requires Java 21 |
Deploying verticles programmatically
You can deploy a verticle using one of the deployVerticle
method, specifying a verticle name or you can pass in a verticle instance you have already created yourself.
Deploying Verticle instances is Java only. |
Verticle myVerticle = new MyVerticle();
vertx.deployVerticle(myVerticle);
You can also deploy verticles by specifying the verticle name.
The verticle name is used to look up the specific VerticleFactory
that will be used to instantiate the actual verticle instance(s).
Different verticle factories are available for instantiating verticles in different languages and for various other reasons such as loading services and getting verticles from Maven at run-time.
This allows you to deploy verticles written in any language from any other language that Vert.x supports.
Here’s an example of deploying some different types of verticles:
vertx.deployVerticle("com.mycompany.MyOrderProcessorVerticle");
// Deploy a JavaScript verticle
vertx.deployVerticle("verticles/myverticle.js");
// Deploy a Ruby verticle verticle
vertx.deployVerticle("verticles/my_verticle.rb");
Rules for mapping a verticle name to a verticle factory
When deploying verticle(s) using a name, the name is used to select the actual verticle factory that will instantiate the verticle(s).
Verticle names can have a prefix - which is a string followed by a colon, which if present will be used to look-up the factory, e.g.
js:foo.js // Use the JavaScript verticle factory
groovy:com.mycompany.SomeGroovyCompiledVerticle // Use the Groovy verticle factory
service:com.mycompany:myorderservice // Uses the service verticle factory
If no prefix is present, Vert.x will look for a suffix and use that to lookup the factory, e.g.
foo.js // Will also use the JavaScript verticle factory
SomeScript.groovy // Will use the Groovy verticle factory
If no prefix or suffix is present, Vert.x will assume it’s a Java fully qualified class name (FQCN) and try and instantiate that.
How are Verticle Factories located?
Most Verticle factories are loaded from the classpath and registered at Vert.x startup.
You can also programmatically register and unregister verticle factories using registerVerticleFactory
and unregisterVerticleFactory
if you wish.
Waiting for deployment to complete
Verticle deployment is asynchronous and may complete some time after the call to deploy has returned.
If you want to be notified when deployment is complete you can deploy specifying a completion handler:
vertx
.deployVerticle("com.mycompany.MyOrderProcessorVerticle")
.onComplete(res -> {
if (res.succeeded()) {
System.out.println("Deployment id is: " + res.result());
} else {
System.out.println("Deployment failed!");
}
});
The completion handler will be passed a result containing the deployment ID string, if deployment succeeded.
This deployment ID can be used later if you want to undeploy the deployment.
Undeploying verticle deployments
Deployments can be undeployed with undeploy
.
Un-deployment is itself asynchronous so if you want to be notified when un-deployment is complete you can deploy specifying a completion handler:
vertx
.undeploy(deploymentID)
.onComplete(res -> {
if (res.succeeded()) {
System.out.println("Undeployed ok");
} else {
System.out.println("Undeploy failed!");
}
});
Specifying number of verticle instances
When deploying a verticle using a verticle name, you can specify the number of verticle instances that you want to deploy:
DeploymentOptions options = new DeploymentOptions().setInstances(16);
vertx.deployVerticle("com.mycompany.MyOrderProcessorVerticle", options);
This is useful for scaling easily across multiple cores. For example you might have a web-server verticle to deploy and multiple cores on your machine, so you want to deploy multiple instances to utilise all the cores.
Passing configuration to a verticle
Configuration in the form of JSON can be passed to a verticle at deployment time:
JsonObject config = new JsonObject().put("name", "tim").put("directory", "/blah");
DeploymentOptions options = new DeploymentOptions().setConfig(config);
vertx.deployVerticle("com.mycompany.MyOrderProcessorVerticle", options);
This configuration is then available via the Context
object or directly using the config
method. The configuration is returned as a JSON object so you can retrieve data as follows:
System.out.println("Configuration: " + config().getString("name"));
Accessing environment variables in a Verticle
Environment variables and system properties are accessible using the Java API:
System.getProperty("prop");
System.getenv("HOME");
High Availability
Verticles can be deployed with High Availability (HA) enabled. In that context, when a verticle is deployed on a vert.x instance that dies abruptly, the verticle is redeployed on another vert.x instance from the cluster.
To run an verticle with the high availability enabled, just append the -ha
switch:
vertx run my-verticle.js -ha
When enabling high availability, no need to add -cluster
.
More details about the high availability feature and configuration in the High Availability and Fail-Over section.
Running Verticles from the command line
You can use Vert.x directly in your Maven or Gradle projects in the normal way by adding a dependency to the Vert.x core library and hacking from there.
However you can also run Vert.x verticles directly from the command line if you wish.
To do this you need to download and install a Vert.x distribution, and add the bin
directory of the installation to your PATH
environment variable. Also make sure you have a Java JDK on your PATH
.
Vert.x supports Java from 8 to 17.
The JDK is required to support on the fly compilation of Java code. |
You can now run verticles by using the vertx run
command. Here are some examples:
# Run a JavaScript verticle vertx run my_verticle.js # Run a Ruby verticle vertx run a_n_other_verticle.rb # Run a Groovy script verticle, clustered vertx run FooVerticle.groovy -cluster
You can even run Java source verticles without compiling them first!
vertx run SomeJavaSourceFile.java
Vert.x will compile the Java source file on the fly before running it. This is really useful for quickly prototyping verticles and great for demos. No need to set-up a Maven or Gradle build first to get going!
For full information on the various options available when executing vertx
on the command line, type vertx
at the command line.
Causing Vert.x to exit
Threads maintained by Vert.x instances are not daemon threads so they will prevent the JVM from exiting.
If you are embedding Vert.x and you have finished with it, you can call close
to close it down.
This will shut-down all internal thread pools and close other resources, and will allow the JVM to exit.
The Context object
When Vert.x provides an event to a handler or calls the start or stop methods of a Verticle
, the execution is associated with a Context
. Usually a context is an event-loop context and is tied to a specific event loop thread. So executions for that context always occur on that exact same event loop thread. In the case of worker verticles and running inline blocking code a worker context will be associated with the execution which will use a thread from the worker thread pool.
To retrieve the context, use the getOrCreateContext
method:
Context context = vertx.getOrCreateContext();
If the current thread has a context associated with it, it reuses the context object. If not a new instance of context is created. You can test the type of context you have retrieved:
Context context = vertx.getOrCreateContext();
if (context.isEventLoopContext()) {
System.out.println("Context attached to Event Loop");
} else if (context.isWorkerContext()) {
System.out.println("Context attached to Worker Thread");
} else if (! Context.isOnVertxThread()) {
System.out.println("Context not attached to a thread managed by vert.x");
}
When you have retrieved the context object, you can run code in this context asynchronously. In other words, you submit a task that will be eventually run in the same context, but later:
vertx.getOrCreateContext().runOnContext( (v) -> {
System.out.println("This will be executed asynchronously in the same context");
});
When several handlers run in the same context, they may want to share data. The context object offers methods to store and retrieve data shared in the context. For instance, it lets you pass data to some action run with runOnContext
:
final Context context = vertx.getOrCreateContext();
context.put("data", "hello");
context.runOnContext((v) -> {
String hello = context.get("data");
});
The context object also let you access verticle configuration using the config
method. Check the Passing configuration to a verticle section for more details about this configuration.
Executing periodic and delayed actions
It’s very common in Vert.x to want to perform an action after a delay, or periodically.
In standard verticles you can’t just make the thread sleep to introduce a delay, as that will block the event loop thread.
Instead you use Vert.x timers. Timers can be one-shot or periodic. We’ll discuss both
One-shot Timers
A one shot timer calls an event handler after a certain delay, expressed in milliseconds.
To set a timer to fire once you use setTimer
method passing in the delay and a handler
long timerID = vertx.setTimer(1000, id -> {
System.out.println("And one second later this is printed");
});
System.out.println("First this is printed");
The return value is a unique timer id which can later be used to cancel the timer. The handler is also passed the timer id.
Periodic Timers
You can also set a timer to fire periodically by using setPeriodic
.
There will be an initial delay equal to the period.
The return value of setPeriodic
is a unique timer id (long). This can be later used if the timer needs to be cancelled.
The argument passed into the timer event handler is also the unique timer id:
Keep in mind that the timer will fire on a periodic basis. If your periodic treatment takes a long amount of time to proceed, your timer events could run continuously or even worse : stack up.
In this case, you should consider using setTimer
instead. Once your treatment has finished, you can set the next timer.
long timerID = vertx.setPeriodic(1000, id -> {
System.out.println("And every second this is printed");
});
System.out.println("First this is printed");
Cancelling timers
To cancel a periodic timer, call cancelTimer
specifying the timer id. For example:
vertx.cancelTimer(timerID);
Automatic clean-up in verticles
If you’re creating timers from inside verticles, those timers will be automatically closed when the verticle is undeployed.
Verticle worker pool
Verticles use the Vert.x worker pool for executing blocking actions, i.e executeBlocking
or worker verticle.
A different worker pool can be specified in deployment options:
vertx.deployVerticle("the-verticle", new DeploymentOptions().setWorkerPoolName("the-specific-pool"));
The Event Bus
The event bus
is the nervous system of Vert.x.
There is a single event bus instance for every Vert.x instance and it is obtained using the method eventBus
.
The event bus allows different parts of your application to communicate with each other, irrespective of what language they are written in, and whether they’re in the same Vert.x instance, or in a different Vert.x instance.
It can even be bridged to allow client-side JavaScript running in a browser to communicate on the same event bus.
The event bus forms a distributed peer-to-peer messaging system spanning multiple server nodes and multiple browsers.
The event bus supports publish/subscribe, point-to-point, and request-response messaging.
The event bus API is very simple. It basically involves registering handlers, unregistering handlers and sending and publishing messages.
First some theory:
The Theory
Addressing
Messages are sent on the event bus to an address.
Vert.x doesn’t bother with any fancy addressing schemes. In Vert.x an address is simply a string. Any string is valid. However, it is wise to use some kind of scheme, e.g. using periods to demarcate a namespace.
Some examples of valid addresses are europe.news.feed1, acme.games.pacman, sausages, and X.
Handlers
Messages are received by handlers. You register a handler at an address.
Many different handlers can be registered at the same address.
A single handler can be registered at many different addresses.
Publish / subscribe messaging
The event bus supports publishing messages.
Messages are published to an address. Publishing means delivering the message to all handlers that are registered at that address.
This is the familiar publish/subscribe messaging pattern.
Point-to-point and Request-Response messaging
The event bus also supports point-to-point messaging.
Messages are sent to an address. Vert.x will then route them to just one of the handlers registered at that address.
If there is more than one handler registered at the address, one will be chosen using a non-strict round-robin algorithm.
With point-to-point messaging, an optional reply handler can be specified when sending the message.
When a message is received by a recipient, and has been handled, the recipient can optionally decide to reply to the message. If they do so, the reply handler will be called.
When the reply is received back by the sender, it too can be replied to. This can be repeated ad infinitum, and allows a dialog to be set up between two different verticles.
This is a common messaging pattern called the request-response pattern.
Best-effort delivery
Vert.x does its best to deliver messages and won’t consciously throw them away. This is called best-effort delivery.
However, in case of failure of all or parts of the event bus, there is a possibility messages might be lost.
If your application cares about lost messages, you should code your handlers to be idempotent, and your senders to retry after recovery.
Types of messages
Out of the box Vert.x allows any primitive/simple type, String, or buffers
to be sent as messages.
However, it’s a convention and common practice in Vert.x to send messages as JSON
JSON is very easy to create, read and parse in all the languages that Vert.x supports, so it has become a kind of lingua franca for Vert.x.
However, you are not forced to use JSON if you don’t want to.
The event bus is very flexible and also supports sending arbitrary objects over the event bus. You can do this by defining a codec
for the objects you want to send.
The Event Bus API
Let’s jump into the API.
Getting the event bus
You get a reference to the event bus as follows:
EventBus eb = vertx.eventBus();
There is a single instance of the event bus per Vert.x instance.
Registering Handlers
This simplest way to register a handler is using consumer
. Here’s an example:
EventBus eb = vertx.eventBus();
eb.consumer("news.uk.sport", message -> {
System.out.println("I have received a message: " + message.body());
});
When a message arrives for your handler, your handler will be called, passing in the message
.
The object returned from call to consumer() is an instance of MessageConsumer
.
This object can subsequently be used to unregister the handler, or use the handler as a stream.
Alternatively you can use consumer
to return a MessageConsumer with no handler set, and then set the handler on that. For example:
EventBus eb = vertx.eventBus();
MessageConsumer<String> consumer = eb.consumer("news.uk.sport");
consumer.handler(message -> {
System.out.println("I have received a message: " + message.body());
});
When registering a handler on a clustered event bus, it can take some time for the registration to reach all nodes of the cluster.
If you want to be notified when this has completed, you can register a completion handler
on the MessageConsumer object.
consumer.completionHandler(res -> {
if (res.succeeded()) {
System.out.println("The handler registration has reached all nodes");
} else {
System.out.println("Registration failed!");
}
});
Un-registering Handlers
To unregister a handler, call unregister
.
If you are on a clustered event bus, un-registering can take some time to propagate across the nodes. If you want to be notified when this is complete, use unregister
.
consumer
.unregister()
.onComplete(res -> {
if (res.succeeded()) {
System.out.println("The handler un-registration has reached all nodes");
} else {
System.out.println("Un-registration failed!");
}
});
Publishing messages
Publishing a message is simple. Just use publish
specifying the address to publish it to.
eventBus.publish("news.uk.sport", "Yay! Someone kicked a ball");
That message will then be delivered to all handlers registered against the address news.uk.sport.
Sending messages
Sending a message will result in only one handler registered at the address receiving the message. This is the point-to-point messaging pattern. The handler is chosen in a non-strict round-robin fashion.
You can send a message with send
.
eventBus.send("news.uk.sport", "Yay! Someone kicked a ball");
Setting headers on messages
Messages sent over the event bus can also contain headers. This can be specified by providing a DeliveryOptions
when sending or publishing:
DeliveryOptions options = new DeliveryOptions();
options.addHeader("some-header", "some-value");
eventBus.send("news.uk.sport", "Yay! Someone kicked a ball", options);
Message ordering
Vert.x will deliver messages to any particular handler in the same order they were sent from any particular sender.
The Message object
The object you receive in a message handler is a Message
.
The body
of the message corresponds to the object that was sent or published.
The headers of the message are available with headers
.
Acknowledging messages / sending replies
When using send
the event bus attempts to deliver the message to a MessageConsumer
registered with the event bus.
In some cases it’s useful for the sender to know when the consumer has received the message and "processed" it using request-response pattern.
To acknowledge that the message has been processed, the consumer can reply to the message by calling reply
.
When this happens it causes a reply to be sent back to the sender and the reply handler is invoked with the reply.
An example will make this clear:
The receiver:
MessageConsumer<String> consumer = eventBus.consumer("news.uk.sport");
consumer.handler(message -> {
System.out.println("I have received a message: " + message.body());
message.reply("how interesting!");
});
The sender:
eventBus
.request("news.uk.sport", "Yay! Someone kicked a ball across a patch of grass")
.onComplete(ar -> {
if (ar.succeeded()) {
System.out.println("Received reply: " + ar.result().body());
}
});
The reply can contain a message body which can contain useful information.
What the "processing" actually means is application-defined and depends entirely on what the message consumer does and is not something that the Vert.x event bus itself knows or cares about.
Some examples:
-
A simple message consumer which implements a service which returns the time of the day would acknowledge with a message containing the time of day in the reply body
-
A message consumer which implements a persistent queue, might acknowledge with
true
if the message was successfully persisted in storage, orfalse
if not. -
A message consumer which processes an order might acknowledge with
true
when the order has been successfully processed so it can be deleted from the database
Sending with timeouts
When sending a message with a reply handler, you can specify a timeout in the DeliveryOptions
.
If a reply is not received within that time, the reply handler will be called with a failure.
The default timeout is 30 seconds.
Send Failures
Message sends can fail for other reasons, including:
-
There are no handlers available to send the message to
-
The recipient has explicitly failed the message using
fail
In all cases, the reply handler will be called with the specific failure.
Message Codecs
You can send any object you like across the event bus if you define and register a message codec
for it.
Message codecs have a name and you specify that name in the DeliveryOptions
when sending or publishing the message:
eventBus.registerCodec(myCodec);
DeliveryOptions options = new DeliveryOptions().setCodecName(myCodec.name());
eventBus.send("orders", new MyPOJO(), options);
If you always want the same codec to be used for a particular type then you can register a default codec for it, then you don’t have to specify the codec on each send in the delivery options:
eventBus.registerDefaultCodec(MyPOJO.class, myCodec);
eventBus.send("orders", new MyPOJO());
You unregister a message codec with unregisterCodec
.
Message codecs don’t always have to encode and decode as the same type. For example you can write a codec that allows a MyPOJO class to be sent, but when that message is sent to a handler it arrives as a MyOtherPOJO class.
Vert.x has built-in codecs for certain data types:
-
basic types (string, byte array, byte, int, long, double, boolean, short, char), or
-
some Vert.x data types (buffers, JSON array, JSON objects), or
-
types implementing the
ClusterSerializable
interface, or -
types implementing the
java.io.Serializable
interface.
In clustered mode, You can define which classes are allowed for encoding and decoding by providing functions which inspect the name of the class: |
Clustered Event Bus
The event bus doesn’t just exist in a single Vert.x instance. By clustering different Vert.x instances together on your network they can form a single, distributed event bus.
Clustering programmatically
If you’re creating your Vert.x instance programmatically you get a clustered event bus by configuring the Vert.x instance as clustered;
VertxOptions options = new VertxOptions();
Vertx
.clusteredVertx(options)
.onComplete(res -> {
if (res.succeeded()) {
Vertx vertx = res.result();
EventBus eventBus = vertx.eventBus();
System.out.println("We now have a clustered event bus: " + eventBus);
} else {
System.out.println("Failed: " + res.cause());
}
});
You should also make sure you have a ClusterManager
implementation on your classpath, for example the Hazelcast cluster manager.
Clustering on the command line
You can run Vert.x clustered on the command line with
vertx run my-verticle.js -cluster
Automatic clean-up in verticles
If you’re registering event bus handlers from inside verticles, those handlers will be automatically unregistered when the verticle is undeployed.
Configuring the event bus
The event bus can be configured. It is particularly useful when the event bus is clustered. Under the hood the event bus uses TCP connections to send and receive messages, so the EventBusOptions
let you configure all aspects of these TCP connections. As the event bus acts as a server and client, the configuration is close to NetClientOptions
and NetServerOptions
.
VertxOptions options = new VertxOptions()
.setEventBusOptions(new EventBusOptions()
.setSsl(true)
.setKeyStoreOptions(new JksOptions().setPath("keystore.jks").setPassword("wibble"))
.setTrustStoreOptions(new JksOptions().setPath("keystore.jks").setPassword("wibble"))
.setClientAuth(ClientAuth.REQUIRED)
);
Vertx
.clusteredVertx(options)
.onComplete(res -> {
if (res.succeeded()) {
Vertx vertx = res.result();
EventBus eventBus = vertx.eventBus();
System.out.println("We now have a clustered event bus: " + eventBus);
} else {
System.out.println("Failed: " + res.cause());
}
});
The previous snippet depicts how you can use SSL connections for the event bus, instead of plain TCP connections.
WARNING: to enforce the security in clustered mode, you must configure the cluster manager to use encryption or enforce security. Refer to the documentation of the cluster manager for further details.
The event bus configuration needs to be consistent in all the cluster nodes.
The EventBusOptions
also lets you specify whether or not the event bus is clustered, the port and host.
When used in containers, you can also configure the public host and port:
VertxOptions options = new VertxOptions()
.setEventBusOptions(new EventBusOptions()
.setClusterPublicHost("whatever")
.setClusterPublicPort(1234)
);
Vertx
.clusteredVertx(options)
.onComplete(res -> {
if (res.succeeded()) {
Vertx vertx = res.result();
EventBus eventBus = vertx.eventBus();
System.out.println("We now have a clustered event bus: " + eventBus);
} else {
System.out.println("Failed: " + res.cause());
}
});
JSON
Unlike some other languages, Java does not have first class support for JSON so we provide two classes to make handling JSON in your Vert.x applications a bit easier.
JSON objects
The JsonObject
class represents JSON objects.
A JSON object is basically just a map which has string keys and values can be of one of the JSON supported types (string, number, boolean).
JSON objects also support null values.
Creating JSON objects
Empty JSON objects can be created with the default constructor.
You can create a JSON object from a string JSON representation as follows:
String jsonString = "{\"foo\":\"bar\"}";
JsonObject object = new JsonObject(jsonString);
You can create a JSON object from a map as follows:
Map<String, Object> map = new HashMap<>();
map.put("foo", "bar");
map.put("xyz", 3);
JsonObject object = new JsonObject(map);
Putting entries into a JSON object
Use the put
methods to put values into the JSON object.
The method invocations can be chained because of the fluent API:
JsonObject object = new JsonObject();
object.put("foo", "bar").put("num", 123).put("mybool", true);
Getting values from a JSON object
You get values from a JSON object using the getXXX
methods, for example:
String val = jsonObject.getString("some-key");
int intVal = jsonObject.getInteger("some-other-key");
Mapping between JSON objects and Java objects
You can create a JSON object from the fields of a Java object as follows:
You can instantiate a Java object and populate its fields from a JSON object as follows:
request.bodyHandler(buff -> {
JsonObject jsonObject = buff.toJsonObject();
User javaObject = jsonObject.mapTo(User.class);
});
Note that both of the above mapping directions use Jackson’s ObjectMapper#convertValue()
to perform the mapping. See the Jackson documentation for information on the impact of field and constructor visibility, caveats on serialization and deserialization across object references, etc.
However, in the simplest case, both mapFrom
and mapTo
should succeed if all fields of the Java class are public (or have public getters/setters), and if there is a public default constructor (or no defined constructors).
Referenced objects will be transitively serialized/deserialized to/from nested JSON objects as long as the object graph is acyclic.
Encoding a JSON object to a String
You use encode
to encode the object to a String form.
JSON arrays
The JsonArray
class represents JSON arrays.
A JSON array is a sequence of values (string, number, boolean).
JSON arrays can also contain null values.
Creating JSON arrays
Empty JSON arrays can be created with the default constructor.
You can create a JSON array from a string JSON representation as follows:
String jsonString = "[\"foo\",\"bar\"]";
JsonArray array = new JsonArray(jsonString);
Adding entries into a JSON array
You add entries to a JSON array using the add
methods.
JsonArray array = new JsonArray();
array.add("foo").add(123).add(false);
Getting values from a JSON array
You get values from a JSON array using the getXXX
methods, for example:
String val = array.getString(0);
Integer intVal = array.getInteger(1);
Boolean boolVal = array.getBoolean(2);
Encoding a JSON array to a String
You use encode
to encode the array to a String form.
Creating arbitrary JSON
Creating JSON object and array assumes you are using valid string representation.
When you are unsure of the string validity then you should use instead Json.decodeValue
Object object = Json.decodeValue(arbitraryJson);
if (object instanceof JsonObject) {
// That's a valid json object
} else if (object instanceof JsonArray) {
// That's a valid json array
} else if (object instanceof String) {
// That's valid string
} else {
// etc...
}
Json Pointers
Vert.x provides an implementation of Json Pointers from RFC6901. You can use pointers both for querying and for writing. You can build your JsonPointer
using a string, a URI or manually appending paths:
JsonPointer pointer1 = JsonPointer.from("/hello/world");
// Build a pointer manually
JsonPointer pointer2 = JsonPointer.create()
.append("hello")
.append("world");
After instantiating your pointer, use queryJson
to query a JSON value. You can update a Json Value using writeJson
:
Object result1 = objectPointer.queryJson(jsonObject);
// Query a JsonArray
Object result2 = arrayPointer.queryJson(jsonArray);
// Write starting from a JsonObject
objectPointer.writeJson(jsonObject, "new element");
// Write starting from a JsonObject
arrayPointer.writeJson(jsonArray, "new element");
You can use Vert.x Json Pointer with any object model by providing a custom implementation of JsonPointerIterator
Buffers
Most data is shuffled around inside Vert.x using buffers.
A buffer is a sequence of zero or more bytes that can read from or written to and which expands automatically as necessary to accommodate any bytes written to it. You can perhaps think of a buffer as smart byte array.
Creating buffers
Buffers can create by using one of the static Buffer.buffer
methods.
Buffers can be initialised from strings or byte arrays, or empty buffers can be created.
Here are some examples of creating buffers:
Create a new empty buffer:
Buffer buff = Buffer.buffer();
Create a buffer from a String. The String will be encoded in the buffer using UTF-8.
Buffer buff = Buffer.buffer("some string");
Create a buffer from a String: The String will be encoded using the specified encoding, e.g:
Buffer buff = Buffer.buffer("some string", "UTF-16");
Create a buffer from a byte[]
byte[] bytes = new byte[] {1, 3, 5};
Buffer buff = Buffer.buffer(bytes);
Create a buffer with an initial size hint. If you know your buffer will have a certain amount of data written to it you can create the buffer and specify this size. This makes the buffer initially allocate that much memory and is more efficient than the buffer automatically resizing multiple times as data is written to it.
Note that buffers created this way are empty. It does not create a buffer filled with zeros up to the specified size.
Buffer buff = Buffer.buffer(10000);
Writing to a Buffer
There are two ways to write to a buffer: appending, and random access. In either case buffers will always expand automatically to encompass the bytes. It’s not possible to get an IndexOutOfBoundsException
with a buffer.
Appending to a Buffer
To append to a buffer, you use the appendXXX
methods. Append methods exist for appending various different types.
The return value of the appendXXX
methods is the buffer itself, so these can be chained:
Buffer buff = Buffer.buffer();
buff.appendInt(123).appendString("hello\n");
socket.write(buff);
Random access buffer writes
You can also write into the buffer at a specific index, by using the setXXX
methods. Set methods exist for various different data types. All the set methods take an index as the first argument - this represents the position in the buffer where to start writing the data.
The buffer will always expand as necessary to accommodate the data.
Buffer buff = Buffer.buffer();
buff.setInt(1000, 123);
buff.setString(0, "hello");
Reading from a Buffer
Data is read from a buffer using the getXXX
methods. Get methods exist for various datatypes. The first argument to these methods is an index in the buffer from where to get the data.
Buffer buff = Buffer.buffer();
for (int i = 0; i < buff.length(); i += 4) {
System.out.println("int value at " + i + " is " + buff.getInt(i));
}
Working with unsigned numbers
Unsigned numbers can be read from or appended/set to a buffer with the getUnsignedXXX
, appendUnsignedXXX
and setUnsignedXXX
methods. This is useful when implementing a codec for a network protocol optimized to minimize bandwidth consumption.
In the following example, value 200 is set at specified position with just one byte:
Buffer buff = Buffer.buffer(128);
int pos = 15;
buff.setUnsignedByte(pos, (short) 200);
System.out.println(buff.getUnsignedByte(pos));
The console shows '200'.
Buffer length
Use length
to obtain the length of the buffer. The length of a buffer is the index of the byte in the buffer with the largest index + 1.
Copying buffers
Use copy
to make a copy of the buffer
Slicing buffers
A sliced buffer is a new buffer which backs onto the original buffer, i.e. it does not copy the underlying data. Use slice
to create a sliced buffers
Buffer re-use
After writing a buffer to a socket or other similar place, they cannot be re-used.
Writing TCP servers and clients
Vert.x allows you to easily write non blocking TCP clients and servers.
Creating a TCP server
The simplest way to create a TCP server, using all default options is as follows:
NetServer server = vertx.createNetServer();
Configuring a TCP server
If you don’t want the default, a server can be configured by passing in a NetServerOptions
instance when creating it:
NetServerOptions options = new NetServerOptions().setPort(4321);
NetServer server = vertx.createNetServer(options);
Start the Server Listening
To tell the server to listen for incoming requests you use one of the listen
alternatives.
To tell the server to listen at the host and port as specified in the options:
NetServer server = vertx.createNetServer();
server.listen();
Or to specify the host and port in the call to listen, ignoring what is configured in the options:
NetServer server = vertx.createNetServer();
server.listen(1234, "localhost");
The default host is 0.0.0.0
which means 'listen on all available addresses' and the default port is 0
, which is a special value that instructs the server to find a random unused local port and use that.
The actual bind is asynchronous, so the server might not actually be listening until some time after the call to listen has returned.
If you want to be notified when the server is actually listening you can provide a handler to the listen
call. For example:
NetServer server = vertx.createNetServer();
server
.listen(1234, "localhost")
.onComplete(res -> {
if (res.succeeded()) {
System.out.println("Server is now listening!");
} else {
System.out.println("Failed to bind!");
}
});
Listening on a random port
If 0
is used as the listening port, the server will find an unused random port to listen on.
To find out the real port the server is listening on you can call actualPort
.
NetServer server = vertx.createNetServer();
server
.listen(0, "localhost")
.onComplete(res -> {
if (res.succeeded()) {
System.out.println("Server is now listening on actual port: " + server.actualPort());
} else {
System.out.println("Failed to bind!");
}
});
Getting notified of incoming connections
To be notified when a connection is made you need to set a connectHandler
:
NetServer server = vertx.createNetServer();
server.connectHandler(socket -> {
// Handle the connection in here
});
When a connection is made the handler will be called with an instance of NetSocket
.
This is a socket-like interface to the actual connection, and allows you to read and write data as well as do various other things like close the socket.
Reading data from the socket
To read data from the socket you set the handler
on the socket.
This handler will be called with an instance of Buffer
every time data is received on the socket.
NetServer server = vertx.createNetServer();
server.connectHandler(socket -> {
socket.handler(buffer -> {
System.out.println("I received some bytes: " + buffer.length());
});
});
Writing data to a socket
You write to a socket using one of write
.
Buffer buffer = Buffer.buffer().appendFloat(12.34f).appendInt(123);
socket.write(buffer);
// Write a string in UTF-8 encoding
socket.write("some data");
// Write a string using the specified encoding
socket.write("some data", "UTF-16");
Write operations are asynchronous and may not occur until some time after the call to write has returned.
Closed handler
If you want to be notified when a socket is closed, you can set a closeHandler
on it:
socket.closeHandler(v -> {
System.out.println("The socket has been closed");
});
Handling exceptions
You can set an exceptionHandler
to receive any exceptions that happen on the socket.
You can set an exceptionHandler
to receive any exceptions that happens before the connection is passed to the connectHandler
, e.g during the TLS handshake.
Event bus write handler
Every socket can register a handler on the event bus, and when any buffers are received in this handler, it writes them to itself. Those are local subscriptions, not reachable from other clustered nodes.
This enables you to write data to a socket which is potentially in a completely different verticle by sending the buffer to the address of that handler.
This feature is disabled by default, however you can enable it using setRegisterWriteHandler
or setRegisterWriteHandler
.
The address of the handler is given by writeHandlerID
.
Local and remote addresses
The local address of a NetSocket
can be retrieved using localAddress
.
The remote address, (i.e. the address of the other end of the connection) of a NetSocket
can be retrieved using remoteAddress
.
Sending files or resources from the classpath
Files and classpath resources can be written to the socket directly using sendFile
. This can be a very efficient way to send files, as it can be handled by the OS kernel directly where supported by the operating system.
Please see the chapter about serving files from the classpath for restrictions of the classpath resolution or disabling it.
socket.sendFile("myfile.dat");
Streaming sockets
Instances of NetSocket
are also ReadStream
and WriteStream
instances, so they can be used to pipe data to or from other read and write streams.
See the chapter on streams for more information.
Upgrading connections to SSL/TLS
A non SSL/TLS connection can be upgraded to SSL/TLS using upgradeToSsl
.
The server or client must be configured for SSL/TLS for this to work correctly. Please see the chapter on SSL/TLS for more information.
Closing a TCP Server
Call close
to close the server. Closing the server closes any open connections and releases all server resources.
The close is actually asynchronous and might not complete until some time after the call has returned. If you want to be notified when the actual close has completed then you can pass in a handler.
This handler will then be called when the close has fully completed.
server
.close()
.onComplete(res -> {
if (res.succeeded()) {
System.out.println("Server is now closed");
} else {
System.out.println("close failed");
}
});
Automatic clean-up in verticles
If you’re creating TCP servers and clients from inside verticles, those servers and clients will be automatically closed when the verticle is undeployed.
Scaling - sharing TCP servers
The handlers of any TCP server are always executed on the same event loop thread.
This means that if you are running on a server with a lot of cores, and you only have this one instance deployed then you will have at most one core utilised on your server.
In order to utilise more cores of your server you will need to deploy more instances of the server.
You can instantiate more instances programmatically in your code:
class MyVerticle extends AbstractVerticle {
NetServer server;
@Override
public void start() throws Exception {
server = vertx.createNetServer();
server.connectHandler(socket -> {
socket.handler(buffer -> {
// Just echo back the data
socket.write(buffer);
});
});
server.listen(1234, "localhost");
}
}
// Create a few instances so we can utilise cores
vertx.deployVerticle(MyVerticle.class, new DeploymentOptions().setInstances(10));
or, you can simply deploy more instances of your server verticle by using the -instances
option on the command line:
> vertx run com.mycompany.MyVerticle -instances 10
Once you do this you will find the echo server works functionally identically to before, but all your cores on your server can be utilised and more work can be handled.
At this point you might be asking yourself 'How can you have more than one server listening on the same host and port? Surely you will get port conflicts as soon as you try and deploy more than one instance?'
Vert.x does a little magic here.*
When you deploy another server on the same host and port as an existing server it doesn’t actually try and create a new server listening on the same host/port.
Instead it internally maintains just a single server, and, as incoming connections arrive it distributes them in a round-robin fashion to any of the connect handlers.
Consequently Vert.x TCP servers can scale over available cores while each instance remains single threaded.
Creating a TCP client
The simplest way to create a TCP client, using all default options is as follows:
NetClient client = vertx.createNetClient();
Configuring a TCP client
If you don’t want the default, a client can be configured by passing in a NetClientOptions
instance when creating it:
NetClientOptions options = new NetClientOptions().setConnectTimeout(10000);
NetClient client = vertx.createNetClient(options);
Making connections
To make a connection to a server you use connect
, specifying the port and host of the server and a handler that will be called with a result containing the NetSocket
when connection is successful or with a failure if connection failed.
NetClientOptions options = new NetClientOptions().setConnectTimeout(10000);
NetClient client = vertx.createNetClient(options);
client
.connect(4321, "localhost")
.onComplete(res -> {
if (res.succeeded()) {
System.out.println("Connected!");
NetSocket socket = res.result();
} else {
System.out.println("Failed to connect: " + res.cause().getMessage());
}
});
Configuring connection attempts
A client can be configured to automatically retry connecting to the server in the event that it cannot connect. This is configured with setReconnectInterval
and setReconnectAttempts
.
Currently, Vert.x will not attempt to reconnect if a connection fails, reconnect attempts and interval only apply to creating initial connections. |
NetClientOptions options = new NetClientOptions().
setReconnectAttempts(10).
setReconnectInterval(500);
NetClient client = vertx.createNetClient(options);
By default, multiple connection attempts are disabled.
Logging network activity
For debugging purposes, network activity can be logged:
NetServerOptions options = new NetServerOptions().setLogActivity(true);
NetServer server = vertx.createNetServer(options);
Here is the output of a simple HTTP server
id: 0x359e3df6, L:/127.0.0.1:8080 - R:/127.0.0.1:65351] READ: 78B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 47 45 54 20 2f 20 48 54 54 50 2f 31 2e 31 0d 0a |GET / HTTP/1.1..| |00000010| 48 6f 73 74 3a 20 6c 6f 63 61 6c 68 6f 73 74 3a |Host: localhost:| |00000020| 38 30 38 30 0d 0a 55 73 65 72 2d 41 67 65 6e 74 |8080..User-Agent| |00000030| 3a 20 63 75 72 6c 2f 37 2e 36 34 2e 31 0d 0a 41 |: curl/7.64.1..A| |00000040| 63 63 65 70 74 3a 20 2a 2f 2a 0d 0a 0d 0a |ccept: */*.... | +--------+-------------------------------------------------+----------------+ [id: 0x359e3df6, L:/127.0.0.1:8080 - R:/127.0.0.1:65351] WRITE: 50B +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 48 54 54 50 2f 31 2e 31 20 32 30 30 20 4f 4b 0d |HTTP/1.1 200 OK.| |00000010| 0a 63 6f 6e 74 65 6e 74 2d 6c 65 6e 67 74 68 3a |.content-length:| |00000020| 20 31 31 0d 0a 0d 0a 48 65 6c 6c 6f 20 57 6f 72 | 11....Hello Wor| |00000030| 6c 64 |ld | +--------+-------------------------------------------------+----------------+ [id: 0x359e3df6, L:/127.0.0.1:8080 - R:/127.0.0.1:65351] READ COMPLETE [id: 0x359e3df6, L:/127.0.0.1:8080 - R:/127.0.0.1:65351] FLUSH
By default, binary data is logged in hex format.
You can reduce the data format verbosity to only print the buffer length instead of the entire data by setting the log data fomat.
NetServerOptions options = new NetServerOptions()
.setLogActivity(true)
.setActivityLogDataFormat(ByteBufFormat.SIMPLE);
NetServer server = vertx.createNetServer(options);
Here is the same output with simple buffer format
[id: 0xda8d41dc, L:/127.0.0.1:8080 - R:/127.0.0.1:65399] READ: 78B [id: 0xda8d41dc, L:/127.0.0.1:8080 - R:/127.0.0.1:65399] WRITE: 50B [id: 0xda8d41dc, L:/127.0.0.1:8080 - R:/127.0.0.1:65399] READ COMPLETE [id: 0xda8d41dc, L:/127.0.0.1:8080 - R:/127.0.0.1:65399] FLUSH [id: 0xda8d41dc, L:/127.0.0.1:8080 - R:/127.0.0.1:65399] READ COMPLETE [id: 0xda8d41dc, L:/127.0.0.1:8080 ! R:/127.0.0.1:65399] INACTIVE [id: 0xda8d41dc, L:/127.0.0.1:8080 ! R:/127.0.0.1:65399] UNREGISTERED
Clients can also log network activity
NetClientOptions options = new NetClientOptions().setLogActivity(true);
NetClient client = vertx.createNetClient(options);
Network activity is logged by Netty with the DEBUG
level and with the io.netty.handler.logging.LoggingHandler
name. When using network activity logging there are a few things to keep in mind:
-
logging is not performed by Vert.x logging but by Netty
-
this is not a production feature
You should read the Netty logging section.
Throttling inbound and outbound bandwidth of TCP connections
TCP server (Net/Http) can be configured with traffic shaping options to enable bandwidth limiting. Both inbound and outbound bandwidth can be limited through TrafficShapingOptions
. For NetServer, traffic shaping options can be set through NetServerOptions
and for HttpServer it can be set through HttpServerOptions
.
NetServerOptions options = new NetServerOptions()
.setHost("localhost")
.setPort(1234)
.setTrafficShapingOptions(new TrafficShapingOptions()
.setInboundGlobalBandwidth(64 * 1024)
.setOutboundGlobalBandwidth(128 * 1024));
NetServer server = vertx.createNetServer(options);
HttpServerOptions options = new HttpServerOptions()
.setHost("localhost")
.setPort(1234)
.setTrafficShapingOptions(new TrafficShapingOptions()
.setInboundGlobalBandwidth(64 * 1024)
.setOutboundGlobalBandwidth(128 * 1024));
HttpServer server = vertx.createHttpServer(options);
These traffic shaping options can also be dynamically updated after server start.
NetServerOptions options = new NetServerOptions()
.setHost("localhost")
.setPort(1234)
.setTrafficShapingOptions(new TrafficShapingOptions()
.setInboundGlobalBandwidth(64 * 1024)
.setOutboundGlobalBandwidth(128 * 1024));
NetServer server = vertx.createNetServer(options);
TrafficShapingOptions update = new TrafficShapingOptions()
.setInboundGlobalBandwidth(2 * 64 * 1024) // twice
.setOutboundGlobalBandwidth(128 * 1024); // unchanged
server
.listen(1234, "localhost")
// wait until traffic shaping handler is created for updates
.onSuccess(v -> server.updateTrafficShapingOptions(update));
HttpServerOptions options = new HttpServerOptions()
.setHost("localhost")
.setPort(1234)
.setTrafficShapingOptions(new TrafficShapingOptions()
.setInboundGlobalBandwidth(64 * 1024)
.setOutboundGlobalBandwidth(128 * 1024));
HttpServer server = vertx.createHttpServer(options);
TrafficShapingOptions update = new TrafficShapingOptions()
.setInboundGlobalBandwidth(2 * 64 * 1024) // twice
.setOutboundGlobalBandwidth(128 * 1024); // unchanged
server
.listen(1234, "localhost")
// wait until traffic shaping handler is created for updates
.onSuccess(v -> server.updateTrafficShapingOptions(update));
Configuring servers and clients to work with SSL/TLS
TCP clients and servers can be configured to use Transport Layer Security - earlier versions of TLS were known as SSL.
The APIs of the servers and clients are identical whether or not SSL/TLS is used, and it’s enabled by configuring the NetClientOptions
or NetServerOptions
instances used to create the servers or clients.
Specifying key/certificate for the server
SSL/TLS servers usually provide certificates to clients in order verify their identity to clients.
Certificates/keys can be configured for servers in several ways:
The first method is by specifying the location of a Java key-store which contains the certificate and private key.
Java key stores can be managed with the keytool utility which ships with the JDK.
The password for the key store should also be provided:
NetServerOptions options = new NetServerOptions().setSsl(true).setKeyStoreOptions(
new JksOptions().
setPath("/path/to/your/server-keystore.jks").
setPassword("password-of-your-keystore")
);
NetServer server = vertx.createNetServer(options);
Alternatively you can read the key store yourself as a buffer and provide that directly:
Buffer myKeyStoreAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/server-keystore.jks");
JksOptions jksOptions = new JksOptions().
setValue(myKeyStoreAsABuffer).
setPassword("password-of-your-keystore");
NetServerOptions options = new NetServerOptions().
setSsl(true).
setKeyStoreOptions(jksOptions);
NetServer server = vertx.createNetServer(options);
Key/certificate in PKCS#12 format (http://en.wikipedia.org/wiki/PKCS_12), usually with the .pfx
or the .p12
extension can also be loaded in a similar fashion than JKS key stores:
NetServerOptions options = new NetServerOptions().setSsl(true).setPfxKeyCertOptions(
new PfxOptions().
setPath("/path/to/your/server-keystore.pfx").
setPassword("password-of-your-keystore")
);
NetServer server = vertx.createNetServer(options);
Buffer configuration is also supported:
Buffer myKeyStoreAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/server-keystore.pfx");
PfxOptions pfxOptions = new PfxOptions().
setValue(myKeyStoreAsABuffer).
setPassword("password-of-your-keystore");
NetServerOptions options = new NetServerOptions().
setSsl(true).
setPfxKeyCertOptions(pfxOptions);
NetServer server = vertx.createNetServer(options);
Another way of providing server private key and certificate separately using .pem
files.
NetServerOptions options = new NetServerOptions().setSsl(true).setPemKeyCertOptions(
new PemKeyCertOptions().
setKeyPath("/path/to/your/server-key.pem").
setCertPath("/path/to/your/server-cert.pem")
);
NetServer server = vertx.createNetServer(options);
Buffer configuration is also supported:
Buffer myKeyAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/server-key.pem");
Buffer myCertAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/server-cert.pem");
PemKeyCertOptions pemOptions = new PemKeyCertOptions().
setKeyValue(myKeyAsABuffer).
setCertValue(myCertAsABuffer);
NetServerOptions options = new NetServerOptions().
setSsl(true).
setPemKeyCertOptions(pemOptions);
NetServer server = vertx.createNetServer(options);
Vert.x supports reading of unencrypted RSA and/or ECC based private keys from PKCS8 PEM files. RSA based private keys can also be read from PKCS1 PEM files. X.509 certificates can be read from PEM files containing a textual encoding of the certificate as defined by RFC 7468, Section 5.
Keep in mind that the keys contained in an unencrypted PKCS8 or a PKCS1 PEM file can be extracted by anybody who can read the file. Thus, make sure to put proper access restrictions on such PEM files in order to prevent misuse. |
Finally, you can also load generic Java keystore, it is useful for using other KeyStore implementations like Bouncy Castle:
NetServerOptions options = new NetServerOptions().setSsl(true).setKeyCertOptions(
new KeyStoreOptions().
setType("BKS").
setPath("/path/to/your/server-keystore.bks").
setPassword("password-of-your-keystore")
);
NetServer server = vertx.createNetServer(options);
Specifying trust for the server
SSL/TLS servers can use a certificate authority in order to verify the identity of the clients.
Certificate authorities can be configured for servers in several ways:
Java trust stores can be managed with the keytool utility which ships with the JDK.
The password for the trust store should also be provided:
NetServerOptions options = new NetServerOptions().
setSsl(true).
setClientAuth(ClientAuth.REQUIRED).
setTrustStoreOptions(
new JksOptions().
setPath("/path/to/your/truststore.jks").
setPassword("password-of-your-truststore")
);
NetServer server = vertx.createNetServer(options);
Alternatively you can read the trust store yourself as a buffer and provide that directly:
Buffer myTrustStoreAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/truststore.jks");
NetServerOptions options = new NetServerOptions().
setSsl(true).
setClientAuth(ClientAuth.REQUIRED).
setTrustStoreOptions(
new JksOptions().
setValue(myTrustStoreAsABuffer).
setPassword("password-of-your-truststore")
);
NetServer server = vertx.createNetServer(options);
Certificate authority in PKCS#12 format (http://en.wikipedia.org/wiki/PKCS_12), usually with the .pfx
or the .p12
extension can also be loaded in a similar fashion than JKS trust stores:
NetServerOptions options = new NetServerOptions().
setSsl(true).
setClientAuth(ClientAuth.REQUIRED).
setPfxTrustOptions(
new PfxOptions().
setPath("/path/to/your/truststore.pfx").
setPassword("password-of-your-truststore")
);
NetServer server = vertx.createNetServer(options);
Buffer configuration is also supported:
Buffer myTrustStoreAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/truststore.pfx");
NetServerOptions options = new NetServerOptions().
setSsl(true).
setClientAuth(ClientAuth.REQUIRED).
setPfxTrustOptions(
new PfxOptions().
setValue(myTrustStoreAsABuffer).
setPassword("password-of-your-truststore")
);
NetServer server = vertx.createNetServer(options);
Another way of providing server certificate authority using a list .pem
files.
NetServerOptions options = new NetServerOptions().
setSsl(true).
setClientAuth(ClientAuth.REQUIRED).
setPemTrustOptions(
new PemTrustOptions().
addCertPath("/path/to/your/server-ca.pem")
);
NetServer server = vertx.createNetServer(options);
Buffer configuration is also supported:
Buffer myCaAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/server-ca.pfx");
NetServerOptions options = new NetServerOptions().
setSsl(true).
setClientAuth(ClientAuth.REQUIRED).
setPemTrustOptions(
new PemTrustOptions().
addCertValue(myCaAsABuffer)
);
NetServer server = vertx.createNetServer(options);
Enabling SSL/TLS on the client
Net Clients can also be easily configured to use SSL. They have the exact same API when using SSL as when using standard sockets.
To enable SSL on a NetClient the function setSSL(true) is called.
Client trust configuration
If the trustALl
is set to true on the client, then the client will trust all server certificates. The connection will still be encrypted but this mode is vulnerable to 'man in the middle' attacks. I.e. you can’t be sure who you are connecting to. Use this with caution. Default value is false.
NetClientOptions options = new NetClientOptions().
setSsl(true).
setTrustAll(true);
NetClient client = vertx.createNetClient(options);
If trustAll
is not set then a client trust store must be configured and should contain the certificates of the servers that the client trusts.
By default, host verification is not configured on the client. This verifies the CN portion of the server certificate against the server hostname to avoid Man-in-the-middle attacks.
You must configure it explicitly on your client
-
""
(empty string) disables host verification -
"HTTPS"
enables HTTP over TLS verification -
LDAPS
enables LDAP v3 extension for TLS verification
NetClientOptions options = new NetClientOptions().
setSsl(true).
setHostnameVerificationAlgorithm(verificationAlgorithm);
NetClient client = vertx.createNetClient(options);
the Vert.x HTTP client uses the TCP client and configures with "HTTPS" the verification algorithm. |
Like server configuration, the client trust can be configured in several ways:
The first method is by specifying the location of a Java trust-store which contains the certificate authority.
It is just a standard Java key store, the same as the key stores on the server side. The client trust store location is set by using the function path
on the jks options
. If a server presents a certificate during connection which is not in the client trust store, the connection attempt will not succeed.
NetClientOptions options = new NetClientOptions().
setSsl(true).
setTrustStoreOptions(
new JksOptions().
setPath("/path/to/your/truststore.jks").
setPassword("password-of-your-truststore")
);
NetClient client = vertx.createNetClient(options);
Buffer configuration is also supported:
Buffer myTrustStoreAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/truststore.jks");
NetClientOptions options = new NetClientOptions().
setSsl(true).
setTrustStoreOptions(
new JksOptions().
setValue(myTrustStoreAsABuffer).
setPassword("password-of-your-truststore")
);
NetClient client = vertx.createNetClient(options);
Certificate authority in PKCS#12 format (http://en.wikipedia.org/wiki/PKCS_12), usually with the .pfx
or the .p12
extension can also be loaded in a similar fashion than JKS trust stores:
NetClientOptions options = new NetClientOptions().
setSsl(true).
setPfxTrustOptions(
new PfxOptions().
setPath("/path/to/your/truststore.pfx").
setPassword("password-of-your-truststore")
);
NetClient client = vertx.createNetClient(options);
Buffer configuration is also supported:
Buffer myTrustStoreAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/truststore.pfx");
NetClientOptions options = new NetClientOptions().
setSsl(true).
setPfxTrustOptions(
new PfxOptions().
setValue(myTrustStoreAsABuffer).
setPassword("password-of-your-truststore")
);
NetClient client = vertx.createNetClient(options);
Another way of providing server certificate authority using a list .pem
files.
NetClientOptions options = new NetClientOptions().
setSsl(true).
setPemTrustOptions(
new PemTrustOptions().
addCertPath("/path/to/your/ca-cert.pem")
);
NetClient client = vertx.createNetClient(options);
Buffer configuration is also supported:
Buffer myTrustStoreAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/ca-cert.pem");
NetClientOptions options = new NetClientOptions().
setSsl(true).
setPemTrustOptions(
new PemTrustOptions().
addCertValue(myTrustStoreAsABuffer)
);
NetClient client = vertx.createNetClient(options);
Specifying key/certificate for the client
If the server requires client authentication then the client must present its own certificate to the server when connecting. The client can be configured in several ways:
The first method is by specifying the location of a Java key-store which contains the key and certificate. Again it’s just a regular Java key store. The client keystore location is set by using the function path
on the jks options
.
NetClientOptions options = new NetClientOptions().setSsl(true).setKeyStoreOptions(
new JksOptions().
setPath("/path/to/your/client-keystore.jks").
setPassword("password-of-your-keystore")
);
NetClient client = vertx.createNetClient(options);
Buffer configuration is also supported:
Buffer myKeyStoreAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/client-keystore.jks");
JksOptions jksOptions = new JksOptions().
setValue(myKeyStoreAsABuffer).
setPassword("password-of-your-keystore");
NetClientOptions options = new NetClientOptions().
setSsl(true).
setKeyStoreOptions(jksOptions);
NetClient client = vertx.createNetClient(options);
Key/certificate in PKCS#12 format (http://en.wikipedia.org/wiki/PKCS_12), usually with the .pfx
or the .p12
extension can also be loaded in a similar fashion than JKS key stores:
NetClientOptions options = new NetClientOptions().setSsl(true).setPfxKeyCertOptions(
new PfxOptions().
setPath("/path/to/your/client-keystore.pfx").
setPassword("password-of-your-keystore")
);
NetClient client = vertx.createNetClient(options);
Buffer configuration is also supported:
Buffer myKeyStoreAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/client-keystore.pfx");
PfxOptions pfxOptions = new PfxOptions().
setValue(myKeyStoreAsABuffer).
setPassword("password-of-your-keystore");
NetClientOptions options = new NetClientOptions().
setSsl(true).
setPfxKeyCertOptions(pfxOptions);
NetClient client = vertx.createNetClient(options);
Another way of providing server private key and certificate separately using .pem
files.
NetClientOptions options = new NetClientOptions().setSsl(true).setPemKeyCertOptions(
new PemKeyCertOptions().
setKeyPath("/path/to/your/client-key.pem").
setCertPath("/path/to/your/client-cert.pem")
);
NetClient client = vertx.createNetClient(options);
Buffer configuration is also supported:
Buffer myKeyAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/client-key.pem");
Buffer myCertAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/client-cert.pem");
PemKeyCertOptions pemOptions = new PemKeyCertOptions().
setKeyValue(myKeyAsABuffer).
setCertValue(myCertAsABuffer);
NetClientOptions options = new NetClientOptions().
setSsl(true).
setPemKeyCertOptions(pemOptions);
NetClient client = vertx.createNetClient(options);
Keep in mind that pem configuration, the private key is not crypted.
Updating SSL/TLS configuration
You can use the updateSSLOptions
method to update the key/certifications or trust on a TCP server or client (e.g. to implement certificate rotation).
Future<Boolean> fut = server.updateSSLOptions(new SSLOptions()
.setKeyCertOptions(
new JksOptions()
.setPath("/path/to/your/server-keystore.jks").
setPassword("password-of-your-keystore")));
When the update succeeds the new SSL configuration is used, otherwise the previous configuration is preserved.
The options object is compared (using equals ) against the existing options to prevent an update when the objects are equals since loading options can be costly. When object are equals, you can use the force parameter to force the update. |
Self-signed certificates for testing and development purposes
Do not use this in production settings, and note that the generated keys are very insecure. |
It is very often the case that self-signed certificates are required, be it for unit / integration tests or for running a development version of an application.
SelfSignedCertificate
can be used to provide self-signed PEM certificate helpers and give KeyCertOptions
and TrustOptions
configurations:
SelfSignedCertificate certificate = SelfSignedCertificate.create();
NetServerOptions serverOptions = new NetServerOptions()
.setSsl(true)
.setKeyCertOptions(certificate.keyCertOptions())
.setTrustOptions(certificate.trustOptions());
vertx.createNetServer(serverOptions)
.connectHandler(socket -> socket.end(Buffer.buffer("Hello!")))
.listen(1234, "localhost");
NetClientOptions clientOptions = new NetClientOptions()
.setSsl(true)
.setKeyCertOptions(certificate.keyCertOptions())
.setTrustOptions(certificate.trustOptions());
NetClient client = vertx.createNetClient(clientOptions);
client
.connect(1234, "localhost")
.onComplete(ar -> {
if (ar.succeeded()) {
ar.result().handler(buffer -> System.out.println(buffer));
} else {
System.err.println("Woops: " + ar.cause().getMessage());
}
});
The client can also be configured to trust all certificates:
NetClientOptions clientOptions = new NetClientOptions()
.setSsl(true)
.setTrustAll(true);
Note that self-signed certificates also work for other TCP protocols like HTTPS:
SelfSignedCertificate certificate = SelfSignedCertificate.create();
vertx.createHttpServer(new HttpServerOptions()
.setSsl(true)
.setKeyCertOptions(certificate.keyCertOptions())
.setTrustOptions(certificate.trustOptions()))
.requestHandler(req -> req.response().end("Hello!"))
.listen(8080);
Revoking certificate authorities
Trust can be configured to use a certificate revocation list (CRL) for revoked certificates that should no longer be trusted. The crlPath
configures the crl list to use:
NetClientOptions options = new NetClientOptions().
setSsl(true).
setTrustStoreOptions(trustOptions).
addCrlPath("/path/to/your/crl.pem");
NetClient client = vertx.createNetClient(options);
Buffer configuration is also supported:
Buffer myCrlAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/crl.pem");
NetClientOptions options = new NetClientOptions().
setSsl(true).
setTrustStoreOptions(trustOptions).
addCrlValue(myCrlAsABuffer);
NetClient client = vertx.createNetClient(options);
Configuring the Cipher suite
By default, the TLS configuration will use the list of Cipher suites of the SSL engine:
-
JDK SSL engine when
JdkSSLEngineOptions
is used -
OpenSSL engine when
OpenSSLEngineOptions
is used
This Cipher suite can be configured with a suite of enabled ciphers:
NetServerOptions options = new NetServerOptions().
setSsl(true).
setKeyStoreOptions(keyStoreOptions).
addEnabledCipherSuite("ECDHE-RSA-AES128-GCM-SHA256").
addEnabledCipherSuite("ECDHE-ECDSA-AES128-GCM-SHA256").
addEnabledCipherSuite("ECDHE-RSA-AES256-GCM-SHA384").
addEnabledCipherSuite("CDHE-ECDSA-AES256-GCM-SHA384");
NetServer server = vertx.createNetServer(options);
When the enabled cipher suites is defined (i.e not empty), it takes precedence over the default cipher suites of the SSL engine.
Cipher suite can be specified on the NetServerOptions
or NetClientOptions
configuration.
Configuring TLS protocol versions
By default, the default TLS configuration enables the following protocols: TLSv1.2 and TLSv1.3. Protocol versions can be enabled by explicitly adding them:
NetServerOptions options = new NetServerOptions().
setSsl(true).
setKeyStoreOptions(keyStoreOptions).
addEnabledSecureTransportProtocol("TLSv1.1");
NetServer server = vertx.createNetServer(options);
They can also be removed:
NetServerOptions options = new NetServerOptions().
setSsl(true).
setKeyStoreOptions(keyStoreOptions).
removeEnabledSecureTransportProtocol("TLSv1.2");
NetServer server = vertx.createNetServer(options);
Protocol versions can be specified on the NetServerOptions
or NetClientOptions
configuration.
TLS 1.0 (TLSv1) and TLS 1.1 (TLSv1.1) are widely deprecated and have been disabled by default since Vert.x 4.4.0. |
SSL engine
The engine implementation can be configured to use OpenSSL instead of the JDK implementation. Before JDK started to use hardware intrinsics (CPU instructions) for AES in Java 8 and for RSA in Java 9, OpenSSL provided much better performances and CPU usage than the JDK engine.
The engine options to use is
-
the
getSslEngineOptions
options when it is set -
otherwise
JdkSSLEngineOptions
NetServerOptions options = new NetServerOptions().
setSsl(true).
setKeyStoreOptions(keyStoreOptions);
// Use JDK SSL engine explicitly
options = new NetServerOptions().
setSsl(true).
setKeyStoreOptions(keyStoreOptions).
setJdkSslEngineOptions(new JdkSSLEngineOptions());
// Use OpenSSL engine
options = new NetServerOptions().
setSsl(true).
setKeyStoreOptions(keyStoreOptions).
setOpenSslEngineOptions(new OpenSSLEngineOptions());
Server Name Indication (SNI)
Server Name Indication (SNI) is a TLS extension by which a client specifies a hostname attempting to connect: during the TLS handshake the client gives a server name and the server can use it to respond with a specific certificate for this server name instead of the default deployed certificate. If the server requires client authentication the server can use a specific trusted CA certificate depending on the indicated server name.
When SNI is active the server uses
-
the certificate CN or SAN DNS (Subject Alternative Name with DNS) to do an exact match, e.g
www.example.com
-
the certificate CN or SAN DNS certificate to match a wildcard name, e.g
*.example.com
-
otherwise the first certificate when the client does not present a server name or the presented server name cannot be matched
When the server additionally requires client authentication:
-
if
JksOptions
were used to set the trust options (options
) then an exact match with the trust store alias is done -
otherwise the available CA certificates are used in the same way as if no SNI is in place
You can enable SNI on the server by setting setSni
to true
and configured the server with multiple key/certificate pairs.
Java KeyStore files or PKCS12 files can store multiple key/cert pairs out of the box.
JksOptions keyCertOptions = new JksOptions().setPath("keystore.jks").setPassword("wibble");
NetServer netServer = vertx.createNetServer(new NetServerOptions()
.setKeyStoreOptions(keyCertOptions)
.setSsl(true)
.setSni(true)
);
PemKeyCertOptions
can be configured to hold multiple entries:
PemKeyCertOptions keyCertOptions = new PemKeyCertOptions()
.setKeyPaths(Arrays.asList("default-key.pem", "host1-key.pem", "etc..."))
.setCertPaths(Arrays.asList("default-cert.pem", "host2-key.pem", "etc...")
);
NetServer netServer = vertx.createNetServer(new NetServerOptions()
.setPemKeyCertOptions(keyCertOptions)
.setSsl(true)
.setSni(true)
);
The client implicitly sends the connecting host as an SNI server name for Fully Qualified Domain Name (FQDN).
You can provide an explicit server name when connecting a socket
NetClient client = vertx.createNetClient(new NetClientOptions()
.setTrustStoreOptions(trustOptions)
.setSsl(true)
);
// Connect to 'localhost' and present 'server.name' server name
client
.connect(1234, "localhost", "server.name")
.onComplete(res -> {
if (res.succeeded()) {
System.out.println("Connected!");
NetSocket socket = res.result();
} else {
System.out.println("Failed to connect: " + res.cause().getMessage());
}
});
It can be used for different purposes:
-
present a server name different than the server host
-
present a server name while connecting to an IP
-
force to present a server name when using shortname
Application-Layer Protocol Negotiation (ALPN)
Application-Layer Protocol Negotiation (ALPN) is a TLS extension for application layer protocol negotiation. It is used by HTTP/2: during the TLS handshake the client gives the list of application protocols it accepts and the server responds with a protocol it supports.
Java TLS supports ALPN (Java 8 with the most recent versions).
OpenSSL ALPN support
OpenSSL also supports (native) ALPN.
OpenSSL requires to configure setOpenSslEngineOptions
and use netty-tcnative jar on the classpath. Using tcnative may require OpenSSL to be installed on your OS depending on the tcnative implementation.
Using a proxy for client connections
The NetClient
supports either a HTTP/1.x CONNECT, SOCKS4a or SOCKS5 proxy.
The proxy can be configured in the NetClientOptions
by setting a ProxyOptions
object containing proxy type, hostname, port and optionally username and password.
Here’s an example:
NetClientOptions options = new NetClientOptions()
.setProxyOptions(new ProxyOptions().setType(ProxyType.SOCKS5)
.setHost("localhost").setPort(1080)
.setUsername("username").setPassword("secret"));
NetClient client = vertx.createNetClient(options);
The DNS resolution is always done on the proxy server, to achieve the functionality of a SOCKS4 client, it is necessary to resolve the DNS address locally.
You can use setNonProxyHosts
to configure a list of host bypassing the proxy. The lists accepts *
wildcard for matching domains:
NetClientOptions options = new NetClientOptions()
.setProxyOptions(new ProxyOptions().setType(ProxyType.SOCKS5)
.setHost("localhost").setPort(1080)
.setUsername("username").setPassword("secret"))
.addNonProxyHost("*.foo.com")
.addNonProxyHost("localhost");
NetClient client = vertx.createNetClient(options);
Using HA PROXY protocol
HA PROXY protocol provides a convenient way to safely transport connection information such as a client’s address across multiple layers of NAT or TCP proxies.
HA PROXY protocol can be enabled by setting the option setUseProxyProtocol
and adding the following dependency in your classpath:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-codec-haproxy</artifactId>
<!--<version>Should align with netty version that Vert.x uses</version>-->
</dependency>
NetServerOptions options = new NetServerOptions().setUseProxyProtocol(true);
NetServer server = vertx.createNetServer(options);
server.connectHandler(so -> {
// Print the actual client address provided by the HA proxy protocol instead of the proxy address
System.out.println(so.remoteAddress());
// Print the address of the proxy
System.out.println(so.localAddress());
});
Writing HTTP servers and clients
Vert.x allows you to easily write non blocking HTTP clients and servers.
Vert.x supports the HTTP/1.0, HTTP/1.1 and HTTP/2 protocols.
The base API for HTTP is the same for HTTP/1.x and HTTP/2, specific API features are available for dealing with the HTTP/2 protocol.
Creating an HTTP Server
The simplest way to create an HTTP server, using all default options is as follows:
HttpServer server = vertx.createHttpServer();
Configuring an HTTP server
If you don’t want the default, a server can be configured by passing in a HttpServerOptions
instance when creating it:
HttpServerOptions options = new HttpServerOptions().setMaxWebSocketFrameSize(1000000);
HttpServer server = vertx.createHttpServer(options);
Configuring an HTTP/2 server
Vert.x supports HTTP/2 over TLS h2
and over TCP h2c
.
-
h2
identifies the HTTP/2 protocol when used over TLS negotiated by Application-Layer Protocol Negotiation (ALPN) -
h2c
identifies the HTTP/2 protocol when using in clear text over TCP, such connections are established either with an HTTP/1.1 upgraded request or directly
To handle h2
requests, TLS must be enabled along with setUseAlpn
:
HttpServerOptions options = new HttpServerOptions()
.setUseAlpn(true)
.setSsl(true)
.setKeyStoreOptions(new JksOptions().setPath("/path/to/my/keystore"));
HttpServer server = vertx.createHttpServer(options);
ALPN is a TLS extension that negotiates the protocol before the client and the server start to exchange data.
Clients that don’t support ALPN will still be able to do a classic SSL handshake.
ALPN will usually agree on the h2
protocol, although http/1.1
can be used if the server or the client decides so.
To handle h2c
requests, TLS must be disabled, the server will upgrade to HTTP/2 any request HTTP/1.1 that wants to upgrade to HTTP/2. It will also accept a direct h2c
connection beginning with the PRI * HTTP/2.0\r\nSM\r\n
preface.
most browsers won’t support h2c , so for serving web sites you should use h2 and not h2c . |
When a server accepts an HTTP/2 connection, it sends to the client its initial settings
. The settings define how the client can use the connection, the default initial settings for a server are:
-
getMaxConcurrentStreams
:100
as recommended by the HTTP/2 RFC -
the default HTTP/2 settings values for the others
Configuring server supported HTTP versions
Default supported HTTP versions depends on the server configuration.
-
when TLS is disabled
-
HTTP/1.1, HTTP/1.0
-
HTTP/2 when
isHttp2ClearTextEnabled
istrue
-
when TLS is enabled and ALPN disabled
-
HTTP/1.1 and HTTP/1.0
-
when TLS is enabled and ALPN enabled
-
the protocols defined by
getAlpnVersions
: by default HTTP/1.1 and HTTP/2
If you want to disable HTTP/2 on the server - when TLS is disabled, set setHttp2ClearTextEnabled
to false
- when TLS is enabled - set (isUseAlpn
) to false
- or remove HTTP/2 from the getAlpnVersions
list
Logging network server activity
For debugging purposes, network activity can be logged.
HttpServerOptions options = new HttpServerOptions().setLogActivity(true);
HttpServer server = vertx.createHttpServer(options);
See the chapter on logging network activity for a detailed explanation.
Start the Server Listening
To tell the server to listen for incoming requests you use one of the listen
alternatives.
To tell the server to listen at the host and port as specified in the options:
HttpServer server = vertx.createHttpServer();
server.listen();
Or to specify the host and port in the call to listen, ignoring what is configured in the options:
HttpServer server = vertx.createHttpServer();
server.listen(8080, "myhost.com");
The default host is 0.0.0.0
which means 'listen on all available addresses' and the default port is 80
.
The actual bind is asynchronous so the server might not actually be listening until some time after the call to listen has returned.
If you want to be notified when the server is actually listening you can provide a handler to the listen
call. For example:
HttpServer server = vertx.createHttpServer();
server
.listen(8080, "myhost.com")
.onComplete(res -> {
if (res.succeeded()) {
System.out.println("Server is now listening!");
} else {
System.out.println("Failed to bind!");
}
});
Getting notified of incoming requests
To be notified when a request arrives you need to set a requestHandler
:
HttpServer server = vertx.createHttpServer();
server.requestHandler(request -> {
// Handle the request in here
});
Handling requests
When a request arrives, the request handler is called passing in an instance of HttpServerRequest
. This object represents the server side HTTP request.
The handler is called when the headers of the request have been fully read.
If the request contains a body, that body will arrive at the server some time after the request handler has been called.
The server request object allows you to retrieve the uri
, path
, params
and headers
, amongst other things.
Each server request object is associated with one server response object. You use response
to get a reference to the HttpServerResponse
object.
Here’s a simple example of a server handling a request and replying with "hello world" to it.
vertx.createHttpServer().requestHandler(request -> {
request.response().end("Hello world");
}).listen(8080);
Request version
The version of HTTP specified in the request can be retrieved with version
Request method
Use method
to retrieve the HTTP method of the request. (i.e. whether it’s GET, POST, PUT, DELETE, HEAD, OPTIONS, etc).
Request URI
Use uri
to retrieve the URI of the request.
Note that this is the actual URI as passed in the HTTP request, and it’s almost always a relative URI.
The URI is as defined in Section 5.1.2 of the HTTP specification - Request-URI
Request path
Use path
to return the path part of the URI
For example, if the request URI was `a/b/c/page.html?param1=abc¶m2=xyz
Then the path would be /a/b/c/page.html
Request query
Use query
to return the query part of the URI
For example, if the request URI was a/b/c/page.html?param1=abc¶m2=xyz
Then the query would be param1=abc¶m2=xyz
Request headers
Use headers
to return the headers of the HTTP request.
This returns an instance of MultiMap
- which is like a normal Map or Hash but allows multiple values for the same key - this is because HTTP allows multiple header values with the same key.
It also has case-insensitive keys, that means you can do the following:
MultiMap headers = request.headers();
// Get the User-Agent:
System.out.println("User agent is " + headers.get("user-agent"));
// You can also do this and get the same result:
System.out.println("User agent is " + headers.get("User-Agent"));
Request authority
Use authority
to return the authority of the HTTP request.
For HTTP/1.x requests the host
header is returned, for HTTP/1 requests the :authority
pseudo header is returned.
Request parameters
Use params
to return the parameters of the HTTP request.
Just like headers
this returns an instance of MultiMap
as there can be more than one parameter with the same name.
Request parameters are sent on the request URI, after the path. For example if the URI was /page.html?param1=abc¶m2=xyz
Then the parameters would contain the following:
param1: 'abc' param2: 'xyz
Note that these request parameters are retrieved from the URL of the request. If you have form attributes that have been sent as part of the submission of an HTML form submitted in the body of a multi-part/form-data
request then they will not appear in the params here.
Remote address
The address of the sender of the request can be retrieved with remoteAddress
.
Absolute URI
The URI passed in an HTTP request is usually relative. If you wish to retrieve the absolute URI corresponding to the request, you can get it with absoluteURI
End handler
The endHandler
of the request is invoked when the entire request, including any body has been fully read.
Reading Data from the Request Body
Often an HTTP request contains a body that we want to read. As previously mentioned the request handler is called when just the headers of the request have arrived so the request object does not have a body at that point.
This is because the body may be very large (e.g. a file upload) and we don’t generally want to buffer the entire body in memory before handing it to you, as that could cause the server to exhaust available memory.
To receive the body, you can use the handler
on the request, this will get called every time a chunk of the request body arrives. Here’s an example:
request.handler(buffer -> {
System.out.println("I have received a chunk of the body of length " + buffer.length());
});
The object passed into the handler is a Buffer
, and the handler can be called multiple times as data arrives from the network, depending on the size of the body.
In some cases (e.g. if the body is small) you will want to aggregate the entire body in memory, so you could do the aggregation yourself as follows:
Buffer totalBuffer = Buffer.buffer();
request.handler(buffer -> {
System.out.println("I have received a chunk of the body of length " + buffer.length());
totalBuffer.appendBuffer(buffer);
});
request.endHandler(v -> {
System.out.println("Full body received, length = " + totalBuffer.length());
});
This is such a common case, that Vert.x provides a bodyHandler
to do this for you. The body handler is called once when all the body has been received:
request.bodyHandler(totalBuffer -> {
System.out.println("Full body received, length = " + totalBuffer.length());
});
Streaming requests
The request object is a ReadStream
so you can pipe the request body to any WriteStream
instance.
See the chapter on streams for a detailed explanation.
Handling HTML forms
HTML forms can be submitted with either a content type of application/x-www-form-urlencoded
or multipart/form-data
.
For url encoded forms, the form attributes are encoded in the url, just like normal query parameters.
For multi-part forms they are encoded in the request body, and as such are not available until the entire body has been read from the wire.
Multi-part forms can also contain file uploads.
If you want to retrieve the attributes of a multi-part form you should tell Vert.x that you expect to receive such a form before any of the body is read by calling setExpectMultipart
with true
, and then you should retrieve the actual attributes using formAttributes
once the entire body has been read:
server.requestHandler(request -> {
request.setExpectMultipart(true);
request.endHandler(v -> {
// The body has now been fully read, so retrieve the form attributes
MultiMap formAttributes = request.formAttributes();
});
});
Form attributes have a maximum size of 8192
bytes. When the client submits a form with an attribute size greater than this value, the file upload triggers an exception on HttpServerRequest
exception handler. You can set a different maximum size with setMaxFormAttributeSize
.
Handling form file uploads
Vert.x can also handle file uploads which are encoded in a multi-part request body.
To receive file uploads you tell Vert.x to expect a multi-part form and set an uploadHandler
on the request.
This handler will be called once for every upload that arrives on the server.
The object passed into the handler is a HttpServerFileUpload
instance.
server.requestHandler(request -> {
request.setExpectMultipart(true);
request.uploadHandler(upload -> {
System.out.println("Got a file upload " + upload.name());
});
});
File uploads can be large we don’t provide the entire upload in a single buffer as that might result in memory exhaustion, instead, the upload data is received in chunks:
request.uploadHandler(upload -> {
upload.handler(chunk -> {
System.out.println("Received a chunk of the upload of length " + chunk.length());
});
});
The upload object is a ReadStream
so you can pipe the request body to any WriteStream
instance. See the chapter on streams for a detailed explanation.
If you just want to upload the file to disk somewhere you can use streamToFileSystem
:
request.uploadHandler(upload -> {
upload.streamToFileSystem("myuploads_directory/" + upload.filename());
});
Make sure you check the filename in a production system to avoid malicious clients uploading files to arbitrary places on your filesystem. See security notes for more information. |
Handling cookies
To remove a cookie, use removeCookie
.
To add a cookie use addCookie
.
The set of cookies will be written back in the response automatically when the response headers are written so the browser can store them.
Cookies are described by instances of Cookie
. This allows you to retrieve the name, value, domain, path and other normal cookie properties.
Same Site Cookies let servers require that a cookie shouldn’t be sent with cross-site (where Site is defined by the registrable domain) requests, which provides some protection against cross-site request forgery attacks. This kind of cookies are enabled using the setter: setSameSite
.
Same site cookies can have one of 3 values:
-
None - The browser will send cookies with both cross-site requests and same-site requests.
-
Strict - The browser will only send cookies for same-site requests (requests originating from the site that set the cookie). If the request originated from a different URL than the URL of the current location, none of the cookies tagged with the Strict attribute will be included.
-
Lax - Same-site cookies are withheld on cross-site subrequests, such as calls to load images or frames, but will be sent when a user navigates to the URL from an external site; for example, by following a link.
Here’s an example of querying and adding cookies:
Cookie someCookie = request.getCookie("mycookie");
String cookieValue = someCookie.getValue();
// Do something with cookie...
// Add a cookie - this will get written back in the response automatically
request.response().addCookie(Cookie.cookie("othercookie", "somevalue"));
Handling compressed body
Vert.x can handle compressed body payloads which are encoded by the client with the deflate, gzip or brotli algorithms.
To enable decompression set setDecompressionSupported
on the options when creating the server.
You need to have Brotli4j on the classpath to decompress Brotli:
-
Maven (in your
pom.xml
):
<dependency>
<groupId>com.aayushatharva.brotli4j</groupId>
<artifactId>brotli4j</artifactId>
<version>${brotli4j.version}</version>
</dependency>
-
Gradle (in your
build.gradle
file):
dependencies {
implementation 'com.aayushatharva.brotli4j:brotli4j:${brotli4j.version}'
runtimeOnly 'com.aayushatharva.brotli4j:native-$system-and-arch:${brotli4j.version}'
}
When using Gradle, you need to add the runtime native library manually depending on your OS and architecture. See the Gradle section of Brotli4j for more details.
By default, decompression is disabled.
Receiving custom HTTP/2 frames
HTTP/2 is a framed protocol with various frames for the HTTP request/response model. The protocol allows other kind of frames to be sent and received.
To receive custom frames, you can use the customFrameHandler
on the request, this will get called every time a custom frame arrives. Here’s an example:
request.customFrameHandler(frame -> {
System.out.println("Received a frame type=" + frame.type() +
" payload" + frame.payload().toString());
});
HTTP/2 frames are not subject to flow control - the frame handler will be called immediately when a custom frame is received whether the request is paused or is not
Sending back responses
The server response object is an instance of HttpServerResponse
and is obtained from the request with response
.
You use the response object to write a response back to the HTTP client.
Setting status code and message
The default HTTP status code for a response is 200
, representing OK
.
Use setStatusCode
to set a different code.
You can also specify a custom status message with setStatusMessage
.
If you don’t specify a status message, the default one corresponding to the status code will be used.
for HTTP/2 the status won’t be present in the response since the protocol won’t transmit the message to the client |
Writing HTTP responses
To write data to an HTTP response, you use one of the write
operations.
These can be invoked multiple times before the response is ended. They can be invoked in a few ways:
With a single buffer:
HttpServerResponse response = request.response();
response.write(buffer);
With a string. In this case the string will encoded using UTF-8 and the result written to the wire.
HttpServerResponse response = request.response();
response.write("hello world!");
With a string and an encoding. In this case the string will encoded using the specified encoding and the result written to the wire.
HttpServerResponse response = request.response();
response.write("hello world!", "UTF-16");
Writing to a response is asynchronous and always returns immediately after write has been queued.
If you are just writing a single string or buffer to the HTTP response you can write it and end the response in a single call to the end
The first call to write results in the response header being written to the response. Consequently, if you are not using HTTP chunking then you must set the Content-Length
header before writing to the response, since it will be too late otherwise. If you are using HTTP chunking you do not have to worry.
Ending HTTP responses
Once you have finished with the HTTP response you should end
it.
This can be done in several ways:
With no arguments, the response is simply ended.
HttpServerResponse response = request.response();
response.write("hello world!");
response.end();
It can also be called with a string or buffer in the same way write
is called. In this case it’s just the same as calling write with a string or buffer followed by calling end with no arguments. For example:
HttpServerResponse response = request.response();
response.end("hello world!");
Closing the underlying connection
You can close the underlying TCP connection with close
.
Non keep-alive connections will be automatically closed by Vert.x when the response is ended.
Keep-alive connections are not automatically closed by Vert.x by default. If you want keep-alive connections to be closed after an idle time, then you configure setIdleTimeout
.
HTTP/2 connections send a {@literal GOAWAY} frame before closing the response.
Setting response headers
HTTP response headers can be added to the response by adding them directly to the headers
:
HttpServerResponse response = request.response();
MultiMap headers = response.headers();
headers.set("content-type", "text/html");
headers.set("other-header", "wibble");
Or you can use putHeader
HttpServerResponse response = request.response();
response.putHeader("content-type", "text/html").putHeader("other-header", "wibble");
Headers must all be added before any parts of the response body are written.
Chunked HTTP responses and trailers
Vert.x supports HTTP Chunked Transfer Encoding.
This allows the HTTP response body to be written in chunks, and is normally used when a large response body is being streamed to a client and the total size is not known in advance.
You put the HTTP response into chunked mode as follows:
HttpServerResponse response = request.response();
response.setChunked(true);
Default is non-chunked. When in chunked mode, each call to one of the write
methods will result in a new HTTP chunk being written out.
When in chunked mode you can also write HTTP response trailers to the response. These are actually written in the final chunk of the response.
chunked response has no effect for an HTTP/2 stream |
To add trailers to the response, add them directly to the trailers
.
HttpServerResponse response = request.response();
response.setChunked(true);
MultiMap trailers = response.trailers();
trailers.set("X-wibble", "woobble").set("X-quux", "flooble");
Or use putTrailer
.
HttpServerResponse response = request.response();
response.setChunked(true);
response.putTrailer("X-wibble", "woobble").putTrailer("X-quux", "flooble");
Serving files directly from disk or the classpath
If you were writing a web server, one way to serve a file from disk would be to open it as an AsyncFile
and pipe it to the HTTP response.
Or you could load it it one go using readFile
and write it straight to the response.
Alternatively, Vert.x provides a method which allows you to serve a file from disk or the filesystem to an HTTP response in one operation. Where supported by the underlying operating system this may result in the OS directly transferring bytes from the file to the socket without being copied through user-space at all.
This is done by using sendFile
, and is usually more efficient for large files, but may be slower for small files.
Here’s a very simple web server that serves files from the file system using sendFile:
vertx.createHttpServer().requestHandler(request -> {
String file = "";
if (request.path().equals("/")) {
file = "index.html";
} else if (!request.path().contains("..")) {
file = request.path();
}
request.response().sendFile("web/" + file);
}).listen(8080);
Sending a file is asynchronous and may not complete until some time after the call has returned. If you want to be notified when the file has been written you can use sendFile
Please see the chapter about serving files from the classpath for restrictions about the classpath resolution or disabling it.
If you use sendFile while using HTTPS it will copy through user-space, since if the kernel is copying data directly from disk to socket it doesn’t give us an opportunity to apply any encryption. |
If you’re going to write web servers directly using Vert.x be careful that users cannot exploit the path to access files outside the directory from which you want to serve them or the classpath It may be safer instead to use Vert.x Web. |
When there is a need to serve just a segment of a file, say starting from a given byte, you can achieve this by doing:
vertx.createHttpServer().requestHandler(request -> {
long offset = 0;
try {
offset = Long.parseLong(request.getParam("start"));
} catch (NumberFormatException e) {
// error handling...
}
long end = Long.MAX_VALUE;
try {
end = Long.parseLong(request.getParam("end"));
} catch (NumberFormatException e) {
// error handling...
}
request.response().sendFile("web/mybigfile.txt", offset, end);
}).listen(8080);
You are not required to supply the length if you want to send a file starting from an offset until the end, in this case you can just do:
vertx.createHttpServer().requestHandler(request -> {
long offset = 0;
try {
offset = Long.parseLong(request.getParam("start"));
} catch (NumberFormatException e) {
// error handling...
}
request.response().sendFile("web/mybigfile.txt", offset);
}).listen(8080);
Piping responses
The server response is a WriteStream
so you can pipe to it from any ReadStream
, e.g. AsyncFile
, NetSocket
, WebSocket
or HttpServerRequest
.
Here’s an example which echoes the request body back in the response for any PUT methods. It uses a pipe for the body, so it will work even if the HTTP request body is much larger than can fit in memory at any one time:
vertx.createHttpServer().requestHandler(request -> {
HttpServerResponse response = request.response();
if (request.method() == HttpMethod.PUT) {
response.setChunked(true);
request.pipeTo(response);
} else {
response.setStatusCode(400).end();
}
}).listen(8080);
You can also use the send
method to send a ReadStream
.
Sending a stream is a pipe operation, however as this is a method of HttpServerResponse
, it will also take care of chunking the response when the content-length
is not set.
vertx.createHttpServer().requestHandler(request -> {
HttpServerResponse response = request.response();
if (request.method() == HttpMethod.PUT) {
response.send(request);
} else {
response.setStatusCode(400).end();
}
}).listen(8080);
Writing HTTP/2 frames
HTTP/2 is a framed protocol with various frames for the HTTP request/response model. The protocol allows other kind of frames to be sent and received.
To send such frames, you can use the writeCustomFrame
on the response. Here’s an example:
int frameType = 40;
int frameStatus = 10;
Buffer payload = Buffer.buffer("some data");
// Sending a frame to the client
response.writeCustomFrame(frameType, frameStatus, payload);
These frames are sent immediately and are not subject to flow control - when such frame is sent there it may be done before other {@literal DATA} frames.
Stream reset
HTTP/1.x does not allow a clean reset of a request or a response stream, for example when a client uploads a resource already present on the server, the server needs to accept the entire response.
HTTP/2 supports stream reset at any time during the request/response:
request.response().reset();
By default, the NO_ERROR
(0) error code is sent, another code can sent instead:
request.response().reset(8);
The HTTP/2 specification defines the list of error codes one can use.
The request handler are notified of stream reset events with the request handler
and response handler
:
request.response().exceptionHandler(err -> {
if (err instanceof StreamResetException) {
StreamResetException reset = (StreamResetException) err;
System.out.println("Stream reset " + reset.getCode());
}
});
Server push
Server push is a new feature of HTTP/2 that enables sending multiple responses in parallel for a single client request.
When a server process a request, it can push a request/response to the client:
HttpServerResponse response = request.response();
// Push main.js to the client
response
.push(HttpMethod.GET, "/main.js")
.onComplete(ar -> {
if (ar.succeeded()) {
// The server is ready to push the response
HttpServerResponse pushedResponse = ar.result();
// Send main.js response
pushedResponse.
putHeader("content-type", "application/json").
end("alert(\"Push response hello\")");
} else {
System.out.println("Could not push client resource " + ar.cause());
}
});
// Send the requested resource
response.sendFile("<html><head><script src=\"/main.js\"></script></head><body></body></html>");
When the server is ready to push the response, the push response handler is called and the handler can send the response.
The push response handler may receive a failure, for instance the client may cancel the push because it already has main.js
in its cache and does not want it anymore.
The push
method must be called before the initiating response ends, however the pushed response can be written after.
Handling exceptions
You can set an exceptionHandler
to receive any exceptions that happens before the connection is passed to the requestHandler
or to the webSocketHandler
, e.g. during the TLS handshake.
Handling invalid requests
Vert.x will handle invalid HTTP requests and provides a default handler that will handle the common case appropriately, e.g. it does respond with REQUEST_HEADER_FIELDS_TOO_LARGE
when a request header is too long.
You can set your own invalidRequestHandler
to process invalid requests. Your implementation can handle specific cases and delegate other cases to to HttpServerRequest.DEFAULT_INVALID_REQUEST_HANDLER
.
HTTP Compression
Vert.x comes with support for HTTP Compression out of the box.
This means you are able to automatically compress the body of the responses before they are sent back to the client.
If the client does not support HTTP compression the responses are sent back without compressing the body.
This allows to handle Client that support HTTP Compression and those that not support it at the same time.
To enable compression use can configure it with setCompressionSupported
.
By default, compression is not enabled.
When HTTP compression is enabled the server will check if the client includes an Accept-Encoding
header which includes the supported compressions. Commonly used are deflate and gzip. Both are supported by Vert.x.
If such a header is found the server will automatically compress the body of the response with one of the supported compressions and send it back to the client.
Whenever the response needs to be sent without compression you can set the header content-encoding
to identity
:
request.response()
.putHeader(HttpHeaders.CONTENT_ENCODING, HttpHeaders.IDENTITY)
.sendFile("/path/to/image.jpg");
Be aware that compression may be able to reduce network traffic but is more CPU-intensive.
To address this latter issue Vert.x allows you to tune the 'compression level' parameter that is native of the gzip/deflate compression algorithms.
Compression level allows to configure gizp/deflate algorithms in terms of the compression ratio of the resulting data and the computational cost of the compress/decompress operation.
The compression level is an integer value ranged from '1' to '9', where '1' means lower compression ratio but fastest algorithm and '9' means maximum compression ratio available but a slower algorithm.
Using compression levels higher that 1-2 usually allows to save just some bytes in size - the gain is not linear, and depends on the specific data to be compressed - but it comports a non-trascurable cost in term of CPU cycles required to the server while generating the compressed response data ( Note that at moment Vert.x doesn’t support any form caching of compressed response data, even for static files, so the compression is done on-the-fly at every request body generation ) and in the same way it affects client(s) while decoding (inflating) received responses, operation that becomes more CPU-intensive the more the level increases.
By default - if compression is enabled via setCompressionSupported
- Vert.x will use '6' as compression level, but the parameter can be configured to address any case with setCompressionLevel
.
HTTP compression algorithms
Vert.x supports out of the box deflate and gzip.
Brotli and zstandard can also be used.
new HttpServerOptions()
.addCompressor(io.netty.handler.codec.compression.StandardCompressionOptions.gzip())
.addCompressor(io.netty.handler.codec.compression.StandardCompressionOptions.deflate())
.addCompressor(io.netty.handler.codec.compression.StandardCompressionOptions.brotli())
.addCompressor(io.netty.handler.codec.compression.StandardCompressionOptions.zstd());
use StandardCompressionOptions static methods to create CompressionOptions |
Brotli and zstandard libraries need to be added to the classpath.
-
Maven (in your
pom.xml
):
<dependency>
<groupId>com.aayushatharva.brotli4j</groupId>
<artifactId>brotli4j</artifactId>
<version>${brotli4j.version}</version>
</dependency>
<dependency>
<groupId>com.github.luben</groupId>
<artifactId>zstd-jni</artifactId>
<version>${zstd-jini.version}</version>
</dependency>
-
Gradle (in your
build.gradle
file):
dependencies {
implementation 'com.aayushatharva.brotli4j:brotli4j:${brotli4j.version}'
runtimeOnly 'com.aayushatharva.brotli4j:native-$system-and-arch:${brotli4j.version}'
implementation 'com.github.luben:zstd-jni:${zstd-jini.version}'
}
When using Gradle, you need to add the runtime native library manually depending on your OS and architecture. See the Gradle section of Brotli4j for more details.
You can configure compressors according to your needs
GzipOptions gzip = StandardCompressionOptions.gzip(6, 15, 8);
Creating an HTTP client
You create an HttpClient
instance with default options as follows:
HttpClient client = vertx.createHttpClient();
If you want to configure options for the client, you create it as follows:
HttpClientOptions options = new HttpClientOptions().setKeepAlive(false);
HttpClient client = vertx.createHttpClient(options);
Vert.x supports HTTP/2 over TLS h2
and over TCP h2c
.
By default, the http client performs HTTP/1.1 requests, to perform HTTP/2 requests the setProtocolVersion
must be set to HTTP_2
.
For h2
requests, TLS must be enabled with Application-Layer Protocol Negotiation:
HttpClientOptions options = new HttpClientOptions().
setProtocolVersion(HttpVersion.HTTP_2).
setSsl(true).
setUseAlpn(true).
setTrustAll(true);
HttpClient client = vertx.createHttpClient(options);
For h2c
requests, TLS must be disabled, the client will do an HTTP/1.1 requests and try an upgrade to HTTP/2:
HttpClientOptions options = new HttpClientOptions().setProtocolVersion(HttpVersion.HTTP_2);
HttpClient client = vertx.createHttpClient(options);
h2c
connections can also be established directly, i.e. connection started with a prior knowledge, when setHttp2ClearTextUpgrade
options is set to false: after the connection is established, the client will send the HTTP/2 connection preface and expect to receive the same preface from the server.
The http server may not support HTTP/2, the actual version can be checked with version
when the response arrives.
When a clients connects to an HTTP/2 server, it sends to the server its initial settings
. The settings define how the server can use the connection, the default initial settings for a client are the default values defined by the HTTP/2 RFC.
Pool configuration
For performance purpose, the client uses connection pooling when interacting with HTTP/1.1 servers. The pool creates up to 5 connections per server. You can override the pool configuration like this:
PoolOptions options = new PoolOptions().setHttp1MaxSize(10);
HttpClient client = vertx.createHttpClient(options);
You can configure various pool options
as follows
-
options#setHttp1MaxSize
the maximum number of opened per HTTP/1.x server (5 by default) -
options#setHttp2MaxSize
the maximum number of opened per HTTP/2 server (1 by default), you should not change this value since a single HTTP/2 connection is capable of delivering the same performance level than multiple HTTP/1.x connections -
options#setCleanerPeriod
the period in milliseconds at which the pool checks expired connections (1 second by default) -
options#setEventLoopSize
sets the number of event loops the pool use (0 by default) -
a value of 0 configures the pool to use the event loop of the caller
-
a positive value configures the pool load balance the creation of connection over a list of event loops determined by the value
-
options#setMaxWaitQueueSize
the maximum number of HTTP requests waiting until a connection is available, when the queue is full, the request is rejected
Logging network client activity
For debugging purposes, network activity can be logged.
HttpClientOptions options = new HttpClientOptions().setLogActivity(true);
HttpClient client = vertx.createHttpClient(options);
See the chapter on logging network activity for a detailed explanation.
Advanced HTTP client creation
You can pass options createHttpClient
methods to configure the HTTP client.
Alternatively you can build a client with the builder API
:
HttpClient build = vertx
.httpClientBuilder()
.with(options)
.build();
In addition to HttpClientOptions
and PoolOptions
, you can set
Making requests
The http client is very flexible and there are various ways you can make requests with it.
The first step when making a request is obtaining an HTTP connection to the remote server:
client
.request(HttpMethod.GET, 8080, "myserver.mycompany.com", "/some-uri")
.onComplete(ar1 -> {
if (ar1.succeeded()) {
// Connected to the server
}
});
The client will connect to the remote server or reuse an available connection from the client connection pool.
Default host and port
Often you want to make many requests to the same host/port with an http client. To avoid you repeating the host/port every time you make a request you can configure the client with a default host/port:
HttpClientOptions options = new HttpClientOptions().setDefaultHost("wibble.com");
// Can also set default port if you want...
HttpClient client = vertx.createHttpClient(options);
client
.request(HttpMethod.GET, "/some-uri")
.onComplete(ar1 -> {
if (ar1.succeeded()) {
HttpClientRequest request = ar1.result();
request
.send()
.onComplete(ar2 -> {
if (ar2.succeeded()) {
HttpClientResponse response = ar2.result();
System.out.println("Received response with status code " + response.statusCode());
}
});
}
});
Writing request headers
You can write headers to a request using the HttpHeaders
as follows:
HttpClient client = vertx.createHttpClient();
// Write some headers using the headers multi-map
MultiMap headers = HttpHeaders.set("content-type", "application/json").set("other-header", "foo");
client
.request(HttpMethod.GET, "some-uri")
.onComplete(ar1 -> {
if (ar1.succeeded()) {
if (ar1.succeeded()) {
HttpClientRequest request = ar1.result();
request.headers().addAll(headers);
request
.send()
.onComplete(ar2 -> {
HttpClientResponse response = ar2.result();
System.out.println("Received response with status code " + response.statusCode());
});
}
}
});
The headers are an instance of MultiMap
which provides operations for adding, setting and removing entries. Http headers allow more than one value for a specific key.
You can also write headers using putHeader
request.putHeader("content-type", "application/json")
.putHeader("other-header", "foo");
If you wish to write headers to the request you must do so before any part of the request body is written.
Writing request and processing response
The HttpClientRequest
request
methods connects to the remote server or reuse an existing connection. The request instance obtained is pre-populated with some data such like the host or the request URI, but you need to send this request to the server.
You can call send
to send a request such as an HTTP GET
and process the asynchronous HttpClientResponse
.
client
.request(HttpMethod.GET, 8080, "myserver.mycompany.com", "/some-uri")
.onComplete(ar1 -> {
if (ar1.succeeded()) {
HttpClientRequest request = ar1.result();
// Send the request and process the response
request
.send()
.onComplete(ar -> {
if (ar.succeeded()) {
HttpClientResponse response = ar.result();
System.out.println("Received response with status code " + response.statusCode());
} else {
System.out.println("Something went wrong " + ar.cause().getMessage());
}
});
}
});
You can also send the request with a body.
send
with a string, the Content-Length
header will be set for you if it was not previously set.
client
.request(HttpMethod.GET, 8080, "myserver.mycompany.com", "/some-uri")
.onComplete(ar1 -> {
if (ar1.succeeded()) {
HttpClientRequest request = ar1.result();
// Send the request and process the response
request
.send("Hello World")
.onComplete(ar -> {
if (ar.succeeded()) {
HttpClientResponse response = ar.result();
System.out.println("Received response with status code " + response.statusCode());
} else {
System.out.println("Something went wrong " + ar.cause().getMessage());
}
});
}
});
send
with a buffer, the Content-Length
header will be set for you if it was not previously set.
request
.send(Buffer.buffer("Hello World"))
.onComplete(ar -> {
if (ar.succeeded()) {
HttpClientResponse response = ar.result();
System.out.println("Received response with status code " + response.statusCode());
} else {
System.out.println("Something went wrong " + ar.cause().getMessage());
}
});
send
with a stream, if the Content-Length
header was not previously set, the request is sent with a chunked Content-Encoding
.
request
.putHeader(HttpHeaders.CONTENT_LENGTH, "1000")
.send(stream)
.onComplete(ar -> {
if (ar.succeeded()) {
HttpClientResponse response = ar.result();
System.out.println("Received response with status code " + response.statusCode());
} else {
System.out.println("Something went wrong " + ar.cause().getMessage());
}
});
Streaming Request body
The send
method send requests at once.
Sometimes you’ll want to have low level control on how you write requests bodies.
The HttpClientRequest
can be used to write the request body.
Here are some examples of writing a POST request with a body:
HttpClient client = vertx.createHttpClient();
client.request(HttpMethod.POST, "some-uri")
.onSuccess(request -> {
request.response().onSuccess(response -> {
System.out.println("Received response with status code " + response.statusCode());
});
// Now do stuff with the request
request.putHeader("content-length", "1000");
request.putHeader("content-type", "text/plain");
request.write(body);
// Make sure the request is ended when you're done with it
request.end();
});
Methods exist to write strings in UTF-8 encoding and in any specific encoding and to write buffers:
request.write("some data");
// Write string encoded in specific encoding
request.write("some other data", "UTF-16");
// Write a buffer
Buffer buffer = Buffer.buffer();
buffer.appendInt(123).appendLong(245l);
request.write(buffer);
If you are just writing a single string or buffer to the HTTP request you can write it and end the request in a single call to the end
function.
request.end("some simple data");
// Write buffer and end the request (send it) in a single call
Buffer buffer = Buffer.buffer().appendDouble(12.34d).appendLong(432l);
request.end(buffer);
When you’re writing to a request, the first call to write
will result in the request headers being written out to the wire.
The actual write is asynchronous and might not occur until some time after the call has returned.
Non-chunked HTTP requests with a request body require a Content-Length
header to be provided.
Consequently, if you are not using chunked HTTP then you must set the Content-Length
header before writing to the request, as it will be too late otherwise.
If you are calling one of the end
methods that take a string or buffer then Vert.x will automatically calculate and set the Content-Length
header before writing the request body.
If you are using HTTP chunking a Content-Length
header is not required, so you do not have to calculate the size up-front.
Ending streamed HTTP requests
Once you have finished with the HTTP request you must end it with one of the end
operations.
Ending a request causes any headers to be written, if they have not already been written and the request to be marked as complete.
Requests can be ended in several ways. With no arguments the request is simply ended:
request.end();
Or a string or buffer can be provided in the call to end
. This is like calling write
with the string or buffer before calling end
with no arguments
request.end("some-data");
// End it with a buffer
Buffer buffer = Buffer.buffer().appendFloat(12.3f).appendInt(321);
request.end(buffer);
Using the request as a stream
An HttpClientRequest
instance is also a WriteStream
instance.
You can pipe to it from any ReadStream
instance.
For, example, you could pipe a file on disk to a http request body as follows:
request.setChunked(true);
file.pipeTo(request);
Chunked HTTP requests
Vert.x supports HTTP Chunked Transfer Encoding for requests.
This allows the HTTP request body to be written in chunks, and is normally used when a large request body is being streamed to the server, whose size is not known in advance.
You put the HTTP request into chunked mode using setChunked
.
In chunked mode each call to write will cause a new chunk to be written to the wire. In chunked mode there is no need to set the Content-Length
of the request up-front.
request.setChunked(true);
// Write some chunks
for (int i = 0; i < 10; i++) {
request.write("this-is-chunk-" + i);
}
request.end();
Request timeouts
You can set an idle timeout to prevent your application from unresponsive servers using setIdleTimeout
or idleTimeout
. When the request does not return any data within the timeout period an exception will fail the result and the request will be reset.
Future<Buffer> fut = client
.request(new RequestOptions()
.setHost(host)
.setPort(port)
.setURI(uri)
.setIdleTimeout(timeoutMS))
.compose(request -> request.send().compose(HttpClientResponse::body));
the timeout starts when the HttpClientRequest is available, implying a connection was obtained from the pool. |
You can set a connect timeout to prevent your application from unresponsive busy client connection pool. The Future<HttpClientRequest>
is failed when a connection is not obtained before the timeout delay.
The connect timeout option is not related to the TCP setConnectTimeout
option, when a request is made against a pooled HTTP client, the timeout applies to the duration to obtain a connection from the pool to serve the request, the timeout might fire because the server does not respond in time or the pool is too busy to serve a request.
You can configure both timeout using setTimeout
Future<Buffer> fut = client
.request(new RequestOptions()
.setHost(host)
.setPort(port)
.setURI(uri)
.setTimeout(timeoutMS))
.compose(request -> request.send().compose(HttpClientResponse::body));
Writing HTTP/2 frames
HTTP/2 is a framed protocol with various frames for the HTTP request/response model. The protocol allows other kind of frames to be sent and received.
To send such frames, you can use the write
on the request. Here’s an example:
int frameType = 40;
int frameStatus = 10;
Buffer payload = Buffer.buffer("some data");
// Sending a frame to the server
request.writeCustomFrame(frameType, frameStatus, payload);
Stream reset
HTTP/1.x does not allow a clean reset of a request or a response stream, for example when a client uploads a resource already present on the server, the server needs to accept the entire response.
HTTP/2 supports stream reset at any time during the request/response:
request.reset();
By default the NO_ERROR (0) error code is sent, another code can sent instead:
request.reset(8);
The HTTP/2 specification defines the list of error codes one can use.
The request handler are notified of stream reset events with the request handler
and response handler
:
request.exceptionHandler(err -> {
if (err instanceof StreamResetException) {
StreamResetException reset = (StreamResetException) err;
System.out.println("Stream reset " + reset.getCode());
}
});
HTTP/2 RST flood protection
An HTTP/2 server is protected against RST flood DDOS attacks (CVE-2023-44487): there is an upper bound to the number of RST frames a server can receive in a time window. The default configuration sets the upper bound to 200
for a duration of 30
seconds.
You can use setHttp2RstFloodMaxRstFramePerWindow
and setHttp2RstFloodWindowDuration
to override these settings.
Handling HTTP responses
You receive an instance of HttpClientResponse
into the handler that you specify in of the request methods or by setting a handler directly on the HttpClientRequest
object.
You can query the status code and the status message of the response with statusCode
and statusMessage
.
request
.send()
.onComplete(ar2 -> {
if (ar2.succeeded()) {
HttpClientResponse response = ar2.result();
// the status code - e.g. 200 or 404
System.out.println("Status code is " + response.statusCode());
// the status message e.g. "OK" or "Not Found".
System.out.println("Status message is " + response.statusMessage());
}
});
Using the response as a stream
The HttpClientResponse
instance is also a ReadStream
which means you can pipe it to any WriteStream
instance.
Response headers and trailers
Http responses can contain headers. Use headers
to get the headers.
The object returned is a MultiMap
as HTTP headers can contain multiple values for single keys.
String contentType = response.headers().get("content-type");
String contentLength = response.headers().get("content-lengh");
Chunked HTTP responses can also contain trailers - these are sent in the last chunk of the response body.
Reading the request body
The response handler is called when the headers of the response have been read from the wire.
If the response has a body this might arrive in several pieces some time after the headers have been read. We don’t wait for all the body to arrive before calling the response handler as the response could be very large and we might be waiting a long time, or run out of memory for large responses.
As parts of the response body arrive, the handler
is called with a Buffer
representing the piece of the body:
client
.request(HttpMethod.GET, "some-uri")
.onComplete(ar1 -> {
if (ar1.succeeded()) {
HttpClientRequest request = ar1.result();
request
.send()
.onComplete(ar2 -> {
HttpClientResponse response = ar2.result();
response.handler(buffer -> {
System.out.println("Received a part of the response body: " + buffer);
});
});
}
});
If you know the response body is not very large and want to aggregate it all in memory before handling it, you can either aggregate it yourself:
request
.send()
.onComplete(ar2 -> {
if (ar2.succeeded()) {
HttpClientResponse response = ar2.result();
// Create an empty buffer
Buffer totalBuffer = Buffer.buffer();
response.handler(buffer -> {
System.out.println("Received a part of the response body: " + buffer.length());
totalBuffer.appendBuffer(buffer);
});
response.endHandler(v -> {
// Now all the body has been read
System.out.println("Total response body length is " + totalBuffer.length());
});
}
});
Or you can use the convenience body
which is called with the entire body when the response has been fully read:
request
.send()
.onComplete(ar1 -> {
if (ar1.succeeded()) {
HttpClientResponse response = ar1.result();
response
.body()
.onComplete(ar2 -> {
if (ar2.succeeded()) {
Buffer body = ar2.result();
// Now all the body has been read
System.out.println("Total response body length is " + body.length());
}
});
}
});
Response end handler
The response endHandler
is called when the entire response body has been read or immediately after the headers have been read and the response handler has been called if there is no body.
Request and response composition
The client interface is very simple and follows this pattern:
-
request
a connection -
send
orwrite
/end
the request to the server -
handle the beginning of the
HttpClientResponse
-
process the response events
You can use Vert.x future composition methods to make your code simpler, however the API is event driven, and you need to understand it otherwise you might experience possible data races (i.e. loosing events leading to corrupted data).
Vert.x Web Client is a higher level API alternative (in fact it is built on top of this client) you might consider if this client is too low level for your use cases |
The client API intentionally does not return a Future<HttpClientResponse>
because setting a completion handler on the future can be racy when this is set outside the event-loop.
Future<HttpClientResponse> get = client.get("some-uri");
// Assuming we have a client that returns a future response
// assuging this is *not* on the event-loop
// introduce a potential data race for the sake of this example
Thread.sleep(100);
get.onSuccess(response -> {
// Response events might have happen already
response
.body()
.onComplete(ar -> {
});
});
Confining the HttpClientRequest
usage within a verticle is the easiest solution as the Verticle will ensure that events are processed sequentially avoiding races.
vertx.deployVerticle(() -> new AbstractVerticle() {
@Override
public void start() {
HttpClient client = vertx.createHttpClient();
Future<HttpClientRequest> future = client.request(HttpMethod.GET, "some-uri");
}
}, new DeploymentOptions());
When you are interacting with the client possibly outside a verticle then you can safely perform composition as long as you do not delay the response events, e.g. processing directly the response on the event-loop.
Future<JsonObject> future = client
.request(HttpMethod.GET, "some-uri")
.compose(request -> request
.send()
.compose(response -> {
// Process the response on the event-loop which guarantees no races
if (response.statusCode() == 200 &&
response.getHeader(HttpHeaders.CONTENT_TYPE).equals("application/json")) {
return response
.body()
.map(buffer -> buffer.toJsonObject());
} else {
return Future.failedFuture("Incorrect HTTP response");
}
}));
// Listen to the composed final json result
future.onSuccess(json -> {
System.out.println("Received json result " + json);
}).onFailure(err -> {
System.out.println("Something went wrong " + err.getMessage());
});
You can also guard the response body with HTTP responses expectations.
Future<JsonObject> future = client
.request(HttpMethod.GET, "some-uri")
.compose(request -> request
.send()
.expecting(HttpResponseExpectation.SC_OK.and(HttpResponseExpectation.JSON))
.compose(response -> response
.body()
.map(buffer -> buffer.toJsonObject())));
// Listen to the composed final json result
future.onSuccess(json -> {
System.out.println("Received json result " + json);
}).onFailure(err -> {
System.out.println("Something went wrong " + err.getMessage());
});
If you need to delay the response processing then you need to pause
the response or use a pipe
, this might be necessary when another asynchronous operation is involved.
Future<Void> future = client
.request(HttpMethod.GET, "some-uri")
.compose(request -> request
.send()
.compose(response -> {
// Process the response on the event-loop which guarantees no races
if (response.statusCode() == 200) {
// Create a pipe, this pauses the response
Pipe<Buffer> pipe = response.pipe();
// Write the file on the disk
return fileSystem
.open("/some/large/file", new OpenOptions().setWrite(true))
.onFailure(err -> pipe.close())
.compose(file -> pipe.to(file));
} else {
return Future.failedFuture("Incorrect HTTP response");
}
}));
Response expectations
As seen above, you must perform sanity checks manually after the response is received.
You can trade flexibility for clarity and conciseness using response expectations.
Response expectations
can guard the control flow when the response does not match a criteria.
The HTTP Client comes with a set of out of the box predicates ready to use:
Future<Buffer> fut = client
.request(options)
.compose(request -> request
.send()
.expecting(HttpResponseExpectation.SC_SUCCESS)
.compose(response -> response.body()));
You can also create custom predicates when existing predicates don’t fit your needs:
HttpResponseExpectation methodsPredicate =
resp -> {
String methods = resp.getHeader("Access-Control-Allow-Methods");
return methods != null && methods.contains("POST");
};
// Send pre-flight CORS request
client
.request(new RequestOptions()
.setMethod(HttpMethod.OPTIONS)
.setPort(8080)
.setHost("myserver.mycompany.com")
.setURI("/some-uri")
.putHeader("Origin", "Server-b.com")
.putHeader("Access-Control-Request-Method", "POST"))
.compose(request -> request
.send()
.expecting(methodsPredicate))
.onSuccess(res -> {
// Process the POST request now
})
.onFailure(err ->
System.out.println("Something went wrong " + err.getMessage()));
Predefined expectations
As a convenience, the HTTP Client ships a few predicates for common uses cases .
For status codes, e.g. HttpResponseExpectation.SC_SUCCESS
to verify that the response has a 2xx
code, you can also create a custom one:
client
.request(options)
.compose(request -> request
.send()
.expecting(HttpResponseExpectation.status(200, 202)))
.onSuccess(res -> {
// ....
});
For content types, e.g. HttpResponseExpectation.JSON
to verify that the response body contains JSON data, you can also create a custom one:
client
.request(options)
.compose(request -> request
.send()
.expecting(HttpResponseExpectation.contentType("some/content-type")))
.onSuccess(res -> {
// ....
});
Please refer to the HttpResponseExpectation
documentation for a full list of predefined expectations.
Creating custom failures
By default, expectations (including the predefined ones) conveys a simple error message. You can customize the exception class by changing the error converter:
Expectation<HttpResponseHead> expectation = HttpResponseExpectation.SC_SUCCESS
.wrappingFailure((resp, err) -> new MyCustomException(resp.statusCode(), err.getMessage()));
creating exception in Java can have a performance cost when it captures a stack trace, so you might want to create exceptions that do not capture the stack trace. By default exceptions are reported using an exception that does not capture the stack trace. |
Reading cookies from the response
You can retrieve the list of cookies from a response using cookies
.
Alternatively you can just parse the Set-Cookie
headers yourself in the response.
30x redirection handling
The client can be configured to follow HTTP redirections provided by the Location
response header when the client receives:
-
a
301
,302
,307
or308
status code along with a HTTP GET or HEAD method -
a
303
status code, in addition the directed request perform an HTTP GET method
Here’s an example:
client
.request(HttpMethod.GET, "some-uri")
.onComplete(ar1 -> {
if (ar1.succeeded()) {
HttpClientRequest request = ar1.result();
request.setFollowRedirects(true);
request
.send()
.onComplete(ar2 -> {
if (ar2.succeeded()) {
HttpClientResponse response = ar2.result();
System.out.println("Received response with status code " + response.statusCode());
}
});
}
});
The maximum redirects is 16
by default and can be changed with setMaxRedirects
.
HttpClient client = vertx.createHttpClient(
new HttpClientOptions()
.setMaxRedirects(32));
client
.request(HttpMethod.GET, "some-uri")
.onComplete(ar1 -> {
if (ar1.succeeded()) {
HttpClientRequest request = ar1.result();
request.setFollowRedirects(true);
request
.send()
.onComplete(ar2 -> {
if (ar2.succeeded()) {
HttpClientResponse response = ar2.result();
System.out.println("Received response with status code " + response.statusCode());
}
});
}
});
One size does not fit all and the default redirection policy may not be adapted to your needs.
The default redirection policy can changed with a custom implementation:
HttpClient client = vertx.httpClientBuilder()
.withRedirectHandler(response -> {
// Only follow 301 code
if (response.statusCode() == 301 && response.getHeader("Location") != null) {
// Compute the redirect URI
String absoluteURI = resolveURI(response.request().absoluteURI(), response.getHeader("Location"));
// Create a new ready to use request that the client will use
return Future.succeededFuture(new RequestOptions().setAbsoluteURI(absoluteURI));
}
// We don't redirect
return null;
})
.build();
The policy handles the original HttpClientResponse
received and returns either null
or a Future<HttpClientRequest>
.
-
when
null
is returned, the original response is processed -
when a future is returned, the request will be sent on its successful completion
-
when a future is returned, the exception handler set on the request is called on its failure
The returned request must be unsent so the original request handlers can be sent and the client can send it after.
Most of the original request settings will be propagated to the new request:
-
request headers, unless if you have set some headers
-
request body unless the returned request uses a
GET
method -
response handler
-
request exception handler
-
request timeout
100-Continue handling
According to the HTTP 1.1 specification a client can set a header Expect: 100-Continue
and send the request header before sending the rest of the request body.
The server can then respond with an interim response status Status: 100 (Continue)
to signify to the client that it is ok to send the rest of the body.
The idea here is it allows the server to authorise and accept/reject the request before large amounts of data are sent. Sending large amounts of data if the request might not be accepted is a waste of bandwidth and ties up the server in reading data that it will just discard.
Vert.x allows you to set a continueHandler
on the client request object
This will be called if the server sends back a Status: 100 (Continue)
response to signify that it is ok to send the rest of the request.
This is used in conjunction with `sendHead`to send the head of the request.
Here’s an example:
client.request(HttpMethod.PUT, "some-uri")
.onSuccess(request -> {
request.response().onSuccess(response -> {
System.out.println("Received response with status code " + response.statusCode());
});
request.putHeader("Expect", "100-Continue");
request.continueHandler(v -> {
// OK to send rest of body
request.write("Some data");
request.write("Some more data");
request.end();
});
request.sendHead();
});
On the server side a Vert.x http server can be configured to automatically send back 100 Continue interim responses when it receives an Expect: 100-Continue
header.
This is done by setting the option setHandle100ContinueAutomatically
.
If you’d prefer to decide whether to send back continue responses manually, then this property should be set to false
(the default), then you can inspect the headers and call writeContinue
to have the client continue sending the body:
httpServer.requestHandler(request -> {
if (request.getHeader("Expect").equalsIgnoreCase("100-Continue")) {
// Send a 100 continue response
request.response().writeContinue();
// The client should send the body when it receives the 100 response
request.bodyHandler(body -> {
// Do something with body
});
request.endHandler(v -> {
request.response().end();
});
}
});
You can also reject the request by sending back a failure status code directly: in this case the body should either be ignored or the connection should be closed (100-Continue is a performance hint and cannot be a logical protocol constraint):
httpServer.requestHandler(request -> {
if (request.getHeader("Expect").equalsIgnoreCase("100-Continue")) {
//
boolean rejectAndClose = true;
if (rejectAndClose) {
// Reject with a failure code and close the connection
// this is probably best with persistent connection
request.response()
.setStatusCode(405)
.putHeader("Connection", "close")
.end();
} else {
// Reject with a failure code and ignore the body
// this may be appropriate if the body is small
request.response()
.setStatusCode(405)
.end();
}
}
});
Creating HTTP tunnels
HTTP tunnels can be created with connect
:
client.request(HttpMethod.CONNECT, "some-uri")
.onSuccess(request -> {
// Connect to the server
request
.connect()
.onComplete(ar -> {
if (ar.succeeded()) {
HttpClientResponse response = ar.result();
if (response.statusCode() != 200) {
// Connect failed for some reason
} else {
// Tunnel created, raw buffers are transmitted on the wire
NetSocket socket = response.netSocket();
}
}
});
});
The handler will be called after the HTTP response header is received, the socket will be ready for tunneling and will send and receive buffers.
connect
works like send
, but it reconfigures the transport to exchange raw buffers.
Client push
Server push is a new feature of HTTP/2 that enables sending multiple responses in parallel for a single client request.
A push handler can be set on a request to receive the request/response pushed by the server:
client.request(HttpMethod.GET, "/index.html")
.onSuccess(request -> {
request
.response().onComplete(response -> {
// Process index.html response
});
// Set a push handler to be aware of any resource pushed by the server
request.pushHandler(pushedRequest -> {
// A resource is pushed for this request
System.out.println("Server pushed " + pushedRequest.path());
// Set an handler for the response
pushedRequest.response().onComplete(pushedResponse -> {
System.out.println("The response for the pushed request");
});
});
// End the request
request.end();
});
If the client does not want to receive a pushed request, it can reset the stream:
request.pushHandler(pushedRequest -> {
if (pushedRequest.path().equals("/main.js")) {
pushedRequest.reset();
} else {
// Handle it
}
});
When no handler is set, any stream pushed will be automatically cancelled by the client with a stream reset (8
error code).
Receiving custom HTTP/2 frames
HTTP/2 is a framed protocol with various frames for the HTTP request/response model. The protocol allows other kind of frames to be sent and received.
To receive custom frames, you can use the customFrameHandler on the request, this will get called every time a custom frame arrives. Here’s an example:
response.customFrameHandler(frame -> {
System.out.println("Received a frame type=" + frame.type() +
" payload" + frame.payload().toString());
});
Enabling compression on the client
The http client comes with support for HTTP Compression out of the box.
This means the client can let the remote http server know that it supports compression, and will be able to handle compressed response bodies.
An http server is free to either compress with one of the supported compression algorithms or to send the body back without compressing it at all. So this is only a hint for the Http server which it may ignore at will.
To tell the http server which compression is supported by the client it will include an Accept-Encoding
header with the supported compression algorithm as value. Multiple compression algorithms are supported. In case of Vert.x this will result in the following header added:
Accept-Encoding: gzip, deflate
The server will choose then from one of these. You can detect if a server compressed the body by checking for the Content-Encoding
header in the response sent back from it.
If the body of the response was compressed via gzip it will include for example the following header:
Content-Encoding: gzip
To enable compression set setDecompressionSupported
on the options used when creating the client.
By default compression is disabled.
HTTP/1.x pooling and keep alive
Http keep alive allows http connections to be used for more than one request. This can be a more efficient use of connections when you’re making multiple requests to the same server.
For HTTP/1.x versions, the http client supports pooling of connections, allowing you to reuse connections between requests.
For pooling to work, keep alive must be true using setKeepAlive
on the options used when configuring the client. The default value is true.
When keep alive is enabled. Vert.x will add a Connection: Keep-Alive
header to each HTTP/1.0 request sent. When keep alive is disabled. Vert.x will add a Connection: Close
header to each HTTP/1.1 request sent to signal that the connection will be closed after completion of the response.
The maximum number of connections to pool for each server is configured using setMaxPoolSize
When making a request with pooling enabled, Vert.x will create a new connection if there are less than the maximum number of connections already created for that server, otherwise it will add the request to a queue.
Keep alive connections will be closed by the client automatically after a timeout. The timeout can be specified by the server using the keep-alive
header:
keep-alive: timeout=30
You can set the default timeout using setKeepAliveTimeout
- any connections not used within this timeout will be closed. Please note the timeout value is in seconds not milliseconds.
HTTP/1.1 pipe-lining
The client also supports pipe-lining of requests on a connection.
Pipe-lining means another request is sent on the same connection before the response from the preceding one has returned. Pipe-lining is not appropriate for all requests.
To enable pipe-lining, it must be enabled using setPipelining
. By default, pipe-lining is disabled.
When pipe-lining is enabled requests will be written to connections without waiting for previous responses to return.
The number of pipe-lined requests over a single connection is limited by setPipeliningLimit
. This option defines the maximum number of http requests sent to the server awaiting for a response. This limit ensures the fairness of the distribution of the client requests over the connections to the same server.
HTTP/2 multiplexing
HTTP/2 advocates to use a single connection to a server, by default the http client uses a single connection for each server, all the streams to the same server are multiplexed over the same connection.
When the clients needs to use more than a single connection and use pooling, the setHttp2MaxPoolSize
shall be used.
When it is desirable to limit the number of multiplexed streams per connection and use a connection pool instead of a single connection, setHttp2MultiplexingLimit
can be used.
HttpClientOptions clientOptions = new HttpClientOptions().
setHttp2MultiplexingLimit(10).
setHttp2MaxPoolSize(3);
// Uses up to 3 connections and up to 10 streams per connection
HttpClient client = vertx.createHttpClient(clientOptions);
The multiplexing limit for a connection is a setting set on the client that limits the number of streams of a single connection. The effective value can be even lower if the server sets a lower limit with the SETTINGS_MAX_CONCURRENT_STREAMS
setting.
HTTP/2 connections will not be closed by the client automatically. To close them you can call close
or close the client instance.
Alternatively you can set idle timeout using setIdleTimeout
- any connections not used within this timeout will be closed. Please note the idle timeout value is in seconds not milliseconds.
HTTP connections
The HttpConnection
offers the API for dealing with HTTP connection events, lifecycle and settings.
HTTP/2 implements fully the HttpConnection
API.
HTTP/1.x implements partially the HttpConnection
API: only the close operation, the close handler and exception handler are implemented. This protocol does not provide semantics for the other operations.
Server connections
The connection
method returns the request connection on the server:
HttpConnection connection = request.connection();
A connection handler can be set on the server to be notified of any incoming connection:
HttpServer server = vertx.createHttpServer(http2Options);
server.connectionHandler(connection -> {
System.out.println("A client connected");
});
Client connections
The connection
method returns the request connection on the client:
HttpConnection connection = request.connection();
A connection handler can be set on a client builder to be notified when a connection has been established happens:
vertx
.httpClientBuilder()
.with(options)
.withConnectHandler(connection -> {
System.out.println("Connected to the server");
})
.build();
Connection settings
The configuration of an HTTP/2 is configured by the Http2Settings
data object.
Each endpoint must respect the settings sent by the other side of the connection.
When a connection is established, the client and the server exchange initial settings. Initial settings are configured by setInitialSettings
on the client and setInitialSettings
on the server.
The settings can be changed at any time after the connection is established:
connection.updateSettings(new Http2Settings().setMaxConcurrentStreams(100));
As the remote side should acknowledge on reception of the settings update, it’s possible to give a callback to be notified of the acknowledgment:
connection
.updateSettings(new Http2Settings().setMaxConcurrentStreams(100))
.onSuccess(v -> System.out.println("The settings update has been acknowledged "));
Conversely the remoteSettingsHandler
is notified when the new remote settings are received:
connection.remoteSettingsHandler(settings -> {
System.out.println("Received new settings");
});
this only applies to the HTTP/2 protocol |
Connection ping
HTTP/2 connection ping is useful for determining the connection round-trip time or check the connection validity: ping
sends a {@literal PING} frame to the remote endpoint:
Buffer data = Buffer.buffer();
for (byte i = 0;i < 8;i++) {
data.appendByte(i);
}
connection
.ping(data)
.onSuccess(pong -> System.out.println("Remote side replied"));
Vert.x will send automatically an acknowledgement when a {@literal PING} frame is received, an handler can be set to be notified for each ping received:
connection.pingHandler(ping -> {
System.out.println("Got pinged by remote side");
});
The handler is just notified, the acknowledgement is sent whatsoever. Such feature is aimed for implementing protocols on top of HTTP/2.
this only applies to the HTTP/2 protocol |
Connection shutdown and go away
Calling shutdown
will send a {@literal GOAWAY} frame to the remote side of the connection, asking it to stop creating streams: a client will stop doing new requests and a server will stop pushing responses. After the {@literal GOAWAY} frame is sent, the connection waits some time (30 seconds by default) until all current streams closed and close the connection:
connection.shutdown();
The shutdownHandler
notifies when all streams have been closed, the connection is not yet closed.
It’s possible to just send a {@literal GOAWAY} frame, the main difference with a shutdown is that it will just tell the remote side of the connection to stop creating new streams without scheduling a connection close:
connection.goAway(0);
Conversely, it is also possible to be notified when {@literal GOAWAY} are received:
connection.goAwayHandler(goAway -> {
System.out.println("Received a go away frame");
});
The shutdownHandler
will be called when all current streams have been closed and the connection can be closed:
connection.goAway(0);
connection.shutdownHandler(v -> {
// All streams are closed, close the connection
connection.close();
});
This applies also when a {@literal GOAWAY} is received.
this only applies to the HTTP/2 protocol |
Connection close
Connection close
closes the connection:
-
it closes the socket for HTTP/1.x
-
a shutdown with no delay for HTTP/2, the {@literal GOAWAY} frame will still be sent before the connection is closed. *
The closeHandler
notifies when a connection is closed.
Client sharing
You can share an HTTP client between multiple verticles or instances of the same verticle. Such client should be created outside of a verticle otherwise it will be closed when the verticle that created it is undeployed
HttpClient client = vertx.createHttpClient(new HttpClientOptions().setShared(true));
vertx.deployVerticle(() -> new AbstractVerticle() {
@Override
public void start() throws Exception {
// Use the client
}
}, new DeploymentOptions().setInstances(4));
You can also create a shared HTTP client in each verticle:
vertx.deployVerticle(() -> new AbstractVerticle() {
HttpClient client;
@Override
public void start() {
// Get or create a shared client
// this actually creates a lease to the client
// when the verticle is undeployed, the lease will be released automaticaly
client = vertx.createHttpClient(new HttpClientOptions().setShared(true).setName("my-client"));
}
}, new DeploymentOptions().setInstances(4));
The first time a shared client is created it will create and return a client. Subsequent calls will reuse this client and create a lease to this client. The client is closed after all leases have been disposed.
By default, a client reuses the current event-loop when it needs to create a TCP connection. The HTTP client will therefore randomly use event-loops of verticles using it in a safe fashion.
You can assign a number of event loop a client will use independently of the client using it
vertx.deployVerticle(() -> new AbstractVerticle() {
HttpClient client;
@Override
public void start() {
// The client creates and use two event-loops for 4 instances
client = vertx.createHttpClient(new HttpClientOptions().setShared(true).setName("my-client"), new PoolOptions().setEventLoopSize(2));
}
}, new DeploymentOptions().setInstances(4));
Server sharing
When several HTTP servers listen on the same port, vert.x orchestrates the request handling using a round-robin strategy.
Let’s take a verticle creating a HTTP server such as:
vertx.createHttpServer().requestHandler(request -> {
request.response().end("Hello from server " + this);
}).listen(8080);
This service is listening on the port 8080. So, when this verticle is instantiated multiple times as with: vertx run io.vertx.examples.http.sharing.HttpServerVerticle -instances 2
, what’s happening ? If both verticles would bind to the same port, you would receive a socket exception. Fortunately, vert.x is handling this case for you. When you deploy another server on the same host and port as an existing server it doesn’t actually try and create a new server listening on the same host/port. It binds only once to the socket. When receiving a request it calls the server handlers following a round robin strategy.
Let’s now imagine a client such as:
vertx.setPeriodic(100, (l) -> {
vertx
.createHttpClient()
.request(HttpMethod.GET, 8080, "localhost", "/")
.onComplete(ar1 -> {
if (ar1.succeeded()) {
HttpClientRequest request = ar1.result();
request
.send()
.onComplete(ar2 -> {
if (ar2.succeeded()) {
HttpClientResponse resp = ar2.result();
resp.bodyHandler(body -> {
System.out.println(body.toString("ISO-8859-1"));
});
}
});
}
});
});
Vert.x delegates the requests to one of the server sequentially:
Hello from i.v.e.h.s.HttpServerVerticle@1
Hello from i.v.e.h.s.HttpServerVerticle@2
Hello from i.v.e.h.s.HttpServerVerticle@1
Hello from i.v.e.h.s.HttpServerVerticle@2
...
Consequently the servers can scale over available cores while each Vert.x verticle instance remains strictly single threaded, and you don’t have to do any special tricks like writing load-balancers in order to scale your server on your multi-core machine.
You can bind on a shared random ports using a negative port value, the first bind will pick a port randomly, subsequent binds on the same port value will share this random port.
vertx.createHttpServer().requestHandler(request -> {
request.response().end("Hello from server " + this);
}).listen(-1);
Using HTTPS with Vert.x
Vert.x http servers and clients can be configured to use HTTPS in exactly the same way as net servers.
Please see configuring net servers to use SSL for more information.
SSL can also be enabled/disabled per request with RequestOptions
or when specifying a scheme with setAbsoluteURI
method.
client
.request(new RequestOptions()
.setHost("localhost")
.setPort(8080)
.setURI("/")
.setSsl(true))
.onComplete(ar1 -> {
if (ar1.succeeded()) {
HttpClientRequest request = ar1.result();
request
.send()
.onComplete(ar2 -> {
if (ar2.succeeded()) {
HttpClientResponse response = ar2.result();
System.out.println("Received response with status code " + response.statusCode());
}
});
}
});
The setSsl
setting acts as the default client setting.
The setSsl
overrides the default client setting
-
setting the value to
false
will disable SSL/TLS even if the client is configured to use SSL/TLS -
setting the value to
true
will enable SSL/TLS even if the client is configured to not use SSL/TLS, the actual client SSL/TLS (such as trust, key/certificate, ciphers, ALPN, …) will be reused
Likewise setAbsoluteURI
scheme also overrides the default client setting.
Server Name Indication (SNI)
Vert.x http servers can be configured to use SNI in exactly the same way as {@linkplain io.vertx.core.net net servers}.
Vert.x http client will present the actual hostname as server name during the TLS handshake.
WebSockets
WebSockets are a web technology that allows a full duplex socket-like connection between HTTP servers and HTTP clients (typically browsers).
Vert.x supports WebSockets on both the client and server-side.
WebSockets on the server
There are two ways of handling WebSockets on the server side.
WebSocket handler
The first way involves providing a webSocketHandler
on the server instance.
When a WebSocket connection is made to the server, the handler will be called, passing in an instance of ServerWebSocket
.
server.webSocketHandler(webSocket -> {
System.out.println("Connected!");
});
Server WebSocket handshake
By default, the server accepts any inbound WebSocket.
You can set a WebSocket handshake handler to control the outcome of a WebSocket handshake, i.e. accept or reject an incoming WebSocket.
server
.webSocketHandshakeHandler(handshake -> {
if (handshake.path().equals("/myapi")) {
handshake.reject();
} else {
handshake.accept();
}
})
.webSocketHandler(webSocket -> {
// Do something
});
You can perform an asynchronous handshake:
server
.webSocketHandshakeHandler(handshake -> {
authenticate(handshake.headers(), ar -> {
if (ar.succeeded()) {
if (ar.result()) {
// Terminate the handshake with the status code 101 (Switching Protocol)
handshake.accept();
} else {
// Reject the handshake with 401 (Unauthorized)
handshake.reject(401);
}
} else {
// Will send a 500 error
handshake.reject(500);
}
});
})
.webSocketHandler(webSocket -> {
// Do something
});
the WebSocket will be automatically accepted after the handler is called unless the WebSocket’s handshake has been set |
Upgrading to WebSocket
The second way of handling WebSockets is to handle the HTTP Upgrade request that was sent from the client, and call toWebSocket
on the server request.
server.requestHandler(request -> {
if (request.path().equals("/myapi")) {
Future<ServerWebSocket> fut = request.toWebSocket();
fut.onSuccess(ws -> {
// Do something
});
} else {
// Reject
request.response().setStatusCode(400).end();
}
});
The server WebSocket
The ServerWebSocket
instance enables you to retrieve the headers
, path
, query
and URI
of the HTTP request of the WebSocket handshake.
WebSockets on the client
The Vert.x WebSocketClient
supports WebSockets.
You can connect a WebSocket to a server using one of the connect
operations.
The returned future will be completed with an instance of WebSocket
when the connection has been made:
WebSocketClient client = vertx.createWebSocketClient();
client
.connect(80, "example.com", "/some-uri")
.onComplete(res -> {
if (res.succeeded()) {
WebSocket ws = res.result();
ws.textMessageHandler(msg -> {
// Handle msg
});
System.out.println("Connected!");
}
});
When connecting from a non Vert.x thread, you can create a ClientWebSocket
, configure its handlers and then connect to the server:
WebSocketClient client = vertx.createWebSocketClient();
client
.webSocket()
.textMessageHandler(msg -> {
// Handle msg
})
.connect(80, "example.com", "/some-uri")
.onComplete(res -> {
if (res.succeeded()) {
WebSocket ws = res.result();
}
});
By default, the client sets the origin
header to the server host, e.g http://www.example.com. Some servers will refuse such request, you can configure the client to not set this header.
WebSocketConnectOptions options = new WebSocketConnectOptions()
.setHost(host)
.setPort(port)
.setURI(requestUri)
.setAllowOriginHeader(false);
client
.connect(options)
.onComplete(res -> {
if (res.succeeded()) {
WebSocket ws = res.result();
System.out.println("Connected!");
}
});
You can also set a different header:
WebSocketConnectOptions options = new WebSocketConnectOptions()
.setHost(host)
.setPort(port)
.setURI(requestUri)
.addHeader(HttpHeaders.ORIGIN, origin);
client
.connect(options)
.onComplete(res -> {
if (res.succeeded()) {
WebSocket ws = res.result();
System.out.println("Connected!");
}
});
older versions of the WebSocket protocol use sec-websocket-origin instead |
Writing messages to WebSockets
If you wish to write a single WebSocket message to the WebSocket you can do this with writeBinaryMessage
or writeTextMessage
:
Buffer buffer = Buffer.buffer().appendInt(123).appendFloat(1.23f);
webSocket.writeBinaryMessage(buffer);
// Write a simple text message
String message = "hello";
webSocket.writeTextMessage(message);
If the WebSocket message is larger than the maximum WebSocket frame size as configured with setMaxWebSocketFrameSize
then Vert.x will split it into multiple WebSocket frames before sending it on the wire.
Writing frames to WebSockets
A WebSocket message can be composed of multiple frames. In this case the first frame is either a binary or text frame followed by zero or more continuation frames.
The last frame in the message is marked as final.
To send a message consisting of multiple frames you create frames using WebSocketFrame.binaryFrame
, WebSocketFrame.textFrame
or WebSocketFrame.continuationFrame
and write them to the WebSocket using writeFrame
.
Here’s an example for binary frames:
WebSocketFrame frame1 = WebSocketFrame.binaryFrame(buffer1, false);
webSocket.writeFrame(frame1);
WebSocketFrame frame2 = WebSocketFrame.continuationFrame(buffer2, false);
webSocket.writeFrame(frame2);
// Write the final frame
WebSocketFrame frame3 = WebSocketFrame.continuationFrame(buffer2, true);
webSocket.writeFrame(frame3);
In many cases you just want to send a WebSocket message that consists of a single final frame, so we provide a couple of shortcut methods to do that with writeFinalBinaryFrame
and writeFinalTextFrame
.
Here’s an example:
webSocket.writeFinalTextFrame("Geronimo!");
// Send a WebSocket message consisting of a single final binary frame:
Buffer buff = Buffer.buffer().appendInt(12).appendString("foo");
webSocket.writeFinalBinaryFrame(buff);
Reading frames from WebSockets
To read frames from a WebSocket you use the frameHandler
.
The frame handler will be called with instances of WebSocketFrame
when a frame arrives, for example:
webSocket.frameHandler(frame -> {
System.out.println("Received a frame of size!");
});
Closing WebSockets
Use close
to close the WebSocket connection when you have finished with it.
Piping WebSockets
The WebSocket
instance is also a ReadStream
and a WriteStream
so it can be used with pipes.
When using a WebSocket as a write stream or a read stream it can only be used with WebSockets connections that are used with binary frames that are no split over multiple frames.
Event bus handlers
Every WebSocket can register two handlers on the event bus, and when any data are received in these handlers, it writes the data to itself. Those are local subscriptions, not reachable from other clustered nodes.
This enables you to write data to a WebSocket which is potentially in a completely different verticle sending data to the address of that handler.
This feature is disabled by default, however you can enable it using setRegisterWebSocketWriteHandlers
or setRegisterWriteHandlers
.
The addresses of the handlers are given by binaryHandlerID
and textHandlerID
.
Using a proxy for HTTP/HTTPS connections
The http client supports accessing http/https URLs via a HTTP proxy (e.g. Squid) or SOCKS4a or SOCKS5 proxy. The CONNECT protocol uses HTTP/1.x but can connect to HTTP/1.x and HTTP/2 servers.
Connecting to h2c (unencrypted HTTP/2 servers) is likely not supported by http proxies since they will support HTTP/1.1 only.
The proxy can be configured in the HttpClientOptions
by setting a ProxyOptions
object containing proxy type, hostname, port and optionally username and password.
Here’s an example of using an HTTP proxy:
HttpClientOptions options = new HttpClientOptions()
.setProxyOptions(new ProxyOptions().setType(ProxyType.HTTP)
.setHost("localhost").setPort(3128)
.setUsername("username").setPassword("secret"));
HttpClient client = vertx.createHttpClient(options);
When the client connects to an http URL, it connects to the proxy server and provides the full URL in the HTTP request ("GET http://www.somehost.com/path/file.html HTTP/1.1").
When the client connects to an https URL, it asks the proxy to create a tunnel to the remote host with the CONNECT method.
For a SOCKS5 proxy:
HttpClientOptions options = new HttpClientOptions()
.setProxyOptions(new ProxyOptions().setType(ProxyType.SOCKS5)
.setHost("localhost").setPort(1080)
.setUsername("username").setPassword("secret"));
HttpClient client = vertx.createHttpClient(options);
The DNS resolution is always done on the proxy server, to achieve the functionality of a SOCKS4 client, it is necessary to resolve the DNS address locally.
Proxy options can also be set per request:
client.request(new RequestOptions()
.setHost("example.com")
.setProxyOptions(proxyOptions))
.compose(request -> request
.send()
.compose(HttpClientResponse::body))
.onSuccess(body -> {
System.out.println("Received response");
});
client connection pooling is aware of proxies (including authentication), consequently two requests to the same host through different proxies do not share the same pooled connection |
You can use setNonProxyHosts
to configure a list of host bypassing the proxy. The lists accept *
wildcard for matching domains:
HttpClientOptions options = new HttpClientOptions()
.setProxyOptions(new ProxyOptions().setType(ProxyType.SOCKS5)
.setHost("localhost").setPort(1080)
.setUsername("username").setPassword("secret"))
.addNonProxyHost("*.foo.com")
.addNonProxyHost("localhost");
HttpClient client = vertx.createHttpClient(options);
Handling of other protocols
The HTTP proxy implementation supports getting ftp:// urls if the proxy supports that.
When the HTTP request URI contains the full URL then the client will not compute a full HTTP url and instead use the full URL specified in the request URI:
HttpClientOptions options = new HttpClientOptions()
.setProxyOptions(new ProxyOptions().setType(ProxyType.HTTP));
HttpClient client = vertx.createHttpClient(options);
client
.request(HttpMethod.GET, "ftp://ftp.gnu.org/gnu/")
.onComplete(ar -> {
if (ar.succeeded()) {
HttpClientRequest request = ar.result();
request
.send()
.onComplete(ar2 -> {
if (ar2.succeeded()) {
HttpClientResponse response = ar2.result();
System.out.println("Received response with status code " + response.statusCode());
}
});
}
});
Using HA PROXY protocol
HA PROXY protocol provides a convenient way to safely transport connection information such as a client’s address across multiple layers of NAT or TCP proxies.
HA PROXY protocol can be enabled by setting the option setUseProxyProtocol
and adding the following dependency in your classpath:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-codec-haproxy</artifactId>
<!--<version>Should align with netty version that Vert.x uses</version>-->
</dependency>
HttpServerOptions options = new HttpServerOptions()
.setUseProxyProtocol(true);
HttpServer server = vertx.createHttpServer(options);
server.requestHandler(request -> {
// Print the actual client address provided by the HA proxy protocol instead of the proxy address
System.out.println(request.remoteAddress());
// Print the address of the proxy
System.out.println(request.localAddress());
});
Automatic clean-up in verticles
If you’re creating http servers and clients from inside verticles, those servers and clients will be automatically closed when the verticle is undeployed.
Using the SharedData API
As its name suggests, the SharedData
API allows you to safely share data between:
-
different parts of your application, or
-
different applications in the same Vert.x instance, or
-
different applications across a cluster of Vert.x instances.
In practice, it provides:
-
synchronous maps (local-only)
-
asynchronous maps
-
asynchronous locks
-
asynchronous counters
The behavior of the distributed data structure depends on the cluster manager you use. Backup (replication) and behavior when a network partition is faced are defined by the cluster manager and its configuration. Please refer to the cluster manager documentation as well as to the underlying framework manual. |
Local maps
Local maps
allow you to share data safely between different event loops (e.g. different verticles) in the same Vert.x instance.
They only allow certain data types to be used as keys and values:
-
immutable types (e.g. strings, booleans, … etc), or
-
types implementing the
Shareable
interface (buffers, JSON arrays, JSON objects, or your own shareable objects).
In the latter case the key/value will be copied before putting it into the map.
This way we can ensure there is no shared access to mutable state between different threads in your Vert.x application. And you won’t have to worry about protecting that state by synchronising access to it.
As a convenience, objects implementing |
Here’s an example of using a shared local map:
SharedData sharedData = vertx.sharedData();
LocalMap<String, String> map1 = sharedData.getLocalMap("mymap1");
map1.put("foo", "bar"); // Strings are immutable so no need to copy
LocalMap<String, Buffer> map2 = sharedData.getLocalMap("mymap2");
map2.put("eek", Buffer.buffer().appendInt(123)); // This buffer will be copied before adding to map
// Then... in another part of your application:
map1 = sharedData.getLocalMap("mymap1");
String val = map1.get("foo");
map2 = sharedData.getLocalMap("mymap2");
Buffer buff = map2.get("eek");
Asynchronous shared maps
Asynchronous shared maps
allow data to be put in the map and retrieved locally or from any other node.
This makes them really useful for things like storing session state in a farm of servers hosting a Vert.x Web application.
They only allow certain data types to be used as keys and values:
-
immutable types (e.g. strings, booleans, … etc), or
-
types implementing the
ClusterSerializable
interface (buffers, JSON arrays, JSON objects, or your own cluster serializable objects), or -
types implementing the
java.io.Serializable
interface.
Getting the map is asynchronous and the result is returned to you in the handler that you specify. Here’s an example:
SharedData sharedData = vertx.sharedData();
sharedData.
<String, String>getAsyncMap("mymap")
.onComplete(res -> {
if (res.succeeded()) {
AsyncMap<String, String> map = res.result();
} else {
// Something went wrong!
}
});
When Vert.x is clustered, data that you put into the map is accessible locally as well as on any of the other cluster members.
In clustered mode, asynchronous shared maps rely on distributed data structures provided by the cluster manager. Beware that the latency relative to asynchronous shared map operations can be much higher in clustered than in local mode. |
If your application doesn’t need data to be shared with every other node, you can retrieve a local-only map:
SharedData sharedData = vertx.sharedData();
sharedData.
<String, String>getLocalAsyncMap("mymap")
.onComplete(res -> {
if (res.succeeded()) {
// Local-only async map
AsyncMap<String, String> map = res.result();
} else {
// Something went wrong!
}
});
Putting data in a map
You put data in a map with put
.
The actual put is asynchronous and the handler is notified once it is complete:
map
.put("foo", "bar")
.onComplete(resPut -> {
if (resPut.succeeded()) {
// Successfully put the value
} else {
// Something went wrong!
}
});
Getting data from a map
You get data from a map with get
.
The actual get is asynchronous and the handler is notified with the result some time later:
map
.get("foo")
.onComplete(resGet -> {
if (resGet.succeeded()) {
// Successfully got the value
Object val = resGet.result();
} else {
// Something went wrong!
}
});
Other map operations
You can also remove entries from an asynchronous map, clear them and get the size.
See the API docs
for a detailed list of map operations.
Asynchronous locks
Asynchronous locks
allow you to obtain exclusive locks locally or across the cluster. This is useful when you want to do something or access a resource on only one node of a cluster at any one time.
Asynchronous locks have an asynchronous API unlike most lock APIs which block the calling thread until the lock is obtained.
To obtain a lock use getLock
. This won’t block, but when the lock is available, the handler will be called with an instance of Lock
, signalling that you now own the lock.
While you own the lock, no other caller, locally or on the cluster, will be able to obtain the lock.
When you’ve finished with the lock, you call release
to release it, so another caller can obtain it:
SharedData sharedData = vertx.sharedData();
sharedData
.getLock("mylock")
.onComplete(res -> {
if (res.succeeded()) {
// Got the lock!
Lock lock = res.result();
// 5 seconds later we release the lock so someone else can get it
vertx.setTimer(5000, tid -> lock.release());
} else {
// Something went wrong
}
});
You can also get a lock with a timeout. If it fails to obtain the lock within the timeout the handler will be called with a failure:
SharedData sharedData = vertx.sharedData();
sharedData
.getLockWithTimeout("mylock", 10000)
.onComplete(res -> {
if (res.succeeded()) {
// Got the lock!
Lock lock = res.result();
} else {
// Failed to get lock
}
});
See the API docs
for a detailed list of lock operations.
In clustered mode, asynchronous locks rely on distributed data structures provided by the cluster manager. Beware that the latency relative to asynchronous shared lock operations can be much higher in clustered than in local mode. |
If your application doesn’t need the lock to be shared with every other node, you can retrieve a local-only lock:
SharedData sharedData = vertx.sharedData();
sharedData
.getLocalLock("mylock")
.onComplete(res -> {
if (res.succeeded()) {
// Local-only lock
Lock lock = res.result();
// 5 seconds later we release the lock so someone else can get it
vertx.setTimer(5000, tid -> lock.release());
} else {
// Something went wrong
}
});
Asynchronous counters
It’s often useful to maintain an atomic counter locally or across the different nodes of your application.
You can do this with Counter
.
You obtain an instance with getCounter
:
SharedData sharedData = vertx.sharedData();
sharedData
.getCounter("mycounter")
.onComplete(res -> {
if (res.succeeded()) {
Counter counter = res.result();
} else {
// Something went wrong!
}
});
Once you have an instance you can retrieve the current count, atomically increment it, decrement and add a value to it using the various methods.
See the API docs
for a detailed list of counter operations.
In clustered mode, asynchronous counters rely on distributed data structures provided by the cluster manager. Beware that the latency relative to asynchronous shared counter operations can be much higher in clustered than in local mode. |
If your application doesn’t need the counter to be shared with every other node, you can retrieve a local-only counter:
SharedData sharedData = vertx.sharedData();
sharedData
.getLocalCounter("mycounter")
.onComplete(res -> {
if (res.succeeded()) {
// Local-only counter
Counter counter = res.result();
} else {
// Something went wrong!
}
});
Using the file system with Vert.x
The Vert.x FileSystem
object provides many operations for manipulating the file system.
There is one file system object per Vert.x instance, and you obtain it with fileSystem
.
A blocking and a non blocking version of each operation is provided. The non blocking versions take a handler which is called when the operation completes or an error occurs.
Here’s an example of an asynchronous copy of a file:
FileSystem fs = vertx.fileSystem();
// Copy file from foo.txt to bar.txt
fs.copy("foo.txt", "bar.txt")
.onComplete(res -> {
if (res.succeeded()) {
// Copied ok!
} else {
// Something went wrong
}
});
The blocking versions are named xxxBlocking
and return the results or throw exceptions directly. In many cases, depending on the operating system and file system, some of the potentially blocking operations can return quickly, which is why we provide them, but it’s highly recommended that you test how long they take to return in your particular application before using them from an event loop, so as not to break the Golden Rule.
Here’s the copy using the blocking API:
FileSystem fs = vertx.fileSystem();
// Copy file from foo.txt to bar.txt synchronously
fs.copyBlocking("foo.txt", "bar.txt");
Many operations exist to copy, move, truncate, chmod and many other file operations. We won’t list them all here, please consult the API docs
for the full list.
Let’s see a couple of examples using asynchronous methods:
vertx.fileSystem()
.readFile("target/classes/readme.txt")
.onComplete(result -> {
if (result.succeeded()) {
System.out.println(result.result());
} else {
System.err.println("Oh oh ..." + result.cause());
}
});
// Copy a file
vertx.fileSystem()
.copy("target/classes/readme.txt", "target/classes/readme2.txt")
.onComplete(result -> {
if (result.succeeded()) {
System.out.println("File copied");
} else {
System.err.println("Oh oh ..." + result.cause());
}
});
// Write a file
vertx.fileSystem()
.writeFile("target/classes/hello.txt", Buffer.buffer("Hello"))
.onComplete(result -> {
if (result.succeeded()) {
System.out.println("File written");
} else {
System.err.println("Oh oh ..." + result.cause());
}
});
// Check existence and delete
vertx.fileSystem()
.exists("target/classes/junk.txt")
.compose(exist -> {
if (exist) {
return vertx.fileSystem().delete("target/classes/junk.txt");
} else {
return Future.failedFuture("File does not exist");
}
}).onComplete(result -> {
if (result.succeeded()) {
System.out.println("File deleted");
} else {
System.err.println("Oh oh ... - cannot delete the file: " + result.cause().getMessage());
}
});
Asynchronous files
Vert.x provides an asynchronous file abstraction that allows you to manipulate a file on the file system.
You open an AsyncFile
as follows:
OpenOptions options = new OpenOptions();
fileSystem
.open("myfile.txt", options)
.onComplete(res -> {
if (res.succeeded()) {
AsyncFile file = res.result();
} else {
// Something went wrong!
}
});
AsyncFile
implements ReadStream
and WriteStream
so you can pipe files to and from other stream objects such as net sockets, http requests and responses, and WebSockets.
They also allow you to read and write directly to them.
Random access writes
To use an AsyncFile
for random access writing you use the write
method.
The parameters to the method are:
-
buffer
: the buffer to write. -
position
: an integer position in the file where to write the buffer. If the position is greater or equal to the size of the file, the file will be enlarged to accommodate the offset. -
handler
: the result handler
Here is an example of random access writes:
vertx.fileSystem()
.open("target/classes/hello.txt", new OpenOptions())
.onComplete(result -> {
if (result.succeeded()) {
AsyncFile file = result.result();
Buffer buff = Buffer.buffer("foo");
for (int i = 0; i < 5; i++) {
file
.write(buff, buff.length() * i)
.onComplete(ar -> {
if (ar.succeeded()) {
System.out.println("Written ok!");
// etc
} else {
System.err.println("Failed to write: " + ar.cause());
}
});
}
} else {
System.err.println("Cannot open file " + result.cause());
}
});
Random access reads
To use an AsyncFile
for random access reads you use the read
method.
The parameters to the method are:
-
buffer
: the buffer into which the data will be read. -
offset
: an integer offset into the buffer where the read data will be placed. -
position
: the position in the file where to read data from. -
length
: the number of bytes of data to read -
handler
: the result handler
Here’s an example of random access reads:
vertx.fileSystem()
.open("target/classes/les_miserables.txt", new OpenOptions())
.onComplete(result -> {
if (result.succeeded()) {
AsyncFile file = result.result();
Buffer buff = Buffer.buffer(1000);
for (int i = 0; i < 10; i++) {
file
.read(buff, i * 100, i * 100, 100)
.onComplete(ar -> {
if (ar.succeeded()) {
System.out.println("Read ok!");
} else {
System.err.println("Failed to write: " + ar.cause());
}
});
}
} else {
System.err.println("Cannot open file " + result.cause());
}
});
Opening Options
When opening an AsyncFile
, you pass an OpenOptions
instance. These options describe the behavior of the file access. For instance, you can configure the file permissions with the setRead
, setWrite
and setPerms
methods.
You can also configure the behavior if the open file already exists with setCreateNew
and setTruncateExisting
.
You can also mark the file to be deleted on close or when the JVM is shutdown with setDeleteOnClose
.
Flushing data to underlying storage.
In the OpenOptions
, you can enable/disable the automatic synchronisation of the content on every write using setDsync
. In that case, you can manually flush any writes from the OS cache by calling the flush
method.
This method can also be called with a handler which will be called when the flush is complete.
Using AsyncFile as ReadStream and WriteStream
AsyncFile
implements ReadStream
and WriteStream
. You can then use them with a pipe to pipe data to and from other read and write streams. For example, this would copy the content to another AsyncFile
:
final AsyncFile output = vertx.fileSystem().openBlocking("target/classes/plagiary.txt", new OpenOptions());
vertx.fileSystem()
.open("target/classes/les_miserables.txt", new OpenOptions())
.compose(file -> file
.pipeTo(output)
.eventually(v -> file.close()))
.onComplete(result -> {
if (result.succeeded()) {
System.out.println("Copy done");
} else {
System.err.println("Cannot copy file " + result.cause().getMessage());
}
});
You can also use the pipe to write file content into HTTP responses, or more generally in any WriteStream
.
Accessing files from the classpath
When vert.x cannot find the file on the filesystem it tries to resolve the file from the class path. Note that classpath resource paths never start with a /
.
Due to the fact that Java does not offer async access to classpath resources, the file is copied to the filesystem in a worker thread when the classpath resource is accessed the very first time and served from there asynchronously. When the same resource is accessed a second time, the file from the filesystem is served directly from the filesystem. The original content is served even if the classpath resource changes (e.g. in a development system).
This caching behaviour can be set on the setFileCachingEnabled
option. The default value of this option is true
unless the system property vertx.disableFileCaching
is defined.
The path where the files are cached is /tmp/vertx-cache-UUID
by default and can be customized by setting the system property vertx.cacheDirBase
. When using this property, note that it should refer to a directory prefix in a process read/writeable location, for example: -Dvertx.cacheDirBase=/tmp/my-vertx-cache
(Note that there’s no UUID).
Each vert.x process will append it’s own UUID in order to keep caches independently of different applications running on the same machine.
The whole classpath resolving feature can be disabled system-wide by setting the system property vertx.disableFileCPResolving
to true
.
these system properties are evaluated once when the io.vertx.core.file.FileSystemOptions class is loaded, so these properties should be set before loading this class or as a JVM system property when launching it. |
If you want to disable classpath resolving for a particular application but keep it enabled by default system-wide, you can do so via the setClassPathResolvingEnabled
option.
Closing an AsyncFile
To close an AsyncFile
call the close
method. Closing is asynchronous and if you want to be notified when the close has been completed you can specify a handler function as an argument.
Datagram sockets (UDP)
Using User Datagram Protocol (UDP) with Vert.x is a piece of cake.
UDP is a connection-less transport which basically means you have no persistent connection to a remote peer.
Instead, you can send and receive packages and the remote address is contained in each of them.
Beside this UDP is not as safe as TCP to use, which means there are no guarantees that a send Datagram packet will receive it’s endpoint at all.
The only guarantee is that it will either receive complete or not at all.
Also, you usually can’t send data which is bigger then the MTU size of your network interface, this is because each packet will be send as one packet.
But, be aware even if the packet size is smaller then the MTU it may still fail.
At which size it will fail depends on the Operating System etc. So rule of thumb is to try to send small packets.
Because of the nature of UDP it is best fit for Applications where you are allowed to drop packets (like for example a monitoring application).
The benefits are that it has a lot less overhead compared to TCP, which can be handled by the NetServer and NetClient (see above).
Creating a DatagramSocket
To use UDP you first need t create a DatagramSocket
. It does not matter here if you only want to send data or send and receive.
DatagramSocket socket = vertx.createDatagramSocket(new DatagramSocketOptions());
The returned DatagramSocket
will not be bound to a specific port. This is not a problem if you only want to send data (like a client), but more on this in the next section.
Sending Datagram packets
As mentioned before, User Datagram Protocol (UDP) sends data in packets to remote peers but is not connected to them in a persistent fashion.
This means each packet can be sent to a different remote peer.
Sending packets is as easy as shown here:
DatagramSocket socket = vertx.createDatagramSocket(new DatagramSocketOptions());
Buffer buffer = Buffer.buffer("content");
// Send a Buffer
socket
.send(buffer, 1234, "10.0.0.1")
.onComplete(asyncResult -> System.out.println("Send succeeded? " + asyncResult.succeeded()));
// Send a String
socket
.send("A string used as content", 1234, "10.0.0.1")
.onComplete(asyncResult -> System.out.println("Send succeeded? " + asyncResult.succeeded()));
Receiving Datagram packets
If you want to receive packets you need to bind the DatagramSocket
by calling listen(…)}
on it.
This way you will be able to receive DatagramPacket`s that were sent to the address and port on which the `DatagramSocket
listens.
Beside this you also want to set a Handler
which will be called for each received DatagramPacket
.
The DatagramPacket
has the following methods:
So to listen on a specific address and port you would do something like shown here:
DatagramSocket socket = vertx.createDatagramSocket(new DatagramSocketOptions());
socket
.handler(packet -> {
// Do something with the packet
})
.listen(1234, "0.0.0.0")
.onComplete(asyncResult -> System.out.println("Send succeeded? " + asyncResult.succeeded()));
;
Be aware that even if the {code AsyncResult} is successed it only means it might be written on the network stack, but gives no guarantee that it ever reached or will reach the remote peer at all.
If you need such a guarantee then you want to use TCP with some handshaking logic build on top.
Multicast
Sending Multicast packets
Multicast allows multiple sockets to receive the same packets. This works by having the sockets join the same multicast group to which you can then send packets.
We will look at how you can join a Multicast Group and receive packets in the next section.
Sending multicast packets is not different from sending normal Datagram packets. The difference is that you pass in a multicast group address to the send method.
This is show here:
DatagramSocket socket = vertx.createDatagramSocket(new DatagramSocketOptions());
Buffer buffer = Buffer.buffer("content");
// Send a Buffer to a multicast address
socket
.send(buffer, 1234, "230.0.0.1")
.onComplete(asyncResult -> System.out.println("Send succeeded? " + asyncResult.succeeded()));
All sockets that have joined the multicast group 230.0.0.1 will receive the packet.
Receiving Multicast packets
If you want to receive packets for specific Multicast group you need to bind the DatagramSocket
by calling listen(…)
on it to join the Multicast group.
This way you will receive DatagramPackets that were sent to the address and port on which the DatagramSocket
listens and also to those sent to the Multicast group.
Beside this you also want to set a Handler which will be called for each received DatagramPacket.
The DatagramPacket
has the following methods:
-
sender()
: The InetSocketAddress which represent the sender of the packet -
data()
: The Buffer which holds the data which was received.
So to listen on a specific address and port and also receive packets for the Multicast group 230.0.0.1 you would do something like shown here:
DatagramSocket socket = vertx.createDatagramSocket(new DatagramSocketOptions());
socket
.handler(packet -> {
// Do something with the packet
})
.listen(1234, "0.0.0.0")
.compose(v -> socket.listenMulticastGroup("230.0.0.1")) // join the multicast group
.onComplete(asyncResult -> System.out.println("Listen succeeded? " + asyncResult.succeeded()));
Unlisten / leave a Multicast group
There are sometimes situations where you want to receive packets for a Multicast group for a limited time.
In this situations you can first start to listen for them and then later unlisten.
This is shown here:
DatagramSocket socket = vertx.createDatagramSocket(new DatagramSocketOptions());
socket
.handler(packet -> {
// Do something with the packet
})
.listen(1234, "0.0.0.0")
.compose(v -> socket.listenMulticastGroup("230.0.0.1")) // join the multicast group
.onComplete(asyncResult -> {
if (asyncResult.succeeded()) {
// will now receive packets for group
// do some work
socket.unlistenMulticastGroup("230.0.0.1").onComplete(asyncResult2 -> {
System.out.println("Unlisten succeeded? " + asyncResult2.succeeded());
});
} else {
System.out.println("Listen failed" + asyncResult.cause());
}
});
Blocking multicast
Beside unlisten a Multicast address it’s also possible to just block multicast for a specific sender address.
Be aware this only work on some Operating Systems and kernel versions. So please check the Operating System documentation if it’s supported.
This an expert feature.
To block multicast from a specific address you can call blockMulticastGroup(…)
on the DatagramSocket like shown here:
DatagramSocket socket = vertx.createDatagramSocket(new DatagramSocketOptions());
// Some code
// This would block packets which are send from 10.0.0.2
socket
.blockMulticastGroup("230.0.0.1", "10.0.0.2")
.onComplete(asyncResult -> System.out.println("block succeeded? " + asyncResult.succeeded()));
DatagramSocket properties
When creating a DatagramSocket
there are multiple properties you can set to change it’s behaviour with the DatagramSocketOptions
object. Those are listed here:
-
setSendBufferSize
Sets the send buffer size in bytes. -
setReceiveBufferSize
Sets the TCP receive buffer size in bytes. -
setReuseAddress
If true then addresses in TIME_WAIT state can be reused after they have been closed. -
setBroadcast
Sets or clears the SO_BROADCAST socket option. When this option is set, Datagram (UDP) packets may be sent to a local interface’s broadcast address. -
setMulticastNetworkInterface
Sets or clears the IP_MULTICAST_LOOP socket option. When this option is set, multicast packets will also be received on the local interface. -
setMulticastTimeToLive
Sets the IP_MULTICAST_TTL socket option. TTL stands for "Time to Live," but in this context it specifies the number of IP hops that a packet is allowed to go through, specifically for multicast traffic. Each router or gateway that forwards a packet decrements the TTL. If the TTL is decremented to 0 by a router, it will not be forwarded.
DatagramSocket Local Address
You can find out the local address of the socket (i.e. the address of this side of the UDP Socket) by calling localAddress
. This will only return an InetSocketAddress
if you bound the DatagramSocket
with listen(…)
before, otherwise it will return null.
Closing a DatagramSocket
You can close a socket by invoking the close
method. This will close the socket and release all resources
DNS client
Often you will find yourself in situations where you need to obtain DNS informations in an asynchronous fashion. Unfortunally this is not possible with the API that is shipped with the Java Virtual Machine itself. Because of this Vert.x offers it’s own API for DNS resolution which is fully asynchronous.
To obtain a DnsClient instance you will create a new via the Vertx instance.
DnsClient client = vertx.createDnsClient(53, "10.0.0.1");
You can also create the client with options and configure the query timeout.
DnsClient client = vertx.createDnsClient(new DnsClientOptions()
.setPort(53)
.setHost("10.0.0.1")
.setQueryTimeout(10000)
);
Creating the client with no arguments or omitting the server address will use the address of the server used internally for non blocking address resolution.
DnsClient client1 = vertx.createDnsClient();
// Just the same but with a different query timeout
DnsClient client2 = vertx.createDnsClient(new DnsClientOptions().setQueryTimeout(10000));
A client uses a single event loop for querying purposes, it can safely be used from any thread, including non Vert.x thread.
lookup
Try to lookup the A (ipv4) or AAAA (ipv6) record for a given name. The first which is returned will be used, so it behaves the same way as you may be used from when using "nslookup" on your operation system.
To lookup the A / AAAA record for "vertx.io" you would typically use it like:
DnsClient client = vertx.createDnsClient(53, "9.9.9.9");
client
.lookup("vertx.io")
.onComplete(ar -> {
if (ar.succeeded()) {
System.out.println(ar.result());
} else {
System.out.println("Failed to resolve entry" + ar.cause());
}
});
lookup4
Try to lookup the A (ipv4) record for a given name. The first which is returned will be used, so it behaves the same way as you may be used from when using "nslookup" on your operation system.
To lookup the A record for "vertx.io" you would typically use it like:
DnsClient client = vertx.createDnsClient(53, "9.9.9.9");
client
.lookup4("vertx.io")
.onComplete(ar -> {
if (ar.succeeded()) {
System.out.println(ar.result());
} else {
System.out.println("Failed to resolve entry" + ar.cause());
}
});
lookup6
Try to lookup the AAAA (ipv6) record for a given name. The first which is returned will be used, so it behaves the same way as you may be used from when using "nslookup" on your operation system.
To lookup the A record for "vertx.io" you would typically use it like:
DnsClient client = vertx.createDnsClient(53, "9.9.9.9");
client
.lookup6("vertx.io")
.onComplete(ar -> {
if (ar.succeeded()) {
System.out.println(ar.result());
} else {
System.out.println("Failed to resolve entry" + ar.cause());
}
});
resolveA
Try to resolve all A (ipv4) records for a given name. This is quite similar to using "dig" on unix like operation systems.
To lookup all the A records for "vertx.io" you would typically do:
DnsClient client = vertx.createDnsClient(53, "9.9.9.9");
client
.resolveA("vertx.io")
.onComplete(ar -> {
if (ar.succeeded()) {
List<String> records = ar.result();
for (String record : records) {
System.out.println(record);
}
} else {
System.out.println("Failed to resolve entry" + ar.cause());
}
});
resolveAAAA
Try to resolve all AAAA (ipv6) records for a given name. This is quite similar to using "dig" on unix like operation systems.
To lookup all the AAAAA records for "vertx.io" you would typically do:
DnsClient client = vertx.createDnsClient(53, "9.9.9.9");
client
.resolveAAAA("vertx.io")
.onComplete(ar -> {
if (ar.succeeded()) {
List<String> records = ar.result();
for (String record : records) {
System.out.println(record);
}
} else {
System.out.println("Failed to resolve entry" + ar.cause());
}
});
resolveCNAME
Try to resolve all CNAME records for a given name. This is quite similar to using "dig" on unix like operation systems.
To lookup all the CNAME records for "vertx.io" you would typically do:
DnsClient client = vertx.createDnsClient(53, "9.9.9.9");
client
.resolveCNAME("vertx.io")
.onComplete(ar -> {
if (ar.succeeded()) {
List<String> records = ar.result();
for (String record : records) {
System.out.println(record);
}
} else {
System.out.println("Failed to resolve entry" + ar.cause());
}
});
resolveMX
Try to resolve all MX records for a given name. The MX records are used to define which Mail-Server accepts emails for a given domain.
To lookup all the MX records for "vertx.io" you would typically do:
DnsClient client = vertx.createDnsClient(53, "9.9.9.9");
client
.resolveMX("vertx.io")
.onComplete(ar -> {
if (ar.succeeded()) {
List<MxRecord> records = ar.result();
for (MxRecord record : records) {
System.out.println(record);
}
} else {
System.out.println("Failed to resolve entry" + ar.cause());
}
});
Be aware that the List will contain the MxRecord
sorted by the priority of them, which means MX records with smaller priority coming first in the List.
The MxRecord
allows you to access the priority and the name of the MX record by offer methods for it like:
record.priority();
record.name();
resolveTXT
Try to resolve all TXT records for a given name. TXT records are often used to define extra information for a domain.
To resolve all the TXT records for "vertx.io" you could use something along these lines:
DnsClient client = vertx.createDnsClient(53, "9.9.9.9");
client
.resolveTXT("vertx.io")
.onComplete(ar -> {
if (ar.succeeded()) {
List<String> records = ar.result();
for (String record : records) {
System.out.println(record);
}
} else {
System.out.println("Failed to resolve entry" + ar.cause());
}
});
resolveNS
Try to resolve all NS records for a given name. The NS records specify which DNS Server hosts the DNS informations for a given domain.
To resolve all the NS records for "vertx.io" you could use something along these lines:
DnsClient client = vertx.createDnsClient(53, "9.9.9.9");
client
.resolveNS("vertx.io")
.onComplete(ar -> {
if (ar.succeeded()) {
List<String> records = ar.result();
for (String record : records) {
System.out.println(record);
}
} else {
System.out.println("Failed to resolve entry" + ar.cause());
}
});
resolveSRV
Try to resolve all SRV records for a given name. The SRV records are used to define extra information like port and hostname of services. Some protocols need this extra information.
To lookup all the SRV records for "vertx.io" you would typically do:
DnsClient client = vertx.createDnsClient(53, "9.9.9.9");
client
.resolveSRV("vertx.io")
.onComplete(ar -> {
if (ar.succeeded()) {
List<SrvRecord> records = ar.result();
for (SrvRecord record : records) {
System.out.println(record);
}
} else {
System.out.println("Failed to resolve entry" + ar.cause());
}
});
Be aware that the List will contain the SrvRecords sorted by the priority of them, which means SrvRecords with smaller priority coming first in the List.
The SrvRecord
allows you to access all information contained in the SRV record itself:
record.priority();
record.name();
record.weight();
record.port();
record.protocol();
record.service();
record.target();
Please refer to the API docs for the exact details.
resolvePTR
Try to resolve the PTR record for a given name. The PTR record maps an ipaddress to a name.
To resolve the PTR record for the ipaddress 10.0.0.1 you would use the PTR notion of "1.0.0.10.in-addr.arpa"
DnsClient client = vertx.createDnsClient(53, "9.9.9.9");
client
.resolvePTR("1.0.0.10.in-addr.arpa")
.onComplete(ar -> {
if (ar.succeeded()) {
String record = ar.result();
System.out.println(record);
} else {
System.out.println("Failed to resolve entry" + ar.cause());
}
});
reverseLookup
Try to do a reverse lookup for an ipaddress. This is basically the same as resolve a PTR record, but allows you to just pass in the ipaddress and not a valid PTR query string.
To do a reverse lookup for the ipaddress 10.0.0.1 do something similar like this:
DnsClient client = vertx.createDnsClient(53, "9.9.9.9");
client
.reverseLookup("10.0.0.1")
.onComplete(ar -> {
if (ar.succeeded()) {
String record = ar.result();
System.out.println(record);
} else {
System.out.println("Failed to resolve entry" + ar.cause());
}
});
Error handling
As you saw in previous sections the DnsClient allows you to pass in a Handler which will be notified with an AsyncResult once the query was complete. In case of an error it will be notified with a DnsException which will hole a DnsResponseCode
that indicate why the resolution failed. This DnsResponseCode can be used to inspect the cause in more detail.
Possible DnsResponseCodes are:
-
NOERROR
No record was found for a given query -
FORMERROR
Format error -
SERVFAIL
Server failure -
NXDOMAIN
Name error -
NOTIMPL
Not implemented by DNS Server -
REFUSED
DNS Server refused the query -
YXDOMAIN
Domain name should not exist -
YXRRSET
Resource record should not exist -
NXRRSET
RRSET does not exist -
NOTZONE
Name not in zone -
BADVERS
Bad extension mechanism for version -
BADSIG
Bad signature -
BADKEY
Bad key -
BADTIME
Bad timestamp
All of those errors are "generated" by the DNS Server itself.
You can obtain the DnsResponseCode from the DnsException like:
DnsClient client = vertx.createDnsClient(53, "10.0.0.1");
client
.lookup("nonexisting.vert.xio")
.onComplete(ar -> {
if (ar.succeeded()) {
String record = ar.result();
System.out.println(record);
} else {
Throwable cause = ar.cause();
if (cause instanceof DnsException) {
DnsException exception = (DnsException) cause;
DnsResponseCode code = exception.code();
// ...
} else {
System.out.println("Failed to resolve entry" + ar.cause());
}
}
});
Vert.x Virtual Threads
Use virtual threads to write Vert.x code that looks like it is synchronous.
You still write the traditional Vert.x code processing events, but you have the opportunity to write synchronous code for complex workflows and use thread locals in such workflows.
Introduction
The non-blocking nature of Vert.x leads to asynchronous APIs. Asynchronous APIs can take various forms including callback style, promises and reactive extensions.
In some cases, programming using asynchronous APIs can be more challenging than using a direct synchronous style, in particular if you have several operations that you want to do sequentially. Also, error propagation is often more complex when using asynchronous APIs.
Virtual thread support allows you to work with asynchronous APIs, but using a direct synchronous style that you’re already familiar with.
It does this by using Java 21 virtual threads. Virtual threads are very lightweight threads that do not correspond to underlying kernel threads. When they are blocked they do not block a kernel thread.
Using virtual threads
You can deploy virtual thread verticles.
A virtual thread verticle is capable of awaiting Vert.x futures and gets the result synchronously.
When the verticle awaits a result, the verticle can still process events like an event-loop verticle.
AbstractVerticle verticle = new AbstractVerticle() {
@Override
public void start() {
HttpClient client = vertx.createHttpClient();
HttpClientRequest req = Future.await(client.request(
HttpMethod.GET,
8080,
"localhost",
"/"));
HttpClientResponse resp = Future.await(req.send());
int status = resp.statusCode();
Buffer body = Future.await(resp.body());
}
};
// Run the verticle a on virtual thread
vertx.deployVerticle(verticle, new DeploymentOptions().setThreadingModel(ThreadingModel.VIRTUAL_THREAD));
Using virtual threads requires Java 21 or above. |
Blocking within a virtual thread verticle
You can use Future.await
to suspend the current virtual thread until the awaited result is available.
The virtual thread is effectively blocked, but the application can still process events.
When a virtual thread awaits for a result and the verticle needs to process a task, a new virtual thread is created to handle this