前言
關於Dubbo入門的網上教程也特別多,因此我沒有專門出關於Dubbo的系列博文(主要呢… 也是在忙些工作上的事兒),用Dubbo特別簡單,但是想要把Dubbo學好,學精還得花費不少時間的,特別是Dubbo的源碼是非常值得研究的!有時間會再出一套關於Dubbo的源碼解析,那麼本篇博文呢,咱也不講別的,主要是帶各位看看Dubbo最核心的部分,也就是遠程調用是如何實現的,如果這個給弄明白了,那麼你對Dubbo的認知會更上一層樓!
如果是初學Dubbo的朋友建議不要往下看啦!以下代碼純本人手寫,可能會有寫的不嚴謹的地方,還請見諒啦!也歡迎各位指出不足~
Dubbo原理分析
好啦,廢話咱也不多說,大家也知道Dubbo本質上就是使用JAVA開發的高性能RPC(Remote Procedure Call)框架,它是一種通過網絡從遠程計算機程序上請求服務,而不需要了解底層網絡技術的協議,當然這麼說很籠統,很官方,也不好理解,那如何去理解這個RPC框架是幹啥的呢?
還是舉個栗子吧!也比方說以前呢,你一個單身狗(我不是針對在場的各位… 哈哈哈)在家,會自己給自己做飯吃,自給自足,做好了飯,自己就可以吃上了,這就是本地調用。
那有一天,你談了個女朋友,女朋友在外面和閨蜜們逛街,肚子餓了,想回家就吃上飯,就call你,“晚上我回家吃飯哦”,那你不得屁顛屁顛的去買菜給準備準備,那麼這種情況對於你女朋友來說就是遠程調用!
懂了什麼是遠程調用,我們再來看這張Dubbo的基本架構圖
首先Provider也就是服務的提供者會先運行,然後將服務註冊到註冊中心,這裏的註冊中心通常指的是Zookeeper,當Provider運行之後,會在Provider項目的Spring容器中生成相對應的服務對象,注意了!Provider和Consumer分別是在不同的JVM上運行的,因此Consumer是無法直接引用Provider中的對象的,這就是爲什麼我們要使用RPC框架來調用對象中的方法的根本原因了!
如上圖,當Provider將AddServiceImpl服務註冊到註冊中心之後,其實並不是把這個對象傳輸給Zookeeper,而是註冊了這個服務的 “身份信息”,我們通過 “身份信息” 可以找到這個方法, “身份信息” 具體包含哪些內容呢?
我這裏就先賣個關子… 哈哈哈,下面手寫框架會重點講到。
當AddServiceImpl服務的信息被註冊之後,Consumer會向註冊中心訂閱這個服務,此時Consumer就相當於是擁有了AddServiceImpl服務的所有信息,你可以把Consumer理解成你女朋友,Provider就是你了,你們倆約定了,做飯的事情你來,那麼就好比在註冊中心註冊了 “做飯” 這個服務,當然可能還有其他的特殊服務我就不多說了… 當服務註冊之後,接下來女朋友想去 “調用” 你去做飯,就只用打電話傳呼你了,此時的電話號碼就是服務的註冊信息(當然這麼說可能有些牽強,大致是這個意思啦!) 。
手寫RPC框架
講了這麼多,主要是讓大家熟悉一下Dubbo是如何工作的,以及核心功能是什麼?接下來就進入今天的正題了,此次手寫RPC框架,包含以下4個項目。
rpc
是父項目,父項目怎麼創建我就不多贅述啦,其餘的幾個項目都是子項目(Maven Moduel),rpc-core
是核心項目,包含RPC調用的核心代碼,rpc-myself
扮演consumer的角色,rpc-roommate
則扮演的是provider的角色,爲了便於大家理解,我這裏就以考試爲例。
相信這麼多年了,大家或多或少會有些抄襲的獨門看家本領吧!(別說沒有!我不信!) 那麼此次呢,你將扮演的就是成績很差的 “菜比”,而你的室友呢扮演的就是學霸的角色,你答應學霸,考試過了,就請吃飯!但前提是我這次複試你一定要把答案發給我,手機一定要保持開機狀態,並且要一直盯着手機看!這次考試能不能過就看大神的了…
學霸一般都是老實人,就答應了,故事就這麼開始了…
第一步:創建rpc父項目
注意要創建Maven的pom項目
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.marco</groupId>
<artifactId>rpc</artifactId>
<version>1.0</version>
<packaging>pom</packaging>
<name>遠程調用</name>
<description>遠程調用</description>
<modules>
<module>rpc-roommate</module>
<module>rpc-myself</module>
<module>rpc-domain</module>
</modules>
</project>
第二步:創建rpc-core項目
rpc-core
項目包含手寫Dubbo框架的核心代碼,這裏先簡單介紹一下這個包裏面的各個包的作用吧。anno
是註解包,coder
是用於對象的二進制字節碼的轉化,domain
就不多解釋啦,關於這裏面的兩個類我們下面會詳細做介紹,invoker
專門處理方法的調用,包含本地方法的調用,以及遠程方法的調用,net
主要用於網絡傳輸,proxy
則存放的代理類,service
嘛主要是模擬服務接口。
rpc-core
的內容就是上面這些啦!先看簡單的anno註解包吧。
anno
@Expose
實則就是Dubbo中的@Service
,@Reference
含義不變。@Expose
作用於類上,@Reference
則作用於屬性上。
package com.marco.anno;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@Retention(RUNTIME)
@Target(ElementType.TYPE)
public @interface Expose {
}
package com.marco.anno;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@Retention(RUNTIME)
@Target(ElementType.FIELD)
public @interface Reference {
}
coder
coder包的主要用途是爲了將對象轉換爲二進制流,以及將二進制流轉爲對象,方便對象在網絡上進行傳輸。
package com.marco.coder;
import java.io.Serializable;
public interface Coder {
/**
* 將對象編碼爲字節碼
* @param object
* @return
*/
byte[] code(Serializable object);
/**
* 將字節數組轉換爲對象
* @return
*/
Object decode(byte[] bt);
}
JDKCoder
是Coder
的實現類,這裏我們使用的還是最傳統的JDK序列化方式傳輸對象,但是此種方式在實際開發中並不推薦,我們可以使用Hessian、Jackson…等等序列化框架優化對象的序列化,使之序列化後的二進制流成倍的縮小,提升傳輸的速度,當然,這裏我們就不去引用別的框架啦!
package com.marco.coder;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class JDKCoder implements Coder {
@Override
public byte[] code(Serializable object) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
// 將對象轉爲字節,寫入到內存中並返回
oos.writeObject(object);
//爲了避免客戶端阻塞,因此採取在二進制流的最末尾設置標記(-1)的措施來規避此問題發生
oos.writeByte(-1);
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
public Object decode(byte[] bt) {
try (ByteArrayInputStream bais = new ByteArrayInputStream(bt);
ObjectInputStream ois = new ObjectInputStream(bais)) {
// 將對象寫入到內存中並返回
Object object = ois.readObject();
return object;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
domain
上面咱們講到,當Provider將AddServiceImpl服務註冊到註冊中心之後,其實並不是把這個對象傳輸給Zookeeper,而是註冊了這個服務的 “身份信息”,我們通過 “身份信息” 可以找到這個方法,這個 “身份信息” 其實就是下面的Request對象(包含接口名稱,方法名稱,傳入的參數信息),當然還包含你需要請求的服務註冊在Zookeeper裏的主機的IP地址和port端口號。
Reponse對象主要是用於回覆信息給發送請求的消費端。
invoker
定義MethodInvoker接口,實現類爲本地調用對象LocalInvoker,和遠程調用對象RPCInvoker
package com.marco.invoker;
import com.marco.domain.Request;
import com.marco.domain.Response;
public interface MethodInvoker {
Response invoker(Request request);
}
package com.marco.invoker;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import com.marco.domain.Request;
import com.marco.domain.Response;
import com.marco.spring.provider.RPCContextAware;
/**
* 本地調用
* @author Marco
*
*/
public class LocalInvoker implements MethodInvoker {
/**
* 用於緩存Class<?>對象
*/
private static Map<String,Class<?>> classCache = new HashMap<>();
@Override
public Response invoker(Request request) {
//獲取request對象中本地調用的參數信息
String interfaceName = request.getInterfaceName();
String methodName = request.getMethod();
Object[] args = request.getArgs();
//獲取被調用接口的實現類的完全限定名
String implClassName = getImplClassName(interfaceName);
Object answer = null;
String msg = "success";
Class<?> clazz = null;
try {
//通過反射獲取到該實現類的Class對象
if(classCache.containsKey(interfaceName)) {
clazz = classCache.get(interfaceName);
} else {
clazz = Class.forName(implClassName);
classCache.put(interfaceName,clazz);
}
Object obj = RPCContextAware.getBean(clazz);
//將被調用方法的參數封裝到paramTypes數組中
Class<?>[] parameterTypes = new Class<?>[args.length];
for (int i = 0; i < args.length; i++) {
parameterTypes[i] = args[i].getClass();
}
//通過反射原理獲取被調用方法的Method對象
Method method = clazz.getMethod(methodName, parameterTypes);
//通過invoke方法獲取最終的結果並返回結果
answer = method.invoke(obj,args);
} catch (Exception e) {
msg = e.getMessage();
e.printStackTrace();
}
return new Response(answer, msg);
}
/**
* 獲取被調用接口的實現類的完全限定名,例如將com.marco.service.UserService
* 轉化爲com.marco.service.impl.UserServiceImpl
* @param interfaceName
* @return
*/
private String getImplClassName(String interfaceName) {
int lastIndexOf = interfaceName.lastIndexOf(".");
String simpleClassName = interfaceName.substring(lastIndexOf);
return interfaceName.replaceAll(simpleClassName, ".impl"+simpleClassName+"Impl");
}
}
package com.marco.invoker;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import com.marco.coder.Coder;
import com.marco.coder.JDKCoder;
import com.marco.domain.Request;
import com.marco.domain.Response;
import com.marco.loadbalance.LoadBalance;
import com.marco.loadbalance.RandomLoadBalance;
import com.marco.net.ConsumerNet;
import com.marco.net.client.ConsumerNetImpl;
import com.marco.register.ServerDiscovery;
import com.marco.register.impl.ServerDiscoveryImpl;
public class RPCInvoker implements MethodInvoker {
private ConsumerNet consumerNet = new ConsumerNetImpl();
private Coder coder = new JDKCoder();
private LoadBalance loadBalance = new RandomLoadBalance();
private ServerDiscovery discovery = new ServerDiscoveryImpl();
@Override
public Response invoker(Request request) {
//獲取服務中所有的服務提供者
List<String> servers = discovery.discovery(request.getInterfaceName());
// 連接提供者
String select = loadBalance.select(servers);
String host = select.split(":")[0];
Integer port = Integer.valueOf(select.split(":")[1]);
consumerNet.connect(host, port);
byte[] result = null;
try (OutputStream outputStream = consumerNet.getOutputStream();
BufferedOutputStream bos = new BufferedOutputStream(outputStream);
InputStream inputStream = consumerNet.getInputStream();
BufferedInputStream bis = new BufferedInputStream(inputStream)) {
// 將request對象轉成字節(二進制)並寫出
byte[] code = coder.code(request);
bos.write(code);
bos.flush();
byte[] by = new byte[1024];
int len = 0;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ((len = bis.read(by)) != -1) {
baos.write(by, 0, len);
// 爲了避免客戶端阻塞,因此採取在最末尾設置標記(-1)的措施規避此問題發生
byte lastByte = by[len - 1];
if (lastByte == -1) {
break;
}
}
result = baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
// 將獲取到的字節流轉爲對象
Object decode = coder.decode(result);
return (Response) decode;
}
}
net
net包下的類,主要是封裝了客戶端和服務端的遠程網絡訪問(Socket)方法,方便調用,當然Dubbo的底層肯定不是使用原生的Socket來進行遠程通信,而是使用Netty框架來實現通信,畢竟Socket的效率實在是太低啦!
關於Netty的原理和解析大家可以參考博文 Netty高性能原理和框架架構解析
package com.marco.net;
import java.io.InputStream;
import java.io.OutputStream;
public interface ConsumerNet {
/**
* 連接提供者
* @param ip
* @param port
*/
void connect(String ip, int port);
/**
* 輸入
* @return
*/
InputStream getInputStream();
/**
* 輸出
* @return
*/
OutputStream getOutputStream();
}
package com.marco.net.client;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import com.marco.net.ConsumerNet;
public class ConsumerNetImpl implements ConsumerNet {
private Socket client;
@Override
public void connect(String ip, int port) {
try {
//創建Socket客戶對象
client = new Socket(ip, port);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public InputStream getInputStream() {
InputStream inputStream = null;
try {
inputStream = client.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
return inputStream;
}
@Override
public OutputStream getOutputStream() {
OutputStream outputStream = null;
try {
outputStream = client.getOutputStream();
} catch (IOException e) {
e.printStackTrace();
}
return outputStream;
}
}
package com.marco.net.server;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import com.marco.net.ProviderNet;
public class ProviderNetImpl implements ProviderNet {
private ServerSocket serverSocket = null;
private Socket server;
public ProviderNetImpl(int port) {
//創建Socket服務對象
try {
serverSocket = new ServerSocket(port);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void listen() {
try {
//開啓監聽
server = serverSocket.accept();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public InputStream getInputStream() {
InputStream inputStream = null;
try {
inputStream = server.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
return inputStream;
}
@Override
public OutputStream getOutputStream() {
OutputStream outputStream = null;
try {
outputStream = server.getOutputStream();
} catch (IOException e) {
e.printStackTrace();
}
return outputStream;
}
}
proxy
定義遠程調用代理類接口RPCProxy
package com.marco.proxy;
/**
* 遠程調用代理
* @author Marco
*
*/
public interface RPCProxy{
/**
* 獲取代理對象
* @param <T>
* @param clazz
* @return
*/
<T> T getProxy(Class<T> clazz);
}
創建代理類RPCObjectProxy
實現RPCProxy
接口,當消費者執行方法,例如addService.add(1,2)
的時候會調用invoke方法,進而通過遠程調用獲取執行完成之後的結果。
package com.marco.proxy;
import java.lang.reflect.Proxy;
import com.marco.domain.Request;
import com.marco.invoker.MethodInvoker;
public class RPCObjectProxy implements RPCProxy {
private MethodInvoker methodInvoker;
public RPCObjectProxy(MethodInvoker methodInvoker) {
super();
this.methodInvoker = methodInvoker;
}
@SuppressWarnings("unchecked")
@Override
public <T> T getProxy(Class<T> clazz) {
return (T) Proxy.newProxyInstance(
RPCObjectProxy.class.getClassLoader(),
new Class<?>[] { clazz },
(proxy, method, args) -> {
//創建請求對象
Request request = new Request();
request.setArgs(args);
request.setInterfaceName(clazz.getName());
request.setMethod(method.getName());
return methodInvoker.invoker(request).getAnswer();
});
}
}
register
註冊和發現服務的接口類ServerRegister
、ServerDiscovery
package com.marco.register;
import java.util.List;
public interface ServerDiscovery {
List<String> discovery(String serverName);
}
package com.marco.register;
public interface ServerRegister {
void register(String serverName, String serverPath);
}
以上接口的實現類ServerRegisterImpl
、ServerDiscoveryImpl
,顧名思義,用途就是方法的註冊和方法的發現(引用)。
package com.marco.register.impl;
import com.marco.register.ServerRegister;
import com.marco.utils.ZookeeperUtils;
public class ServerRegisterImpl implements ServerRegister {
@Override
public void register(String serverName, String serverPath) {
ZookeeperUtils.addEphemeralNode(serverName, serverPath);
}
}
package com.marco.register.impl;
import java.util.List;
import com.marco.register.ServerDiscovery;
import com.marco.utils.ZookeeperUtils;
public class ServerDiscoveryImpl implements ServerDiscovery {
@Override
public List<String> discovery(String serverName) {
return ZookeeperUtils.getSubNode(serverName);
}
}
utils
大家也發現了,上面的ServerRegisterImpl
以及ServerDiscoveryImpl
本質上就是調用了ZookeeperUtils
中的addEphemeralNode()
創建臨時節點和getSubNode()
獲取子節點的方法,ZookeeperUtils
中封裝了我們手寫Dubbo中所有的關於和Zookeeper
集成的操作。
並且使用一個簡單的 “緩存” serverCache,將被暴露並註冊在Zookeeper的服務進行緩存處理,適當的提高了框架的性能,但是需要注意的一點是,當Provider服務端因爲某種原因,導致服務停止之後,Zookeeper會監控到這個異常,並且會通知到Consumer,那麼此時我們的緩存serverCache就必須得更新或者刪除節點得信息。
否則,如果不更新緩存,假設當Provider的服務子節點有更新,從localhost:8888
變成localhost:9999
,那麼此時我們不更新緩存讀出來的數據依然是localhost:8888
,此時我們根據這個 “身份信息” 再次去連接服務端時,必定是會報 java.net.ConnectException: Connection refused
等連接異常的。
因此在以下得代碼中,我通過zkClient.subscribeChildChanges()
的監聽子節點方法,解決了緩存的髒數據讀取問題。
package com.marco.utils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.I0Itec.zkclient.ZkClient;
/**
* 操作一: zk 創建持久節點
* 操作二: zk 創建臨時節點
* 操作三: zk 獲取父節點裏面的子節點
* 操作四: zk 訂閱節點
* @author Marco
*
*/
public class ZookeeperUtils {
/**
* Zookeeper的連接地址
*/
private static final String ZK_URL = "127.0.0.1:2181";
/**
* 聲明ZkClient
*/
private static ZkClient zkClient = null;
/**
* 緩存,key爲服務名稱,value爲該服務下的類的全類名
*/
private static Map<String,List<String>> serverCache = new HashMap<String,List<String>>();
/**
* 初始化ZkClient
*/
static {
zkClient = new ZkClient(ZK_URL, 5*1000, 20*1000);
}
/**
* 創建持久節點
* @param nodeName
*/
public static void addPersistentNode(String nodeName) {
if(!zkClient.exists("/" + nodeName)) {
//若節點不存在,則創建新的節點
zkClient.createPersistent("/" + nodeName);
}
System.out.println("節點創建完成");
}
/**
* 創建臨時節點
* @param parentNodeName
* @param nodeName
*/
public static void addEphemeralNode(String parentNodeName, String nodeName) {
//若父節點不存在,則創建新的節點
addPersistentNode(parentNodeName);
zkClient.createEphemeral("/" + parentNodeName + "/" + nodeName);
System.out.println("臨時節點創建完成");
/*try {
System.in.read();
} catch (IOException e) {
e.printStackTrace();
}*/
}
/**
* 獲取子節點
* @param nodeName
* @return
*/
public static List<String> getSubNode(String serverName) {
//如果緩存中有serverName的服務,則直接從緩存中獲取
if(serverCache.containsKey(serverName)) {
return serverCache.get(serverName);
}
if(!zkClient.exists("/" + serverName)) {
throw new RuntimeException("沒有" + serverName + "的服務提供者");
}
List<String> servers = zkClient.getChildren("/" + serverName);
//如果緩存中沒有serverName的服務,則存入緩存中
serverCache.put(serverName, servers);
/**
* 採用節點訂閱的方式,解決節點緩存的數據髒讀問題
*/
zkClient.subscribeChildChanges("/" + serverName, (parentPath, currentChilds) -> {
System.out.println("已解決緩存的髒讀問題!");
serverCache.put(serverName, currentChilds);
});
return servers;
}
}
spring
接下來就是我們的集成spring的包,可以算是咱們核心包中的核心了,直接亮出代碼吧!
在ReferenceBeanContext
類中,我們實現了spring的BeanPostProcessor
接口,並重寫postProcessBeforeInitialization()
和postProcessAfterInitialization()
,這兩個方法很類似,但是依然有區別,前者是在bean實例化,依賴注入之後及自定義初始化方法(例如:配置文件中bean標籤添加init-method
屬性指定Java類中初始化方法、@PostConstruct
註解指定初始化方法,Java類實現InitailztingBean
接口)之前調用。
而後置處理器的postProcessorAfterInitailization
方法是在bean實例化、依賴注入及自定義初始化方法之後調用,實現BeanPostProcessor
並借用此方法的主要目的是爲了查找IoC容器中的是否擁有屬性上設置@Reference
註解的對象,如果有,則獲取這個屬性的名稱,得到它的接口,並從IoC容器中取出這個和接口對應的實現類對象,而這個實現類對象,恰巧是我們使用@Expose
(等同於Dubbo中的@Service
)註解暴露並註冊的服務類對象。
package com.marco.spring.consumer;
import java.lang.reflect.Field;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
import com.marco.anno.Reference;
import com.marco.invoker.RPCInvoker;
import com.marco.proxy.RPCObjectProxy;
import com.marco.proxy.RPCProxy;
@Component
public class ReferenceBeanContext implements BeanPostProcessor{
private RPCProxy rpcProxy = new RPCObjectProxy(new RPCInvoker());
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
Field[] declaredFields = bean.getClass().getDeclaredFields();
if(null != declaredFields && declaredFields.length > 0) {
for (Field field : declaredFields) {
//檢查該屬性上是否有@Reference註解
Reference reference = field.getAnnotation(Reference.class);
//如果屬性上有@Reference註解,則將代理對象注入給該屬性
if(null != reference) {
System.out.println(beanName + "類中的屬性" + field.getName() + "上有" + reference + "註解");
//設置強制注入
field.setAccessible(true);
Class<?> clazz = field.getType();
//獲取代理對象
Object proxy = rpcProxy.getProxy(clazz);
//注入代理對象
try {
field.set(bean, proxy);
} catch (IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
return bean;
}
}
package com.marco.spring.provider;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import com.marco.anno.Expose;
import com.marco.coder.Coder;
import com.marco.coder.JDKCoder;
import com.marco.domain.Request;
import com.marco.domain.Response;
import com.marco.invoker.LocalInvoker;
import com.marco.invoker.MethodInvoker;
import com.marco.net.ProviderNet;
import com.marco.net.server.ProviderNetImpl;
import com.marco.register.ServerRegister;
import com.marco.register.impl.ServerRegisterImpl;
@Component
public class RPCContextAware implements ApplicationContextAware, InitializingBean{
/**
* IoC容器對象
*/
private static ApplicationContext applicationContext = null;
/**
* 用於保存被暴露的服務
*/
private Map<String,String> exposedServers = new HashMap<String, String>();
/**
* 用於將服務註冊到註冊表中
*/
private ServerRegister register = new ServerRegisterImpl();
/**
* 用於編碼、解碼
*/
private Coder coder = new JDKCoder();
/**
* 本地調用處理器
*/
private MethodInvoker methodInvoker = new LocalInvoker();
/**
* 需要監聽端口,默認不設置則爲6666
*/
@Value("${port:6666}")
private int port;
/**
* 提供服務者的網絡連接裝置
*/
private ProviderNet providerNet;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
/**
* 在IoC容器中搜索類上有Expose註解的類
* key 類的名稱,如addServiceImpl
* value 對象
*/
RPCContextAware.applicationContext = applicationContext;
Map<String, Object> beans = applicationContext.getBeansWithAnnotation(Expose.class);
if(!beans.isEmpty()) {
beans.forEach((k,v) -> {
Class<?>[] interfaces = v.getClass().getInterfaces();
//添加需要被暴露的服務
exposedServers.put(interfaces == null ? v.getClass().getName():interfaces[0].getName(), "localhost:" + port);
});
}
System.out.println("需要註冊的服務有" + exposedServers);
}
@Override
public void afterPropertiesSet() throws Exception {
/**
* 將暴露的服務註冊到Zookeeper中
*/
exposedServers.forEach((k,v) -> {
register.register(k, v);
System.out.println(k + "服務已經註冊成功,暴露服務的地址是" + v);
});
listen();
}
private void listen() {
System.out.println("*************監聽就緒***************");
providerNet = new ProviderNetImpl(port);
// 循環監聽port端口(等待菜比隊友的通知),解決服務端被請求一次就死機的問題
while(true) {
providerNet.listen();
// 當收到消息之後,監聽中止,取消阻塞狀態
Request request = null;
System.out.println("收到菜比室友的題目了...");
try (InputStream inputStream = providerNet.getInputStream();
BufferedInputStream bis = new BufferedInputStream(inputStream);
OutputStream outputStream = providerNet.getOutputStream();
BufferedOutputStream bos = new BufferedOutputStream(outputStream)) {
byte[] by = new byte[1024];
int len = 0;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ((len = bis.read(by)) != -1) {
baos.write(by, 0, len);
//字節數組長度沒有1024表示這是最後一次讀取數據了
if(len < 1024) {
break;
}
}
// 獲取request對象,通過coder的decode解碼方法將二進制流轉爲Request對象
request = (Request) coder.decode(baos.toByteArray());
System.out.println("題目爲" + request.toString());
// 執行invoker方法獲取結果
Response answer = methodInvoker.invoker(request);
System.out.println("計算出來的答案爲" + answer);
bos.write(coder.code(answer));
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 通過clazz從IoC容器中獲取已經被創建出來的對象
* @param clazz
* @return
*/
public static Object getBean(Class<?> clazz) {
return applicationContext.getBean(clazz);
}
}
loadbalance
loadbalance包主要是爲了實現負載均衡,目前只添加了Random隨機選擇的策略,有興趣的朋友可以試着添加輪詢RoundRobin 、ConsistentHash等策略。
package com.marco.loadbalance;
import java.util.List;
public interface LoadBalance {
/**
* 從list集合中挑選一個服務出來
* @param servers
* @return
*/
String select(List<String> servers);
}
package com.marco.loadbalance;
import java.util.List;
import java.util.Random;
public class RandomLoadBalance implements LoadBalance {
private static Random random = new Random();
@Override
public String select(List<String> servers) {
if(null == servers || servers.isEmpty()) {
throw new RuntimeException("當前註冊服務列表爲空!");
}
if(servers.size() == 1) {
return servers.get(0);
}
int nextInt = random.nextInt(servers.size());
return servers.get(nextInt);
}
}
service
AddService類主要是模擬被暴露的服務接口,就暫且先放在這裏吧。
package com.marco.service;
public interface AddService {
Integer add(Integer a, Integer b);
}
最後,我們需要導入rpc-core
項目所依賴的jar包spring-context
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.marco</groupId>
<artifactId>rpc</artifactId>
<version>1.0</version>
</parent>
<artifactId>rpc-core</artifactId>
<dependencies>
<!-- https://mvnrepository.com/artifact/com.101tec/zkclient -->
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.11</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.16.RELEASE</version>
</dependency>
</dependencies>
</project>
第三步:創建rpc-roommate項目
以上就是我們的難點部分,接下來創建的這兩個子項目主要是爲了測試我們的手寫Dubbo框架是否能正常運行,達到我們想要的效果。
大家注意看AddServiceImpl
類上的註解,添加@Expose
註解的目的正是將服務暴露出去,並在Zookeeper中註冊,關於這一塊的邏輯業務代碼請參照上面的RPCContextAware
類的實現。
package com.marco.service.impl;
import com.marco.anno.Expose;
import com.marco.service.AddService;
@Expose
public class AddServiceImpl implements AddService {
@Override
public Integer add(Integer a, Integer b) {
return a + b;
}
}
Roommate
可以把它當作是一個啓動類,因爲provider
的核心業務邏輯代碼在RPCContextAware
類中,但是它和我們當前的Roommate
並不在一個項目裏面,因此需要利用xml文件將RPCContextAware
加載並置入Spring的IoC容器中,這樣才能使RPCContextAware
中得方法被執行調用,完成服務得註冊,以及服務端的監聽。
所以我們通過new ClassPathXmlApplicationContext()
加載spring-rpc-provider.xml
配置文件。
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Roommate {
public static void main(String[] args) {
System.out.println("等待菜比室友發題目過來");
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring-rpc-provider.xml");
context.start();
}
}
spring-rpc-provider.xml
配置文件內容如下。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">
<context:component-scan base-package="com.marco.service.impl"></context:component-scan>
<context:property-placeholder location="classpath:/properties/app.properties"/>
<bean name="rPCContextAware" class="com.marco.spring.provider.RPCContextAware"></bean>
</beans>
除了RPCContextAware
需要被放入IoC容器中,我們的AddServiceImpl
也應該被掃描,置入容器中,這就是爲何我在@Expose
註解中添加了@Component
元註解的原因。
另外還有一個小細節,就是properties/app.properties
文件的加載。
不知道大家有沒有留意RPCContextAware
中的這段代碼,通過SpEL表達式,動態的獲取端口的值,僅需要在app.properties
文件中添加你要運行的服務的端口號(例如:port=6666
)就可以了,:6666
的意思是當配置文件什麼都沒有填寫的時候,默認會以port=6666來啓動我們的服務。
/**
* 需要監聽端口,默認不設置則爲6666
*/
@Value("${port:6666}")
private int port;
我們大可以嘗試運行一下,比方說端口號是7777,運行之後的結果如下
假如我們沒有寫任何端口號,則執行port=6666
我們接着再運行一個port=8888的服務,並查看我們的Zookeeper註冊中心,發現服務註冊成功了!並且子節點有三個!顯然是正常的狀態。
第四步:創建rpc-myself項目
終於到我們的最後一步了。rpc-myself項目模擬的就是消費端,結構如下
AddController
類主要爲了模擬遠程獲取AddService
服務,相關的業務邏輯核心代碼請參照上面的ReferenceBeanContext
類。
package com.marco.controller;
import org.springframework.stereotype.Component;
import com.marco.anno.Reference;
import com.marco.service.AddService;
@Component
public class AddController {
@Reference
private AddService addService;
public Integer add(Integer a, Integer b) {
return addService.add(a, b);
}
}
最後是Consumer端的啓動類Myself
package com.marco;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.marco.controller.AddController;
public class Myself {
public static void main(String[] args){
System.out.println("這道題目不會做 1+1=? 我要求教我的室友");
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring-rpc-consumer.xml");
// AddService addService = new RPCObjectProxy(new RPCInvoker()).getProxy(AddService.class);
AddController addController = context.getBean(AddController.class);
while(true) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Integer answer = addController.add(1, 4);
System.out.println("室友發送過來的答案是" + answer.toString());
}
}
}
同樣的,Myself
中也是通過new ClassPathXmlApplicationContext("classpath:spring-rpc-consumer.xml")
的方式將ReferenceBeanContext
類和AddController
類注入到IoC容器中。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.marco.controller"></context:component-scan>
<bean name="referenceBeanContext" class="com.marco.spring.consumer.ReferenceBeanContext"></bean>
</beans>
終於完成了!趕緊啓動看看效果吧!
通過測試發現,我們的random策略着實生效了,每間隔5秒鐘,消費端就會隨機挑選一個服務端,並通過Socket向服務戶端發送一次請求,收到Request請求的服務戶端,則會解析Request對象,找到對應的服務方法,將方法執行完成的結果,通過Socket返回給消費端,通過這種方式,將遠程調用的所有過程全部 “隱藏”。這就是神奇的RPC框架!!