【RSocket】使用 RSocket(二)——四種通信模式實踐
Source Code: https://github.com/joexu01/rsocket-demo
0. 四種通信模式
讓我們來簡單復習一下 RSocket 的四種通信模式:
-
即發即忘 - FireAndForget:立即發送一個請求,無需為這個請求發送響應報文。適用于監控埋點,日志上報等,這種場景下無需回執,丟失幾個請求無傷大雅
-
請求響應 - RequestResponse:請求方發送一條請求消息,響應方收到請求后并返回一條響應消息。傳統的HTTP是典型的Request-Response
-
流式響應 - RequestStream:請求方發送一個請求報文,響應方發回N個響應報文。傳統的MQ是典型的RequestStream
-
雙向通道 - Channel:創建一個通道上下文,雙方可以互相發送消息。IM是個典型的RequestChannel通訊場景
1. 客戶端生成和解析路由信息
*本篇文章的客戶端示例文件在 rsocket-client-raw/src/main/java/org/example/FourCommunicationScheme.java
我們使用 decodeRoute
和 encodeRoute
函數來解碼和編碼路由信息。
static String decodeRoute(ByteBuf metadata) {
final RoutingMetadata routingMetadata = new RoutingMetadata(metadata);
return routingMetadata.iterator().next();
}
static ByteBuf encodeRoute(String route) {
return TaggingMetadataCodec.createTaggingContent(
ByteBufAllocator.DEFAULT,
Collections.singletonList(route));
}
2. RequestResponse
服務端處理函數
在這里我們編寫一個簡單的 Handler,它的 Route 是 test.echo
,它接收一個請求并返回請求 Payload 的 data 中的字符串。
@MessageMapping("test.echo")
public Mono<String> simplyEcho(String data) throws InterruptedException {
Thread.sleep(1500);
logger.info("[test.echo]Received echo string from client: {}", data);
return Mono.just(String.format("[test.echo]I received your string: %s. Thank you.", data));
}
注意,這里的參數也可以是 Mono<String>
,然后對 Mono 進行操作并返回。事實上,如果嚴格按照響應式編程的策略,這里應該直接對 Mono
進行操作。
客戶端發送請求
- 生成 metadata 的 route 信息,然后將字符串和 metadata 放入 Payload
ByteBuf routeMetadata = encodeRoute("test.echo");
Payload echoPayload = ByteBufPayload.create(
ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "This is a message from client using rsocket-java library."),
routeMetadata);
- 新建一個 RequestResponse,并對這次請求做一些設置;可以看到 RequestResponse 方法返回的數據類型是
Mono<Payload>
。然后我們對這個 Mono 設定一些操作(具體操作請看代碼注釋):
Mono<Payload> requestResponse = socket.requestResponse(echoPayload);
requestResponse
// 當 subscribe() 操作開始執行時打印一下日志
.doOnSubscribe(subscription -> logger.info("Test1 subscribed to {}", subscription.toString()))
// 當攜帶的請求成功后要做的事情
.doOnSuccess(payload -> {
logger.info("Test1 - Successfully returned: {}", payload.getDataUtf8());
payload.release();
})
.doOnError(throwable -> logger.info("Test1 doOnError: {}", throwable.toString()))
// 可以使用 timeout 丟棄等待超時的 Mono
//.timeout(Duration.ofSeconds(1))
// 可以使用 doOnTerminate 在請求結束后做一些工作
// .doOnTerminate(() -> {})
// 但是一定要設置 doOnError
//.doOnError(TimeoutException.class, e -> logger.info("Test1 doOnError: {}", e.toString()))
// .onErrorReturn(TimeoutException.class, DefaultPayload.create("Payload: Test1 - timeout"))
// 可以使用 log() 來觀察數據的狀態
//.log()
// 客戶端在執行 subscribe() 操作時才會開始從服務端接收數據流
// 在響應式編程中使用 subscribe 操作符是訂閱一個數據流并處理發布的數據、錯誤和完成信號的核心方式之一
.subscribe();
請求發出后主線程不會阻塞,所以我們需要使用 socket.onClose().block();
保持連接。
然后我們嘗試運行服務端和客戶端,看看一看客戶端的輸出:
[main] INFO org.example.RSocketClientRaw - My UUID is 0718ef3b-9ee0-42f1-9003-700a8aa9a98d
[main] INFO org.example.RSocketClientRaw - Test1 subscribed to RequestResponseRequesterMono
[reactor-tcp-epoll-2] INFO org.example.RSocketClientRaw - Test1 - Successfully returned: [test.echo]I received your string: This is a message from client using rsocket-java library.. Thank you.
服務端日志:
2023-03-12 21:47:29.291 INFO 32099 --- [or-http-epoll-2] o.example.controller.RSocketController : [connect.setup]Client connection: 0718ef3b-9ee0-42f1-9003-700a8aa9a98d
2023-03-12 21:47:32.304 INFO 32099 --- [or-http-epoll-2] o.example.controller.RSocketController : [test.echo]Received echo string from client: This is a message from client using rsocket-java library.
客戶端成功地發出請求并收到來自服務端的回復。
3. FireAndForget
服務端
@MessageMapping("upload.log")
public void fireAndForgetHandler(@Headers Map<String, Object> header, RSocketRequester requester, String data) {
header.forEach((k, v) -> System.out.printf("[upload.log]header key: %s, val: %s\n", k, v));
System.out.printf("[upload.log]UploadEventLogs: Received log string from client: %s\n", data);
}
服務端接受一個請求,不返回任何結果(Fire'n'Forget),只在服務端打印 Header 的內容。
客戶端
// 測試 FnF
routeMetadata = TaggingMetadataCodec.createTaggingContent(ByteBufAllocator.DEFAULT, Collections.singletonList("upload.log"));
socket.fireAndForget(
ByteBufPayload.create(
ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "This is a log from client using rsocket-java library."),
routeMetadata))
.doOnSubscribe(subscription -> logger.info("Test2 - Fire And Forget onSubscribe: {}", subscription.toString()))
.subscribe();
客戶端輸出:
[main] INFO org.example.RSocketClientRaw - Test2 - Fire And Forget onSubscribe: FireAndForgetRequesterMono
服務端輸出:
2023-03-10 15:10:25.675 INFO 5318 --- [or-http-epoll-4] o.example.controller.RSocketController : [test.echo]Received echo string from client: This is a message from client using rsocket-java library.
[upload.log]header key: dataBufferFactory, val: NettyDataBufferFactory (PooledByteBufAllocator(directByDefault: true))
[upload.log]header key: rsocketRequester, val: org.springframework.messaging.rsocket.DefaultRSocketRequester@607cc59
[upload.log]header key: lookupDestination, val: upload.log
[upload.log]header key: contentType, val: application/binary
[upload.log]header key: rsocketFrameType, val: REQUEST_FNF
[upload.log]UploadEventLogs: Received log string from client: This is a log from client using rsocket-java library.
4. RequestStream
服務端
服務端接收一個 Mono<String>
然后返回給客戶端包含 10 個 String
的 Flux
。
事實上,嚴格按照響應式編程的策略,這里應該直接對 Mono
進行操作,可以使用 flatMapMany()
把生成的數據流通過異步方式處理,擴展出新的數據流。下面是擴展新數據流的簡單示例:
Mono.just(3)
.flatMapMany(i -> Flux.range(0, i))
.subscribe(System.out::println);
在這里為了演示方便就先打印 Mono
然后新生成一個 Flux
。
@MessageMapping("handler.request.stream")
public Flux<String> responseStreaming(Mono<String> request) {
request
.doOnNext(s -> logger.info("[handler.request.stream]: {}", s))
// 可以使用 then() 結束操作鏈
.then()
.subscribe();
return Flux
.range(1, 10)
.map(idx -> String.format("Resp from Server: %s, Thank you!", idx));
}
客戶端
請看代碼注釋來理解對數據流 Flux 的各種操作:
// 測試 RequestStream
routeMetadata = encodeRoute("handler.request.stream");
Flux<Payload> requestStream = socket.requestStream(
ByteBufPayload.create(
ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "TEST3 - Request&Stream"),
routeMetadata));
requestStream
// 當然可以使用 map 對每個 Payload 進行操作,這會改變數據
// .map(payload -> System.out.printf("%s\n", payload.getDataUtf8()))
.doOnSubscribe(subscription -> logger.info("Test3 subscribed to {}", subscription.toString()))
// 使用 doOnNext 不會對流的數據進行改變
// doOnNext()是一個 Reactor 式流操作符,它允許編寫者注冊一個在每次出現新元素時執行的回調函數
.doOnNext(nextPayload -> System.out.println("Test3 Received payload: " + nextPayload.getDataUtf8()))
// 當需要從流中選擇一些特定的元素時,可以使用 Flux.take(long n) 操作符
// 該操作符將創建一個新的 Flux,該 Flux 包含原始 Flux 的前 n 個元素
// take 操作符發出了指定數量的元素之后,就不再接收任何元素,并且將取消其上游發布者的訂閱
// 在這里服務端使用 Flux.range 來限定 Flux 流中的元素個數
// 如果服務端使用 Flux.interval 生成一個無限長度的流,客戶端使用 take 接收限定個數的元素
// 便會取消發布者的訂閱
.take(5)
.subscribe();
客戶端輸出結果:
[main] INFO org.example.RSocketClientRaw - My UUID is 28afc749-75e1-4289-8607-14810103de6c
[main] INFO org.example.RSocketClientRaw - Test3 subscribed to RequestStreamRequesterFlux
Test3 Received payload: Resp from Server: 1, Thank you!
Test3 Received payload: Resp from Server: 2, Thank you!
Test3 Received payload: Resp from Server: 3, Thank you!
Test3 Received payload: Resp from Server: 4, Thank you!
Test3 Received payload: Resp from Server: 5, Thank you!
服務端接收到了請求:
2023-03-12 22:01:33.520 INFO 32099 --- [or-http-epoll-3] o.example.controller.RSocketController : [handler.request.stream]: TEST3 - Request&Stream
5. Channel
服務端
服務端接收來自客戶端的整數字符串,將它們乘以2以后發送回去。我們不妨把處理客戶端請求流的函數封裝為一個 Spring Service:
@Service
public class MathService {
public Flux<String> doubleInteger(Flux<String> request) {
return request
.map(s -> {
System.out.println("received " + s);
int i = Integer.parseInt(s);
return String.valueOf(i * 2);
});
}
}
編寫處理函數:
@Autowired
private MathService mathService;
@MessageMapping("handler.request.channel")
public Flux<String> responseChannel(Flux<String> payloads) {
return this.mathService.doubleInteger(payloads);
}
客戶端
Flux<Payload> payloadFlux = Flux.range(-5, 10)
.delayElements(Duration.ofMillis(500))
.map(obj ->
{
ByteBuf metadata = encodeRoute("handler.request.channel");
return ByteBufPayload.create(
ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, obj.toString()), metadata);
});
Flux<Payload> channelResp = socket.requestChannel(payloadFlux);
channelResp
.doOnSubscribe(subscription -> logger.info("Test4 subscribed to {}", subscription.toString()))
.doOnError(throwable -> logger.info(throwable.toString()))
.doOnNext(nextPayload -> System.out.println("Test4 Received payload: " + nextPayload.getDataUtf8()))
.subscribe();
客戶端輸出:
[main] INFO org.example.RSocketClientRaw - My UUID is 96ff8fe7-416c-4607-9518-463114725a7a
[main] INFO org.example.RSocketClientRaw - Test4 subscribed to RequestChannelRequesterFlux
Test4 Received payload: -10
Test4 Received payload: -8
Test4 Received payload: -6
Test4 Received payload: -4
Test4 Received payload: -2
Test4 Received payload: 0
Test4 Received payload: 2
Test4 Received payload: 4
Test4 Received payload: 6
Test4 Received payload: 8
服務端輸出:
2023-03-12 22:07:05.542 INFO 33083 --- [or-http-epoll-2] o.example.controller.RSocketController : [connect.setup]Client connection: 96ff8fe7-416c-4607-9518-463114725a7a
received -5
received -4
received -3
received -2
received -1
received 0
received 1
received 2
received 3
received 4
下一篇文章會展示服務端如何主動調用客戶端的函數。如有錯誤歡迎在評論區批評指正!