基于RSocket的Java与浏览器JS通信

RSocket简介

看这里

Reactor Core

关于Spring的Reactor项目,可以参考这里学习

Java服务端

Maven依赖

 <!-- 基于Netty的RSocket -->
 <dependency>
     <groupId>io.rsocket</groupId>
     <artifactId>rsocket-core</artifactId>
     <version>1.0.1</version>
 </dependency>
 <dependency>
     <groupId>io.rsocket</groupId>
     <artifactId>rsocket-transport-netty</artifactId>
     <version>1.0.1</version>
 </dependency>

创建RSocket服务端

共三步:

  1. 创建基于WebSocket的传输层transport;
// 创建一个Http服务,WebSocket协议依赖Http协议
HttpServer server = HttpServer.create()
        .host("localhost")
        .port(8009);

// 实例化RSocket的transport,基于WebSocket实现
WebsocketServerTransport transport = WebsocketServerTransport.create(server);
  1. 创建一个用于实现四种交互方式的RSocket实现类
    private static class RSocketImpl implements RSocket {
        @Override
        public Mono<Void> fireAndForget(Payload payload) {...}

        @Override
        public Mono<Payload> requestResponse(Payload payload) {...}

        @Override
        public Flux<Payload> requestStream(Payload payload) {...}

        @Override
        public Flux<Payload> requestChannel(Publisher<Payload> payloads) {...}
    }
  1. 创建RSocket服务端,并用RSocket实现类接收rsocket连接请求
CloseableChannel channel = RSocketServer.create()
        .acceptor((setup, rSocket) -> {
            // rSocket 是客户端的RSocket
            // 建立连接时会进入这里,在此处进行身份校验
            // 这里就简单的校验一下metadata的值是否为123456,生产中应该将鉴权信息解析出来并校验有效性
            String meta = setup.getMetadataUtf8();
            if (meta.equalsIgnoreCase("123456")) {
                // 校验成功时,需要返回一个RSocket实例,用于处理客户端的四种请求
                System.out.println("客户端连接建立");
                return Mono.just(new RSocketImpl());
            } else {
                // 校验失败时,返回错误信息,此时会触发客户端的onError,RSocket连接也没有成功建立
                System.out.println("密码错误");
                return Mono.error(new IllegalArgumentException("密码错误"));
            }
        })
        // 绑定到传输层transport,可以选择不同的transport实现,底层的传输协议可以不同(WebSocket\TCP\Aero)
        .bind(transport)
        .block();
System.out.println("server启动" + channel.address());

上面在接收新的客户端连接请求时,进行了鉴权操作,比较简单实现,生产环境中可以考虑使用用户名密码、AK\SK、JWT等携带鉴权信息进行校验。

服务端对四种交互方式的处理

在服务端想要处理一个客户端的四种交互请求,只需要在RSocket实现类中处理即可,这里是在RSocketImpl类中。

fireAndForget

@Override
publicMono<Void> fireAndForget(Payload payload) {
    System.out.println("[fireAndForget]Client:" + payload.getDataUtf8());
    return Mono.empty();
}

这个模式下,客户端发送消息后不关心服务端的返回,因此服务端在接收到数据后返回一个empty就可以了。请求一次。

requestResponse

@Override
public Mono<Payload> requestResponse(Payload payload) {
    System.out.println("[requestResponse]Client:" + payload.getDataUtf8());
    System.out.println("[requestResponse]Server:好的,多喝热水");
    return Mono.just(DefaultPayload.create(wrapMsg("好的,多喝热水")));
}

requestResponse类似Http协议的请求与响应,客户端的一次请求服务端需要有一个响应。因为只需要一个响应,所以返回一个Mono即可。请求一次,返回一次。

requestStream

@Override
public Flux<Payload> requestStream(Payload payload) {
    System.out.println("[requestStream]Client:" + payload.getDataUtf8());
    return Flux.range(0, 4)
            .map(i -> {
                if (i == 0) {
                    return "重要的事情说三遍";
                } else {
                    return "多喝热水 * " + i;
                }
            })
            .doOnNext(str -> {
                System.out.println("[requestStream]Server:" + str);
            })
            .map(RSServer::wrapMsg)
            .map(DefaultPayload::create);
}

requestStream模式下,客户端请求的是一系列数据,服务端会返回多个数据给客户端,因此方法返回的是一个Flux。Flux代表的事件源每发射一个事件,就会向客户端发送一个消息(事件)。请求一次,返回多次。

requestChannel

requestChannel模式得到一个双工通道,客户端和服务端均可以向通道中发送任意数量的消息,直到有一方终止通道。请求多次,返回多次。
在RSocket的服务端中,双工通道用一个客户端事件源Publisher和一个服务端事件源Flux来体现。当客户端或服务端事件源有一方调用了onComplete或onError时,通道会被关闭。
这里我验证双工通道的方法是将服务端写成一个echo服务,每次收到客户端消息的时候都返回一个数据。先看代码

@Override
public Flux<Payload> requestChannel(Publisher<Payload> payloads) {
    // 这里接收到的第一个payload是申请channel时传递的payload
    Flux<Payload> clientPayloadFlux = Flux.from(payloads);

    // 返回值的Flux是返回给客户端的payload,不断的异步发射payload来实现向客户端发送数据

    // 要实现在接收到客户端消息的时候,返回给客户端一个消息,首先需要订阅客户端的flux,然后再发射出去
    // 这里写了一个EchoController来实现
    EchoController controller = new EchoController();
    clientPayloadFlux.subscribe(controller);
    return Flux.from(controller);
}

这个方法里,入参payloads就是客户端的事件源Publisher,为了方便操作,一般也把它封装为Flux。为了得到客户端发送过来的数据,需要订阅clientPayloadFlux;为了发送数据给客户端,需要创建一个Publisher事件源来发射数据。在这里我创建了一个EchoController类,它既是订阅客户端Flux的Subscriber,也是发射事件给客户端的Publisher,声明如下。

class EchoController extends BaseSubscriber<Payload> implements Publisher<Payload> {
	// 用于统计从客户端收到了多少个消息
    private AtomicInteger msgCounter = new AtomicInteger(0);
    private Subscriber<? super Payload> subscriber;

    @Override
    public void subscribe(Subscriber<? super Payload> s) {
        this.subscriber = s;
    }

    @Override
    protected void hookOnNext(Payload payload) {
        // 在这个钩子函数中,处理客户端发送过来的数据
        String data = payload.getDataUtf8();
        System.out.println("[requestChannel]Client:" + data);

        // 处理完客户端发送的数据,再返回给客户端一个payload
        // 通过Subscriber来发射,但如果是第一个payload进来,这个时候publisher还没被订阅,subscriber为空,不能发射数据
        if (this.subscriber != null) {
            String msg = "知道了,你多喝热水(" + msgCounter.incrementAndGet() + ")";
            System.out.println("[requestChannel]Server:" + msg);
            this.subscriber.onNext(DefaultPayload.create(wrapMsg(msg)));
        }
    }

    @Override
    protected void hookOnError(Throwable t) {
        System.out.println("[requestChannel]Client发送了一个异常");
        t.printStackTrace();
    }

    @Override
    protected void hookOnComplete() {
        System.out.println("[requestChannel]Client结束了这个channel");
    }
}

EchoController通过继承BaseSubscriber来实现订阅者的功能。
BaseSubscriber中有hookOnXXX方法,重写这些方法即可实现Subscriber中onXXX方法同样的功能,而且子类实现的hookOnNext和hookOnComplete中如果抛出异常,会被自动转交给hookOnError处理,比较方便。继承BaseSubscriber相比较直接实现Subscriber的其他好处,待研究。

EchoController实现Publisher的subscribe方法,来将Subscriber存储下来,并在接收到客户端数据时,通过存储的Subscriber来发射数据给客户端。

wrapMsg

因为JS客户端使用JSON来格式化Payload,所以发送数据给客户端时,要发送一个JSON字符串,这个方法用来将字符串消息包装为JSON串。

private static String wrapMsg(String msg) {
    Map<String, String> data = new HashMap<>();
    data.put("msg", msg);
    return JacksonUtil.toJSONString(data);
}

JS客户端

RSocket的js实现有两种,一个是在Node环境中使用的基于TCP协议实现,另一个是在浏览器环境中使用的基于WebSocket实现,我们使用浏览器的实现。

创建项目 & 安装依赖

首先新建一个Vue项目,通过vue-cli创建(请自行百度)完成后,安装rsocket的npm依赖,共有两个。

npm install --save rsocket-core
npm install --save rsocket-websocket-client

连接服务端

在任意一个组件中可以开始创建RSocket客户端连接,我在App.vue组件的created函数中建立连接。

// 创建一个RSocket客户端
window.rsclient = new RSocketClient({
    // payload数据使用json序列化
    serializers: JsonSerializers,
    setup: {
        keepAlive: 60000,
        lifetime: 180000,
        dataMimeType: 'application/json',
        metadataMimieType: 'application/json',
    },
    // 在浏览器端,需要使用RSocketWebSocketClient作为传输协议
    transport: new RSocketWebSocketClient({ url: 'ws://localhost:8009' }),
})

// 连接服务端
window.rsclient.connect();

这里创建连接会报错,提示REJECTED_SETUP密码错误,这是因为Java服务端在建立连接时校验了setup payload中的密码,而我在建立连接的时候并没传递setup payload,修改如下即可成功建立连接。
另外,为了方便测试,在连接服务端成功后,将rsocket实例赋值给全局变量引用。

// 创建一个RSocket客户端
window.rsclient = new RSocketClient({
   // payload数据使用json序列化
    serializers: JsonSerializers,
    setup: {
        keepAlive: 60000,
        lifetime: 180000,
        dataMimeType: 'application/json',
        metadataMimieType: 'application/json',
        payload: {
            metadata: 123456,
        }
    },
    // 在浏览器端,需要使用RSocketWebSocketClient作为传输协议
    transport: new RSocketWebSocketClient({ url: 'ws://localhost:8009' }),
});

//连接服务端
window.rsclient.connect().then(rsocket => {
    console.info('成功连接到服务端');
    window.rsocket = rsocket;
});

Flowable API

RSocketJS并没有依赖reactor-core-js,而是自己实现了两个类Flowable、Single来实现类似RxJS的操作,分别对应reactor中的Flux和Mono。
Flowable是发射一系列事件的事件源;Single是只发射一个事件,而且直接会触发onComplete操作符。
使用方式与Flux和Mono类似,订阅即可。

flowable.subscribe({
	onNext: payload => {},
	onComplete: () => {},
	onError: error => {},
	onSubscribe: subscription => {
		subscription.request(0x7fffffff);
		// subscription.cancel();
	}
});
single.subscribe({
	onComplete: payload => {},
	onError: error => {},
	onSubscribe: cancel => {
		// 调用cancel方法来停止触发onComplete或onError  
		cancel();
	}
});

四种交互方式

fireAndForget

window.rsFireAndForget = (msg) => {
    // fireAndForget模式下,客户端将消息发送出去之后,不关心服务端的返回
    window.rsocket.fireAndForget({
        data: msg
    });
};

window.rsFireAndForget('hello, faf');

连接服务端成功后,执行上面代码,可以在服务端看到控制台有如下输出

客户端连接建立
[fireAndForget]Client:"hello, faf"

requestResponse

window.rsRequestResponse = (msg) => {
    // requestResponse模式下,服务端会返回一个payload,在RSocketJS中用Single表示
    const single = window.rsocket.requestResponse({
        data: msg
    });
    single.subscribe({
        onComplete: payload => {
            console.info(`[requestResponse]${payload.data.msg}`)
        }
    });
};

window.rsRequestResponse('requestAndResponse');
客户端连接建立
[requestResponse]Client:"requestAndResponse"
[requestResponse]Server:好的,多喝热水

requestStream

window.rsRequestStream = msg => {
    window.rsocket.requestStream({
        data: msg
    }).subscribe({
        onNext: payload => {
            console.info(`[requestStream]${payload.data.msg}`);
        },
        onComplete: () => {
            console.info('[requestStream][complete]')
        },
        onError: () => {
            debugger
        },
        // requestStream时,数据是懒发送的,只有在返回的Flowable被订阅,并且被request(n)请求数据之后才发送
        // 因此这里还需要request一下
        onSubscribe: s => {
            s.request(INT32_MAX);// 0x7fffffff
        }
    });
};

window.rsRequestStream('hello, stream');

这里有一个坑,开始的时候,订阅flowable时没有写onSubscribe,也没有去request,导致不论怎么样都没有发送requestStream的数据包。后来查官方文档,requestStream接口有这样一句:
在这里插入图片描述
大致意思是,客户端请求requestStream的数据包是懒发送到服务端的,也就是并没有真正的发送,只有在返回的Flowable被订阅,且调用了request方法请求数据才表明需要请求交互,才会发送数据包。而且request的时候,参数n的最大值是32位有符号整型的最大值,也就是0x7fffffff,超过这个值会报错。
按照上面的方式发起请求后,服务端控制台可见如下输出:

客户端连接建立
[requestStream]Client:"hello, stream"
[requestStream]Server:重要的事情说三遍
[requestStream]Server:多喝热水 * 1
[requestStream]Server:多喝热水 * 2
[requestStream]Server:多喝热水 * 3

requestChannel

requestChannel得到一个双工通道,客户端与服务端均可以发送消息,我在获取到channel之后赋值给全局变量,这样方便在控制台测试输入。

window.rsRequestChannel = msg => {
   if (window.rsChannel) {
       return;
   }

   window.rsChannel = {};
   // 创建一个Flowable用于发射数据给服务端
   window.rsChannel.flowable = new Flowable(subscriber => {
       // subscriber用与发射数据
       window.rsChannel.subscriber = subscriber;
       // availableRequest表示服务端能接受多少个数据
       window.rsChannel.availableRequest = 0;
       // 发射数据,判断了服务端能不能接受新的数据,如果不能就丢弃,如果能就通过subscriber发射
       window.rsChannel.onNext = msg => {
           if (window.rsChannel.availableRequest > 0) {
               window.rsChannel.subscriber.onNext({
                   data: msg
               });
               window.rsChannel.availableRequest--;
           }
       };

       // 当这个Flowable被订阅的时候,传递一个subscription,用于处理订阅者的取消订阅或请求数据操作
       subscriber.onSubscribe({
           cancel: () => {
               // 当订阅者取消订阅时,设置不能再发送数据
               // 一般是服务端发生异常主动断开连接或挂掉时
               window.rsChannel = null;
           },
           request: n => {
               // 当服务端准备好接受数据,会调用该方法提示能接受的数据数量n
               window.rsChannel.availableRequest += n;
               console.info('[requestChannel]服务端已就绪 ' + n);

               if (n === 1) {
                   // 第一次就绪时发射第一个数据
                   window.rsChannel.subscriber.onNext({
                       data: msg
                   });
                   window.rsChannel.availableRequest--;
               }
           }
       })
   });

   window.rsocket.requestChannel(window.rsChannel.flowable)
       .subscribe({
           onNext: payload => {
               console.info(`[requestChannel]${payload.data.msg}`);
           },
           onComplete: () => {
               window.rsChannel = null;
               console.info('[requestChannel][complete]')
           },
           onError: () => {
               debugger
           },
           onSubscribe: s => {
               console.info('[requestChannel]客户端已就绪');
               s.request(INT32_MAX);
           }
       });
}

和requestStream一样,返回Flowable时也是懒发送数据的,需要订阅返回的Flowable并request(n)。

上面创建客户端发送数据的Flowable时,考虑到服务端请求的数据数量可能不多,做了背压处理,如果觉得不需要,可以将创建Flowable的代码改成下面这样:

// 创建一个Flowable用于发射数据给服务端
window.rsChannel.flowable = new Flowable(subscriber => {
    // subscriber用与发射数据
    window.rsChannel.subscriber = subscriber;
    window.rsChannel.onNext = msg => {
        window.rsChannel.subscriber.onNext({
            data: msg
        });
    };

    // 当这个Flowable被订阅的时候,传递一个subscription,用于处理订阅者的取消订阅或请求数据操作
    subscriber.onSubscribe({
        cancel: () => {
            // 当订阅者取消订阅时,设置不能再发送数据
            // 一般是服务端发生异常主动断开连接或挂掉时
            window.rsChannel = null;
        },
        request: n => {
            // 当服务端准备好接受数据,会调用该方法提示能接受的数据数量n
            console.info('[requestChannel]服务端已就绪 ' + n);
            // 第一次就绪时发射第一个数据
            if (n === 1) {
                window.rsChannel.subscriber.onNext({
                    data: msg
                });
            }
        }
    })
});

在控制台进行如下输入,可在服务端控制台看到对应的输出:
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值