-
Notifications
You must be signed in to change notification settings - Fork 167
RSocket Service Interface Design
Alibaba RSocket Broker的Java端接入采用的是基于Java Interface接口,对于普通的RPC接口来说,你几乎不用关注。 但是考虑到系统和接口设计的整体性,你还是需要了解一下服务接口规范,同时了解为何这么设计。 按照Java的规范,我们从Package开始,然后是Interface,最后到Method,进行详细的介绍。
Package是用于划分服务的命名空间,这个在其他语言中都有体现,而且gRPC中也有对应的package。
在设计服务接的package时,我们希望package有特定的特征,并率属于某一特定的应用下,这个和HTTP的虚拟主机或者子域名设计是类似的。 假设你将某一子域名视为一些服务的集合,那么这个子域名通常会承担路由功能,如发布HTTP REST API服务。 虽然RSocket Broker设计上并没有对package有这个要求,但是我们希望你能够提前意识到这点,这对你以后的服务规划和治理有一定的帮助。 Package规划的好,在某种程度上我们可以基于package进行一定的路由,我们不建议两个应用共享某一个package进行RSocket服务发布。
- 👎不好的设计: com.alibaba.user这个package同时被App1和App2进行RSocket服务发布。
- 👍好的设计: App1使用com.alibaba.user.account这个package, app2使用com.alibaba.user.security这个package
这个是标准的Java Interface即可,你只需要标准的Java规范编写即可,这里一定要注意Service Name要求大写开头。 另外Alibaba RSocket Broker竭力鼓励你采用Microservice API Patterns 我们也在向这方面靠拢。
🚫 切忌不同的应用发布package和service name都相同的服务。
提示: Alibaba RSocket Broker不是完全基于Java Interface名称进行服务接口定义的,你可以自定义服务名。
- 服务端例子: 刚开始时,你设计了一个接口,名称为 com.alibaba.user.UserService,但是在后续你发现,可能AccountService更贴切点, 考虑到之前的兼容性,代码你使用AccountService,但是发布的时候你可以继续使用"com.alibaba.user.UserService"对外提供服务。 样例代码如下:
@RSocketService(serviceInterface = AccountService.class, name = "com.alibaba.user.UserService")
public class AccountServiceImpl implements AccountService {
}
- 接入端例子 另外一个考虑是应用接入的要求,如果命名规范不太好,应用端的接口名可能和应用中的术语重名,如上述的UserService,你的应用中也有这个服务接口, 你可以考虑新建一个接口extend UserService,在构建通讯即可时指定一下服务名称即可。 代码如下:
@ServiceMapping(value="com.alibaba.user.UserService")
interface AlibabaUserService extends UserService {
}
这个和许多开发语言中的alias关键字设计意义的,如 alias Email = String
method的命名规范和Java保持一致,但是考虑到RSocket对多语言的支持,我们不支持函数重载,也就是函数不能重名, 这个和gRPC、Thrift等设计是一致的,如果你坚持使用要使用函数重载,可以通过@ServiceMapping来支持,稍后会说明。
由于函数的签名会涉及到对应的RSocket请求,这里主要讲述一下对应函数签名设置:
Fire and Forget是不需要有确认的,所以函数的返回值类型只能为Mono,如下:
Mono<Void> methodName(params)
对应典型的RPC请求,基本不需要调整什么,返回值为Mono类型就可以,注意不能为Mono,如下:
Mono<Result> methodName(params)
Request/Stream表示你要接收流式数据,所以返回值类型为Flux即可,如下:
request_stream: Flux<Result> methodName(params)
Channel是双向通讯,就RSocket这个场景中,有四个函数签名方式,如下:
- 在双向通讯中,有时第一个消息非常重要,根据第一个消息参数才能决定返回Flux如何构建,如下:
Flux<Result> methodName(Object param1, Flux<Object> flux);
- 无首参数型
Flux<Result> methodName(Flux<Object> flux);
- 返回值为Mono,你也可以理解为stream/request,考虑到首参类型,有以下两种:
Mono<Result> methodName(Object param1, Flux<Object> flux);
Mono<Result> methodName(Flux<Object> flux);
上述的函数签接口设计,这个是和Spring RSocket是一致的,你可以参考 https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#rsocket 这样可以保证你通过Spring RSocket的原生接口同样可以访问你设计的RSocket Service。
在通讯中,我们可能要传输一下字节流或者二进制数据,如视频流、图片、合同文件等等,这个时候参数和返回值可能为ByteBuf,ByteBuffer或者byte[]。
这里有一个约定,如果是ByteBuf或者ByteBuffer做为参数时,函数只能有一个参数,不能有多个参数,如下:
//合法
Mono<Long> saveAvatar(ByteBuf content)
//不合法
Mono<Long> saveAvatar(String nick, ByteBuf content)
这样做的考虑主要是性能的考虑,如果你需要参入参数,请参考RSocket Binary: https://github.com/alibaba/alibaba-rsocket-broker/wiki/RSocket-Binary
Alibaba RSocket还提供了一个Java Annotation,也就是@ServiceMapping,其主要的目的是调整RSocket通讯接口的默认设置,通常你不需要进行任何设置,在某些情况下你可能会用到。
如你需要重新命名函数名称,同时保持一定兼容性,这个和interface即可重命名是一样的道理。
public interface UserService {
@ServiceMapping("findUserById")
Mono<User> findById(Integer id);
}
有时接入到想调整一下接口设计,如对多个接口进行组合一下,不想使用服务端提供的那么多函数接口的interface或者对方SDK太大,你可以进行以下调整:
interface UserAggregateService {
@ServiceMapping(value = "com.alibaba.user.UserService.findUserById")
Mono<User> findUserById(Integer id);
@ServiceMapping(value = "com.alibaba.user.AccountService.findAccountById")
Mono<Account> findAccountById(Integer id);
}
如果你使用Kotlin来编写FaaS,你可能会用到这样的特性: 你不需要引入对方那么大的SDK,Kotlin的interface和data class也非常简单。
前面我们讲到考虑多语言的要求,函数不能重名。如果你还想在Java中使用函数重载,请使用@ServiceMapping调整value值来达到函数重载的目的。样例代码如下:
interface UserService {
@ServiceMapping(value = "findUserById")
Mono<User> findUser(Integer id);
@ServiceMapping(value = "findUserByIdOrEmail")
Mono<User> findUser(Integer id, String email);
}
Java 8的interface设计中提供了default method,这个特性对接口设计非常有用。如RPC调用中传入File对象,支持这个特性非常麻烦,但是借助于default method就不一样啦,代码如下:
Mono<Long> saveAvatar(ByteBuf content);
default Mono<Long> saveAvatar(Long id, File avatar) {
// some logic here
}
利用好default method,可以非常容易实现以下一些特性:
- 利用default method保持接口兼容: 如函数参数调整都,都可以通过default method来保证一定的兼容性
- default method对调用方友好,如File, InputStream等原本RPC非常难支持的,现在通过default method进行转换一些都可以支持
- 预处理逻辑: 如果你想在客户端进行一些数据验证等,default method也是很好的手段
- 性能考量: 如果你不想做对象序列化,你可以default method将对象转换为ByteBuf,然后使用ByteBuf传输数据,这个就非常高效。
- default method和interface method支持重名的: 虽然服务接口中的函数是不允许重名的,但是default method可以和这些函数重名,主要是default method的逻辑运行都是在客户端的,并未参与到实际的通讯中。
Alibaba RSocket默认支持Interface default method,不需要什么额外设置。
我们知道RSocketRemoteServiceBuilder和@ServiceMapping都可以定义一些RSocket服务对应的元信息,如group, version, endpoint, service name等, 如果RSocketRemoteServiceBuilder和@ServiceMapping同时定义了这些元信息,那就会出现冲突,那么该如何处理? 在目前的逻辑中, RSocketRemoteServiceBuilder对group, version和endpoint这三者有最高的优先权,而@ServiceMapping对其他元信息具有最好的优先权, 这个考虑主要是group, version和endpoint这三者和Ops和程序运行的环境相关联,所以拥有最高的优先权,而其他如service name, en/decoding等这些和代码相关连, 那么编写代码的开发者定义在代码内的@ServiceMapping就具有最高的优先权,而服务的调用方通常不太了解这些细节,即便一些配置错误,也不应该影响到服务的调用。
- Binary: byte stream
- Async message
- Multi transports
- Reactive Semantics
- request/response
- request/stream
- fire-and-forget
- channel
- TCP+TLS
- WebSocket+TLS
- UDP(Aeron)
- RDMA