RPC框架初體驗之入門
項目地址:https://github.com/shirukai/learn-demo-rpc.git
RPC全稱Remote Procedure Call,顧名思義,遠程過程調用的意思。關於RPC的介紹,可以參考一下簡書上《如何給老婆解釋什麼是RPC》這篇文章,很有趣。RPC這個概念,我第一次接觸是在《Spark內核設計的藝術》這本書裏。後來在看微服務的時候,也提及到了幾款RPC框架,比如Thrift、Dubbo、gRPC。所以決定認真的學習一下RPC以及這幾種框架。下面將會在本篇文章裏入門RPC,動手實現一個簡單的RPC,再基於Netty實現一個RPC,最後簡單介紹一下幾款常見的RPC框架,以及它們的優缺點。後面將會以系列的形式分別介紹這幾款常見的RPC框架的使用。
1 動手實現一個簡單的RPC
爲了深入理解RPC,這裏動手實現了一個簡單的RPC,服務之前通過簡單的socket進行通訊。
1.1 項目描述
在learn-demo-rpc項目下有一個simple-rpc的模塊,該模塊實現了一個簡單的RPC,其中包括四個子模塊
simple-rpc/
├── simple-rpc-api
├── simple-rpc-consumer
├── simple-rpc-core
└── simple-rpc-provider
- simple-rpc-api: 該模塊提供服務接口
- simple-rpc-core: RPC核心實現
- simple-rpc-consumer: RPC消費者服務
- simple-rpc-provider: RPC提供者服務
simple-rpc-provider 模塊實現simple-rpc-api定義的相關接口,並通過simple-rpc-core模塊創建提供者服務。
simple-rpc-consumer通過simple-rpc-core模塊創建消費者服務,並通過simple-rpc-api模塊的接口進行RPC。
項目演示:
啓動simple-rpc-provider模塊裏的DemoServiceProvider
啓動simple-rpc-consumer裏的DemoServiceConsumer
1.2 simple-rpc-core模塊
該模塊爲核心模塊,分別提供了服務者、消費者服務創建。如下所示,主要包括四個功能。request包定義RPC請求數據類型,response包定義RPC響應數據類型,server提供RPC的provider服務,client提供RPC的consumer服務。
1.2.1 request
在該包下創建RpcRequest類,用來定義請求數據類型實體,該實體主要包含,遠程調用的方法名稱、參數列表、參數類型列表。
package learn.demo.rpc.simple.core.request;
import java.io.Serializable;
import java.util.Arrays;
/**
* Created by shirukai on 2019-06-21 15:02
* RPC 請求
*/
public class RpcRequest implements Serializable {
private static final long serialVersionUID = 4932007273709224551L;
/**
* 方法名稱
*/
private String methodName;
/**
* 參數列表
*/
private Object[] parameters;
/**
* 參數類型
*/
private Class<?>[] parameterTypes;
/**
* 省略get、set方法。
*/
}
1.2.2 response
在該包下創建RpcResponse類,用來定義RPC響應的數據類型,其中包含響應狀態status,用來描述請求是否執行成功,它有兩個狀態succeed和failed。響應信息message主要存放異常響應時的錯誤信息。響應數據data,遠程調用方法的返回值。
public class RpcResponse implements Serializable {
public static String SUCCEED = "succeed";
public static String FAILED = "failed";
private static final long serialVersionUID = 6595683424889346485L;
/**
* 響應狀態
*/
private String status = "succeed";
/**
* 響應信息,如異常信息
*/
private String message;
/**
* 響應數據,返回值
*/
private Object data;
/**
* 省略get、set方法
*/
}
1.2.3 server
在該包下創建RpcProvider類,用來定義創建Provider服務的方法。原理很簡單,根據指定的端口創建ServerSocket,監聽客戶端發送數據。接收到客戶端發送數據後,反序列化成Request,獲取其中的方法名和參數類型及參數列表,根據傳入的接口class和實例,通過反射機制,調用該方法,拿到執行結果後封裝成RpcResponse返回給客戶端。具體實現如下:
package learn.demo.rpc.simple.core.server;
import learn.demo.rpc.simple.core.request.RpcRequest;
import learn.demo.rpc.simple.core.response.RpcResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
/**
* Created by shirukai on 2019-06-21 16:26
* RPC provider
*/
public class RpcProvider<T> {
private static final Logger log = LoggerFactory.getLogger(RpcProvider.class);
private T ref;
private Class<?> interfaceClass;
public void setRef(T ref) {
this.ref = ref;
}
public RpcProvider<T> setInterfaceClass(Class<?> interfaceClass) {
this.interfaceClass = interfaceClass;
return this;
}
public void export(int port) {
try {
log.info("The RPC Server is starting, address:{}, bind:{}", InetAddress.getLocalHost().getHostAddress(), port);
ServerSocket listener = new ServerSocket(port);
while (true) {
Socket socket = listener.accept();
// 接收數據並進行反序列化
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
// 獲取請求對象
Object object = objectInputStream.readObject();
if (object instanceof RpcRequest) {
RpcRequest request = (RpcRequest) object;
log.info("Received request:{}", request);
// 處理請求
RpcResponse response = handleRequest(request);
// 將結果返回給客戶端
log.info("Send response to client.{}", response);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
objectOutputStream.writeObject(response);
}
socket.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
private RpcResponse handleRequest(RpcRequest request) {
RpcResponse response = new RpcResponse();
try {
log.info("The server is handling request.");
Method method = interfaceClass.getMethod(request.getMethodName(), request.getParameterTypes());
Object data = method.invoke(ref, request.getParameters());
response.setData(data);
} catch (Exception e) {
response.setStatus(RpcResponse.FAILED).setMessage(e.getMessage());
}
return response;
}
}
1.2.4 client
客戶端的實現比較有趣,包含如下內容
client/
├── RpcClient.java
├── RpcConsumer.java
└── proxy
└── RpcInvocationHandler.java
原理也不復雜,通過上面的結構可以看出,我們clien裏包含了一個proxy包,該包主要實現的是一個動態代理。客戶端實現API接口的動態代理,生成接口實例,表面上調用的接口方法,通過代理後,經過RpcClient進行的遠程調用,也就是我們的定義的RPC,拿到結果後再返回。
1.2.4.1 RpcClient
RpcClient主要是與遠程ServerSocket進行通訊的,創建根據IP和端口創建Socket,將RpcRequest進行序列化之後,發送給遠程服務。
package learn.demo.rpc.simple.core.client;
import learn.demo.rpc.simple.core.request.RpcRequest;
import learn.demo.rpc.simple.core.response.RpcResponse;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
/**
* Created by shirukai on 2019-06-21 15:42
* Rpc客戶端
*/
public class RpcClient {
/**
* 服務地址
*/
private String address;
/**
* 服務端口
*/
private int port;
public RpcResponse send(RpcRequest rpcRequest) throws Exception {
Socket socket = new Socket(address, port);
//請求序列化
ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
//將請求發給服務提供方
objectOutputStream.writeObject(rpcRequest);
// 將響應體反序列化
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
Object response = objectInputStream.readObject();
if (response instanceof RpcResponse) {
return (RpcResponse) response;
}
throw new RuntimeException("Return error");
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
}
1.2.4.2 RpcInvocationHandler
在proxy下創建RpcInvocationHandler,調用處理器,繼承InvocationHandler接口並實現其invoke方法。實現代理邏輯。如下所示:
package learn.demo.rpc.simple.core.client.proxy;
import learn.demo.rpc.simple.core.client.RpcClient;
import learn.demo.rpc.simple.core.request.RpcRequest;
import learn.demo.rpc.simple.core.response.RpcResponse;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
/**
* Created by shirukai on 2019-06-21 15:43
* RPC 代理處理器
*/
public class RpcInvocationHandler implements InvocationHandler {
private RpcClient client;
public RpcInvocationHandler(RpcClient client) {
this.client = client;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 構建請求對象
RpcRequest rpcRequest = new RpcRequest();
rpcRequest.setMethodName(method.getName())
.setParameterTypes(method.getParameterTypes())
.setParameters(args);
// 使用客戶端發送請求
RpcResponse response = client.send(rpcRequest);
// 響應成功返回結果
if (RpcResponse.SUCCEED.equals(response.getStatus())) {
return response.getData();
}
throw new RuntimeException(response.getMessage());
}
}
1.2.4.3 RpcConsumer
該類通過實例化RpcClient以及創建代理實例來構建生產者
package learn.demo.rpc.simple.core.client;
import learn.demo.rpc.simple.core.client.proxy.RpcInvocationHandler;
import java.lang.reflect.Proxy;
/**
* Created by shirukai on 2019-06-21 16:11
* 生產者構建器
*/
public class RpcConsumer {
private String address;
private int port;
private Class<?> interfaceClass;
public RpcConsumer setAddress(String address) {
this.address = address;
return this;
}
public RpcConsumer setPort(int port) {
this.port = port;
return this;
}
public RpcConsumer setInterface(Class<?> interfaceClass) {
this.interfaceClass = interfaceClass;
return this;
}
public <T> T get() {
RpcClient client = new RpcClient();
client.setAddress(address);
client.setPort(port);
// 實例化RPC代理處理器
RpcInvocationHandler handler = new RpcInvocationHandler(client);
return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class[]{interfaceClass}, handler);
}
}
1.3 simple-rpc-api模塊
上文也提到,該模塊只是提供一個公用的接口API,沒有特殊方法。如下提供一個名爲DemoService的接口,定義如下接口:
package learn.demo.rpc.simple.api;
/**
* Created by shirukai on 2019-06-21 10:54
* DemoService 接口
*/
public interface DemoService {
String sayHello(String name);
String sayGoodbye(String name);
}
1.4 simple-rpc-provider模塊
上面我們已經實現了核心模塊core以及接口api,這裏我們調用這兩個模塊進行提供者服務的創建。分爲接口實現,和服務創建兩部分。
1.4.1 API接口實現
引入我們創建的simple-rpc-api模塊
<dependency>
<groupId>learn.demo</groupId>
<artifactId>simple-rpc-api</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
創建DemoServiceImpl實現DemoService接口
package learn.demo.rpc.simple.provider;
import learn.demo.rpc.simple.api.DemoService;
/**
* Created by shirukai on 2019-06-21 10:55
* 接口實現類
*/
public class DemoServiceImpl implements DemoService {
@Override
public String sayHello(String name) {
return "This is simple RPC service.\nHello " + name;
}
@Override
public String sayGoodbye(String name) {
return "This is simple RPC service.\nGoodbye " + name;
}
}
1.4.2 RPC Provider服務創建
使用simple-rpc-core模塊創建RpcProvider實例,然後啓動服務。這裏向外暴露端口9090。
package learn.demo.rpc.simple.provider;
import learn.demo.rpc.simple.api.DemoService;
import learn.demo.rpc.simple.core.server.RpcProvider;
/**
* Created by shirukai on 2019-06-21 10:56
* 服務提供者
*/
public class DemoServiceProvider {
public static void main(String[] args) {
DemoServiceImpl demoService = new DemoServiceImpl();
RpcProvider<DemoService> provider = new RpcProvider<>();
provider.setInterfaceClass(DemoService.class)
.setRef(demoService);
provider.export(9090);
}
}
1.5 simple-rpc-consumer模塊
通過simple-rpc-cor模塊創建RpcConsumer實例,設置Provider地址和端口,然後獲取接口實例,調用相關方法。
package learn.demo.rpc.simple.consumer;
import learn.demo.rpc.simple.api.DemoService;
import learn.demo.rpc.simple.core.client.RpcConsumer;
/**
* Created by shirukai on 2019-06-21 11:29
* 消費者
*/
public class DemoServiceConsumer {
public static void main(String[] args) {
RpcConsumer consumer = new RpcConsumer();
consumer.setAddress("127.0.0.1");
consumer.setPort(9090);
consumer.setInterface(DemoService.class);
DemoService service = consumer.get();
System.out.println(service.sayGoodbye("hahah"));
}
}
2 基於ZooKeeper註冊中心的RPC實現
上面我們介紹了通過直連的方式,實現了一個簡單RPC,我們也可以通過註冊中心的形式去實現RPC,這涉及到了服務註冊和服務發現。這裏使用ZooKeeper作爲註冊中心,也簡單的進行了RPC的實現,其中有些地方沒有進行詳細實現,比如服務的負載均衡。該部分的代碼在learn-demo-rpc下的zk-registry-rpc模塊下,目錄結構如simple-rpc相同,如下所示:
zk-registry-rpc/
├── zk-registry-rpc-api
├── zk-registry-rpc-consumer
├── zk-registry-rpc-core
└── zk-registry-rpc-provider
內容改動不大,主要在core的實現上,加入了註冊中心,進行服務發現和服務註冊。在Rpc的請求上,也加入了ID字段,用來表示需要調用哪個服務。下面將對幾處修改的地方進行講解。
2.1 zk-registry-rpc-core模塊
2.1.1 request
上面提到對RpcRequest進行簡單修改,加入Id字段,用來描述調用的是那個接口下的服務,所以此id是使用接口名生成的。
/**
* 請求ID,接口類名
*/
private String id;
2.1.2 registry
這裏主要是對註冊中心的實現,其中包括ProviderInfo實體類,用來描述提供者信息,如id、address、port。另外包括RpcZKRegistryService註冊中心服務。
2.1.2.1 ProviderInfo
package learn.demo.rpc.zk.core.registry;
import com.alibaba.fastjson.JSON;
/**
* Created by shirukai on 2019-06-25 16:34
* Provider信息
*/
public class ProviderInfo {
/**
* 提供者ID
*/
private String id;
/**
* 提供者地址
*/
private String address;
/**
* 提供者端口
*/
private int port;
public String getId() {
return id;
}
public ProviderInfo setId(String id) {
this.id = id;
return this;
}
public String getAddress() {
return address;
}
public ProviderInfo setAddress(String address) {
this.address = address;
return this;
}
public int getPort() {
return port;
}
public ProviderInfo setPort(int port) {
this.port = port;
return this;
}
public String toJSONString() {
return JSON.toJSONString(this);
}
@Override
public String toString() {
return "ProviderInfo{" +
"id='" + id + '\'' +
", address='" + address + '\'' +
", port=" + port +
'}';
}
}
2.1.2.2 RpcZKRegistryService
註冊中心的實現,主要包括三個功能:服務註冊、服務發現、服務監聽。Provider通過調用註冊中的服務註冊,將自己的信息註冊到ZK中,Consumer通過調用註冊中心的服務發現,查找自己想要請求的服務列表,並通過服務監聽,更新服務列表。具體實現如下所示:
package learn.demo.rpc.zk.core.registry;
import com.alibaba.fastjson.JSON;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.PathChildrenCache;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.apache.curator.retry.RetryNTimes;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* Created by shirukai on 2019-06-25 16:42
* Rpc註冊服務
*/
public class RpcZKRegistryService {
private static final Logger log = LoggerFactory.getLogger(RpcZKRegistryService.class);
private static final String NAMESPACE = "zk-rpc";
private static final String RPC_PROVIDER_NODE = "/provider";
private final Map<String, ProviderInfo> remoteProviders = new HashMap<>();
private CuratorFramework zkClient;
public RpcZKRegistryService(String zkConnectString) {
RetryPolicy retryPolicy = new RetryNTimes(3, 5000);
// 獲取客戶端
this.zkClient = CuratorFrameworkFactory.builder()
.connectString(zkConnectString)
.sessionTimeoutMs(10000)
.retryPolicy(retryPolicy)
.namespace(NAMESPACE)
.build();
this.zkClient.start();
}
/**
* 註冊服務
*
* @param providerInfo 提供者信息
*/
public void register(ProviderInfo providerInfo) {
String nodePath = RPC_PROVIDER_NODE + "/" + providerInfo.getId();
try {
// 判斷節點存不存在,不存在則創建,存在則報異常
Stat stat = zkClient.checkExists().forPath(nodePath);
if (stat == null) {
// 創建臨時節點
zkClient.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
.withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
.forPath(nodePath, providerInfo.toJSONString().getBytes());
} else {
log.error("The provider already exists.{}", providerInfo.toJSONString());
}
} catch (Exception e) {
log.error("Registration provider failed.{}", e.getMessage());
}
}
/**
* 訂閱服務
*
* @param id 提供者ID,接口名字
*/
public void subscribe(String id) {
try {
// 獲取所有的Provider
List<String> providerIds = zkClient.getChildren().forPath(RPC_PROVIDER_NODE);
for (String providerId : providerIds) {
// 如果與訂閱服務相同,則獲取節點信息
if (providerId.contains(id)) {
String nodePath = RPC_PROVIDER_NODE + "/" + providerId;
byte[] data = zkClient.getData().forPath(nodePath);
ProviderInfo info = JSON.parseObject(data, ProviderInfo.class);
this.remoteProviders.put(providerId, info);
}
}
// 添加監聽事件
addProviderWatch(id);
} catch (Exception e) {
log.error("Subscription provider failed.");
}
}
/**
* 添加監聽事件
*
* @param id 提供者ID,接口名字
*/
private void addProviderWatch(String id) throws Exception {
// 創建子節點緩存
final PathChildrenCache childrenCache = new PathChildrenCache(this.zkClient, RPC_PROVIDER_NODE, true);
childrenCache.start(PathChildrenCache.StartMode.BUILD_INITIAL_CACHE);
// 添加子節點監聽事件
childrenCache.getListenable().addListener((client, event) -> {
String nodePath = event.getData().getPath();
// 如果監聽節點爲訂閱的ProviderID
if (nodePath.contains(id)) {
if (event.getType().equals(PathChildrenCacheEvent.Type.CHILD_REMOVED)) {
// 節點移除
this.remoteProviders.remove(nodePath);
} else if (event.getType().equals(PathChildrenCacheEvent.Type.CHILD_ADDED)) {
byte[] data = event.getData().getData();
ProviderInfo info = JSON.parseObject(data, ProviderInfo.class);
// 節點添加
this.remoteProviders.put(nodePath, info);
}
}
});
}
public Map<String, ProviderInfo> getRemoteProviders() {
return remoteProviders;
}
}
2.1.3 server
server的改動不大,主要是加入了設置zk連接的方法,以及將自己的信息通過註冊中心註冊到zk的邏輯。
// 設置zk連接
public RpcProvider<T> setZKConnectString(String zkConnectString) {
this.registryService = new RpcZKRegistryService(zkConnectString);
return this;
}
// 生成服務信息
ProviderInfo providerInfo = new ProviderInfo();
providerInfo.setAddress(InetAddress.getLocalHost().getHostAddress())
.setPort(port)
.setId(interfaceClass.getName());
// 創建服務
ServerSocket listener = new ServerSocket(port);
// 服務創建完成後將信息註冊到zk
registryService.register(providerInfo);
2.1.4 client
客戶端主要修改了RpcConsumer,添加了服務發現,和模擬負載均衡的兩個方法。
/**
* 獲取所有Providers
*
* @return list
*/
private List<ProviderInfo> lookupProviders() {
// 訂閱服務
registryService.subscribe(interfaceClass.getName());
// 獲取所有Provider
Map<String, ProviderInfo> providers = registryService.getRemoteProviders();
return new ArrayList<>(providers.values());
}
/**
* 模擬負載均衡
*
* @param providers provider 列表
* @return ProviderInfo
*/
private static ProviderInfo chooseTarget(List<ProviderInfo> providers) {
if (providers == null || providers.isEmpty()) {
throw new RuntimeException("providers has not exits!");
}
return providers.get(0);
}
然後再創建代理實例之前調用服務發現和負載均衡方法
public <T> T get() {
List<ProviderInfo> providers = lookupProviders();
ProviderInfo provider = chooseTarget(providers);
RpcClient client = new RpcClient();
client.setAddress(provider.getAddress());
client.setPort(provider.getPort());
// 實例化RPC代理處理器
RpcInvocationHandler handler = new RpcInvocationHandler(client);
return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class[]{interfaceClass}, handler);
}
3 常見RPC框架
上面通過動手實現一個簡單的RPC,大體對RPC的工作流程有了一定的瞭解,當然我們寫的只是一個簡單的RPC,Demo級別的,只能玩玩不能用於生產。如果想提高性能,可以考慮使用NIO Socket進行通信,也可以基於Netty進行RPC通信,也可以通過利用一下已有的RPC框架,這裏就簡單對比一下幾款常見的RPC框架。
PRC對比 | Dubbo | Motan | Thrift | Grpc |
---|---|---|---|---|
開發語言 | java | java | 跨語言 | 跨語言 |
服務治理 | ✓ | ✓ | ✗ | ✗ |
多種序列化 | ✓ | ✓ | 只支持thrift | 只支持protobuf |
多種註冊中心 | ✓ | ✓ | ✗ | ✗ |
管理中心 | ✓ | ✓ | ✗ | ✗ |
跨語言通訊 | ✗ | ✗ | ✓ | ✓ |
整體性能 | 3 | 4 | 5 | 3 |
等有時間整理一下Dubbo以及Thrift的簡單使用。