[Network] RSocket Protocol
While HTTP provides simple yet effective features for application protocols, it can sometimes fall short in defining detailed application semantics—the specific meanings and interpretations of an application’s behavior. As a client-server-based interaction model, HTTP often requires additional logic within the application to handle complex scenarios like request-response processing, status code interpretation, Server-Sent Events (SSE), and flow control.
Binary protocols like HTTP/2.0 have introduced multiplexing and binary framing, which improve resource efficiency and speed. However, they are not inherently fully bi-directional. Features like Server Push allow a server to push resources before a client explicitly asks for them, but this is primarily used for caching static assets (CSS, JS, images) required by web browsers. The RSocket protocol was developed to overcome these limitations of HTTP while providing advanced interaction models and features.
RSocket is an application-level protocol providing asynchronous Reactive Streams semantics. Originally developed by Netflix to replace inefficient HTTP communication in microservices with a low-overhead alternative, RSocket is a binary protocol designed for use over byte-stream transports such as TCP, Aeron, and WebSockets. It offers several key features over a single connection:
- Supports multiple interaction models beyond simple request-response, including streaming responses and push.
- Provides application-level flow control semantics.
- Utilizes binary encoding and multiplexing over a single connection.
- Features task cancellation and session resumability.
The interaction models provided by the RSocket protocol include:
- Request-Response: Send one message and receive one response.
- Request-Stream: Send one message and receive a stream of messages.
- Channel: Bi-directional message streaming between both parties.
- Fire-and-Forget: Send a one-way message without expecting a response.
One of RSocket’s defining characteristics is that once a connection is established, the distinction between “client” and “server” disappears as both sides become symmetrical peers. Either side can initiate any of the interaction models above. In RSocket terminology, the participating parties are referred to as the Requester and the Responder, and these interactions are simply called “requests” or “request streams.”
RSocket is implemented in various programming languages. The Java library is built upon Project Reactor, a Reactive Streams implementation, while its transports for TCP and WebSockets are based on Reactor Netty. Reactor Netty is a networking engine that supports backpressure for HTTP (including WebSockets), TCP, and UDP. Signals from an application’s Reactive Streams Publisher are propagated transparently across the network via the RSocket protocol.
RSocket is inherently reactive and provides protocol-level reactive features:
- Reactive Semantics: Unlike typical application-level logic, RSocket carries Reactive Streams semantics across network boundaries. For streaming requests like Request-Stream and Channel, backpressure signals move between the Requester and Responder, allowing the Requester to slow down the Responder. This reduces reliance on network-layer congestion control and minimizes the need for buffering at various levels.
- Request Throttling (Leasing): This feature limits the total number of requests allowed by the other side over a given period. It uses
LEASEframes, which are periodically renewed. - Session Resumption: Designed to maintain state in case of connectivity loss, this feature manages session state transparently for the application. It works effectively alongside backpressure to pause producers and minimize the amount of state required for resumption.
Connection Setup and Communication Process
- Connection Setup: Initially, a client connects to a server via a low-level streaming transport (e.g., TCP or WebSocket) and sends a
SETUPframe to the Responder to configure connection parameters. While the Responder can reject theSETUPframe, once it is accepted, both sides can initiate requests. UnlessSETUPindicates the use of leasing semantics to limit request counts, both sides must wait for aLEASEframe before sending requests. - Requests: Once the connection is established, either side can initiate requests using
REQUEST_RESPONSE,REQUEST_STREAM,REQUEST_CHANNEL, orREQUEST_FNFframes. Each frame delivers a message from the Requester to the Responder. The Responder can returnPAYLOADframes as responses. InREQUEST_CHANNEL, the Requester can also send additionalPAYLOADframes containing more request messages. For interactions involving message streams (Request-Stream and Channel), the Responder must respect the Requester’s demand signals. Demand is expressed as a count of messages. Initial demand is specified in the request frame, and subsequent demand is signaled viaREQUEST_Nframes. Additionally, either side can send metadata-only notifications related to the entire connection usingMETADATA_PUSHframes.
Flow Control
RSocket provides application-level flow control, allowing network traffic to be throttled based on logical requirements and application logic.
In RSocket, the receiver uses REQUEST_N frames to control the number of messages it can process. This corresponds to the request(n) semantic in Reactive Streams. Traffic is regulated between the sender and receiver based on the receiver’s capacity as signaled by these frames.
This application-level flow control is independent of the byte-based flow control at the transport layer (like TCP congestion control). Consequently, RSocket can adjust the message flow based on the application’s processing speed before the receiver’s buffers are even full.
gRPC vs. RSocket
While both gRPC and RSocket are designed for high-performance microservices communication, they differ in philosophy and implementation:
- Protocol Layer: gRPC is an RPC (Remote Procedure Call) framework built on top of HTTP/2, whereas RSocket is a message-driven application protocol that sits directly on top of transport layers.
- Interaction Model: gRPC supports unary (request-response) and streaming but primarily follows a client-server model. RSocket is symmetrical (P2P), meaning once connected, either side can initiate any of the four natively supported interaction models.
- Flow Control (Backpressure): gRPC relies on HTTP/2’s byte-based flow control, which limits granular control over application logic. In contrast, RSocket provides logical element-level backpressure at the application layer, allowing for precise control based on actual item counts.
- Serialization: gRPC is strictly coupled with Protocol Buffers (Protobuf). RSocket is serialization-agnostic, allowing developers to freely choose between JSON, CBOR, Protobuf, or any other format for the binary frames’ payloads.
Spring and RSocket
Spring Framework provides first-class support for RSocket starting from version 5.2, integrating seamlessly with Project Reactor’s Mono and Flux types.
Adding the spring-boot-starter-rsocket dependency automatically configures the infrastructure required for RSocket servers and clients. In controllers, the @MessageMapping annotation is used to define handlers for specific routes, while @ConnectMapping handles connection-level logic, such as authentication or initial state setup.
RSocketRequester is a flexible, fluent API used on the client side to send requests. It simplifies encoding/decoding and metadata configuration, offering a much more convenient interface than dealing with raw RSocket interfaces directly.
Starting with Spring 6, declarative clients are supported via the @RSocketExchange annotation. By simply defining an interface with these annotations, Spring generates a proxy to handle RSocket calls, similar to how HttpServiceProxyFactory works for HTTP’s WebClient.
RSocket also integrates with the broader Spring ecosystem, including Spring Security for authentication/authorization and Spring Cloud Gateway for RSocket routing.
Prometheus RSocket Proxy
The Prometheus RSocket Proxy allows Prometheus to scrape metrics from applications even when they cannot open an ingress port (e.g., when they only allow outbound traffic). This project is maintained as part of the Micrometer observability project.
How it works:
- The application establishes an outbound TCP RSocket connection to the proxy (or proxy cluster). Once established, the connection becomes symmetrical, allowing the proxy to act as a Requester to fetch metrics.
- Prometheus scrapes the
/metrics/connectedand/metrics/proxyendpoints on the proxy rather than the application itself. - When the proxy receives a scrape request from Prometheus, it uses a request-response sequence over the established RSocket connections to fetch metrics from the connected application instances. The results are aggregated into a single response for Prometheus.
sequenceDiagram
Prometheus->>RSocket Proxy: Scrape metrics (on behalf of the app)
RSocket Proxy->App: Fetch metrics via RSocket
Because RSocket connections are persistent, the application does not need to maintain a continuous stream of metrics to the proxy; rather, the bi-directional connection remains open. If a connection is lost, the client automatically reconnects to the proxy. This allows proxy clusters to scale out or rebalance connections without interrupting the metrics collection flow.
TLS and Security
Since RSocket is transport-agnostic, it relies on the security mechanisms of the underlying transport layer (e.g., TCP, WebSocket).
Transport Layer Security (TLS)
When running over TCP or WebSockets, RSocket can use TLS (Transport Layer Security) to encrypt the communication. TLS provides:
- Encryption: Protects data from eavesdropping by encrypting the stream.
- Authentication: Verifies the identity of the server (and optionally the client) via digital certificates.
- Integrity: Ensures that data has not been tampered with during transit.
Application-Level Security
Beyond transport-level encryption, RSocket can implement authentication and authorization using metadata. Since RSocket frames separate metadata from data, security credentials can be handled independently of the payload processing logic.
Key authentication methods:
- Simple Authentication: The traditional method of sending a username and password.
- JWT (Bearer Token): A token-based approach where the client sends a JWT in the metadata, which the server then validates to identify the user.
Authentication timing:
- Setup-time: Authentication occurs once when the
SETUPframe is sent during connection establishment. This is ideal for environments where a single user (e.g., a mobile app) maintains a dedicated connection. - Request-time: Authentication info is included in every
PAYLOADframe. This is useful in shared-connection environments (e.g., a web app acting as a gateway) where fine-grained access control is needed for each individual request.
Spring Security Support
Spring provides robust support for RSocket security via the spring-security-rsocket module. It can be enabled using the @EnableRSocketSecurity annotation. Authorization rules can be defined using an interceptor (PayloadSocketAcceptorInterceptor) to restrict access to specific routes. Furthermore, authenticated user information can be injected into handler methods using the @AuthenticationPrincipal annotation.
References
- https://docs.spring.io/spring-framework/reference/rsocket.html
- https://web.dev/articles/performance-http2
- https://www.digitalocean.com/community/tutorials/http-1-1-vs-http-2-what-s-the-difference
- https://datatracker.ietf.org/doc/html/rfc7540
- https://developer.okta.com/blog/2018/09/21/reactive-programming-with-spring
- https://medium.com/netifi/differences-between-grpc-and-rsocket-e736c954e60
- https://tanzu.vmware.com/developer/guides/rsocket-tls-spring-boot/
- https://youtu.be/ipVfRdl5SP0?feature=shared
Comments