RMI簡單介紹

謎一樣的Coder發表於2018-08-16

前言

這篇部落格主要介紹RMI(Remote Method Invocation)遠端方法呼叫,這個東西現在用的不多,只是作為分散式基礎,進行一個簡單瞭解,為了引出RPC的相關概念,並對RPC做一個簡單認識,方便後面學習Dubbo

RMI簡介

RMI全稱是remote method invocation 遠端方法呼叫,一種用於遠端過程呼叫的應用程式程式設計介面,是純java的網路分散式應用系統的核心解決方案之一。RMI使用Java遠端訊息交換協議JRMP(Java Remote Messaging Protocol)進行通訊,JRMP是專門為Java物件制定的,對非Java語言開發的應用系統支援不足,不能與非java語言的物件進行通訊。

RMI簡單實現

目前瞭解的RMI好像有兩種實現方式,一種是單獨執行登錄檔,另一種是在伺服器端執行登錄檔,由於本身這裡的RMI只是為了方便自己理解RPC框架,這裡不再糾結於那種方式,而是簡單採用手動操作,能完成呼叫就算成功,至於深入理解,後面會補上。

1、建立服務端專案rmi-server,建立相應的包,然後編寫對外的服務介面

/**
 * 
 * @author liman
 * @createtime 2018年8月16日
 * @contract 15528212893
 * @comment:
 * 建立遠端介面,需要繼承至remote
 */
public interface PersonService extends Remote{
	
	public List<PersonEntity> GetList() throws RemoteException;

}

該介面需要實現Remote介面,這個介面是個標記介面,由於遠端方法呼叫的本質是網路通訊,只是隱藏了底層實現。網路通訊出現異常需要處理,所以所有的方法都必須要丟擲RemoteException以說明該方法可能丟擲網路通訊異常。

2、編寫遠端方法介面實現類

package com.learn.rmi.self.JRMP;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.LinkedList;
import java.util.List;

/**
 * 
 * @author liman
 * @createtime 2018年8月16日
 * @contract 15528212893
 * @comment:
 * 遠端服務介面的實現類,這個實現類需要繼承UnicastRemoteObject並實現PersonService介面
 */
public class PersonServiceImpl extends UnicastRemoteObject implements PersonService{

	protected PersonServiceImpl() throws RemoteException {
		super();
	}

	public List<PersonEntity> GetList() throws RemoteException {
		System.out.println("Get Person Start!");
		List<PersonEntity> personList=new LinkedList<PersonEntity>();

		PersonEntity person1=new PersonEntity();
		person1.setAge(25);
		person1.setId(0);
		person1.setName("Leslie");
		personList.add(person1);

		PersonEntity person2=new PersonEntity();
		person2.setAge(25);
		person2.setId(1);
		person2.setName("Rose");
		personList.add(person2);

		return personList;
	}

}

該方法的方法引數和返回值會在網路上傳輸,所以必須要實現序列化介面。

3、建立客戶端專案,rmi-client

將前兩部的服務端介面程式碼(PersonService)和實體(PersonEntity)拷貝到客戶端中,需要保證package路徑與服務端一致。首先承認這步操作有點騷氣,一般這個步驟在之前的開發中都會有java編譯器自帶的RMI註冊服務完成。

4、編寫服務端的服務釋出程式

package com.learn.rmi.self.JRMP;

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

/**
 * 
 * @author liman
 * @createtime 2018年8月16日
 * @contract 15528212893
 * @comment:
 * 服務端註冊通訊埠
 */
public class ServerProgram {
	public static void main(String[] args) {
		try {
			PersonService personService = new PersonServiceImpl();
			
			//註冊通訊埠
			LocateRegistry.createRegistry(6600);
			//註冊通訊路徑
			Naming.rebind("rmi://127.0.0.1:6600/PersonService", personService);
			
			System.out.println("Service start");
		} catch (Exception e) {
			e.printStackTrace();
		} 
	}
}

LocateRegistry.createRegistry(6600);——註冊通訊埠。Naming.rebind()——註冊通訊路徑。

5、編寫客戶端程式碼

在確認將服務端的service介面和對應的實體類拷貝過來之後,編寫客戶端程式碼

package com.learn.rmi.self.JRMP;

import java.rmi.Naming;
import java.util.List;

/**
 * 
 * @author liman
 * @createtime 2018年8月16日
 * @contract 15528212893
 * @comment:
 * 客戶端進行測試
 */
public class ClientProgram {
	public static void main(String[] args) {
		try {
			PersonService personService = (PersonService)Naming.lookup("rmi://127.0.0.1:6600/PersonService");
			
			List<PersonEntity> personList = personService.GetList();
			
			for(PersonEntity person:personList) {
				System.out.println("ID:"+person.getId()+" Age:"+person.getAge()+" Name:"+person.getName());
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

6、測試執行

先執行服務端程式碼,完成之後控制檯輸出如下結果,表示服務端啟動成功

然後啟動客戶端:

 客戶端沒有維護PersonService的邏輯,只是保留了服務端的介面資訊,客戶端能成功呼叫相應的服務。不過有幾個細節需要注意。

1、涉及到的序列化物件需要實現Serializable介面,這裡的就是PersonEntity物件。

2、客戶端和服務端PersonEntity中的serialVersionUID的值要一致,否則會丟擲二者不一致的異常。關於serialVersionUID這個欄位的作用,在序列化一文中有過總結,但是不夠全面,這裡可以參看這篇文章——serialVersionUID欄位的作用

RMI底層通訊(取消註冊中心)

其實RMI的原理和RPC框架很像,前幾天在看原始碼的過程中,終於成功被原始碼繞暈,看來還是先只能簡單實現,原始碼的理解後面再深入。

分為客戶端和服務端,底層實質就是序列化和反序列化的通訊。

客戶端程式碼

1、客戶端測試程式碼

主要通過獲取服務端遠端的物件和呼叫結果

package com.learn.rmi.rpc;

/**
 * Created by liman on 2018/8/18.
 * QQ:657271181
 * e-mail:liman65727@sina.com
 */
public class ClientDemo {

    public static void main(String[] args) {
        RpcClientProxy rpcClientProxy = new RpcClientProxy();

        ILearnHello learnHello = rpcClientProxy.clientProxy(ILearnHello.class,"localhost",8888);
        System.out.println(learnHello.sayHello("liman"));
    }

}

2、RpcClientProxy代理類

package com.learn.rmi.rpc;

import com.learn.rmi.ref.rpc.RemoteInvocationHandler;

import java.lang.reflect.Proxy;

/**
 * Created by liman on 2018/8/18.
 * QQ:657271181
 * e-mail:liman65727@sina.com
 *
 * 客戶端的代理
 */
public class RpcClientProxy {

    public <T> T clientProxy(final Class<T> interfaceClass,final String host,final int port){
        return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(),
                new Class[]{interfaceClass}, new RemoteInvocationHandler(host,port));
    }

}

3、RemoteInvocationHandler類,設定請求物件中需要呼叫的目標方法和引數,並完成請求的傳送和結果獲取

package com.learn.rmi.rpc;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * Created by liman on 2018/8/18.
 * QQ:657271181
 * e-mail:liman65727@sina.com
 *
 * 需要發起客戶端和服務端的遠端呼叫。
 */
public class RemoteInvocationHandler implements InvocationHandler{

    private String host;
    private int port;

    public RemoteInvocationHandler(String host, int port) {
        this.host = host;
        this.port = port;
    }

    /**
     * 需要跟遠端發起呼叫
     * 設定了需要呼叫的遠端方法資訊,然後釋出到遠端
     * @param proxy
     * @param method
     * @param args
     * @return
     * @throws Throwable
     */
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        RpcRequest request = new RpcRequest();
        request.setClassName(method.getDeclaringClass().getName());
        request.setMethodName(method.getName());
        request.setParameters(args);

        TCPTransport tcpTransport = new TCPTransport(this.host,this.port);

        return tcpTransport.send(request);
    }
}

4、TCPTransport,真正的請求傳送

package com.learn.rmi.rpc;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;

/**
 * Created by liman on 2018/8/18.
 * QQ:657271181
 * e-mail:liman65727@sina.com
 * <p>
 * 專門管理socket
 */
public class TCPTransport {

    private String host;
    private int port;

    public TCPTransport(String host, int port) {
        this.host = host;
        this.port = port;
    }

    //建立一個新的連線
    private Socket newSocket() {
        System.out.println("建立一個新連線");

        Socket socket = null;
        try {
            socket = new Socket(host, port);
            return socket;
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("建立連線異常!");
        }
    }

    public Object send(RpcRequest request) {
        Socket socket = null;
        try {
            socket = newSocket();
            ObjectOutputStream outputStream = new ObjectOutputStream
                    (socket.getOutputStream());
            outputStream.writeObject(request);
            outputStream.flush();

            //處理返回的結果
            ObjectInputStream inputStream = new ObjectInputStream
                    (socket.getInputStream());
            Object result = inputStream.readObject();
            inputStream.close();
            outputStream.close();
            return result;
        } catch (Exception e) {
            throw new RuntimeException("發起服務呼叫異常");
        } finally {
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

5、遠端目標的介面

package com.learn.rmi.rpc;

/**
 * Created by liman on 2018/8/18.
 * QQ:657271181
 * e-mail:liman65727@sina.com
 */
public interface ILearnHello {

    public String sayHello(String msg);

}

6、請求物件,客戶端主要完成的任務就是封裝一個RPCRequest請求物件,然後傳送到遠端伺服器,這不操作在真正的RPC框架中並不是這麼實現的,後續需要深入學習這一點。

package com.learn.rmi.rpc;

import java.io.Serializable;

/**
 * Created by liman on 2018/8/18.
 * QQ:657271181
 * e-mail:liman65727@sina.com
 *
 * 傳輸物件
 *
 */
public class RpcRequest implements Serializable{

    private static final long serialVersionUID = 903071532920235263L;

    private String className;
    private String methodName;
    private Object[] parameters;

    public String getClassName() {
        return className;
    }

    public void setClassName(String className) {
        this.className = className;
    }

    public String getMethodName() {
        return methodName;
    }

    public void setMethodName(String methodName) {
        this.methodName = methodName;
    }

    public Object[] getParameters() {
        return parameters;
    }

    public void setParameters(Object[] parameters) {
        this.parameters = parameters;
    }
}

服務端程式碼

1、業務介面

package com.learn.rmi.rpc;

/**
 * Created by liman on 2018/8/18.
 * QQ:657271181
 * e-mail:liman65727@sina.com
 */
public interface ILearnHello {

    public String sayHello(String msg);

}

2、業務介面實現類,這個和介面都需要放在與客戶端同一個目錄名稱下

package com.learn.rmi.rpc;

/**
 * Created by liman on 2018/8/18.
 * QQ:657271181
 * e-mail:liman65727@sina.com
 */
public class LearnHelloImpl implements ILearnHello{

    @Override
    public String sayHello(String msg) {
        return "Hello "+msg+" this is remote service";
    }
}

3、RPCRequest,由於這裡是最簡陋的實現,需要服務端維護一個RPCRequest物件,同樣需要與客戶端放在相同目錄下

package com.learn.rmi.rpc;

import java.io.Serializable;

/**
 * Created by liman on 2018/8/18.
 * QQ:657271181
 * e-mail:liman65727@sina.com
 *
 * 傳輸物件
 *
 */
public class RpcRequest implements Serializable{

    private static final long serialVersionUID = 903071532920235263L;

    private String className;
    private String methodName;
    private Object[] parameters;

    public String getClassName() {
        return className;
    }

    public void setClassName(String className) {
        this.className = className;
    }

    public String getMethodName() {
        return methodName;
    }

    public void setMethodName(String methodName) {
        this.methodName = methodName;
    }

    public Object[] getParameters() {
        return parameters;
    }

    public void setParameters(Object[] parameters) {
        this.parameters = parameters;
    }
}

4、將服務釋出到指定的埠上(即使在指定的埠上處理請求)

package com.learn.rmi.rpc;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Created by liman on 2018/8/18.
 * QQ:657271181
 * e-mail:liman65727@sina.com
 *
 * 負責將服務釋出出去
 */
public class RpcServer {

    //定義一個執行緒池
    private final ExecutorService executorService = Executors.newCachedThreadPool();

    /**
     * 將指定的服務釋出出去
     * @param service 待發布的服務
     * @param port 埠
     */
    public void publisher(final Object service,int port){
        ServerSocket serverSocket = null;
        try {
            //啟動一個服務監聽
            serverSocket = new ServerSocket(port);
            System.out.println("服務開啟,等待客戶端請求");
            //迴圈監聽服務
            while(true){
                //迴圈處理客戶端請求
                Socket socket = serverSocket.accept();
                executorService.execute(new ProcessorHandler(socket,service));
            }

        } catch (IOException e) {
            e.printStackTrace();
        }finally{
            if(serverSocket!=null){
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

5、ProcessHandler類,真正的處理請求程式碼

package com.learn.rmi.rpc;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.Socket;

/**
 * Created by liman on 2018/8/18.
 * QQ:657271181
 * e-mail:liman65727@sina.com
 */
public class ProcessorHandler implements Runnable{

    //待發布的服務
    private Socket socket;
    private Object service;

    public ProcessorHandler(Socket socket, Object service) {
        this.socket = socket;
        this.service = service;
    }

    @Override
    public void run() {
        //處理客戶端的請求,這裡採用多執行緒的方式完成
        ObjectInputStream objectInputStream = null;
        try {
            objectInputStream = new ObjectInputStream(socket.getInputStream());

            //獲取遠端傳輸的物件,通過反序列化獲取
            RpcRequest request = (RpcRequest) objectInputStream.readObject();

            //處理結果
            Object result = invoke(request);

            //將結果輸出到客戶端
            ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
            outputStream.writeObject(result);

            //各種流的簡單關閉,這裡就沒有在finally中關閉
            outputStream.flush();
            objectInputStream.close();
            outputStream.close();

        } catch (Exception e) {
            e.printStackTrace();
        }finally{
            if(objectInputStream!=null){
                try {
                    objectInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    /**
     * 通過反射獲取客戶端需要呼叫的方法,並將結果返回
     * @param request
     * @return
     */
    private Object invoke(RpcRequest request) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Object[] args = request.getParameters();

        Class<?>[] types = new Class[args.length];
        for(int i = 0;i<args.length;i++){
            types[i] = args[i].getClass();
        }

        //獲取方法資訊
        Method method = service.getClass().getMethod(request.getMethodName(),types);

        return method.invoke(service,args);
    }
}

6、服務端測試類

package com.learn.rmi.rpc;

/**
 * Created by liman on 2018/8/19.
 * QQ:657271181
 * e-mail:liman65727@sina.com
 */
public class ServerDemo {

    public static void main(String[] args) {
        ILearnHello iLearnHello = new LearnHelloImpl();

        RpcServer rpcServer = new RpcServer();
        rpcServer.publisher(iLearnHello,8889);
    }

}

執行結果

服務端執行結果

客戶端執行結果

 說明

上述針對RMI的總結十分簡單,後面針對RMI的實現也是十分簡單,直接省去了註冊中心這一步。後續需要學習深入