Vert.x 学习笔记(一)

222 阅读5分钟

介绍

Vert.x的目标是做基于JVM的Node.js,是来自 Eclipse 的开源、响应式和多语言软件开发工具包,可以嵌入到Spring、Quarkus等框架中。

  • 最开始我总是把Vert.x和Spring进行对比,觉得Vert.x的启动有些太简单了,但是实际上运行一个Vert.x实例就是很简单。
  • 不要把使用Spring框架的固化思维转移到Vert.x上,毕竟两者就不是一个编程范式。

Vert.x的两个核心组件:Verticle和EventBus;

Vert.x的一个核心原则:不要阻塞EventLoop;

Verticle

Verticle 是 Vert.x 中的基本处理单元,作用是封装处理事件的逻辑。

一个Vert.x应用程序通常由多个Verticle 实例组成,不同的 Verticle 实例通过在事件总线上发送消息来相互通信。

EventBus

EventBus 是 Vert.x 的神经系统,它可以连接同一个Vert.x实例或者多个不同的Vert.x实例的Verticle,并确保这些Verticle之间相互协作。

由于 Vert.x 是响应式编程范式,Verticle 被部署成功之后会保持休眠状态,直到收到消息或事件。 Verticle 可以通过 EventBus 相互通信,消息或事件可以是从字符串到复杂对象的任何内容。

理想情况下,消息处理是异步的,消息在 EventBus 上排队,并将控制权返回给发送者。随后它会出列到监听垂直队列。使用 Future 和回调方法发送响应。

基于EventBus可以构建一个跨越多个服务器节点和多个浏览器的分布式点对点消息传递系统。

另外,EventBus 也支持发布/订阅、点对点和请求-响应消息传递。

黄金法则:不要阻塞EventLoop

Vert.x API 是非阻塞的,不会阻塞事件循环。一旦EventLoop被阻塞,那么应用程序也将完全停止。

下面是一些常见的阻塞操作:

  • 线程休眠:Thread.sleep()
  • 等待锁
  • 等待互斥体或监视器,比如synchronized同步锁
  • 执行一个长链接数据库操作并等待结果
  • 进行需要大量时间的复杂计算
  • 循环等待

但在实际的业务不可避免一些阻塞操作,那么如何在Vert.x中执行阻塞操作呢?使用Worker Verticle。

Maven配置

<dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>io.vertx</groupId>
        <artifactId>vertx-stack-depchain</artifactId>
        <version>4.5.8</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
</dependencyManagement>

如果在Mac上运行Vert.x,可能需要添加下面的依赖:

<dependency>
  <groupId>io.netty</groupId>
  <artifactId>netty-resolver-dns-native-macos</artifactId>
  <version>4.1.75.Final</version>
  <classifier>osx-aarch_64</classifier>
  <scope>runtime</scope>
</dependency>

构建一个TCP服务

现在分别使用java.io和Vert.x构建相同的TCP服务,对比一下实现复杂度。

另外也引入Node.js的实现,和Vert.x做一下对比。

使用java.io包构建TCP服务:

package org.xqd.learning.client;

import java.io.*;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;

public class SynchronousEcho {
    public static void main(String[] args) {
        try (ServerSocket server = new ServerSocket()) {
            server.bind(new InetSocketAddress(3000));
            while (true) {   // <1>
                Socket socket = server.accept();
                new Thread(clientHandler(socket)).start();
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static Runnable clientHandler(Socket socket) {
        return () -> {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                 PrintWriter writer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()))) {
                String line = "";
                while (!"/quit".equals(line)) {
                    line = reader.readLine();
                    System.out.println("~ " + line);
                    writer.write(line + "\n");
                    writer.flush();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        };
    }
}
  • 使用ServerSocket创建一个服务器,然后绑定端口;
  • 在while循环内,服务器使用主线程来接受连接,每个连接都分配一个新的线程来处理 I/O。
  • clientHandler方法内需要从socket内接收数据流,然后转换数据格式,再进行处理。

现在使用Vert.x来实现一个相同的TCP服务:

package org.xqd.learning.server;

import io.vertx.core.Vertx;
import io.vertx.core.net.NetSocket;

public class VertxServer {
    public static void main(String[] args) {
        Vertx vertx = Vertx.vertx();
        vertx.createNetServer().connectHandler(VertxServer::handleNewClient).listen(3000);
    }

    private static void handleNewClient(NetSocket socket) {
        socket.handler(buffer -> {
            socket.write(buffer);
            if (buffer.toString().endsWith("/quit\n")) {
                socket.close();
            }
        });
    }
}

使用Vert.x仅仅只需要几行代码就实现了前面的功能。

可以看一下使用Node.js实现一个类似的TCP服务代码:

const net = require('net');

// 创建一个TCP服务器
net.createServer((socket) => {
    // 监听数据事件
    socket.on('data', (data) => {
        console.log('接收到数据:', data.toString());
        // 检查是否接收到"/quit"
        if (data.toString().trim() === '/quit') {
            console.log('接收到/quit,正在关闭连接...');
            socket.end('连接已关闭');
        }
    });
}).listen(3000, () => {
    console.log('服务器正在监听3000端口');
});

同样是函数式编程,也非常地简练。

一个简单的Hello World 的 Vert.x

在前面的示例中只使用了Vert.x,

public class HelloVerticle extends AbstractVerticle {
  public static void main(String[] args) {
    Vertx vertx = Vertx.vertx();
    vertx.deployVerticle(new HelloVerticle());
  }

  @Override
  public void start() throws Exception {
    System.out.println("Welcome to Vertx");
  }
}

因为Vert.x是一个库,而不是一个框架。所以可以在main方法或者任意类中创建一个Vert.x的实例,然后部署Verticle。

上面的例子在main方法中创建一个Vert.x实例,然后部署了一个Verticle。

如果想部署一个Verticle,那么这个类需要继承AbstractVerticle。

部署和下线Verticle

既然Vert.x可以部署Verticle,那么也可以下线Verticle。

程序主入口:

package org.xqd.learning.deployer;

import io.vertx.core.Vertx;

public class Main {
    public static void main(String[] args) {
        Vertx vertx = Vertx.vertx();
        vertx.deployVerticle(new Deployer());
    }
}

主流程在一个Verticle中实现:

package org.xqd.learning.deployer;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.AsyncResult;

public class Deployer extends AbstractVerticle {
    @Override
    public void start() throws Exception {
        int delay = 2000;
        for (int i = 0; i < 50; i++) {
            vertx.setTimer(delay, data -> deployer());
            delay += 1000;
        }
    }

    private void deployer() {
        vertx.deployVerticle(new EmptyVerticle(), this::checkDeployer);
    }

    private void checkDeployer(AsyncResult<String> ar) {
        if (ar.succeeded()) {
            String id = ar.result();
            vertx.setTimer(2000, tid -> undeployer(id));
        } else {
            System.out.println("部署失败");
        }
    }

    private void undeployer(String id) {
        vertx.undeploy(id, ar -> {
            if (ar.succeeded()) {
                System.out.println("这个ID已经下线: " + id);
            } else {
                System.out.println("这个ID下线失败: " + id);
            }
        });
    }
}
  • start方法:使用定时器间隔1秒部署一个Verticle。每次触发定时器执行时会调用deployer方法;
  • deployer方法:使用deployVerticle方法部署一个Verticle,部署后回调checkDeployer方法;
  • checkDeployer方法:通过ar参数判断Verticle是否部署成功,如果部署成功,再次使用定时器,两秒后触发,回调undeployer方法;
  • undeployer方法:使用传过来的id,下线一个Verticle;

在两个Verticle之间传递消息

前面的示例只是一个Verticle,但是如果有多个Verticle,那么Verticle之前如何传递消息呢?就是前面提到的EventBus。

EventBus负责将接收到的消息转发到对应的Handler上。

获取事件总线的引用:

EventBus eb = vertx.eventBus();

每个 Vert.x 实例都有一个事件总线实例。

注册一个handler,也可以被视为消费者:

EventBus eb = vertx.eventBus();

eb.consumer("news.uk.sport", message -> {
  System.out.println("I have received a message: " + message.body());
});

当一个消息到达”news.uk.sport“,那么这个handler就会被回调处理消息。

可以注册,那同样可以取消注册一个handler(就像前面部署Verticle和下线Verticle一样)

consumer
  .unregister()
  .onComplete(res -> {
    if (res.succeeded()) {
      System.out.println("The handler un-registration has reached all nodes");
    } else {
      System.out.println("Un-registration failed!");
    }
  });

总结

Verticle和EventBus由vertx-core提供,都是最基础的组件。后续可能从Web入手,结合更多的demo学习和讲解Vert.x。