V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
liubsyy
V2EX  ›  Java

基于 netty+zk 开发高性能 rpc 框架

  •  
  •   liubsyy · 328 天前 · 1889 次点击
    这是一个创建于 328 天前的主题,其中的信息可能已经有所发展或是发生改变。

    笔者在开发基于客户端/服务端模式通信的插件的时候,需要用到轻量级最小包依赖的 RPC 框架,而市面上的 RPC 框架份量过于庞大,最终打包下来都是几十兆甚至上百兆,而这里面大多数功能我都用不上,于是思来想去我决定写一款属于自己的轻量级 RPC 框架 ShadowRPC ,简单易用快速接入。

    技术栈

    协议序列化/反序列化

    网络通信基于 TCP/IP 为基础自定义应用层协议,常见的序列化/反序列化工具有 java 原生序列化、json 、kryo 、protobuf 、fst 和 hessian 等。

    在不考虑跨语言的情况下,从序列化时长/序列化大小/易用性/扩展性这几方面考虑,综合性比较强的是 kryo ,但不支持跨语言,protobuf 性能最强且支持跨语言,但是使用时需要事先基于 proto 生成一个类。

    最终选择 kryo 和 protobuf 两种序列化工具,使用的时候可选序列化类型,前者序列化几乎不受限制,后者支持跨语言,但是必须事先生成 proto 类型的类并使用其作为序列化工具。

    通信框架使用

    高性能异步非阻塞框架非 Netty 不可了,客户端和服务端基于 Netty 开发可事半功倍。

    除了基于 netty 外,有时需要更小的包依赖,所以 client 除了支持基于 netty 模块,还会开发一个无任何依赖的模块 mini-client ,打完包仅几十 kb 。

    服务注册和发现

    注册中心选择 zookeeper 作为服务注册和服务发现,当然如果只用单点模式的话其实是不需要注册中心的,所以 zookeeper 是可选组件。

    快速使用

    1.定义实体作为序列化的对象(可选)

    @ShadowEntity
    public class MyMessage {
        @ShadowField(1)
        private String content;
    
        @ShadowField(2)
        private int num;
    }
    
    

    如果是 protobuf 序列化方式,定义 proto 格式再用 maven 插件 protobuf-maven-plugin 生成实体

    message MyMessage {
        string content = 1;
        int32 num = 2;
    }
    

    2.编写接口和服务类

    @ShadowInterface
    public interface IHello {
        String hello(String msg);
        MyMessage say(MyMessage message);
    }
    

    然后编写服务实现类

    @ShadowService(serviceName = "helloservice")
    public class HelloService implements IHello {
        @Override
        public String hello(String msg) {
            return "Hello,"+msg;
        }
        @Override
        public MyMessage say(MyMessage message) {
            MyMessage message1 = new MyMessage();
            message1.setContent("hello received "+"("+message.getContent()+")");
            message1.setNum(message.getNum()+1);
            return message1;
        }
    }
    

    3.最后指定序列化类型和端口,启动服务端

    单点启动模式如下:

    ServerBuilder.newBuilder()
            .serverConfig(serverConfig)
            .addPackage("rpctest.hello")
            .build()
            .start(); 
    
    

    使用 zk 作为注册中心集群模式启动

    String ZK_URL = "localhost:2181";
    ServerConfig serverConfig = new ServerConfig();
    serverConfig.setGroup("DefaultGroup");
    serverConfig.setPort(2023);
    serverConfig.setRegistryUrl(ZK_URL);
    serverConfig.setQpsStat(true); //统计 qps
    serverConfig.setSerializer(SerializerEnum.KRYO.name());
    ServerBuilder.newBuilder()
                    .serverConfig(serverConfig)
                    .addPackage("rpctest.hello")
                    .build()
                    .start();
    
    

    4.客户端调用 rpc 服务

    ModulePool.getModule(ClientModule.class).init(new ClientConfig());
    ShadowClient shadowClient = new ShadowClient("127.0.0.1",2023);
    shadowClient.init();
    
    IHello helloService = shadowClient.createRemoteProxy(IHello.class,"shadowrpc://DefaultGroup/helloservice";
    
    MyMessage message = new MyMessage();
    message.setNum(100);
    message.setContent("Hello, Server!");
    
    System.out.printf("发送请求 : %s\n",message);
    MyMessage response = helloService.say(message);
    System.out.printf("接收服务端消息 : %s\n",response);    
    

    使用 zk 作为服务发现负载均衡调用各个服务器

    ClientConfig config = new ClientConfig();
    config.setSerializer(SerializerStrategy.KRYO.name());
    ModulePool.getModule(ClientModule.class).init(config);
    String ZK_URL="localhost:2181";
    ShadowClientGroup shadowClientGroup = new ShadowClientGroup(ZK_URL);
    shadowClientGroup.init();
    
    IHello helloService = shadowClientGroup.createRemoteProxy(IHello.class, "shadowrpc://DefaultGroup/helloservice");
    List<ShadowClient> shadowClientList = shadowClientGroup.getShadowClients("DefaultGroup");
    
    System.out.println("所有服务器: "+shadowClientList.stream().map(c-> c.getRemoteIp()+":"+c.getRemotePort()).collect(Collectors.toList()));
    
    for(int i = 0 ;i<shadowClientList.size() * 5; i++) {
        String hello = helloService.hello(i + "");
        System.out.println(hello);
    }
    
    

    源码

    篇幅有限,所有源码见: https://github.com/Liubsyy/ShadowRPC

    目前仅供学习交流使用,后续我将逐步打磨此 rpc 框架达到企业级水准。

    7 条回复    2024-01-29 16:20:00 +08:00
    toby1902
        1
    toby1902  
       328 天前
    大佬,我之前也写了一个自己用的 RPC ,不过是基于 RabbitMQ 消息队列,使用 Spring-Boot 开发的,我看了一些你的代码,欢迎交流哦,https://github.com/naivetoby/simple-rpc
    runningman
        2
    runningman  
       328 天前
    能不能把 zookeeper 换成 etcd 或者 nacos
    Cambra1n
        3
    Cambra1n  
       328 天前
    想问下用 zk 作为服务发现有什么考量吗?这个使用场景下,Eureka 在可用性上应该更有优势吧。
    liubsyy
        4
    liubsyy  
    OP
       328 天前
    @runningman @Cambra1n 当然可以,后续把常用的组件都接上,通过配置可选
    vok2aDe12AsWDirE
        5
    vok2aDe12AsWDirE  
       328 天前
    协议的设计一般还要有魔数,我看使用的是:LengthFieldBasedFrameDecoder & MessageHandler 可以参考: https://github.com/yint-tech/sekiro-open 支持跨语言的设计建议还是使用 WebSocket 作为底层通讯,在此之上增加自己的语义。
    liubsyy
        6
    liubsyy  
    OP
       327 天前
    @bytebuff 阁下也玩只狼吗,几周目了?
    runningman
        7
    runningman  
       327 天前
    @liubsyy 那就挺好。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2709 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 07:31 · PVG 15:31 · LAX 23:31 · JFK 02:31
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.