手寫簡單的RPC

码虎發表於2024-04-18

手寫簡單的RPC

1.何為RPC

RPC(Remote Procedure Call,遠端過程呼叫)是一種透過網路從遠端計算機程式上請求服務,而不需要了解底層網路技術的協議。RPC協議假定某些傳輸協議的存在,如TCP或UDP,為通訊程式之間攜帶資訊資料。在OSI網路通訊模型中,RPC跨越了傳輸層和應用層。RPC使得開發包括網路分散式多程式在內的應用程式更加容易。

2.工作原理

RPC採用客戶機/伺服器模式。請求程式就是一個客戶機,而服務提供程式就是一個伺服器。首先,客戶機呼叫程序傳送一個有程序引數的呼叫資訊到服務程序,然後等待應答資訊。在伺服器端,程序保持睡眠狀態直到呼叫資訊的到達為止。當一個呼叫資訊到達,伺服器獲得程序引數,計算結果,傳送應答資訊,然後等待下一個呼叫資訊,最後,客戶端呼叫程序接收應答資訊,獲得程序結果,然後呼叫執行繼續進行。

3.架構描述

本例透過服務生產者(provider)服務消費者(comsumer)服務框架(framework)三個模組實現簡單的RPC案例。

其中服務生產者者負責提供服務,服務消費者透過http請求去呼叫服務提供者提供的方法,服務框架負責處理服務消費者呼叫服務提供者的相關邏輯處理。

image

本例中,消費端需要呼叫介面ProviderService.class 中的某個方法。生產者模組提供了其具體實現類ProviderServiceImpl.class ;最後,消費者需要透過RPC去呼叫生成者提供的這個方法。

廢話不多說,直接上程式碼!

4.服務生產者具體實現

package com.myrpc;

import com.myrpc.apis.ProviderService;
import com.myrpc.domain.ServiceBean;
import com.myrpc.domain.ServiceMetaInfo;
import com.myrpc.register.ServiceRegister;
import com.myrpc.server.HttpServer;
import com.myrpc.service.impl.ProviderServiceImpl;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * @author huliua
 * @version 1.0
 * @date 2024-04-14 15:26
 */
public class ProviderMain {
    public static void main(String[] args) {
        // 構建服務元資訊
        ServiceMetaInfo serviceMetaInfo = new ServiceMetaInfo();
        // 服務的主鍵資訊
        serviceMetaInfo.setKey(UUID.randomUUID().toString());
        // 服務名稱
        serviceMetaInfo.setServiceName("provider");
        // 版本號
        serviceMetaInfo.setVersion("1.0");
        // ip
        serviceMetaInfo.setHost("localhost");
        // 埠
        serviceMetaInfo.setPort(8080);

        // 構建提供的服務列表
        List<ServiceBean> beanList = new ArrayList<>();
        ServiceBean serviceBean = new ServiceBean();
        serviceBean.setBeanName(ProviderService.class.getName());
        serviceBean.setBeanClass(ProviderServiceImpl.class);
        beanList.add(serviceBean);

        try {
            // 服務註冊
            ServiceRegister.register(serviceMetaInfo, beanList);

            // 啟動服務
            HttpServer httpServer = new HttpServer();
            httpServer.start(serviceMetaInfo);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

其中ServiceMetaInfo 是框架模組提供的服務資訊類,主要用於記錄服務的資訊

package com.myrpc.domain;

import cn.hutool.core.util.RandomUtil;
import lombok.Data;

import java.io.Serializable;

/**
 * 服務的後設資料
 * @author huliua
 * @version 1.0
 * @date 2024-04-15 21:20
 */
@Data
public class ServiceMetaInfo implements Serializable {

    private String key;

    private String serviceName;

    private String host;

    private Integer port;

    private String version;
}

ServiceBean 是框架提供的類,主要用於記錄一個服務下會提供哪些服務,包含類名、以及對應實現類的類名。

package com.myrpc.domain;

import lombok.Data;

import java.io.Serializable;

/**
 * @author huliua
 * @version 1.0
 * @date 2024-04-14 17:54
 */
@Data
public class ServiceBean implements Serializable {

    private String beanName;

    private Class<?> beanClass;
}

ServiceRegister 是框架提供的類,主要用於服務註冊。本例中只實現了本地的服務註冊,後續可以把服務資訊註冊到redis、nacos、zookeeper中。

package com.myrpc.register;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import com.myrpc.domain.ServiceBean;
import com.myrpc.domain.ServiceMetaInfo;

import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 服務註冊中心
 *
 * @author huliua
 * @version 1.0
 * @date 2024-04-14 15:27
 */
public class ServiceRegister {

    private static final String filePath = "/myrpc/register.txt";

    /**
     * 本地服務元資訊列表
     */
    private static final Map<String, List<ServiceMetaInfo>> localServiceMetaInfoMap = new HashMap<>();

    /**
     * 本地服務列表
     */
    private static final Map<String, List<ServiceBean>> localServiceBeanMap = new HashMap<>();

    /**
     * 服務註冊
     */
    public static void register(ServiceMetaInfo serviceMetaInfo, List<ServiceBean> serviceList) throws IOException {
        // 先實現本地註冊
        List<ServiceMetaInfo> services = localServiceMetaInfoMap.get(serviceMetaInfo.getServiceName());
        if (CollectionUtil.isEmpty(services)) {
            services = new ArrayList<>();
        }
        services.add(serviceMetaInfo);
        localServiceMetaInfoMap.put(serviceMetaInfo.getServiceName(), services);

        // 儲存該服務名下提供的服務列表
        localServiceBeanMap.put(serviceMetaInfo.getKey(), serviceList);

        // 遠端服務註冊(暫時使用存入本地檔案的方式代替)
        FileOutputStream fileOutputStream = new FileOutputStream(filePath);
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(localServiceMetaInfoMap);
    }

    /**
     * 根據服務名獲取服務資訊
     *
     * @param serviceName 服務名
     * @return 返回註冊中心的服務列表
     */
    public static List<ServiceMetaInfo> getService(String serviceName) {
        // 優先從本地快取中讀取
        List<ServiceMetaInfo> serviceList = localServiceMetaInfoMap.get(serviceName);
        if (CollUtil.isNotEmpty(serviceList)) {
            return serviceList;
        }
        // 從遠端註冊中心中讀取(暫時使用讀取本地檔案的方式代替)
        FileInputStream fileInputStream = null;
        ObjectInputStream objectInputStream = null;
        try {
            fileInputStream = new FileInputStream(filePath);
            objectInputStream = new ObjectInputStream(fileInputStream);
            Map<String, List<ServiceMetaInfo>> resMap = (Map<String, List<ServiceMetaInfo>>) objectInputStream.readObject();
            return resMap.get(serviceName);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            try {
                assert fileInputStream != null;
                fileInputStream.close();
                assert objectInputStream != null;
                objectInputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * 根據key獲取服務列表
     * @param key
     * @return
     */
    public static List<ServiceBean> getServiceBeanList(String key) {
        return localServiceBeanMap.get(key);
    }
}

HttpServer 是框架中提供的類,主要作用是啟動tomcat,監聽請求,並配置請求分發器DispatcherServlet

package com.myrpc.server;

import com.myrpc.dispatcher.DispatcherServlet;
import com.myrpc.domain.ServiceMetaInfo;
import org.apache.catalina.*;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardEngine;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.startup.Tomcat;

/**
 * @author huliua
 * @version 1.0
 * @date 2024-04-14 17:24
 */
public class HttpServer {

    public void start(ServiceMetaInfo service) {
        Tomcat tomcat = new Tomcat();

        Server server = tomcat.getServer();
        org.apache.catalina.Service tomcatService = server.findService("Tomcat");

        Connector connector = new Connector();
        connector.setPort(service.getPort());

        Engine engine = new StandardEngine();
        engine.setDefaultHost(service.getHost());

        Host host = new StandardHost();
        host.setName(service.getHost());

        String contextPath = "";
        Context context = new StandardContext();
        context.setPath(contextPath);
        context.addLifecycleListener(new Tomcat.FixContextListener());

        host.addChild(context);
        engine.addChild(host);

        tomcatService.setContainer(engine);
        tomcatService.addConnector(connector);

        tomcat.addServlet(contextPath, "dispatcher", new DispatcherServlet());
        context.addServletMappingDecoded("/*", "dispatcher");

        try {
            tomcat.start();
            tomcat.getServer().await();
        } catch (LifecycleException e) {
            e.printStackTrace();
        }

    }
}

DispatcherServlet 是框架提供的類,主要作用是處理請求。當有請求到達時,透過HttpServerHandler 處理請求

package com.myrpc.dispatcher;

import com.myrpc.handler.HttpServerHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServlet;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;

/**
 * @author huliua
 * @version 1.0
 * @date 2024-04-14 17:34
 */
@Slf4j
public class DispatcherServlet extends HttpServlet {

    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        log.info("有新的請求待處理...");
        new HttpServerHandler().handler(req, res);
    }
}

HttpServerHandler 是框架提供的類,主要作用是處理遠端呼叫請求。根據遠端服務呼叫資訊,透過SPI機制找到對應的實現類,完成方法的呼叫並將返回值寫入請求響應中。

package com.myrpc.handler;

import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.ServiceLoaderUtil;
import com.alibaba.fastjson2.JSON;
import com.myrpc.domain.Invocation;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import org.apache.commons.io.IOUtils;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @author huliua
 * @version 1.0
 * @date 2024-04-14 17:35
 */
@SuppressWarnings("all")
public class HttpServerHandler {

    public void handler(ServletRequest req, ServletResponse res) {
        try {
            ObjectInputStream objectInputStream = new ObjectInputStream(req.getInputStream());
            Invocation invocation = (Invocation) objectInputStream.readObject();
            Class<?> serviceClass = ClassUtil.getClassLoader().loadClass(invocation.getClassName());
            Object serviceImpl = ServiceLoaderUtil.loadFirstAvailable(serviceClass);

            // 服務呼叫
            Method method = serviceClass.getMethod(invocation.getMethodName(), invocation.getParamTypes());
            Object result = method.invoke(serviceImpl, invocation.getArgs());

            // 寫入響應
            IOUtils.write(JSON.toJSONString(result), res.getOutputStream());
        } catch (FileNotFoundException | NoSuchMethodException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
}

LoadBalance 是框架提供的類,主要用於實現負載均衡,這裡以隨機的方式為例

package com.myrpc.loadbalance.impl;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.RandomUtil;
import com.myrpc.domain.ServiceMetaInfo;
import com.myrpc.loadbalance.LoadBalance;

import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

/**
 * 隨機負載均衡
 *
 * @author huliua
 * @version 1.0
 * @date 2024-04-14 16:25
 */
public class RandomLoadBalance implements LoadBalance {

    public ServiceMetaInfo loadBalance(List<ServiceMetaInfo> serviceList) {
        if (CollectionUtil.isEmpty(serviceList)) {
            return null;
        }
        ThreadLocalRandom random = RandomUtil.getRandom();
        int index = random.nextInt(serviceList.size());
        return serviceList.get(index);
    }
}

5.消費者具體實現

透過JDK代理的方式,獲取代理物件,然後呼叫代理物件的方法實現遠端呼叫。

package com.myrpc;

import com.myrpc.apis.ProviderService;
import com.myrpc.bo.ResponseResult;
import com.myrpc.proxy.ProxyFactory;

import java.util.List;
import java.util.Map;

/**
 * @author huliua
 * @version 1.0
 * @date 2024-04-14 15:26
 */
public class ConsumerMain {
    public static void main(String[] args) {
        ProviderService providerService = ProxyFactory.getProxy("provider", ProviderService.class);
        ResponseResult<List<Map<String, Object>>> result = providerService.say();
        System.out.println(result);
    }
}

ProxyFactory 是框架提供的類,主要用於建立代理物件。當呼叫代理物件的方法時,都會走到這裡的invoke 邏輯中:根據呼叫方法的方法名、方法引數、返回值型別等資訊構建遠端方法呼叫引數,然後發起http請求去實現遠端方法呼叫。

package com.myrpc.proxy;

import com.myrpc.client.HttpClient;
import com.myrpc.domain.Invocation;
import com.myrpc.domain.RpcResponse;
import com.myrpc.retry.Retryer;

import java.lang.reflect.Proxy;

/**
 * @author huliua
 * @version 1.0
 * @date 2024-04-14 21:59
 */
public class ProxyFactory {

    public static <T> T getProxy(String serviceName, Class<?> interfaceClass) {
        Object proxyInstance = Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class[]{interfaceClass}, (proxy, method, args) -> {
            // 構建方法呼叫資訊
            Invocation invocation = new Invocation();
            invocation.setServiceName(serviceName);
            invocation.setClassName(interfaceClass.getName());
            invocation.setMethodName(method.getName());
            invocation.setParamTypes(method.getParameterTypes());
            invocation.setArgs(args);
            invocation.setReturnType(method.getReturnType());

            HttpClient httpClient = new HttpClient();
            // 服務重試
            RpcResponse response = Retryer.doRetry(() -> httpClient.send(invocation));
            if (response.getData() != null) {
                return response.getData();
            } else {
                // TODO: 重試多次後,服務降級
                throw new RuntimeException(response.getException());
            }
        });
        return (T) proxyInstance;
    }
}

Invocation是框架提供的類,主要用於儲存方法呼叫的資訊,比如方法名、引數、返回值型別等

package com.myrpc.domain;

import lombok.Data;

import java.io.Serializable;

/**
 * @author huliua
 * @version 1.0
 * @date 2024-04-14 17:38
 */
@Data
public class Invocation implements Serializable {

    private String serviceName;

    private String className;

    private String methodName;

    private Class[] paramTypes;

    private Object[] args;

    private Class returnType;
}

HttpClient是框架提供的類,是客戶端的核心類。主要用於根據方法呼叫引數發現服務,再透過負載均衡獲取具體的服務,然後根據服務的後設資料(主要為主機、埠資訊)發起http請求,實現服務的遠端呼叫

package com.myrpc.client;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import com.alibaba.fastjson2.JSON;
import com.myrpc.domain.Invocation;
import com.myrpc.domain.ServiceMetaInfo;
import com.myrpc.loadbalance.LoadBalance;
import com.myrpc.loadbalance.impl.RandomLoadBalance;
import com.myrpc.register.ServiceRegister;
import org.apache.commons.io.IOUtils;

import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * 提供給服務呼叫端使用
 *
 * @author huliua
 * @version 1.0
 * @date 2024-04-14 19:41
 */
public class HttpClient {

    /**
     * 客戶端--服務列表快取
     */
    private static final Map<String, List<ServiceMetaInfo>> serviceCacheMap = new HashMap<>();

    private final LoadBalance loadBalance;

    public HttpClient() {
        loadBalance = new RandomLoadBalance();
    }

    public Object send(Invocation invocation) {
        try {
            // 優先從本地快取中獲取服務
            List<ServiceMetaInfo> serviceList = serviceCacheMap.get(invocation.getServiceName());
            if (CollUtil.isEmpty(serviceList)) {
                // 本地快取沒有,則從註冊中心獲取
                serviceList = ServiceRegister.getService(invocation.getServiceName());
            }
						
						// 負載均衡
            ServiceMetaInfo service = loadBalance.loadBalance(serviceList);
            if (null == service) {
                throw new RuntimeException("service not found");
            }

						// 發起請求
            URL url = new URL("http", service.getHost(), service.getPort(), "/");
            HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();

            httpURLConnection.setRequestMethod("POST");
            httpURLConnection.setDoOutput(true);

            OutputStream outputStream = httpURLConnection.getOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(outputStream);

            oos.writeObject(invocation);
            oos.flush();
            oos.close();

            InputStream inputStream = httpURLConnection.getInputStream();
            // 返回響應
            return JSON.parseObject(IOUtils.toString(inputStream), invocation.getReturnType());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

Retryer是框架提供的類,主要用於實現重試。當服務異常時,透過重試機制多次重新請求。保證服務的高可用。本例中預設會進行3次重試,每次重試直接間隔1秒。

package com.myrpc.retry;

import com.myrpc.domain.RpcResponse;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

/**
 * 服務重試機制
 * @author huliua
 * @version 1.0
 * @date 2024-04-16 15:55
 */
@Slf4j
public class Retryer {

    /**
     * 最大重試次數
     */
    public static final int MAX_RETRY_TIMES = 3;
    /**
     * 重試間隔時間,單位:秒
     */
    public static final int RETRY_SLEEP_SECOND = 1;

    public static RpcResponse doRetry(Callable<?> callable) throws InterruptedException {
        RpcResponse res = new RpcResponse();
        int retryTimes = 0;
        while (retryTimes < MAX_RETRY_TIMES) {
            try {
                Object callResult = callable.call();
                res.setData(callResult);
                break;
            } catch (Exception e) {
                retryTimes++;
                log.info("retrying......retry times: {}", retryTimes);
                TimeUnit.SECONDS.sleep(RETRY_SLEEP_SECOND);
                res.setException(e);
            }
        }
        return res;
    }
}

6.啟動,測試!

6.1 先啟動服務生產者

image

6.2 再啟動消費者

image

大功告成~~

7.Github倉庫

Github-myrpc

相關文章