Vert.x5高级指南
本指南介绍了Vert.x(5.0)的高级内部内容,旨在解释和讨论以下主题:
- Vert.x设计
- 内部API
- 与Netty的集成
当你希望:
- 更好地理解Vert.x内部机制
- 将Vert.x与第三方库集成
- 使用Netty和Vert.x进行网络编程时,可阅读本指南。
本指南为实时更新版本,你可以通过在代码仓库中提交PR或创建Issue参与贡献。
本指南中公开了一些Vert.x内部API,请注意这些API可能会根据需要进行更改。 |
---|
Vert.x中的上下文
io.vertx.core.Context
接口是Vert.x的核心组件。从高层次来看,上下文可以被视为控制应用如何执行事件(或由处理程序创建的任务)的执行流程。
大多数事件通过上下文进行调度,当应用消费事件时,很可能会有一个与之关联的上下文用于事件的调度。
Verticle上下文
部署Verticle实例时,Vert.x会为该实例创建并关联一个上下文。你可以在Verticle中通过AbstractVerticle
的context
字段访问此上下文:
public class MyVerticle extends AbstractVerticle {
public void start() {
JsonObject config = context.config();
}
}
当部署MyVerticle
时,Vert.x会发出start
事件,start
方法由Verticle上下文的线程调用:
- 默认情况下,上下文始终是事件循环上下文,调用线程是事件循环线程。
- 当Verticle作为工作线程部署时,调用线程是Vert.x工作线程池中的一个线程。
临时上下文
自Vert.x 3起,支持在不使用Verticle的情况下使用Vert.x API,这就引出了一个有趣的问题:使用的是哪个上下文?
当调用Vert.x API时,Vert.x会将当前线程与一个临时事件循环上下文相关联。Vertx#getOrCreateContext()
方法会在首次为非Vert.x线程调用时创建一个上下文,后续调用则返回该上下文。
因此,异步Vert.x API的回调会在同一个上下文上执行:
public class Main {
public static void main(String[] args) {
WebClient client = WebClient.create(vertx);
for (int i = 0; i < 4; i++) {
client
.get(8080, "myserver.mycompany.com", "/some-uri")
.send()
.onSuccess(ar -> {
// 所有回调都在同一个上下文上
});
}
}
}
这种行为与之前的主要版本不同,Vert.x 3会为每个HTTP请求创建不同的上下文。尽管Vert.x鼓励将代码限制在上下文中,但这种行为避免了潜在的数据竞争。
上下文传播
大多数Vert.x API都了解上下文。在上下文中执行的异步操作将使用相同的上下文回调应用程序。同样,事件处理程序也会在同一个上下文上调度。
public class MyVerticle extends AbstractVerticle {
public void start() {
Future<HttpServer> future = vertx.createHttpServer()
.requestHandler(request -> {
// 在Verticle上下文中执行
})
.listen(8080, "localhost");
future.onComplete(ar -> {
// 在Verticle上下文中执行
});
}
}
处理上下文
大多数应用程序不需要与上下文进行紧密交互,但有时访问上下文会很有用,例如,当应用程序使用另一个在自己的线程上执行回调的库,而你希望在原始上下文中执行代码时。
如上所述,Verticle可以通过context
字段访问其上下文,但这意味着需要使用Verticle并引用Verticle,这可能并不总是方便的。
你可以使用getOrCreateContext()
获取当前上下文:
Context context = vertx.getOrCreateContext();
你也可以使用静态方法Vertx.currentContext()
:
Context context = Vertx.currentContext();
后者如果当前线程未与上下文关联,可能会返回null,而前者会在需要时创建一个上下文,因此永远不会返回null。
获取上下文后,你可以使用它在该上下文中运行代码:
public void integrateWithExternalSystem(Handler<Event> handler) {
// 捕获当前上下文
Context context = vertx.getOrCreateContext();
// 在应用程序上下文上运行事件处理程序
externalSystem.onEvent(event -> {
context.runOnContext(v -> handler.handle(event));
});
}
实际上,许多Vert.x API和第三方库都是这样实现的。
事件循环上下文
事件循环上下文使用事件循环来运行代码:处理程序直接在IO线程上执行,因此:
- 处理程序始终在同一个线程上执行。
- 处理程序绝不能阻塞线程,否则会导致与该事件循环关联的所有IO任务出现饥饿。
这种行为通过保证关联的处理程序始终在同一个线程上执行,极大地简化了线程模型,从而无需同步和其他锁定机制。
这是默认且最常用的上下文类型。未使用工作线程标志部署的Verticle始终使用事件循环上下文进行部署。
工作线程上下文
工作线程上下文分配给启用了工作线程选项部署的Verticle。工作线程上下文与标准事件循环上下文的区别在于,工作线程在单独的工作线程池中执行。
与事件循环线程的分离允许工作线程上下文执行会阻塞事件循环的阻塞操作:阻塞此类线程除了阻塞一个线程外,不会影响应用程序。
与事件循环上下文一样,工作线程上下文确保处理程序在任何给定时间只在一个线程上执行。也就是说,在工作线程上下文上执行的处理程序将始终按顺序执行——一个接一个——但不同的操作可能在不同的线程上执行。
上下文异常处理程序
可以在上下文中设置异常处理程序,以捕获在上下文中运行的任务抛出的任何未检查异常。如果未设置异常处理程序,则会调用Vertx
的异常处理程序。
context.exceptionHandler(throwable -> {
// 处理此上下文抛出的任何异常
});
vertx.exceptionHandler(throwable -> {
// 处理上下文中未捕获的任何异常
});
如果完全未设置处理程序,异常将被记录为错误,消息为“Unhandled exception”。你可以使用reportException
在上下文中报告异常:
context.reportException(new Exception());
触发事件
runOnContext
是在上下文中执行一段代码的最常用方法。尽管它非常适合将外部库与Vert.x集成,但对于将在事件循环级别执行的代码(如Netty事件)与应用程序代码集成,并不总是最佳选择。
根据情况,有一些内部方法可以实现类似的行为:
ContextInternal#dispatch(E, Handler<E>)
ContextInternal#execute(E, Handler<E>)
ContextInternal#emit(E, Handler<E>)
调度(dispatch)
dispatch
假定调用者线程是上下文线程,它将当前执行线程与上下文关联:
assertNull(Vertx.currentContext());
context.dispatch(event, evt -> {
assertSame(context, Vertx.currentContext());
});
处理程序还会被阻塞线程检查器监控。最后,处理程序抛出的任何异常都会报告给上下文:
context.exceptionHandler(err -> {
// 应该接收下面抛出的异常
});
context.dispatch(event, evt -> {
throw new RuntimeException();
});
执行(execute)
execute
在上下文中执行任务。如果调用者线程已经是上下文线程,则直接执行任务;否则,会安排任务执行。
触发(emit)
emit
是execute
和dispatch
的组合:
default void emit(E event, Handler<E> eventHandler) {
execute(v -> dispatch(argument, task));
}
emit
可以从任何线程向处理程序触发事件:
- 从任何线程调用时,其行为类似于
runOnContext
。 - 从上下文线程调用时,它会使用上下文线程的本地关联、阻塞线程检查器,并在上下文中报告失败。
在大多数情况下,emit
方法是让应用程序处理事件的方式。dispatch
和execute
方法的主要目的是为代码提供更多控制,以实现非常特定的功能。
上下文感知的Future
在Vert.x 4之前,Future
是静态创建的对象,与上下文没有特定关系。Vert.x 4提供了基于Future的API,该API需要遵循与Vert.x 3相同的语义:Future上的任何回调都应可预测地在同一个上下文上运行。
Vert.x 4 API创建绑定到调用者上下文的Future,这些Future在上下文中运行回调:
Promise<String> promise = context.promise();
Future<String> future = promise.future();
future.onSuccess(handler);
上面的代码与以下代码非常相似:
Promise<String> promise = Promise.promise();
Future<String> future = promise.future();
future.onSuccess(result -> context.emit(result, handler));
此外,该API还允许创建成功和失败的Future:
Future<String> succeeded = context.succeededFuture("OK usa");
Future<String> failed = context.failedFuture("Oh sorry");
上下文与追踪
从Vert.x 4开始,Vert.x与流行的分布式追踪系统集成。追踪库通常依赖线程本地存储来传播追踪数据,例如,处理HTTP请求时收到的追踪信息应在整个HTTP客户端中传播。
Vert.x以类似的方式集成追踪,但依赖上下文而不是线程本地存储。上下文确实由Vert.x API传播,因此为实现追踪提供了可靠的存储。
由于给定服务器处理的所有HTTP请求都使用创建HTTP服务器的同一个上下文,因此服务器上下文会为每个HTTP请求复制,以确保每个HTTP请求的唯一性。
public class MyVerticle extends AbstractVerticle {
public void start() {
vertx.createHttpServer()
.requestHandler(request -> {
// 在复制的Verticle上下文中执行
})
.listen(8080, "localhost");
}
}
复制共享原始上下文的大部分特征,并提供特定的本地存储。
vertx.createHttpServer()
.requestHandler(request -> {
JsonObject specificRequestData = getRequestData(request);
Context context = vertx.getOrCreateContext();
context.putLocal("my-stuff", specificRequestData);
processRequest(request);
})
.listen(8080, "localhost");
以后应用程序可以使用它:
Context context = vertx.getOrCreateContext();
JsonObject specificRequestData = context.getLocal("my-stuff");
ContextInternal#duplicate()
复制当前上下文,可用于确定与追踪关联的活动范围:
public void startProcessing(Request request) {
Context duplicate = context.duplicate();
request.setContext(duplicate);
}
关闭钩子(Close Hooks)
关闭钩子是Vert.x的内部功能,用于创建在Verticle或Vertx实例关闭时收到通知的组件。它可用于实现Verticle中的自动清理功能,如Vert.x HTTP服务器。
接收关闭通知的契约由io.vertx.core.Closeable
接口及其close(Promise<Void> closePromise)
方法定义:
@Override
public void close(Promise<Void> completion) {
// 执行清理操作,该方法将完成Future
doClose(completion);
}
ContextInternal#addCloseHook
方法注册一个Closeable
实例,以便在上下文关闭时收到通知:
context.addCloseHook(closeable);
由Verticle部署创建的上下文会在Verticle实例停止时调用钩子。否则,钩子会在Vertx实例关闭时调用。
Context#removeCloseHook
取消注册关闭钩子,当资源在关闭钩子调用之前关闭时,应使用该方法。
context.removeCloseHook(closeable);
钩子使用弱引用实现以避免泄漏,尽管如此,仍应取消注册钩子。在复制上下文中添加钩子会将钩子添加到原始上下文。同样,VertxInternal
也暴露了相同的操作,以在Vertx实例关闭时接收通知。
与Netty集成
Netty是Vert.x的依赖项之一。事实上,Netty为Vert.x的网络服务提供动力。Vert.x Core提供了此类库应具备的基本网络服务:
- TCP
- HTTP
- UDP
- DNS
这些都是使用Netty的各种组件构建的。Netty社区实现了广泛的组件,本章将解释如何将这些组件集成到Vert.x中。
在本章中,我们将构建一个TIME协议的客户端和服务器。Netty文档提供了这个简单协议的客户端/服务器实现,我们将重点关注这些组件的集成。
Netty集成点
本章的主要目的是解释Vert.x的一些内部接口。这些接口是扩展,暴露了与Netty交互的低级方法,对直接重用Netty的组件很有用。
大多数用户不需要处理此扩展,因此这些方法被隔离在扩展接口中。 |
---|
客户端引导(Bootstrapping Clients)
ContextInternal
扩展了io.vertx.core.Context
,并像VertxInternal
一样暴露了各种Netty集成点。通常,上下文通过Vertx#getOrCreateContext()
方法获取,该方法返回当前执行上下文,必要时创建新上下文:在Verticle中调用时,getOrCreateContext()
返回该Verticle的上下文;在非Vert.x线程(如main线程或单元测试)中使用时,会创建并返回新上下文。
Context context = vertx.getOrCreateContext();
// 强制转换以访问额外方法
Internals contextInternal = (Internals) context;
上下文始终与Netty事件循环相关联,因此使用此上下文可确保我们的组件重用现有的事件循环(如果存在),或使用新的事件循环。ContextInternal#nettyEventLoop()
方法返回此特定事件循环,我们可以在Bootstrap
(用于客户端)或ServerBootstrap
(用于服务器)上使用它:
ContextInternal contextInt = (ContextInternal) context;
EventLoop eventLoop = contextInt.nettyEventLoop();
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(eventLoop);
获取与此上下文关联的事件循环 | |
---|---|
为客户端创建引导程序 |
服务器引导(Bootstrapping Servers)
VertxInternal
扩展了io.vertx.core.Vertx
,其中VertxInternal#getAcceptorEventLoopGroup()
返回用于在服务器上接受连接的EventLoopGroup
,其典型用法是在ServerBootstrap
上:
ContextInternal contextInt = (ContextInternal) context;
EventLoop eventLoop = contextInt.nettyEventLoop();
VertxInternal vertxInt = contextInt.owner();
EventLoopGroup acceptorGroup = vertxInt.getAcceptorEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.group(acceptorGroup, eventLoop);
获取与此上下文关联的事件循环 | |
---|---|
获取Vertx的 acceptor 事件循环组 | |
为服务器创建引导程序 |
处理事件
现在我们对ContextInternal
有了更多了解,来看一下如何使用它处理Netty事件,如网络事件、通道生命周期等。ContextInternal#emit
方法用于向应用程序触发事件,因为它确保:
- 上下文并发:重用当前事件循环线程或在工作线程上执行。
- 当前上下文与调度线程的线程本地关联。
- 抛出的任何未捕获异常都会在上下文中报告,此类异常要么被记录,要么传递给
Context#exceptionHandler
。
以下是一个展示服务器引导程序的简短示例:
Handler<Channel> bindHandler = ch -> {
};
bootstrap.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) {
context.emit(ch, bindHandler);
}
});
Promise<Void> bindPromise = context.promise();
bootstrap.bind(socketAddress).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
// 向应用程序发出绑定成功信号
bindPromise.complete();
} else {
// 向应用程序发出绑定错误信号
bindPromise.fail(future.cause());
}
}
});
return bindPromise.future();
emit
的典型用法是向同一个处理程序调度一个或多个事件,如事件处理程序。对于Future,ContextInternal#promise
方法创建一个Promise,其行为与emit
方法的监听器类似。
服务器
原始服务器示例可在此处找到。Vert.x TIME服务器暴露了一个简单的API:
- 创建TimeServer的静态方法。
- 用于绑定服务器的listen方法和用于取消绑定的close方法。
- 用于设置处理请求的处理程序的requestHandler。
public interface TimeServer {
/**
* @return 新的时间服务器
*/
static TimeServer create(Vertx vertx) {
return new TimeServerImpl(vertx);
}
/**
* 设置处理时间请求时调用的处理程序。处理程序应使用时间值完成Future。
*
* @param handler 要调用的处理程序
* @return 此对象
*/
TimeServer requestHandler(Handler<Promise<Long>> handler);
/**
* 启动并绑定时间服务器。
*
* @param port 服务器端口
* @param host 服务器主机
* @return 套接字绑定时完成的Future
*/
Future<Void> listen(int port, String host);
/**
* 关闭时间服务器。
*/
void close();
}
实现一个提供当前JVM时间的TIME服务器非常简单:
Vertx vertx = Vertx.vertx();
// 创建时间服务器
TimeServer server = TimeServer.create(vertx);
server.requestHandler(time -> {
time.complete(System.currentTimeMillis());
});
// 启动服务器
server.listen(8037, "0.0.0.0")
.onSuccess(v -> System.out.println("Server started"))
.onFailure(err -> err.printStackTrace());
服务器引导程序
首先看一下ServerBootstrap
的创建和配置:
EventLoopGroup acceptorGroup = vertx.getAcceptorEventLoopGroup();
EventLoop eventLoop = context.nettyEventLoop();
bootstrap = new ServerBootstrap();
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.group(acceptorGroup, eventLoop);
bootstrap.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
TimeServerHandler handler = new TimeServerHandler(context, requestHandler);
pipeline.addLast(handler);
}
});
VertxInternal返回要用作acceptor组的事件循环组 | |
---|---|
ContextInternal返回要用作child组的事件循环 | |
创建并配置Netty的ServerBootstrap | |
使用使用服务器requestHandler初始化的TimeServerHandler配置通道 |
ServerBootstrap
的创建非常直接,与原始版本非常相似。主要区别在于我们重用了Verticle和Vert.x提供的事件循环,这确保我们的服务器与应用程序共享相同的资源。请注意,TimeServerHandler
使用服务器的requestHandler
初始化,该处理程序将在处理TIME请求时使用。
服务器绑定
现在看一下绑定操作,同样非常简单,与原始示例没有太大区别:
Promise<Void> promise = context.promise();
ChannelFuture bindFuture = bootstrap.bind(host, port);
bindFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
if (future.isSuccess()) {
channel = future.channel();
promise.complete();
} else {
promise.fail(future.cause());
}
}
});
return promise.future();
创建绑定到服务器上下文的Promise | |
---|---|
完成或成功处理结果Promise | |
返回Future结果 |
最重要的部分是创建上下文Promise,使应用程序能够了解绑定结果。
服务器处理程序
现在用TimeServerHandler
完成我们的服务器,它是Netty原始TimeServerHandler
的改编版本:
Promise<Long> result = Promise.promise();
context.emit(result, requestHandler);
result.future().onComplete(ar -> {
if (ar.succeeded()) {
ByteBuf time = ctx.alloc().buffer(4);
time.writeInt((int) (ar.result() / 1000L + 2208988800L));
ChannelFuture f = ctx.writeAndFlush(time);
f.addListener((ChannelFutureListener) channelFuture -> ctx.close());
} else {
ctx.close();
}
});
创建一个将由requestHandler解析的新空白Promise | |
---|---|
让上下文使用emit向requestHandler触发事件 | |
当requestHandler实现完成关联的Promise时,调用Future处理程序 | |
将当前时间写入通道,并在之后关闭它 | |
如果应用程序失败,我们只需关闭套接字 |
当发生TIME请求事件时,使用emit
将需要完成的Promise传递给requestHandler
。当此Promise完成时,处理程序将把时间结果写入通道或关闭通道。
客户端
原始客户端示例可在此处找到。Vert.x时间客户端暴露了一个简单的API:
- 创建TimeClient的静态方法。
- 用于从服务器检索时间值的客户端getTime方法。
public interface TimeClient {
/**
* @return 新的时间客户端
*/
static TimeClient create(Vertx vertx) {
return new TimeClientImpl(vertx);
}
/**
* 从服务器获取当前时间。
*
* @param port 服务器端口
* @param host 服务器主机名
* @return 结果Future
*/
Future<Long> getTime(int port, String host);
}
TIME客户端的使用非常简单:
Vertx vertx = Vertx.vertx();
// 创建时间客户端
TimeClient server = TimeClient.create(vertx);
// 获取时间
server.getTime(8037, "localhost").onComplete(ar -> {
if (ar.succeeded()) {
System.out.println("Time is " + new Date(ar.result()));
} else {
ar.cause().printStackTrace();
}
});
客户端引导程序
首先看一下Bootstrap
的创建和配置:
EventLoop eventLoop = context.nettyEventLoop();
// 创建并配置Netty引导程序
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoop);
bootstrap.channel(NioSocketChannel.class);
bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
bootstrap.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new TimeClientHandler(result));
}
});
return bootstrap;
ContextInternal返回要用作child组的事件循环 | |
---|---|
创建并配置Netty的Bootstrap | |
使用使用服务器resultHandler初始化的TimeServerHandler配置通道 |
Bootstrap
的创建非常直接,与原始版本非常相似。主要区别在于我们重用了Verticle提供的事件循环,这确保我们的客户端与Verticle重用相同的事件循环。与服务器示例一样,我们使用ContextInternal
获取Netty的EventLoop
并设置到Bootstrap
上。请注意,TimeServerHandler
使用客户端的resultHandler
初始化,该处理程序将在收到TIME请求结果时调用。
客户端连接
引导程序设置与原始示例非常相似,在失败的情况下,应用程序回调使用一个保存整体结果的Promise。
ChannelFuture connectFuture = bootstrap.connect(host, port);
connectFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
result.fail(future.cause()); // 2
}
}
});
连接到服务器 | |
---|---|
连接错误时,使Promise失败 |
我们只关心将连接失败传播给应用程序,当引导程序成功连接时,TimeServerHandler
将处理对应用程序的网络响应。
客户端处理程序
现在用TimeServerHandler
完成我们的客户端,它是Netty原始TimeClientHandler
的改编版本:
ByteBuf m = (ByteBuf) msg;
long currentTimeMillis;
try {
currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L;
resultPromise.complete(currentTimeMillis);
resultPromise = null;
ctx.close();
} finally {
m.release();
}
解码来自服务器的时间响应 | |
---|---|
使用响应完成resultPromise | |
将resultPromise设置为null | |
关闭通道 |
同样,当发生TIME响应事件时,我们完成resultPromise
。
使用Netty TCP编解码器
在前面的部分中,我们研究了Vert.x和Netty如何共享资源以及Netty事件向Vert.x应用程序的传播。在本节中,我们将研究现有Netty编解码器的集成。
Netty编解码器非常适合封装和重用网络协议的编码器和解码器。Netty的基本发行版提供了一些流行协议的编解码器,如HTTP、Redis、Memcached或MQTT。
客户端和服务器可以在这些编解码器的基础上使用Vert.x构建,例如,Vert.x HTTP组件重用Netty的HTTP编解码器,Vert.x的MQTT协议也是如此。Vert.x TCP客户端/服务器可以自定义以重用Netty编解码器。实际上,NetSocket
的通道可用于自定义管道并读写任意消息。
以这种方式重用NetSocket
有很多价值:
- 扩展Vert.x生态系统,你的客户端/服务器将与该生态系统完全集成,即你可以将中间件与现有的Vert.x中间件、文件系统等混合搭配。
- 基于
NetSocket
功能构建:SSL/TLS、域套接字、客户端Socks/HTTP代理处理、服务器Verticle扩展、指标、SNI处理。
在本章中,我们将编写一个客户端,但同样的技术也可以用于以相同的方式在Netty编解码器的基础上编写服务器。
本章中实现的一切也可以使用“与Netty集成”一章中展示的技术实现。 |
---|
Memcached客户端
作为示例,本章我们将在Netty的Memcached二进制编解码器的基础上构建一个简单的Memcached客户端。Memcached是一种流行的免费开源、高性能、分布式内存对象缓存系统。
该协议有两个版本,文本版和二进制版。在本节中,我们将为该文档中描述的二进制协议构建一个客户端。
客户端的使用非常简单:
Vertx vertx = Vertx.vertx();
MemcachedClient.connect(vertx, 11211, "localhost")
.compose(client -> {
System.out.println("connected");
// 存储一个值
return client.set("foo", "bar").compose(v -> {
System.out.println("Put successful");
// 现在检索相同的值
return client.get("foo");
});
}).onSuccess(res -> {
System.out.println("Get successful " + res + "");
}).onFailure(err -> err.printStackTrace());
你可以使用Docker轻松启动Memcached服务器来尝试此示例:
docker run --rm --name my-memcache -p 11211:11211 -d memcached
Memcached客户端剖析
客户端提供了一个简单的API,用于连接到Memcached服务器并获取/设置条目。
public interface MemcachedClient {
/**
* 连接到Memcached,completionHandler将获取MemcachedClient实例。
*/
static Future<MemcachedClient> connect(Vertx vertx, int port, String host) {
return MemcachedClientImpl.connect(vertx, port, host, null);
}
/**
* 连接到Memcached,completionHandler将获取MemcachedClient实例。
*/
static Future<MemcachedClient> connect(Vertx vertx, int port, String host, NetClientOptions options) {
return MemcachedClientImpl.connect(vertx, port, host, options);
}
/**
* 获取缓存条目。
*
* @param key 条目键
* @return 结果Future
*/
Future<@Nullable String> get(String key);
/**
* 设置缓存条目。
*
* @param key 条目键
* @param value 条目值
* @return 结果Future
*/
Future<Void> set(String key, String value);
}
Memcached编解码器
Netty提供的Memcached编解码器负责将Netty的ByteBuf
与Memcached请求和响应进行编码和解码。我们的客户端只需要使用Memcached对象:
- 向管道写入
FullBinaryMemcacheRequest
:- 有一个
key
属性:提供缓存条目键的ByteBuf
。 - 有一个
opCode
属性:指示操作的枚举,GET
和SET
。 - 有一个
extras
属性:提供额外信息的ByteBuf
,仅在MemcachedSET
请求中使用。 - 有一个
content
属性:提供缓存条目值的ByteBuf
,仅在MemcachedSET
请求中使用。
- 有一个
- 从管道读取
FullBinaryMemcacheResponse
:- 有一个
status
属性:操作成功时值为0。 - 有一个
content
属性:提供缓存条目值的ByteBuf
,仅在MemcachedGET
响应中使用。
- 有一个
Memcached提供的协议比GET或SET更丰富,但我们在本节中不涉及,因为目标只是演示,而不是完整的客户端。 |
---|
连接到服务器
首先看一下客户端连接的实现:
NetClient tcpClient = options != null ? vertx.createNetClient(options) : vertx.createNetClient();
// 连接到Memcached实例
Future<NetSocket> connect = tcpClient.connect(port, host);
return connect.map(so -> {
// 创建客户端
MemcachedClientImpl memcachedClient = new MemcachedClientImpl((VertxInternal) vertx, (NetSocketInternal) so);
// 初始化客户端:配置管道并设置处理程序
memcachedClient.init();
return memcachedClient;
});
connect
实现创建一个Vert.xNetClient
来连接到实际的Memcached服务器。连接成功时:
- Vert.x
NetSocket
被强制转换为NetSocketInternal
。 - 创建并初始化Memcached客户端。
NetSocketInternal
是一个高级接口,提供了一些我们构建客户端所需的额外方法:
channelHandlerContext()
返回NetSocket的Netty处理程序的上下文。writeMessage(Object, Handler<AsyncResult<Void>>)
向管道写入对象。messsageHandler(Handler<Object>)
设置处理管道消息的处理程序。
Memcached客户端的init
方法使用其中一些方法来:
- 使用Memcached编解码器初始化NetSocket。
- 设置消息处理程序来处理Memcached响应。
ChannelPipeline pipeline = so.channelHandlerContext().pipeline();
// 添加Memcached消息聚合器
pipeline.addFirst("aggregator", new BinaryMemcacheObjectAggregator(Integer.MAX_VALUE));
// 添加Memcached解码器
pipeline.addFirst("memcached", new BinaryMemcacheClientCodec());
// 设置消息处理程序以处理Memcached消息
so.messageHandler(this::processResponse);
请求/响应关联
Memcached协议是流水线协议,响应按请求发送的顺序接收。因此,客户端需要维护一个inflight
FIFO队列,这是一个简单的JavaConcurrentLinkedQueue
。当向Memcached服务器发送请求时,响应处理程序会添加到队列中。当收到响应时,处理程序会出队并处理响应。
发送Memcached请求消息
客户端有一个writeRequest
方法,用于向管道发送请求:
- 写入请求消息。
- 写入成功时,将响应处理程序添加到
inflight
队列,以便处理响应。 - 返回:
return so.writeMessage(request).compose(v -> {
// 消息已成功编码并发送
// 创建响应Promise并将其添加到inflight队列,以便服务器确认后解析
Promise<FullBinaryMemcacheResponse> promise = vertx.promise();
inflight.add(promise);
//
return promise.future();
});
处理Memcached响应消息
客户端有一个processResponse
方法,每次Memcached编解码器解码响应时都会调用该方法:
- 出队响应处理程序。
- 释放Netty消息,因为响应消息是池化的,必须调用此方法,否则会发生内存泄漏。
FullBinaryMemcacheResponse response = (FullBinaryMemcacheResponse) msg;
try {
// 获取将处理响应的处理程序
Promise<FullBinaryMemcacheResponse> handler = inflight.poll();
// 处理消息
handler.complete(response);
} finally {
// 释放引用计数消息
response.release();
}
发送Memcached GET请求
MemcachedGET
相当简单:
- 创建
FullBinaryMemcacheRequest
。 - 设置
key
属性。 - 设置
opCode
属性为BinaryMemcacheOpcodes.GET
。 - 调用
writeRequest
传递请求并提供响应处理程序。
ByteBuf keyBuf = Unpooled.copiedBuffer(key, StandardCharsets.UTF_8);
// 创建Memcached请求
FullBinaryMemcacheRequest request = new DefaultFullBinaryMemcacheRequest(keyBuf, Unpooled.EMPTY_BUFFER);
// 设置Memcached操作 opcode 以执行 GET
request.setOpcode(BinaryMemcacheOpcodes.GET);
// 执行请求并处理响应
return writeRequest(request).map(response -> processGetResponse(response));
处理Memcached GET响应
MemcachedGET
响应由processGetResponse
处理:
short status = response.status();
switch (status) {
case 0:
// GET 成功
return response.content().toString(StandardCharsets.UTF_8);
case 1:
// 空响应 -> null
return null;
default:
// Memcached错误
throw new MemcachedError(status);
}
响应的status
属性指示响应是否成功。我们需要特别注意status
为1的情况,因为客户端将其视为Javanull
值。
发送Memcached SET请求
MemcachedSET
也很简单:
- 创建
FullBinaryMemcacheRequest
。 - 设置
key
属性。 - 设置
opCode
属性为BinaryMemcacheOpcodes.SET
。 - 设置
extras
属性为值0xDEADBEEF_00001C20
:0xDEADBEEF
必须按协议使用,00001C20
是设置为2小时的过期时间。 - 设置
value
属性。 - 调用
writeRequest
传递请求并提供响应处理程序。
ByteBuf keyBuf = Unpooled.copiedBuffer(key, StandardCharsets.UTF_8);
// 创建Memcached请求
FullBinaryMemcacheRequest request = new DefaultFullBinaryMemcacheRequest(keyBuf, Unpooled.EMPTY_BUFFER);
// 设置Memcached操作 opcode 以执行 GET
request.setOpcode(BinaryMemcacheOpcodes.GET);
// 执行请求并处理响应
return writeRequest(request).map(response -> processGetResponse(response));
处理Memcached SET响应
MemcachedSET
响应由processSetResponse
处理:
short status = response.status();
if (status == 0) {
// SET 成功
return null;
} else {
// Memcached错误
throw new MemcachedError(status);
}