RPC即遠程過程調用,它的實現方式有很多,比如webservice等。框架調多了,煩了,沒激情了,我們就該問自己,這些框架的作用到底是什麼,來找回當初的激情。 一般來說,我們寫的系統就是一個單機系統,一個web伺服器一個資料庫服務,但是當這單台伺服器的處理能力受硬體成本的限制,是不能無限的提升處理性 ...
RPC即遠程過程調用,它的實現方式有很多,比如webservice等。框架調多了,煩了,沒激情了,我們就該問自己,這些框架的作用到底是什麼,來找回當初的激情。
一般來說,我們寫的系統就是一個單機系統,一個web伺服器一個資料庫服務,但是當這單台伺服器的處理能力受硬體成本的限制,是不能無限的提升處理性能的。這個時候我們使用RPC將原來的本地調用轉變為調用遠端的伺服器上的方法,給系統的處理能力和吞吐量帶來了提升。
RPC的實現包括客戶端和服務端,即服務的調用方和服務的提供方。服務調用方發送rpc請求到服務提供方,服務提供方根據調用方提供的參數執行請求方法,將執行的結果返回給調用方,一次rpc調用完成。
先讓我們利用socket簡單的實現RPC,來看看他是什麼鬼樣子。
原文和作者一起討論:http://www.cnblogs.com/intsmaze/p/6056763.html
可接網站開發,java開發。
新浪微博:intsmaze劉洋洋哥
微信:intsmaze
服務端代碼如下
服務端的提供服務的方法
package cn.intsmaze.tcp.two.service;
public class SayHelloServiceImpl {
public String sayHello(String helloArg) {
if(helloArg.equals("intsmaze"))
{
return "intsmaze";
}
else
{
return "bye bye";
}
}
}
服務端啟動接收外部方法請求的埠類,它接收到來自客戶端的請求數據後,利用反射知識,創建指定類的對象,並調用對應方法,然後把執行的結果返回給客戶端即可。
package cn.intsmaze.tcp.two.service;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.Socket;
public class Provider {
public static void main(String[] args) throws Exception {
ServerSocket server=new ServerSocket(1234);
while(true)
{
Socket socket=server.accept();
ObjectInputStream input=new ObjectInputStream(socket.getInputStream());
String classname=input.readUTF();//獲得服務端要調用的類名
String methodName=input.readUTF();//獲得服務端要調用的方法名稱
Class<?>[] parameterTypes=(Class<?>[]) input.readObject();//獲得服務端要調用方法的參數類型
Object[] arguments=(Object[]) input.readObject();//獲得服務端要調用方法的每一個參數的值
Class serviceclass=Class.forName(classname);//創建類
Object object = serviceclass.newInstance();//創建對象
Method method=serviceclass.getMethod(methodName, parameterTypes);//獲得該類的對應的方法
Object result=method.invoke(object, arguments);//該對象調用指定方法
ObjectOutputStream output=new ObjectOutputStream(socket.getOutputStream());
output.writeObject(result);
socket.close();
}
}
}
服務調用者代碼
調用服務的方法,主要就是客戶端啟動一個socket,然後向提供服務的服務端發送數據,其中的數據就是告訴服務端去調用哪一個類的哪一個方法,已經調用該方法的參數是多少,然後結束服務端返回的數據即可。
package cn.intsmaze.tcp.two.client;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
public class consumer {
@SuppressWarnings({ "unused", "rawtypes" })
public static void main(String[] arg) throws Exception
{
//我們要想調用遠程提供的服務,必須告訴遠程我們要調用你的哪一個類,這裡我們可以在本地創建一個interface來獲取類的名稱,但是這樣我們必須
//保證該interface和遠程的interface的所在包名一致。這種方式不好。所以我們還是通過硬編碼的方式吧。
//雖然webservice就是這樣的,我個人覺得不是多好。
// String interfacename=SayHelloService.class.getName();
String classname="cn.intsmaze.tcp.two.service.SayHelloServiceImpl";
String method="sayHello";
Class[] argumentsType={String.class};
Object[] arguments={"intsmaze"};
Socket socket=new Socket("127.0.0.1",1234);
ObjectOutputStream output=new ObjectOutputStream(socket.getOutputStream());
output.writeUTF(classname);
output.writeUTF(method);
output.writeObject(argumentsType);
output.writeObject(arguments);
ObjectInputStream input=new ObjectInputStream(socket.getInputStream());
Object result=input.readObject();
System.out.println(result);
socket.close();
}
}
當然實際中出於性能考慮,往往採用非阻塞式I/O,避免無限的等待,帶來系統性能的消耗。
上面的只是一個簡單的過程,當系統之間的調用變的複雜之後,該方式有如下不足:服務調用者代碼以硬編碼的方式指明所調用服務的信息(類名,方法名),當服務提供方改動所提供的服務的代碼後,服務調用者必須修改代碼進行調整,不然會導致服務調用者無法成功進行遠程方法調用導致系統異常,並且當服務提供者宕機下線了,服務調用者並不知道服務端是否存活,仍然會進行訪問,導致異常。
一個系統中,服務提供者往往不是一個,而是多個,那麼服務消費者如何從眾多的服務者找到對應的服務進行RPC就是一個問題了,因為這個時候我們不能在在服務調用者代碼中硬編碼指出調用哪一個服務的地址等信息,因為我們可以想象,沒有一個統一的地方管理所有服務,那麼我們在錯綜複雜的系統之間無法理清有哪些服務,已經服務的調用關係,這簡直就是災難。
這個時候就要進行服務的註冊,通過一個第三方的存儲介質,當服務的提供者上線時,通過代碼將所提供的服務的相關信息寫入到存儲介質中,寫入的主要信息以key-value方式:服務的名稱:(類名,方法名,參數類型,參數,IP地址,埠)。服務的調用者向遠程調用服務時,會先到第三方存儲介質中根據所要調用的服務名得到(類名,方法名,參數類型,參數,IP地址,埠)等參數,然後再向服務端發出調用請求。通過這種方式,代碼就變得靈活多變,不會再因為一個局部的變得引發全局架構的變動。因為一般的改動是不會變得服務的名稱的。這種方式其實就是soa架構,服務消費者通過服務名稱,從眾多服務中找到要調用的服務的相關信息,稱為服務的路由。
下麵通過一個靜態MAP對象來模擬第三方存儲的介質。
package cn.intsmaze.tcp.three;
import net.sf.json.JSONObject;
public class ClassWays {
String classname;//類名
String method;//方法
Class[] argumentsType;//參數類型
String ip;//服務的ip地址
int port;//服務的埠
get,set......
}
第三方存儲介質,這裡固定了服務提供者的相關信息,理想的模擬是,當服務啟動後,自動向該類的map集合添加信息。但是因為服務端和客戶端啟動時,是兩個不同的jvm進程,客戶端時無法訪問到服務端寫到靜態map集合的數據的。
package cn.intsmaze.tcp.three;
import java.util.HashMap;
import java.util.Map;
import net.sf.json.JSONObject;
public class ServiceRoute {
public static Map<String,String> NAME=new HashMap<String, String>();
public ServiceRoute()
{
ClassWays classWays=new ClassWays();
Class[] argumentsType={String.class};
classWays.setArgumentsType(argumentsType);
classWays.setClassname("cn.intsmaze.tcp.three.service.SayHelloServiceImpl");
classWays.setMethod("sayHello");
classWays.setIp("127.0.0.1");
classWays.setPort(1234);
JSONObject js=JSONObject.fromObject(classWays);
NAME.put("SayHello", js.toString());
}
}
接下來看服務端代碼的美麗面孔吧。
package cn.intsmaze.tcp.three.service;public class Provider {
//服務啟動的時候,組裝相關信息,然後寫入第三方存儲機制,供服務的調用者去獲取
public void reallyUse() {
ClassWays classWays = new ClassWays();
Class[] argumentsType = { String.class };
classWays.setArgumentsType(argumentsType);
classWays.setClassname("cn.intsmaze.tcp.three.service.SayHelloServiceImpl");
classWays.setMethod("sayHello");
classWays.setIp("127.0.0.1");
classWays.setPort(1234);
JSONObject js=JSONObject.fromObject(classWays);
//模擬第三方存儲介質,實際中應該是redis,mysql,zookeeper等。
ServiceRoute.NAME.put("SayHello", js.toString());
}
public static void main(String[] args) throws Exception {
ServerSocket server = new ServerSocket(1234);
//實際中,這個地方應該調用如下方法,但是因為簡單的模擬服務的註冊,將註冊的信息硬編碼在ServiceRoute類中,這個類的構造方法裡面會自動註冊服務的相關信息。
//server.reallyUse();
while (true) {
Socket socket = server.accept();
ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
String classname = input.readUTF();
String methodName = input.readUTF();
Class<?>[] parameterTypes = (Class<?>[]) input.readObject();
Object[] arguments = (Object[]) input.readObject();
Class serviceclass = Class.forName(classname);
Object object = serviceclass.newInstance();
Method method = serviceclass.getMethod(methodName, parameterTypes);
Object result = method.invoke(object, arguments);
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
output.writeObject(result);
socket.close();
}
}
}
服務的調用者代碼:
package cn.intsmaze.tcp.three.client;public class Consumer {
public Object reallyUse(String provideName,Object[] arguments) throws Exception
{
//模擬從第三方存儲介質拿去數據
ServiceRoute serviceRoute=new ServiceRoute();
String js=serviceRoute.NAME.get(provideName);
JSONObject obj = new JSONObject().fromObject(js);
ClassWays classWays = (ClassWays)JSONObject.toBean(obj,ClassWays.class);
String classname=classWays.getClassname();
String method=classWays.getMethod();
Class[] argumentsType=classWays.getArgumentsType();
Socket socket=new Socket(classWays.getIp(),classWays.getPort());
ObjectOutputStream output=new ObjectOutputStream(socket.getOutputStream());
output.writeUTF(classname);
output.writeUTF(method);
output.writeObject(argumentsType);
output.writeObject(arguments);
ObjectInputStream input=new ObjectInputStream(socket.getInputStream());
Object result=input.readObject();
socket.close();
return result;
}
@SuppressWarnings({ "unused", "rawtypes" })
public static void main(String[] arg) throws Exception
{
Consumer consumer=new Consumer();
Object[] arguments={"intsmaze"};
Object result=consumer.reallyUse("SayHello",arguments);
System.out.println(result);
}
}
回到開始的問題現在我們保證了服務調用者對服務的調用的相關參數以動態的方式進行控制,通過封裝,服務調用者只需要指定每一次調用時的參數的值即可。但是當服務提供者宕機下線了,服務調用者並不知道服務端是否存活,仍然會進行訪問,導致異常。這個時候我們該如何考慮解決了?
剩下的我就不寫代碼示例了,代碼只是思想的表現形式,就像開發語言一直變化,但是思想是不變的。
服務下線我們應該把該服務從第三方存儲刪除,在服務提供方寫代碼進行刪除控制,也就是服務下線前訪問第三方刪除自己提供的服務。這樣當然行不通的,因為服務宕機時,才不會說,我要宕機了,服務提供者你快去第三方存儲介質刪掉該服務信息。所以這個時候我們就要在第三方存儲介質上做手腳,比如服務提供方並不是直接把服務信息寫入第三方存儲介質,而是與一個第三方系統進行交互,第三方系統把接收到來自服務提供者的服務信息寫入第三方存儲介質中,然後在服務提供者和第三方系統間建立一個心跳檢測,當第三方系統檢測到服務提供者宕機後,就會自動到第三方介質中刪除對應服務信息。
這個時候我們就可以選擇zookeeper作為第三方存儲介質,服務啟動會到zookeeper上面創建一個臨時目錄,該目錄存儲該服務的相關信息,當服務端宕機了,zookeeper會自動刪除該文件夾,這個時候就實現了服務的動態上下線了。
這個地方其實就是dubbo的一大特色功能:服務配置中心——動態註冊和獲取服務信息,來統一管理服務名稱和其對於的伺服器的信息。服務提供者在啟動時,將其提供的服務名稱,伺服器地址註冊到服務配置中心,服務消費者通過配置中心來獲得需要調用服務的機器。當伺服器宕機或下線,相應的機器需要動態地從服務配置中心移除,並通知相應的服務消費者。這個過程中,服務消費者只在第一次調用服務時需要查詢服務配置中心,然後將查詢到的信息緩存到本地,後面的調用直接使用本地緩存的服務地址信息,而不需要重新發起請求到服務配置中心去獲取相應的服務地址,直到服務的地址列表有變更(機器上線或者下線)。
zookeeper如何知道的?zookeeper其實就是會和客戶端直接有一個心跳檢測來判斷的,zookeeper功能很簡單的,可以自己去看對應的書籍即可。
隨著業務的發展,服務調用者的規模發展到一定的階段,對服務提供方也帶來了巨大的壓力,這個時候服務提供方就不在是一臺機器了,而是一個服務集群了。這個時候服務調用者如何知道調用服務集群的哪一臺機器?
多台伺服器組成的集群,在請求到來時,需要有一個負載均衡程式從服務的地址列表中選取一臺伺服器進行訪問(服務的負載均衡)。這個時候我們可能會這樣做,在第三方介質中存儲每一個服務集群的代理地址,這樣服務消費者獲取對應服務集群的代理地址,向代理髮送請求,然後再由代理負責轉發請求到對應的服務機器。比如nginx。(實現原理,每一個服務啟動,向第三方存儲介質存儲該服務的代理地址即可,具體多個相同服務產生代理地址重覆可以通過代碼進行控制,這裡就不扯了,都是有經驗的開發人員,服務下線的檢測,第三方存儲介質進行控制。)
我們來看看這種有什麼問題,使用nginx進行負載均衡,一旦nginx宕機,依賴他的服務均將失效。所以我們會對動態註冊和獲取服務信息這個功能進行改進,同一個服務每上線一臺新的機器,在zookeeper中的每一個服務名下麵創建一個臨時目錄,一個臨時目錄對應該服務的一個集群,進而實現了服務的集群功能。然後考慮服務的調用者,因為內部系統的調用不像對外的服務,他的訪問數量是可控的且有限的,所以就沒有必要將負載均衡演算法在zookeeper那一端進行實現,就是服務調用者每一次調用都到zookeeper上通過負載均衡來選擇對應服務下的某一個機器目錄,只需要在服務的調用端編寫負載均衡演算法,具體就不需要講太多,條條大路通羅馬。
如果有沒有講明白的可以留言,我進行更正。基本上一個RPC就是這樣,剩下的一些基於RPC的框架無非就是實現了多些協議,以及一些多種語言環境的考慮和效率的提升。
覺得不錯點個推薦吧,看在我花了一天時間把自己的知識整理分析,謝謝嘍。