携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第 3 天
Vertx 官方 DEMO 示栗(三)
Vertx 快速上手指北(三)
第三章: Vertx 中的:EventBus(事件总线)
本章大纲:
- 事件总线 EventBus 是什么?
- 能干什么?
- 怎么用?
- 有哪些消息传递模式?
- 事件总线 EventBus 的集群特性?
- 使用事件总线 EventBus 发布服务接口(请听下下章分解)
个人总结:
- 本章概要:Vertx “事件总线(EventBus)” 的基础用法
- 本章是非常 “干货” ,直接上了一个 “栗子”
- 上完 “栗子” 直接上 “集群栗子” 🥱 呃……
- 等吃饱了,我们再具体说说 EventBus 的消息传递模型
废话不说,直接上栗子
博主亲自上手画的栗子图
graph TD
温度传感器HeatSensor1 --> 传感器问题日志记录程序
温度传感器HeatSensor2 --> 传感器问题日志记录程序
温度传感器HeatSensor... --> 传感器问题日志记录程序
温度传感器HeatSensorN --> 传感器问题日志记录程序
温度传感器HeatSensor1 --> HTTP服务器
温度传感器HeatSensor2 --> HTTP服务器
温度传感器HeatSensor... --> HTTP服务器
温度传感器HeatSensorN --> HTTP服务器
HTTP服务器 --最新数据--> 传感器数据服务SensorData
传感器数据服务SensorData --平均值--> HTTP服务器
HTTP服务器 ---> WEB客户端
HTTP服务器 ---> SSE长连接客户端
图:栗子执行逻辑
- HeatSensor
/**
* 以非固定速率生成温度测量值
* 并将其发布给传感器的订阅者
* 每个 HeatSensor 都有一个唯一的传感器标识符。
*/
public class HeatSensor extends AbstractVerticle {
private final Random random = new Random();
/**
* 温度传感器的唯一 ID
*/
private final String sensorId = UUID.randomUUID().toString();
private double temperature = 21.0;
@Override
public void start() {
scheduleNextUpdate();
}
private void scheduleNextUpdate() {
vertx.setTimer(random.nextInt(5000) + 1000, this::update);
}
/**
* 获得平均温度,并发布最新温度事件,publish 模式
* @param timerId
*/
private void update(long timerId) {
temperature = temperature + (delta() / 10);
JsonObject payload = new JsonObject()
.put("id", sensorId)
.put("temp", temperature);
vertx.eventBus().publish("sensor.updates", payload);
scheduleNextUpdate();
}
private double delta() {
if (random.nextInt() > 0) {
return random.nextGaussian();
} else {
return -random.nextGaussian();
}
}
}
- 传感器问题日志记录程序
/**
* 监视新的温度测量值
* 并使用SLF4J记录它们。
*/
public class Listener extends AbstractVerticle {
private final Logger logger = LoggerFactory.getLogger(Listener.class);
private final DecimalFormat format = new DecimalFormat("#.##");
/**
* 记录日志
*/
@Override
public void start() {
EventBus bus = vertx.eventBus();
bus.<JsonObject>consumer("sensor.updates", msg -> {
JsonObject body = msg.body();
String id = body.getString("id");
String temperature = format.format(body.getDouble("temp"));
logger.info("{} reports a temperature ~{}C", id, temperature);
});
}
}
- HTTP服务器
/**
* HttpServer 创建 HTTP 服务器
* 为 web 界面提供服务
* 每当观察到一个新的温度测量时,它就会向它的客户端推送新值(SSE)
* 并定期请求当前平均值并更新所有客户端。
*/
public class HttpServer extends AbstractVerticle {
/**
* 创建 HTTP 服务器
*/
@Override
public void start() {
vertx.createHttpServer()
.requestHandler(this::handler)
.listen(config().getInteger("port", 8080));
}
/**
* 响应数据
* @param request
*/
private void handler(HttpServerRequest request) {
if ("/".equals(request.path())) {
request.response().sendFile("index.html");
} else if ("/sse".equals(request.path())) {
sse(request);
} else {
request.response().setStatusCode(404);
}
}
/**
* SSE 长连接示栗
* @param request
*/
private void sse(HttpServerRequest request) {
HttpServerResponse response = request.response();
response
// 标记 MIME 为 SSE
.putHeader("Content-Type", "text/event-stream")
// 实时流,禁用浏览器缓存
.putHeader("Cache-Control", "no-cache")
//
.setChunked(true);
/**
* 每个 sse 长连接启动一个订阅,用于实时推送结果数据
*/
MessageConsumer<JsonObject> consumer = vertx.eventBus().consumer("sensor.updates");
consumer.handler(msg -> {
response.write("event: update\n");
response.write("data: " + msg.body().encode() + "\n\n");
});
/**
* 启动一个 request 获得数据
* periodicStream 类似 AIO,有比 setPeriodic 更多的 api
* 支持主动 fetch 模式 pause
* 支持通过 pipe() 将 periodic 中的结果转发给另外的 WriteStream
* 相当于一个 Stream 包装的 setPeriodic
*/
TimeoutStream ticks = vertx.periodicStream(1000);
ticks.handler(id -> {
vertx.eventBus().<JsonObject>request("sensor.average", "", reply -> {
if (reply.succeeded()) {
response.write("event: average\n");
response.write("data: " + reply.result().body().encode() + "\n\n");
}
});
});
// 提供了一种异步接收 response 的思路
response.endHandler(v -> {
consumer.unregister();
ticks.cancel();
});
}
}
- 传感器数据服务SensorData
/**
* SensorData
* 保存每个传感器的最新观测值的记录。
* 它还支持基于 reply 模式的平均值计算: 回复基于最新数据的平均值 average
*/
public class SensorData extends AbstractVerticle {
/**
* 保存每个传感器的最新观测值的记录
*/
private final HashMap<String, Double> lastValues = new HashMap<>();
/**
* 订阅最新的温度记录
* 订阅温度平均值计算
*/
@Override
public void start() {
EventBus bus = vertx.eventBus();
bus.consumer("sensor.updates", this::update);
bus.consumer("sensor.average", this::average);
}
private void update(Message<JsonObject> message) {
JsonObject json = message.body();
lastValues.put(json.getString("id"), json.getDouble("temp"));
}
private void average(Message<JsonObject> message) {
double avg = lastValues.values().stream()
.collect(Collectors.averagingDouble(Double::doubleValue));
JsonObject json = new JsonObject().put("average", avg);
message.reply(json);
}
}
单体部署
public class Main {
public static void main(String[] args) {
Vertx vertx = Vertx.vertx();
vertx.deployVerticle("chapter3.HeatSensor", new DeploymentOptions().setInstances(4));
vertx.deployVerticle("chapter3.Listener");
vertx.deployVerticle("chapter3.SensorData");
vertx.deployVerticle("chapter3.HttpServer");
}
}
集群栗子
- 云端集群部署
public class FirstInstance {
private static final Logger logger = LoggerFactory.getLogger(FirstInstance.class);
/**
* vertx 集群(cluster)模式实例
* @param args
*/
public static void main(String[] args) {
Vertx.clusteredVertx(new VertxOptions(), ar -> {
if (ar.succeeded()) {
logger.info("First instance has been started");
Vertx vertx = ar.result();
// 可以直接使用类型全限定名来启动实例
vertx.deployVerticle("chapter3.HeatSensor", new DeploymentOptions().setInstances(4));
vertx.deployVerticle("chapter3.HttpServer");
} else {
logger.error("Could not start", ar.cause());
}
});
}
}
- 局域网端集群部署
public class SecondInstance {
private static final Logger logger = LoggerFactory.getLogger(SecondInstance.class);
public static void main(String[] args) {
Vertx.clusteredVertx(new VertxOptions(), ar -> {
if (ar.succeeded()) {
logger.info("Second instance has been started");
Vertx vertx = ar.result();
vertx.deployVerticle("chapter3.HeatSensor", new DeploymentOptions().setInstances(4));
vertx.deployVerticle("chapter3.Listener");
vertx.deployVerticle("chapter3.SensorData");
JsonObject conf = new JsonObject().put("port", 8081);
vertx.deployVerticle("chapter3.HttpServer", new DeploymentOptions().setConfig(conf));
} else {
logger.error("Could not start", ar.cause());
}
});
}
}
栗子吃好了,我们来唠唠
-
什么是事件总线
事件总线是一种以异步方式发送和接收消息的方法。向目的地发送消息,并从目的地检索消息。含
- body
- 通常使用 Vert.x JSON 格式进行编码
- 提供多语言支持
- 可以注册自定义编码器/解码器(codecs)
- 博主温馨提示:截至目前,支持力度一般般,可用但不是很灵活(效率至上)
- 支持通过自定义 codecs 直接传输 Java 对象
- 存储元数据的可选 headers
- 过期时间戳 expiration
- 过期未处理的消息将被丢弃
- body
-
事件总线可以扩展到应用程序进程之外
- 事件总线也可以跨集群
- 事件总线可以直连到 嵌入式
- 事件总线可以直连到 外部消息代理
- 事件总线可以直连到 远程客户端
- 事件总线可以直连到 web浏览器中的 js 应用程序
图:除了官方提供的,还有这么多客户端们
消息传递模式
- 点到点消息传递
- 请求-应答消息
- 发布/订阅消息传递
- 感觉不是很好使?请听下回再分解
你这个事件总线,他能当 MQ 用么 🤔?
简而言之 事件总线 是 “事件” 总线, MQ 是消息 MQ
-
和 MQ 相似之处:常见的消息传递模式
- 例如发布/订阅模式,该模式在集成分布式和异构应用程序时非常流行
-
不同之处:
- 用于应用程序内部的 Verticle 间的通信,而不是消息用于应用程序之间的通信总线
-
不同于 ActiveMQ、RabbitMQ、ZeroMQ或Apache Kafka
- Vertx 可以与这些消息 Broker 继承,事件总线不能替代它们
- eventBus 消息不能 ack 确认
- eventBus 消息无优先级
- eventBus 消息不能做路由规则
- eventBus 消息不能做转换规则(模式适应,散/聚,等等)
- eventBus 消息不能从崩溃中恢复(持久化)
-
事件总线仅仅携带用于 Verticle 服务做异步处理的原子(volatile)事件。
你说你这个 EventBus 不能从崩溃中回复,那你这个消息,它丢了咋办
- 官方说的:同时使用中间件(成本更高)来防止事件丢失,请听下回分解。所以,给点时间,博主找找……………………………………😶(PS 博主已经找了好几章了,还是没有………………………………)
集群模式下的 EventBus
启用分布式事件总线对于服务是无感的。
事件总线API具有用于声明消息处理程序的localConsumer方法,当与集群一起运行时,这些消息处理程序仅在本地工作。
本章完……………