SpringBoot uses the RSocket protocol

1, Problems of HTTP in micro service scenario

For the SpringCloud microservice architecture, each SpringBoot project is a service. Each service exposes the REST interface and calls each other through the HTTP protocol.

Traditional HTTP adopts heavy text transmission, and the transmission message is divided into three parts: starting line, header and main body.

  1. The first line of the message is the starting line, which is used to explain what to do in the request message and what happens in the response message.

  2. There are zero or more header fields after the starting line of the header field. Each header field contains a name and a value, separated by a colon (:) for ease of parsing. The header ends with a blank line. Adding a header field is as simple as adding a new line.

  3. After the blank line is the optional message body, which contains all types of data. The request body includes the data to be sent to the Web server; The data to be returned to the client is loaded in the response body. The starting line and header are in text form and structured, while the subject is different. The subject can contain any binary data (such as pictures, videos, audio tracks, software programs). Of course, the body can also contain text.

Using HTTP as the information transmission protocol between microservices will face the following problems:

  1. HTTP/1.1 is a stateless protocol. Some additional header information can only be transmitted in an uncompressed manner, which increases the server interaction overhead.
  2. HTTP/1.1 is based on the request response mode and belongs to a unary operation. Therefore, users can get a response every time they send a request. They cannot send other requests until they receive a response.
  3. HTTP/1.1 is based on TCP. It requires three handshakes to ensure a reliable connection, which is very time-consuming.

2, RSocket protocol

RSocket protocol is a new communication protocol developed by Facebook, Netifi, Privota and other companies. It adopts binary point-to-point data transmission and is mainly used in distributed architecture. It is a new network communication layer 7 (application layer) protocol based on Reactive Streams.

RSocket protocol has the characteristics of Multiplexed, Bidirectional Streaming, Flow Control, socket recovery, asynchronous message passing, transport layer decoupling and Transport independent.

RSocket official website: https://rsocket.io/

2.1 multiplexing

Before the HTTP/3.0 standard, all HTTP protocols were implemented based on TCP protocol, so in the HTTP/1.0 protocol version, each user's request needs to create a new TCP connection to the server (3 handshakes and 4 waves) In order to solve the problem of TCP performance, the support of TCP connection multiplexing is proposed in the HTTP/1.1 protocol version. However, at this time, only one user's request is allowed to be processed at a time, and other requests are allowed to continue to use this TCP connection for request processing only after the request processing is completed. In this way, if the processing operation of a request is very time-consuming, the It will lead to the degradation of subsequent request processing performance.
Therefore, in order to further solve the problem of request processing performance, the connection operation is further improved in HTTP/2.0, allowing a TCP connection to process requests from multiple clients at the same time. In this way, even if a request operation takes time, it will not affect the overall processing performance, as shown in the figure. However, the HTTP protocol based on TCP protocol will always have performance Therefore, QUIC is used as a new transport layer protocol in HTTP/3.0 protocol version. QUIC is implemented based on UDP protocol and has its own multiplexing structure.

QUIC(Quick UDP lnternet Connection) is an Internet transport layer protocol with low delay based on UDP developed by Google. In November 2016, the international Internet Engineering Task Force (IETF) held the first QUIC working group meeting, which attracted extensive attention in the industry. This also means that QUIC has begun its standardization process and become a new generation transport layer protocol.
QUIC well solves the various requirements faced by today's transport layer and application layer, including handling more connections, security and low latency. QUIC integrates the characteristics of protocols such as TCP, TLS and HTTP/2.0.


In the HTTP/2.0 protocol, the key problem is to solve the problem of TCP connection multiplexing, but in the HTTP protocol, all data is transmitted in the form of text, so there will be problems of excessive data transmission and limited transmission structure in the actual development, and RSocket is a binary protocol, which can easily transmit all kinds of data without data Due to the limitation of format, users can also compress according to their own needs.
In RSocket, the message body is divided into two parts: data and metadata, which can ensure that a small amount of metadata can still be exposed to other services under high-speed data transmission.

2.2 bidirectional flow

RSocket implements two-way flow communication support. The two-way flow can be used to realize the communication processing between the server and the client. In this way, in the process of request and response processing, the client can send requests to the server, and the server can also send requests to the client.

RSocket has four data interaction modes:

  1. Request and response: request / response, similar to the communication characteristics of HTTP, providing asynchronous communication and multiplexing support;
  2. Request response stream: request / streaming response. One request corresponds to multiple streaming responses, such as obtaining video list or product list,
  3. Fire and forget: it is triggered asynchronously and requires no response. It can be used for logging;
  4. Channel (bi directional streams): two-way asynchronous communication. Message flow flows asynchronously in both directions of server and client;

2.3 flow control

In the distributed project development environment, if the data produced by the producer is too fast, it will lead to the failure of consumers to process it in time. In the end, there may be an increase in the occupation rate of memory and CPU, resulting in the non response of the server or client. If there is no good implementation control, the whole application may be caused by the avalanche problem In order to avoid this situation, a set of flow control mechanism is needed to coordinate the processing speed between producers and consumers.

Stream let | flow control is provided in RSocket. As an application layer protocol, RSocket does not implement flow control based on the network layer of bytes, but flow control based on the number of frames in the application layer (controlling the number of messages produced by the producer)

2.4 connection recovery

Due to the rise of mobile network, there are great challenges in the stability of network connection. When the network fails, the connection recovery should be carried out in time. The connection recovery function is provided in RSocket. At the same time, in order to simplify the user's processing operation, the user will not have any perception after the connection recovery is successful, When the connection recovery fails, the corresponding callback function will be triggered through the onError event, so that the response can be maintained during the Stream and the transmission of duplicate data information can be reduced, because in the multiplex structure, repeated transmission means an increase in network pressure.

The core principle of the "SocketResumption" recovery mechanism provided in RSocket is that after the network Connection is re established, the user request is not processed from the beginning. The client and server need to be able to automatically save the Stream state on the Connection for a period of time after the Connection is interrupted. After the Connection is restored, the client will send this state information to the server, The server side will make grey judgment. If the recovery is successful, continue the previous Stream operation.

2.5 asynchronous messaging

The RSocket protocol adopts the form of asynchronous message transmission when transmitting data. The transmitted content is Frame (application layer Frame, such as FrameHeader, RESUME, etc.). At the same time, the RSocket transmission does not contain a clear target access path like HTTP protocol, and all access is realized by the routing module.
RSocket protocol uses frames to speed up data transmission. Each frame may be request content, response content or protocol related data information, and an application message may be divided into multiple different segments to be saved in one frame (packet sticking and unpacking in TCP).

2.6 transport layer decoupling and

RSocket protocol is an application layer connection oriented protocol and does not depend on the transport layer protocol. Therefore, users can freely choose different application scenarios. For example, TCP can be used for data center construction, WebSocket can be used for asynchronous browser interaction, and HTTP/2.0 can be used for HTTP services.

3, Basic development of RSocket

Create the springboot project rsocket base and import dependencies:

<dependency>
    <groupid>org.springframework.boot</groupid>
    <artifactid>spring-boot-starter-rsocket</artifactid>
</dependency>

Define the RSocket processing class, which needs to implement the RSocket interface.

package com.it.rsocket.server.handle;

import io.rsocket.Payload;
import io.rsocket.RSocket;
import io.rsocket.util.DefaultPayload;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Slf4j
public class MessageRSocketHandler implements RSocket {

    @Override
    public Mono<void> fireAndForget(Payload payload) { // No response
        // Generally, this unresponsive operation can be used in the logging mode
        // Payload indicates all additional data. For RSocket, all data communications are transmitted through this structure
        String message = payload.getDataUtf8(); // get data
        log.info("[fireAndForget]Accept request data:{}", message);
        return Mono.empty(); // Return empty message
    }

    @Override
    public Mono<payload> requestResponse(Payload payload) { // Traditional mode with data response
        String message = payload.getDataUtf8();
        log.info("[requestResponse]Accept request data:{}", message);
        return Mono.just(DefaultPayload.create("[ECHO]" + message)); // Data response
    }

    @Override
    public Flux<payload> requestStream(Payload payload) { // Process data flow
        String message = payload.getDataUtf8();
        log.info("[requestStream]Accept request data:{}", message);
        return Flux.fromStream(message.chars()// Convert received string to int stream data
                .mapToObj(c -> Character.toUpperCase((char) c)) // Character encoding to uppercase
                .map(Object::toString) // Call toString
                .map(DefaultPayload::create)); // Create Payload additional data
    }

    @Override
    public Flux<payload> requestChannel(Publisher<payload> payloads) { // Bidirectional flow
        return Flux.from(payloads).map(Payload::getDataUtf8).map(msg -> {
            log.info("[requestChannel]Accept request data:{}", msg);
            return msg; // Return sent data content
        }).map(DefaultPayload::create);
    }
}

The above implements the processing mechanism of the RSocket core, but in the actual development, the processing core needs to be bound with the client request, so a connector needs to be created at this time.

package com.it.rsocket.server.acceptor;

import com.it.server.rsocket.handle.MessageRSocketHandler;
import io.rsocket.ConnectionSetupPayload;
import io.rsocket.RSocket;
import io.rsocket.SocketAcceptor;
import reactor.core.publisher.Mono;

public class MessageRSocketAcceptor implements SocketAcceptor {
    @Override
    public Mono<rsocket> accept(ConnectionSetupPayload setup, RSocket sendingSocket) {
        return Mono.just(new MessageRSocketHandler());
    }
}

Since SpringBoot container management is not introduced, you need to manually start and close the service and create an exclusive tool class.

package com.it.rsocket.server;

import com.it.rsocket.server.acceptor.MessageRSocketAcceptor;
import io.rsocket.core.RSocketServer;
import io.rsocket.frame.decoder.PayloadDecoder;
import io.rsocket.transport.netty.server.TcpServerTransport;
import reactor.core.Disposable;

public class MessageServer { // RSocket state control

    private static Disposable disposable; // Used to release tasks

    public static void start() { // Start service
        RSocketServer server = RSocketServer.create(); // Create an RSocket server
        server.acceptor(new MessageRSocketAcceptor()); // Create connector
        server.payloadDecoder(PayloadDecoder.ZERO_COPY); // Zero copy technology
        disposable = server.bind(TcpServerTransport.create(8080)).subscribe(); // Open subscription
    }

    public static void stop() {
        disposable.dispose(); // Release connection
    }
}

Write test classes.

package com.it.rsocket;

import com.it.rsocket.server.MessageServer;
import io.rsocket.Payload;
import io.rsocket.RSocket;
import io.rsocket.core.RSocketConnector;
import io.rsocket.transport.netty.client.TcpClientTransport;
import io.rsocket.util.DefaultPayload;
import org.junit.Test;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.TestMethodOrder;
import reactor.core.publisher.Flux;

import java.time.Duration;

@TestMethodOrder(MethodOrderer.OrderAnnotation.class) // Manual configuration method execution sequence
public class TestMessageServer {

    private static RSocket rSocket;

    private static Flux<payload> getRequestPayload() { // Transmitted data
        return Flux.just("RSocket", "SpringCloud", "Redis", "Netty")
                .delayElements(Duration.ofSeconds(1))
                .map(DefaultPayload::create);
    }

    @Test
    public static void testFireAndForget() {
        getRequestPayload().flatMap(payload -> rSocket.fireAndForget(payload))
                .blockLast(Duration.ofMillis(1));
    }

    @BeforeAll // Execute before test
    public void setUpClient() {
        MessageServer.start(); // Start the server
        rSocket = RSocketConnector.connectWith(TcpClientTransport.create(8080)).block(); // Client connection
    }

    @AfterAll // Execute after test
    public void stopServer() {
        MessageServer.stop(); // Shut down the server
    }
}
```</payload></rsocket></payload></payload></payload></payload></void>

Posted by jgmiddel on Mon, 06 Dec 2021 10:54:01 -0800