JAVA RPC: http://jbm3072.iteye.com/blog/1088102
目標:讓客戶端調用遠程機器(不同JVM上)的方法.
技術:RPC(Remote Process Call遠程過程調用)
優點:使用RPC,可以像使用本地的程序(本地JVM)一樣使用遠程服務器上的程序。使用RPC的好處是簡化了遠程服務訪問。提高了開發效率。
做法:在分發代碼時,只需要將接口分發給客戶端使用,在客戶端看來只有接口,沒有具體類實現。這樣保證了代碼的可擴展性和安全性。
基礎:Java反射機制,動態代理,Java IO/NIO/Socket
Server接口
public interface Server {
public void stop();
public void start();
public void register(Class interfaceDefiner,Class impl);
public void call(Invocation invo);
public boolean isRunning();
public int getPort();
}
啓動服務器
Server server = new RPC.RPCServer();
server.register(Echo.class, RemoteEcho.class);
server.start(); // 1.向服務器註冊接口和實現類並啓動服務器
RPC
public class RPC {
public static <T> T getProxy(final Class<T> clazz,String host,int port) {
final Client client = new Client(host,port);
InvocationHandler handler = new InvocationHandler() {
// 5. 當客戶端調用生成的代理對象的方法,實際上調用的是該回調方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Invocation invo = new Invocation(); // 封裝Invocation對象,要調用的接口,接口的方法,參數
invo.setInterfaces(clazz);
invo.setMethod(new com.xuyuan.j2ee.rpc.protocal.Method(method.getName(),method.getParameterTypes()));
invo.setParams(args);
client.invoke(invo); // 6. 客戶端向服務器發送Invocation對象
return invo.getResult(); // 12. 回調方法invoke()結束,返回遠程方法的執行結果
}
};
// 3. 生成接口的代理對象,傳入回調對象InvocationHandler.在調用接口的方法時,會調用回調方法
T t = (T) Proxy.newProxyInstance(RPC.class.getClassLoader(), new Class[] {clazz}, handler);
return t;
}
public static class RPCServer implements Server{
private Listener listener;
private Map<String ,Object> serviceEngine = new HashMap<String, Object>();
public void call(Invocation invo) {
Object obj = serviceEngine.get(invo.getInterfaces().getName()); //根據接口名,找到對應的處理類(實現類)
Method m = obj.getClass().getMethod(invo.getMethod().getMethodName(), invo.getMethod().getParams());
Object result = m.invoke(obj, invo.getParams()); // 9. 利用反射,調用方法,返回值設置到Invocation對象中
invo.setResult(result);
}
public void register(Class interfaceDefiner, Class impl) {
this.serviceEngine.put(interfaceDefiner.getName(), impl.newInstance());
}
public void start() {
listener = new Listener(this);
this.isRuning = true;
listener.start(); // 1.啓動服務器,監聽器是個線程類,會調用run()
}
}
}
監聽器
public class Listener extends Thread {
private ServerSocket socket;
private Server server;
public void run() {
socket = new ServerSocket(server.getPort()); // 2. 創建ServerSocket,接受客戶端的連接
while (server.isRunning()) {
Socket client = socket.accept();
// 8. 接收客戶端傳遞的Invocation對象,裏面包含了客戶端想要調用的接口,方法,參數
ObjectInputStream ois = new ObjectInputStream(client.getInputStream());
Invocation invo = (Invocation) ois.readObject();
// 9. 讓服務器調用真正的目標方法
server.call(invo);
// 10. 往客戶端寫回數據,同樣給客戶端發送Invocation對象
ObjectOutputStream oos = new ObjectOutputStream(client.getOutputStream());
oos.writeObject(invo);
}
}
}
客戶端
public class Client {
private Socket socket;
private ObjectOutputStream oos;
private ObjectInputStream ois;
public void invoke(Invocation invo) throws UnknownHostException, IOException, ClassNotFoundException {
socket = new Socket(host, port);
oos = new ObjectOutputStream(socket.getOutputStream());
// 7. 客戶端向服務器寫數據。因爲客戶端不能直接調用接口方法(在不同JVM上),
// 可以通過傳遞帶有接口,方法,參數的Invocation對象給服務器,讓服務器解析出對象並真正調用方法
oos.writeObject(invo);
// 11. 接收服務器返回的數據Invocation對象,對象裏也含有方法的執行結果
ois = new ObjectInputStream(socket.getInputStream());
Invocation result = (Invocation) ois.readObject();
invo.setResult(result.getResult());
}
}
啓動客戶端,調用接口的方法
Echo echo = RPC.getProxy(Echo.class, "127.0.0.1", 20382); // 3. 生成接口Echo的代理實現類
String res = echo.echo("hello,hello"); // 4. 像使用本地的程序一樣來調用Echo中的echo方法
總結:
因爲客戶端和服務器位於不同的JVM上(不同的機器),要讓客戶端能夠調用到服務器上的某個類的某個方法,需要讓客戶端和服務器能夠進行遠程通信。
遠程通信需要協議。 這裏的協議就是Echo接口。 暴露給客戶端的是Echo接口的某個方法。在服務器上有Echo接口的具體實現。
所以我們的目的是客戶端能夠調用到位於服務器上的Echo接口的實現類的某個方法。
Invocation:
負責客戶端和服務器的數據交互,即客戶端想要調用哪個接口,哪個方法,方法帶有什麼參數。以及服務器返回給客戶端方法的執行結果。
需要實現序列化,因爲要在網絡環境下傳輸。
InvocationHandler:
回調對象,客戶端調用接口的方法,實際上會調用回調方法invoke()
在回調方法裏,封裝了客戶端要調用的接口,方法,參數。封裝成Invocation對象
回調方法結束,客戶端調用接口的方法的過程也就結束了。具體是怎麼調用接口的方法:
Client:
將上面封裝好的Invocation對象,通過Socket或者NIO編程發送給服務器。
而服務器的監聽器一直在監聽客戶端的數據
將封裝了接口,方法,參數的Invocation發送給服務器的目的是讓服務器調用位於服務器上的接口的實現類的方法。
Listener:
負責監聽客戶端的寫入數據, ==》監聽到客戶端發送的Invocation對象,讀取出來
委託給Server調用客戶端想要調用的方法, ==》Invocation對象包含了客戶端想要調用的接口,方法,參數
調用方法結束後,向客戶端回寫數據 ==》調用方法的返回值也一併封裝到Invocation對象傳輸給客戶端
Server:
取得客戶端發送的Invocation對象後,解析出接口,方法,參數。這樣服務器就知道了客戶端想要調用的接口和方法
這裏還有一個過程,就是在服務器啓動的時候,要先註冊接口和接口的實現類的關係。
這樣當客戶端傳遞含有接口名字(也只能傳遞接口)的Invocation對象時,服務器就能知道該接口對應的實現類。
利用反射機制,真正調用到服務器上的接口的實現類的方法,並傳入參數
Client:
服務器調用接口實現類的方法結束後,還會返回Invocation對象給客戶端。
客戶端同樣能解析出Invocation對象,取得返回值。這樣客戶端僅僅和接口打交道,隱藏了數據交互的過程。
服務器啓動時序圖
客戶端連接服務器時序圖
Hadoop的RPC採用客戶機/服務器模式。請求程序就是一個客戶機, 而服務提供程序就是一個服務器。當我們討論HDFS時, 通信可能發生在:
情景 |
服務器 |
Client-NameNode |
NameNode |
Client-DataNode |
DataNode |
DataNode-NameNode |
NameNode |
DataNode-DateNode |
某一個DateNode是服務器, 另一個是客戶端 |
如果我們考慮Hadoop的Map/Reduce以後, 這些系統間的通信就更復雜了。爲了解決這些客戶機/服務器之間的通信, Hadoop引入了一個RPC框架。該RPC框架利用的Java的反射能力, 避免了某些RPC解決方案中需要根據某種接口語言(如CORBA的IDL)生成存根和框架的問題。
IPC
實現RPC的一種方法,具有快速、簡單的特點。 它不像Sun公司提供的標準RPC包,基於Java序列化。
IPC無需創建網絡stubs和skeletons。
IPC中的方法調用要求參數和返回值的數據類型必須是Java的基本類型,String和Writable接口的實現類,以及元素爲以上類型的數組。
接口方法應該只拋出IOException異常。
使用模型 採用客戶/服務器模型
Server:它把Java接口暴露給客戶端。指定好監聽端口和接受遠程調用的對象實例後,通過RPC.getServer()可以得到Server實例。
Client:連接Server,調用它所暴露的方法。Client必須指定遠程機器的地址,端口和Java接口類,通過RPC.getClient()可以得到Client實例。
Server不可以向Client發出調用,但在Hadoop中,有雙向調用的需求。 比如在DFS,NameNode和DataNode需要互相瞭解狀態。