【RSocket】使用 RSocket(三)——服务端主动调用客户端方法
1. 编写客户端接收请求的逻辑
我们可以在初始化 Rsocket 实例的时候指定客户端可以被调用的方法,使用 acceptor()
指定可被调用的方法和方法使用的通信模型类型:
- 通信类型为
RequestResponse
时:.acceptor(SocketAcceptor.forRequestResponse(payload -> {}))
- 通信类型为
FireAndForget
时.acceptor(SocketAcceptor.forFireAndForget(payload -> {}))
- 通信类型为
RequestStream
时.acceptor(SocketAcceptor.forRequestStream(payload -> {}))
- 通信类型为
RequestStream
时.acceptor(SocketAcceptor.forRequestChannel(
payloads ->
Flux.from(payloads)...));
接下来编写客户端方法的处理逻辑,以 RequestResponse
为例
public static void main(String[] args) {
final Logger logger = LoggerFactory.getLogger(RSocketClientRaw.class);
// 随机生成 UUID 标识客户端
UUID uuid = UUID.randomUUID();
logger.info("My UUID is {}", uuid);
// 生成 SETUP 阶段(建立连接时) Payload 使用的 route 信息
ByteBuf setupRouteMetadata = encodeRoute("connect.setup");
RSocket socket = RSocketConnector.create()
// 设置 metadata MIME Type,方便服务端根据 MIME 类型确定 metadata 内容
.metadataMimeType(WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.getString())
// SETUP 阶段的 Payload,data 里面存放 UUID
.setupPayload(ByteBufPayload.create(
ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, uuid.toString()),
setupRouteMetadata))
// 编写 Request&Response Acceptor
.acceptor(SocketAcceptor.forRequestResponse(
payload -> {
String route = decodeRoute(payload.sliceMetadata());
logger.info("[Client Acceptor] Received RequestResponse[route={}]", route);
String metadataUtf8 = payload.getMetadataUtf8();
String dataUtf8 = payload.getDataUtf8();
logger.info("[Client Acceptor] This Req&Resp contains data: {}, metadata: {}", dataUtf8, metadataUtf8);
payload.release();
if ("request.status.callback".equals(route)) {
return Mono.just(ByteBufPayload.create("Thanks for handling my task!"));
} else if ("request.server.call".equals(route)) {
return Mono.just(ByteBufPayload.create("You called my handler actively from server!"));
}
byte[] respBytes = String
.format("Client received your message, but no handler matched. Your meta is %s and data is %s",
metadataUtf8, dataUtf8).getBytes();
return Mono.just(DefaultPayload.create(respBytes));
}
))
// 设置重连策略
.reconnect(Retry.backoff(2, Duration.ofMillis(500)))
.connect(
TcpClientTransport.create(
TcpClient.create()
.host("127.0.0.1")
.port(8099)))
.block();
在这里我们设置客户端能够接收 RequestResponse
类型的服务端请求,仔细观察可以看到,服务端发送的请求也是可以携带包含路由信息的 metadata
的,在客户端,我们也可以根据 Payload 中的路由信息将请求分发到不同方法中处理。
为了方便演示,如果服务端调用时指定的路由信息是 request.status.callback
,那么服务端就是在完成一个由客户端发起的,异步执行的任务后调用客户端的回调函数返回任务执行结果。
如果服务端调用时指定的路由信息是 request.server.call
,那么服务端就是在主动调用客户端以获取一些状态信息。
当然,使用上面的代码设置客户端可被调用的 RSocket 方法有一个局限性,那就是我们只能设置 RequestResponse
FireAndForget
RequestStream
Channel
这四种通信模式的其中一种。也就是说,用这种方法,服务端无法同时向服务端发出 RequestResponse
FireAndForget
RequestStream
Channel
请求。本文会在第四部分展示如何让客户端支持同时响应这四种通信模式。
2. 场景:客户端提交一个耗时任务,服务端完成任务后使用回调函数返回结果
如果客户端提交一个耗时任务,服务端可以接受这个任务然后立刻返回响应:“任务提交成功”,然后执行任务。当任务执行完,服务端再使用回调函数将结果返回给客户端。
我们不妨将执行任务的模块封装成一个 Spring Service:
@Service
public class RequestProcessor {
private static final Logger logger = LoggerFactory.getLogger(RequestProcessor.class);
public void processRequests(RSocketRequester rSocketRequester, UUID uuid) {
logger.info("[RequestProcessor.processRequests]I'm handling this!");
ByteBuf routeMetadata = TaggingMetadataCodec.createTaggingContent(ByteBufAllocator.DEFAULT, Collections.singletonList("request.status.callback"));
Mono.just("Your request " + uuid + " is completed")
.delayElement(Duration.ofSeconds(ThreadLocalRandom.current().nextInt(10, 15)))
.flatMap(
m -> rSocketRequester.rsocketClient()
.requestResponse(
Mono.just(ByteBufPayload.create(
ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT,
String.format("[TASK %s]This is a task result from server using spring.", uuid)),
routeMetadata
)))
.doOnSuccess(p -> logger.info("[RequestProcessor.processRequests]Received from client: {}", p.getDataUtf8()))
)
.subscribe();
}
}
这个 Service 中的方法接收一个 RSocketRequester
和一个 任务的 UUID,当任务完成时,这个方法会生成一个 Payload 存放任务结果,指定 metadata 中的路由信息为 request.status.callback
。这样客户端在收到这个 RequestResponse 时就能知道这是一个已经提交任务的回调。在这里我们使用 delayElement
模拟处理任务时耗时的操作。
值得注意的是,RSocketRequester
参数的来源,我们在编写服务端接收任务提交的方法时可以将其作为参数,这是 Spring RSocket 的固定用法,这样就可以拿到服务端-客户端连接的 RSocketRequester 实例,然后就可以在 Service 中通过 RSocketRequester 实例调用客户端的回调函数:
@MessageMapping("handler.task")
public Mono<String> task(String request, RSocketRequester rSocketRequester) {
logger.info("[handler.request]Client request: {}", request);
UUID uuid = UUID.randomUUID();
this.requestProcessor.processRequests(rSocketRequester, uuid);
return Mono.just(uuid.toString());
}
3. 场景:服务端主动调用客户端获取信息
我们在【RSocket】使用 RSocket (一)——建立连接一文中已经在连接建立的时刻将客户端-服务端连接的 RSocketRequester
实例保存在一个 ConcurrentHashMap
中了。我们可以通过一些机制,比如定时任务,或者使用 REST API 向服务端下命令的方式,让服务端主动调用已经建立连接的客户端的 RSocket 方法。
在这个示例里,我们编写两个 REST API,一个 API 返回所有已连接到服务端的客户端信息,包括客户端 UUID、连接建立的时间等:
@ResponseBody
@GetMapping("/client/list")
public List<ConnectedClientDto> clientsInfo() {
List<ConnectedClientDto> info = new ArrayList<>();
RSocketController.clientsManager.clients.forEach((key, value) -> {
info.add(new ConnectedClientDto(key, value.connectedTime));
});
return info;
}
另一个 API 用于触发服务端向客户端发送请求:
@GetMapping("/client/call")
public ServerResponse callFromServer(String clientRoute, String clientUUID) {
RSocketRequester requester = RSocketController.clientsManager.getClientRequester(clientUUID);
if (requester == null) {
return new ServerResponse("failed: client rSocket has closed.");
}
ByteBuf routeMetadata = TaggingMetadataCodec
.createTaggingContent(ByteBufAllocator.DEFAULT, Collections.singletonList(clientRoute));
Mono.just("Server is calling you.")
// .delayElement(Duration.ofSeconds(ThreadLocalRandom.current().nextInt(5, 10)))
.flatMap(m -> requester.rsocketClient().requestResponse(
Mono.just(
ByteBufPayload.create(
ByteBufUtil.writeUtf8(
ByteBufAllocator.DEFAULT,
"This is a message from server using spring-stack."),
routeMetadata)))
.doOnSubscribe(subscription -> logger.info("subscribed."))
.doOnError(throwable -> logger.error("Error when calling client: {}", throwable.toString()))
.doOnSuccess(p -> logger.info("[test.connect.requester]Received from client: {}.", p.getDataUtf8()))
)
.subscribe();
return new ServerResponse(String.format("request from server has sent to the client %s.", clientUUID));
}
我们首先启动服务端再启动客户端,然后测试上述两个 API:
启动两个客户端和服务端后查看连接信息
向其中一个客户端发送一个请求
可以从客户端的输出看到客户端接收到了这次请求
4. 让客户端同时接收不同类型的请求
前面我们提到如果使用 .acceptor(SocketAcceptor.for...)
来添加客户端可以被调用的方法时,只能指定四种通信模式中的一种。
这时候,我们可以实现 io.rsocket.SocketAcceptor
接口,重写 accept
方法,accept
方法的返回值是 Mono<RSocket>
,我们可以实现 RSocket
接口并重写其中 fireAndForget
requestResponse
requestStream
requestChannel
四个方法来达到让客户端同时接收四种通信模式的目的。
首先实现 RSocket
接口,并重写其中的方法:
// https://github.com/joexu01/rsocket-demo/blob/master/rsocket-client-raw/src/main/java/org/example/service/ClientService.java
public class ClientService implements RSocket {
Logger logger = LoggerFactory.getLogger(ClientService.class);
static String decodeRoute(ByteBuf metadata) {
final RoutingMetadata routingMetadata = new RoutingMetadata(metadata);
return routingMetadata.iterator().next();
}
@Override
public Mono<Void> fireAndForget(Payload payload) {
logger.info("Receiving: " + payload.getDataUtf8());
return Mono.empty();
}
@Override
public Mono<Payload> requestResponse(Payload payload) {
logger.info("Receiving: " + payload.getDataUtf8());
return Mono.just(DefaultPayload.create("Client received your RequestResponse"));
}
@Override
public Flux<Payload> requestStream(Payload payload) {
return Flux.range(-5, 10)
.delayElements(Duration.ofMillis(500))
.map(obj ->
ByteBufPayload.create(
ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, obj.toString())));
}
@Override
public Flux<Payload> requestChannel(Publisher<Payload> payloads) {
return Flux.range(-5, 10)
.delayElements(Duration.ofMillis(500))
.map(obj ->
ByteBufPayload.create(
ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, obj.toString())));
}
}
这只是一个示例,如果业务需要也可以解析 Payload 中的 metadata 来实现路由。
接下来我们实现 RSocketAcceptor
接口:
// https://github.com/joexu01/rsocket-demo/blob/master/rsocket-client-raw/src/main/java/org/example/SocketAcceptorImpl.java
public class SocketAcceptorImpl implements SocketAcceptor {
@Override
public Mono<RSocket> accept(ConnectionSetupPayload connectionSetupPayload, RSocket rSocket) {
return Mono.just(new ClientService());
}
}
然后我们在初始化客户端的时候这样设定 Acceptor 即可:
RSocket socket = RSocketConnector.create().acceptor(new SocketAcceptorImpl())
【RSocket】使用 RSocket(三)——服务端主动调用客户端方法的更多相关文章
- WebSocket安卓客户端实现详解(三)–服务端主动通知
WebSocket安卓客户端实现详解(三)–服务端主动通知 本篇依旧是接着上一篇继续扩展,还没看过之前博客的小伙伴,这里附上前几篇地址 WebSocket安卓客户端实现详解(一)–连接建立与重连 We ...
- 微信-小程序-开发文档-服务端-接口调用凭证:auth.getAccessToken
ylbtech-微信-小程序-开发文档-服务端-接口调用凭证:auth.getAccessToken 1.返回顶部 1. auth.getAccessToken 本接口应在服务器端调用,详细说明参见服 ...
- Spring Cloud 服务端注册与客户端调用
Spring Cloud 服务端注册与客户端调用 上一篇中,我们已经把Spring Cloud的服务注册中心Eureka搭建起来了,这一章,我们讲解如何将服务注册到Eureka,以及客户端如何调用服务 ...
- X32位 天堂2 二章/三章 服务端协议号修改方法
[本方法适合于2004-2006年之间天堂2由初章服务端修改至二章.三章端时协议号匹配问题]服务端版本位32位初章服务端 目前大部分SF用的协议号情况: 服务端是419 客户端是 417 419 42 ...
- 在HTTP通讯过程中,是客户端还是服务端主动断开连接?
比如说:IE访问IIS,获取文件,肯定是要建立一个连接,这个连接在完成通讯后,是客户端Close了连接,还是服务端Close了连接.我用程序测模拟IE和IIS,都没有收到断开连接的消息,也就是都没有触 ...
- 使用SignalR从服务端主动推送警报日志到各种终端(桌面、移动、网页)
微信公众号:Dotnet9,网站:Dotnet9,问题或建议:请网站留言, 如果对您有所帮助:欢迎赞赏. 使用SignalR从服务端主动推送警报日志到各种终端(桌面.移动.网页) 阅读导航 本文背景 ...
- Delphi XE5通过WebService开发Web服务端和手机客户端
Delphi XE5通过WebService开发Web服务端和手机客户端介绍 我们开发一个三层的android程序 建立一个webservices stand-alone vcl applicati ...
- centos 6.5环境利用iscsi搭建SAN网络存储服务及服务端target和客户端initiator配置详解
一.简介 iSCSI(internet SCSI)技术由IBM公司研究开发,是一个供硬件设备使用的.可以在IP协议的上层运行的SCSI指令集,这种指令集合可以实现在IP网络上运行SCSI协议,使其能够 ...
- 服务端挂了,客户端的 TCP 连接还在吗?
作者:小林coding 计算机八股文网站:https://xiaolincoding.com 大家好,我是小林. 如果「服务端挂掉」指的是「服务端进程崩溃」,服务端的进程在发生崩溃的时候,内核会发送 ...
- [发布]SuperIO v2.2.5 集成OPC服务端和OPC客户端
SuperIO 下载:本站下载 百度网盘 1.修复串口号大于等于10的时候导致IO未知状态. 2.优化RunIODevice(io)函数内部处理流程,二次开发可以重载这个接口. 3.优化IO接收数据, ...
随机推荐
- 实验:STM32-ARDUINO-ESP01采用AT指令,通过MQTT连接上ONENET
1.硬件准备 要求:STM32支持Arduino. 2.程序逻辑结构 3.主流程状态机 4.测试数据抓图 5. 关键程序代码 unsigned char g_ArrTemp[1024]; int AT ...
- Yii2安装步骤
通过 Composer 安装了半天一直网络报错,配置了镜像,又报错依赖,运行跳过依赖,这个命令还是报错,我裂开. 最后只能利用归档文件安装,1.下http://www.yiiframework.com ...
- Python获取本地通达信软件的股票信息(一)
说明 在量化计算中,经常需要取得类似最新的股票代码表等相关的股票信息.虽然可以网络上可以很容易获得这些信息,但当处于离线或网络状况不佳状态时,我们可以从诸如<通达信金融终端>之类的本地数据 ...
- c二级
一·基本数据结构与算法 算法基本概念 算法:解决问题的方法 程序:用某种语言来诠释算法,将算法写成代码. 算法基本特征: 1.可行性 2.确定性 3.有穷性 4.有足够的情报 算法的基本要素 1.算法 ...
- C# WPF 自学 MVVM简单介绍
一.MVVM介绍 MVVM是Model-View-ViewModel(模型-视图-视图模型)的缩写形式 1.View就是用xaml实现的界面,负责与用户交互,接收用户输入,把数据展现给用户. 2.Vi ...
- css/js使用小技巧记录
1.白底小图标换色 .iconBox { position: relative; width: 19px; height: 19px; overflow: hidden; // 隐藏原本颜色的图片 . ...
- html页面下载为docx文档
1.安装要用到的两个插件:html-docx-js-typescript.file-saver. 2.导入两个方法: import { asBlob } from 'html-docx-js-type ...
- software engineering homework 1
1. 回顾你过去将近3年的学习经历 当初你报考的时候,是真正喜欢计算机这个专业吗? 你现在后悔选择了这个专业吗? 你认为你现在最喜欢的领域是什么(可以是计算机的也可以是其它领域)? 答:一开始感觉编程 ...
- json中有List集合时,转换List集合内元素的时间格式
1 public class User implements Serializable { 2 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss" ...
- centos 发送邮件
安装mailx yum install -y mailx 配置邮件服务器信息 vi /etc/mail.rc set from=xxxx@yyy.com set smtp=smtp.yyy.com s ...