基於 Netty 重構 RPC 框架

01 RPC 概述

下面的這張圖,大概很多小夥伴都見到過,這是 Dubbo 官網中的一張圖描述了項目架構的演進過程。
阿里官方

它描述了每一種架構需要的具體配置和組織形態。當網站流量很小時,只需一個應用,將所有功能都部署在一起, 以減少部署節點和成本,我們通常會採用單一應用架構。之後出現了 ORM 框架,主要用於簡化增刪改查工作流的,數 據訪問框架 ORM 是關鍵。

隨着用戶量增加,當訪問量逐漸增大,單一應用增加機器,帶來的加速度越來越小 ,我們需要將應用拆分成互不 干擾的幾個應用,以提升效率,於是就出現了垂直應用架構。MVC 架構就是一種非常經典的用於加速前端頁面開發的 架構。

當垂直應用越來越多,應用之間交互不可避免,將核心業務抽取出來,作爲獨立的服逐漸形成穩定的服務中心, 使前端應用能更快速的響應,多變的市場需求,就出現了分佈式服務架構。分佈式架構下服務數量逐漸增加,爲了提 高管理效率,RPC 框架應運而生。RPC 用於提高業務複用及整合的,分佈式服務框架下 RPC 是關鍵。

下一代框架,將會是流動計算架構佔據主流。當服務越來越多,容量的評估,小服務的資源浪費等問題,逐漸明 顯。此時,需要增加一個調度中心 ,基於訪問壓力實時管理集羣容量,提高集羣利用率。SOA 架構就是用於提高及其 利用率的,資源調度和治理中心 SOA 是關鍵。

Netty 基本上是作爲架構的技術底層而存在的,主要完成高性能的網絡通信。

02 環境預設

第一步:我們先將項目環境搭建起來,創建 pom.xml 配置文件如下:

 <dependency>
      <groupId>io.netty</groupId>
      <artifactId>netty-all</artifactId>
      <version>4.1.6.Final</version>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.16.10</version>
    </dependency>

第二步:創建項目結構。

在沒有 RPC 框架以前,我們的服務調用是這樣的,如下圖:
非RPC
從上圖可以看出接口的調用完全沒有規律可循,想怎麼調,就怎麼調。這導致業務發展到一定階段之後,對接口 的維護變得非常困難。於是有人提出了服務治理的概念。所有服務間不允許直接調用,而是先到註冊中心進行登記, 再由註冊中心統一協調和管理所有服務的狀態並對外發布,調用者只需要記住服務名稱,去找註冊中心獲取服務即可。這樣,極大地規範了服務的管理,可以提高了所有服務端可控性。整個設計思想其實在我們生活中也能找到活生生的 案例。例如:我們平時工作交流,大多都是用 IM 工具,而不是面對面吼。大家只需要相互記住運營商(也就是註冊中 心)提供的號碼(如:騰訊 QQ)即可。再比如:我們打電話,所有電話號碼有運營商分配。我們需要和某一個人通 話時,只需要撥通對方的號碼,運營商(註冊中心,如中國移動、中國聯通、中國電信)就會幫我們將信號轉接過去。

目前流行的 RPC 服務治理框架主要有 Dubbo 和 Spring Cloud,下面我以比較經典的 Dubbo 爲例。Dubbo 核 心模塊主要有四個:Registry 註冊中心、Provider 服務端、Consumer 消費端、Monitor 監控中心,如下所示:

註冊中心 (Registry)
消費端 (Consumer)
服務端 (Provider)
監控中心 (Monitor)

爲了方便,我們將所有模塊全部放到一個項目中,主要模塊包括:

api:主要用來定義對外開放的功能與服務接口。
protocol:主要定義自定義傳輸協議的內容。
registry:主要負責保存所有可用的服務名稱和服務地址。
provider:實現對外提供的所有服務的具體功能。
consumer:客戶端調用。
monitor:完成調用鏈監控。

下面,我們先把項目結構搭建好,具體的項目結構截圖如下:
項目結構

03 代碼實戰

3.1 創建 API 模塊

首先創建 API 模塊,provider 和 consumer 都遵循 API 模塊的規範。爲了簡化,創建兩個 Service 接口,分別是:

package com.xinfan.netty.rpc.api;

/**
 * IRpcHelloService
 *
 * @author Lss
 * @date 2020/2/18 22:50
 * @Version 1.0
 */
public interface IRpcHelloService {
    String hello(String name);
}

創建 IRpcService 接口,完成模擬業務加、減、乘、除運算,具體代碼如下:

package com.xinfan.netty.rpc.api;

/**
 * IRpcService
 *
 * @author Lss
 * @date 2020/2/18 22:51
 * @Version 1.0
 */
public interface IRpcService {
    /** 加 */
    public int add(int a,int b);
    /** 減 */
    public int sub(int a,int b);
    /** 乘 */
    public int mult(int a,int b);
    /** 除 */
    public int div(int a,int b);
}

至此,API 模塊就定義完成了,非常簡單。接下來,我們要確定傳輸規則,也就是傳輸協議,協議內容當然要自定義, 才能體現出 Netty 的優勢。

3.2 創建自定義協議

Netty 中內置的 HTTP 協議,需要 HTTP 的編、解碼器來完成解析。我們來 看自定義協議如何設定?

在 Netty 中要完成一個自定義協議,其實非常簡單,只需要定義一個普通的 Java 類即可。我們現在手寫 RPC 主要 是完成對 Java 代碼的遠程調用(類似於 RMI,大家應該都很熟悉了),遠程調用 Java 代碼哪些內容是必須由網絡來 傳輸的呢?譬如,服務名稱?需要調用該服務的哪個方法?方法的實參是什麼?這些信息都需要通過客戶端傳送到服 務端去。

下面我們來看具體的代碼實現,定義 InvokerProtocol 類:

import lombok.Data;

import java.io.Serializable;
/**
 * InvokerProtocol
 *
 * @author Lss
 * @date 2020/2/18 22:54
 * @Version 1.0
 */
@Data
public class InvokerProtocol implements Serializable {

    //類名
    private String className;
    //函數名稱(方法名)
    private String methodName;
    //參數類型
    private Class<?>[] parames;
    //參數列表
    private Object[] values;
}

從上面的代碼看出來,協議中主要包含的信息有類名、函數名、形參列表和實參列表,通過這些信息就可以定位到一 個具體的業務邏輯實現。

3.3 實現 Provider 服務端業務邏輯

我們將 API 中定義的所有功能在 provider 模塊中實現,分別創建兩個實現類:

RpcHelloServiceImpl 類:

package com.xinfan.netty.rpc.provider;

import com.xinfan.netty.rpc.api.IRpcHelloService;

/**
 * RpcHelloServiceImpl
 *
 * @author Lss
 * @date 2020/2/18 23:00
 * @Version 1.0
 */
public class RpcHelloServiceImpl implements IRpcHelloService{

    @Override
    public String hello(String name) {
        return "Hello " + name + "!";
    }
}

RpcServiceImpl 類:

package com.xinfan.netty.rpc.provider;

import com.xinfan.netty.rpc.api.IRpcService;

/**
 * RpcServiceImpl
 *
 * @author Lss
 * @date 2020/2/18 23:00
 * @Version 1.0
 */
public class RpcServiceImpl implements IRpcService {
    @Override
    public int add(int a, int b) {
        return a + b;
    }

    @Override
    public int sub(int a, int b) {
        return a - b;
    }

    @Override
    public int mult(int a, int b) {
        return a * b;
    }

    @Override
    public int div(int a, int b) {
        return a / b;
    }
}

3.4 完成 Registry 服務註冊

Registry 註冊中心主要功能就是負責將所有 Provider 的服務名稱和服務引用地址註冊到一個容器中,並對外發布。 Registry 應該要啓動一個對外的服務,很顯然應該作爲服務端,並提供一個對外可以訪問的端口。先啓動一個 Netty 服務,創建 RpcRegistry 類,具體代碼如下:

package com.xinfan.netty.rpc.registry;

import io.netty.bootstrap.ServerBootstrap;

import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;




/**
 * RpcRegistry
 *
 * @author Lss
 * @date 2020/2/18 23:27
 * @Version 1.0
 */
public class RpcRegistry {

    private int port;
    public RpcRegistry(int port){
        this.port = port;
    }
    public void start(){
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {

                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            //自定義協議解碼器
                            /** 入參有5個,分別解釋如下
                             maxFrameLength:框架的最大長度。如果幀的長度大於此值,則將拋出TooLongFrameException。
                             lengthFieldOffset:長度字段的偏移量:即對應的長度字段在整個消息數據中得位置
                             lengthFieldLength:長度字段的長度。如:長度字段是int型表示,那麼這個值就是4(long型就是8)
                             lengthAdjustment:要添加到長度字段值的補償值
                             initialBytesToStrip:從解碼幀中去除的第一個字節數
                             */
                            pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4));
                            //自定義協議編碼器
                            pipeline.addLast(new LengthFieldPrepender(4));
                            //對象參數類型編碼器
                            pipeline.addLast("encoder",new ObjectEncoder());
                            //對象參數類型解碼器
                            pipeline.addLast("decoder",new ObjectDecoder(Integer.MAX_VALUE,ClassResolvers.cacheDisabled(null)));                            
                            pipeline.addLast(new RegistryHandler());
                        }
                    })
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true);
            ChannelFuture future = b.bind(port).sync();
            System.out.println("GP RPC Registry start listen at " + port );
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }


    public static void main(String[] args) throws Exception {
        new RpcRegistry(8080).start();
    }
}

在 RegistryHandler 中實現註冊的具體邏輯,上面的代碼,主要實現服務註冊和服務調用的功能。因爲所有模塊創 建在同一個項目中,爲了簡化,服務端沒有采用遠程調用,而是直接掃描本地 Class,然後利用反射調用。代碼實現如 下:

package com.xinfan.netty.rpc.registry;

import com.xinfan.netty.rpc.protocol.InvokerProtocol;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;


import java.io.File;
import java.io.FileInputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

/**
 * RegistryHandler
 *
 * @author Lss
 * @date 2020/2/18 23:58
 * @Version 1.0
 */
public class RegistryHandler extends ChannelInboundHandlerAdapter {

    //用保存所有可用的服務
    public static ConcurrentHashMap<String, Object> registryMap = new ConcurrentHashMap<String,Object>();

    //保存所有相關的服務類
    private List<String> classNames = new ArrayList<String>();

    public RegistryHandler(){
        //完成遞歸掃描
        scannerClass("com.xinfan.netty.rpc.provider");
        doRegister();
    }


    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        Object result = new Object();
        InvokerProtocol request = (InvokerProtocol)msg;

        //當客戶端建立連接時,需要從自定義協議中獲取信息,拿到具體的服務和實參
        //使用反射調用
        if(registryMap.containsKey(request.getClassName())){
            Object clazz = registryMap.get(request.getClassName());
            Method method = clazz.getClass().getMethod(request.getMethodName(), request.getParames());
            result = method.invoke(clazz, request.getValues());
        }
        ctx.write(result);
        ctx.flush();
        ctx.close();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }


    /*
     * 遞歸掃描
     */
    private void scannerClass(String packageName){
        URL url = this.getClass().getClassLoader().getResource(packageName.replaceAll("\\.", "/"));
        File dir = new File(url.getFile());
        for (File file : dir.listFiles()) {
            //如果是一個文件夾,繼續遞歸
            if(file.isDirectory()){
                scannerClass(packageName + "." + file.getName());
            }else{
                classNames.add(packageName + "." + file.getName().replace(".class", "").trim());
            }
        }
    }

    /**
     * 完成註冊
     */
    private void doRegister(){
        if(classNames.size() == 0){ return; }
        for (String className : classNames) {
            try {
                Class<?> clazz = Class.forName(className);
                Class<?> i = clazz.getInterfaces()[0];
                registryMap.put(i.getName(), clazz.newInstance());
            } catch (Exception e) {
                e.printStackTrace();
            }
            }
        }
}

至此,註冊中心的基本功能就已完成,下面來看客戶端的代碼實現。

3.5 實現 Consumer 遠程調用

梳理一下基本的實現思路,主要完成一個這樣的功能:API 模塊中的接口功能在服務端實現(並沒有在客戶端實現)。 因此,客戶端調用 API 中定義的某一個接口方法時,實際上是要發起一次網絡請求去調用服務端的某一個服務。而這 個網絡請求首先被註冊中心接收,由註冊中心先確定需要調用的服務的位置,再將請求轉發至真實的服務實現,最終 調用服務端代碼,將返回值通過網絡傳輸給客戶端。整個過程對於客戶端而言是完全無感知的,就像調用本地方法一 樣。具體調用過程如下圖所示:
實現邏輯
下面來看代碼實現,創建 RpcProxy 類:

package com.xinfan.netty.rpc.consumer.proxy;

import com.xinfan.netty.rpc.protocol.InvokerProtocol;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * RpcProxy
 *
 * @author Lss
 * @date 2020/2/19 12:10
 * @Version 1.0
 */
public class RpcProxy  {

    public static <T> T create(Class<?> clazz){
        //clazz傳進來本身就是interface
        MethodProxy proxy = new MethodProxy(clazz);
        Class<?> [] interfaces = clazz.isInterface() ?
                new Class[]{clazz} :
                clazz.getInterfaces();
        T result = (T) Proxy.newProxyInstance(clazz.getClassLoader(),interfaces,proxy);
        return result;
    }

    private static class MethodProxy implements InvocationHandler {
        private Class<?> clazz;
        public MethodProxy(Class<?> clazz){
            this.clazz = clazz;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args)  throws Throwable {
            //如果傳進來是一個已實現的具體類(本次演示略過此邏輯)
            if (Object.class.equals(method.getDeclaringClass())) {
                try {
                    return method.invoke(this, args);
                } catch (Throwable t) {
                    t.printStackTrace();
                }
                //如果傳進來的是一個接口(核心)
            } else {
                return rpcInvoke(proxy,method, args);
            }
            return null;
        }


        /**
         * 實現接口的核心方法
         * @param method
         * @param args
         * @return
         */
        public Object rpcInvoke(Object proxy,Method method,Object[] args){

            //傳輸協議封裝
            InvokerProtocol msg = new InvokerProtocol();
            msg.setClassName(this.clazz.getName());
            msg.setMethodName(method.getName());
            msg.setValues(args);
            msg.setParames(method.getParameterTypes());

            final RpcProxyHandler consumerHandler = new RpcProxyHandler();
            EventLoopGroup group = new NioEventLoopGroup();
            try {
                Bootstrap b = new Bootstrap();
                b.group(group)
                        .channel(NioSocketChannel.class)
                        .option(ChannelOption.TCP_NODELAY, true)
                        .handler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            public void initChannel(SocketChannel ch) throws Exception {
                                ChannelPipeline pipeline = ch.pipeline();
                                //自定義協議解碼器(服務端的代碼可以複製過來)
                                /** 入參有5個,分別解釋如下
                                 maxFrameLength:框架的最大長度。如果幀的長度大於此值,則將拋出TooLongFrameException。
                                 lengthFieldOffset:長度字段的偏移量:即對應的長度字段在整個消息數據中得位置
                                 lengthFieldLength:長度字段的長度:如:長度字段是int型表示,那麼這個值就是4(long型就是8)
                                 lengthAdjustment:要添加到長度字段值的補償值
                                 initialBytesToStrip:從解碼幀中去除的第一個字節數
                                 */
                                pipeline.addLast("frameDecoder", new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4));
                                //自定義協議編碼器
                                pipeline.addLast("frameEncoder", new LengthFieldPrepender(4));
                                //對象參數類型編碼器
                                pipeline.addLast("encoder", new ObjectEncoder());
                                //對象參數類型解碼器
                                pipeline.addLast("decoder", new ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.cacheDisabled(null)));
                                pipeline.addLast("handler",consumerHandler);
                            }
                        });

                ChannelFuture future = b.connect("localhost", 8080).sync();
                future.channel().writeAndFlush(msg).sync();
                future.channel().closeFuture().sync();
            } catch(Exception e){
                e.printStackTrace();
            }finally {
                group.shutdownGracefully();
            }
            return consumerHandler.getResponse();
        }

    }
}

接收網絡調用的返回值 RpcProxyHandler 類:

package com.xinfan.netty.rpc.consumer.proxy;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

/**
 * RpcProxyHandler
 *
 * @author Lss
 * @date 2020/2/19 12:36
 * @Version 1.0
 */
public class RpcProxyHandler extends ChannelInboundHandlerAdapter{

    private Object response;

    public Object getResponse() {
        return response;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        response=msg;
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("client exception is general");
    }
}

完成客戶端調用代碼 RpcConsumer類:

package com.xinfan.netty.rpc.consumer;

import com.xinfan.netty.rpc.api.IRpcHelloService;
import com.xinfan.netty.rpc.api.IRpcService;
import com.xinfan.netty.rpc.consumer.proxy.RpcProxy;

/**
 * RpcConsumer
 *
 * @author Lss
 * @date 2020/2/19 12:55
 * @Version 1.0
 */
public class RpcConsumer {

    public static void main(String[] args) {
        IRpcHelloService rpcHello= RpcProxy.create(IRpcHelloService.class);
        System.out.println(rpcHello.hello("四川情場浪子"));
        IRpcService service=RpcProxy.create(IRpcService.class);
        System.out.println("8 + 2 = " + service.add(8, 2));
        System.out.println("8 - 2 = " + service.sub(8, 2));
        System.out.println("8 * 2 = " + service.mult(8, 2));
        System.out.println("8 / 2 = " + service.div(8, 2));
        //Dubbo 中的 Monitor 是用 Spring 的 AOP 埋點來實現的,我沒有引入 Spring 框架
    }
}

3.6 Monitor 監控

Dubbo 中的 Monitor 是用 Spring 的 AOP 埋點來實現的,我沒有引入 Spring 框架,在本代碼中不實現監控的功 能。感興趣的小夥伴,可以回顧之前 Spring AOP 的課程自行完善此功能。

04 運行效果演示

第一步,啓動註冊中心,運行結果如下:
啓動註冊中心

第二步,運行客戶端,調用結果如下:
運行客戶端

通過以上案例演示,相信小夥伴們對 Netty 的應用已經有了一個比較深刻的印象,本次只是對 RPC 的基本實現原理做了一個簡單的實現,感興趣的小夥伴可以在本項 目的基礎上繼續完善 RPC 的其他細節。歡迎留言

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章