摘自: https://my.oschina.net/hosee/blog/711632
摘要: 本文主要說明RPC的原理,以及通過Hadoop來舉例在實踐中如何實現(xiàn)RPC,本文主要通過摘取網(wǎng)上Blog(參見Reference)來整理RPC原理。
在學(xué)校期間大家都寫過不少程序,比如寫個hello world服務(wù)類,然后本地調(diào)用下,如下所示。這些程序的特點是服務(wù)消費方和服務(wù)提供方是本地調(diào)用關(guān)系。
public class Test {
public static void main(String[] args) {
HelloWorldService helloWorldService = new HelloWorldServiceImpl();
helloWorldService.sayHello("test");
}
}
而一旦踏入公司尤其是大型互聯(lián)網(wǎng)公司就會發(fā)現(xiàn),公司的系統(tǒng)都由成千上萬大大小小的服務(wù)組成,各服務(wù)部署在不同的機器上,由不同的團隊負責(zé)。
這時就會遇到兩個問題:
- 要搭建一個新服務(wù),免不了需要依賴他人的服務(wù),而現(xiàn)在他人的服務(wù)都在遠端,怎么調(diào)用?
- 其它團隊要使用我們的新服務(wù),我們的服務(wù)該怎么發(fā)布以便他人調(diào)用?下文將對這兩個問題展開探討。
1 如何調(diào)用他人的遠程服務(wù)?
由于各服務(wù)部署在不同機器,服務(wù)間的調(diào)用免不了網(wǎng)絡(luò)通信過程,服務(wù)消費方每調(diào)用一個服務(wù)都要寫一坨網(wǎng)絡(luò)通信相關(guān)的代碼,不僅復(fù)雜而且極易出錯。
如果有一種方式能讓我們像調(diào)用本地服務(wù)一樣調(diào)用遠程服務(wù),而讓調(diào)用者對網(wǎng)絡(luò)通信這些細節(jié)透明,那么將大大提高生產(chǎn)力,比如服務(wù)消費方在執(zhí)行helloWorldService.sayHello("test")時,實質(zhì)上調(diào)用的是遠端的服務(wù)。這種方式其實就是RPC(Remote Procedure Call Protocol),在各大互聯(lián)網(wǎng)公司中被廣泛使用,如阿里巴巴的hsf、dubbo(開源)、Facebook的thrift(開源)、Google grpc(開源)、Twitter的finagle(開源)等。
要讓網(wǎng)絡(luò)通信細節(jié)對使用者透明,我們需要對通信細節(jié)進行封裝,我們先看下一個RPC調(diào)用的流程涉及到哪些通信細節(jié):
- 服務(wù)消費方(client)調(diào)用以本地調(diào)用方式調(diào)用服務(wù);
- client stub接收到調(diào)用后負責(zé)將方法、參數(shù)等組裝成能夠進行網(wǎng)絡(luò)傳輸?shù)南Ⅲw;
- client stub找到服務(wù)地址,并將消息發(fā)送到服務(wù)端;
- server stub收到消息后進行解碼;
- server stub根據(jù)解碼結(jié)果調(diào)用本地的服務(wù);
- 本地服務(wù)執(zhí)行并將結(jié)果返回給server stub;
- server stub將返回結(jié)果打包成消息并發(fā)送至消費方;
- client stub接收到消息,并進行解碼;
- 服務(wù)消費方得到最終結(jié)果。
RPC的目標就是要2~8這些步驟都封裝起來,讓用戶對這些細節(jié)透明。
1.1 怎么做到透明化遠程服務(wù)調(diào)用?
怎么封裝通信細節(jié)才能讓用戶像以本地調(diào)用方式調(diào)用遠程服務(wù)呢?對java來說就是使用代理!java代理有兩種方式:
- jdk 動態(tài)代理
- 字節(jié)碼生成
盡管字節(jié)碼生成方式實現(xiàn)的代理更為強大和高效,但代碼維護不易,大部分公司實現(xiàn)RPC框架時還是選擇動態(tài)代理方式。
下面簡單介紹下動態(tài)代理怎么實現(xiàn)我們的需求。我們需要實現(xiàn)RPCProxyClient代理類,代理類的invoke方法中封裝了與遠端服務(wù)通信的細節(jié),消費方首先從RPCProxyClient獲得服務(wù)提供方的接口,當(dāng)執(zhí)行helloWorldService.sayHello("test")方法時就會調(diào)用invoke方法。
public class RPCProxyClient implements java.lang.reflect.InvocationHandler{
private Object obj;
public RPCProxyClient(Object obj){
this.obj=obj;
}
/**
* 得到被代理對象;
*/
public static Object getProxy(Object obj){
return java.lang.reflect.Proxy.newProxyInstance(obj.getClass().getClassLoader(),
obj.getClass().getInterfaces(), new RPCProxyClient(obj));
}
/**
* 調(diào)用此方法執(zhí)行
*/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
//結(jié)果參數(shù);
Object result = new Object();
// ...執(zhí)行通信相關(guān)邏輯
// ...
return result;
}
}
public class Test {
public static void main(String[] args) {
HelloWorldService helloWorldService = (HelloWorldService)RPCProxyClient.getProxy(HelloWorldService.class);
helloWorldService.sayHello("test");
}
}
1.2 怎么對消息進行編碼和解碼?
1.2.1 確定消息數(shù)據(jù)結(jié)構(gòu)
上節(jié)講了invoke里需要封裝通信細節(jié)(通信細節(jié)再后面幾章詳細探討),而通信的第一步就是要確定客戶端和服務(wù)端相互通信的消息結(jié)構(gòu)。客戶端的請求消息結(jié)構(gòu)一般需要包括以下內(nèi)容:
1)接口名稱
在我們的例子里接口名是“HelloWorldService”,如果不傳,服務(wù)端就不知道調(diào)用哪個接口了;
2)方法名
一個接口內(nèi)可能有很多方法,如果不傳方法名服務(wù)端也就不知道調(diào)用哪個方法;
3)參數(shù)類型&參數(shù)值
參數(shù)類型有很多,比如有bool、int、long、double、string、map、list,甚至如struct(class);以及相應(yīng)的參數(shù)值;
4)超時時間
5)requestID,標識唯一請求id,在下面一節(jié)會詳細描述requestID的用處。
同理服務(wù)端返回的消息結(jié)構(gòu)一般包括以下內(nèi)容。
1)返回值
2)狀態(tài)code
3)requestID
1.2.2 序列化
一旦確定了消息的數(shù)據(jù)結(jié)構(gòu)后,下一步就是要考慮序列化與反序列化了。
什么是序列化?序列化就是將數(shù)據(jù)結(jié)構(gòu)或?qū)ο筠D(zhuǎn)換成二進制串的過程,也就是編碼的過程。
什么是反序列化?將在序列化過程中所生成的二進制串轉(zhuǎn)換成數(shù)據(jù)結(jié)構(gòu)或者對象的過程。
為什么需要序列化?轉(zhuǎn)換為二進制串后才好進行網(wǎng)絡(luò)傳輸嘛!
為什么需要反序列化?將二進制轉(zhuǎn)換為對象才好進行后續(xù)處理!
現(xiàn)如今序列化的方案越來越多,每種序列化方案都有優(yōu)點和缺點,它們在設(shè)計之初有自己獨特的應(yīng)用場景,那到底選擇哪種呢?從RPC的角度上看,主要看三點:
- 通用性,比如是否能支持Map等復(fù)雜的數(shù)據(jù)結(jié)構(gòu);
- 性能,包括時間復(fù)雜度和空間復(fù)雜度,由于RPC框架將會被公司幾乎所有服務(wù)使用,如果序列化上能節(jié)約一點時間,對整個公司的收益都將非常可觀,同理如果序列化上能節(jié)約一點內(nèi)存,網(wǎng)絡(luò)帶寬也能省下不少;
- 可擴展性,對互聯(lián)網(wǎng)公司而言,業(yè)務(wù)變化飛快,如果序列化協(xié)議具有良好的可擴展性,支持自動增加新的業(yè)務(wù)字段,而不影響老的服務(wù),這將大大提供系統(tǒng)的靈活度。
目前互聯(lián)網(wǎng)公司廣泛使用Protobuf、Thrift、Avro等成熟的序列化解決方案來搭建RPC框架,這些都是久經(jīng)考驗的解決方案。
1.3 通信
消息數(shù)據(jù)結(jié)構(gòu)被序列化為二進制串后,下一步就要進行網(wǎng)絡(luò)通信了。目前有兩種常用IO通信模型:1)BIO;2)NIO。一般RPC框架需要支持這兩種IO模型。
如何實現(xiàn)RPC的IO通信框架呢?
- 使用java nio方式自研,這種方式較為復(fù)雜,而且很有可能出現(xiàn)隱藏bug,但也見過一些互聯(lián)網(wǎng)公司使用這種方式;
- 基于mina,mina在早幾年比較火熱,不過這些年版本更新緩慢;
- 基于netty,現(xiàn)在很多RPC框架都直接基于netty這一IO通信框架,省力又省心,比如阿里巴巴的HSF、dubbo,Twitter的finagle等。
1.4 消息里為什么要有requestID?
如果使用netty的話,一般會用channel.writeAndFlush()方法來發(fā)送消息二進制串,這個方法調(diào)用后對于整個遠程調(diào)用(從發(fā)出請求到接收到結(jié)果)來說是一個異步的,即對于當(dāng)前線程來說,將請求發(fā)送出來后,線程就可以往后執(zhí)行了,至于服務(wù)端的結(jié)果,是服務(wù)端處理完成后,再以消息的形式發(fā)送給客戶端的。于是這里出現(xiàn)以下兩個問題:
- 怎么讓當(dāng)前線程“暫停”,等結(jié)果回來后,再向后執(zhí)行?
- 如果有多個線程同時進行遠程方法調(diào)用,這時建立在client server之間的socket連接上會有很多雙方發(fā)送的消息傳遞,前后順序也可能是隨機的,server處理完結(jié)果后,將結(jié)果消息發(fā)送給client,client收到很多消息,怎么知道哪個消息結(jié)果是原先哪個線程調(diào)用的?
如下圖所示,線程A和線程B同時向client socket發(fā)送請求requestA和requestB,socket先后將requestB和requestA發(fā)送至server,而server可能將responseA先返回,盡管requestA請求到達時間更晚。我們需要一種機制保證responseA丟給ThreadA,responseB丟給ThreadB。
怎么解決呢?
- client線程每次通過socket調(diào)用一次遠程接口前,生成一個唯一的ID,即requestID(requestID必需保證在一個Socket連接里面是唯一的),一般常常使用AtomicLong從0開始累計數(shù)字生成唯一ID;
- 將處理結(jié)果的回調(diào)對象callback,存放到全局ConcurrentHashMap里面put(requestID, callback);
- 當(dāng)線程調(diào)用channel.writeAndFlush()發(fā)送消息后,緊接著執(zhí)行callback的get()方法試圖獲取遠程返回的結(jié)果。在get()內(nèi)部,則使用synchronized獲取回調(diào)對象callback的鎖,再先檢測是否已經(jīng)獲取到結(jié)果,如果沒有,然后調(diào)用callback的wait()方法,釋放callback上的鎖,讓當(dāng)前線程處于等待狀態(tài)。
- 服務(wù)端接收到請求并處理后,將response結(jié)果(此結(jié)果中包含了前面的requestID)發(fā)送給客戶端,客戶端socket連接上專門監(jiān)聽消息的線程收到消息,分析結(jié)果,取到requestID,再從前面的ConcurrentHashMap里面get(requestID),從而找到callback對象,再用synchronized獲取callback上的鎖,將方法調(diào)用結(jié)果設(shè)置到callback對象里,再調(diào)用callback.notifyAll()喚醒前面處于等待狀態(tài)的線程。
public Object get() {
synchronized (this) { // 旋鎖
while (!isDone) { // 是否有結(jié)果了
wait(); //沒結(jié)果是釋放鎖,讓當(dāng)前線程處于等待狀態(tài)
}
}
}
private void setDone(Response res) {
this.res = res;
isDone = true;
synchronized (this) { //獲取鎖,因為前面wait()已經(jīng)釋放了callback的鎖了
notifyAll(); // 喚醒處于等待的線程
}
}
2 如何發(fā)布自己的服務(wù)?
如何讓別人使用我們的服務(wù)呢?有同學(xué)說很簡單嘛,告訴使用者服務(wù)的IP以及端口就可以了啊。確實是這樣,這里問題的關(guān)鍵在于是自動告知還是人肉告知。
人肉告知的方式:如果你發(fā)現(xiàn)你的服務(wù)一臺機器不夠,要再添加一臺,這個時候就要告訴調(diào)用者我現(xiàn)在有兩個ip了,你們要輪詢調(diào)用來實現(xiàn)負載均衡;調(diào)用者咬咬牙改了,結(jié)果某天一臺機器掛了,調(diào)用者發(fā)現(xiàn)服務(wù)有一半不可用,他又只能手動修改代碼來刪除掛掉那臺機器的ip。現(xiàn)實生產(chǎn)環(huán)境當(dāng)然不會使用人肉方式。
有沒有一種方法能實現(xiàn)自動告知,即機器的增添、剔除對調(diào)用方透明,調(diào)用者不再需要寫死服務(wù)提供方地址?當(dāng)然可以,現(xiàn)如今zookeeper被廣泛用于實現(xiàn)服務(wù)自動注冊與發(fā)現(xiàn)功能!
簡單來講,zookeeper可以充當(dāng)一個服務(wù)注冊表
(Service Registry),讓多個服務(wù)提供者
形成一個集群,讓服務(wù)消費者
通過服務(wù)注冊表獲取具體的服務(wù)訪問地址(ip+端口)去訪問具體的服務(wù)提供者。如下圖所示:
具體來說,zookeeper就是個分布式文件系統(tǒng),每當(dāng)一個服務(wù)提供者部署后都要將自己的服務(wù)注冊到zookeeper的某一路徑上: /{service}/{version}/{ip:port}, 比如我們的HelloWorldService部署到兩臺機器,那么zookeeper上就會創(chuàng)建兩條目錄:分別為/HelloWorldService/1.0.0/100.19.20.01:16888 /HelloWorldService/1.0.0/100.19.20.02:16888。
zookeeper提供了“心跳檢測”功能,它會定時向各個服務(wù)提供者發(fā)送一個請求(實際上建立的是一個 Socket 長連接),如果長期沒有響應(yīng),服務(wù)中心就認為該服務(wù)提供者已經(jīng)“掛了”,并將其剔除,比如100.19.20.02這臺機器如果宕機了,那么zookeeper上的路徑就會只剩/HelloWorldService/1.0.0/100.19.20.01:16888。
服務(wù)消費者會去監(jiān)聽相應(yīng)路徑(/HelloWorldService/1.0.0),一旦路徑上的數(shù)據(jù)有任務(wù)變化(增加或減少),zookeeper都會通知服務(wù)消費方服務(wù)提供者地址列表已經(jīng)發(fā)生改變,從而進行更新。
更為重要的是zookeeper與生俱來的容錯容災(zāi)能力(比如leader選舉),可以確保服務(wù)注冊表的高可用性。
3.Hadoop中RPC實例分析
ipc.RPC類中有一些內(nèi)部類,為了大家對RPC類有個初步的印象,就先羅列幾個我們感興趣的分析一下吧:
Invocation :用于封裝方法名和參數(shù),作為數(shù)據(jù)傳輸層。
ClientCache :用于存儲client對象,用socket factory作為hash key,存儲結(jié)構(gòu)為hashMap <SocketFactory, Client>。
Invoker :是動態(tài)代理中的調(diào)用實現(xiàn)類,繼承了InvocationHandler.
Server :是ipc.Server的實現(xiàn)類。
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
???
ObjectWritable value = (ObjectWritable)
client.call(new Invocation(method, args), remoteId);
???
return value.get();
}
如果你發(fā)現(xiàn)這個invoke()方法實現(xiàn)的有些奇怪的話,那你就對了。一般我們看到的動態(tài)代理的invoke()方法中總會有 method.invoke(ac, arg); 這句代碼。而上面代碼中卻沒有,這是為什么呢?其實使用 method.invoke(ac, arg); 是在本地JVM中調(diào)用;而在hadoop中,是將數(shù)據(jù)發(fā)送給服務(wù)端,服務(wù)端將處理的結(jié)果再返回給客戶端,所以這里的invoke()方法必然需要進行網(wǎng)絡(luò)通信。而網(wǎng)絡(luò)通信就是下面的這段代碼實現(xiàn)的:
ObjectWritable value = (ObjectWritable)
client.call(new Invocation(method, args), remoteId);
Invocation類在這里封裝了方法名和參數(shù)。其實這里網(wǎng)絡(luò)通信只是調(diào)用了Client類的call()方法。那我們接下來分析一下ipc.Client源碼吧。和第一章一樣,同樣是3個問題
- 客戶端和服務(wù)端的連接是怎樣建立的?
- 客戶端是怎樣給服務(wù)端發(fā)送數(shù)據(jù)的?
- 客戶端是怎樣獲取服務(wù)端的返回數(shù)據(jù)的?
3.1 客戶端和服務(wù)端的連接是怎樣建立的?
public Writable call(Writable param, ConnectionId remoteId)
throws InterruptedException, IOException {
Call call = new Call(param); //將傳入的數(shù)據(jù)封裝成call對象
Connection connection = getConnection(remoteId, call); //獲得一個連接
connection.sendParam(call); // 向服務(wù)端發(fā)送call對象
boolean interrupted = false;
synchronized (call) {
while (!call.done) {
try {
call.wait(); // 等待結(jié)果的返回,在Call類的callComplete()方法里有notify()方法用于喚醒線程
} catch (InterruptedException ie) {
// 因中斷異常而終止,設(shè)置標志interrupted為true
interrupted = true;
}
}
if (interrupted) {
Thread.currentThread().interrupt();
}
if (call.error != null) {
if (call.error instanceof RemoteException) {
call.error.fillInStackTrace();
throw call.error;
} else { // 本地異常
throw wrapException(remoteId.getAddress(), call.error);
}
} else {
return call.value; //返回結(jié)果數(shù)據(jù)
}
}
}
具體代碼的作用我已做了注釋,所以這里不再贅述。但到目前為止,你依然不知道RPC機制底層的網(wǎng)絡(luò)連接是怎么建立的。分析代碼后,我們會發(fā)現(xiàn)和網(wǎng)絡(luò)通信有關(guān)的代碼只會是下面的兩句了:
Connection connection = getConnection(remoteId, call); //獲得一個連接
connection.sendParam(call); // 向服務(wù)端發(fā)送call對象
先看看是怎么獲得一個到服務(wù)端的連接吧,下面貼出ipc.Client類中的getConnection()方法。
private Connection getConnection(ConnectionId remoteId,
Call call)
throws IOException, InterruptedException {
if (!running.get()) {
// 如果client關(guān)閉了
throw new IOException("The client is stopped");
}
Connection connection;
//如果connections連接池中有對應(yīng)的連接對象,就不需重新創(chuàng)建了;如果沒有就需重新創(chuàng)建一個連接對象。
//但請注意,該//連接對象只是存儲了remoteId的信息,其實還并沒有和服務(wù)端建立連接。
do {
synchronized (connections) {
connection = connections.get(remoteId);
if (connection == null) {
connection = new Connection(remoteId);
connections.put(remoteId, connection);
}
}
} while (!connection.addCall(call)); //將call對象放入對應(yīng)連接中的calls池,就不貼出源碼了
//這句代碼才是真正的完成了和服務(wù)端建立連接哦~
connection.setupIOstreams();
return connection;
}
下面貼出Client.Connection類中的setupIOstreams()方法:
private synchronized void setupIOstreams() throws InterruptedException {
???
try {
???
while (true) {
setupConnection(); //建立連接
InputStream inStream = NetUtils.getInputStream(socket); //獲得輸入流
OutputStream outStream = NetUtils.getOutputStream(socket); //獲得輸出流
writeRpcHeader(outStream);
???
this.in = new DataInputStream(new BufferedInputStream
(new PingInputStream(inStream))); //將輸入流裝飾成DataInputStream
this.out = new DataOutputStream
(new BufferedOutputStream(outStream)); //將輸出流裝飾成DataOutputStream
writeHeader();
// 跟新活動時間
touch();
//當(dāng)連接建立時,啟動接受線程等待服務(wù)端傳回數(shù)據(jù),注意:Connection繼承了Tread
start();
return;
}
} catch (IOException e) {
markClosed(e);
close();
}
}
再有一步我們就知道客戶端的連接是怎么建立的啦,下面貼出Client.Connection類中的setupConnection()方法:
private synchronized void setupConnection() throws IOException {
short ioFailures = 0;
short timeoutFailures = 0;
while (true) {
try {
this.socket = socketFactory.createSocket(); //終于看到創(chuàng)建socket的方法了
this.socket.setTcpNoDelay(tcpNoDelay);
???
// 設(shè)置連接超時為20s
NetUtils.connect(this.socket, remoteId.getAddress(), 20000);
this.socket.setSoTimeout(pingInterval);
return;
} catch (SocketTimeoutException toe) {
/* 設(shè)置最多連接重試為45次。
* 總共有20s*45 = 15 分鐘的重試時間。
*/
handleConnectionFailure(timeoutFailures++, 45, toe);
} catch (IOException ie) {
handleConnectionFailure(ioFailures++, maxRetries, ie);
}
}
}
終于,我們知道了客戶端的連接是怎樣建立的了,其實就是創(chuàng)建一個普通的socket進行通信。
3.2 客戶端是怎樣給服務(wù)端發(fā)送數(shù)據(jù)的?
下面貼出Client.Connection類的sendParam()方法吧:
public void sendParam(Call call) {
if (shouldCloseConnection.get()) {
return;
}
DataOutputBuffer d=null;
try {
synchronized (this.out) {
if (LOG.isDebugEnabled())
LOG.debug(getName() + " sending #" + call.id);
//創(chuàng)建一個緩沖區(qū)
d = new DataOutputBuffer();
d.writeInt(call.id);
call.param.write(d);
byte[] data = d.getData();
int dataLength = d.getLength();
out.writeInt(dataLength); //首先寫出數(shù)據(jù)的長度
out.write(data, 0, dataLength); //向服務(wù)端寫數(shù)據(jù)
out.flush();
}
} catch(IOException e) {
markClosed(e);
} finally {
IOUtils.closeStream(d);
}
}
3.3 客戶端是怎樣獲取服務(wù)端的返回數(shù)據(jù)的?
下面貼出Client.Connection類和Client.Call類中的相關(guān)方法:
方法一:
public void run() {
???
while (waitForWork()) {
receiveResponse(); //具體的處理方法
}
close();
???
}
方法二:
private void receiveResponse() {
if (shouldCloseConnection.get()) {
return;
}
touch();
try {
int id = in.readInt(); // 阻塞讀取id
if (LOG.isDebugEnabled())
LOG.debug(getName() + " got value #" + id);
Call call = calls.get(id); //在calls池中找到發(fā)送時的那個對象
int state = in.readInt(); // 阻塞讀取call對象的狀態(tài)
if (state == Status.SUCCESS.state) {
Writable value = ReflectionUtils.newInstance(valueClass, conf);
value.readFields(in); // 讀取數(shù)據(jù)
//將讀取到的值賦給call對象,同時喚醒Client等待線程,貼出setValue()代碼方法三
call.setValue(value);
calls.remove(id); //刪除已處理的call
} else if (state == Status.ERROR.state) {
???
} else if (state == Status.FATAL.state) {
???
}
} catch (IOException e) {
markClosed(e);
}
}
方法三:
public synchronized void setValue(Writable value) {
this.value = value;
callComplete(); //具體實現(xiàn)
}
protected synchronized void callComplete() {
this.done = true;
notify(); // 喚醒client等待線程
}
完成的功能主要是:啟動一個處理線程,讀取從服務(wù)端傳來的call對象,將call對象讀取完畢后,喚醒client處理線程。就這么簡單,客戶端就獲取了服務(wù)端返回的數(shù)據(jù)了哦~。客戶端的源碼分析就到這里了哦,下面我們來分析Server端的源碼吧。
3.4 ipc.Server源碼分析
為了讓大家對ipc.Server有個初步的了解,我們先分析一下它的幾個內(nèi)部類吧:
Call :用于存儲客戶端發(fā)來的請求
Listener : 監(jiān)聽類,用于監(jiān)聽客戶端發(fā)來的請求,同時Listener內(nèi)部還有一個靜態(tài)類,Listener.Reader,當(dāng)監(jiān)聽器監(jiān)聽到用戶請求,便讓Reader讀取用戶請求。
Responder :響應(yīng)RPC請求類,請求處理完畢,由Responder發(fā)送給請求客戶端。
Connection :連接類,真正的客戶端請求讀取邏輯在這個類中。
Handler :請求處理類,會循環(huán)阻塞讀取callQueue中的call對象,并對其進行操作。
private void initialize(Configuration conf) throws IOException {
???
// 創(chuàng)建 rpc server
InetSocketAddress dnSocketAddr = getServiceRpcServerAddress(conf);
if (dnSocketAddr != null) {
int serviceHandlerCount =
conf.getInt(DFSConfigKeys.DFS_NAMENODE_SERVICE_HANDLER_COUNT_KEY,
DFSConfigKeys.DFS_NAMENODE_SERVICE_HANDLER_COUNT_DEFAULT);
//獲得serviceRpcServer
this.serviceRpcServer = RPC.getServer(this, dnSocketAddr.getHostName(),
dnSocketAddr.getPort(), serviceHandlerCount,
false, conf, namesystem.getDelegationTokenSecretManager());
this.serviceRPCAddress = this.serviceRpcServer.getListenerAddress();
setRpcServiceServerAddress(conf);
}
//獲得server
this.server = RPC.getServer(this, socAddr.getHostName(),
socAddr.getPort(), handlerCount, false, conf, namesystem
.getDelegationTokenSecretManager());
???
this.server.start(); //啟動 RPC server Clients只允許連接該server
if (serviceRpcServer != null) {
serviceRpcServer.start(); //啟動 RPC serviceRpcServer 為HDFS服務(wù)的server
}
startTrashEmptier(conf);
}
查看Namenode初始化源碼得知:RPC的server對象是通過ipc.RPC類的getServer()方法獲得的。下面咱們?nèi)タ纯磇pc.RPC類中的getServer()源碼吧:
public static Server getServer(final Object instance, final String bindAddress, final int port,
final int numHandlers,
final boolean verbose, Configuration conf,
SecretManager<? extends TokenIdentifier> secretManager)
throws IOException {
return new Server(instance, conf, bindAddress, port, numHandlers, verbose, secretManager);
}
這時我們發(fā)現(xiàn)getServer()是一個創(chuàng)建Server對象的工廠方法,但創(chuàng)建的卻是RPC.Server類的對象。哈哈,現(xiàn)在你明白了我前面說的“RPC.Server是ipc.Server的實現(xiàn)類”了吧。不過RPC.Server的構(gòu)造函數(shù)還是調(diào)用了ipc.Server類的構(gòu)造函數(shù)的,因篇幅所限,就不貼出相關(guān)源碼了。
初始化Server后,Server端就運行起來了,看看ipc.Server的start()源碼吧:
/** 啟動服務(wù) */
public synchronized void start() {
responder.start(); //啟動responder
listener.start(); //啟動listener
handlers = new Handler[handlerCount];
for (int i = 0; i < handlerCount; i++) {
handlers[i] = new Handler(i);
handlers[i].start(); //逐個啟動Handler
}
}
分析過ipc.Client源碼后,我們知道Client端的底層通信直接采用了阻塞式IO編程,當(dāng)時我們曾做出猜測:Server端是不是也采用了阻塞式IO。現(xiàn)在我們仔細地分析一下吧,如果Server端也采用阻塞式IO,當(dāng)連接進來的Client端很多時,勢必會影響Server端的性能。hadoop的實現(xiàn)者們考慮到了這點,所以他們采用了java NIO來實現(xiàn)Server端,那Server端采用java NIO是怎么建立連接的呢?分析源碼得知,Server端采用Listener監(jiān)聽客戶端的連接,下面先分析一下Listener的構(gòu)造函數(shù)吧:
public Listener() throws IOException {
address = new InetSocketAddress(bindAddress, port);
// 創(chuàng)建ServerSocketChannel,并設(shè)置成非阻塞式
acceptChannel = ServerSocketChannel.open();
acceptChannel.configureBlocking(false);
// 將server socket綁定到本地端口
bind(acceptChannel.socket(), address, backlogLength);
port = acceptChannel.socket().getLocalPort();
// 獲得一個selector
selector= Selector.open();
readers = new Reader[readThreads];
readPool = Executors.newFixedThreadPool(readThreads);
//啟動多個reader線程,為了防止請求多時服務(wù)端響應(yīng)延時的問題
for (int i = 0; i < readThreads; i++) {
Selector readSelector = Selector.open();
Reader reader = new Reader(readSelector);
readers[i] = reader;
readPool.execute(reader);
}
// 注冊連接事件
acceptChannel.register(selector, SelectionKey.OP_ACCEPT);
this.setName("IPC Server listener on " + port);
this.setDaemon(true);
}
在啟動Listener線程時,服務(wù)端會一直等待客戶端的連接,下面貼出Server.Listener類的run()方法:
public void run() {
???
while (running) {
SelectionKey key = null;
try {
selector.select();
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
key = iter.next();
iter.remove();
try {
if (key.isValid()) {
if (key.isAcceptable())
doAccept(key); //具體的連接方法
}
} catch (IOException e) {
}
key = null;
}
} catch (OutOfMemoryError e) {
???
}
下面貼出Server.Listener類中doAccept()方法中的關(guān)鍵源碼吧:
void doAccept(SelectionKey key) throws IOException, OutOfMemoryError {
Connection c = null;
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel channel;
while ((channel = server.accept()) != null) { //建立連接
channel.configureBlocking(false);
channel.socket().setTcpNoDelay(tcpNoDelay);
Reader reader = getReader(); //從readers池中獲得一個reader
try {
reader.startAdd(); // 激活readSelector,設(shè)置adding為true
SelectionKey readKey = reader.registerChannel(channel);//將讀事件設(shè)置成興趣事件
c = new Connection(readKey, channel, System.currentTimeMillis());//創(chuàng)建一個連接對象
readKey.attach(c); //將connection對象注入readKey
synchronized (connectionList) {
connectionList.add(numConnections, c);
numConnections++;
}
???
} finally {
//設(shè)置adding為false,采用notify()喚醒一個reader,其實代碼十三中啟動的每個reader都使
//用了wait()方法等待。因篇幅有限,就不貼出源碼了。
reader.finishAdd();
}
}
}
當(dāng)reader被喚醒,reader接著執(zhí)行doRead()方法。
下面貼出Server.Listener.Reader類中的doRead()方法和Server.Connection類中的readAndProcess()方法源碼:
方法一:
void doRead(SelectionKey key) throws InterruptedException {
int count = 0;
Connection c = (Connection)key.attachment(); //獲得connection對象
if (c == null) {
return;
}
c.setLastContact(System.currentTimeMillis());
try {
count = c.readAndProcess(); // 接受并處理請求
} catch (InterruptedException ieo) {
???
}
???
}
方法二:
public int readAndProcess() throws IOException, InterruptedException {
while (true) {
???
if (!rpcHeaderRead) {
if (rpcHeaderBuffer == null) {
rpcHeaderBuffer = ByteBuffer.allocate(2);
}
//讀取請求頭
count = channelRead(channel, rpcHeaderBuffer);
if (count < 0 || rpcHeaderBuffer.remaining() > 0) {
return count;
}
// 讀取請求版本號
int version = rpcHeaderBuffer.get(0);
byte[] method = new byte[] {rpcHeaderBuffer.get(1)};
???
data = ByteBuffer.allocate(dataLength);
}
// 讀取請求
count = channelRead(channel, data);
if (data.remaining() == 0) {
???
if (useSasl) {
???
} else {
processOneRpc(data.array());//處理請求
}
???
}
}
return count;
}
}
下面貼出Server.Connection類中的processOneRpc()方法和processData()方法的源碼。
方法一:
private void processOneRpc(byte[] buf) throws IOException,
InterruptedException {
if (headerRead) {
processData(buf);
} else {
processHeader(buf);
headerRead = true;
if (!authorizeConnection()) {
throw new AccessControlException("Connection from " + this
+ " for protocol " + header.getProtocol()
+ " is unauthorized for user " + user);
}
}
}
方法二:
private void processData(byte[] buf) throws IOException, InterruptedException {
DataInputStream dis =
new DataInputStream(new ByteArrayInputStream(buf));
int id = dis.readInt(); // 嘗試讀取id
Writable param = ReflectionUtils.newInstance(paramClass, conf);//讀取參數(shù)
param.readFields(dis);
Call call = new Call(id, param, this); //封裝成call
callQueue.put(call); // 將call存入callQueue
incRpcCount(); // 增加rpc請求的計數(shù)
}
4. RPC與web service
RPC:
Web service
web service接口就是RPC中的stub組件,規(guī)定了server能夠提供的服務(wù)(web service),這在server和client上是一致的,但是也是跨語言跨平臺的。同時,由于web service規(guī)范中的WSDL文件的存在,現(xiàn)在各平臺的web service框架,都可以基于WSDL文件,自動生成web service接口 。
其實兩者差不多,只是傳輸?shù)膮f(xié)議不同。
Reference: