Reactive Application Development with Vert.X WebSockets

Project address: https://github.com/wjw465150/ReactiveLongRunning

Handle long-running operations

Most Web APIs are developed to perform simple CRUD operations. Most operations performed by the API are blocking in nature, especially querying databases and communicating with other services through messaging services. Incoming requests are serviced by threads that perform security and authorization checks, input integrity checks, check business rules, perform data transactions, and more. The client expects these operations to complete within the scope of the request, and the response will indicate the success or failure of the operation.

Sometimes we are tasked with building functionality on the front end, which involves calling APIs to perform operations that may take more than reasonable time. The operation may not complete within the request timeout, then the API will send a response indicating the current status of the operation before closing the request. Therefore, the front end needs to implement a mechanism to query the current state of the operation to know when the operation has completed. Or users of the application need to reload the application's views to understand the current state.
insert image description here
Not only does this design of the application increase the load on the backend to respond to polling requests to inquire about the status of previous operations, but the frontend application needs to add a polling mechanism to its code.

It would be beneficial if we could design the frontend to respond more quickly to backend events using a push mechanism. This approach will open the door to building rich user experiences by engaging users in providing up-to-date information. Enabling a push mechanism will reduce churn in the backend to support getting the current state of various business operations.

In this article, we'll explore how WebSockets can be used to enable real-time updates to the front end to provide the latest status of a transaction.

Before looking at the reference implementation, let's familiarize ourselves with the techniques we will use to build the sample application.

About Vert.x

Vert.x is a toolkit for creating reactive, non-blocking, and asynchronous applications that run on the Java Virtual Machine (JVM). It consists of several components that help you create reactive applications. It is designed to be cloud native.

Since Vert.x supports asynchronous applications, it can be used to create applications with a large number of messages, large event handling, HTTP interactions, and more.
insert image description here
The Vert.x Core API contains the backbone for writing Vert.x applications, as well as low-level support for HTTP, TCP, UDP, filesystems, asynchronous streams, and many other building blocks. It is also used by many other components of Vert.x.

warn

The Vert.x API is non-blocking and doesn't block the event loop, but that doesn't help much if you block the event loop yourself in the handler.

If you do, then the event loop won't be able to do anything else while it's blocked. If you block all event loops in your Vertx instance, your application will come to a complete halt!

Examples of blocking include:

  1. Thread.sleep()

    thread sleep

  2. Waiting on a lock

    waiting for lock

  3. Waiting on a mutex or monitor (e.g. synchronized section)

    Waiting on a mutex or monitor (e.g. a synchronized section)

  4. Doing a long lived database operation and waiting for a result

    Perform long-running database operations and wait for results

  5. Doing a complex calculation that takes some significant time.

    Do complex calculations that take a lot of time.

  6. Spinning in a loop

    spin in a loop

About WebSockets

WebSocket is a communication protocol that provides a full-duplex communication channel over a single TCP connection. The current API specification that allows web applications to use this protocol is called WebSockets.

WebSocket is different from HTTP. Both protocols reside at layer 7 of the OSI model and rely on TCP at layer 4. Although they are different, RFC 6455 states that WebSocket "is designed to work over HTTP ports 443 and 80, with support for HTTP proxies and intermediaries," making it compatible with HTTP. For compatibility, the WebSocket handshake is changed from the HTTP protocol to the WebSocket protocol using the HTTP Upgrade header.
insert image description here
The WebSocket protocol enables interaction between a web browser (or other client application) and a web server with lower overhead than half-duplex alternatives such as HTTP polling, facilitating real-time data transfer to and from the server .

Additionally, WebSocket enables message streaming on top of TCP.

sample application

To demonstrate the power of Vert.x and the flexibility that WebSocket brings, we'll write a very simple web application on the JVM using the Vert.x HTTP Server. The image below will describe what our sample application looks like:
insert image description here

Build Vertices

Verticles are chunks of code that are deployed and run by Vert.x. A Vert.x instance maintains N event loop threads by default (where N defaults to core*2). Vertices can be written in any language supported by Vert.x, and a single application can contain verticles written in multiple languages.

Here is an example verticle:

public class MyVerticle extends AbstractVerticle {
    
    

 // Called when verticle is deployed
 public void start() {
    
    
 }

 // Optional - called when verticle is undeployed
 public void stop() {
    
    
 }

}

Let's go through the steps of creating an HTTP server to handle incoming REST and WebSockets requests. We'll start by defining an HTTP router object to handle incoming REST requests.

// 创建一个router对象
m_router = Router.router(vertx);
// 一个简单的transaction端点
m_router.get("/api/transaction/:customer/:tid").handler(this::handleTransaction);
// 提供静态内容(如 HTML 和 JS 文件)的处理程序
m_router.route("/static/*").handler(StaticHandler.create());
....
....
// 创建 HTTP 服务器
vertx.createHttpServer()
    .requestHandler(m_router)
    .listen(
            // Retrieve the port from the
            // configuration, default to 8080.
            port,
            result -> {
    
    
                if (result.succeeded()) {
    
    
                    m_logger.info("Listening now on port {}", port);
                    // Initialize application
                } else {
    
    
                    m_logger.error("Failed to listen", result.cause());
                }
            }
    );

Now that we've defined a basic HTTP server to handle incoming HTTP requests, we'll add a handler to handle incoming WebSocket requests. To make creating and using WebSockets easier on the client side, we'll use a popular WebSockets-based JS library called SockJS.

What is SockJS?

Now that we've defined a basic HTTP server to handle incoming HTTP requests, we'll add a handler to handle incoming WebSocket requests. To make creating and using WebSockets easier on the client side, we'll use a popular WebSockets-based JS library: SockJS.

Let's define a SockJS handler to handle incoming WebSocket requests:

// 创建一个SockJS处理器.
SockJSHandlerOptions options = new SockJSHandlerOptions()
        .setHeartbeatInterval(2000)
        .setRegisterWriteHandler(true); // We need an identifier
SockJSHandler ebHandler = SockJSHandler.create(vertx, options);

// 我们的websocket端点: /eventbus/
m_router.route("/eventbus/*").subRouter(ebHandler.socketHandler(sockJSSocket -> {
    
    
    // 提取标识符
    final String id = sockJSSocket.writeHandlerID();
    // 创建一个对象来映射客户端套接字
    ClientConnection connection = new ClientConnection(id, sockJSSocket, vertx.eventBus());
    // 跟踪打开的连接
    m_client_connections.put(id, connection);
    // 注册结束回调
    sockJSSocket.endHandler((Void) -> {
    
    
        connection.stop();
        m_client_connections.remove(id);
    });
    // 开始连接
    connection.start();
}));

We will now implement our REST endpoint handler. To keep our application from blocking, we'll use a worker verticle outside of the main event loop to handle requests. The request will be forwarded to the worker using a message on the EventBus.

private void handleTransaction(RoutingContext rc) {
    
    
    // 抓取请求和响应对象
    HttpServerResponse response = rc.response();
    // 提取参数
    String customer = rc.pathParam("customer");
    String tid = rc.pathParam("tid");

    // 构建消息以发送给worker
    JsonObject requestObject = new JsonObject();
    requestObject.put("tid", tid);
    requestObject.put("customer", customer);
    requestObject.put("status", TransactionStatus.PENDING.value());
    
    // 通过事件总线向worker发送消息
    // 指定一个处理程序来处理从worker返回的响应
    vertx.eventBus().request("WORKER", requestObject.encode(), result -> {
    
    
        if (result.succeeded()) {
    
    
            String resp = result.result().body().toString();
            // 如果响应过早关闭
            if (response.closed() || response.ended()) {
    
    
                return;
            }
            // 将响应发送给客户端
            response
                    .setStatusCode(201)
                    .putHeader("content-type",
                            "application/json; charset=utf-8")
                    .end(resp);
        } else {
    
    
            if (response.closed() || response.ended()) {
    
    
                return;
            }
            response
                    .setStatusCode(404)
                    .putHeader("content-type",
                            "application/json; charset=utf-8")
                    .end();
        }
    });

}

Now that we have defined our main verticle, with an HTTP server and endpoint handler, we will now move on to defining our worker verticles to handle requests.

We'll define a simple worker that generates transactional updates on a timer with different states. Each state transition will be announced via the event bus so that listeners can receive and process updates as necessary.

public class ApiWorkerVerticle extends AbstractVerticle {
    
    
    ...
    @Override
    public void start() throws Exception {
    
    
        ...
        // 获取应用配置
        ConfigRetriever retriever = ConfigRetriever.create(vertx,
                new ConfigRetrieverOptions().addStore(fileStore));
        retriever.getConfig(
                config -> {
    
    
                    // 一旦我们有了配置就开始初始化
                    startup(config.result());
                }
        );
    }

    private void startup(JsonObject config) {
    
    
        // 处理应用程序配置
        processConfig(config);
        // 创建我们的事件总线监听器。
        // 我们将接收此消费者的处理程序从主 Verticle 发送的所有请求
        worker_consumer = vertx.eventBus().consumer("WORKER");
        worker_consumer.handler(m -> {
    
    
            handleRequest(m);
        });
    }

    @Override
    public void stop() throws Exception {
    
    
        ...
    }
}

We define our worker verticles with event bus listeners. The listener will receive all requests sent from the ApiVerticle. The handler will process each request as it is received.

private void handleRequest(Message<String> m) {
    
    
    // Extract the payload from the message
    JsonObject requestObject = new JsonObject(m.body());
    final String tid = requestObject.getString("tid");
    activeTransactions.put(tid, requestObject);
    // Prepare and respond to the request, before we process the request.
    requestObject.put("status", TransactionStatus.PENDING.value());
    requestObject.put("type", "transaction-status");
    m.reply(requestObject.encode());
    // process the transaction
    handleTransaction(tid);
}

Our transaction processing is very simple. We will use a timer to generate timely updates of the different states of the transaction.

private void handleTransaction(final String tid) {
    
    
        // 设置一个 5 秒的定时器
        vertx.setTimer(5000, x -> {
    
    
            updateTransaction(tid);
        });
    }

    // 事务的各种状态
    public enum TransactionStatus {
    
    
        PENDING("PENDING"),
        INITIATED("INITIATED"),
        RUNNING("RUNNING"),
        FINALIZING("FINALIZING"),
        COMPLETE("COMPLETE");
        ...
    }

Every time the timer fires, we update the state of the transaction and publish the update via the event bus. Once we have reached the final state, we will remove the active transaction from the trace.

private void updateTransaction(final String tid) {
    
    
    // 获取事务
    JsonObject requestObject = activeTransactions.get(tid);
    if (requestObject != null) {
    
    
        TransactionStatus status = TransactionStatus.valueOf(requestObject.getString("status"));
        if (status.ordinal() < TransactionStatus.COMPLETE.ordinal()) {
    
    
            TransactionStatus nextStatus = TransactionStatus.values()[status.ordinal() + 1];
            // 准备更新消息
            requestObject.put("status", nextStatus.value());
            requestObject.put("type", "transaction-status");
            // 发送前保存
            activeTransactions.put(tid, requestObject);
            // 通过事件总线发布更新
            publishTransaction(requestObject);
            if (nextStatus.ordinal() < TransactionStatus.COMPLETE.ordinal()) {
    
    
                // 如果我们不处于终端状态,则安排下一次更新
                handleTransaction(tid);
            } else {
    
    
                // 如果我们达到了终止状态,则删除事务
                activeTransactions.remove(tid);
            }
        }
    }
}

private void publishTransaction(final JsonObject obj) {
    
    
    // 在 CUSTOMER 主题上发布消息,只有对此 CUSTOMER 事件感兴趣的听众才会收到此消息
    vertx.eventBus().publisher(obj.getString("customer")).write(obj.encode());
}

We now have the necessary components to handle incoming transaction requests, process it in workers, and publish updates via the event bus. We now need to create a representation for customers who are interested in hearing about these updates. We'll define a connection object to represent a client's persistent connection over WebSocket. This connection object will create listeners for the CUSTOMER topic via the event bus and deliver any events received to the client via WebSocket.

public class ClientConnection {
    
    

        public ClientConnection(final String writeHandlerID,
                                final SockJSSocket socket,
                                final EventBus eventBus)
        {
    
    
            // Save the socket and the socket identifier
            this.id = writeHandlerID;
            this.socket = socket;
            // We will use the event bus to listen to updates
            // published by the worker verticle for a CUSTOMER
            this.eventBus = eventBus;
        }

        // Attach a handler to process messages received from the client
        public void start() {
    
    
            socket.handler(
                buffer -> {
    
    
                    String message = buffer.toString();
                    handleMessage(message);
                }
            );
        }
    }

Since we don't want to have a persistent WebSocket connection all the time, we'll create it and only use it for the time span of the transaction request.

private void handleMessage(final String messageStr) {
    
    
        JsonObject messageObj = new JsonObject(messageStr);
        String messageType = messageObj.getString("type");
        // 当客户端发送“listen”请求时,我们将设置监听器
        if (messageType.equalsIgnoreCase("listen")) {
    
    
            setupListener(messageObj.getString("customer"));
        }
    }

    private void setupListener(final String customer) {
    
    
        // 为 CUSTOMER 主题创建事件总线消费者
        if (consumer == null) {
    
    
            consumer = eventBus.consumer(customer);
            consumer.handler(event -> {
    
    
                // 将传入的事件总线消息传递给客户端
                socket.write(event.body());
            });
        }
        // 为监听请求发送 ACK
        JsonObject obj = new JsonObject();
        obj.put("type", "listen-ack");
        obj.put("customer", customer);
        sendMessage(obj.encode());
    }

    private void sendMessage(final String message) {
    
    
        socket.write(message);
    }

We now have a working API to handle incoming HTTP and WebSocket requests. Now let's create a client that can make HTTP requests and open WebSocket connections during transactions.

    // 定义socket对象
    var socket = null;

    function openSocket() {
    
    
        // Open a socket
        socket = new SockJS("/eventbus/");
        socket.onopen = function () {
    
    
            listen();
        };
        // 定义一个处理程序来处理传入的消息
        socket.onmessage = function(msg) {
    
    
            processMessage(msg.data);
        };
        ...
    }

    function closeSocket() {
    
    
        socket.close();
        socket = null;
    }

    function submitTransaction() {
    
    
        // 在我们提交事务之前打开套接字
        // 打开套接字后,我们将进行 HTTP REST 调用
        openSocket();
    }

    // 发送一个请求来监听更新
    function listen() {
    
    
        var message = {
    
    };
        message.type = "listen";
        message.customer = document.getElementById("customer-value").value;
        sendMessage(JSON.stringify(message));
    }

    function sendMessage(msg) {
    
    
        socket.send(msg);
    }

    function processMessage(msg) {
    
    
        var obj = JSON.parse(msg);
        if (obj.type === 'listen-ack') {
    
    
            handleListenAck(obj);
        } else if (obj.type === 'transaction-status') {
    
    
            handleTransactionStatus(obj);
        }
    }

    function handleListenAck(obj) {
    
    
        // 进行 API 调用
        sendTransaction();
    }

    function handleTransactionStatus(obj) {
    
    
        if (obj.status === 'COMPLETE') {
    
    
            closeSocket();
        }
    }

    // REST 请求
    function sendTransaction() {
    
    
        var cid = document.getElementById("customer-value").value;
        var tid = document.getElementById("transaction-value").value;
        let xmlHttpReq = new XMLHttpRequest();
        xmlHttpReq.onreadystatechange = function () {
    
    
            if (xmlHttpReq.readyState === 4 && (xmlHttpReq.status === 200 || xmlHttpReq.status === 201))
                display("REST RCV: " + xmlHttpReq.responseText);
        }
        xmlHttpReq.open("GET", "http://localhost:8080/api/transaction/" + cid + "/" + tid, true); // true for asynchronous
        xmlHttpReq.send(null);
    }

We now have a JS front end that makes transactional requests over HTTP and listens for transactional updates over WebSocket during transaction processing.

A complete working example can be found in this demo .

If you run the sample program

Construct

gradlew clean build -x test

run

java -Dreactiveapi.config=conf/config.json -jar build/libs/ReactiveLongRunning-1.0-fat.jar

use

Open with a browser: http://localhost:8080/static/index.html,
specify the transaction ID and customer ID, and then submit.

The final running result is shown in the figure below:
insert image description here


<<<<<< [完] >>>>>>

Guess you like

Origin blog.csdn.net/wjw465150/article/details/129362065