在學習了Dubbo之後, 我發現自己好像瞭解了Dubbo的實現原理, 又好像不是很瞭解, 畢竟我只是背誦了下概念, 沒有深入的去看源碼. 這裏我就來手寫一個簡化版的Dubbo框架, 通過動手實踐來深入理解Dubbo的實現原理.
Dubbo的實現原理
RPC調用的過程
我們先來看下RPC調用的過程.
- 服務容器負責啓動,加載,運行服務提供者。
- 服務提供者在啓動時,向註冊中心註冊自己提供的服務。
- 服務消費者在啓動時,向註冊中心訂閱自己所需的服務。
- 註冊中心返回服務提供者地址列表給消費者,如果有變更,註冊中心將基於長連接推送變更數據給消費者。
- 服務消費者,從提供者地址列表中,基於軟負載均衡算法,選一臺提供者進行RPC調用,如果調用失敗,再選另一臺調用。
- 服務消費者和提供者,在內存中累計調用次數和調用時間,定時每分鐘發送一次統計數據到監控中心。
RPC調用的原理
RPC調用的原理是: 動態代理, 反射, 網絡傳輸.
- 消費者從註冊中心獲取到服務提供者的地址後, 與服務提供者建立TCP連接.
- 消費者將服務的全限定類名(String), 方法名(String), 方法參數類型(Class[]), 方法參數(Object[]), 通過TCP傳輸給服務提供者.
- 服務提供者獲取到這些數據後, 通過反射調用對應服務的方法, 然後將執行結果通過TCP返回給服務消費者.
- 整個RPC調用過程被封裝到動態代理中, 對用戶來說是透明的.
Dubbo架構
Dubbo框架設計分爲十層:
- service 服務層, 爲服務提供者和服務消費者提供接口.
- config 配置層, 提供dubbo的各種配置.
- proxy 服務接口透明代理, 生成動態代理.
- registry 註冊中心層, 負責服務的註冊與發現.
- cluster 路由層, 封裝多個提供者的路由及負載均衡.
- monitor 監控層, RPC調用次數和調用時間監控.
- protocol 遠程調用層, 封裝 RPC 調用.
- exchange 信息交換層, 封裝請求響應模式, 同步轉異步.
- transport 網絡傳輸層, 抽象 mina 和 netty 爲統一接口.
- serialize 數據序列化層, 提供數據序列化的接口.
手寫簡化版的Bubbo框架
我們根據Dubbo的框架設計來手寫一個簡化版的Dubbo, 其中序列化協議使用Java原生的Serializable, 網絡傳輸協議使用原生的TCP, 負載均衡使用隨機算法, 註冊中心使用ZooKeeper, 動態代理使用JDK Proxy.
服務提供者
(1) ZooKeeper常量
定義了ZooKeeper的地址和Dubbo註冊中心的根節點路徑.
/**
* @author litianxiang
* @date 2020/3/17 11:45
*/
public class ZooKeeperConst {
/**
* ZooKeeper的地址
*/
public static String host = "xxx.xx.xx.xxx:2181";
/**
* Dubbo在ZooKeeper上的根節點
*/
public static String rootNode = "/dubbo";
}
(2) 註冊中心
這裏使用ZooKeeper來實現註冊中心, 將服務及服務提供者地址註冊到註冊中心.
/**
* @author litianxiang
* @date 2020/3/17 11:28
*/
public class RegisterCenter {
private static Logger logger = LoggerFactory.getLogger(RegisterCenter.class);
private ZooKeeper zk;
/**
* 連接ZooKeeper, 創建dubbo根節點
*/
public RegisterCenter() {
try {
CountDownLatch connectedSignal = new CountDownLatch(1);
zk = new ZooKeeper(ZooKeeperConst.host, 5000, new Watcher() {
@Override
public void process(WatchedEvent event) {
if (event.getState() == Event.KeeperState.SyncConnected) {
connectedSignal.countDown();
}
}
});
//因爲監聽器是異步操作, 要保證監聽器操作先完成, 即要確保先連接上ZooKeeper再返回實例.
connectedSignal.await();
//創建dubbo註冊中心的根節點(持久節點)
if (zk.exists(ZooKeeperConst.rootNode, false) == null) {
zk.create(ZooKeeperConst.rootNode, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
} catch (Exception e) {
logger.error("connect zookeeper server error.", e);
}
}
/**
* 將服務和服務提供者URL註冊到註冊中心
* @param serviceName 服務名稱
* @param serviceProviderAddr 服務所在TCP地址
*/
public void register(String serviceName, String serviceProviderAddr) {
try {
//創建服務節點
String servicePath = ZooKeeperConst.rootNode + "/" + serviceName;
if (zk.exists(servicePath, false) == null) {
zk.create(servicePath, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
//創建服務提供者節點
String serviceProviderPath = servicePath + "/" + serviceProviderAddr;
if (zk.exists(serviceProviderPath, false) == null) {
zk.create(serviceProviderPath, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
}
logger.info("服務註冊成功, 服務路徑: " + serviceProviderPath);
} catch (Exception e) {
logger.error("註冊中心-註冊服務報錯", e);
}
}
}
(3) 接口全限定類名, 方法名, 方法參數類型, 方法參數的包裝類
這裏爲了簡單, 使用Java自帶的序列化協議.
/**
* 封裝接口名, 方法名, 參數字節碼數組, 參數對象
*/
public class Invocation implements Serializable {
private static final long serialVersionUID = -2798340582119604989L;
/**
* 接口名
*/
private String interfaceName;
/**
* 方法名
*/
private String methodName;
/**
* 參數字節碼數組
*/
private Class[] paramTypes;
/**
* 參數對象
*/
private Object[] params;
public Invocation(String interfaceName, String methodName, Class[] paramTypes, Object[] params) {
this.interfaceName = interfaceName;
this.methodName = methodName;
this.paramTypes = paramTypes;
this.params = params;
}
public String getInterfaceName() {
return interfaceName;
}
public void setInterfaceName(String interfaceName) {
this.interfaceName = interfaceName;
}
public String getMethodName() {
return methodName;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
public Class[] getParamTypes() {
return paramTypes;
}
public void setParamTypes(Class[] paramTypes) {
this.paramTypes = paramTypes;
}
public Object[] getParams() {
return params;
}
public void setParams(Object[] params) {
this.params = params;
}
}
(4) RPC監聽服務
用來監聽Consumer遠程調用的TCP連接, 接收到Consumer傳輸過來的數據後, 通過反射調用對應的方法, 然後將結果返回給Consumer. Dubbo使用的是Netty框架, 這裏爲了簡單, 我們使用原生的TCP連接.
/**
* RPC監聽服務, 監聽consumer遠程調用的tcp連接
* @author litianxiang
* @date 2020/3/17 18:01
*/
public class RpcServer {
private static Logger logger = LoggerFactory.getLogger(RpcServer.class);
private Map<String, Class> serviceMap;
public RpcServer(Map<String, Class> serviceMap) {
this.serviceMap = serviceMap;
}
/**
* 啓動RPC監聽服務
*/
public void start() {
//監聽端口, 處理rpc請求
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(12000);
logger.info("RPC監聽服務啓動...");
while (true) {
Socket socket = serverSocket.accept();
new Thread(new ServerHandler(socket, serviceMap)).start();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
/**
* 處理RPC, 通過反射執行方法
* @author litianxiang
* @date 2020/3/6 17:52
*/
public class ServerHandler implements Runnable {
private Socket socket;
private Map<String, Class> serviceMap;
public ServerHandler(Socket socket, Map<String, Class> serviceMap) {
this.socket = socket;
this.serviceMap = serviceMap;
}
@Override
public void run() {
ObjectInputStream in = null;
ObjectOutputStream out = null;
try {
in = new ObjectInputStream(socket.getInputStream());
out = new ObjectOutputStream(socket.getOutputStream());
//獲取Invocation對象
Invocation invocation = (Invocation) in.readObject();
//執行對應方法
Class clazz = serviceMap.get(invocation.getInterfaceName());
Method method = clazz.getMethod(invocation.getMethodName(), invocation.getParamTypes());
Object invoke = method.invoke(clazz.newInstance(), invocation.getParams());
//返回方法執行結果
out.writeObject(invoke);
out.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (out != null) {
try {
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
socket = null;
}
}
}
(5) Provider啓動類
這裏會模擬dubbo的service配置, 將接口名及其對應的實現類儲存到serviceMap中, 然後將服務和服務提供者地址註冊到註冊中心, 最後再啓動對Consumer遠程調用的監聽.
/**
* @author litianxiang
* @date 2020/3/6 15:32
*/
public class Provider {
private static Logger logger = LoggerFactory.getLogger(Provider.class);
private static Map<String, Class> serviceMap = new HashMap<>();
private static String tcpHost = "127.0.0.1:12000";
static {
/**
* 模擬service配置處理邏輯
* <dubbo:service interface="com.client.service.IBookService" ref="bookService" />
* <bean id="bookService" class="com.provider.service.BookServiceImpl" />
*/
serviceMap.put(IBookService.class.getName(), BookServiceImpl.class);
}
public static void main(String[] args) {
//將服務和服務提供者URL註冊到註冊中心
RegisterCenter registerCenter = new RegisterCenter();
for (Map.Entry<String, Class> entry : serviceMap.entrySet()) {
registerCenter.register(entry.getKey(), tcpHost);
}
//監聽Consumer的遠程調用(爲了簡化代碼, 這裏使用TCP代替Netty)
RpcServer rpcServer = new RpcServer(serviceMap);
rpcServer.start();
}
}
服務消費者
(1) 負載均衡
爲了簡單, 這裏直接使用的是隨機算法.
public class RandomLoadBalance {
/**
* 隨機一個provider
* @param providerList provider列表
* @return provider
*/
public String doSelect(List<String> providerList) {
int size = providerList.size();
Random random = new Random();
return providerList.get(random.nextInt(size));
}
}
(2) 服務訂閱類
服務訂閱類提供向註冊中心訂閱服務的功能, 涉及服務發現與負載均衡.
public class ServiceSubscribe {
private static Logger logger = LoggerFactory.getLogger(ServiceSubscribe.class);
private ZooKeeper zk;
private List<String> providerList;
/**
* 連接ZooKeeper, 創建dubbo根節點
*/
public ServiceSubscribe() {
try {
CountDownLatch connectedSignal = new CountDownLatch(1);
zk = new ZooKeeper(ZooKeeperConst.host, 5000, new Watcher() {
@Override
public void process(WatchedEvent event) {
if (event.getState() == Event.KeeperState.SyncConnected) {
connectedSignal.countDown();
}
}
});
//因爲監聽器是異步操作, 要保證監聽器操作先完成, 即要確保先連接上ZooKeeper再返回實例.
connectedSignal.await();
//創建dubbo註冊中心的根節點(持久節點)
if (zk.exists(ZooKeeperConst.rootNode, false) == null) {
zk.create(ZooKeeperConst.rootNode, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
} catch (Exception e) {
logger.error("connect zookeeper server error.", e);
}
}
/**
* 在註冊中心訂閱服務, 返回對應的服務url
* 只要第一次獲取到了服務的RPC地址, 後面註冊中心掛掉之後, 仍然可以繼續通信.
* @param serviceName 服務名稱
* @return 服務host
*/
public String subscribe(String serviceName) {
//服務節點路徑
String servicePath = ZooKeeperConst.rootNode + "/" + serviceName;
try {
//獲取服務節點下的所有子節點, 即服務的RPC地址
providerList = zk.getChildren(servicePath, new Watcher() {
@Override
public void process(WatchedEvent event) {
if (event.getType() == Event.EventType.NodeChildrenChanged) {
try {
//循環監聽
providerList = zk.getChildren(servicePath, true);
} catch (KeeperException | InterruptedException e) {
logger.error("Consumer在ZooKeeper訂閱服務-註冊監聽器報錯", e);
}
}
}
});
} catch (Exception e) {
logger.error("從註冊中心獲取服務報錯.", e);
}
logger.info(serviceName + "的服務提供者列表: " + providerList);
//負載均衡
RandomLoadBalance randomLoadBalance = new RandomLoadBalance();
return randomLoadBalance.doSelect(providerList);
}
}
(3) RPC代理類
根據JDK Proxy生成一個代理對象, 封裝RPC調用的過程.
public class RpcServiceProxy {
private ServiceSubscribe serviceSubscribe;
public RpcServiceProxy(ServiceSubscribe serviceSubscribe) {
this.serviceSubscribe = serviceSubscribe;
}
/**
* 獲取RPC代理
* @param clazz
* @return
*/
public Object getProxy(final Class clazz) {
return Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//在註冊中心訂閱服務, 返回對應的服務url
String rpcHost = serviceSubscribe.subscribe(clazz.getName());
String[] split = rpcHost.split(":");
//與遠程服務建立連接
Socket socket = new Socket(split[0], Integer.parseInt(split[1]));
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
//向RPC服務傳輸Invocation對象
String className = clazz.getName();
String methodName = method.getName();
Class[] paramTypes = method.getParameterTypes();
Invocation invocation = new Invocation(className, methodName, paramTypes, args);
out.writeObject(invocation);
out.flush();
//接收方法執行結果
Object object = in.readObject();
in.close();
out.close();
socket.close();
return object;
}
});
}
}
(4) Consumer啓動類
消費者啓動後, 會向註冊中心訂閱服務, 經過負載均衡獲取到對應的服務後, 再進行RPC調用.
public class Consumer {
private static Logger logger = LoggerFactory.getLogger(Consumer.class);
public static void main(String[] args) {
//在註冊中心訂閱服務, 獲取服務所在的url, 然後通過代理遠程調用服務
ServiceSubscribe serviceSubscribe = new ServiceSubscribe();
RpcServiceProxy rpcServiceProxy = new RpcServiceProxy(serviceSubscribe);
//獲取RPC代理
IBookService bookService = (IBookService) rpcServiceProxy.getProxy(IBookService.class);
BookDTO bookInfo = bookService.getBookInfo(1);
System.out.println(bookInfo);
}
}
測試
(1) 先修改註冊中心的地址
public static String host = "xxx.xx.xx.xxx:2181";
(2) Service
public class BookDTO implements Serializable{
private static final long serialVersionUID = 1934175717377394706L;
private int id;
private String name;
private String desc;
private String author;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
@Override
public String toString() {
return "BookDTO{" +
"id=" + id +
", name='" + name + '\'' +
", desc='" + desc + '\'' +
", author='" + author + '\'' +
'}';
}
}
public interface IBookService {
BookDTO getBookInfo(int id);
}
public class BookServiceImpl implements IBookService {
@Override
public BookDTO getBookInfo(int id) {
if (id == 1) {
BookDTO bookDTO = new BookDTO();
bookDTO.setId(1);
bookDTO.setName("仙逆");
bookDTO.setDesc("順爲凡, 逆爲仙, 只在心中一念間.");
bookDTO.setAuthor("耳根");
return bookDTO;
} else {
return new BookDTO();
}
}
}
(3) 啓動Provider
(4) 啓動Consumer