前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >手写RPC框架(二)--手写客户端和服务端源码

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

原创
作者头像
小时的棒棒糖
修改2023-11-30 21:33:25
2610
修改2023-11-30 21:33:25
举报

##整体模块:

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" +
            "    @Override\n" +
            "    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腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.客户端 : 如何来动态地生成桩?
  • 2.服务端:RPC 服务是怎么实现的?
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档