<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-core</artifactId>
<version>5.0.12</version>
</dependency>
Vert.x Core 核心技术手册
Vert.x 的心脏是由一套底层的 Java API 构成的,我们称之为 Vert.x Core。
Vert.x Core 提供了构建现代响应式系统所需的各类基础核心能力:
-
编写 TCP 客户端与服务端
-
编写 HTTP 客户端与服务端(包含 WebSocket 支持)
-
事件总线 (Event Bus)
-
共享数据 (Shared Data) —— 本地 Map 与集群分布式 Map
-
周期性与延迟定时任务
-
部署与卸载 Verticle
-
数据报套接字 (UDP Datagram Sockets)
-
异步 DNS 客户端
-
异步文件系统访问
-
虚拟线程 (Virtual Threads)
-
高可用性 (High Availability)
-
本地原生传输层 (Native Transports)
-
集群化 (Clustering)
Core 模块中的功能偏向基础设施底层 —— 在这里你不会找到数据库直连访问、业务授权鉴权或高层 Web 路由这类功能;这类丰富的上层特性由 Vert.x ext(扩展库家族)全面提供。
Vert.x Core 小巧而轻量。你只需按需挑选引入所需的能力。它完全支持嵌入到你现有的任何应用程序中 —— 我们从不强迫你为了使用 Vert.x 而改变既有的项目工程架构。
你可以从 Vert.x 支持的任何多语言环境中使用 Core。更妙的是 —— 我们不会强迫你在其他语言中生搬硬套 Java 的调用风格。例如,强制 Ruby 开发者遵守 Java 编码习惯是别扭的;因此,Vert.x 会针对每门语言自动生成符合该语言地道表达习惯(Idiomatic)的核心 API 封装。
在后续文档中,我们将直接使用 Core 指代 Vert.x Core。
如果你使用 Maven 或 Gradle,请在构建描述文件中添加以下依赖以引入 Vert.x Core API:
-
Maven(在
pom.xml中):
-
Gradle(在
build.gradle中):
dependencies {
compile 'io.vertx:vertx-core:5.0.12'
}
接下来让我们深入探讨 Core 中的核心概念与特性。
从 Vert.x 的起源说起
在 Vert.x 的世界里,几乎所有的交互都需要通过 Vertx 根对象来进行!
它是整个 Vert.x 的控制调度中枢,也是你开展一切工作的起点,包括创建客户端与服务端、获取 Event Bus 引用、设置定时器等等。
那么,如何获取一个 Vertx 实例呢?
如果你是将 Vert.x 嵌入到现有应用中,直接按如下方式创建实例即可:
Vertx vertx = Vertx.vertx();
| 绝大多数应用程序全生命周期只需要一个 Vert.x 单例即可;不过在需要隔离 Event Bus 或隔离不同服务端/客户端集群的场景下,也可以创建多个独立的 Vert.x 实例。 |
创建 Vertx 实例时指定配置选项
如果默认参数不满足你的需求,可以在创建 Vert.x 实例时传入自定义选项:
Vertx vertx = Vertx.vertx(new VertxOptions().setWorkerPoolSize(40));
VertxOptions 对象提供了丰富的配置项,允许你调整集群、高可用、线程池大小等各类运行参数。
创建集群版 Vert.x 实例
如果你需要创建一个集群化 Vert.x 实例(有关集群详情请参阅 事件总线 章节),通常需要使用异步方式来构建 Vertx 对象。
这是因为集群中的不同 Vert.x 实例在局域网内完成组网发现与协商通常需要耗费数秒时间。在此期间,为了避免阻塞调用线程,Vert.x 采用异步回调的方式交付初始化结果。
领略流式(Fluent)API 风格
你可能已经注意到,在前文的示例中大量采用了流式(Fluent)链式调用的 API 风格。
Fluent API 允许将多个方法调用链式串联。例如:
request.response().putHeader("Content-Type", "text/plain").end("some text");
这是贯穿整个 Vert.x API 的典型设计范式,尽情享受这种流畅的体验吧。
链式调用可以让代码更加精炼易读。当然,如果你不喜欢流式风格,我们从不强求,你完全可以采用传统的写法:
HttpServerResponse response = request.response();
response.putHeader("Content-Type", "text/plain");
response.end("some text");
好莱坞原则:别打给我,有事我通知你
Vert.x 的 API 本质上是事件驱动(Event-Driven)的。这意味着当 Vert.x 内部发生你感兴趣的事情时,Vert.x 会主动向你发送事件通知。
一些典型的事件示例包括:
-
定时器触发跳动
-
套接字上有网络数据到达
-
磁盘文件异步读取完毕
-
系统捕获到未处理异常
-
HTTP 服务端接收到一个新请求
你通过向 Vert.x API 提供处理器(Handlers)来处理这些事件。例如,想要每隔一秒接收一次定时器事件,可以编写:
vertx.setPeriodic(1000, id -> {
// 该处理器将每隔 1 秒被调用一次
System.out.println("定时器跳动!");
});
接收 HTTP 请求同样简洁直观:
server.requestHandler(request -> {
// 每当服务端接收到 HTTP 请求时触发该处理器
request.response().end("hello world!");
});
当 Vert.x 捕获到事件需要派发给你的处理器时,Vert.x 会以异步方式回调它。
这引出了 Vert.x 架构中至关重要的几大基石概念:
决不阻塞线程!
除了极少数以 Sync 结尾的同步文件系统操作外,Vert.x 中的任何 API 都绝对不会阻塞调用线程。
如果结果能够立刻给出,它会直接同步返回;否则,通常需要你提供一个回调处理器,在稍后的某个时间点异步接收事件。
正因为 Vert.x 的所有 API 均不阻塞线程,这意味着你可以使用极少数的系统线程来支撑极其庞大的并发吞吐。
在传统的阻塞式 API 中,调用线程在以下场景会被迫挂起等待:
-
从 Socket 读取网络数据
-
向磁盘写入数据
-
向对端发送消息并同步等待响应
-
……以及许多其他 I/O 场景
在上述所有情况下,当你的线程处于等待阻塞时,它无法执行任何其他有意义的工作 —— 处于完全闲置浪费状态。
这意味着如果使用传统的阻塞 API 来应对高并发,你就必须创建海量的操作系统线程,以避免整个系统陷入停滞。
然而,每个线程都需要占用不可小觑的系统资源(例如栈内存消耗)和昂贵的 CPU 上下文切换成本。在现代系统所需的超高并发场景下,传统的阻塞模型往往会迅速触碰性能天花板。
Reactor 与 Multi-Reactor 模式
前文提到,Vert.x 是事件驱动的 —— 当事件就绪时,Vert.x 会将其派发给处理器。
在绝大多数场景下,Vert.x 会使用一个名为 Event Loop(事件循环) 的专有线程来调用你的处理器。
由于 Vert.x 与你的业务代码中均不执行阻塞操作,Event Loop 线程可以像旋转木马一样永不停歇地在各个就绪事件与处理器之间快速轮转派发。
正因为没有任何阻塞耗时,单个 Event Loop 可以在极短时间内派发处理海量事件。例如,单个 Event Loop 线程即可在眨眼之间轻松处理数以万计的并发 HTTP 请求。
这就是计算机体系中著名的 Reactor 模式(反应器模式)。
你可能对此有所耳闻 —— 例如 Node.js 便是该模式的典型实现。
在标准的经典 Reactor 实现中,系统内部通常只有单个 Event Loop 线程在无限循环中派发所有事件。 单线程 Reactor 的硬伤在于:在任何时刻它只能运行在单个 CPU 核心上。如果你希望单线程的应用充分利用多核服务器的硬件算力,就必须在外部启动并管理多个操作系统进程。
Vert.x 在这里开创性地进行了升维改造:Vertx 实例内部维护的不是单一事件循环,而是多个并发 Event Loop 线程。 默认情况下,Vert.x 会根据宿主机可用的 CPU 核心数自动设定 Event Loop 的数量(也可按需覆盖配置)。
这意味着单个 Vert.x 进程就能天然自适应伸缩至整台服务器的所有 CPU 核心,免去了繁琐的多进程管理。
为了与传统的单线程 Reactor 进行区分,我们将这种先进的并发架构命名为 Multi-Reactor 模式(多反应器模式)。
| 尽管一个 Vertx 实例内部包含多个 Event Loop,但任何给定的处理器实例绝不会被并发执行;在绝大多数场景下(工作线程 Verticle 除外),特定的处理器将始终在同一个固定的 Event Loop 线程上被串行回调。 |
黄金法则:绝对不要阻塞 Event Loop!
我们已经知道 Vert.x 自身的 API 是非阻塞的,绝不阻塞 Event Loop;但如果你自己在处理器中编写了阻塞操作,那框架层的一切优化都将付诸东流。
一旦你在处理器中执行了耗时阻塞,该 Event Loop 在阻塞解除之前将无法处理任何其他事件!如果你阻塞了 Vertx 实例中的所有 Event Loop 线程,你的整个应用程序将会陷入彻底的卡死假死状态!
所以切记:绝对不要阻塞 Event Loop!请时刻牢记此警告。
典型的常见阻塞操作包括:
-
Thread.sleep() -
等待获取锁(Lock)
-
等待互斥量或对象监视器(如进入
synchronized代码块) -
执行耗时漫长的数据库阻塞查询并同步等待结果
-
执行消耗大量 CPU 算力的复杂密集计算
-
在死循环或长时间轮询中自旋
如果上述操作导致 Event Loop 在相当长的一段时间内无法处理其他任务,那你的架构设计就必须立即重构并纠正。
那么……多长时间算作“相当长的一段时间”呢?
这取决于你的业务场景和并发要求。如果你只有一个 Event Loop 线程,且希望每秒处理 10000 个 HTTP 请求,显然每个请求的处理耗时绝不能超过 0.1 毫秒,否则系统就会发生排队拥堵。
如果你发现应用程序的响应迟缓或失去响应,这通常是在某处无意中阻塞了 Event Loop 的危险信号。为了帮助你精准定位此类瓶颈,当 Vert.x 检测到某个 Event Loop 超过一段时间未返回时,会自动在控制台输出醒目的警告日志:
Thread vertx-eventloop-thread-3 has been blocked for 20458 ms
Vert.x 还会附带打印出完整的堆栈轨迹(Stack Trace),帮助你一针见血地查出究竟是哪一行代码造成了阻塞。
如果你希望关闭此类日志报警或调整阈值,可以在创建 Vertx 实例前在 VertxOptions 中进行定制。
Future 异步结果
Vert.x 使用 Future 来表达异步计算的结果。
任何异步方法都会返回一个 Future 对象,用以承载该调用的最终状态:成功 (Success) 或 失败 (Failure)。
你无法直接与 Future 的内部结果发生交互;相反,你需要注册一个处理器(Handler)。当 Future 完成并且结果就绪时,该处理器将被触发回调,这与处理其他类型的事件完全一致。
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("文件大小 = " + props.size());
} else {
System.out.println("操作失败: " + ar.cause().getMessage());
}
});
|
切勿混淆 Future 与 Promise。 如果说 Future 代表了异步结果的“读端(Read-side)”,那么 Promise 就是“写端(Write-side)”。 Promise 允许你在稍后的某个时间点手动填充或提供异步结果。 |
Future 组合 (Future Composition)
compose 方法可用于链式编排多个 Future:
-
当当前的 Future 成功完成时,应用指定的函数(该函数返回一个新的 Future)。当该返回的 Future 也成功完成时,整个组合操作宣告成功。
-
当当前的 Future 失败时,整个组合操作立即宣告失败。
FileSystem fs = vertx.fileSystem();
Future<Void> future = fs
.createFile("/foo")
.compose(v -> {
// 当文件创建成功后 (fut1),执行此写入操作:
return fs.writeFile("/foo", Buffer.buffer());
})
.compose(v -> {
// 当文件写入成功后 (fut2),执行此移动操作:
return fs.move("/foo", "/bar");
});
在这个示例中,3 个异步操作被顺序串联:
-
创建文件
-
向该文件写入数据
-
移动文件
只有当这 3 个步骤全部执行成功时,最终的 future 才会标记为成功。
只要其中任意一步发生失败,最终的 Future 就会判定为失败。
除此以外,Future 还提供了更丰富的算子:map、recover、otherwise、andThen 以及作为 compose 语义别名的 flatMap。
Future 协同 (Future Coordination)
通过 Vert.x 的 Future,可以极其轻松地实现多任务协同。它天然支持并发协同(并行触发多个异步任务)与时序协同(链式编排异步任务)。
Future.all 接收多个 Future 参数(最多 6 个直接参数),并返回一个聚合 Future:当且仅当所有传入的 Future 全部成功时才判定为成功;只要有任意一个失败,聚合 Future 即判定为失败:
Future<HttpServer> httpServerFuture = httpServer.listen();
Future<NetServer> netServerFuture = netServer.listen();
Future.all(httpServerFuture, netServerFuture).onComplete(ar -> {
if (ar.succeeded()) {
// 所有服务端均已成功启动
} else {
// 至少有一个服务端启动失败
}
});
这些异步操作是并发执行的,绑定到返回 Future 上的 Handler 将在整个组合任务完成时被触发。
当其中一个操作失败时,聚合结果 Future 也会被标记为失败。
当所有操作均成功时,聚合 Future 成功完成。
在操作成功时,resultAt 方法能够保证返回结果严格按照调用 Future.all 时传入的顺序排列。在上面的示例中,无论哪一个服务端最先完成监听,你都可以通过 resultAt(0) 获取 httpServer 的结果,通过 resultAt(1) 获取 netServer 的结果。
作为替代方案,你也可以直接传入一个包含 Future 的列表(List,可为空):
Future.all(Arrays.asList(future1, future2, future3));
all 协同策略会等待所有 Future 执行成功(或在出现首个失败时停止),而 any 协同策略则会等待首个成功的 Future。Future.any 接收多个 Future 参数(最多 6 个),只要有任意一个 Future 成功即判定为成功;当所有 Future 全部失败时才判定为失败:
Future.any(future1, future2).onComplete(ar -> {
if (ar.succeeded()) {
// 至少有一个 Future 成功
} else {
// 全部 Future 均失败
}
});
同样支持传入 Future 列表:
Future.any(Arrays.asList(f1, f2, f3));
join 协同策略则会等待所有 Future 全部执行结束(无论成功或失败)。Future.join 接收多个 Future 参数(最多 6 个),当所有 Future 全部成功时判定为成功;当所有 Future 执行完毕但至少有一个失败时,判定为失败:
Future.join(future1, future2, future3).onComplete(ar -> {
if (ar.succeeded()) {
// 全部成功
} else {
// 全部执行完成,但至少有一个失败
}
});
同样支持传入 Future 列表:
Future.join(Arrays.asList(future1, future2, future3));
与 CompletionStage 的互操作性
Vert.x 的 Future API 提供了与 JDK 标准异步可组合接口 CompletionStage 的双向无缝转换能力。
我们可以通过 toCompletionStage 方法将 Vert.x Future 转换为标准 CompletionStage,示例如下:
Future<String> future = vertx.createDnsClient().lookup("vertx.io");
future.toCompletionStage().whenComplete((ip, err) -> {
if (err != null) {
System.err.println("无法解析域名 vertx.io");
err.printStackTrace();
} else {
System.out.println("vertx.io => " + ip);
}
});
反之,我们也可以通过 Future.fromCompletionStage 将标准 CompletionStage 转换为 Vert.x Future。该方法提供了 2 种重载变体:
-
第一种变体仅接收单个
CompletionStage参数,并在完成该CompletionStage的原始线程中触发调用Future的回调方法; -
第二种变体额外接收一个
Context上下文参数,保证Future的回调逻辑精确分发到指定的 Vert.x 上下文中执行。
在绝大多数场景下,为了遵循 Vert.x 的线程模型规范,强烈推荐使用带有 Context 参数的变体。因为 Vert.x 的 Future 绝大多数时候都是与 Vert.x 核心组件、扩展库与客户端配合使用的。
|
以下是将 CompletionStage 转换为 Vert.x Future 并分发至 Vert.x 上下文的完整示例:
Future.fromCompletionStage(completionStage, vertx.getOrCreateContext())
.flatMap(str -> {
String key = UUID.randomUUID().toString();
return storeInDb(key, str);
})
.onSuccess(str -> {
System.out.println("已获得执行结果: " + str);
})
.onFailure(err -> {
System.err.println("处理过程中发生错误");
err.printStackTrace();
});
Verticle 架构模型
Vert.x 开箱即用提供了一套轻量、高伸缩且类似 Actor 模型的部署与并发组件体系 —— Verticle,帮助你避免自行设计复杂的并发隔离逻辑。
这套模型完全是可选的,如果你不想使用它,Vert.x 绝不强迫你以这种方式构建应用。
该模型虽然不是教条式的纯粹 Actor 实现,但它在并发隔离、水平伸缩和模块化部署上汲取了 Actor 模型的精髓。
若选用该模型,你的业务代码会被组织为一个个独立的 Verticle 组件。
Verticle 是交由 Vert.x 部署和运行的代码执行单元。一个 Vert.x 实例默认维护 N 个 Event Loop 线程(默认 N = CPU 核心数 * 2)。Verticle 可以使用 Vert.x 支持的任意编程语言编写,同一个应用完全支持混用多语言编写的 Verticle。
你可以把 Verticle 想象成 Actor 模型 中的 Actor。
一个典型的生产应用通常由多个运行在同一 Vert.x 实例中的 Verticle 组件协同构成。不同的 Verticle 之间不共享内存状态,而是通过 事件总线 (Event Bus) 发送消息实现松耦合通信。
编写 Verticle
Verticle 类必须实现 Deployable 接口。
你可以直接实现该接口,但通常最简便优雅的方式是继承基础抽象类 VerticleBase。
一个标准的 Verticle 编写示例如下:
class MyVerticle extends VerticleBase {
// 当 Verticle 被部署时调用
public Future<?> start() throws Exception {
return super.start();
}
// 可选:当 Verticle 被卸载时调用
public Future<?> stop() throws Exception {
return super.stop();
}
}
通常你只需按需重写 start 启动方法。
当 Vert.x 部署该 Verticle 时会触发 start 方法;只有当该方法返回的 Future 成功完成时,该 Verticle 才被视为正式启动就绪。
你还可以选择性地重写 stop 停止方法。当该 Verticle 被卸载时 Vert.x 会触发它;当返回的 Future 完成时,Verticle 正式停止。
下面是一个更完整的实战示例:
class MyVerticle extends VerticleBase {
private HttpServer server;
@Override
public Future<?> start() {
server = vertx.createHttpServer().requestHandler(req -> {
req.response()
.putHeader("content-type", "text/plain")
.end("Hello from Vert.x!");
});
// 绑定并监听端口:
return server.listen(8080);
}
}
你甚至可以用单行 Lambda 表达式编写极简 Verticle:
Deployable verticle = context -> vertx
.createHttpServer()
.requestHandler(req -> req.response()
.putHeader("content-type", "text/plain")
.end("Hello from Vert.x!"))
.listen(8080);
你不需要在 Verticle 的 stop 方法中手动关闭由它启动的 HTTP 服务端。当 Verticle 卸载时,Vert.x 会自动关闭它启动的全部服务实例。
|
Vert.x 4 中的 Verticle 与 AbstractVerticle 怎么了?
在 Vert.x 5 全面转向 Future 驱动的异步模型后,原先 Vert.x 4 中由 Verticle 与 AbstractVerticle 定义的基于 Promise 的入参协议显得有些冗余:
class MyVerticle extends AbstractVerticle {
@Override
public void start(Promise<Void> startPromise) throws Exception {
Future<String> future = bindService();
// 过去需要繁琐地桥接 Promise:
future.onComplete(ar -> {
if (ar.succeeded()) {
startPromise.complete();
} else {
startPromise.fail(ar.cause());
}
});
// 或者简写为:
future
.<Void>mapEmpty()
.onComplete(startPromise);
}
}
尽管如此,Verticle 和 AbstractVerticle 在 Vert.x 5 中并未废弃。你可以继续使用它们,但在 Vert.x 5 中推荐直接使用返回 Future 的 VerticleBase。
Verticle 的类型
Verticle 主要包含以下两类核心类型:
- 标准 Verticle (Standard Verticles)
-
最常用且最高效的类型 —— 它们始终绑定在一个固定的 Event Loop 线程上调度执行。下一节将详细展开。
- 工作线程 Verticle (Worker Verticles)
-
运行在专门的 Worker 线程池中。同一个 Verticle 实例在任何时刻绝不会被多个线程并发执行。
标准 Verticle (Standard Verticles)
标准 Verticle 在创建时会被分配一个 Event Loop 线程,其 start 方法将由该 Event Loop 线程调度执行。当你在该 Event Loop 中调用任何接受 Handler 的 Core API 时,Vert.x 能够保证这些 Handler 回调同样在该固定的 Event Loop 上执行。
这保证了同一个 Verticle 实例内部的所有业务代码永远只在单个线程上串行流转(前提是你没有在内部自己创建第三方线程并反向调用它!)。
这一设计意味着你可以像编写传统单线程程序一样放心地编写业务逻辑,将并发、多核调度与线程安全完全委托给 Vert.x。从此不再需要担惊受怕地维护 synchronized、volatile,彻底远离了传统手动编写多线程应用时令人头疼的竞态条件与死锁噩梦。
工作线程 Verticle (Worker Verticles)
工作线程 Verticle 与标准 Verticle 结构类似,唯一的区别在于它由 Vert.x 的 Worker 线程池中的线程调度执行,而不是占用宝贵的 Event Loop 线程。
Worker Verticle 专为执行阻塞式遗留代码而设计,因为它的运行绝不会阻塞任何 Event Loop。
如果你不想为了运行阻塞代码而单独定义一个 Worker Verticle,你也可以直接在 Event Loop 中使用 行内阻塞代码 (executeBlocking)。
如果你希望将某个 Verticle 作为 Worker Verticle 进行部署,可以通过 setThreadingModel 进行配置:
DeploymentOptions options = new DeploymentOptions().setThreadingModel(ThreadingModel.WORKER);
vertx.deployVerticle(new MyOrderProcessorVerticle(), options);
Vert.x 能够保证同一个 Worker Verticle 实例在任何时刻绝不会被多个线程并发执行,但在不同时间片上可能会由不同的 Worker 线程交替调度。
虚拟线程 Verticle (Virtual Thread Verticles)
虚拟线程 Verticle 类似标准 Verticle,但它运行在 JDK 21+ 的虚拟线程(Virtual Threads)之上,而非绑死在操作系统的物理线程或 Event Loop 上。
虚拟线程 Verticle 专为搭配 Vert.x Future 实现直观的同步阻塞式编程体验而设计。
将其指定为虚拟线程 Verticle 进行部署的示例如下:
DeploymentOptions options = new DeploymentOptions().setThreadingModel(ThreadingModel.VIRTUAL_THREAD);
vertx.deployVerticle(new MyOrderProcessorVerticle(), options);
| 该特性需要运行在 Java 21 或更高版本 的 JVM 上。 |
以编程方式部署 Verticle
你可以通过 deployVerticle 方法部署一个已由你实例化的 Verticle 对象,也可以传入类名让系统动态加载:
VerticleBase myVerticle = new MyVerticle();
vertx.deployVerticle(myVerticle);
通过类名部署的示例如下:
vertx.deployVerticle("com.mycompany.MyOrderProcessorVerticle");
Verticle 名称到 Verticle Factory 的映射规则
通过字符串名称部署 Verticle 时,系统会通过该名称匹配对应的 VerticleFactory 工厂实例。
Verticle 名称可以包含前缀(以冒号结尾的字符串),例如:
groovy:com.mycompany.SomeGroovyCompiledVerticle // 使用 Groovy 工厂
如果没有前缀,Vert.x 会检查文件后缀名(例如 .groovy)。
若既无前缀也无后缀,Vert.x 默认将其视作 Java 的全限定类名(FQCN)进行反射实例化。
如何定位与加载 Verticle Factory?
大部分 Verticle 工厂通过 Classpath 中的 Java SPI 机制在 Vert.x 启动时自动发现注册。
你也可以通过 registerVerticleFactory 与 unregisterVerticleFactory 在运行时动态增删工厂。
异步等待部署完成
Verticle 的部署过程是完全异步的。如果你需要在部署成功时得到回调或获取系统分配的全局唯一 部署 ID (Deployment ID),可以监听返回的 Future:
vertx
.deployVerticle(new MyOrderProcessorVerticle())
.onComplete(res -> {
if (res.succeeded()) {
System.out.println("部署成功,Deployment ID: " + res.result());
} else {
System.out.println("部署失败: " + res.cause().getMessage());
}
});
后续如果需要卸载该 Verticle,就需要使用该部署 ID。
卸载与撤回 Verticle 部署
通过调用 undeploy(deploymentID) 可以卸载指定的 Verticle:
vertx
.undeploy(deploymentID)
.onComplete(res -> {
if (res.succeeded()) {
System.out.println("Verticle 已成功卸载");
} else {
System.out.println("卸载失败!");
}
});
指定 Verticle 部署实例数
在部署 Verticle 时,你可以一次性部署多个并发实例。你需要传入一个工厂函数 Callable<Deployable>,以便 Vert.x 为每个实例独立进行实例化:
DeploymentOptions options = new DeploymentOptions().setInstances(16);
vertx.deployVerticle(() -> new MyOrderProcessorVerticle(), options);
这在多核服务器上进行水平扩展时极其实用。例如你的服务器拥有 16 个核心,部署 16 个 Web 服务 Verticle 实例即可轻而易举地占满所有 CPU。
向 Verticle 传递配置参数
可以在部署时以 JSON 格式向 Verticle 注入业务配置参数:
JsonObject config = new JsonObject().put("name", "tim").put("directory", "/blah");
DeploymentOptions options = new DeploymentOptions().setConfig(config);
vertx.deployVerticle(new MyOrderProcessorVerticle(), options);
在 Verticle 内部可以通过 config() 方法方便地读取该配置:
System.out.println("获取配置项 name: " + config().getString("name"));
优雅退出 Vert.x
Vert.x 实例内部维护的工作线程不是守护线程(Daemon Threads),因此它们会阻止 JVM 进程直接退出。
如果你是以嵌入模式运行 Vert.x 且任务已经执行完毕,可以显式调用 close():
vertx.close();
该操作会安全停止所有内部线程池并释放各类资源,随后 JVM 进程将自然退出。
上下文对象 (Context)
当 Vert.x 向处理器派发事件,或调用某个 Verticle 的 start / stop 生命周期方法时,其执行过程总是绑定着一个 Context 上下文。通常该上下文是一个 Event Loop 上下文,与特定的 Event Loop 线程紧密绑定。在运行 Worker Verticle 或行内阻塞代码时,则会关联一个从工作线程池中分配的 Worker 上下文。
通过 getOrCreateContext() 可以获取当前的上下文:
Context context = vertx.getOrCreateContext();
if (context.isEventLoopContext()) {
System.out.println("当前运行在 Event Loop 上下文中");
} else if (context.isWorkerContext()) {
System.out.println("当前运行在 Worker 线程上下文中");
} else if (!Context.isOnVertxThread()) {
System.out.println("当前未运行在 Vert.x 管理的线程中");
}
在获取到 Context 对象后,你可以将任务异步提交回该上下文调度执行:
vertx.getOrCreateContext().runOnContext((v) -> {
System.out.println("该代码块将在相同的上下文中异步调度执行");
});
Context 对象还支持在同一上下文中流转共享键值对数据:
final Context context = vertx.getOrCreateContext();
context.put("data", "hello");
context.runOnContext((v) -> {
String hello = context.get("data");
});
执行周期性与延迟任务
在异步非阻塞的 Vert.x 应用中,你绝不能通过 Thread.sleep() 来实现延迟,因为这会直接卡死 Event Loop。
应当使用 Vert.x 提供的轻量级定时器。定时器分为单次触发 (One-shot) 与 周期性触发 (Periodic) 两种。
单次触发定时器 (One-shot Timers)
单次定时器在设定的毫秒延迟后触发一次事件处理器:
long timerID = vertx.setTimer(1000, id -> {
System.out.println("一秒之后打印此内容");
});
System.out.println("先打印此内容");
方法返回一个全局唯一的定时器 ID(long 类型),可用于在超时前取消定时器。
周期性定时器 (Periodic Timers)
使用 setPeriodic 设置周期性反复触发的定时器:
long timerID = vertx.setPeriodic(1000, id -> {
System.out.println("每隔一秒打印此内容");
});
请注意:如果周期性任务内部执行耗时较长,定时事件可能会接连堆积。在这种情况下,更好的做法是在单次任务执行完毕后,再动态设定下一次的 setTimer。
Future 形式的定时器 API
Vert.x 提供了将定时器直接作为 Future 使用的现代化 API:
Future<String> timer = vertx
.timer(10, TimeUnit.SECONDS)
.map(v -> "Success");
timer.onSuccess(value -> {
System.out.println("定时器触发: " + value);
});
timer.onFailure(cause -> {
System.out.println("定时器被取消: " + cause.getMessage());
});
在 Verticle 中自动清理定时器
如果你是在 Verticle 内部创建的定时器,当该 Verticle 被卸载时,Vert.x 会自动将它们全部注销并清理。
Verticle 专有 Worker 线程池
Verticle 使用 Worker 线程池来执行阻塞任务。你可以在部署时为特定的 Verticle 声明其专用的独立线程池:
vertx.deployVerticle(new MyOrderProcessorVerticle(), new DeploymentOptions().setWorkerPoolName("the-specific-pool"));
事件总线 (The Event Bus)
Event Bus 是 Vert.x 的神经中枢。
每一个 Vert.x 实例都有且仅有一个 Event Bus 单例对象,可以通过 eventBus() 方法获取。
Event Bus 允许应用程序的不同模块之间进行解耦通信,无论这些模块是由何种编程语言编写的,也无论它们是运行在同一个 Vert.x 实例内,还是分布在不同的 Vert.x 实例节点上。
它甚至可以通过桥接器(Bridge)扩展至前端浏览器,让浏览器中运行的客户端 JavaScript 能够在同一条 Event Bus 上收发消息。
由此,Event Bus 构成了一个横跨多个服务端节点与客户端浏览器的分布式点对点消息通信网络。
Event Bus 原生支持三种消息通信模式:发布/订阅 (Publish/Subscribe)、点对点 (Point-to-Point) 以及 请求/响应 (Request-Response)。
Event Bus 的 API 极其简洁直观,核心操作仅包含:注册处理器、注销处理器、发送消息以及发布消息。
下面先了解其核心设计理念:
核心概念与理论
地址寻址 (Addressing)
在 Event Bus 上传递消息时,消息会被发送至某个地址 (Address)。
Vert.x 没有设计繁杂花哨的寻址方案,在 Vert.x 中,地址本质上就是一个纯字符串。 任何非空字符串都是合法的地址。不过,通常推荐使用带命名空间的点分格式,例如使用小圆点划分业务层级。
一些常见且规范的地址示例如:europe.news.feed1、acme.games.pacman、orders.service 以及 user.events。
发布 / 订阅消息模式 (Publish / Subscribe)
Event Bus 原生支持发布 (Publish) 消息。
消息被发布到某个目标地址。发布意味着:该消息会被广播并投递给所有注册在该地址上的全部处理器。
这就是经典的发布/订阅 (Publish/Subscribe) 广播模式。
点对点与请求 / 响应模式 (Point-to-Point & Request-Response)
Event Bus 同样原生支持点对点 (Point-to-Point) 消息。
消息被发送(Send)到目标地址。Vert.x 会将该消息仅仅路由并投递给注册在该地址上的某一个处理器。
如果该地址上注册了多个处理器,Vert.x 会使用非严格轮询算法(Round-Robin)挑选其中一个处理器处理该消息。
在点对点通信中,发送方在发送消息时可以指定一个可选的应答处理器(Reply Handler)。
当接收方收到并处理完消息后,可以选择性地向发送方做出响应回复。如果做出了回复,发送方注册的应答处理器将被触发回调。
当发送方收到回复后,甚至可以再次进行应答。这一过程可以无限往复(Ad Infinitum),从而在两个不同的 Verticle 之间建立起双向对话通道。
这就是企业开发中广泛使用的请求/响应 (Request-Response) 模式。
尽力而为投递 (Best-effort delivery)
Vert.x 会竭尽全力投递消息,绝不会无故丢弃任何消息。这种机制被称为尽力而为(Best-effort)投递。
然而,在网络中断、机器宕机或集群部分故障的极端情况下,仍存在消息丢失的可能性。
如果你的业务场景对消息丢失极为敏感,应当将消息处理器设计为幂等(Idempotent)的,并让发送方在通信故障后具备重试机制。
消息数据类型 (Types of messages)
开箱即用状态下,Vert.x 允许发送任何 Java 基本数据类型、字符串或 Buffer 缓冲区。
不过,在 Vert.x 生态中,最通用的最佳实践是将消息承载为 JSON 格式。
JSON 在 Vert.x 支持的所有多语言体系(Java、Kotlin、Groovy 等)中都极易创建、读取与反序列化,已成为 Vert.x 社区事实上的“通用语言(Lingua Franca)”。
当然,如果你不想使用 JSON,Vert.x 从不强求。
Event Bus 具备极高的灵活性,还支持在总线上传递任意自定义对象。你只需为你想要传输的自定义类注册一个专用的 消息编解码器 (MessageCodec) 即可。
Event Bus API 指南
接下来让我们进入代码实践。
获取 Event Bus 实例
你可以通过以下方式获取 Event Bus 的引用:
EventBus eb = vertx.eventBus();
在同一个 Vert.x 实例内,Event Bus 是唯一的全局单例。
注册消息处理器 (Registering Handlers)
注册处理器最简单的方法是调用 consumer。
示例如下:
EventBus eb = vertx.eventBus();
eb.consumer("news.uk.sport", message -> {
System.out.println("收到订阅消息: " + message.body());
});
当目标地址有新消息到达时,你的处理器将被自动调用,并传入一个 Message 消息包装对象。
调用 consumer() 方法返回的对象是一个 MessageConsumer 实例。
该对象后续可用于注销该处理器,或者将处理器转换为响应式流(Stream)。
另外,你也可以先调用不带 handler 的 consumer() 方法获取一个 MessageConsumer,随后再为其绑定处理器:
EventBus eb = vertx.eventBus();
MessageConsumer<String> consumer = eb.consumer("news.uk.sport");
consumer.handler(message -> {
System.out.println("收到订阅消息: " + message.body());
});
在集群化 Event Bus 上注册处理器时,该注册信息的集群广播同步可能需要短暂的时间才能同步至所有节点。
如果你希望在全网注册同步完成时收到明确通知,可以使用 MessageConsumer 对象上的 completion future:
consumer.completion().onComplete(res -> {
if (res.succeeded()) {
System.out.println("处理器注册信息已成功同步至集群所有节点");
} else {
System.out.println("处理器注册失败!");
}
});
注销消息处理器 (Un-registering Handlers)
如需注销某个处理器,直接调用 unregister 即可。
在集群模式下,注销信息的网络广播同样需要短暂耗时。如果你需要等待注销全网生效,可以监听该方法返回的 Future:
consumer
.unregister()
.onComplete(res -> {
if (res.succeeded()) {
System.out.println("处理器注销操作已在集群全节点生效");
} else {
System.out.println("注销失败!");
}
});
发布广播消息 (Publishing messages)
广播发布消息极其简单。调用 publish 并指定广播地址即可:
eventBus.publish("news.uk.sport", "最新快讯:比赛开始了!");
该消息将被分发给所有注册在 news.uk.sport 地址上的监听处理器。
发送点对点消息 (Sending messages)
发送点对点消息时,目标地址上注册的多个处理器中将仅有一个会收到该消息。处理器挑选遵循非严格轮询机制。
使用 send 方法发送消息:
eventBus.send("news.uk.sport", "点对点任务消息");
设置消息头 (Setting headers on messages)
在 Event Bus 上传递的消息支持附加 Headers 元数据。你可以在发送或发布时通过传入 DeliveryOptions 进行指定:
DeliveryOptions options = new DeliveryOptions();
options.addHeader("trace-id", "abc-123");
eventBus.send("news.uk.sport", "附带元数据的消息", options);
消息保序性 (Message ordering)
对于来自同一个发送方的消息序列,Vert.x 能够保证目标处理器接收到的消息顺序与发送方的发送顺序严格一致。
应答确认与回复响应 (Acknowledging messages / sending replies)
当使用点对点 request() 或 send() 时,发送方往往希望知道消费者何时成功接收并“处理”了该消息,这就是经典的请求/响应 (Request-Response) 模式。
消费者在处理完业务逻辑后,可以调用 reply 进行应答确认。
调用该方法会将应答消息发送回原发送方,并触发发送方的应答处理器。
代码示例:
接收方(消费者):
MessageConsumer<String> consumer = eventBus.consumer("news.uk.sport");
consumer.handler(message -> {
System.out.println("收到请求: " + message.body());
// 回复响应给发送方
message.reply("处理成功,感谢通知!");
});
发送方:
eventBus
.request("news.uk.sport", "请求内容:查询实时比分")
.onComplete(ar -> {
if (ar.succeeded()) {
System.out.println("收到对端回复: " + ar.result().body());
}
});
回复同样可以包含正文对象。
所谓“处理完毕”的具体业务含义完全由应用程序自行定义,Vert.x 底层并不强加干涉。例如:
- 一个返回时间的查询服务,回复正文中包含当前的时间戳;
- 一个消息持久化队列,写入存储成功则回复 true,失败则回复 false;
- 一个订单处理组件,在订单落库校验完成后做出应答。
发送超时控制
在使用请求/响应模式发送消息时,你可以通过 DeliveryOptions 指定应答超时时间。
如果在规定时间内未收到接收方的回复,应答 Future 将自动以失败结束。默认超时时间为 30 秒:
DeliveryOptions options = new DeliveryOptions().setSendTimeout(5000); // 5 秒超时
eventBus.request("orders", "订单内容", options).onFailure(err -> {
System.err.println("请求处理超时或失败: " + err.getMessage());
});
发送失败类型 (Send Failures)
消息发送可能因多种原因失败,常见情况包括:
- 目标地址上当前没有任何可用的消息处理器;
- 接收方通过 fail(failureCode, message) 显式标记了处理失败。
在所有异常情况下,发送方的 Future 都会收到具体且包含错误码的失败原因。
自定义消息编解码器 (Message Codecs)
通过定义并注册自定义的 MessageCodec,你可以将任何自定义 Java 对象直接在 Event Bus 上收发。
每个编解码器都有一个唯一名称。在发送或发布时,可以通过 DeliveryOptions 指定使用该编解码器:
eventBus.registerCodec(myCodec);
DeliveryOptions options = new DeliveryOptions().setCodecName(myCodec.name());
eventBus.send("orders", new MyPOJO(), options);
如果你希望某种特定类型始终使用同一个编解码器,可以将其注册为默认编解码器,这样在每次发送时就无需重复指定:
eventBus.registerDefaultCodec(MyPOJO.class, myCodec);
// 直接发送,自动应用默认编解码器
eventBus.send("orders", new MyPOJO());
通过 unregisterCodec 可以注销某个编解码器。
编解码器并不要求编码前后的类型完全一致。例如,你可以编写一个编解码器,允许发送方传入 MyPOJO 对象,而在接收端将其转换为 MyOtherPOJO 对象交由处理器处理。
Vert.x 针对以下数据类型内置了开箱即用的编解码器:
- Java 基本类型(String、byte[]、byte、int、long、double、boolean、short、char);
- Vert.x 核心数据结构(Buffer、JsonObject、JsonArray);
- 实现了 ClusterSerializable 接口的高效序列化类型;
- 实现了标准 java.io.Serializable 接口的类。
|
在集群模式下,出于网络安全考量,默认会直接拒绝 你必须通过注册安全白名单检查函数,显式放行允许编解码的类名:
- |
集群化 Event Bus (Clustered Event Bus)
Event Bus 不仅存在于单个 Vert.x 实例内部。 通过在局域网内将多个 Vert.x 节点组建为集群,它们可以在逻辑上融合成一条全网统一的分布式 Event Bus。
以编程方式启动集群版 Vert.x 实例的示例如下:
VertxOptions options = new VertxOptions();
Vertx
.clusteredVertx(options)
.onComplete(res -> {
if (res.succeeded()) {
Vertx vertx = res.result();
EventBus eventBus = vertx.eventBus();
System.out.println("集群版 Event Bus 已成功就绪: " + eventBus);
} else {
System.out.println("集群启动失败: " + res.cause());
}
});
请确保你的项目类路径(Classpath)中已经引入了某个集群管理器实现库(如 vertx-hazelcast、vertx-infinispan 或 vertx-zookeeper)。
在 Verticle 中自动清理
如果你是在某个 Verticle 内部注册的 Event Bus 处理器,当该 Verticle 被卸载(Undeployed)时,Vert.x 会自动将这些处理器全部安全注销,无需手动清理。
配置 Event Bus
Event Bus 提供了丰富的配置选项,这在集群化部署时尤为关键。
在底层,集群版 Event Bus 采用 TCP 连接在各节点间收发数据,因此 EventBusOptions 提供了对这些底层 TCP 连接全方位的配置能力,其选项结构与 NetClientOptions 及 NetServerOptions 非常接近。
VertxOptions options = new VertxOptions()
.setEventBusOptions(new EventBusOptions()
.setSsl(true)
.setKeyCertOptions(new JksOptions().setPath("keystore.jks").setPassword("wibble"))
.setTrustOptions(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("基于 mTLS 双向认证的集群 Event Bus 已就绪: " + eventBus);
} else {
System.out.println("启动失败: " + res.cause());
}
});
上面的代码展示了如何使用 SSL/TLS 加密通道替代明文 TCP 连接来传输集群消息。
| 为确保集群环境下的绝对通信安全,你必须同时为集群管理器配置加密与安全认证。详情请参阅对应集群管理器的官方文档。 |
集群内所有节点的 Event Bus 配置必须保持一致。
EventBusOptions 还允许指定绑定的监听端口与主机地址。当部署在 Docker 容器或 Kubernetes Pod 中时,还可以配置外部映射的公开主机名与端口:
VertxOptions options = new VertxOptions()
.setEventBusOptions(new EventBusOptions()
.setClusterPublicHost("node1.internal.k8s")
.setClusterPublicPort(1234)
);
Vertx
.clusteredVertx(options)
.onComplete(res -> {
if (res.succeeded()) {
Vertx vertx = res.result();
EventBus eventBus = vertx.eventBus();
System.out.println("集群 Event Bus 启动成功: " + eventBus);
} else {
System.out.println("启动失败: " + 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 指针(Json Pointers)
Vert.x 提供了对 RFC6901 JSON 指针规范 的完整实现。
你可以同时使用指针进行数据查询和写入。你可以通过字符串、URI 或手动追加路径的方式构建 JsonPointer:
JsonPointer pointer1 = JsonPointer.from("/hello/world");
// 手动构建指针
JsonPointer pointer2 = JsonPointer.create()
.append("hello")
.append("world");
Object result1 = objectPointer.queryJson(jsonObject);
// 查询 JsonArray
Object result2 = arrayPointer.queryJson(jsonArray);
// 从 JsonObject 开始写入
objectPointer.writeJson(jsonObject, "new element");
// 从 JsonArray 开始写入
arrayPointer.writeJson(jsonArray, "new element");
你还可以通过提供自定义的 JsonPointerIterator 实现,将 Vert.x Json Pointer 应用到任意对象模型中。
Buffer 缓冲区
在 Vert.x 内部,绝大多数二进制数据与网络报文都是以 Buffer 为载体进行流转与交换的。
Buffer 是一个包含零个或多个字节的序列,支持自由读写,并且能够根据写入的数据量按需自动扩容。你可以将 Buffer 视为一个具备自适应动态扩容能力的“智能字节数组”。
创建 Buffer
可以通过静态工厂方法 Buffer.buffer 创建缓冲区。
Buffer 可以从纯文本字符串、原生 byte 数组初始化,也可以直接创建初始为空的空缓冲区。
创建示例:
创建空的初始 Buffer:
Buffer buff = Buffer.buffer();
从字符串创建 Buffer(默认使用 UTF-8 编码):
Buffer buff = Buffer.buffer("一些文本内容");
从字符串创建 Buffer 并显式指定编码字符集:
Buffer buff = Buffer.buffer("一些文本内容", "UTF-16");
从 byte[] 字节数组创建 Buffer:
byte[] bytes = new byte[] {1, 3, 5};
Buffer buff = Buffer.buffer(bytes);
预分配初始容量建议:如果你预先知道需要写入的数据大致容量,可以在创建时传入容量提示(Size Hint)。这样可以提前一次性分配足够的内存空间,避免随着数据追加导致多次底层内存重新分配和数据拷贝,性能更优。
请注意:以此方式创建的 Buffer 初始状态仍为空,它不会预先用零字节填充该空间。
Buffer buff = Buffer.buffer(10000);
向 Buffer 写入数据
向 Buffer 写入数据有两种常见方式:末尾追加 (Appending) 与 随机访问写入 (Random Access)。
无论采用哪种方式,Buffer 都会在容量不足时自动伸展扩容以容纳数据,你绝不会遇到数组越界的 IndexOutOfBoundsException 异常。
追加写入 (Appending)
使用 appendXXX 系列方法向缓冲区末尾追加数据。Vert.x 提供了针对各类基础数据类型的重载版本。
appendXXX 方法返回 Buffer 自身的引用,支持流式链式调用:
Buffer buff = Buffer.buffer();
buff.appendInt(123).appendString("hello\n");
socket.write(buff);
随机访问写入 (Random Access)
使用 setXXX 系列方法可以在指定的下标索引处写入数据。各方法接收的第一个参数为写入的起始下标。
若指定的下标超出了当前容量,Buffer 同样会自动透明扩容以确保数据安全写入:
Buffer buff = Buffer.buffer();
buff.setInt(1000, 123);
buff.setString(0, "hello");
从 Buffer 读取数据
使用 getXXX 系列方法从 Buffer 中读取数据。传入的首个参数为读取的起始偏移下标:
Buffer buff = Buffer.buffer();
for (int i = 0; i < buff.length(); i += 4) {
System.out.println("下标 " + i + " 处的 int 值为: " + buff.getInt(i));
}
处理无符号数 (Unsigned Numbers)
通过 getUnsignedXXX、appendUnsignedXXX 以及 setUnsignedXXX 系列方法,可以方便地读写无符号整型数值。这在实现追求极致网络带宽节省的高性能自定义二进制协议编解码器时尤为实用。
在以下示例中,数值 200 仅占用 1 个字节即可被写入指定位置:
Buffer buff = Buffer.buffer(128);
int pos = 15;
buff.setUnsignedByte(pos, (short) 200);
System.out.println(buff.getUnsignedByte(pos));
控制台将输出 200。
获取 Buffer 长度
调用 length() 获取当前缓冲区的有效字节长度。其值为缓冲区中包含的最大有效字节索引 + 1。
深度拷贝 Buffer
调用 copy() 可以创建并返回当前 Buffer 及其底层数据的深拷贝副本。
切片 Buffer (Slicing)
切片生成的 Buffer 视图与原 Buffer 共享同一份底层物理内存数据,不会触发任何额外的数据内存拷贝。
使用 slice() 创建切片视图。
Buffer 对象的复用约束
一旦将某个 Buffer 写入 Socket 或其他流式网络管道后,该 Buffer 实例就不能再被二次重复复用。
编写 TCP 服务端与客户端
Vert.x 允许你极其轻松地构建高性能、非阻塞的 TCP 客户端与服务端。
配置 TCP 服务端
如果不希望使用默认参数,可以在创建时传入一个 TcpServerConfig 配置对象:
TcpServerConfig config = new TcpServerConfig()
.setPort(4321);
NetServer server = vertx.createNetServer(config);
启动服务端监听
调用 listen 重载方法即可让服务端启动端口监听。
使用配置中指定的主机与端口启动监听:
NetServer server = vertx.createNetServer();
server.listen();
或者在调用 listen 时直接显式指定主机与端口(将覆盖配置对象中的设置):
NetServer server = vertx.createNetServer();
server.listen(1234, "localhost");
默认绑定的主机为 0.0.0.0(表示监听本地所有可用的网卡地址),默认端口为 0(这是一个特殊值,指示底层自动寻找一个空闲的未被占用的本地端口进行绑定)。
网络绑定的底层动作是异步的,因此调用 listen 方法返回后,服务端可能尚未真正完成绑定。
如果你需要在服务端真正启动监听并就绪时获得回调通知,可以监听返回的 Future:
NetServer server = vertx.createNetServer();
server
.listen(1234, "localhost")
.onComplete(res -> {
if (res.succeeded()) {
System.out.println("TCP 服务端已成功启动监听!");
} else {
System.out.println("端口绑定失败: " + res.cause().getMessage());
}
});
监听随机端口
如果指定端口为 0,操作系统会自动分配一个未使用的空闲端口。
如需获取服务端实际绑定的真实端口,可以调用 actualPort():
NetServer server = vertx.createNetServer();
server
.listen(0, "localhost")
.onComplete(res -> {
if (res.succeeded()) {
System.out.println("服务端已成功监听在动态端口: " + server.actualPort());
} else {
System.out.println("绑定失败!");
}
});
监听 Unix 域套接字 (Domain Socket)
在运行于 JDK 16+ 或启用了 本地原生网络传输层 的环境下,TCP 服务端支持直接监听 Unix 域套接字文件:
NetServer netServer = vertx.createNetServer();
// 仅在 JDK 16+ 或启用 Native Transport 时可用
SocketAddress address = SocketAddress.domainSocketAddress("/var/tmp/myservice.sock");
netServer
.connectHandler(so -> {
// 处理接入连接
})
.listen(address)
.onComplete(ar -> {
if (ar.succeeded()) {
// 成功绑定至 Unix Domain Socket
} else {
// 错误处理
}
});
监听新连接接入
通过注册 connectHandler 可以监听客户端连接的建立事件:
NetServer server = vertx.createNetServer();
server.connectHandler(socket -> {
// 在此处处理接入的 Socket 连接
});
每当有新的 TCP 连接接入时,该处理器将被调用,并传入对应的 NetSocket 实例。
NetSocket 是对真实网络连接的高层抽象,允许你读写数据以及执行关闭等操作。
从 Socket 读取数据
如需从 Socket 中读取数据,需在 socket 上注册 handler:
每当网络上有数据块到达时,该处理器将被自动回调,并传入包含二进制字节的 Buffer 对象:
NetServer server = vertx.createNetServer();
server.connectHandler(socket -> {
socket.handler(buffer -> {
System.out.println("成功接收数据,长度: " + buffer.length() + " 字节");
});
});
向 Socket 写入数据
通过调用 write 方法可以向连接发送数据:
Buffer buffer = Buffer.buffer().appendFloat(12.34f).appendInt(123);
socket.write(buffer);
// 以 UTF-8 编码写入字符串
socket.write("Hello World");
// 使用指定的字符集编码写入字符串
socket.write("Hello World", "UTF-16");
写入操作是异步非阻塞的,调用 write 返回后,底层操作系统可能在片刻后才真正完成网络数据包的排队与刷写。
连接关闭监听
如果希望在 Socket 连接断开时收到通知,可以注册 closeHandler:
socket.closeHandler(v -> {
System.out.println("TCP 连接已断开");
});
异常与错误处理
-
你可以在
NetSocket上注册exceptionHandler来捕获该连接生命周期中发生的任何网络异常。 -
你也可以在
NetServer上直接设置exceptionHandler,用于捕获连接正式交由connectHandler之前发生的异常(例如 TLS 握手协商失败)。
本地与远程地址查询
-
调用
localAddress()获取该连接的本地监听地址; -
调用
remoteAddress()获取对端客户端的远程网络地址。
发送文件或类路径资源
通过 sendFile 可以将本地磁盘文件或 Classpath 中的资源直接写入 Socket。在受支持的操作系统上,这会直接调用操作系统的零拷贝(Zero-Copy / sendfile 系统调用),执行效率极高:
socket
.sendFile("myfile.dat")
.onSuccess(v -> System.out.println("文件发送成功"))
.onFailure(err -> System.out.println("文件发送失败: " + err.getMessage()));
流式数据传输 (Streaming)
NetSocket 同时实现了 ReadStream 与 WriteStream 接口,因此可以直接利用 Vert.x 的流水线(Pipe)将网络流量导向或拉取自其他流式介质(如文件流、HTTP 流)。
关于流控与背压详情,请查阅 流式处理章节。
TCP 优雅关机 (Graceful Shutdown)
调用 shutdown 会启动停机过渡阶段,让服务端或客户端有机会在网络协议层处理清理动作:
server
.shutdown()
.onSuccess(res -> {
System.out.println("服务端已平滑停止");
});
优雅关机会等待现有全部 Socket 连接自然关闭或关机超时触发。超时触发时,所有未断开的连接将被强制切断。
每个开启的 Socket 都会收到一个 shutdown 事件通知,允许在底层真正断开前发送协议层的告警或帧:
socket.shutdownHandler(v -> {
socket
// 发送协议层关闭帧
.write(closeFrame())
// 等待接收对端的响应关闭帧
.compose(success -> closeFrameHandler(socket))
// 最终物理关闭套接字
.eventually(() -> socket.close());
});
没有注册 shutdownHandler 的套接字会被直接立即关闭。
默认关机超时时间为 30 秒,你可以自定义超时时长:
server
.shutdown(Duration.ofSeconds(60))
.onSuccess(res -> {
System.out.println("服务端已平滑停止");
});
强制立即关闭
调用 close() 方法会立刻物理断开全部活跃连接并释放系统资源。与 shutdown 不同,close 不存在缓冲宽限期:
server
.close()
.onSuccess(res -> {
System.out.println("服务端已立即关闭");
});
在 Verticle 中自动清理
如果你是在某个 Verticle 内部创建的 TCP 服务端或客户端,当该 Verticle 被卸载时,Vert.x 会自动将它们全部妥善关闭并回收资源。
水平伸缩:端口共享机制
单个 TCP 服务端的事件处理器总是运行在同一个 Event Loop 线程上。
如果你的宿主机拥有众多 CPU 核心,而你只部署了一个服务实例,那么最多只能利用其中一个 CPU 核心的算力。
为了榨干多核性能,你需要在代码中部署多个服务实例:
class MyVerticle extends VerticleBase {
NetServer server;
@Override
public Future<?> start() {
server = vertx.createNetServer();
server.connectHandler(socket -> {
socket.handler(buffer -> {
// 回显接收到的数据
socket.write(buffer);
});
});
return server.listen(1234, "localhost");
}
}
// 部署 10 个实例以充分利用多核 CPU
vertx.deployVerticle(MyVerticle.class, new DeploymentOptions().setInstances(10));
一旦这样部署,回显服务在功能上与之前完全一致,但所有的 CPU 核心都能被并发调动起来!
此时你可能会产生疑问:“多个服务实例监听相同的主机和端口,难道不会报端口冲突(Port Conflict)错误吗?”
这是 Vert.x 内部的巧妙魔法所在:
当你在同一个主机与端口上部署另一个服务实例时,Vert.x 底层并不会真正去创建多个独立的操作系统监听套接字。
相反,Vert.x 内部维护着一个共享的监听服务端。当外部 TCP 握手接入时,底层会自动采用轮询分发策略(Round-Robin)将接入的连接交错分配给不同实例的 connectHandler。
因此,Vert.x TCP 服务端天然能够在多核 CPU 之间线性格局扩展,同时每一个 Verticle 实例内部依然保持舒适单线程的简洁体验。
配置 TCP 客户端
通过 TcpClientConfig 可以自定义客户端参数:
TcpClientConfig config = new TcpClientConfig()
.setConnectTimeout(Duration.ofSeconds(10));
NetClient client = vertx.createNetClient(config);
发起网络连接
调用 connect 方法连接目标服务端,返回一个解析为 NetSocket 的 Future:
TcpClientConfig options = new TcpClientConfig()
.setConnectTimeout(Duration.ofSeconds(10));
NetClient client = vertx.createNetClient(options);
client
.connect(4321, "localhost")
.onComplete(res -> {
if (res.succeeded()) {
System.out.println("网络连接已建立!");
NetSocket socket = res.result();
} else {
System.out.println("连接失败: " + res.cause().getMessage());
}
});
发起 Unix 域套接字连接
在 JDK 16+ 或 Native Transport 环境下,客户端同样支持直连 Unix Domain Socket:
NetClient netClient = vertx.createNetClient();
SocketAddress addr = SocketAddress.domainSocketAddress("/var/tmp/myservice.sock");
netClient
.connect(addr)
.onComplete(ar -> {
if (ar.succeeded()) {
// 连接成功
} else {
// 错误处理
}
});
配置重连重试
你可以让客户端在初次建连失败时自动重试,通过 setReconnectInterval(重试间隔)和 setReconnectAttempts(重试次数)进行配置:
TcpClientConfig options = new TcpClientConfig()
.setReconnectAttempts(10)
.setReconnectInterval(Duration.ofMillis(500));
NetClient client = vertx.createNetClient(options);
| 当前重试策略仅在建立初始连接时生效;连接中途断开后的重连由上层应用逻辑自行编排。 |
网络流量日志监控
在排查网络通信疑难杂症时,可以开启网络报文日志:
TcpServerConfig options = new TcpServerConfig()
.setLogConfig(new LogConfig()
.setEnabled(true));
NetServer server = vertx.createNetServer(options);
控制台将输出精细的 Hex 进制网络抓包视图:
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
你也可以将输出格式精简为仅打印字节长度:
TcpServerConfig options = new TcpServerConfig()
.setLogConfig(new LogConfig()
.setEnabled(true)
.setDataFormat(ByteBufFormat.SIMPLE));
NetServer server = vertx.createNetServer(options);
TCP 流量整形与带宽限速
TCP 服务端可以通过 TrafficShapingOptions 对进出流量进行精细限速:
TcpServerConfig config = new TcpServerConfig()
.setHost("localhost")
.setPort(1234)
.setTrafficShapingOptions(new TrafficShapingOptions()
.setInboundGlobalBandwidth(64 * 1024) // 入站限速 64 KB/s
.setOutboundGlobalBandwidth(128 * 1024)); // 出站限速 128 KB/s
NetServer server = vertx.createNetServer(config);
限速策略支持在服务运行期间动态热更新:
TrafficShapingOptions update = new TrafficShapingOptions()
.setInboundGlobalBandwidth(2 * 64 * 1024) // 动态扩容至 128 KB/s
.setOutboundGlobalBandwidth(128 * 1024);
server
.listen(1234, "localhost")
.onSuccess(v -> server.updateTrafficShapingOptions(update));
配置 SSL/TLS 加密通信
TCP 客户端与服务端支持使用标准的 TLS 加密协议。开启 SSL/TLS 后,对上层应用的读写 API 没有任何破坏,完全透明兼容。
服务端启用 SSL/TLS
通过 ServerSSLOptions 配置密钥库证书,并在 TcpServerConfig 中将 ssl 设为 true:
ServerSSLOptions sslOptions = new ServerSSLOptions()
.setKeyCertOptions(
new JksOptions()
.setPath("/path/to/your/server-keystore.jks")
.setPassword("password-of-your-keystore")
);
TcpServerConfig config = new TcpServerConfig()
.setSsl(true);
NetServer server = vertx.createNetServer(config, sslOptions);
更多配置请参阅 SSL 服务端配置指南。
客户端启用 SSL/TLS
客户端可以通过 TcpClientConfig 全局配置 SSL,或在单次 ConnectOptions 建立连接时进行细粒度定制:
ClientSSLOptions sslOptions = new ClientSSLOptions()
.setTrustOptions(new JksOptions()
.setPath("/path/to/your/truststore.jks")
.setPassword("password-of-your-truststore")
);
TcpClientConfig config = new TcpClientConfig()
.setSsl(true);
NetClient client = vertx.createNetClient(config, sslOptions);
客户端 SNI 扩展 (Server Name Indication)
客户端在发起 TLS 握手时,默认会隐式将连接的主机名作为 SNI 发送。你也可以在建连时显式传递指定的服务器名称:
// 连接到 localhost,但在 TLS 握手时声明 server.name 域名证书
client
.connect(1234, "localhost", "server.name")
.onComplete(res -> {
if (res.succeeded()) {
System.out.println("SNI 连接成功!");
}
});
客户端主机名验证
为了防御 中间人攻击 (MITM),可以在客户端开启针对证书 CN 与 SAN 的严格校验:
-
""(空字符串):禁用主机名验证; -
"HTTPS":开启标准 HTTPS 风格的主机名校验算法; -
"LDAPS":开启 LDAP 规范的主机校验算法。
ClientSSLOptions sslOptions = new ClientSSLOptions()
.setTrustOptions(trustOptions)
.setHostnameVerificationAlgorithm("HTTPS");
NetClient client = vertx.createNetClient(config, sslOptions);
在线平滑升级至 SSL/TLS (StartTLS)
对于明文建立的 TCP 连接,支持在中途调用 upgradeToSsl 平滑升级为加密连接(即 StartTLS 机制):
socket.upgradeToSsl()
.onSuccess(v -> {
System.out.println("成功平滑升级至 TLS 加密连接");
});
运行时动态更新证书 (证书热轮转)
利用 updateSSLOptions 方法可以在不断开现有服务的前提下,动态更新服务端或客户端的证书密钥与受信任列表,从而优雅实现证书热更新轮转(Certificate Rotation):
Future<Boolean> fut = server.updateSSLOptions(new ServerSSLOptions()
.setKeyCertOptions(
new JksOptions()
.setPath("/path/to/new-server-keystore.jks")
.setPassword("new-password")));
当更新成功时将采用新的 SSL 配置,否则将保留先前的配置。
选项对象会与现有选项进行比较(使用 equals),以避免在对象相同时进行更新,因为加载选项可能开销较大。当对象相同时,你可以使用 force 参数强制执行更新。
|
吊销证书颁发机构(CRL 证书吊销列表)
你可以将信任库配置为使用证书吊销列表(Certificate Revocation List, CRL),以拒绝已吊销且不再受信任的证书。通过 crlPath 配置所使用的 CRL 列表文件路径:
ClientSSLOptions options = new ClientSSLOptions()
.setTrustOptions(trustOptions)
.addCrlPath("/path/to/your/crl.pem");
同样支持直接传入 Buffer 进行配置:
Buffer myCrlAsABuffer = vertx.fileSystem().readFileBlocking("/path/to/your/crl.pem");
ClientSSLOptions options = new ClientSSLOptions()
.setTrustOptions(trustOptions)
.addCrlValue(myCrlAsABuffer);
配置加密套件(Cipher suite)
默认情况下,TLS 配置会使用底层 SSL 引擎自带的默认加密套件列表:
-
当使用
JdkSSLEngineOptions时,使用 JDK SSL 引擎; -
当使用
OpenSSLEngineOptions时,使用 OpenSSL 引擎。
你可以通过添加启用的加密套件来显式定制:
ServerSSLOptions options = new ServerSSLOptions().
setKeyCertOptions(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");
一旦定义了启用的加密套件列表(即非空),它将完全覆盖 SSL 引擎原有的默认套件。
加密套件可以在 ServerSSLOptions 或 ClientSSLOptions 上进行配置。
配置 TLS 协议版本
默认情况下,TLS 默认配置会启用以下安全协议:TLSv1.2 和 TLSv1.3。你可以显式追加需要的协议版本:
ServerSSLOptions options = new ServerSSLOptions().
setKeyCertOptions(keyStoreOptions).
addEnabledSecureTransportProtocol("TLSv1.1");
也可以显式移除不需要的协议:
ServerSSLOptions options = new ServerSSLOptions().
setKeyCertOptions(keyStoreOptions).
removeEnabledSecureTransportProtocol("TLSv1.2");
协议版本可以在 ServerSSLOptions 或 ClientSSLOptions 上配置。
| TLS 1.0(TLSv1)和 TLS 1.1(TLSv1.1)已被行业广泛弃用,自 Vert.x 4.4.0 起默认处于禁用状态。 |
SSL 引擎(SSL engine)
你可以将底层引擎实现配置为使用 OpenSSL 代替 JDK 原生实现。 在 JDK 尚未针对 Java 8 中的 AES 以及 Java 9 中的 RSA 引入硬件级 CPU 指令加速(Hardware Intrinsics)之前,OpenSSL 的吞吐性能和 CPU 资源占用显著优于 JDK 引擎。
ServerSSLOptions sslOptions = new ServerSSLOptions()
.setKeyCertOptions(keyStoreOptions);
// 使用默认 JDK SSL 引擎
HttpServer server = vertx.createHttpServer(sslOptions);
// 显式指定使用 JDK SSL 引擎
server = vertx.httpServerBuilder()
.with(sslOptions)
.with(new JdkSSLEngineOptions())
.build();
// 使用 OpenSSL 引擎
server = vertx.httpServerBuilder()
.with(sslOptions)
.with(new OpenSSLEngineOptions())
.build();
后量子密码学(Post Quantum Cryptography / PQC)
量子计算机的崛起未来将使 x25519 等经典密钥交换协议失效,因为量子计算有能力快速破解传统密钥。 Vert.x 支持通过 X25519MLKEM768(NIST 官方推荐标准)实现后量子密码学(PQC)密钥交换,以确保 TLS 会话能够抵御未来量子计算机的攻击。
PQC 通过以下两个属性进行配置:
-
setKeyExchangeGroups:密钥交换算法组名称列表(例如["X25519MLKEM768", "X25519"]) -
setPqcEnforcementPolicy:强制策略,可选值包括RELAXED(宽松,默认值)、CLIENT_NEGOTIATED(客户端协商)或STRICT(严格)
这三种强制模式的行为如下:
-
RELAXED(宽松模式):按原样使用指定的密钥交换组;若未指定任何组,则 SSL 引擎执行常规协商。不对 PQC 做强制要求。
-
CLIENT_NEGOTIATED(客户端协商模式):服务端要求具备 PQC 支持(若组列表中不存在 X25519MLKEM768,会自动将其前置追加),但兼容容忍不支持 PQC 的客户端。
-
STRICT(严格模式):服务端与客户端都必须支持 PQC。密钥交换组会被完全替换为仅保留 X25519MLKEM768。不支持 PQC 的客户端将在 TLS 握手阶段直接失败。
对于 STRICT 和 CLIENT_NEGOTIATED 模式,若当前使用的 SSL 引擎不支持 PQC,应用程序将启动失败,并抛出错误 "PQC enforcement policy requires X25519MLKEM768 but the configured SSL engine does not support it" 或 "PQC enforcement policy requires X25519MLKEM768 but neither JDK nor OpenSSL support it"。
在 STRICT 和 CLIENT_NEGOTIATED 模式下,你需要支持 X25519MLKEM768 的 SslEngine。JdkSslEngine 从 JDK 27 开始支持(预览特性),而 OpenSSL 从 OpenSSL 3.5 起提供支持。若用户未指定 SslEngine,系统将按顺序自动选择第一个支持 PQC 的引擎(优先从 JdkSslEngine 开始尝试)。
使用 OpenSSL >= 3.5 需要引入以下依赖:
-
io.netty:netty-tcnative-classes(版本由 Netty BOM 统一管理) -
OpenSSL 提供程序,例如
io.smallrye:smallrye-openssl
PQC 强制策略行为对照表:
| pqc-enforcement-policy | server-pqc-supports | client-pqc-supports | 结果 |
|---|---|---|---|
strict |
false |
required / true / false |
应用启动失败,报错 "PQC enforcement policy STRICT requires X25519MLKEM768 but the configured SSL engine does not support it" |
true |
true / required |
应用正常启动且 SSL 握手成功 |
|
true |
false |
应用正常启动但 SSL 握手失败 |
|
client_negotiated |
false |
required / true / false |
应用启动失败,报错 "PQC enforcement policy STRICT requires X25519MLKEM768 but the configured SSL engine does not support it" |
true |
required / true / false |
应用正常启动且 SSL 握手成功 |
|
relaxed |
true |
required / true / false |
应用正常启动且 SSL 握手成功 |
false |
true / false |
应用正常启动且 SSL 握手成功 |
|
false |
required |
应用正常启动但 SSL 握手失败 |
客户端代理支持
NetClient 原生支持 HTTP/1.x CONNECT、SOCKS4a 或 SOCKS5 代理。
TcpClientConfig config = new TcpClientConfig()
.setProxyOptions(new ProxyOptions().setType(ProxyType.SOCKS5)
.setHost("localhost").setPort(1080)
.setUsername("username").setPassword("secret"));
NetClient client = vertx.createNetClient(config);
HAProxy Protocol 协议支持
HAProxy Protocol 提供了一种在穿透多层 NAT 或 TCP 代理时,安全透传对端真实客户端真实 IP 的标准化机制。
开启 setUseProxyProtocol(true) 即可自动解析前端代理上报的真实 IP:
TcpServerConfig config = new TcpServerConfig().setUseProxyProtocol(true);
NetServer server = vertx.createNetServer(config);
server.connectHandler(so -> {
// 打印由 HAProxy Protocol 传递的真实对端客户端地址
System.out.println("真实客户端 IP: " + so.remoteAddress());
// 打印反向代理服务器自身的连接地址
System.out.println("代理节点 IP: " + so.localAddress());
});
编写 HTTP 服务端与客户端
Vert.x 允许你轻松编写高性能的非阻塞 HTTP 客户端与服务端。
Vert.x 完整支持 HTTP/1.0、HTTP/1.1、HTTP/2 以及 HTTP/3 协议。
无论对于 HTTP/1.x、HTTP/2 还是 HTTP/3,核心的基础 API 设计高度保持一致,同时为处理各协议特有的专有机制提供了针对性的 API 扩展。
创建 HTTP 服务端
使用默认配置创建 HTTP 服务端的最简方式如下:
HttpServer server = vertx.createHttpServer();
默认情况下,该服务端支持明文传输的 HTTP/1.1、HTTP/2 以及 WebSocket。
配置 HTTP 服务端
如果你需要对默认行为进行定制,可以在创建服务端时传入一个 HttpServerConfig 实例:
HttpServerConfig config = new HttpServerConfig()
.setVersions(HttpVersion.HTTP_1_1)
.setFormDecoderConfig(new FormDecoderConfig()
.setMaxFields(512))
.setHttp1Config(new Http1ServerConfig()
.setMaxInitialLineLength(1024))
.setCompressionConfig(new CompressionConfig()
.setCompressionEnabled(true)
.addGzip());
HttpServer server = vertx.createHttpServer(config);
配置选项覆盖了以下几个主要方面:
-
允许接受的协议版本
-
超时时间与安全阈值限制
-
HTTP/1.x 专属配置
-
HTTP/2 专属配置
-
HTTP/3 专属配置
-
WebSocket
-
TLS 安全配置
-
传输层:TCP 与 QUIC
-
内容压缩
-
可观测性(Metrics/Tracing)
-
网络活动日志
配置 HTTPS 服务端
Vert.x HTTP 服务端配置使用 HTTPS 的方式,与 TCP 或 QUIC 服务端完全相同。
ServerSSLOptions sslOptions = new ServerSSLOptions()
.setKeyCertOptions(
new JksOptions().
setPath("/path/to/your/server-keystore.jks").
setPassword("password-of-your-keystore")
);
HttpServer server = vertx.createHttpServer(sslOptions);
在 HttpServerConfig 上不再像旧版 HttpServerOptions 那样提供单独的 ssl 开关属性,创建服务端时只要传入 SSL 选项,就表明该服务端启用 TLS。
|
你可以参阅相关章节了解更多关于 SSL 服务端配置 的细节。
配置 HTTP/1.x 服务端
Vert.x 支持在明文与 TLS 链路上运行 HTTP/1.1 和 HTTP/1.0。
HttpServerConfig config = new HttpServerConfig()
.setHttp1Config(new Http1ServerConfig()
.setMaxInitialLineLength(1024));
Http1ServerConfig 负责封装 HTTP/1.x 特有的配置参数。
配置 HTTP/2 服务端
Vert.x 同时支持基于 TLS 的 HTTP/2(h2)以及基于明文 TCP 的 HTTP/2(h2c)。
-
h2:表示在 TLS 加密链路上运行的 HTTP/2 协议。 -
h2c:表示在 TCP 明文链路上运行的 HTTP/2 协议;此类连接可以通过 HTTP/1.1 Upgrade 升级请求建立,也可以直接通过协议前导建立。
要处理 h2 请求,必须启用 TLS:
HttpServerConfig config = new HttpServerConfig()
.setHttp2Config(new Http2ServerConfig()
.setInitialSettings(new Http2Settings()
.setMaxConcurrentStreams(250)));
在明文模式下(未启用 TLS),服务端支持通过发送 HTTP/1.1 Upgrade 升级请求切换至 HTTP/2 的 h2c 模式。它同样接受客户端通过预置知识(Prior Knowledge)以 PRI * HTTP/2.0\r\nSM\r\n 协议序言直接发起的 h2c 连接。
目前主流浏览器均不支持 h2c。若要对外提供网站 Web 访问服务,请务必使用 h2(启用 TLS)而非 h2c。
|
Http2ServerConfig 用于配置 HTTP/2 专属属性。
当服务端接受 HTTP/2 连接时,会向客户端发送其 初始设置(Initial Settings)。这些设置定义了客户端使用该连接的边界规则,服务端的默认初始设置为:
-
getMaxConcurrentStreams:100(符合 HTTP/2 RFC 推荐标准)。 -
其余设置均遵循 HTTP/2 规范的默认值。
配置 HTTP/3 服务端
Vert.x 支持在 QUIC 协议之上运行 HTTP/3,我们建议先阅读 QUIC 服务端章节。
QUIC 是 HTTP 的新一代传输层协议,它取代了 TCP 并原生内嵌 TLS 1.3(在 QUIC 中 TLS 握手是强制的)。
QUIC 的底层实现依托于 Netty 和 Cloudflare 的 QUICHE 库,需要根据操作系统和平台架构引入本地原生库依赖,详情请阅读 原生 QUIC 依赖。
HttpServerConfig config = new HttpServerConfig()
.setVersions(HttpVersion.HTTP_3);
Http3ServerConfig 负责封装 HTTP/3 的特有配置。
QUIC 在传输层实现了 HTTP/2 的部分核心特性,例如控制单个连接能够承载的最大并发流数量:
HttpServerConfig config = new HttpServerConfig()
.setVersions(HttpVersion.HTTP_3);
QuicServerConfig quicConfig = config.getQuicConfig();
quicConfig.setTransportConfig(new QuicConfig()
.setInitialMaxStreamsBidi(250));
QUIC 服务端可能会面临 UDP 放大攻击。
默认情况下,服务端仅启用了基础的 Token 校验,这非常适合本地开发调试;但在生产环境中,我们强烈建议配置基于 密码学密钥 的安全地址校验。
混合 HTTP 服务端(Hybrid HTTP Server)
同一个服务端实例可以同时处理 TCP 流量(HTTP/1.x 和 HTTP/2)以及 QUIC 流量(HTTP/3):
HttpServerConfig config = new HttpServerConfig()
.setVersions(HttpVersion.HTTP_1_1, HttpVersion.HTTP_2, HttpVersion.HTTP_3);
ServerSSLOptions sslOptions = new ServerSSLOptions()
.setKeyCertOptions(new JksOptions().setPath("/path/to/my/keystore"));
HttpServer server = vertx.createHttpServer(config, sslOptions);
混合服务端在启动时会同时绑定两个物理端口:
-
一个用于处理 HTTP/1.x 和/或 HTTP/2 流量的 TCP 端口
-
一个用于处理 HTTP/3 流量的 QUIC 端口(基于 UDP)
两个端口可以分别独立配置:
HttpServerConfig config = new HttpServerConfig()
.setVersions(HttpVersion.HTTP_1_1, HttpVersion.HTTP_2, HttpVersion.HTTP_3)
.setTcpPort(tcpPort)
.setQuicPort(quicPort);
也可以使用同一个端口号(TCP 与 UDP 端口号在操作系统网络层彼此独立互不冲突):
HttpServerConfig config = new HttpServerConfig()
.setVersions(HttpVersion.HTTP_1_1, HttpVersion.HTTP_2, HttpVersion.HTTP_3)
.setPort(port);
除非 DNS 域名解析配置直接依赖指向 HTTP/3 端点的 HTTPS 资源记录(HTTPS RR),否则 HTTP 服务端向客户端通告 HTTP/3 端点的推荐方式是发送符合 RFC 7838 HTTP 替代服务(Alt-Svc) 规范的通告:
response.writeAltSvc("h3=\":443\"");
调用 writeAltSvc 会自动完成以下操作:
-
对于 HTTP/1.1 请求,写入
Alt-SvcHTTP 响应头 -
对于 HTTP/2 请求,发送一个 ALTSVC 扩展帧
高级 HTTP 服务端构建(Builder API)
你可以向 createHttpServer 传入配置选项来构建 HTTP 服务端。
另外,你还可以使用构建器风格的 HttpServerBuilder API:
HttpServer server = vertx
.httpServerBuilder()
.with(config)
.build();
除了支持配置 HttpServerConfig 和 ServerSSLOptions 外,你还可以:
-
注册连接事件处理器,在客户端与此服务端建立 网络连接 时接收通知
-
配置
SSLEngineOptions定制底层 SSL 引擎
启动服务端监听
要让服务端开始监听传入请求,可以使用各种重载形式的 listen 方法。
在配置中预设的主机名和端口上启动监听:
HttpServer server = vertx.createHttpServer();
server.listen();
或者在调用 listen 时显式传入主机与端口,忽略配置中的预设值:
HttpServer server = vertx.createHttpServer();
server.listen(8080, "myhost.com");
默认主机为 0.0.0.0(表示监听所有本地网卡地址),默认端口为 80。
实际的端口绑定是异步执行的,因此在 listen 方法返回*之后*的一小段时间内,服务端可能尚未真正就绪。
如果你希望在服务端真正开始监听时接收回调通知,可以向 listen 传入处理器:
HttpServer server = vertx.createHttpServer();
server
.listen(8080, "myhost.com")
.onComplete(res -> {
if (res.succeeded()) {
System.out.println("服务端已成功开始监听!");
} else {
System.out.println("端口绑定失败!");
}
});
监听 Unix 域套接字(Unix domain sockets)
当运行在 JDK 16+ 或启用了 本地原生传输(Native Transport) 时,HTTP 服务端支持监听 Unix 域套接字(Domain Socket):
HttpServer httpServer = vertx.createHttpServer();
// 仅在 JDK 16+ 或使用原生传输时可用
SocketAddress address = SocketAddress.domainSocketAddress("/var/tmp/myservice.sock");
httpServer
.requestHandler(req -> {
// 处理应用程序业务逻辑
})
.listen(address)
.onComplete(ar -> {
if (ar.succeeded()) {
// 成功绑定到 Unix 域套接字
} else {
// 错误处理
}
});
接收接入请求通知
要监听接入的 HTTP 请求,你需要注册一个 requestHandler:
HttpServer server = vertx.createHttpServer();
server.requestHandler(request -> {
// 在此处处理到达的 HTTP 请求
});
处理 HTTP 请求
当新请求到达时,请求处理器被调用并传入一个 HttpServerRequest 实例。该对象代表了服务端的 HTTP 请求。
当请求的全部 HTTP 请求头(Headers)被完整读取完毕后,该处理器就会被立即调用。
如果该请求包含请求体(Body),请求体数据将在请求处理器被调用之后的后续时刻陆续到达服务端。
每个服务端请求对象都天然关联着一个对应的服务端响应对象。你可以调用
response 方法获取该 HttpServerResponse 响应对象的引用。
以下是一个处理请求并向客户端回复 "hello world" 的简明示例:
vertx
.createHttpServer()
.requestHandler(request -> {
request.response().end("Hello world");
})
.listen(8080);
请求版本(Request version)
请求中指定的 HTTP 协议版本可以通过 version 获取。
请求方法(Request method)
使用 method 获取请求的 HTTP 方法(如 GET、POST、PUT、DELETE、HEAD、OPTIONS 等)。
请求 URI
使用 uri 获取请求的原始 URI。
请注意:这是 HTTP 请求中实际传递的原始 URI,在绝大多数情况下它是一个相对 URI。
该 URI 的规范定义参见 RFC 2616 HTTP 规范第 5.1.2 节 - Request-URI。
请求路径(Request path)
使用 path 提取 URI 中的路径部分。
例如,若请求 URI 为 /a/b/c/page.html?param1=abc¶m2=xyz:
则请求路径为 /a/b/c/page.html。
请求查询字符串(Request query string)
使用 query 提取 URI 中的查询字符串部分。
例如,若请求 URI 为 /a/b/c/page.html?param1=abc¶m2=xyz:
则查询字符串为 param1=abc¶m2=xyz。
请求头(Request headers)
使用 headers 获取 HTTP 请求头。
它返回一个 MultiMap 实例——其行为类似于常规 Map,但允许同一个键拥有多个值(因为 HTTP 协议允许同一个 Header 存在多条记录)。
此外,MultiMap 的键是大小写不敏感的,这意味着以下两种写法完全等价:
MultiMap headers = request.headers();
// 获取 User-Agent 头:
System.out.println("User agent is " + headers.get("user-agent"));
// 这样获取结果完全相同:
System.out.println("User agent is " + headers.get("User-Agent"));
请求 Authority 权限
使用 authority 获取 HTTP 请求的 Authority 信息。
对于 HTTP/1.x 请求,返回的是 Host 请求头;对于 HTTP/2 和 HTTP/3 请求,返回的是 :authority 伪首部(Pseudo Header)。
请求参数(Request parameters)
使用 params 获取 HTTP 请求参数。
请求参数出现在请求 URI 路径之后的查询串中。例如 URI 为 /page.html?param1=abc¶m2=xyz:
参数 MultiMap 将包含:
param1: abc param2: xyz
请注意:这里的请求参数是从请求 URL 中提取的。如果你通过 multipart/form-data 的请求体提交 HTML 表单属性,这些表单项不会出现在这里的 URL 请求参数中。
远端网络地址(Remote address)
可以通过 remoteAddress 获取请求发起方的网络套接字地址。
绝对 URI(Absolute URI)
HTTP 请求中传递的 URI 通常是相对路径。如果你需要获取对应于该请求的完整绝对 URI,可以调用 absoluteURI。
请求结束监听(End handler)
请求的 endHandler 会在整个请求(包括全部请求体数据)被完整读取完毕时被调用。
==== 从请求体中读取数据(Reading Data from the Request Body)
HTTP 请求通常携带着需要读取的请求体(Body)。如前所述,当请求的 Header 刚到达时,请求处理器就会被立即触发,因此此时请求对象中尚未包含请求体内容。
这是因为请求体可能非常庞大(例如用户上传的大文件),出于性能和防暴内存考虑,Vert.x 绝不会在把请求交给你的业务代码之前盲目地把整个请求体缓存在内存中,否则极易耗尽系统可用内存。
要以流式方式接收请求体,你可以在请求对象上注册 handler 数据处理器。每当请求体的一个分块到达时,该处理器就会被回调:
request.handler(buffer -> {
System.out.println("收到一块请求体数据,长度为:" + buffer.length());
});
传递给处理器的对象是一个 Buffer 实例。随着网络数据不断到达,该处理器会被多次调用,具体次数取决于请求体的大小。
在很多业务场景下(例如请求体很小,如普通的 JSON 载荷),你希望在内存中直接聚合完整的请求体,你可以手动进行累加:
Buffer totalBuffer = Buffer.buffer();
request.handler(buffer -> {
System.out.println("收到一块请求体数据,长度为:" + buffer.length());
totalBuffer.appendBuffer(buffer);
});
request.endHandler(v -> {
System.out.println("请求体已完整接收,总长度为:" + totalBuffer.length());
});
这种场景非常普遍,因此 Vert.x 提供了开箱即用的 bodyHandler 自动为你完成内存聚合。当全部请求体数据接收完毕后,该处理器会被统一回调一次:
request.bodyHandler(totalBuffer -> {
System.out.println("请求体已完整接收,总长度为:" + totalBuffer.length());
});
流式请求处理(Streaming requests)
请求对象本身实现了 ReadStream 接口,因此你可以通过管道(Pipe)将请求体无缝传输到任何 WriteStream 实例中并自动享受背压控制。
详细说明请参阅 流与背压控制 章节。
处理 HTML 表单(Handling HTML forms)
HTML 表单可以通过 application/x-www-form-urlencoded 或 multipart/form-data 两种 Content-Type 进行提交。
对于 URL 编码的表单(x-www-form-urlencoded),表单属性直接编码在 URL 中,提取方式与普通 Query 查询参数完全相同。
对于多部分表单(multipart/form-data),表单属性被编码在请求体中,因此只有在整个请求体被完全读取后才能解析获取。
多部分表单中还可以包含文件上传。
如果你希望提取 multipart 表单的属性,你必须在读取任何请求体数据*之前*,通过调用 setExpectMultipart(true) 告知 Vert.x 预期接收此类表单;随后在整个请求体读取完成时,调用 formAttributes 提取表单属性:
server.requestHandler(request -> {
request.setExpectMultipart(true);
request.endHandler(v -> {
// 此时请求体已完整读取,提取表单属性
MultiMap formAttributes = request.formAttributes();
});
});
表单属性默认的最大允许大小为 8192 字节。若客户端提交的单个表单属性超出了此限制,文件上传将在 HttpServerRequest 的异常处理器中触发异常。你可以通过 setMaxAttributeSize 调整此上限:
HttpServerConfig config = new HttpServerConfig()
.setFormDecoderConfig(
new FormDecoderConfig().setMaxAttributeSize(maxAttrSize));
处理表单文件上传(Handling form file uploads)
Vert.x 同样能够处理编码在 multipart 请求体中的文件上传。
要接收上传的文件,你需要告知 Vert.x 期望接收 multipart 表单,并在请求上注册 uploadHandler。
每当服务端检测到一个新上传的文件到达时,该处理器就会被回调一次。
传递给处理器的对象是一个 HttpServerFileUpload 实例:
server.requestHandler(request -> {
request.setExpectMultipart(true);
request.uploadHandler(upload -> {
System.out.println("正在处理文件上传:" + upload.name());
});
});
上传的文件可能非常庞大,因此 Vert.x 不会在单一 Buffer 中提供完整的文件内容以避免内存溢出,而是将文件数据按分块(Chunk)流式递送:
request.uploadHandler(upload -> {
upload.handler(chunk -> {
System.out.println("收到文件分块,长度为:" + chunk.length());
});
});
Upload 上传对象本身实现了 ReadStream,因此你可以使用管道将其直接转存到任何 WriteStream 中。
如果你只是想把文件保存到本地磁盘上的某个路径,可以直接调用便捷方法 streamToFileSystem:
request.uploadHandler(upload -> {
upload.streamToFileSystem("myuploads_directory/" + upload.filename());
});
| 在生产环境中,请务必严格校验客户端上传的文件名(filename),防止恶意客户端利用路径穿越攻击将文件写入文件系统的任意受限敏感目录。 |
处理 Cookie
要删除 Cookie,调用 removeCookie。
要添加/设置 Cookie,调用 addCookie。
在发送响应头时,配置的 Cookie 集合会自动写入响应中,以便客户端浏览器保存。
Cookie 由 Cookie 类进行抽象建模,允许你读取或设置名称、值、域名(Domain)、路径(Path)以及其他标准的 Cookie 属性。
SameSite 属性允许服务端要求浏览器不要在跨站请求中发送特定 Cookie,从而有效防御跨站请求伪造(CSRF)攻击。可以通过 setSameSite 进行配置。
SameSite 可选值包括:
-
None:浏览器在跨站请求和同站请求中均会发送 Cookie。 -
Strict:浏览器仅在同站请求中发送 Cookie(请求必须源自设置该 Cookie 的同一站点)。若导航来源 URL 与目标地址不同,标记为 Strict 的 Cookie 绝不会被携带。 -
Lax:在跨站子请求中(例如加载第三方图片或 iframe)不发送 Cookie;但在用户从外部站点直接点击链接导航至本站时,仍会正常发送该 Cookie。
以下是查询和添加 Cookie 的代码示例:
Cookie someCookie = request.getCookie("mycookie");
String cookieValue = someCookie.getValue();
// 处理 Cookie 业务逻辑...
// 添加新 Cookie —— 在响应头发送时将自动回写给客户端
request.response().addCookie(Cookie.cookie("othercookie", "somevalue"));
处理压缩请求体(Handling compressed body)
Vert.x 能够自动处理客户端使用 deflate、gzip、snappy 或 brotli 算法压缩编码的请求体载荷。
要开启请求体自动解压缩,请在创建服务端时在压缩配置中将 setDecompressionEnabled 设为 true。
Snappy 解压原生支持,无需外部依赖。
解压缩 Brotli 需要类路径中存在 Brotli4j 依赖,解压缩 Zstandard 则需要 Zstd-jni 依赖:
-
Maven(在你的
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(在你的
build.gradle文件中引入):
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}'
}
使用 Gradle 时,你需要根据目标操作系统和 CPU 架构手动引入 runtime 原生动态库。详情参见 Brotli4j 的 Gradle 配置说明。
默认情况下,自动解压缩处于禁用状态。
接收自定义控制帧(Receiving custom frames)
HTTP/2 和 HTTP/3 是基于二进制分帧的协议,除了标准的请求/响应分帧模型外,协议规范还允许发送或接收其他自定义类型的帧。
要接收自定义帧,可以在请求上注册 customFrameHandler。每当有自定义帧到达时该处理器都会被回调:
request.customFrameHandler(frame -> {
System.out.println("收到帧类型=" + frame.type() +
" 载荷内容=" + frame.payload().toString());
});
自定义帧不受常规流控限制——无论当前的数据流处于何种流控状态,一旦收到自定义帧,帧处理器就会被立即调用。
发送 HTTP 响应(Sending back responses)
服务端响应对象是 HttpServerResponse 的实例,可以通过请求对象的 response 方法获取。
你可以使用该响应对象向 HTTP 客户端回写数据。
设置状态码与状态消息
HTTP 响应的默认状态码为 200(表示 OK)。
调用 setStatusCode 设置不同的状态码。
你还可以通过 setStatusMessage 指定自定义的状态消息。
若未指定状态消息,系统将自动使用状态码对应的标准文本。
| 对于 HTTP/2 和 HTTP/3,响应中不再包含文本状态消息,因为协议二进制规范仅传输数字状态码。 |
写入 HTTP 响应体
要向 HTTP 响应写入数据,可以使用各类重载的 write 方法。
在响应结束之前,这些方法可以被多次调用:
写入单个 Buffer:
HttpServerResponse response = request.response();
response.write(buffer);
写入字符串(默认使用 UTF-8 编码并发送到底层网络):
HttpServerResponse response = request.response();
response.write("hello world!");
使用指定字符集编码写入字符串:
HttpServerResponse response = request.response();
response.write("hello world!", "UTF-16");
向响应写入数据是完全异步的,一旦数据进入内部写入队列,方法就会立即返回。
如果你只需要向响应写入单段字符串或单个 Buffer,可以直接调用重载的 end 在一次调用中完成写入并关闭响应。
第一次调用 write 会触发响应头被物理写入网络。因此,若未启用分块传输(Chunked),你必须在调用 write 之前先显式设置好 Content-Length 响应头;若启用了分块传输,则无需提前设置 Content-Length。
结束 HTTP 响应
当你完成响应写入后,必须调用 end 结束该响应。
结束响应有多种方式:
不带任何参数直接结束响应:
HttpServerResponse response = request.response();
response.write("hello world!");
response.end();
或者在结束时直接传入最终的字符串或 Buffer:
HttpServerResponse response = request.response();
response.end("hello world!");
设置响应头(Setting response headers)
可以通过 headers 获取响应头 MultiMap 并直接操作:
HttpServerResponse response = request.response();
MultiMap headers = response.headers();
headers.set("content-type", "text/html");
headers.set("other-header", "wibble");
或者使用支持链式调用的便捷方法 putHeader:
HttpServerResponse response = request.response();
response
.putHeader("content-type", "text/html")
.putHeader("other-header", "wibble");
注意:响应头必须在向响应体写入任何数据之前设置完毕,否则请求头已发送,设置将不再生效。
分块传输与 Trailer 尾部头(Chunked HTTP responses and trailers)
Vert.x 支持 HTTP 分块传输编码(Chunked Transfer Encoding)。该模式允许响应体以分块形式陆续发送,而无需在发送前预先计算并指定 Content-Length。
通过 setChunked(true) 启用分块模式:
HttpServerResponse response = request.response();
response.setChunked(true);
默认情况下,分块传输未启用。当启用后,每次调用 write 都会作为独立的 HTTP 分块写入网络。
在分块模式下,你还可以在响应末尾写入 Trailer 尾部头(在响应体彻底结束之后发送的 HTTP 标头):
HttpServerResponse response = request.response();
response.setChunked(true);
MultiMap trailers = response.trailers();
trailers.set("X-Checksum", "1294823490");
或者使用便捷的链式调用方法 putTrailer:
HttpServerResponse response = request.response();
response.setChunked(true);
response.putTrailer("X-Checksum", "1294823490");
直接从磁盘或 Classpath 提供静态文件
如果你正在开发 Web 服务端并需要向客户端提供磁盘或类路径中的静态文件,可以直接调用便捷方法 sendFile。
该方法极其高效,在底层操作系统支持时会自动利用操作系统的零拷贝(Zero-Copy)技术(例如 Linux 上的 sendfile 系统调用),直接将磁盘文件块从内核态传输到网卡,完全绕过用户态内存拷贝:
HttpServerResponse response = request.response();
response.sendFile("webroot/index.html");
该方法还支持指定发送文件的字节偏移量与长度,非常适合实现大文件的断点续传(HTTP Range 请求):
HttpServerResponse response = request.response();
// 发送从 offset 开始的 length 个字节
response.sendFile("webroot/largefile.dat", offset, length);
有关从 Classpath 提供文件的规则及缓存机制,请参阅 Classpath 资源加载说明。
响应管道传输(Piping responses)
由于 HttpServerResponse 实现了 WriteStream 接口,你可以使用管道(Pipe)将任何 ReadStream(例如 AsyncFile、NetSocket 或另一个 HTTP 客户端响应)管道连接到该响应上:
AsyncFile file = vertx.fileSystem().openBlocking("myfile.dat", new OpenOptions());
file.pipeTo(response);
管道会自动处理数据流速差异并施加背压。
发送自定义控制帧(Sending custom frames)
在 HTTP/2 和 HTTP/3 模式下,你可以向客户端发送自定义扩展帧:
HttpServerResponse response = request.response();
response.writeCustomFrame(10, 0, Buffer.buffer("payload"));
流取消与重置(Stream cancellation)
在 HTTP/2 和 HTTP/3 中,服务端可以重置取消正在传输的流。调用 reset 发送 RST_STREAM 帧并终止该流:
response.reset(8); // 8 代表 HTTP/2 CANCEL 错误码
处理异常(Handling exceptions)
若在向响应写回数据或处理请求的过程中发生异常,可以在响应对象上注册 exceptionHandler 捕获异常:
response.exceptionHandler(err -> {
System.err.println("响应传输中发生错误:" + err.getMessage());
});
处理非法 HTTP 请求(Handling invalid requests)
当客户端发送了不合规范的畸形 HTTP 请求报文时,服务端可以通过 invalidRequestHandler 进行针对性捕获和定制处理:
server.invalidRequestHandler(request -> {
System.out.println("收到畸形非法请求,版本:" + request.version());
request.response().setStatusCode(400).end();
});
HTTP 压缩机制(HTTP Compression)
Vert.x 支持在向客户端发送响应体之前自动对其进行内容压缩(Content Compression)。
这可以通过在创建服务端时配置 CompressionConfig 来启用:
HttpServerConfig config = new HttpServerConfig()
.setCompressionConfig(new CompressionConfig()
.setCompressionEnabled(true));
HttpServer server = vertx.createHttpServer(config);
当启用压缩后,Vert.x 会自动检查客户端发送的 Accept-Encoding 请求头,并在响应中挑选匹配的最优支持算法进行压缩编码,同时自动设置正确的 Content-Encoding 响应头。
HTTP 压缩算法(HTTP compression algorithms)
Vert.x 开箱即支持多种业界主流的压缩算法:
-
gzip -
deflate -
snappy -
brotli -
zstandard
你可以通过配置添加并启用特定的算法,并定制其压缩级别(Level)与触发压缩的最小响应体大小阈值(Threshold):
HttpServerConfig config = new HttpServerConfig()
.setCompressionConfig(new CompressionConfig()
.setCompressionEnabled(true)
.setMinLength(1024) // 低于 1KB 的响应体不进行压缩
.addGzip(new GzipConfig().setLevel(6))
.addBrotli(new BrotliConfig().setQuality(4))
.addZstandard(new ZstdConfig().setLevel(3)));
请注意:使用 Brotli 和 Zstandard 算法同样需要前述对应的本地依赖包(Brotli4j 或 Zstd-jni)。
创建 HTTP 客户端(Creating an HTTP client)
使用默认配置创建 HTTP 客户端的最简方式如下:
HttpClient client = vertx.createHttpClient();
默认情况下,该客户端支持 HTTP/1.1 与 HTTP/2,并在明文和 TLS 加密链路上均可运行。
配置 HTTP 客户端
如果你想配置客户端,可以按如下方式创建:
HttpClientConfig config = new HttpClientConfig()
.setVersions(HttpVersion.HTTP_1_1, HttpVersion.HTTP_2)
.setConnectTimeout(Duration.ofSeconds(5))
.setIdleTimeout(Duration.ofSeconds(30));
HttpClient client = vertx.createHttpClient(config);
配置选项主要包括:
-
客户端支持的协议版本(HTTP/1.1, HTTP/2, HTTP/3)
-
连接池行为与并发限制
-
连接超时与空闲超时
-
各协议专属参数
-
TLS 安全选项
-
代理配置
配置 HTTPS 客户端
要让 HTTP 客户端连接 HTTPS 目标服务器,你可以为客户端配置 SSL 信任选项:
ClientSSLOptions sslOptions = new ClientSSLOptions()
.setTrustOptions(
new JksOptions().
setPath("/path/to/your/truststore.jks").
setPassword("password-of-your-truststore")
);
HttpClient client = vertx.createHttpClient(sslOptions);
详情请参阅 SSL 客户端配置 章节。
配置 HTTP/1.x 客户端
针对 HTTP/1.x 协议的专用配置参数由 Http1ClientConfig 负责封装:
HttpClientConfig config = new HttpClientConfig()
.setHttp1Config(new Http1ClientConfig()
.setKeepAlive(true)
.setPipelining(false));
配置 HTTP/2 客户端
要让客户端通过 HTTP/2 进行通信,可以在配置中指定支持该协议:
HttpClientConfig config = new HttpClientConfig()
.setVersions(HttpVersion.HTTP_2)
.setHttp2Config(new Http2ClientConfig()
.setInitialSettings(new Http2Settings()
.setMaxConcurrentStreams(100)));
Http2ClientConfig 封装了 HTTP/2 特定方面的配置。
配置 HTTP/3 客户端
Vert.x 支持基于 QUIC 的 HTTP/3 客户端通信:
HttpClientConfig config = new HttpClientConfig()
.setVersions(HttpVersion.HTTP_3);
HttpClient client = vertx.createHttpClient(config, sslOptions);
关于底层原生依赖和运行机制,请参阅 原生 QUIC 与 QUIC 客户端章节。
混合 HTTP 客户端(Hybrid HTTP client)
你可以创建一个混合客户端,同时支持连接不同的协议端点:
HttpClientConfig config = new HttpClientConfig()
.setVersions(HttpVersion.HTTP_1_1, HttpVersion.HTTP_2, HttpVersion.HTTP_3);
HttpClient client = vertx.createHttpClient(config, sslOptions);
该客户端会在握手阶段根据目标服务端的能力自动进行最优协议协商。
连接 Unix 域套接字(Making connections to Unix domain sockets)
在支持的环境下,客户端同样可以直接连接至本地 Unix 域套接字服务端:
SocketAddress address = SocketAddress.domainSocketAddress("/var/tmp/myservice.sock");
client.request(HttpMethod.GET, address, "/endpoint")
.compose(req -> req.send())
.onComplete(ar -> {
if (ar.succeeded()) {
System.out.println("收到响应:" + ar.result().statusCode());
}
});
连接池配置(Pool configuration)
Vert.x HTTP 客户端内部默认使用连接池来复用 TCP/TLS 连接以减少频繁握手开销。
你可以通过 PoolOptions 精细控制连接池参数:
PoolOptions poolOptions = new PoolOptions()
.setMaxSize(50) // 每个目标主机的最大连接数
.setMaxWaitQueueSize(100) // 等待可用连接的队列最大排队数
.setCleanerPeriod(Duration.ofSeconds(10)); // 定期检查回收空闲连接的周期
HttpClient client = vertx.createHttpClient(poolOptions);
高级 HTTP 客户端构建(Builder API)
如同服务端一样,你可以使用流式的构建器 API 创建客户端:
HttpClient client = vertx
.httpClientBuilder()
.with(config)
.with(poolOptions)
.with(sslOptions)
.build();
发起 HTTP 请求(Making requests)
要向远端服务器发起 HTTP 请求,可以使用 request 系列方法:
client.request(HttpMethod.GET, 8080, "localhost", "/users")
.onComplete(ar -> {
if (ar.succeeded()) {
HttpClientRequest request = ar.result();
// 配置请求并发送
}
});
默认主机与端口
如果在创建客户端时配置了默认主机和端口,发起请求时可以省略这些参数:
HttpClientConfig config = new HttpClientConfig()
.setDefaultHost("api.example.com")
.setDefaultPort(443);
HttpClient client = vertx.createHttpClient(config, sslOptions);
// 发起请求时只需提供相对 URI
client.request(HttpMethod.GET, "/v1/data");
写入请求头(Writing request headers)
可以使用 putHeader 写入请求标头:
request
.putHeader("Content-Type", "application/json")
.putHeader("Authorization", "Bearer token123");
或者直接操作其 headers() MultiMap。
写入请求并处理响应(Writing request and processing response)
使用 send 方法可以在完成请求发送的同时返回表示异步响应的 Future:
发送不带请求体的简单 GET 请求:
client.request(HttpMethod.GET, 8080, "localhost", "/")
.compose(req -> req.send())
.onComplete(ar -> {
if (ar.succeeded()) {
HttpClientResponse response = ar.result();
System.out.println("收到响应状态码:" + response.statusCode());
}
});
直接发送字符串请求体:
client.request(HttpMethod.POST, 8080, "localhost", "/submit")
.compose(req -> req.send("{\"name\":\"Alice\"}"))
.onComplete(ar -> {
// 处理响应
});
直接发送 Buffer 请求体:
Buffer buffer = Buffer.buffer().appendInt(123);
client.request(HttpMethod.POST, 8080, "localhost", "/submit")
.compose(req -> req.send(buffer))
.onComplete(ar -> {
// 处理响应
});
流式传输请求体(Streaming Request body)
对于大文件或流式生成的数据,你可以将请求体作为流逐步写入:
client.request(HttpMethod.POST, 8080, "localhost", "/upload")
.compose(req -> {
req.setChunked(true);
req.write(Buffer.buffer("第一块数据"));
req.write(Buffer.buffer("第二块数据"));
return req.end();
});
结束流式 HTTP 请求
一旦流式写入完成,必须调用 end 告知对端请求体已全部发射完毕。
将请求作为 WriteStream 使用
HttpClientRequest 实现了 WriteStream 接口,因此可以直接利用管道(Pipe)从文件或其他流中直接对接入站:
AsyncFile file = vertx.fileSystem().openBlocking("largefile.zip", new OpenOptions());
client.request(HttpMethod.POST, 8080, "localhost", "/upload")
.compose(req -> {
req.setChunked(true);
return file.pipeTo(req);
});
分块 HTTP 请求(Chunked HTTP requests)
若未能预先知道完整的请求体大小,可以调用 setChunked(true) 启用分块传输编码。
表单提交(Form submissions)
客户端提供了专门的表单提交便捷 API:
MultiMap form = MultiMap.caseInsensitiveMultiMap();
form.add("username", "admin");
form.add("password", "secret");
client.request(HttpMethod.POST, 8080, "localhost", "/login")
.compose(req -> req.send(form))
.onComplete(ar -> {
// 处理登录响应
});
HTTPS 请求与 SNI
向 HTTPS 服务端发起请求时,客户端会自动提取目标主机名并通过 SNI 扩展发送给服务端。
请求协议版本指定
如果你需要针对单个请求强制使用特定的 HTTP 协议版本,可以在发起请求的 RequestOptions 中指定:
RequestOptions options = new RequestOptions()
.setPort(8080)
.setHost("localhost")
.setURI("/test")
.setVersion(HttpVersion.HTTP_2);
client.request(options);
请求超时设置(Request timeouts)
你可以为请求设置等待超时的时长。若服务端未能在指定时间内返回响应,请求将自动触发超时失败:
client.request(HttpMethod.GET, 8080, "localhost", "/slow-op")
.compose(req -> req.setTimeout(5000).send())
.onComplete(ar -> {
if (ar.failed()) {
System.err.println("请求超时或发生错误:" + ar.cause());
}
});
写入自定义控制帧
在 HTTP/2 或 HTTP/3 模式下,你可以向对端发送扩展自定义控制帧:
request.writeCustomFrame(10, 0, Buffer.buffer("payload"));
处理 HTTP 响应(Handling HTTP responses)
当服务端完成响应头的发送后,客户端将接收到一个 HttpClientResponse 响应对象。
你可以通过该对象读取响应状态码、状态消息、响应头、Cookie 以及响应体。
将响应作为读取流使用(Using the response as a stream)
HttpClientResponse 实现了 ReadStream 接口,因此你可以通过注册 handler 逐块接收响应体 Buffer,也可以使用管道(Pipe)将其直接传输到文件或写入流中:
response.handler(buffer -> {
System.out.println("收到一块响应体数据,大小为:" + buffer.length());
});
响应头与 Trailer 尾部头
使用 headers 读取 HTTP 响应头 MultiMap:
String contentType = response.headers().get("content-type");
如果服务端以分块传输模式发送了 Trailer 尾部头,可以在响应结束时通过 trailers 读取:
response.endHandler(v -> {
String checksum = response.trailers().get("X-Checksum");
});
读取完整响应体(Reading the response body)
如果你不需要流式逐块处理,而是希望一次性获取聚合完整的响应体 Buffer,可以直接调用 body() 方法,该方法返回一个包含完整 Buffer 的 Future:
client.request(HttpMethod.GET, 8080, "localhost", "/data")
.compose(req -> req.send())
.compose(resp -> resp.body())
.onComplete(ar -> {
if (ar.succeeded()) {
Buffer body = ar.result();
System.out.println("收到完整响应内容:" + body.toString());
}
});
响应结束处理器(Response end handler)
响应的 endHandler 会在全部响应体数据到达后被触发回调。
请求与响应流的异步链式编排(Flow composition)
借助 Vert.x Future 的 Fluent API,你可以优雅地将发起请求、发送数据、解析响应与异常处理连缀成整洁的异步流水线:
client.request(HttpMethod.POST, 8080, "localhost", "/api/item")
.compose(request -> request.send(Buffer.buffer("payload")))
.compose(response -> {
if (response.statusCode() == 200) {
return response.body();
} else {
return Future.failedFuture("服务端返回错误状态码:" + response.statusCode());
}
})
.onSuccess(body -> System.out.println("成功处理响应:" + body.toString()))
.onFailure(err -> System.err.println("处理流程出错:" + err.getMessage()));
响应断言机制(Response expectations)
Vert.x 提供了 expecting 断言机制,允许你在响应体处理之前预先声明对响应的期望条件(如期望状态码为 200,或期望 Content-Type 为 JSON)。若响应不满足该断言,流程将自动以失败终止,而无需编写冗长重复的 if-else 校验逻辑:
client.request(HttpMethod.GET, 8080, "localhost", "/api/user")
.compose(req -> req.send())
.expecting(HttpResponseExpectation.SC_OK)
.expecting(HttpResponseExpectation.JSON)
.compose(resp -> resp.body())
.onSuccess(body -> {
// 此时已完全确保状态码为 200 且 Content-Type 为 JSON
JsonObject user = body.toJsonObject();
})
.onFailure(err -> {
// 断言失败或网络异常统一在此捕获
});
预定义断言(Predefined expectations)
HttpResponseExpectation 提供了大量预定义断言:
-
SC_OK:期望 HTTP 200 -
SC_SUCCESS:期望 2xx 成功状态码 -
SC_REDIRECT:期望 3xx 重定向状态码 -
SC_CLIENT_ERROR:期望 4xx 客户端错误 -
SC_SERVER_ERROR:期望 5xx 服务端错误 -
JSON:期望 Content-Type 包含application/json -
status(int code):期望指定的状态码
自定义断言失败(Creating custom failures)
你可以自定义断言函数,当不满足预期时抛出带有业务语义的领域异常。
从响应中读取 Cookie
使用 cookies 方法读取服务端通过 Set-Cookie 响应头返回的 Cookie 列表:
for (String cookie : response.cookies()) {
System.out.println("收到 Cookie:" + cookie);
}
30x 重定向自动处理(30x redirection handling)
Vert.x HTTP 客户端支持自动跟踪 HTTP 301、302、303、307、308 等重定向状态码。
通过在客户端配置中设置 RedirectPolicy 启用重定向支持:
HttpClientConfig config = new HttpClientConfig()
.setRedirectPolicy(RedirectPolicy.FOLLOW);
HttpClient client = vertx.createHttpClient(config);
默认重定向策略
支持的重定向策略枚举包括:
-
RedirectPolicy.NEVER:从不自动跟随重定向(默认行为)。 -
RedirectPolicy.FOLLOW:自动跟随安全的重定向(例如对 GET、HEAD 请求的重定向,以及 303 See Other 转换请求)。 -
RedirectPolicy.FOLLOW_ALL:对所有状态码均尝试跟随重定向。
自定义重定向策略
你可以通过实现 RedirectHandler 函数编写高度定制的重定向决策逻辑(例如根据特定域名白名单、Header 或认证凭据决定是否允许跨域重定向)。
创建 HTTP 隧道(Creating HTTP tunnels)
通过发送 CONNECT 方法请求,客户端可以建立一个双向透传的 HTTP 隧道(HTTP Tunnel),常用于穿越正向代理访问外部加密服务:
client.request(HttpMethod.CONNECT, 8080, "proxy.example.com", "target.example.com:443")
.compose(req -> req.send())
.onComplete(ar -> {
if (ar.succeeded()) {
HttpClientResponse resp = ar.result();
if (resp.statusCode() == 200) {
// 成功建立隧道,转换为双向透传 NetSocket
NetSocket socket = resp.netSocket();
}
}
});
接收自定义控制帧
在 HTTP/2 和 HTTP/3 响应中,同样可以使用 customFrameHandler 监听服务端发送的自定义扩展帧。
开启客户端自动解压缩(Enabling decompression on the client)
当服务端返回压缩编码的响应体时,客户端可以在配置中启用自动解压:
HttpClientConfig config = new HttpClientConfig()
.setCompressionConfig(new CompressionConfig()
.setDecompressionEnabled(true));
HttpClient client = vertx.createHttpClient(config);
开启后,客户端会自动向请求中添加支持的 Accept-Encoding 头,并在收到压缩响应体时透明地将其解压缩为原始明文字节流。
客户端负载均衡(Client side load balancing)
Vert.x HTTP 客户端原生支持客户端软负载均衡。你可以配置目标服务器集群节点列表,并选用轮询(Round Robin)或随机等策略发起连接分配。
HTTP/1.x 连接池与 Keep-Alive 保持连接
在 HTTP/1.1 中,默认开启 Keep-Alive 以便在同一个底层 TCP 连接上依次复用发送多个按序请求。
连接池会自动管理这些连接的生命周期。当某个请求完成且连接仍保持健康时,该连接将被归还至空闲连接池;当连接空闲超时(idleTimeout)时将被自动释放关闭。
HTTP/1.1 管线化(HTTP/1.1 pipelining)
HTTP/1.1 支持管线化(Pipelining)技术,允许客户端在未收到前一个请求的响应之前,先行发送下一个请求。
你可以通过 Http1ClientConfig.setPipelining(true) 启用该功能。请注意:根据 HTTP/1.1 规范,服务端必须严格按照请求到达的先后顺序依次返回响应,容易受到队头阻塞(Head-of-Line Blocking)影响。
多路复用 HTTP(Multiplexed HTTP)
HTTP/2 和 HTTP/3 原生支持单连接多路复用(Multiplexing):
-
在单个 TCP/TLS 连接(HTTP/2)或单个 QUIC UDP 连接(HTTP/3)上,可以并发交错传输成百上千个独立的请求/响应流(Streams)。
-
彻底解决了 HTTP/1.1 协议层面的队头阻塞问题。
-
Vert.x 连接池会自动识别多路复用能力,将发往同一主机的多个并发请求路由复用至已建立的多路复用连接上。
非池化独立连接(Un-pooled client connections)
如果你需要直接完全控制单条底层物理连接的生命周期(绕过连接池),可以使用非池化 API 直接建立独立的连接。
100-Continue 握手处理
在发送超大请求体之前,客户端可以在请求头中添加 Expect: 100-continue,询问服务端是否愿意接收该请求。若服务端回复 100 Continue 状态码,客户端再开始正式传输实际的请求体,从而避免不必要的网络带宽浪费。
HTTP 连接抽象(HTTP connections)
Vert.x 将底层的物理网络链路抽象为 HttpConnection 对象。
服务端连接(Server connections)
服务端可以通过连接监听器获取接入的 HttpConnection 实例,用于监听连接级事件(如 GOAWAY 帧、PING 探测等)。
客户端连接(Client connections)
客户端同样可以从请求中获取对应的 HttpConnection,以监控底层的连接健康状况。
连接配置设置(Connection settings)
对于 HTTP/2 连接,可以通过 Http2Settings 查看和动态协商双方的流控上限与窗口参数。
连接优雅停机(Connection shutdown)
调用连接的 shutdown() 方法会向对端发送协议级关闭控制帧(例如 HTTP/2 的 GOAWAY 帧),通知对端该连接即将关闭且不再接收新的流,但允许当前正在传输中的活跃流正常完成。
连接强制断开(Connection close)
调用 close() 会立即强行掐断底层物理连接并释放相关资源。
=== 客户端共享(Client sharing)
如果你在多个 Verticle 实例中创建具有相同配置名称的客户端,它们会自动共享底层的连接池与网络资源,从而避免为每个 Verticle 重复创建冗余的连接池。
TCP 服务端多核端口共享(TCP Server sharing)
在多核 CPU 机器上充分利用硬件性能是 Vert.x 的核心优势之一。
如果你在多个 Verticle 实例中(或者同一个 Verticle 的多个部署副本中)绑定相同的端口与主机,Vert.x 不会报错,而是会智能地将它们绑定在底层的同一个物理服务器套接字上(利用操作系统的端口复用特性)。
当有新的客户端连接到达时,底层的分发机制会以轮询(Round-Robin)的方式将连接均匀分发给各个 Verticle 实例的事件循环(Event Loop)线程进行处理。这使得你的应用程序无需在外部额外架设负载均衡器,就能开箱即用地在单机各 CPU 核心之间实现极致的高并发线性扩展。
QUIC 服务端端口共享(QUIC server sharing)
同样地,对于基于 UDP 的 QUIC 服务端,Vert.x 同样支持多核端口共享,各个并发数据包和连接将被均匀调度到绑定的各个 Verticle 事件循环中。
Verticle 中的自动资源清理
当你在 Verticle 内部创建 HTTP 服务端或客户端时,当该 Verticle 被卸载(Undeployed)时,Vert.x 会自动平滑关闭这些服务端和客户端并释放底层持有的全部网络与内存资源,彻底杜绝资源泄露。
网络活动日志(Logging network activity)
出于底层网络协议诊断与性能调试目的,可以为 HTTP 服务端或客户端开启网络活动日志:
HttpServerConfig config = new HttpServerConfig()
.setLogConfig(new LogConfig().setEnabled(true));
HttpServer server = vertx.createHttpServer(config);
底层 Netty 将在 DEBUG 级别以十六进制形式完整记录收发的数据包。请注意:这属于高开销的诊断工具,严禁在生产环境中开启。
服务器名称指示(SNI)
Vert.x 服务端完整支持 TLS SNI(Server Name Indication)扩展。当同一个物理服务器托管多个不同域名的 HTTPS 站点时,服务端可以根据客户端在握手阶段出示的域名,动态匹配并返回对应的证书与私钥。
详情请参阅 服务端 SSL 配置。
使用 HAProxy 协议(Using HA PROXY protocol)
当 Vert.x 服务端部署在 HAProxy、AWS ELB 或 Nginx 等反向代理之后时,由于直连的是代理服务器,服务端直接读取的远程套接字 IP 是代理服务器的内网 IP。
通过开启 HAProxy PROXY Protocol 支持,代理服务器会在 TCP 连接刚建立时发送包含原始客户端真实 IP 和端口的前导元数据报文:
HttpServerConfig config = new HttpServerConfig()
.setProxyProtocolConfig(new ProxyProtocolConfig().setEnabled(true));
HttpServer server = vertx.createHttpServer(config);
开启后,你可以直接通过 request.remoteAddress() 获取穿透代理后的原始真实客户端 IP。
TCP 连接带宽限流(Throttling bandwidth)
Vert.x 允许你对 TCP 连接的入站(读取)和出站(写入)带宽进行精确限速(流量整形 Traffic Shaping):
HttpServerConfig config = new HttpServerConfig()
.setInboundTrafficShapingConfig(new TrafficShapingConfig()
.setReadLimit(1024 * 1024)) // 限制读取带宽为 1 MB/s
.setOutboundTrafficShapingConfig(new TrafficShapingConfig()
.setWriteLimit(2 * 1024 * 1024)); // 限制写入带宽为 2 MB/s
HTTP/2 高级特性
连接 Ping 心跳(Connection ping)
HTTP/2 连接支持双向发送 PING 帧来探测网络连通性并测量端到端往返时延(RTT):
connection.ping(Buffer.buffer("12345678"))
.onSuccess(pong -> System.out.println("收到 PING 回应!"));
HTTP/2 RST 洪水攻击防护(RST flood protection)
恶意的 HTTP/2 客户端可能会以极高频率向服务端发送大量 RST_STREAM 帧,导致服务端 CPU 耗尽(CVE 常见漏洞攻击模型)。
Vert.x HTTP/2 服务端内建了 RST 洪水攻击防御机制。你可以通过配置窗口时间内允许的最大 RST 帧阈值进行安全加固,一旦超过阈值服务端将自动关闭该恶意连接。
动态热更新 HTTP/2 设置(Updating HTTP/2 settings)
服务端与客户端可以在运行期间动态发送新配置的 Http2Settings 帧以协商调整窗口大小和流并发上限。
WebSocket 双向通信
Vert.x 原生支持在 HTTP 服务端与客户端之间建立全双工、低延迟的 WebSocket 长连接通信。
服务端 WebSocket
WebSocket 处理器(WebSocket handler)
你可以在 HTTP 服务端上注册专门的 webSocketHandler 来处理所有的 WebSocket 连接请求:
server.webSocketHandler(ws -> {
System.out.println("有新的 WebSocket 客户端接入:" + ws.path());
ws.handler(buffer -> {
System.out.println("收到客户端消息:" + buffer.toString());
ws.writeTextMessage("服务端已收到:" + buffer.toString());
});
});
服务端 WebSocket 握手校验
在接受连接之前,服务端可以检查请求路径、Header 或 Token 凭据,并决定接受还是拒绝该握手请求:
server.webSocketHandler(ws -> {
if (ws.path().equals("/ws/chat")) {
ws.accept();
} else {
ws.reject(404);
}
});
在普通 HTTP 请求中升级为 WebSocket
你还可以在常规的 requestHandler 中根据特定业务条件动态将 HTTP 请求升级(Upgrade)为 WebSocket:
server.requestHandler(request -> {
if (request.path().equals("/custom-ws")) {
request.toWebSocket().onSuccess(ws -> {
// 成功完成协议升级,开始 WebSocket 全双工通信
});
} else {
request.response().end("普通 HTTP 响应");
}
});
客户端 WebSocket
客户端可以通过 webSocket 发起对远端 WebSocket 服务端的连接:
client.webSocket(8080, "localhost", "/ws/chat")
.onComplete(ar -> {
if (ar.succeeded()) {
WebSocket ws = ar.result();
ws.writeTextMessage("你好,服务端!");
ws.handler(buffer -> {
System.out.println("收到服务端推送:" + buffer.toString());
});
}
});
向 WebSocket 发送文本与二进制消息
使用 writeTextMessage 发送字符串文本,使用 writeBinaryMessage 发送二进制 Buffer:
ws.writeTextMessage("文本消息");
ws.writeBinaryMessage(Buffer.buffer().appendInt(100));
向 WebSocket 写入底层数据帧(Frames)
对于更精细的协议控制,可以直接写入底层 WebSocket 帧(包括文本帧、二进制帧、Continuation 延续帧、Ping 帧、Pong 帧):
WebSocketFrame frame = WebSocketFrame.textFrame("分片消息帧", true);
ws.writeFrame(frame);
从 WebSocket 读取数据帧
可以通过 frameHandler 直接监听接收每一个底层的 WebSocketFrame:
ws.frameHandler(frame -> {
if (frame.isText()) {
System.out.println("收到文本帧:" + frame.textData());
} else if (frame.isBinary()) {
System.out.println("收到二进制帧:" + frame.binaryData().length());
}
});
关闭 WebSocket 连接
调用 close() 关闭连接,可以传递符合 RFC 6455 规范的关闭状态码(如 1000 正常关闭)与可选的关闭原因文本:
ws.close((short) 1000, "正常结束通信");
WebSocket 管道传输(Piping WebSockets)
由于 WebSocket 同时实现了 ReadStream 和 WriteStream,因此你可以将其与任何流对象(如文件、TCP Socket 或另一个 WebSocket)通过管道无缝对接。
使用代理(HTTP/HTTPS/SOCKS5 代理)
Vert.x HTTP 客户端支持通过正向代理服务器发起网络请求。
可以通过 ProxyOptions 配置代理参数:
ProxyOptions proxyOptions = new ProxyOptions()
.setType(ProxyType.HTTP) // 可选 HTTP 或 SOCKS5
.setHost("proxy.corp.internal")
.setPort(8080)
.setUsername("user")
.setPassword("pass");
HttpClientConfig config = new HttpClientConfig()
.setProxyOptions(proxyOptions);
HttpClient client = vertx.createHttpClient(config);
支持的代理协议类型包括:
-
ProxyType.HTTP:标准的 HTTP/HTTPS 正向代理 -
ProxyType.SOCKS5:SOCKS5 代理协议
使用 SharedData 共享数据 API
顾名思义,SharedData API 允许你在以下各方之间安全地共享数据:
-
应用程序的不同组件之间;
-
同一个 Vert.x 实例中的不同应用程序之间;
-
跨 Vert.x 实例集群的不同应用程序之间。
在实际使用中,它提供了以下能力:
-
同步 Map(仅限本地)
-
异步 Map(本地或分布式)
-
异步锁(Lock)
-
异步计数器(Counter)
| 分布式数据结构的行为高度依赖于你所使用的集群管理器(Cluster Manager)。 数据备份(副本复制机制)以及发生网络分区(脑裂)时的处理策略均由集群管理器及其配置决定。 详情请参阅具体集群管理器的文档及底层分布式框架(如 Hazelcast、Infinispan、Zookeeper)的用户手册。 |
本地 Map(Local maps)
本地 Map 允许你在同一个 Vert.x 实例内的不同事件循环(例如不同的 Verticle)之间安全地共享数据。
它仅允许特定数据类型作为键(Key)和值(Value):
-
不可变类型(如 String、Boolean 等);
-
实现了
Shareable接口的类型(Buffer、JSON 数组、JSON 对象或你自定义的 shareable 对象)。
在后一种情况下,键或值在放入 Map 之前会被深拷贝。
通过这种设计,我们可以确保 Vert.x 应用程序中不同线程之间不存在对可变状态的共享并发访问。 因此你完全不必担心需要通过编写复杂的线程同步代码来保护状态。
|
为方便使用,实现了 |
以下是使用本地共享 Map 的示例:
SharedData sharedData = vertx.sharedData();
LocalMap<String, String> map1 = sharedData.getLocalMap("mymap1");
map1.put("foo", "bar"); // String 是不可变对象,因此无需拷贝
LocalMap<String, Buffer> map2 = sharedData.getLocalMap("mymap2");
map2.put("eek", Buffer.buffer().appendInt(123)); // 该 Buffer 在存入 Map 之前会被深拷贝
// 然后……在应用程序的另一个部分中:
map1 = sharedData.getLocalMap("mymap1");
String val = map1.get("foo");
map2 = sharedData.getLocalMap("mymap2");
Buffer buff = map2.get("eek");
异步共享 Map(Asynchronous shared maps)
异步共享 Map 允许数据在本地以及集群中的任意其他节点之间进行存放和读取。
这在托管 Vert.x Web 应用程序的服务器集群中管理会话状态(Session State)等场景下非常实用。
它同样仅允许特定数据类型作为键和值:
-
不可变类型(例如 String、Boolean 等);
-
实现了
ClusterSerializable接口的类型(Buffer、JSON 数组、JSON 对象或你自定义的集群序列化对象); -
实现了
java.io.Serializable接口的类型。
获取该 Map 的过程是异步的,获取结果将在你指定的处理器中返回。示例如下:
SharedData sharedData = vertx.sharedData();
sharedData.
<String, String>getAsyncMap("mymap")
.onComplete(res -> {
if (res.succeeded()) {
AsyncMap<String, String> map = res.result();
} else {
// 获取失败,进行异常处理
}
});
当 Vert.x 运行在集群模式下时,你存入 Map 的数据在本地以及集群中的任何其他成员节点上均可访问。
| 在集群模式下,异步共享 Map 依赖于集群管理器提供的分布式数据结构。 需要注意的是,与本地模式相比,集群模式下异步共享 Map 操作的网络往返延迟会显著增高。 |
如果你的应用程序并不需要将数据跨所有节点共享,你可以获取一个仅限本地的异步 Map:
SharedData sharedData = vertx.sharedData();
sharedData.
<String, String>getLocalAsyncMap("mymap")
.onComplete(res -> {
if (res.succeeded()) {
// 获取到仅限本地的异步 Map
AsyncMap<String, String> map = res.result();
} else {
// 发生错误
}
});
存放数据到 Map
你可以通过 put 方法将数据放入 Map 中。
实际的存放操作是异步进行的,返回的 Future 会在操作完成时收到通知:
map
.put("foo", "bar")
.onComplete(resPut -> {
if (resPut.succeeded()) {
// 数据写入成功
} else {
// 写入失败处理
}
});
从 Map 获取数据
你可以通过 get 方法获取数据。
实际的获取过程是异步的,返回的 Future 会在稍后携带查询结果通知你:
map
.get("foo")
.onComplete(resGet -> {
if (resGet.succeeded()) {
// 成功获取值
Object val = resGet.result();
} else {
// 获取失败处理
}
});
异步锁(Asynchronous locks)
异步锁 允许你在本地或跨整个集群获取排他互斥锁。
当你希望在集群的任意时刻仅由单个节点执行某项操作或访问排他资源时,这非常有用。
与大多数会阻塞当前调用线程直到获取锁的传统锁 API 不同,异步锁拥有完全非阻塞的异步 API。
在你持有锁的期间,本地或集群中的任何其他调用者都无法获取该锁。
当你使用完毕后,调用 release 释放该锁,以便其他调用者能够获取:
SharedData sharedData = vertx.sharedData();
sharedData
.getLock("mylock")
.onComplete(res -> {
if (res.succeeded()) {
// 成功获取锁!
Lock lock = res.result();
// 5 秒后释放锁,让其他进程能够获取
vertx.setTimer(5000, tid -> lock.release());
} else {
// 获取锁出错
}
});
你也可以指定超时时间获取锁。如果在超时时间内未能成功获取锁,处理器将收到失败通知:
SharedData sharedData = vertx.sharedData();
sharedData
.getLockWithTimeout("mylock", 10000)
.onComplete(res -> {
if (res.succeeded()) {
// 成功获取锁!
Lock lock = res.result();
} else {
// 超时或未能获取到锁
}
});
有关锁操作的完整细节,请参阅 API 文档。
| 在集群模式下,异步锁依赖于集群管理器提供的分布式锁实现。 请注意,与本地锁相比,集群模式下异步锁的网络开销和延迟会更高。 |
如果你的应用程序不需要跨节点共享锁,你可以获取一个仅限本地的锁:
SharedData sharedData = vertx.sharedData();
sharedData
.getLocalLock("mylock")
.onComplete(res -> {
if (res.succeeded()) {
// 仅限本地的锁
Lock lock = res.result();
// 5 秒后释放锁
vertx.setTimer(5000, tid -> lock.release());
} else {
// 获取失败
}
});
在某些情况下,你会使用锁 API 来执行异步任务,并在异步调用周围应用加锁/解锁模式。Vert.x 提供了更加简洁的 withLock API 来封装此惯用模式:
SharedData sharedData = vertx.sharedData();
Future<String> res = sharedData.withLock("mylock", () -> {
// 已成功持有锁!
Future<String> future = getAsyncString();
// 当该 future 完成时,锁将被自动释放
return future;
});
该方法会在调用 supplier 闭包之前先获取锁,并在 supplier 返回的 future 敲定(无论成功还是失败)时自动释放锁。
异步计数器(Asynchronous counters)
在本地或应用程序的各个节点之间维护一个原子计数器(Atomic Counter)是非常常见的需求。
你可以使用 Counter 来轻松实现。
通过 getCounter 获取计数器实例:
SharedData sharedData = vertx.sharedData();
sharedData
.getCounter("mycounter")
.onComplete(res -> {
if (res.succeeded()) {
Counter counter = res.result();
} else {
// 获取计数器失败
}
});
一旦拥有计数器实例,你就可以使用其提供的各种方法获取当前计数值、原子递增、原子递减或增加特定值。
有关计数器操作的详细信息,请查阅 API 文档。
| 在集群模式下,异步计数器依赖于集群管理器提供的分布式原子长整型实现。 请注意集群模式下的计数器操作比本地模式具有更高的网络往返延迟。 |
如果你仅需要在本地节点内使用计数器,可以获取本地专用计数器:
SharedData sharedData = vertx.sharedData();
sharedData
.getLocalCounter("mycounter")
.onComplete(res -> {
if (res.succeeded()) {
// 仅限本地的原子计数器
Counter counter = res.result();
} else {
// 获取失败
}
});
在 Vert.x 中使用文件系统(FileSystem)
Vert.x 的 FileSystem 对象提供了丰富的文件系统操作方法。
每个 Vert.x 实例都拥有且仅有一个文件系统对象,你可以通过 fileSystem 方法获取它。
所有操作均同时提供了阻塞版本与非阻塞版本。非阻塞版本接收一个处理器(Handler)作为参数,在操作完成或发生错误时进行异步回调。
以下是异步拷贝文件的示例:
FileSystem fs = vertx.fileSystem();
// 将文件从 foo.txt 复制到 bar.txt
fs.copy("foo.txt", "bar.txt")
.onComplete(res -> {
if (res.succeeded()) {
// 拷贝成功!
} else {
// 发生异常处理
}
});
阻塞版本的方法命名规则为 xxxBlocking,它们会直接返回执行结果或直接抛出异常。在许多情况下,取决于底层操作系统和文件系统的实现,部分可能阻塞的操作实际上能够非常快速地返回,这也是 Vert.x 提供它们的原因。但在事件循环(Event Loop)中调用它们之前,强烈建议你在目标生产环境中严格测试其耗时,以确保绝不违背黄金法则(Golden Rule)。
以下是使用阻塞式 API 进行复制的示例:
FileSystem fs = vertx.fileSystem();
// 以同步阻塞方式将文件从 foo.txt 复制到 bar.txt
fs.copyBlocking("foo.txt", "bar.txt");
Vert.x 提供了大量用于复制、移动、截断、修改权限(chmod)等文件操作的方法。此处不再逐一罗列,详情请参阅 API 文档。
下面看几个使用异步方法的典型示例:
vertx.fileSystem()
.readFile("target/classes/readme.txt")
.onComplete(result -> {
if (result.succeeded()) {
System.out.println(result.result());
} else {
System.err.println("读取失败:" + result.cause());
}
});
// 复制文件
vertx.fileSystem()
.copy("target/classes/readme.txt", "target/classes/readme2.txt")
.onComplete(result -> {
if (result.succeeded()) {
System.out.println("文件复制成功");
} else {
System.err.println("复制失败:" + result.cause());
}
});
// 写入文件
vertx.fileSystem()
.writeFile("target/classes/hello.txt", Buffer.buffer("Hello"))
.onComplete(result -> {
if (result.succeeded()) {
System.out.println("文件写入成功");
} else {
System.err.println("写入失败:" + result.cause());
}
});
// 检查是否存在并删除
vertx.fileSystem()
.exists("target/classes/junk.txt")
.compose(exist -> {
if (exist) {
return vertx.fileSystem().delete("target/classes/junk.txt");
} else {
return Future.failedFuture("文件不存在");
}
}).onComplete(result -> {
if (result.succeeded()) {
System.out.println("文件删除成功");
} else {
System.err.println("无法删除文件:" + result.cause().getMessage());
}
});
异步文件(AsyncFile)
Vert.x 提供了异步文件抽象,允许你以非阻塞方式操作文件系统上的具体文件。
打开一个 AsyncFile 对象的示例如下:
OpenOptions options = new OpenOptions();
fileSystem
.open("myfile.txt", options)
.onComplete(res -> {
if (res.succeeded()) {
AsyncFile file = res.result();
} else {
// 打开文件失败!
}
});
AsyncFile 实现了 ReadStream 和 WriteStream 接口,因此你可以通过管道(pipe)将文件与其他流对象(如 NetSocket、HTTP 请求/响应、WebSocket 等)相互连接。
它们同样允许你直接进行随机读写。
随机写入(Random access writes)
要对 AsyncFile 进行随机位置写入,请使用
write 方法。
该方法的参数说明如下:
-
buffer:要写入的 Buffer 缓冲区。 -
position:文件中开始写入数据的整型偏移位置。若该位置大于或等于当前文件大小,文件将自动扩容以容纳新的偏移量。
随机写入示例如下:
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("写入成功!");
// ...
} else {
System.err.println("写入失败:" + ar.cause());
}
});
}
} else {
System.err.println("无法打开文件:" + result.cause());
}
});
随机读取(Random access reads)
要对 AsyncFile 进行随机位置读取,请使用
read
方法。
该方法的参数说明如下:
-
buffer:用于接收读取数据的目标 Buffer。 -
offset:在目标 Buffer 中开始存放数据的偏移量。 -
position:文件中开始读取数据的起始字节偏移位置。 -
length:要读取的字节数量。 -
handler:结果回调处理器。
随机读取示例如下:
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("读取成功!");
} else {
System.err.println("读取失败:" + ar.cause());
}
});
}
} else {
System.err.println("无法打开文件:" + result.cause());
}
});
打开选项(OpenOptions)
打开 AsyncFile 时,你需要传入一个 OpenOptions 实例。
这些选项定义了文件访问的底层行为。例如,你可以使用
setRead、setWrite
以及 setPerms 方法配置文件读写权限。
你还可以使用
setCreateNew 和
setTruncateExisting
配置当目标文件已存在时的创建或截断行为。
此外,你还可以通过 setDeleteOnClose 标记文件在关闭或 JVM 关闭时自动删除。
刷盘同步(Flushing data to underlying storage)
在 OpenOptions 中,你可以使用
setDsync 开启或禁用每次写入时自动将数据物理同步到底层存储。在非自动同步情况下,你可以通过调用 flush 方法手动将操作系统内核缓存区的数据刷写到磁盘。
该方法同样支持传入一个处理器,在刷盘完成时接收异步回调。
将 AsyncFile 作为 ReadStream 和 WriteStream 使用
AsyncFile 实现了 ReadStream 和 WriteStream 接口。因此你可以配合管道(pipe)在文件与其他流之间高效传输数据。例如,下面的代码演示了将一个文件的内容复制到另一个 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(() -> file.close()))
.onComplete(result -> {
if (result.succeeded()) {
System.out.println("复制完成");
} else {
System.err.println("无法复制文件:" + result.cause().getMessage());
}
});
你同样可以使用管道将文件内容直接写入 HTTP 响应体,或者写入任何通用的 WriteStream 中。
从 Classpath 中访问文件(Accessing files from the classpath)
当 Vert.x 在文件系统上找不到指定路径的文件时,它会尝试从类路径(Classpath)中解析该文件。请注意:Classpath 资源的相对路径绝不以 / 开头。
由于 Java 原生并不提供对 Classpath 资源的异步非阻塞访问机制,当某个 Classpath 资源第一次被访问时,Vert.x 会在 Worker 线程中将其解压复制到本地文件系统缓存中,随后再通过异步文件的方式提供服务。当同一资源被第二次访问时,将直接从文件系统缓存中提供。即使 Classpath 中的原始资源后来发生改变(例如在开发环境中),也依然会优先使用缓存中的已有内容。
这种缓存策略可以通过 setFileCachingEnabled
选项进行配置。该选项默认值为 true,除非你定义了系统属性 vertx.disableFileCaching。
缓存文件的默认存放路径为 /tmp/vertx-cache-UUID,你可以通过设置 JVM 系统属性 vertx.cacheDirBase 来进行自定义。使用此属性时,请注意它应当指向一个进程具有读写权限的目录前缀,例如:-Dvertx.cacheDirBase=/tmp/my-vertx-cache(注意末尾不含 UUID)。
每个 Vert.x 进程都会在其后追加独立的 UUID,以确保在同一台物理机上运行的不同应用程序之间的缓存互不干扰。
如果需要彻底在全局禁用 Classpath 解析功能,可以将系统属性 vertx.disableFileCPResolving 设置为 true。
这些系统属性在 io.vertx.core.file.FileSystemOptions 类被加载时仅求值一次,因此必须在加载该类之前或在 JVM 启动参数中预先设定好。
|
如果你希望仅对某个特定应用程序禁用 Classpath 解析,而在全局其他地方保持默认开启,可以通过 setClassPathResolvingEnabled 选项来局部配置。
关闭 AsyncFile
要关闭 AsyncFile,请调用 close 方法。关闭操作是异步进行的,如果你需要在关闭完成后收到通知,可以传入一个处理器回调作为参数。
数据报套接字(UDP Datagram Sockets)
在 Vert.x 中使用用户数据报协议(UDP)非常轻而易举。
UDP 是一种无连接的传输层协议,这意味着你与远端节点之间没有持久连接。
相反,你可以发送和接收独立的数据包(Datagram Packet),每个数据包本身都包含了目标或源端的远程网络地址。
此外,UDP 不像 TCP 那样具备可靠性保证,这意味着发送的数据报数据包不能保证一定会送达目标端点。
UDP 提供的唯一保证是:数据包要么完整接收,要么完全接收不到。
另外,你通常无法发送超过网络接口 MTU 大小的数据,因为每个数据包都会作为单个物理数据报传输。
但请注意,即便数据包大小小于 MTU,它仍然可能发送失败。
具体在什么尺寸下会失败取决于底层操作系统和网络栈配置等。因此通用经验法则是:尽可能发送较小的数据包。
基于 UDP 的这种特性,它最适合允许丢包的场景(例如监控指标上报、日志收集等)。
其核心优势在于相比 TCP(如前述 NetServer 和 NetClient 处理的流式连接)开销极低。
创建 DatagramSocket
要使用 UDP,首先需要创建一个 DatagramSocket。无论你只是想发送数据,还是希望同时收发数据,创建方式都完全相同:
DatagramSocket socket = vertx.createDatagramSocket(new DatagramSocketOptions());
返回的 DatagramSocket 初始并未绑定到特定端口。如果你仅作为客户端发送数据,这完全没有问题;更多绑定细节请参阅下一小节。
发送数据报数据包(Datagram packets)
如前所述,用户数据报协议(UDP)以数据包形式向对端发送数据,但在彼此之间并不建立持久连接。
这意味着每个数据包都可以发送给完全不同的远端节点。
发送数据包极其简单,示例如下:
DatagramSocket socket = vertx.createDatagramSocket(new DatagramSocketOptions());
Buffer buffer = Buffer.buffer("content");
// 发送 Buffer 数据
socket
.send(buffer, 1234, "10.0.0.1")
.onComplete(asyncResult -> System.out.println("发送成功?" + asyncResult.succeeded()));
// 发送普通字符串数据
socket
.send("A string used as content", 1234, "10.0.0.1")
.onComplete(asyncResult -> System.out.println("发送成功?" + asyncResult.succeeded()));
接收数据报数据包
如果你想接收数据包,则需要调用 listen(…) 方法将 DatagramSocket 绑定到指定的监听地址和端口。
这样你就可以接收发送到该端口的 DatagramPacket 数据包。
此外,你还需要配置一个 Handler,每当收到新的 DatagramPacket 时该处理器都会被回调。
DatagramPacket 提供了以下核心方法:
在指定地址和端口上监听的示例如下:
DatagramSocket socket = vertx.createDatagramSocket(new DatagramSocketOptions());
socket
.handler(packet -> {
// 处理接收到的数据报
})
.listen(1234, "0.0.0.0")
.onComplete(asyncResult -> System.out.println("监听启动?" + asyncResult.succeeded()));
请注意:即便异步结果表示成功(succeeded 为 true),它也仅仅意味着数据包已成功写入到本机的网络栈缓冲区,绝不代表数据包已经或将一定会送达远端对端。
如果你需要严格的投递可靠性保证,请改用 TCP 并构建对应的确认握手逻辑。
组播(Multicast)
发送组播数据包
组播允许多个套接字接收相同的数据包。其原理是让这些套接字加入同一个组播组(Multicast Group),然后向该组播地址发送数据。
发送组播数据包与发送普通数据报完全相同,唯一的区别在于将目标地址指定为组播组地址:
DatagramSocket socket = vertx.createDatagramSocket(new DatagramSocketOptions());
Buffer buffer = Buffer.buffer("content");
// 向组播地址发送 Buffer
socket
.send(buffer, 1234, "230.0.0.1")
.onComplete(asyncResult -> System.out.println("发送成功?" + asyncResult.succeeded()));
所有加入了组播组 230.0.0.1 的套接字都将收到该数据包。
接收组播数据包
如果你想接收特定组播组的数据包,需要先通过调用 listen(…) 绑定 DatagramSocket,然后调用加入组播组的方法。
这样你不仅可以接收直接发往该端口的单播数据包,还能接收发往该组播组的数据包。
示例如下:
DatagramSocket socket = vertx.createDatagramSocket(new DatagramSocketOptions());
socket
.handler(packet -> {
// 处理数据包
})
.listen(1234, "0.0.0.0")
.compose(v -> socket.listenMulticastGroup("230.0.0.1")) // 加入组播组
.onComplete(asyncResult -> System.out.println("监听与组播加入成功?" + asyncResult.succeeded()));
退出/离开组播组
在某些场景下,你可能只想在特定时间内接收组播数据包。
此时你可以先监听加入,之后再离开该组播组:
DatagramSocket socket = vertx.createDatagramSocket(new DatagramSocketOptions());
socket
.handler(packet -> {
// 处理数据包
})
.listen(1234, "0.0.0.0")
.compose(v -> socket.listenMulticastGroup("230.0.0.1")) // 加入组播组
.onComplete(asyncResult -> {
if (asyncResult.succeeded()) {
// 当前已开始接收组播数据
// 执行业务逻辑...
socket.unlistenMulticastGroup("230.0.0.1").onComplete(asyncResult2 -> {
System.out.println("退出组播组成功?" + asyncResult2.succeeded());
});
} else {
System.out.println("监听失败:" + asyncResult.cause());
}
});
屏蔽特定组播发送源(Blocking multicast)
除了完全退出组播组之外,还可以针对特定的发送方源 IP 地址屏蔽组播流量。
请注意:该特性依赖于底层操作系统和内核版本的支持,请查阅目标系统文档确认是否受支持。
这是一项高级网络特性。调用 blockMulticastGroup(…) 示例如下:
DatagramSocket socket = vertx.createDatagramSocket(new DatagramSocketOptions());
// ... 其他业务初始化代码 ...
// 屏蔽来自 10.0.0.2 发送的所有组播数据包
socket
.blockMulticastGroup("230.0.0.1", "10.0.0.2")
.onComplete(asyncResult -> System.out.println("屏蔽成功?" + asyncResult.succeeded()));
DatagramSocket 配置属性
创建 DatagramSocket 时,可以通过 DatagramSocketOptions 对象配置多种网络行为:
-
setSendBufferSize:设置发送缓冲区大小(字节)。 -
setReceiveBufferSize:设置接收缓冲区大小(字节)。 -
setReuseAddress:若设为 true,套接字关闭后处于 TIME_WAIT 状态的端口地址可以被快速复用。 -
setTrafficClass:设置 IP 流量类别(QoS/ToS)。 -
setBroadcast:启用或禁用 SO_BROADCAST 套接字选项。设置后,UDP 数据包可以向本地子网的广播地址发送。 -
setMulticastNetworkInterface:指定用于组播通信的网络接口网卡名称。 -
setMulticastTimeToLive:设置 IP_MULTICAST_TTL 套接字选项。TTL 代表跳数限制(Time to Live),指定组播数据包允许跨越的路由器跳数。每经过一个转发路由器 TTL 便递减 1,减至 0 时数据包将被丢弃。
获取本地套接字地址
你可以通过调用 localAddress 查看套接字的本地绑定地址。仅当该套接字事先通过 listen(…) 成功绑定后,才会返回有效的 InetSocketAddress,否则返回 null。
关闭 DatagramSocket
你可以通过调用 close 方法关闭套接字并释放所有相关的底层网络资源。
异步 DNS 客户端
在开发过程中,你经常需要以异步非阻塞的方式获取 DNS 解析信息。 不幸的是,Java 虚拟机自带的原生网络 API 并不支持异步 DNS 解析。为此,Vert.x 提供了完全非阻塞异步的专用 DNS 解析客户端 API。
你可以直接通过 Vertx 实例创建 DnsClient 实例:
DnsClient client = vertx.createDnsClient(53, "10.0.0.1");
你也可以通过配置选项并指定查询超时时间(单位毫秒):
DnsClient client = vertx.createDnsClient(new DnsClientOptions()
.setPort(53)
.setHost("10.0.0.1")
.setQueryTimeout(10000)
);
如果不传递任何参数或省略服务端地址,客户端将默认使用 Vert.x 内部用于非阻塞地址解析的系统 DNS 服务器地址。
你也可以传入尚未解析的服务端域名,客户端会在发起正式查询前自动先对该服务器地址进行解析:
DnsClient client1 = vertx.createDnsClient();
// 功能相同,但配置了不同的查询超时时间
DnsClient client2 = vertx.createDnsClient(new DnsClientOptions().setQueryTimeout(10000));
客户端内部使用单一事件循环(Event Loop)来执行查询,因此可以安全地从任何线程(包括非 Vert.x 线程)中并发调用。
lookup 查询
尝试查询指定域名的 A 记录(IPv4)或 AAAA 记录(IPv6)。系统将直接采用最先返回的记录,其行为与操作系统自带的 nslookup 命令行工具一致。
查询 vertx.io 的 A / AAAA 记录的典型代码如下:
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("解析记录失败:" + ar.cause());
}
});
lookup4 查询 IPv4
尝试仅查询指定域名的 A 记录(IPv4)。系统将使用最先返回的记录,行为等同于系统的 IPv4 nslookup。
查询示例:
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("解析记录失败:" + ar.cause());
}
});
lookup6 查询 IPv6
尝试仅查询指定域名的 AAAA 记录(IPv6)。系统将使用最先返回的记录:
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("解析记录失败:" + ar.cause());
}
});
resolveA 解析所有 A 记录
尝试解析指定域名对应的所有 A 记录(IPv4)。这类似于在 Unix/Linux 系统上执行 dig 命令。
示例如下:
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("解析记录失败:" + ar.cause());
}
});
resolveAAAA 解析所有 AAAA 记录
尝试解析指定域名对应的所有 AAAA 记录(IPv6):
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("解析记录失败:" + ar.cause());
}
});
resolveCNAME 解析规范别名
尝试解析指定域名的所有 CNAME 别名记录:
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("解析记录失败:" + ar.cause());
}
});
resolveMX 解析邮件交换记录
尝试解析指定域名的所有 MX 记录。MX 记录用于定义由哪台邮件服务器接收发往该域名的电子邮件。
示例如下:
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("解析记录失败:" + ar.cause());
}
});
请注意:返回的列表中包含按照优先级(Priority)由小到大严格排序的 MxRecord 对象(优先级数值越小,优先权越高)。
MxRecord 提供了以下方法访问其属性:
record.priority();
record.name();
resolveTXT 解析文本记录
尝试解析域名的所有 TXT 记录。TXT 记录通常用于提供域名的附加验证与说明信息(例如 SPF、DKIM 等):
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("解析记录失败:" + ar.cause());
}
});
resolveNS 解析名称服务器记录
尝试解析域名的所有 NS 记录,以获取托管该域名 DNS 信息的权威域名服务器:
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("解析记录失败:" + ar.cause());
}
});
resolveSRV 解析服务定位记录
尝试解析域名的所有 SRV 记录。SRV 记录用于指定某项特定服务的主机名、端口号、优先级及权重等元数据信息:
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("解析记录失败:" + ar.cause());
}
});
请注意,返回的列表已经按照记录优先级进行了预先排序。
SrvRecord 允许你读取记录中的完整明细:
record.priority();
record.name();
record.weight();
record.port();
record.protocol();
record.service();
record.target();
详情请参阅 API 文档。
resolvePTR 解析指针记录
尝试解析 PTR 记录以将 IP 地址映射回域名。
解析 IP 地址 10.0.0.1 的 PTR 记录时,需使用标准的逆向解析格式 "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("解析记录失败:" + ar.cause());
}
});
reverseLookup 反向域名解析
尝试对 IP 地址执行反向域名解析。这在本质上与解析 PTR 记录相同,但更加简便,允许你直接传入标准 IP 地址字符串,而无需手动组装复杂的 .in-addr.arpa 查询后缀:
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("解析记录失败:" + ar.cause());
}
});
错误处理机制(Error handling)
如前所述,DnsClient 允许你传入一个处理器,查询完成后会以 AsyncResult 形式收到通知。当发生错误时,将抛出携带 DnsResponseCode 状态码的 DnsException,指示解析失败的具体原因。你可以据此深入排查故障。
常见的 DnsResponseCode 包括:
所有这些错误码均由 DNS 服务器端返回。
你可以从 DnsException 中获取对应的 DnsResponseCode:
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("解析条目失败:" + ar.cause());
}
}
});
流(Streams)
Vert.x 中有许多对象支持数据的读取与写入。
在 Vert.x 中,写入操作(write)调用会立即返回,写入数据在内部排队。
显而易见,如果你向某个对象写入数据的速度,超过了它向底层资源实际写入数据的速度,写入队列就会无限膨胀——最终导致内存耗尽。
为了解决这个问题,Vert.x API 中的部分对象提供了一种简单而强大的流控(back-pressure,即背压)机制。
任何支持流控的“可写入”对象都会实现 WriteStream 接口;
而任何支持流控的“可读取”对象都会实现 ReadStream 接口。
我们来看一个典型的例子:从 ReadStream 读取数据并写入 WriteStream。
这一机制适用于任何符合 ReadStream 和 WriteStream 规范的对象,包括 HTTP 请求、HTTP 响应、异步文件 I/O、WebSocket 等。
|
一种朴素的处理方式是直接把读出来的数据立刻写入目标流:
NetServer server = vertx.createNetServer(
new NetServerOptions().setPort(1234).setHost("localhost")
);
server.connectHandler(sock -> {
sock.handler(buffer -> {
// 直接将数据写入 AsyncFile
asyncFile.write(buffer);
});
}).listen();
上述代码存在一个严重的问题:如果从 Socket 读取数据的速度大于写入文件的速度,数据就会在 AsyncFile 的写入队列中不断积压,最终耗尽内存。
例如,当实际文件存储在缓慢的网络文件系统上时就会出现这种情况,此时就需要对连接施加背压。
由于 AsyncFile 实现了 WriteStream,我们可以在写入之前先检查 WriteStream 队列是否已满:
NetServer server = vertx.createNetServer(
new NetServerOptions().setPort(1234).setHost("localhost")
);
server.connectHandler(sock -> {
sock.handler(buffer -> {
if (!asyncFile.writeQueueFull()) {
asyncFile.write(buffer);
}
});
}).listen();
这个示例虽然不会耗尽内存,但一旦写入队列满了,就会导致数据丢失。
我们真正想要的效果是:当写入队列满时,暂停(pause)NetSocket 的读取:
NetServer server = vertx.createNetServer(
new NetServerOptions().setPort(1234).setHost("localhost")
);
server.connectHandler(sock -> {
sock.handler(buffer -> {
asyncFile.write(buffer);
if (asyncFile.writeQueueFull()) {
sock.pause();
}
});
}).listen();
离目标很接近了,但还差一步。
现在当文件队列满时 NetSocket 会被暂停,但当写入队列消化完积压的数据后,我们还需要恢复(resume)它的读取:
NetServer server = vertx.createNetServer(
new NetServerOptions().setPort(1234).setHost("localhost")
);
server.connectHandler(sock -> {
sock.handler(buffer -> {
asyncFile.write(buffer);
if (asyncFile.writeQueueFull()) {
sock.pause();
asyncFile.drainHandler(done -> {
sock.resume();
});
}
});
}).listen();
大功告成!
当写入队列准备好接受更多数据时,drainHandler 事件处理器就会被回调,从而恢复 NetSocket 继续读取数据。
在编写 Vert.x 应用程序时,这种场景非常普遍,因此 Vert.x 提供了 pipeTo 方法,自动为你处理所有这类繁琐的流控逻辑。
你只需要传入目标 WriteStream 即可:
NetServer server = vertx.createNetServer(
new NetServerOptions().setPort(1234).setHost("localhost")
);
server.connectHandler(sock -> {
sock.pipeTo(asyncFile);
}).listen();
这段简练的代码与之前冗长示例的效果完全一致,并且它还自动处理了流的异常和终止:当管道以成功或失败状态完成时,目标 WriteStream 都会被正确关闭结束(ended)。
你也可以在管道操作完成时接收通知:
server.connectHandler(sock -> {
// 将 Socket 管道连接到目标文件,并注册处理器监听完成结果
sock.pipeTo(asyncFile)
.onComplete(ar -> {
if (ar.succeeded()) {
System.out.println("管道传输成功");
} else {
System.out.println("管道传输失败");
}
});
}).listen();
当目标资源的初始化是异步过程时,你可以创建一个 Pipe 实例,
它会先暂停源端读取,直到管道连接至目标端时再恢复源端:
server.connectHandler(sock -> {
// 创建一个用于异步连接的管道
Pipe<Buffer> pipe = sock.pipe();
// 打开目标文件
fs.open("/path/to/file", new OpenOptions())
.onComplete(ar -> {
if (ar.succeeded()) {
AsyncFile file = ar.result();
// 将 Socket 传输到文件,并在传输完成时关闭文件
pipe.to(file);
} else {
sock.close();
}
});
}).listen();
当你需要中止传输时,直接关闭该管道即可:
vertx.createHttpServer()
.requestHandler(request -> {
// 创建一个异步使用的管道
Pipe<Buffer> pipe = request.pipe();
// 打开目标文件
fs.open("/path/to/file", new OpenOptions())
.onComplete(ar -> {
if (ar.succeeded()) {
AsyncFile file = ar.result();
// 将数据流管道传输到文件,完成后关闭文件
pipe.to(file);
} else {
// 关闭管道并恢复请求流,请求体中的 Buffer 数据将被丢弃
pipe.close();
// 发送错误响应
request.response().setStatusCode(500).end();
}
});
}).listen(8080);
当管道关闭时,流上的各类处理器会被解绑重置,且 ReadStream 会恢复读取。
如上所述,默认情况下当流完成时,目标端总是会自动关闭结束。你可以在管道对象上精细控制此行为:
-
endOnFailure:控制发生失败时是否结束目标端 -
endOnSuccess:控制读取流正常结束时是否结束目标端 -
endOnComplete:控制无论何种完成状态是否都结束目标端
以下是一个简短示例:
src.pipe()
.endOnSuccess(false)
.to(dst)
.onComplete(rs -> {
// 追加一些文本内容,然后手动关闭文件
dst.end(Buffer.buffer("done"));
});
现在我们更深入地探讨 ReadStream 和 WriteStream 的具体方法:
ReadStream 读取流
ReadStream 被众多核心组件实现,包括 HttpClientResponse、DatagramSocket、
HttpClientRequest、HttpServerFileUpload、
HttpServerRequest、MessageConsumer、
NetSocket、WebSocket 以及 AsyncFile。
-
handler: 设置用于接收 ReadStream 数据的处理器。 -
pause: 暂停流。暂停期间,处理器不会收到任何新数据项。 -
fetch: 从流中拉取指定数量的数据项。有数据到达时会调用处理器。多次 fetch 的数量是累加的。 -
resume: 恢复流的读取。有数据到达时将调用处理器。调用resume()等价于拉取Long.MAX_VALUE个数据项。 -
exceptionHandler: 当 ReadStream 发生异常时被调用。 -
endHandler: 当流读取到达末尾时被调用。若 ReadStream 代表文件,则在遇到 EOF 时触发;若是 HTTP 请求,则在请求体结束时触发;若是 TCP Socket,则在连接关闭时触发。
读取流始终处于以下两种模式之一:流动模式(flowing)或拉取模式(fetch):
-
当流处于 flowing 模式时,新元素一旦到达就会立刻递送给处理器。
-
当流处于 fetch 模式时,仅当有显式请求时,才会将请求数量的元素递送给处理器。
当获取到一个热读取流(hot stream,例如 HttpServerRequest)时,流默认处于 flowing 模式;而当获取到一个冷读取流(cold stream,例如 AsyncFile)时,流默认处于 fetch 模式且初始需求量为 0。
以下方法会切换流的工作模式:
-
resume()将流切换为 flowing 模式。 -
pause()将流切换为 fetch 模式,并将当前的拉取需求量重置为0。 -
fetch(long)请求特定数量的元素,并将其累加到实际需求量中。
WriteStream 写入流
WriteStream 被众多核心组件实现,包括 HttpClientRequest、HttpServerResponse、
WebSocket、NetSocket 以及 AsyncFile。
核心方法:
-
write: 向 WriteStream 写入对象。该方法绝不阻塞。写入内容会在内部排队,并异步写入到底层资源中。 -
setWriteQueueMaxSize: 设置写入队列被视为“满”的对象阈值;一旦超过该阈值,writeQueueFull方法将返回true。请注意,即使写入队列已被视为满,继续调用 write 仍然会接收数据并进入队列。具体数量的衡量取决于流的具体实现,对于Buffer,该大小代表实际写入的字节数而非 Buffer 对象的个数。 -
writeQueueFull: 如果写入队列被视为已满,则返回true。 -
exceptionHandler: 当WriteStream发生异常时被调用。 -
drainHandler: 当WriteStream队列清空并被视为不再处于满状态时被回调。
归约流(Reducing streams)
Java Collector 收集器可以像 java.util.Stream 那样对 ReadStream 进行规约计算,但整个过程完全是以异步非阻塞方式进行的:
Future<Long> result = stream.collect(Collectors.counting());
result.onSuccess(count -> System.out.println("Stream emitted " + count + " elements"));
注意:调用 collect 会覆盖之前在流上设置的任何数据处理器。
记录解析器(Record Parser)
记录解析器允许你轻松解析由特定字节序列(分隔符)或固定大小记录分隔的流协议。它将输入的字节 Buffer 序列转换为按指定规则结构化(定长或定界切分)的 Buffer 序列。
例如,如果你面对一个以 '\n' 分隔的简单 ASCII 文本流协议,输入如下所示:
buffer1:HELLO\nHOW ARE Y
buffer2:OU?\nI AM
buffer3: DOING OK
buffer4:\n
记录解析器将输出如下完整的记录:
buffer1:HELLO
buffer2:HOW ARE YOU?
buffer3:I AM DOING OK
来看对应的代码实现:
final RecordParser parser = RecordParser.newDelimited("\n", h -> {
System.out.println(h.toString());
});
parser.handle(Buffer.buffer("HELLO\nHOW ARE Y"));
parser.handle(Buffer.buffer("OU?\nI AM"));
parser.handle(Buffer.buffer(" DOING OK"));
parser.handle(Buffer.buffer("\n"));
你也可以通过以下方式解析固定大小的数据块:
RecordParser.newFixed(4, h -> {
System.out.println(h.toString());
});
详情请参阅 RecordParser 类。
JSON 流式解析器(Json Parser)
通常你可以轻松解析常规 JSON 结构,但这要求一次性提供完整的 JSON 内容;然而当需要解析超大 JSON 文件或流时,一次性载入内存通常不可行。
Vert.x 提供的非阻塞 JSON 解析器是一个事件驱动的解析器,专为高效处理超大结构而设计。它能够将输入的 Buffer 数据流转换为一系列精细的 JSON 解析事件。
JsonParser parser = JsonParser.newParser();
// 为各类解析事件设置监听处理器
parser.handler(event -> {
switch (event.type()) {
case START_OBJECT:
// 对象开始
break;
case END_OBJECT:
// 对象结束
break;
case START_ARRAY:
// 数组开始
break;
case END_ARRAY:
// 数组结束
break;
case VALUE:
// 处理值事件
String field = event.fieldName();
if (field != null) {
// 位于对象内部
} else {
// 位于数组或顶层
if (event.isString()) {
} else {
// ...
}
}
break;
}
});
该解析器是完全非阻塞的,事件的派发由传入的 Buffer 驱动:
JsonParser parser = JsonParser.newParser();
// 触发 start array 事件
// 触发 start object 事件
// 触发 "firstName":"Bob" 事件
parser.handle(Buffer.buffer("[{\"firstName\":\"Bob\","));
// 触发 "lastName":"Morane" 事件
// 触发 end object 事件
parser.handle(Buffer.buffer("\"lastName\":\"Morane\"},"));
// 触发 start object 事件
// 触发 "firstName":"Luke" 事件
// 触发 "lastName":"Lucky" 事件
// 触发 end object 事件
parser.handle(Buffer.buffer("{\"firstName\":\"Luke\",\"lastName\":\"Lucky\"}"));
// 触发 end array 事件
parser.handle(Buffer.buffer("]"));
// 必须在结束时调用 end
parser.end();
事件驱动的解析提供了最精细的控制,但代价是需要处理繁杂的底层颗粒度事件。JSON 解析器允许你根据需要在特定结构上将其直接作为整体值(Value)来处理:
JsonParser parser = JsonParser.newParser();
parser.objectValueMode();
parser.handler(event -> {
switch (event.type()) {
case START_ARRAY:
// 开始数组
break;
case END_ARRAY:
// 结束数组
break;
case VALUE:
// 处理每个完整的对象值
break;
}
});
parser.handle(Buffer.buffer("[{\"firstName\":\"Bob\"},\"lastName\":\"Morane\"),...]"));
parser.end();
值模式(Value Mode)可以在解析过程中动态开启和切换,从而允许你在细粒度事件与完整的 JSON 对象值事件之间自由切换:
JsonParser parser = JsonParser.newParser();
parser.handler(event -> {
// 开始对象处理
switch (event.type()) {
case START_OBJECT:
// 开启对象值模式以直接处理每个条目,此后解析器将不再逐一发出 start object 事件
parser.objectValueMode();
break;
case VALUE:
// 处理每个对象条目
// 获取当前对象所属的字段名
String id = event.fieldName();
System.out.println("用户 ID " + id + " : " + event.value());
break;
case END_OBJECT:
// 切回对象事件模式,解析器重新恢复发射 start/end object 细粒度事件
parser.objectEventMode();
break;
}
});
parser.handle(Buffer.buffer("{\"39877483847\":{\"firstName\":\"Bob\"},\"lastName\":\"Morane\"),...}"));
parser.end();
对于数组同样支持这种模式切换:
JsonParser parser = JsonParser.newParser();
parser.handler(event -> {
switch (event.type()) {
case START_OBJECT:
// 开启数组值模式,解析器将不再逐一发射 start array 事件
parser.arrayValueMode();
break;
case VALUE:
// 处理每个数组
System.out.println("值 : " + event.value());
break;
case END_OBJECT:
// 切回数组事件模式
parser.arrayEventMode();
break;
}
});
parser.handle(Buffer.buffer("[0,1,2,3,4,...]"));
parser.end();
你还可以直接解码为 POJO 对象:
parser.handler(event -> {
// 处理每个解析对象
String id = event.fieldName();
User user = event.mapTo(User.class);
System.out.println("用户 ID " + id + " : " + user.firstName + " " + user.lastName);
});
只要解析器在处理 Buffer 时发生故障,就会抛出异常,除非你提前配置了异常处理器:
JsonParser parser = JsonParser.newParser();
parser.exceptionHandler(err -> {
// 捕获任何流解析或反序列化解码错误
});
该解析器还能处理连续的 JSON 流:
-
连缀拼接的 JSON 流:
{"temperature":30}{"temperature":50} -
按行换行分隔的 JSON 流(NDJSON):
{"an":"object"}\r\n3\r\n"a string"\r\nnull
更多细节请查阅 JsonParser 类。
线程安全性 (Thread Safety)
绝大部分 Vert.x 核心对象都是跨线程并发安全的。然而,当你在创建该对象的同一个 Context 上下文中访问它时,整体性能往往是极致最佳的。
例如,如果你在一个 Verticle 中创建了 NetServer,那么最好始终从该 Verticle 的 Event Loop 中操作该连接对象。只要你遵循 Vert.x 的标准组件隔离模型,这一切就会自然得到保证。
运行阻塞式代码
在一个理想的乌托邦世界里,所有 API 都是纯异步非阻塞的。
然而,现实世界并非如此完美。
现实中,JVM 生态内的大多数类库(例如最基础的 JDBC API)本质上都是同步阻塞的。无论 Vert.x 如何强大,也无法施展魔法瞬间让 JDBC 变成非阻塞。
因此,Vert.x 必须为你提供一套安全的机制,让你能在异步应用中平滑、安全地整合传统的阻塞式 API。
正如前文强调的黄金法则,你绝不能直接在 Event Loop 中调用阻塞操作。正确的做法是通过 executeBlocking 将阻塞逻辑转交至 Worker 线程池执行,并返回一个异步 Future:
vertx.executeBlocking(() -> {
// 在此处调用可能导致线程阻塞的第三方阻塞 API
return someAPI.blockingMethod("hello");
}).onComplete(res -> {
System.out.println("阻塞任务执行结果: " + res.result());
});
提交至 executeBlocking 的任务耗时应当在合理范围内(例如几秒以内)。严禁执行无限死循环或极度漫长的轮询。若任务阻塞超过 10 秒,系统会输出警告。长时间持续运行的独立任务应当使用由应用自行管理的专用线程,并通过 Event Bus 或 runOnContext 与 Verticle 交互。
|
默认情况下,在同一个 Context 内连续发起的多次 executeBlocking 会按照提交顺序严格串行执行。
如果你不在意执行顺序,可以将 ordered 参数指定为 false,此时各阻塞任务将在 Worker 线程池中并发执行。
另外,使用独立命名的 WorkerExecutor 可以开辟隔离的自定义工作池:
WorkerExecutor executor = vertx.createSharedWorkerExecutor("my-worker-pool", 10);
executor.executeBlocking(() -> {
return someAPI.blockingMethod("hello");
}).onComplete(res -> {
System.out.println("执行完成: " + res.result());
});
不再需要该执行器时必须记得关闭它:executor.close()。如果在 Verticle 内部创建,Vert.x 会在卸载时自动为你关闭。
Vert.x SPI 扩展机制
Vert.x 提供了多处称为 SPI (Service Provider Interface) 的可插拔扩展点,通常基于 Java 标准的 ServiceLoader 机制在类路径上自动发现加载。
指标度量与链路追踪 SPI
默认情况下,Vert.x 不主动采集指标也不记录 Tracing。相反,它暴露了标准的 SPI 接口,允许第三方监控平台无缝接入(如 Micrometer、OpenTelemetry、Zipkin 等)。
集群管理器 SPI (Cluster Manager SPI)
在分布式集群模式下,集群管理器负责: * 集群中 Vert.x 节点的自动发现与成员关系维护 * 维护全集群范围的 Event Bus 主题订阅列表 * 分布式共享 Map 支持 * 分布式互斥锁 (Distributed Locks) * 分布式原子计数器 (Distributed Counters)
Vert.x 官方默认提供的集群管理器基于 Hazelcast,但由于集群层是全插拔设计的,你可以自由替换为 Infinispan、Apache Ignite 或 Apache ZooKeeper。
Vert.x 构建器模式 (Builder)
除传统的 Vertx.vertx() 与 Vertx.clusteredVertx() 外,你还可以使用更现代的 Builder 建造者模式来编排创建 Vertx 实例:
Vertx vertx = Vertx.builder()
.with(options)
.withTracer(tracerFactory)
.withMetrics(metricsFactory)
.build();
对于集群模式:
Future<Vertx> vertx = Vertx.builder()
.with(options)
.withClusterManager(clusterManager)
.buildClustered();
日志系统整合
Vert.x 内部使用统一的日志抽象 API,并能够无缝适配各种主流的日志底层实现框架。
日志实现的挑选优先级如下:
1. 系统属性 vertx.logger-delegate-factory-class-name 指定的工厂;
2. Classpath 中存在 vertx-default-jul-logging.properties 时使用 JDK 日志 (JUL);
3. 类路径中存在的第三方日志门面,按此优先级:SLF4J → Log4J2;
4. 默认兜底使用 JDK 自带日志 (JUL)。
Netty 内部日志控制
Netty 拥有自己独立的日志推导体系,其优先级同样为 SLF4J → Log4J → Log4J2 → java.util.logging。
你可以通过显式调用设置 Netty 的内部日志工厂:
// 强制让 Netty 使用 Log4j 2 日志框架
InternalLoggerFactory.setDefaultFactory(Log4J2LoggerFactory.INSTANCE);
域名与主机名异步解析 (DNS)
为了避免传统阻塞式的 JVM 域名解析卡顿,Vert.x 内置了一套完全异步非阻塞的 DNS 解析器。
域名解析优先查找操作系统的 hosts 文件,随后向 DNS 服务器列表发起并发查询。默认使用操作系统的系统 DNS,或备用公共 DNS(如 8.8.8.8)。
你可以自定义配置 DNS 服务器:
Vertx vertx = Vertx.vertx(new VertxOptions().
setAddressResolverOptions(
new AddressResolverOptions().
addServer("192.168.0.1").
addServer("192.168.0.2:40000"))
);
如某些特殊场景需要恢复使用 JVM 自带的阻塞解析器,可以通过系统属性 -Dvertx.disableDnsResolver=true 进行切换。
|
本地原生网络传输层 (Native Transport)
在 Linux 与 macOS (BSD) 操作系统上,Vert.x 支持激活基于操作系统内核的 本地原生传输层:
Vertx vertx = Vertx.vertx(new VertxOptions().
setPreferNativeTransport(true)
);
// 检查 Native 是否成功启用
boolean usingNative = vertx.isNativeTransportEnabled();
System.out.println("当前运行在 Native 传输层: " + usingNative);
Linux 原生 epoll
在 Linux 上引入 Netty epoll 扩展可以解锁更强大的网络选项(如 SO_REUSEPORT、TCP_FASTOPEN 等):
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport-native-epoll</artifactId>
<classifier>linux-x86_64</classifier>
</dependency>
Linux 原生 io_uring
在超高并发场景下引入最前沿的 Linux 内核级异步 I/O:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport-native-io_uring</artifactId>
<classifier>linux-x86_64</classifier>
</dependency>
macOS / BSD 原生 kqueue
在 macOS 上启用高效的 kqueue 事件多路复用:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport-native-kqueue</artifactId>
<classifier>osx-aarch_64</classifier>
</dependency>
安全最佳实践指引
Vert.x 是一个赋予开发者极高自由度的技术工具箱,这赋予了你强大的掌控力,同时也意味着开发者需对系统的安全性承担责任。
Web 应用程序
在构建 Web 应用时,强烈推荐使用 Vert.x-Web 而非仅仅使用 Vert.x Core。Vert.x-Web 内置了路径规范化(防目录遍历穿越攻击)、安全的静态资源挂载以及受控的文件上传安全目录隔离。
集群 Event Bus 流量加密
跨服务器节点组建集群时,节点之间的内部通信默认采用明文传输。如果传输涉及机密敏感数据且节点处于不可信公网,务必为 Event Bus 配置 SSL/TLS 双向加密认证。
配置 Vert.x 本地缓存
当 Vert.x 需要从 Classpath(例如打包在 FatJar 中的文件)中读取静态资产时,由于从 Jar 读取输入流是阻塞操作,Vert.x 默认会将这些文件解压缓存至本地 $CWD/.vertx 目录中。
在只读不可写容器(如 Kubernetes 只读根文件系统)环境中,你可以通过 JVM 参数指定可写缓存路径:
java -jar my-fat.jar vertx.cacheDirBase=/tmp/vertx-cache
在前端静态资源调试开发期间,若希望修改立刻生效而不被缓存干扰,可以设置 -Dvertx.disableFileCaching=true。