簡易RPC框架實現

Hodu發表於2018-01-15

寫在最前面

PRC(Remote Procedure Call) 遠端過程呼叫。通俗的講就是程式通過RPC框架呼叫遠端主機的方法就如同呼叫本地方法一樣。Dubbo就是這樣一個Rpc框架,本文主要參考Dubbo的設計思路,簡易實現了Rpc框架。 本文涉及到知識點包括:

  • Jdk 動態代理
  • serialization 序列化
  • Netty 相關
  • Zookeeper 使用

1、Rpc框架

Rpc 框架一般分為三個部分,Registry(註冊中心)、Provider(提供者)、Consumer(消費者)。

簡易RPC框架實現

  1. Registry 服務的註冊中心,可以通過zookeeper、redis等實現。
  2. Provider 服務提供者被呼叫方,提供服務供消費者呼叫
  3. Consumer 消費者,通過訂閱相應的服務獲取需要呼叫服務的ip和埠號呼叫遠端provider提供的服務。

2、代理

java中常見的代理有JDK動態代理、Cglib動態代理、靜態代理(ASM等位元組碼技術)。

2.1、JDK 代理

舉個例子

@Override
   @SuppressWarnings("unchecked")
   public <T> T createProxyBean(Class<T> rpcServiceInterface) {
       return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class<?>[]{rpcServiceInterface}, new AryaRpcInvocationHandler());
   }
複製程式碼

JDK代理生成代理物件主要通過java.lang.reflect.Proxy類的newProxyInstance方法。JDK代理需要被代理物件必須實現介面。

2.2、Cglib

Cglib實際上是對ASM的易用性封裝,Cglib不需要目標物件必須實現某一個介面,相對JDK動態代理更加靈活。

       Enhancer en = new Enhancer();  
       en.setSuperclass(clazz);  
       en.setCallback(new MethodInterceptor() {  
           @Override  
           public Object intercept(Object arg0, Method method, Object[] args, MethodProxy arg3) throws Throwable {  
               Object o = method.invoke(object, args);  
               return o;  
           }  
       });  
       return en.create();
複製程式碼

2.3、靜態代理

通過位元組碼技術對class檔案進行修改,使用和學習成本相對較高,需要對Class的檔案結構以及各種符號引用有比較深的認識,才能較好的使用,因為是對位元組碼的修改所以相對的效能上也比動態代理要好一些。

3、序列化

我們知道資料在網路上傳輸都是通過二進位制流的形式進行進行的。當Consumer呼叫Provider時傳輸的引數需要先進行序列化,provider接收到引數時需要進行反序列化才能拿到需要的引數資料,所以序列化的效能對RPC的呼叫效能有很大的影響。目前主流的序列化方式有很多包括:Kryo、Protostuff、hessian。等

Protostuff是google序列化Protosbuff的開源實現,專案中我們用到它的序列化方式

/**
* @author HODO
*/
public class ProtostuffSerializer implements Serializer {

   @Override
   public byte[] serialize(Object object) {
       Class targetClass = object.getClass();
       RuntimeSchema schema = RuntimeSchema.createFrom(targetClass);
       LinkedBuffer linkedBuffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
       return ProtostuffIOUtil.toByteArray(object, schema, linkedBuffer);
   }

   @SuppressWarnings("unchecked")
   @Override
   public <T> T deserialize(byte[] bytes, Class<T> targetClass) {
       RuntimeSchema schema = RuntimeSchema.createFrom(targetClass);
       T object = (T) schema.newMessage();
       ProtostuffIOUtil.mergeFrom(bytes, object, schema);
       return object;
   }
}

複製程式碼

4、Netty

Netty是一個高效能、非同步事件驅動的NIO框架,它提供了對TCP、UDP和檔案傳輸的支援。舉個例子: Netty 服務端程式碼

public class NettyServer {

   private ApplicationContext applicationContext;

   public NettyServer(ApplicationContext applicationContext) {
       this.applicationContext = applicationContext;
   }

   public void init(int port) {
       EventLoopGroup boss = new NioEventLoopGroup();
       EventLoopGroup worker = new NioEventLoopGroup();

       try {
           ServerBootstrap bootstrap = new ServerBootstrap();
           bootstrap.group(boss, worker);
           bootstrap.channel(NioServerSocketChannel.class);
           bootstrap.option(ChannelOption.SO_BACKLOG, 1024);
           bootstrap.option(ChannelOption.TCP_NODELAY, true);
           bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
           bootstrap.localAddress(port);
           bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
               @Override
               public void initChannel(SocketChannel socketChannel) throws Exception {
                   ChannelPipeline channelPipeline = socketChannel.pipeline();
                   channelPipeline.addLast(new NettyServerHandler(applicationContext));
               }
           });
           ChannelFuture f = bootstrap.bind().sync();
           if (f.isSuccess()) {
               System.out.println("Netty埠號:" + port);
           }
           f.channel().closeFuture().sync();
       } catch (Exception e) {
           e.printStackTrace();
       } finally {
           boss.shutdownGracefully();
           worker.shutdownGracefully();
       }
   }

}
複製程式碼

Netty 客服端程式碼

public class NettyClient {

    private int port;
    private String host;

    private final CountDownLatch countDownLatch = new CountDownLatch(1);

    SerializerFactory serializerFactory = new SerializerFactory();
    Serializer serializer = serializerFactory.getSerialize(ProtostuffSerializer.class);


    public NettyClient(String host, int port) {
        this.port = port;
        this.host = host;
    }

    public NettyClient(String inetAddress) {
        if (inetAddress != null && inetAddress.length() != 0) {
            String[] strings = inetAddress.split(":");
            this.host = strings[0];
            this.port = Integer.valueOf(strings[1]);
        }
    }

    public RpcResponse invoker(RpcRequest rpcRequest) throws InterruptedException {

        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            final NettyClientHandler clientHandler = new NettyClientHandler();
            bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class).option(ChannelOption.SO_KEEPALIVE, true).handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel socketChannel) throws Exception {
                    socketChannel.pipeline().addLast(clientHandler);
                }});
            ChannelFuture future = bootstrap.connect(new InetSocketAddress(host, port)).sync();

            serializer.serialize(rpcRequest);
            future.channel().writeAndFlush(Unpooled.buffer().writeBytes(serializer.serialize(rpcRequest)));
            countDownLatch.await();
            // 等待連結關閉
            //future.channel().closeFuture().sync();
            return clientHandler.getRpcResponse();
        } finally {
            eventLoopGroup.shutdownGracefully();
        }
    }

    public class NettyClientHandler extends ChannelInboundHandlerAdapter {


        private RpcResponse rpcResponse;

        /**
         * 接收 Rpc 呼叫結果
         *
         * @param ctx netty 容器
         * @param msg 服務端答覆訊息
         * @throws Exception
         */
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            ByteBuf buf = (ByteBuf) msg;
            byte[] req = new byte[buf.readableBytes()];
            buf.readBytes(req);
            rpcResponse = serializer.deserialize(req, RpcResponse.class);
            countDownLatch.countDown();
        }

        RpcResponse getRpcResponse() {
            return rpcResponse;
        }
    }
}

複製程式碼

5、註冊中心 zookeeper

選用了zookeeper作為註冊中心,在建議Rpc框架中提供了註冊中心的擴充套件。只要實現RegistryManager介面即可。zookeeper常用的命令列:

1、客服端指令碼連線zookeeper伺服器不指定-server預設連線本地服務

./zkCli -service ip:port

2、建立

create [-s] [-e] path data acl

建立一個節點-s -e分別指定節點的型別和特性:順序和臨時節點預設建立的是臨時節點,acl用於許可權控制

3、讀取

ls path只能看指定節點下的一級節點

get path檢視指定節點的資料和屬性資訊

4、更新

set path data [version]

可以指定更新操作是基於哪一個版本當更新的 path 不存在時報 Node does not exist

5、刪除

`delete path [version]``

6、Spring 支援

在框架中還提供了兩個註解@RpcConsumerRpcProvider 在專案中只要引入

    <dependency>
			<groupId>com.yoku.arya</groupId>
			<artifactId>arya</artifactId>
			<version>1.0-SNAPSHOT</version>
		</dependency>
複製程式碼

在provider端容器注入

  @Bean
	public RpcProviderProcessor rpcProviderProcessor() {
		return new RpcProviderProcessor();
	}
複製程式碼

在comsumer端容器注入

  @Bean
	public RpcConsumerProcessor rpcConsumerProcessor() {
		return new RpcConsumerProcessor();
	}
複製程式碼

專案完整的程式碼 arya github.com/hoodoly/ary…

框架使用Demo github.com/hoodoly/ary…

歡迎 star

聯絡方式:gunriky@163.com 有問題可以直接聯絡

相關文章