在使用dubbo的時候,我有些疑問:
1.爲什麼我們引入了dubbo相關jar包,配置了consumer,就可以調用接口方法實現遠程調用呢?
其實較爲簡單的思路就是:
服務端:註冊+socket通信監聽。
客戶端:jdk動態代理+代理中(匹配註冊信息+socket通信傳參)。
服務端:利用反射調用方法並返回結果。
核心就是:動態代理+socket通信+反射。
爲什麼要註冊?是爲了可維護提供者信息,而不是寫死。
爲什麼要jdk動態代理?是爲了消費者在調用方法的時候進行攔截處理(匹配註冊,socket通信傳參)。
爲什麼使用反射?是爲了提供者的socket接收到信息後根據信息通過反射機制調用方法。
有了這些後,A系統就可以調用B系統接口了。
正文:
本文章向大家介紹手寫Dubbo框架「建議收藏」,主要內容包括一句話認識Dubbo、一句話明白RPC、手寫可擴展的RPC協議、項目地址、多模塊設計、服務端、註冊中心實現、HTTP協議、消費端、測試、優化、netty實現?、基本概念、基礎應用、原理機制和需要注意的事項等,並結合實例形式分析了其使用技巧,希望通過本文能幫助到大家理解應用這部分內容。
大家好,又見面了,我是你們的朋友全棧君。
手寫Dubbo框架
- 一句話認識Dubbo
- 瞭解Dubbo
- 一句話明白RPC
- RPC是什麼?
- 手寫可擴展的RPC協議
- 緣起
- 項目地址
- 多模塊設計
- 服務端
- 提供API
- 註冊服務,啓動tomcat
- 註冊中心實現
- 服務註冊形式
- 兩個數據bean
- 具體實現
- HTTP協議
- 內嵌tomcat啓動
- 具體實現
- 消費端
- 測試
- 優化
- 動態代理
- 以文本形式實現註冊中心
- 優化後的消費端
- netty實現?
一句話認識Dubbo
Dubbo是阿里巴巴公司開源的一個高性能優秀的服務框架,使得應用可通過高性能的 RPC 實現服務的輸出和輸入功能,可以和Spring框架無縫集成。現在已成爲Apache的開源項目。
瞭解Dubbo
詳細瞭解直接進dubbo官網看中文文檔:http://dubbo.apache.org/zh-cn/docs/user/preface/architecture.html
一句話明白RPC
RPC是什麼?
RPC(Remote Procedure Call)—遠程過程調用
,它是一種通過網絡從遠程計算機程序上請求服務,而不需要了解底層網絡技術的計算機通信協議。該協議允許運行於一臺計算機的程序調用另一臺計算機的子程序,而程序員無需額外的爲這個交互作用編程,如果涉及的軟件採用面向對象編程(java),那麼遠程過程調用亦可稱作遠程調用
或遠程方法調用
。只要支持網絡傳輸的協議就是RPC協議,RPC是一種框架。
手寫可擴展的RPC協議
緣起
公司的項目也在用Dubbo,近日又看一關於手寫dubbo的視頻,於是乎想着手敲一下簡單的dubbo框架
項目地址
https://github.com/ghostKang/dubbo-study
多模塊設計
按照官網架構圖,模塊內容設計如下
- 服務端:提供API,啓動的時候要註冊服務
- 消費端:從註冊中心獲取服務,調用子服務
- 註冊中心:保存服務配置
- RPC協議:基於Tomcat的HttpProtocol,基於Netty的DubboProtocol 由於模塊之間還要引用jar包,於是在手寫實現時以包的形式代表各個模塊
服務端
提供API
也就是接口,實現接口
public interface HelloService {
public void sayHello(String username);
}
public class HelloServiceImpl implements HelloService {
public void sayHello(String username) {
System.out.println("Hello:"+username);
}
}
註冊服務,啓動tomcat
public class Provider {
public static void main(String[] args) {
// 註冊服務
URL url = new URL("localhost",8080);
Register.regist(url, HelloService.class.getName(), HelloServiceImpl.class);
// 啓動tomcat
HttpServer httpServer = new HttpServer();
httpServer.start(url.getHostname(),url.getPort());
}
}
註冊中心實現
服務註冊形式
以接口名爲key,通過服務調用地址找到具體實現類爲。 在消費端,直接傳接口名就可以找到具體實現。
Map<interfacename,Map<URL,Class>>
兩個數據bean
Invocation .java 要實現Serializable,在服務消費端設值後序列化成對象流傳輸,然後在服務提供端轉爲對象,獲取接口名,從註冊中心獲取實現類,從而調用方法。
public class Invocation implements Serializable {
private String interfaceName;
private String methodName;
private Object[] params;
private Class[] paramTypes;
public Invocation(String interfaceName, String methodName, Object[] params, Class[] paramTypes) {
this.interfaceName = interfaceName;
this.methodName = methodName;
this.params = params;
this.paramTypes = paramTypes;
}
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 Object[] getParams() {
return params;
}
public void setParams(Object[] params) {
this.params = params;
}
public Class[] getParamTypes() {
return paramTypes;
}
public void setParamTypes(Class[] paramTypes) {
this.paramTypes = paramTypes;
}
}
URL .java 地址接口類
public class URL {
private String hostname;
private Integer port;
public URL(String hostname,Integer port){
this.hostname = hostname;
this.port = port;
}
public String getHostname() {
return hostname;
}
public void setHostname(String hostname) {
this.hostname = hostname;
}
public Integer getPort() {
return port;
}
public void setPort(Integer port) {
this.port = port;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
URL url = (URL) o;
if (hostname != null ? !hostname.equals(url.hostname) : url.hostname != null) return false;
return port != null ? port.equals(url.port) : url.port == null;
}
@Override
public int hashCode() {
int result = hostname != null ? hostname.hashCode() : 0;
result = 31 * result + (port != null ? port.hashCode() : 0);
return result;
}
}
具體實現
Register.java
public class Register {
private static Map<String,Map<URL,Class>> REGISTER = new HashMap<String, Map<URL, Class>>();
/**
* 註冊服務(暴露接口)
* @param url
* @param interfaceName
* @param implClass
*/
public static void regist(URL url,String interfaceName,Class implClass){
Map<URL,Class> map = new HashMap<URL, Class>();
map.put(url,implClass);
REGISTER.put(interfaceName,map);
}
/**
* 從註冊中心獲取實現類(發現服務)
* @param url
* @param interfaceName
* @return
*/
public static Class get(URL url,String interfaceName){
return REGISTER.get(interfaceName).get(url);
}
}
HTTP協議
內嵌tomcat啓動
引入內嵌tomcat依賴
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>9.0.12</version>
</dependency>
tomcat結構 server.xml
<Server port="8005" shutdown="SHUTDOWN">
<Service name="Catalina">
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
URIEncoding="UTF-8"/>
<Engine name="Catalina" defaultHost="localhost">
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
<Context path="" doBase="WORKDIR" reloadable="true"/>
</Host>
</Engine>
</Service>
</Server>
是不是很熟悉,根據這個xml結構構建一個tomcat啓動類
具體實現
HttpServer.java
public class HttpServer {
public void start(String hostname,Integer port){
// 實例一個tomcat
Tomcat tomcat = new Tomcat();
// 構建server
Server server = tomcat.getServer();
/**
* 在getServer的時候,就在方法內部執行了
* Service service = new StandardService();
* service.setName("Tomcat");
* server.addService(service);
*/
// 獲取service
Service service = server.findService("Tomcat");
// 構建Connector
Connector connector = new Connector();
connector.setPort(port);
connector.setURIEncoding("UTF-8");
// 構建Engine
Engine engine = new StandardEngine();
engine.setDefaultHost(hostname);
// 構建Host
Host host = new StandardHost();
host.setName(hostname);
// 構建Context
String contextPath = "";
Context context = new StandardContext();
context.setPath(contextPath);
context.addLifecycleListener(new Tomcat.FixContextListener());// 生命週期監聽器
// 然後按照server.xml,一層層把子節點添加到父節點
host.addChild(context);
engine.addChild(host);
service.setContainer(engine);
service.addConnector(connector);
// service在getServer時就被添加到server節點了
// tomcat是一個servlet,設置路徑與映射
tomcat.addServlet(contextPath,"dispatcher",new DispatcherServlet());
context.addServletMappingDecoded("/client/*","dispatcher");
try {
tomcat.start();// 啓動tomcat
tomcat.getServer().await();// 接受請求
}catch (LifecycleException e){
e.printStackTrace();
}
}
}
HttpServerHandler.java 所有http請求交給HttpServerHandler處理,即服務消費端的遠程調用
public class DispatcherServlet extends HttpServlet{
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 方便後期在此拓展服務
new HttpServerHandler().handler(req, resp);
}
}
public class HttpServerHandler {
public void handler(HttpServletRequest req, HttpServletResponse resp){
try{
// Http請求流轉爲對象
InputStream is = req.getInputStream();
ObjectInputStream ois = new ObjectInputStream(is);
Invocation invocation = (Invocation)ois.readObject();
// 尋找註冊中心的實現類,通過反射執行方法
Class implClass = Register.get(new URL("localhost",8080),invocation.getInterfaceName());
Method method = implClass.getMethod(invocation.getMethodName(),invocation.getParamTypes());
String result = (String) method.invoke(implClass.newInstance(),invocation.getParams());
// 將結果返回
IOUtils.write(result,resp.getOutputStream());
}catch (Exception e){
e.printStackTrace();
}
}
}
注意: URL一定要重寫equals與hashCode方法,否則Register.get(new URL("localhost",8080),invocation.getInterfaceName());
時爲null。
消費端
consumer .java
public class consumer {
public static void main(String[] args) {
// 調用哪個方法
Invocation invocation = new Invocation(
HelloService.class.getName(),
"sayHello",
new Object[]{"yukang"},
new Class[]{String.class});
// 發現服務器
String result = new HttpClient().post("localhost",8080,invocation);
System.out.println(result);
}
}
HttpClient.java
public class HttpClient {
/**
* 遠程方法調用
* @param hostname
* @param port
* @param invocation
* @return
*/
public String post(String hostname, Integer port, Invocation invocation){
try {
// 進行http連接
URL url = new URL("http",hostname,port,"/client/");
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
connection.setRequestMethod("POST");
connection.setDoOutput(true);// 必填項
// 將對象寫入輸出流
OutputStream os = connection.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(os);
oos.writeObject(invocation);
oos.flush();
oos.close();
// 將輸入流轉爲字符串(此處可是java對象)
InputStream is = connection.getInputStream();
return IOUtils.toString(is);
}catch (MalformedURLException e){
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}
return null;
}
}
測試
先啓動服務端
再啓動服務端
優化
dubbo是直接引入接口jar包,調用接口方法就可以獲取結果,於是使用到了動態代理返回一個代理對象。
動態代理
ProxyFactory.java
public class ProxyFactory<T> {
public static <T> T getProxy(Class interfaceClass){
return (T)Proxy.newProxyInstance(
interfaceClass.getClassLoader(), new Class[]{interfaceClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 調用哪個方法
Invocation invocation = new Invocation(
interfaceClass.getName(),
method.getName(),
args,
new Class[]{String.class});
// 模擬負載均衡,隨機獲取服務器
URL url = Register.random(interfaceClass.getName());
// 調用
HttpClient httpClient = new HttpClient();
return httpClient.post(url.getHostname(),url.getPort(),invocation);
}
});
}
}
以文本形式實現註冊中心
因爲消費端與服務端是兩個進程,消費端是獲取不到服務端的REGISTER
的,所以需要在服務端註冊時將URL寫入文本,因然後在消費端根據interfaceName隨機調度已發佈服務的服務器地址。 Register.java
public class Register {
private static Map<String,Map<URL,Class>> REGISTER = new HashMap<String, Map<URL, Class>>();
/**
* 註冊服務(暴露接口)
* @param url
* @param interfaceName
* @param implClass
*/
public static void regist(URL url,String interfaceName,Class implClass){
Map<URL,Class> map = new HashMap<URL, Class>();
map.put(url,implClass);
REGISTER.put(interfaceName,map);
// 寫入文本
saveFile();
}
/**
* 從註冊中心獲取實現類(發現服務)
* @param url
* @param interfaceName
* @return
*/
public static Class get(URL url,String interfaceName){
return REGISTER.get(interfaceName).get(url);
}
/**
* 模擬負載均衡,隨機獲取服務器
* @param interfaceName
* @return
*/
public static URL random(String interfaceName){
REGISTER = getFile();
if(REGISTER != null){
return REGISTER.get(interfaceName).keySet().iterator().next();
}
return null;
}
/**
* 寫入文本
*/
public static void saveFile(){
try {
FileOutputStream fos = new FileOutputStream("D://register.text");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(REGISTER);
oos.flush();
oos.close();
}catch (IOException e){
e.printStackTrace();
}
}
/**
* 獲取文本
* @return
*/
public static Map<String,Map<URL,Class>> getFile(){
try {
FileInputStream fis = new FileInputStream("D://register.text");
ObjectInputStream ois = new ObjectInputStream(fis);
return (Map<String,Map<URL,Class>>)ois.readObject();
}catch (IOException | ClassNotFoundException e){
e.printStackTrace();
}
return null;
}
public static Class getClass(URL url,String interfaceName){
REGISTER = getFile();
if(REGISTER != null){
return REGISTER.get(interfaceName).get(url);
}
return null;
}
}
優化後的消費端
consumer.java
public class consumer {
public static void main(String[] args) {
// 此處模擬spring容器
HelloService service = ProxyFactory.getProxy(HelloService.class);
String result = service.sayHello("yukang");
System.out.println(result);
}
}
netty實現?
發佈者:全棧程序員棧長,轉載請註明出處:https://javaforall.cn/128109.html原文鏈接:https://javaforall.cn
參考:
http://www.manongjc.com/detail/63-scdxsyseltrpppe.html