##整体模块:
Module 说明
client 客户端
server 服务端
rpc-api RPC框架接口
hello-service-api 接口定义
rpc-netty 基于Netty实现的RPC框架
1.客户端 : 如何来动态地生成桩?
在 RPC 框架中,最关键的就是理解“桩”的实现原理,桩是 RPC 框架在客户端的服
务代理,它和远程服务具有相同的方法签名,或者说是实现了相同的接口,客户端在调用
RPC 框架提供的服务时,实际调用的就是“桩”提供的方法,在桩的实现方法中,它会发
请求到服务端获取调用结果并返回给调用方。
- 首先我们先定一个 StubFactory 接口,这个接口就只有一个方法:它的功能就是创建一个桩的实例
public interface StubFactory {
<T> T createStub(Transport transport, Class<T> serviceClass);
}
- 如何来实现这个工厂方法,创建桩呢?这个桩它是一个由 RPC 框架生成的类,这个类它要实现给定的接口,里面的逻辑就是把方法名和参数封装成请求,发送给服务端,然后再把服务端返回的调用结果返回给调用方
public class DynamicStubFactory implements StubFactory{
private final static String STUB_SOURCE_TEMPLATE =
"package com.github.liyue2008.rpc.client.stubs;n"
"import com.github.liyue2008.rpc.serialize.SerializeSupport;n"
"n"
"public class %s extends AbstractStub implements %s {n"
" @Overriden"
" public String %s(String arg) {n"
" return SerializeSupport.parse(n"
" invokeRemote(n"
" new RpcRequest(n"
" "%s",n"
" "%s",n"
" SerializeSupport.serialize(arg)n"
" )n"
" )n"
" );n"
" }n"
"}";
@Override
@SuppressWarnings("unchecked")
public <T> T createStub(Transport transport, Class<T> serviceClass) {
try {
// 填充模板
String stubSimpleName = serviceClass.getSimpleName() "Stub";
String classFullName = serviceClass.getName();
String stubFullName = "com.github.liyue2008.rpc.client.stubs." stubSimpleName;
String methodName = serviceClass.getMethods()[0].getName();
String source = String.format(STUB_SOURCE_TEMPLATE, stubSimpleName, classFullName, methodName, classFullName, methodName);
// 编译源代码
JavaStringCompiler compiler = new JavaStringCompiler();
Map<String, byte[]> results = compiler.compile(stubSimpleName ".java", source);
// 加载编译好的类
Class<?> clazz = compiler.loadClass(stubFullName, results);
// 把 Transport 赋值给桩
ServiceStub stubInstance = (ServiceStub) clazz.newInstance();
stubInstance.setTransport(transport);
// 返回这个桩
return (T) stubInstance;
} catch (Throwable t) {
throw new RuntimeException(t);
}
}
}
- 解读:我们采用的方式是:先生成桩的源代码,然后动态地编译这个生成的源代码,然后再加载到 JVM 中。
- 一起来看一下这段代码,静态变量 STUB_SOURCE_TEMPLATE 是桩的源代码的模板,我们需要做的就是,填充模板中变量,生成桩的源码,然后动态的编译、加载这个桩就可以了。
先来看这个模板,它唯一的这个方法中,就只有一行代码,把接口的类名、方法名和序列化后的参数封装成一个 RpcRequest 对象,调用父类 AbstractStub 中的 invokeRemote 方法,发送给服务端。invokeRemote 方法的返回值就是序列化的调用结果,我们在模板中把这个结果反序列化之后,直接作为返回值返回给调用方就可以了。
再来看下面的 createStrub 方法,从 serviceClass 这个参数中,可以取到服务接口定义的所有信息,包括接口名、它有哪些方法、每个方法的参数和返回值类型等等。通过这些信息,我们就可以来填充模板,生成桩的源代码。
桩的类名就定义为:“接口名 Stub”,为了避免类名冲突,我们把这些桩都统一放到固定的包 com.github.liyue2008.rpc.client.stubs 下面。填充好模板生成的源代码存放在 source 变量中,然后经过动态编译、动态加载之后,我们就可以拿到这个桩的类 clazz,利用反射创建一个桩的实例 stubInstance。把用于网络传输的对象 transport 赋值给桩,这样桩才能与服务端进行通信。到这里,我们就实现了动态创建一个桩。
小结: 编程技巧:使用依赖倒置原则解耦调用者和实现
通过定义一个接口来解耦调用方和实现。在设计上这种方法称为“依赖倒置原则(Dependence Inversion Principle)”,它的核心思想是,调用方不应依赖于具体实现,而是为实现定义一个接口,让调用方和实现都依赖于这个接口。这种方法也称为“面向接口编程”。
2.服务端:RPC 服务是怎么实现的?
RPC 框架的服务端主要需要实现下面这两个功能:
- 服务端的业务代码把服务的实现类注册到 RPC 框架中 ;
- 接收客户端桩发出的请求,调用服务的实现类并返回结果。
首先来看服务端中,使用 Netty 接收所有请求数据的处理类 RequestInvocation 的 channelRead0 方法。
代码语言:java复制@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, Command request) throws Exception {
RequestHandler handler = requestHandlerRegistry.get(request.getHeader().getType());
if(null != handler) {
Command response = handler.handle(request);
if(null != response) {
channelHandlerContext.writeAndFlush(response).addListener((ChannelFutureListener) channelFuture -> {
if (!channelFuture.isSuccess()) {
logger.warn("Write response failed!", channelFuture.cause());
channelHandlerContext.channel().close();
}
});
} else {
logger.warn("Response is null!");
}
} else {
throw new Exception(String.format("No handler for request with type: %d!", request.getHeader().getType()));
}
}
根据请求命令的 Hdader 中的请求类型 type,去 requestHandlerRegistry 中查找对应的请求处理器 RequestHandler,然后调用请求处理器去处理请求,最后把结果发送给客户端。
RPC 框架服务端最核心的部分如下:
代码语言:java复制@Override
public Command handle(Command requestCommand) {
Header header = requestCommand.getHeader();
// 从 payload 中反序列化 RpcRequest
RpcRequest rpcRequest = SerializeSupport.parse(requestCommand.getPayload());
// 查找所有已注册的服务提供方,寻找 rpcRequest 中需要的服务
Object serviceProvider = serviceProviders.get(rpcRequest.getInterfaceName());
// 找到服务提供者,利用 Java 反射机制调用服务的对应方法
String arg = SerializeSupport.parse(rpcRequest.getSerializedArguments());
Method method = serviceProvider.getClass().getMethod(rpcRequest.getMethodName(), String.class);
String result = (String ) method.invoke(serviceProvider, arg);
// 把结果封装成响应命令并返回
return new Command(new ResponseHeader(type(), header.getVersion(), header.getRequestId()), SerializeSupport.serialize(result));
// ...
}
- 把 requestCommand 的 payload 属性反序列化成为 RpcRequest;
- 根据 rpcRequest 中的服务名,去成员变量 serviceProviders 中查找已注册服务实现类的实例;
- 找到服务提供者之后,利用 Java 反射机制调用服务的对应方法;
- 把结果封装成响应命令并返回,在 RequestInvocation 中,它会把这个响应命令发送给客户端。
小结:
在 RPC 框架的服务端处理客户端请求的业务逻辑中,我们分两层做了两次请求分发:
在 RequestInvocation 类中,根据请求命令中的请求类型 (command.getHeader().getType()),分发到对应的请求处理器 RequestHandler 中;
RpcRequestHandler 类中,根据 RPC 请求中的服务名,把 RPC 请求分发到对应的服务实现类的实例中去。
这两次分发采用的设计是差不多的,但你需要注意的是,这并不是一种过度设计。原因是,我们这两次分发分别是在不同的业务抽象分层中,第一次分发是在服务端的网络传输层抽象中,它是网络传输的一部分,而第二次分发是 RPC 框架服务端的业务层,是 RPC 框架服务端的一部分。良好的分层设计,目的也是让系统各部分更加的“松耦合,高内聚”。
_源码链接地址:_GitHub 的simple-rpc-framework项目中
我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!