我是怎麼寫出eos的(RPC印象)

1 引言

在eos中,通常咱們都有client和server兩個應用,server端寫了服務PersonService,在client中直接能夠調用,上代碼:html

PersonService service = com.sunsharing.eos.client.ServiceContext.getBean(PersonService.class);
Map map = service.exportData(batchNo,key));

這時想起相似的兩個場景:前端

1)隨着頁面的複雜,服務愈來愈多,部署在不一樣的機器上,如何簡單方便的讓遠程服務的調用如同本地服務調用通常?java

2)甚至能夠聯想到前端ajax的調用,瀏覽器端如何調用後端所寫的服務?ajax

上述有幾點表現:json

  • 客戶端與服務端分屬不一樣空間內存區域後端

  • 透明調用機制讓使用者沒必要顯式的區分本地調用和遠程調用瀏覽器

說了這麼多,其實,我只是想聊聊RPC...服務器

2 何爲RPC

RPC(Remote Procedure Call Protocol),即遠程過程調用。通俗的講,兩臺服務器A,B,一個應用部署在A服務器上,想要調用B服務器上應用提供的函數/方法,因爲不在一個內存空間,不能直接調用,須要經過網絡來表達調用的語義和傳達調用的數據。RPC讓構建分佈式計算(應用)更容易,在提供強大的遠程調用能力時不損失本地調用的語義簡潔性。網絡

一個簡單RPC的過程是這樣的:
數據結構

(圖片來源:https://www.cs.rutgers.edu/~pxk/417/notes/03-rpc.html

這其中包含了如下流程步驟:

  1. client調用以本地調用方式調用服務

  2. client stub接收到調用後負責將方法、參數等組裝成可以進行網絡傳輸的消息體

  3. client stub找到服務地址,經過網絡將消息發送到服務端

  4. server stub收到消息後進行解碼

  5. server stub根據解碼結果調用本地的服務

  6. 本地服務執行並將結果返回給server stub

  7. server stub將返回結果打包成消息並返回給client

  8. client stub接收到消息,並進行解碼

  9. client獲得最終結果

RPC的目標就是要2~8這些步驟都封裝起來,讓用戶對這些細節透明,以適應引言中提出的場景。RPC 在整個過程當中,體現了逐層抽象,將複雜的協議編解碼和數據傳輸封裝到了一個函數中。

3 RPC過程分析

提及來其實挺簡單的,可是作起來,還有不少的細節!

3.1 如何作到本地服務存根調用

對於流程步驟1,client中是沒有該服務實現的,咱們須要讓client stub接收到調用時候,可以執行步驟2的事情。對於java來講就是使用代理。java代理有兩種方式:1) jdk 動態代理;2)字節碼生成。儘管字節碼生成方式實現的代理更爲強大和高效,但代碼不易維護,大部分公司實現RPC框架時仍是選擇動態代理方式。

public <T> T getProxy(final Class<T> clazz,final String implClassName) throws RpcException {
        InvocationHandler handler = new InvocationHandler() {

            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                Object result = new Object();
                //dosomething...
                return result;
            }
        };
        T t = (T) Proxy.newProxyInstance(RPCClient.class.getClassLoader(), new Class[]{clazz}, handler);
        return t;
    }

流程步驟5,server端中解碼後須要執行調用實際服務實現。java 中實現代碼的動態接口調用除了原生的 jdk 自帶的反射,一些第三方庫也提供了性能更優的反射調用。

 Method m = obj.getClass().getMethod(invocation.getMethodName(), invocation.getParameterTypes());
 Object o = m.invoke(obj, invocation.getArguments());

 3.2 怎麼對入參和結果進行編碼和解碼

對於流程步驟二、六、四、8,在client和server之間,咱們須要有一個兩方都能認識的消息做爲交互通信的媒介。(此部分屬於摘抄)

3.2.1 肯定消息數據結構

客戶端的請求消息結構通常須要包括如下內容:

  1. 服務接口名稱

  2. 方法名

  3. 參數類型&參數值

  4. 超時時間

  5. requestID,標識惟一請求id

同理服務端返回的消息結構通常包括如下內容

  1. 返回值

  2. 狀態code

  3. requestID

3.2.2 序列化

一旦肯定了消息的數據結構後,下一步就是要考慮序列化與反序列化了。

什麼是序列化?序列化就是將數據結構或對象轉換成二進制串的過程,也就是編碼的過程。

什麼是反序列化?將在序列化過程當中所生成的二進制串轉換成數據結構或者對象的過程。

爲何須要序列化?轉換爲二進制串後纔好進行網絡傳輸嘛!爲何須要反序列化?將二進制轉換爲對象纔好進行後續處理!

因爲Java提供了良好的默認支持,實現基本的對象序列化是件比較簡單的事。待序列化的Java類只須要實現Serializable接口便可。Serializable僅是一個標記接口,並不包含任何須要實現的具體方法。實現該接口只是爲了聲明該Java類的對象是能夠被序列化的。實際的序列化和反序列化工做是經過ObjectOuputStream和ObjectInputStream來完成的。(連接閱讀:Java對象序列化與RMI

現現在序列化的方案愈來愈多,每種序列化方案都有優勢和缺點,它們在設計之初有本身獨特的應用場景,那到底選擇哪一種呢?從RPC的角度上看,主要看三點:

  1. 通用性,好比是否能支持Map等複雜的數據結構;

  2. 性能,包括時間複雜度和空間複雜度,因爲RPC框架將會被公司幾乎全部服務使用,若是序列化上能節約一點時間,對整個公司的收益都將很是可觀,同理若是序列化上能節約一點內存,網絡帶寬也能省下很多;

  3. 可擴展性,對互聯網公司而言,業務變化快,若是序列化協議具備良好的可擴展性,支持自動增長新的業務字段,刪除老的字段,而不影響老的服務,這將大大提供系統的健壯性。

目前國內各大互聯網公司普遍使用hessian、protobuf、thrift、avro等成熟的序列化解決方案來搭建RPC框架,這些都是久經考驗的解決方案。甚至json的方式也何嘗不是一種方式。

3.3 網絡通訊傳輸

協議編碼以後,天然就是須要將編碼後的 RPC 請求消息傳輸到服務方,服務方執行後返回結果消息或確認消息給客戶方。RPC 的應用場景實質是一種可靠的請求應答消息流,和 HTTP 相似。固然若是你願意,沒有否決你用HTTP的方式。

對於性能的考慮上,須要瞭解目前有兩種IO通訊模型:1)BIO;2)NIO。(連接閱讀:一個故事講清楚 NIO)。鏈接能夠是按需鏈接,調用結束後就斷掉,也能夠是長鏈接,多個遠程過程調用共享同一個鏈接。

如今不少RPC框架都直接基於netty,好比阿里巴巴的HSF、dubbo等。

3.4 服務發現

一個RPC過程當中,簡單的話,可能不須要服務發現的動做,配置好服務的IP以及端口就能夠了。可是在分佈式的應用中,就是一個很重要也很必要的過程。

好比說,當部分服務遷移到另外一臺機器B上,這時候就須要告訴調用者,部分服務要走哪一個地址。若是你是手動的被動去修改,那麼當又新增一臺機器C掛載一部分服務。這時候調用者又須要手動修改;更復雜的狀況下,A和B機器上都同時部署相同的服務作分壓,那麼咱們確定會想去實現負載均衡,那問題來了,手動的方式須要輪詢機器服務是否存活,不然哪天機器A掛了,你會發現服務時好時壞。

分佈式的一個很重要的點就是能實現服務發現,即調用者不用事先知道服務在哪,再也不須要寫死服務提供方地址。機器的增添、剔除對調用方透明。其中,zookeeper是一個很好的選擇,它被普遍用於實現服務自動註冊與發現功能,相對也比較成熟。

4 finally

思考:回到前言所講的前端ajax的調用,瀏覽器端如何調用後端所寫的服務(某個java類)?

提示:這區別於前面所提的client stub的存根調用,更像動態調用服務的方式。

  1. client調用:JS調用方式,能夠提供統一的封裝接口

  2. 服務消息的格式數據肯定:服務名,調用方法名,入參......

  3. 數據序列化、反序列化:json

  4. 網絡通訊:ajax、http

  5. 服務執行:java反射

編碼:

out.close();
相關文章
相關標籤/搜索