【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接收数据, ...
随机推荐
- holiday11
holiday11--linux basis From today I will write my note in English ,hope I will stick to it. user and ...
- 笛卡尔树 Cartesian tree
给个板子题 笛卡尔树是这样的一种数据结构:对于 \(n\) 个二元组 \((key, value)\) 形成的笛卡尔树,满足如下性质 其 \(key\) 值满足二叉搜索树性质 (中序排列单调递增),\ ...
- 挖坑——未完成题目列表QwQ
OI一些的小计划: 日拱一卒,功不唐捐! Unfinished luogu P2814 家谱 Luogu P2076 聚会 luogu P2212 Watering the Fields 草坪上有N ...
- ssh连接不上、Xshell意外关闭Socket error Event: 32 Error: 10053.
Xshell意外关闭可能会出现这种问题,如遇如下错误可解决: Connecting to 47.106.80.28:22- Connection established. To escape to l ...
- Linux软件防火墙iptables
Netfilter组件 内核空间,集成在linux内核中 官网文档:https://netfilter.org/documentation/ 扩展各种网络服务的结构化底层框架 内核中选取五个位置放了五 ...
- PHP Redis - Set(集合)
Redis 的 set 无序集合,与 list 类似,特殊之处在于 set 可以自动排重,不会出现重复数据 集合中最大的成员数为 232-1 (4294967295, 每个集合可存储40多亿个成员). ...
- 将Vue项目部署到Nginx中,出现的400,405,200响应空等问题处理
最近用Vue3写了个项目,然后对接后台接口. 在本地vue配置文件中,配置了反向代理.成功请求了后端接口. 自测没有问题. 打包vue,发布到nginx中.运行nginx,成功显示了页面. 当点击页面 ...
- DML操作数据
添加数据 insert into 表名(列的名称)(数据);ps:列的名称用` `包围可以减少出错 添加全部数据的时候可以把列的名称省略: 修改数据 update 表名 set 列名=数据,列名= ...
- Java (新)将Excel数据读取到ListMap
Java (新)将Excel数据读取到ListMap Maven依赖: pom.xml <!-- excel --> <dependency> <groupId>o ...
- Delphi数据库备份
此处代码只是测试代码,仅仅是测试 //环境:D7+SQL Server 2008 1 unit Unit1; 2 3 interface 4 5 uses 6 Windows, Messages, S ...