实现一个简单的RPC

268 阅读4分钟
  1. 前言 项目使用Java开发,实现类似dubbo的框架 纯属个人兴趣实现,如果您觉得项目框架存在问题,或者实现有问题,欢迎您告诉我,可以加我QQ或者github上提交issue,同时我自己认为有问题的地方已经写在Readme中,谢谢!!!
    QQ号:1758619238
    项目地址:github.com/orgs/Hongko…

准备工作——技术选用和参考

代理(Proxy)

客户端调用接口时需要用到代理

  • Java动态代理(InvocationHandler)
  • CGLIB
  • Javassist

序列化

客户端与服务端数据传输时需要使用序列化

  • Java序列化(Serializable)
  • ProtoBuf(google提供的高效议数据交换格式工具库)
  • JSON(Jackson、fastJSON等)
  • kyro(二进制字节数组)

消费者和提供者交互

  • Netty(dubbo)
  • Http(Spring cloud)

注册中心

  • zookeeper
  • nacos
  • eureke(不推荐使用)

服务提供者和消费者配置

  • xml(自定义xsd文件,xml解析类,一般来说配置信息不多,使用DOM解析即可)
  • 注解(自定义扫描类或者整合Spring,使用Spring Context)

项目实现

dubbo包括Registry、Provider、Consumer和Monitor。目前只实现Registry、Provider、Consumer,也没有实现Spring结合的部分。同时注册中心只支持Zookeeper,其他没有实现。

服务注册

实现 ServiceRegistry 接口,首先扫描包文件获取需要注册的服务,我是自定义实现的工具类(可能存在问题,如果方便还是使用Spring的上下文获取简单而且不易出错),将服务转换为路径保存在注册中心,路径格式 casio/serviceName/provider/hostname,注册时选择 CreateMode.EPHEMERAL(临时型节点),这样服务宕机了,注册中心保存的节点会删除。

@SPI("zk")
public interface ServiceRegistry {
    void register(ServiceConfig serviceConfig) throws Exception;
}

服务发现

实现 ServiceDiscovery 接口,首先将要发现的服务转换成对应的路径(dubbo中包装为URL类实现,我在项目中简化了,只是一个String),通过注册中心找到服务的提供者主机名(ip+port),使用负载均衡选取其中一个主机名,连接服务提供者的NettyServer。消费者调用时需要注意几点,首先不要注册多个bootstrap,连接不同ip时会生成不同的 Channel,消息的发现也是通过 Channel 实现,同时数据发送给提供者后需要等待提供者处理完之后返回,因此需要 Future 模式来获取数据,这里使用 CompletableFuture 类处理。

@SPI("zk")
public interface ServiceDiscovery {
    // 服务发现接口
    InetSocketAddress lookup(RpcRequest rpcRequest);
}

协议

+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+ 
|  BYTE  |   BYTE |  BYTE  |           int            |         ........         |        | 
+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+--------+ 
|  magic | version|  type  |       content length     |            content byte[]                  | 
+--------+--------------------------------------------+--------+--------+--------+--------+--------+

协议通过RpcMessage类包装,magic等静态变量放在ProtocolConstants类中

@Data
public class RpcMessage implements Serializable {
    private byte type;  //类型:请求还是响应
    private byte version;  // 版本
    private byte[] content; // 具体传递的内容

}

一次RPC调用需要请求和返回两个实体类,分别使用RpcRequestRpcResponse表示,也是RpcMessage的content属性部分。

将请求和响应包装成协议的格式进行传输和解析,需要继承Netty的 LengthFieldBasedFrameDecoder 类将数据包装成 RpcMessage 类,通过 MessageToByteEncoder 将数据解析,同时判断接收的数据是否存在丢失。

// 实体包装成协议
protected void encode(ChannelHandlerContext channelHandlerContext, RpcMessage rpcMessage, ByteBuf byteBuf) {
    byteBuf.writeByte(ProtocolConstants.MAGIC);
    byteBuf.writeByte(ProtocolConstants.VERSION);
    byteBuf.writeByte(rpcMessage.getType());
    byteBuf.writeInt(rpcMessage.getContent().length);
    byteBuf.writeBytes(rpcMessage.getContent());
}
// 读取将二进制转换为RpcMessage实体类
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) {
    if (in.readableBytes() < ProtocolConstants.MIN_LENGTH) {
        log.error("数据解码有误,长度小于" + ProtocolConstants.MIN_LENGTH);
        return null;
    }
    int i;
    // 判断魔数是否正确
    while (true) {
        i = in.readerIndex();
        in.markReaderIndex();
        if (in.readByte() == ProtocolConstants.MAGIC) {
            break;
        }
        in.resetReaderIndex();
        in.readByte();
        if (in.readableBytes() < ProtocolConstants.MIN_LENGTH) return null;
    }
    byte version  = in.readByte();
    byte type = in.readByte();
    int length = in.readInt();
    // 判断包长度是否完整,否则还原指针位置
    if (in.readableBytes() < length) {
        in.readerIndex(i);
        return null;
    }
    byte[] data = new byte[length];
    in.readBytes(data);
    RpcMessage message = new RpcMessage();
    message.setVersion(version);
    message.setType(type);
    message.setContent(data);
    return message;
}

SPI

SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,主要用于查找对应的服务。Java的SPI机制默认放在Resource的 META-INF/services/ 文件夹下,文件名为接口名,内容为具体实现类,一行一个实现类,由于Java SPI 在查找扩展实现类的时候遍历 SPI 的配置文件并且将实现类全部实例化,无法指定实例化其中一个实现类。
因此dubbo自己实现了SPI的机制,内容变为name=具体实现类名的格式,和@SPI。通过@SPI的value值选择一个,调用getDefaultExtension时只实例化指定的实现类。
下面是读取 META-INF 的文件,加载到缓存中,参考dubbo,相对源码做了许多简化。

private Map<String, Class<?>> loadExtensionClasses() throws ExtensionException {
    SPI spi = type.getAnnotation(SPI.class);
    if (spi != null) {
        String value = spi.value();
        if (StringUtils.isNotBlank(value)) {
            String[] names = value.split(NAME_SEPARATOR);
            if (names.length == 1) {
                defaultName = names[0];
            } else {
                throw new ExtensionException("type:" + type.getName() + " @SPI value is error");
            }
        }
    }
    Map<String, Class<?>> extensionClasses = new HashMap<>();
    loadDirectory(extensionClasses, CASIO_PATH);
    loadDirectory(extensionClasses, SERVICES_PATH);
    return extensionClasses;
}

// 加载 resource/META-INF指定文件夹下的文件信息
private void loadDirectory(Map<String, Class<?>> extensionClasses, String path) {
    String fileName = path + type.getName();
    ClassLoader classLoader = ExtensionLoader.class.getClassLoader();
    Enumeration<URL> resources;
    try {
        if (classLoader == null) {
            resources = ClassLoader.getSystemResources(fileName);
        } else {
            resources = classLoader.getResources(fileName);
        }
        if (resources != null) {
            while (resources.hasMoreElements()) {
                URL url = resources.nextElement();
                loadResource(extensionClasses, url);
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

private void loadResource(Map<String, Class<?>> extensionClasses, URL url) {
    try (
            BufferedReader reader =
                    new BufferedReader(new InputStreamReader(url.openStream(), StandardCharsets.UTF_8))
    ) {
        String line;
        while ((line = reader.readLine()) != null) {
            String[] ss = line.split(NAME_SEPARATOR);
            if (ss.length == 2) {
                String name = ss[0];
                String className = ss[1];
                //
                Class<?> clazz = Class.forName(className);
                extensionClasses.putIfAbsent(name, clazz);
            }
        }
    } catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
    }
}

获取指定name的实现类

public T getDefaultExtension() throws ExtensionException {
    getExtensionClasses();
    return getExtension(defaultName);
}

@SuppressWarnings("unchecked")
public T getExtension(String name) throws ExtensionException {
    if (StringUtils.isBlank(name)) {
        throw new ExtensionException("extension name is null");
    }
    if ("true".equals(name)) {
        // dubbo中getDefaultExtension()
        throw new ExtensionException("extension name is true");
    }
    Holder<Object> holder = cachedInstances.get(name);
    if (holder == null) {
        cachedInstances.putIfAbsent(name, new Holder<>());
        holder = cachedInstances.get(name);
    }
    Object instance = holder.get();
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
                instance = createExtension(name);
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}

@SuppressWarnings("unchecked")
public T createExtension(String name) throws ExtensionException {
    Class<?> clazz = getExtensionClasses().get(name);
    if (clazz == null) {
        throw new ExtensionException("name: " + name + " has no class");
    }
    try {
        Object instance = EXTENSION_INSTANCE_MAP.get(clazz);
        if (instance == null) {
            EXTENSION_INSTANCE_MAP.putIfAbsent(clazz, clazz.newInstance());
            instance = EXTENSION_INSTANCE_MAP.get(clazz);
        }
        return (T) instance;
    } catch (InstantiationException | IllegalAccessException e) {
        e.printStackTrace();
    }
    return null;
}

参考

参考阿里文章:zhuanlan.zhihu.com/p/388848964
dubbo项目:github.com/apache/dubb…
几位大佬自己实现的RPC框架(相比我的完善许多,给我提供了许多思路):github.com/Snailclimb/…

最后,希望大家能够star一下,如果有兴趣差不多可以一起写项目,加入Hongkong-Reporters这个组,谢谢!!!