手写RPC框架(二)--手写客户端和服务端源码

2023-11-30 21:33:25 浏览数 (2)

##整体模块:

Module 说明

client 客户端

server 服务端

rpc-api RPC框架接口

hello-service-api 接口定义

rpc-netty 基于Netty实现的RPC框架

1.客户端 : 如何来动态地生成桩?

在 RPC 框架中,最关键的就是理解“桩”的实现原理,桩是 RPC 框架在客户端的服

务代理,它和远程服务具有相同的方法签名,或者说是实现了相同的接口,客户端在调用

RPC 框架提供的服务时,实际调用的就是“桩”提供的方法,在桩的实现方法中,它会发

请求到服务端获取调用结果并返回给调用方。

  • 首先我们先定一个 StubFactory 接口,这个接口就只有一个方法:它的功能就是创建一个桩的实例
代码语言:java复制
public interface StubFactory {
    <T> T createStub(Transport transport, Class<T> serviceClass);
}
  • 如何来实现这个工厂方法,创建桩呢?这个桩它是一个由 RPC 框架生成的类,这个类它要实现给定的接口,里面的逻辑就是把方法名和参数封装成请求,发送给服务端,然后再把服务端返回的调用结果返回给调用方
代码语言:java复制
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腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!

0 人点赞