用java序列化和阻塞IO模型實現RPC

RPC是遠程過程調用,對於java而言,就是兩個JVM通信,一個JVM a想要調用另一個JVM b中的類。b把執行結果在發送給a的過程。好,我們就是要來實現這個過程。
兩個接口:

public interface IDiff {
    double diff(double a,double b);
}

public interface ISum {
    public int sum(int a, int b);
}

兩個實現:

public class DiffImpl implements IDiff{
    @Override
    public double diff(double a, double b) {
        return a - b;
    }
}

public class SumImpl implements ISum {

    public int sum(int a, int b) {
        return a + b;
    }

}

我們假設這兩個類在服務器上,客戶端沒有這兩個類,但客戶端還是想使用這兩個服務,所以我們可以通過網絡,把客戶端的想法,告訴服務器,我要使用那兩個類。而在這之前,服務器先要開啓這個服務。
服務端:

public class RPCServer {
    private static final Logger LOG = LoggerFactory.getLogger(RPCServer.class);
    private static final int threadSize = 10;
    private final ExecutorService threadPool;
    /**
     *服務在一開始就是確定的
     *不允許客戶端自動添加服務 
     *Key爲全限定接口名,Value爲接口實現類對象
     */
    private final Map<String, Object> servicePool;
    private final int port;
    private volatile boolean stop;

    public RPCServer(Map<String, Object> servicePool, int port) {
        this.port = port;
        this.threadPool = Executors.newFixedThreadPool(threadSize);
        this.servicePool = servicePool;
    }

    /**
     * RPC服務端處理函數 監聽指定TPC端口
     * 每次有請求過來的時候調用服務,放入線程池中處理.
     */
    @SuppressWarnings("resource")
    public void service() throws IOException {
        ServerSocket serverSocket = new ServerSocket(port);
        while (!stop) {
            final Socket socket = serverSocket.accept();
            threadPool.execute(new Runnable() {
                public void run() {
                    try {
                        process(socket);
                    } catch (Exception e) {
                        LOG.warn("Illegal calls",e);
                    } finally {
                        IOUtil.close(socket);
                    }
                }
            });
        }

    }

    public void stop() {
        stop = true;
        threadPool.shutdown();
    }

    /**
     * 調用服務 通過TCP Socket返回結果對象
     * 有可能因爲客戶端的
     * 類名錯誤
     * 方法名錯誤
     * 參數錯誤
     * 而調用失敗
     */
    private void process(Socket socket) throws Exception {
        ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
        Message message = (Message) in.readObject();
        // 調用服務
        Object result = call(message);
        if(result == null){
            LOG.warn("Without this service, the service interface is "+message.getInterfaceName());
            IOUtil.close(socket);
            return;
        }
        ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
        out.writeObject(result);
        IOUtil.close(socket);
    }

    /**
     * 服務處理函數 通過包名+接口名
     * 在servicePool中找到對應服務
     * 通過調用方法參數類型數組獲取Method對象
     * 通過Method.invoke(對象,參數)調用對應服務
     */
    private Object call(Message message) throws Exception {
        String interfaceName = message.getInterfaceName();
        Object service = servicePool.get(interfaceName);
        if (service == null) {
            return null;
        }
        Class<?> serviceClass = Class.forName(interfaceName);
        Method method = serviceClass.getMethod(message.getMethodName(), message.getParamsTypes());
        Object result = method.invoke(service, message.getParameters());
        return result;
    }
}

開啓服務:

public static void main(String[] args){
        Map<String,Object> servicePool = new HashMap<String, Object>();
//        先將服務確定好,才能調用,不允許客戶端自動添加服務
        servicePool.put(ISum.class.getName(), new SumImpl());
        servicePool.put(IDiff.class.getName(), new DiffImpl());
        RPCServer server = new RPCServer(servicePool,8080);
        try {
            server.service();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

這是一個通用的消息格式,用於在client和server之間傳遞消息,
消息主要包括;客戶端需要調用的接口,方法名,參數類型
以及參數數組,這樣是爲了唯一確定調用的是哪個類的哪個方法,參數是什麼,以及什麼類型。就是通過反射調用的。

/**
 * RPC調用條件
 * 1.調用接口名稱 (包名+接口名) 
 * 2.調用方法名 
 * 3.調用參數Class類型數組 
 * 4.調用接口的參數數組
 */
public class Message implements Serializable {
    private static final long serialVersionUID = 1L;
    // 包名+接口名稱 
    private String interfaceName;

    private String methodName;

    private Class<?>[] paramsTypes;

    private Object[] parameters;

    public Message() {
    }

    public Message(String interfaceName, String methodName, 
            Class<?>[] paramsTypes, Object[] parameters) {
        this.interfaceName = interfaceName;
        this.methodName = methodName;
        this.paramsTypes = paramsTypes;
        this.parameters = parameters;
    }

    //setters and getters
}

上面是服務端的代碼,主要就是使用接口名作爲key,實現類作爲值;
使用反射調用方法,再把結果用java序列化的方式寫會客戶端。
好了,我們再來看看客戶端的實現。

/**
 * @author root
 *客戶端比較簡單,就是連接服務器
 *然後用java序列化,把對象發送給服務器
 */
public class RPCClient {

    // 服務端地址
    private final String serverAddress;
    // 服務端端口
    private final int serverPort;


    public RPCClient(String serverAddress, int serverPort) {
        this.serverAddress = serverAddress;
        this.serverPort = serverPort;
    }

    /**
     * 同步的請求和接收結果
     */
    public Object sendAndReceive(Message transportMessage) {
        Object result = null;
        Socket socket = null;
        try {
            socket = new Socket(serverAddress, serverPort);
            // 反序列化 TransportMessage對象
            ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
            out.writeObject(transportMessage);
            ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
            // 阻塞等待讀取結果並反序列化結果對象
            result = in.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            IOUtil.close(socket);
        }
        return result;
    }


    public String getServerAddress() {
        return serverAddress;
    }

    public int getServerPort() {
        return serverPort;
    }

}

客戶端請求服務器:

public class TestClient {
    public static void main(String[] args) {
        String serverAddress = "localhost";
        int serverPort = 8080;
        int count = 10;
        final RPCClient client = new RPCClient(serverAddress, serverPort);
        final Random random = new Random();
        ExecutorService exe = Executors.newFixedThreadPool(count );
        for (int i = 0; i < count; i++) {

            exe.execute(new Runnable() {
                public void run() {
                    Message transportMessage = null;
                    if(random.nextBoolean())
                        transportMessage = buildMessage();
                    else
                        transportMessage = buildMessage2();
                    Object result = client.sendAndReceive(transportMessage);
                    System.out.println(result);
                }
            });
        }
        exe.shutdown();
    }

    /**
     * 創建一次消息調用
     * @return
     */
    public static Message buildMessage() {
        String interfaceName = ISum.class.getName();
        Class<?>[] paramsTypes = { int.class, int.class};
        Object[] parameters = { 9, 3};
        String methodName = "sum";
        Message transportMessage = new Message(interfaceName,
                methodName, paramsTypes, parameters);
        return transportMessage;
    }


    public static Message buildMessage2() {
        String interfaceName = IDiff.class.getName();
        Class<?>[] paramsTypes = { double.class, double.class};
        Object[] parameters = { 9.0, 3.0};
        String methodName = "diff";
        Message transportMessage = new Message(interfaceName,
                methodName, paramsTypes, parameters);
        return transportMessage;
    }
}

大家可以看到客戶端其實很簡單:就是把消息發給服務器,然後接受調用結果。好了這就是一個rpc調用的過程,但是這個效率很低,低的原因主要是java序列化和阻塞的IO模型。在高併發的場景下,這種線程阻塞的方式,如果使用能適應高併發高效的NIO和更好的java序列化框架(比如protobuf),效果會更好。
先開啓服務器,在開啓客戶端,這是一次客戶端打印的結果:
這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章