由淺入深讓你搞透RPC,不要讓框架遮住你的眼

由淺入深讓你搞透RPC,不要讓框架遮住你的眼

HTTP常常接觸,你們也不陌生,這是一個超文本傳輸協議,可以在網絡直接傳輸數據。目前微服務項目很火,微服務之間基本都是使用HTTP傳輸,例如Feign,OkHttp,RestTemlpate等等。可是分佈式這個話題目前還不過期,今天在這裏說下分佈式的基石:RPC(Remote Procedure Call:遠程過程調用)技術,在Java中稱爲RMI( Remote Method Invocation ,遠程方法調用)。白話解釋下:該技術就是讓使用者在調用一個服務時的時候無感知地調用一個遠程服務。這種無感知讓大部分開發者不須要了解技術自己就能夠很容易的使用 ,可是框架封裝了底層(各類抽象、分層),使得沒法知道內部是如何運行, 下面來介紹下什麼是RPCjava

寫文章不能千篇一概,專業名字一大堆,各類技術天上飛。咱們一步步來,由淺入深。編程

存在這麼一個方法網絡

public class UserService {

    public String findNameById(Long id) {
        return "my name " + id;
    }
}

正常調用負載均衡

public class Test {

    public static void main(String[] args) {
        UserService userService = new UserService();
        String name = userService.findNameById(1L);
        System.out.println(name);
    }

}

控制檯框架

my name 1

Process finished with exit code 0

這裏不涉及框架,只是基本功能。到這裏方法也調用了,功能也實現了。socket

方法執行核心

可是,某一天領導告訴你,你寫的UserService沒有用,具體的處理流程是另外一我的管的,並且另外一我的不和你一個項目,他(提供者)寫好了一個實現tcp

public class OtherUserService {

    public String findNameById(Long id) {
        return "other name " + id ;
    }
}

此時你一想,他在大西洋,我怎麼去調用他寫的方法?因而RPC即可以出手了,RPC能幹嗎?RPC能順着網線去找到他。且看RPC如何作分佈式

先說方法調用: 正常狀況下是經過實例對象+點操做符+方法名稱調用ide

userService.findNameById(1L);

可是一個遠程服務,是拿不到實例對象的,也就不能去寫一個點操做符去調用,這時候方法的調用還能夠經過反射微服務

public Object invoke(Object obj, Object... args){...}
// 第一個參數爲 實例對象,第二個對象爲 參數列表

可是Method這個也只能經過反射來獲取,

public Method getMethod(String name, Class<?>... parameterTypes)
// 第一個參數爲 方法名稱,第二個參數爲參數類型列表

這樣以來,方法的調用即可以分解爲

  • 獲取類的Method實例(須要類標識、方法名稱(標識)、方法參數類型列表)
  • 經過Method實例調用方法(須要類實例對象、參數列表)

其中經過實例是能夠獲取其類型的,instance->class,這裏的參數列表即可以獲取對應參數類型列表,類實例對象能夠獲取類Class,可是這是在同一個JVM中才能完成的,一旦分離開來,這些都不能獲取,並且分離時類實例對象只有提供者具備,使用者是不知道的(若是知道就不必去遠程調用了)。

因此:遠程執行一個方法最少須要4個條件:類標識、方法標識、方法參數類型列表、方法參數列表。拿到這些條件,提供方就能夠找到實例對象、調用對應的方法了,並且爲了避免隨意構造實例對象,提供者會主動控制實例對象的構建,以後須要放到一個地方以方便交給Method的來調用

因此基本功能示意圖以下,描述爲:順着網線,帶着四大金剛,直接找到提供方,圍毆一頓後,把結果帶回來給使用者

RPC基本示意圖

RPC就是對以上功能的完善與封裝,好吧,上面描述太粗魯了,代碼寫的要優雅!!!,要符合設計原則

接口分離原則

使用方和提供方是兩個不一樣的模塊,模塊與模塊之間應該是經過接口解耦的。因此通常在使用RPC服務時都會把接口層給解耦出來,做爲第三方jar給使用方和提供方使用,也便於鎖定實例對象。

單一職責原則

一個功能就應該職責單一,不只容易看懂,也便於後續重構。RPC實際上是多個功能模塊組合起來的。例如:使用方參數封裝模塊、數據傳輸模塊職責、數據序列化模塊、提供方管理類實例模塊等。一個完善的RPC功能組件,其職責也會分的越細,每一個模塊功能越單一。

開放封閉原則

做爲一個RPC,其設計應該是知足開放封閉原則的,每一個模塊都應該是可擴展的,可是RPC流程應該是對內封閉的。

里氏替換原則

構建實例對象的時候,其類型定義應該是接口類型,而不是具體的實現類型。

依賴倒置原則

高層模塊不該該依賴低層模塊,兩者都應該依賴其抽象對象-->抽象不該該依賴細節,細節應該依賴抽象-->應該針對接口編程,不該該針對實現編程。
 
 

RPC簡單設計

先定義接口


接口應該單獨一個包,做爲jar對外提供。使用方和提供方都依賴該jar

public interface IUserService {
    /**
     * 經過ID獲取用戶名
     *
     * @param id 用戶ID
     * @return java.lang.String
     * @author Tinyice
     */
    String findNameById(Long id);
}

 

提供方實現


簡單實現

public class OtherUserService implements IUserService {
    @Override
    public String findNameById(Long id) {
        return "other name " + id ;
    }
}

 

使用方請求參數封裝


主要封裝四大調用條件對象

@Getter
@Setter
public class RpcRequest implements java.io.Serializable {

    private static final long serialVersionUID = -7223166969378743326L;

    /**
     * 類名稱
     */
    private String className;

    /**
     * 方法名稱
     */
    private String methodName;

    /**
     * 參數類型列表
     */
    private Class<?>[] parameterTypes;

    /**
     * 參數列表
     */
    private Object[] parameters;
}

 

網絡傳輸工具封裝


網絡傳輸方式有不少種,這裏使用socket

@Slf4j
public class TcpTransport {

    private String host;

    private int port;

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

    public Socket newSocketInstance() {
        Socket socket;
        try {
            socket = new Socket(host, port);
            log.info("[{}] 客戶端新建鏈接:服務端地址=【{}】",LocalDateTime.now(), socket.getRemoteSocketAddress());
            return socket;
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("客戶端鏈接失敗");
        }

    }

    public Object send(RpcRequest rpcRequest) {
        Socket socket = null;
        try {
            socket = newSocketInstance();
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
            objectOutputStream.writeObject(rpcRequest);
            objectOutputStream.flush();
            log.info("[{}] 請求參數序列化完畢",LocalDateTime.now());

            ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
            Object obj = objectInputStream.readObject();
            objectOutputStream.close();
            objectInputStream.close();
            log.info("[{}] 請求結果接收完畢",LocalDateTime.now());
            return obj;
        } catch (Exception e) {
            throw new RuntimeException("RPC 調用異常");
        } finally {
            try {
                if (socket != null) {
                    socket.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }
}

其中send方法就是數據傳輸方法,包含請求參數的傳輸和請求結果的接收。

 

網絡請求發起


爲了讓使用方感知不到具體實現的位置(本地仍是原程),通常都會使用代理技術給接口構建一個本地代理對象,經過代理對象來進行遠程調用,使得使用者產生使用的是本地方法的錯覺。。

動態代理,主要分爲JDK動態代理和CGLIB動態代理,不懂原理的能夠去看我寫的文章。爲了簡單,這裏使用JDK動態代理

public class RpcClientProxy {

    public <T> T clientProxy(final Class<T> interfaces, final String host, final int port) {

        return (T) Proxy.newProxyInstance(interfaces.getClassLoader(), new Class[]{interfaces}, new RpcInvocationHandler(host, port));
    }
}

RpcInvocationHandler爲處理代理流程的類,也是網絡請求發起類,發起位置在invoke方法,這是JDK動態代理的核心

@Slf4j
public class RpcInvocationHandler implements InvocationHandler {

    private String host;

    private int port;

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) {

        log.info("客戶端開始封裝參數【RpcRequest】");
        RpcRequest rpcRequest = new RpcRequest();
        rpcRequest.setClassName(method.getDeclaringClass().getName());
        rpcRequest.setMethodName(method.getName());
        rpcRequest.setParameterTypes(method.getParameterTypes());
        rpcRequest.setParameters(args);

        log.info("客戶端開始獲取傳輸工具【tcpTransport】");
        TcpTransport tcpTransport = new TcpTransport(host, port);

        log.info("客戶端開始發送請求參數【RpcRequest】");
        return tcpTransport.send(rpcRequest);
    }
}

知道JDK動態代理,也就指定invoke對java方法調用的意義:方法的真正執行流程。這裏封裝了請求參數、而後發起網絡請求,獲取到網絡請求結果。這個過程替代了方法的執行過程。

 

服務端接口監聽


客戶端和服務端是須要網絡通訊的,這邊也要創建網絡監聽,這裏爲了簡單,將類實例對象存儲功能放在了一塊兒

@Slf4j
public class RpcServer {

    private ExecutorService executorService=Executors.newCachedThreadPool();

    public void  publishServer(final Map<String,Object> serviceRegistCenter, int port){
        ServerSocket serverSocket;
        try {
            serverSocket=new ServerSocket(port);
            log.info("服務端啓動監聽,端口=[{}]",port);
            while (true) {
                Socket socket=serverSocket.accept();
                log.info("服務端監聽到新連接,客戶端地址=【{}】",socket.getRemoteSocketAddress());
                executorService.execute(new RpcServerProcessor(socket,serviceRegistCenter));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

服務端方法調用


上面的RpcServerProcessor就是服務端對客戶端請求的處理方法

@Slf4j
public class RpcServerProcessor implements Runnable {

    private Socket socket;

    private Map<String, Object> serviceRegistCenter;

    public RpcServerProcessor(Socket socket, Map<String, Object> serviceRegistCenter) {
        this.socket = socket;
        this.serviceRegistCenter = serviceRegistCenter;
    }

    @Override
    public void run() {
        //  獲取客戶端傳輸對象,並反序列化
        try (ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream());
             ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream())) {
            RpcRequest rpcRequest = (RpcRequest) inputStream.readObject();
            log.info("[{}] 服務端反序列化完畢", LocalDateTime.now());
            Object obj = invoke(rpcRequest);
            log.info("[{}] 服務端調用完畢", LocalDateTime.now());
            objectOutputStream.writeObject(obj);
            objectOutputStream.flush();
            log.info("[{}] 服務端序列化完畢——將結果發送給客戶端", LocalDateTime.now());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private Object loadBalance(String className) {
        return serviceRegistCenter.get(className);
    }

    private Object invoke(RpcRequest rpcRequest) throws Exception {
        Object[] args = rpcRequest.getParameters();
        Class<?>[] types = rpcRequest.getParameterTypes();
        String className = rpcRequest.getClassName();
        // 服務發現,負載均衡
        Object service = loadBalance(className);
        // 服務調用
        Method method = service.getClass().getMethod(rpcRequest.getMethodName(), types);
        return method.invoke(service, args);

    }
}

這裏也有個invoke方法,就是剛開始分析的反射方法調用,瞭解這塊核心也就指定了RPC如何執行遠程方法的。這裏也涉及到了服務發現與負載均衡。

功能測試

先啓動服務端,服務端發佈服務

@Slf4j
public class ServerBootStrap {

    public static void main(String[] args) {
        // 註冊中心
        Map<String, Object> registCenter = new HashMap<>(16, 1);
        // 實例構建
        IUserService userService = new OtherUserService();
        // 實例註冊
        registCenter.put(IUserService.class.getName(), userService);
        RpcServer rpcServer = new RpcServer();
        log.info("[{}] 服務端發佈對外服務【IUserService】", LocalDateTime.now());
        rpcServer.publishServer(registCenter, 8888);
    }
}

控制檯

[2020-09-25T20:29:40.699] 服務端發佈對外服務【IUserService】
服務端啓動監聽,端口=[8888]

客戶端調用

@Slf4j
public class ClientBootStrap {

    public static void main(String[] args) {
        RpcClientProxy proxy = new RpcClientProxy();
        IUserService userService = proxy.clientProxy(IUserService.class, "localhost", 8888);
        log.info("[{}] 客戶端開始發起RPC調用", LocalDateTime.now());
        String name = userService.findNameById(101L);
        System.out.println(name);

    }
}

客戶端控制檯

[2020-09-25T20:29:55.737] 客戶端開始發起RPC調用
客戶端開始封裝參數【RpcRequest】
客戶端開始獲取傳輸工具【tcpTransport】
客戶端開始發送請求參數【RpcRequest】
[2020-09-25T20:29:55.748] 客戶端新建鏈接:服務端地址=【localhost/127.0.0.1:8888】
[2020-09-25T20:29:55.763] 請求參數序列化完畢
[2020-09-25T20:29:55.769] 請求結果接收完畢
other name 101

Process finished with exit code 0

服務端控制檯

[2020-09-25T20:29:40.699] 服務端發佈對外服務【IUserService】
服務端啓動監聽,端口=[8888]
服務端監聽到新連接,客戶端地址=【/127.0.0.1:29456】
[2020-09-25T20:29:55.766] 服務端反序列化完畢
[2020-09-25T20:29:55.767] 服務端調用完畢
[2020-09-25T20:29:55.767] 服務端序列化完畢——將結果發送給客戶端
能夠經過時間順序觀察下調用流程。

 

總結

以上示例說明了RPC調用流程:包括服務註冊、服務發現、負載均衡、請求參數封裝、數據傳輸、序列化與反序列化等等,這裏爲了簡單示例,只是作了演示。一個完善的RPC也是對上面功能的完善。例如:

服務註冊能夠選擇JVM、Redis、Zookeeper、Nacos等

負載均衡:隨機負載、輪詢負載、權重負載等

數據傳輸:能夠定製傳輸協議,例如dubbo、http、tcp/ip等

序列化:開源的序列化工具也不少,選擇適宜的序列化能提升RPC能力,常見的有:JDK序列化、Hessian、Hessian二、Kryo、protostuff等

其餘優化:服務配置、重試策略、失敗拒絕策略、版本控制、權限控制等等。

 

在與Spring Boot集成中,服務客戶端調用對象的代理能夠和Sping的代理配合,實現無縫鏈接,直接注入使用。

 

原文地址:程序猿微錄 由淺入深讓你搞透RPC,不要讓框架遮住你的眼

相關文章
相關標籤/搜索