手寫簡單的RPC
1.何為RPC
RPC(Remote Procedure Call,遠端過程呼叫)是一種透過網路從遠端計算機程式上請求服務,而不需要了解底層網路技術的協議。RPC協議假定某些傳輸協議的存在,如TCP或UDP,為通訊程式之間攜帶資訊資料。在OSI網路通訊模型中,RPC跨越了傳輸層和應用層。RPC使得開發包括網路分散式多程式在內的應用程式更加容易。
2.工作原理
RPC採用客戶機/伺服器模式。請求程式就是一個客戶機,而服務提供程式就是一個伺服器。首先,客戶機呼叫程序傳送一個有程序引數的呼叫資訊到服務程序,然後等待應答資訊。在伺服器端,程序保持睡眠狀態直到呼叫資訊的到達為止。當一個呼叫資訊到達,伺服器獲得程序引數,計算結果,傳送應答資訊,然後等待下一個呼叫資訊,最後,客戶端呼叫程序接收應答資訊,獲得程序結果,然後呼叫執行繼續進行。
3.架構描述
本例透過服務生產者(provider)
、服務消費者(comsumer)
、服務框架(framework)
三個模組實現簡單的RPC案例。
其中服務生產者者負責提供服務,服務消費者透過http請求去呼叫服務提供者提供的方法,服務框架負責處理服務消費者呼叫服務提供者的相關邏輯處理。
本例中,消費端需要呼叫介面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 先啟動服務生產者
6.2 再啟動消費者
大功告成~~
7.Github倉庫
Github-myrpc