从0到1开发一个简单的RPC框架

1 篇文章 0 订阅

什么是RPC?

RPC就是远程过程调用,在工作中很多需求背景都会有多个服务器,有服务器A、服务器B、服务器C。有时候就会需要服务器B调用服务器A上的某个方法,这时候就需要用到RPC了。
在这里插入图片描述
市面上也有很多成熟的RPC框架,在我国常用的就是Dubbo。因为它有比较好的社区动态,也有比较成熟的案例项目。下面对一些常见的框架进行对比:

Dubbo、Motan、gRPC、Thrift,这四种是比较常见的RPC框架

RPC开发商生态/社区活跃度适配语言
Dubbo阿里Java/Golang
Motan新浪×Java
gRPCGoogleC++/Java/Python/Objective-C/C#/Ruby/Go/PHP/Dart
ThriftFacebook比gRPC更多

上面简单了解了一下什么是RPC,接下来就开始撸代码吧。

想要完成一个简单的RPC框架需要的组成部分:注册中心、数据传输以及负载均衡。

需要使用的技术:
注册中心(zookeeper)
数据传输(netty)
序列化/反序列化(kryo)
负载均衡算法
Java反射

我们再来看看RPC框架的整体逻辑:
在这里插入图片描述

  1. 服务端(生产者)先向注册中心中注册信息(name:ip:port)。
  2. 消费者(客户端)就根据配置信息去注册中心中拉取对应的IP地址。
  3. 客户端通过Netty进行传输,先对数据进行序列化。
  4. 服务端接收到信息后进行反序列化获取到传输数据,对数据进行解析访问到指定的方法。
  5. 访问方法返回后,再通过Netty的WriteAndFlush传输到客户端。
  6. 客户端解析后获得数据,此时同步锁解除,进行接下来的逻辑。

注册中心

在这里我使用的是Zookeeper作为注册中心(你想要使用其他的当然也没问题,使用mysql都可以),然后使用Curator Framework就可以简单的操作ZK了。

zk的简单命令

查看常用命令
通过命令help查看ZK常用的命令

创建节点

##创建根节点,如果根节点没创建,则无法创建子节点
[zk: localhost:2181(CONNECTED) 2] create /shenweiqu
Created /shenweiqu

##创建子节点test,对应的数据为123
[zk: localhost:2181(CONNECTED) 3] create /shenweiqu/test '123'
Created /shenweiqu/test

获取节点数据

[zk: localhost:2181(CONNECTED) 4] get /shenweiqu/test
123

更新节点数据

[zk: localhost:2181(CONNECTED) 5] set /shenweiqu/test '123123'
[zk: localhost:2181(CONNECTED) 6] get /shenweiqu/test
123123

查看某个目录下的子节点

[zk: localhost:2181(CONNECTED) 9] ls /shenweiqu
[test]
##没有加目录名,则查询根目录所有节点
[zk: localhost:2181(CONNECTED) 10] ls /
[Lionfish, admin, brokers, cluster, config, consumers, controller_epoch, dolphinscheduler, feature, isr_change_notification, latest_producer_id_block, lionfish, log_dir_event_notification, shenweiqu, zookeeper]

查看节点状态

[zk: localhost:2181(CONNECTED) 12] stat /shenweiqu
cZxid = 0x63aae7
ctime = Tue Feb 07 09:54:28 CST 2023
mZxid = 0x63aae7
mtime = Tue Feb 07 09:54:28 CST 2023
pZxid = 0x63aae8
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 1

删除节点

[zk: localhost:2181(CONNECTED) 15] delete /shenweiqu/test
[zk: localhost:2181(CONNECTED) 16] ls /shenweiqu
[]

Curator的简单用法

创建Curator连接

 public static CuratorFramework zkClient() {
        ExponentialBackoffRetry retry = new ExponentialBackoffRetry(BASE_SLEEP_TIME_MS, MAX_RETRIES);
        String hostAddress = "";
        try {
            InetAddress localHost = Inet4Address.getLocalHost();
            hostAddress = localHost.getHostAddress();

        } catch (Exception e) {
            e.printStackTrace();
            hostAddress = "127.0.0.1";
        }

        CuratorFramework client = CuratorFrameworkFactory.builder().
                connectString(hostAddress + ":2181").
                retryPolicy(retry).
                build();
        client.start();
        return client;
    }

创建节点
通常我们将zk上的节点分为4个部分:
持久节点(PERSISTENT):只要创建就一直存在,即使集群宕机,直到手动删除。
临时节点(EPHEMERAL):临时节点的生命周期与客户端绑定,客户端的会话消失,临时节点也就消失。而且,临时节点不能作为子节点,只能够做为叶子节点。
持久顺序节点(PERSISTENT_SEQUENTIAL):除了具有持久节点的特性外,子节点还具有顺序性。
临时顺序节点(EPHEMERAL_SEQUENTIAL):除了具有临时节点的特性外,子节点还具有顺序性。

 client.create().withMode(CreateMode.PERSISTENT).forPath(path);

这种方法可以创建节点,但是如果没有创建节点就创建子节点就会报错,加上creatingParentsIfNeeded即可。

 client.create().creatingParentsIfNeeded().
 withMode(CreateMode.PERSISTENT).forPath(path);

想要创建其他类型的节点,修改withMode即可。

删除节点

client.delete().forPath(path);//如果path下有子节点则会报错提示:Node not empty: path
client.delete().deletingChildrenIfNeeded().forPath(path);

获取/更新节点下的数据

byte[] data = client.getData().forPath(path);
client.setData().forPath(path,"123".getBytes());

获取节点下的子节点

client.getChildren().forPath(path);

监听器
可以给某个节点添加监听器,当该节点的子节点发生变化时,就会调用回调函数。

PathChildrenCache pathChildrenCache = new PathChildrenCache(zkClient(), path, true);
PathChildrenCacheListener pathChildrenCacheListener = (client, cache) -> {
   do something
};

pathChildrenCache.getListenable().addListener(pathChildrenCacheListener);
pathChildrenCache.start();

代码如下

public class CuratorUtils {

    private final static Map<String, List<String>> SERVICE_ADDRESS_MAP = new ConcurrentHashMap<>();
    private final static int BASE_SLEEP_TIME_MS = 3000;
    private final static int MAX_RETRIES = 3;

    public static CuratorFramework zkClient() {
        //重试策略,3秒重试3次
        ExponentialBackoffRetry retry = new ExponentialBackoffRetry(BASE_SLEEP_TIME_MS, MAX_RETRIES);
        String hostAddress = "";
        try {
            InetAddress localHost = Inet4Address.getLocalHost();
            hostAddress = localHost.getHostAddress();

        } catch (Exception e) {
            e.printStackTrace();
            hostAddress = "127.0.0.1";
        }

        CuratorFramework client = CuratorFrameworkFactory.builder().
                connectString(hostAddress + ":2181").
                retryPolicy(retry).
                build();
        client.start();
        return client;
    }


    public static boolean createPersistentNode(String path) {
        CuratorFramework client = zkClient();
        try {
            if (client.checkExists().forPath(path) == null) {
                client.create().creatingParentsIfNeeded()
                        .withMode(CreateMode.PERSISTENT)
                        .forPath(path);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    public void deleteNode(String path){
        CuratorFramework client = zkClient();
        try{
            client.delete().deletingChildrenIfNeeded().forPath(path);
        }catch(Exception e){
            e.printStackTrace();
        }
    }

    public String getNode(String path){
        try{
            CuratorFramework client = zkClient();
            if (client.checkExists().forPath(path) != null) {
                byte[] bytes = client.getData().forPath(path);
                return new String(bytes);
            }
        }catch(Exception e){
            e.printStackTrace();
        }
        return null;
    }

    public void setNode(String path){
        try{
            CuratorFramework client = zkClient();
            if (client.checkExists().forPath(path) != null) {
                client.setData().forPath(path,"123".getBytes());
            }
        }catch(Exception e){
            e.printStackTrace();
        }
    }


    public static List<String> getNodeChildrens(String path) {
        if (SERVICE_ADDRESS_MAP.containsKey(path)) {
            return SERVICE_ADDRESS_MAP.get(path);
        }
        try {
            CuratorFramework client = zkClient();
            List<String> urls = client.getChildren().forPath(path);
            SERVICE_ADDRESS_MAP.put(path, urls);
            registerWatcher(path);
            return urls;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

    static void registerWatcher(String path) {

        try {
            PathChildrenCache pathChildrenCache = new PathChildrenCache(zkClient(), path, true);
            PathChildrenCacheListener pathChildrenCacheListener = (client, cache) -> {
                List<String> urls = client.getChildren().forPath(path);
                SERVICE_ADDRESS_MAP.put(path, urls);
            };

            pathChildrenCache.getListenable().addListener(pathChildrenCacheListener);
            pathChildrenCache.start();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

}

数据传输

数据传输在这里我使用的是Netty。也可以使用Java自带的Socket,不过Socket是阻塞IO,性能低功能也单一。还可以使用NIO,不过直接使用NIO很麻烦,那还不如使用基于NIO开发出来的Netty。

在这里插入图片描述
在网络上传输的中所能够支持的数据类型就是二进制, 对象是没有办法直接传输的,所以我们需要先将对象进行序列化,然后再进行传输。但是不提倡直接使用Java自带的序列化接口,因为没有足够的安全性,我这里使用的是Kryo序列化框架。

服务端启动,等待连接
服务端启动前,会去注册中心(zk)中注册一个服务

ServerBootstrap b = new ServerBootstrap();

        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
                    .childOption(ChannelOption.TCP_NODELAY, true)
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            pipeline.addLast(new IdleStateHandler(30, 0, 0, TimeUnit.SECONDS));
                            pipeline.addLast(new RpcDecoder());
                            pipeline.addLast(new RpcEncoder());
                            pipeline.addLast(new NettyRpcServerHandler());
                        }
                    });

            ChannelFuture channelFuture = b.bind(PORT).sync();//等待端口绑定
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        }

服务端处理客户端传递的数据
客户端数据传输时,就会将对象转为二进制,所以在服务端就需要将二进制数据再次转为对象。创建一个解码类,继承ByteToMessageDecoder类。

protected void decode(ChannelHandlerContext ctx, ByteBuf byteBuf, List<Object> list) throws Exception {
   if (byteBuf.readableBytes() > 4) {
       byteBuf.markReaderIndex();//记录阅读索引
       int r = byteBuf.readInt();
       if (r < 1) {
           log.error("byte length is valid [{}]", r);
           return;
       }
       byte[] b = new byte[r];
       byteBuf.readBytes(b);

       Object d = kryoSerializer.deserialize(b, genericClass);
       list.add(d);
   }
}

这步是在Handler之前就进行了,将数据处理好后,再去进行最终目的:方法的调用。创建一个类,继承ChannelInboundHandlerAdapter类,重写方channelRead方法。

RpcMessage rpcMessage = (RpcMessage) msg;
RpcRequest rpcRequest = (RpcRequest) rpcMessage.getData();
Object data = serviceHandler.handler(rpcRequest);//服务处理
RpcResponse response = RpcResponse.builder().data(data).requestId(rpcRequest.getRequestId()).build();
rpcMessage.setData(response);
ctx.writeAndFlush(rpcMessage).addListener(ChannelFutureListener.CLOSE);//将返回的数据封装后,返回给客户端

客户端启动,连接服务端
1、先去注册中心中拉取服务lookupService
2、获取到SocketInetAddress连接服务端获得通信频道Channel
3、再使用Channel将数据传输到服务端writeAndFlush

 String path = "/lionfish/server/RPC_SERVER";
 InetSocketAddress inetSocketAddress = serviceHandler.lookupService(path);

 Channel channel = getChannel(inetSocketAddress);

 RpcMessage rpcMessage = RpcMessage.builder().requestId(rpcRequest.getRequestId()).data(rpcRequest).build();
 channel.writeAndFlush(rpcMessage).addListener((ChannelFutureListener) future -> {
     if (future.isSuccess()) {
         log.info("client send message:[{}]", rpcRequest.toString());
     } else {
         future.cause().printStackTrace();
     }
 });

 channel.closeFuture().sync();
 AttributeKey<RpcResponse> key = AttributeKey.valueOf("rpcResponse");//获取数据源中的数据
 return channel.attr(key).get();

服务端返回数据后,客户端需要对数据进行解码,然后处理数据后获取数据,整体流程结束。

 RpcMessage message = (RpcMessage) msg;
 RpcResponse response = (RpcResponse) message.getData();
 AttributeKey<Object> key = AttributeKey.valueOf("rpcResponse");
 ctx.channel().attr(key).set(response);//将返回的数据放到一个数据源中
 ctx.channel().close();

负载均衡

负载均衡的算法有很多:随机算法、绝对公平算法、轮询算法、轮询权重算法、随机权重算法等。我这里选择的是随机权重算法,根据自定义的服务器权重,进行轮询,并随机。

List<String> ips = new ArrayList<>();
for (String url : urls) {
    String[] paths = url.split(":");
    int weight = 1;
    if (paths.length >= 3) {
        weight = Integer.parseInt(paths[2]);
    }
    for (int i = 0; i < weight; i++) {
        ips.add(paths[0] + ":" + paths[1]);
    }
}
Collections.shuffle(ips);//先将ips数组的顺序打乱,再获取随机数随机获得对应的ip地址
return ips;

方法调用

RPC框架最重要的就是远程方法的调用,我这里使用的是注解调用。启动项目后,先将当前项目所有包含注解RequestMapping的类全部都找出来,然后和传输过来的数据进行对比。如果类的注解名与方法的注解名一致,则通过Java反射对方法进行invoke。

获取注解类代码

PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();

String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
ClassUtils.convertClassNameToResourcePath("github.rpcserver") + "/**/*Controller.class";

try {
    Resource[] resources = resolver.getResources(pattern);
    CachingMetadataReaderFactory factory = new CachingMetadataReaderFactory(resolver);
    for (Resource resource : resources) {
        MetadataReader metadataReader = factory.getMetadataReader(resource);
        ClassMetadata classMetadata = metadataReader.getClassMetadata();
        String className = classMetadata.getClassName();
        Class<?> aClass = Class.forName(className);
        RequestMapping annotation = aClass.getAnnotation(RequestMapping.class);
        if (annotation != null) {
            classes.add(aClass);
        }
    }
} catch (Exception e) {
    e.printStackTrace();
}

反射代码

for (Class<?> v : classes) {
    RequestMapping annotation = v.getAnnotation(RequestMapping.class);
    String value = annotation.value()[0];
    if (interfaceName.equals(value)) {
        Method[] methods = v.getMethods();
        for (Method method : methods) {
            RequestMapping anno = method.getAnnotation(RequestMapping.class);
            String s = anno.value()[0];
            if (s.equals(o.getMethod())) {
                return method.invoke(v.newInstance(), o.getParameters());
            }
        }
    }
}

以上关于简单的RPC框架就完成了,代码还是比较乱,后面也会尝试进行优化。

有兴趣的可以去>>github<<上看看

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值