手寫RPC框架

Algoric發表於2019-04-01

一、RPC簡介

最近看hadoop底層通訊,都是通過RPC實現的。

RPC(Remote Procedure Call Protocol)遠端呼叫: 遠端過程呼叫是一種常用的分散式網路通訊協議,它允許執行於 一臺計算機的程式呼叫另一臺計算機的子程式,同時將網路的通訊細節隱藏起來, 使得使用者無須額外地為這個互動作用程式設計。分散式系統之間的通訊大都通過RPC實現

二、RPC請求過程

手寫RPC框架
  1. client發起服務呼叫請求
  2. client stub代理程式將呼叫的方法,引數按照一定格式封裝,通過服務方的地址,發起網路請求
  3. 訊息通過網路傳送到服務端,server stub接收到訊息,進行解包,反射呼叫本地對應的服務
  4. 本地服務執行將結果返回給server stub,然後server stub會將結果訊息打包返回到客戶端
  5. client stub接收訊息解碼,得到最終結果.

三、RPC框架架構

要寫一個RPC框架,需要哪些組成部分?

  1. 序列化方式。序列化主要作用是將結構化物件轉為位元組流以便於通過網路進行傳輸或寫入持久儲存。
  2. 遠端代理物件,一般使用jdk動態代理或者cglib代理
  3. 服務暴露 設定註冊中心Zookeeper
  4. 網路通訊,基於事件驅動的Reactor模式

四、RPC框架示例

  1. 服務提供者,執行在伺服器端,提供服務介面定義與服務實現類
  2. 服務釋出者,執行在伺服器端,負責將本地服務釋出成遠端服務,管理遠端服務,提供給服務消費者使用
  3. 服務消費者,執行在客戶端,通過遠端代理物件呼叫遠端服務

服務端程式碼

服務介面:

//計算學生年齡和的介面
public interface CalculateService {
    String cal(Student sta, Student stb);
}

public class CalculateServiceImpl implements CalculateService {
    @Override
    public String cal(Student sta, Student stb) {
        return "學生年齡之和:" + (sta.getAge() + stb.getAge());
    }
}
複製程式碼

服務釋出

public class PublishUtilI {
    //服務介面集合
    private static List<Object> serviceList;
    private static ThreadPoolExecutor executor = new ThreadPoolExecutor(5,10,10, TimeUnit.SECONDS,
                                                    new LinkedBlockingQueue<Runnable>(10));

    public static void publish(int port,Object... services) throws IOException {
        serviceList= Arrays.asList(services);
        ServerSocket server = new ServerSocket(port);
        Socket client;
        while (true) {
            //阻塞等待請求
            client = server.accept();
            //使用執行緒池處理請求
            executor.submit(new ServerHandler(client, serviceList));
        }

    }
}
複製程式碼

反射呼叫服務

  1. 讀取客戶端傳送的服務名
  2. 判斷服務是否釋出
  3. 如果釋出,反射呼叫服務端對應服務
  4. 返回結果給客戶端
public class ServerHandler implements Runnable {
    private Socket client = null;

    private List<Object> serviceList = null;

    public ServerHandler(Socket client, List<Object> service) {
        this.client = client;
        this.serviceList = service;
    }

    @Override
    public void run() {
        try (
                ObjectInputStream input = new ObjectInputStream(client.getInputStream());
                ObjectOutputStream output = new ObjectOutputStream(client.getOutputStream())
        ) {
            // 讀取客戶端要訪問那個service
            Class serviceClass = (Class) input.readObject();

            // 找到該服務類
            Object obj = findService(serviceClass);
            if (obj == null) {
                output.writeObject(serviceClass.getName() + "服務未發現");
            } else {
                //利用反射呼叫該方法,返回結果
                String methodName = input.readUTF(); //讀取UTF編碼的String字串
                //讀取引數型別
                Class<?>[] parameterTypes = (Class<?>[]) input.readObject();
                //讀取引數
                Object[] arguments = (Object[]) input.readObject();
                Method method = obj.getClass().getMethod(methodName, parameterTypes);
                //反射執行方法
                Object result = method.invoke(obj, arguments);
                output.writeObject(result);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private Object findService(Class serviceClass)  {
        for (Object obj : serviceList) {
            boolean isFather = serviceClass.isAssignableFrom(obj.getClass());
            if (isFather) {
                return obj;
            }
        }
        return null;
    }
}
複製程式碼

客戶端程式碼


public class Client {
    public static void main(String[] args) {
        CallProxyHandler handler = new CallProxyHandler("127.0.0.1", 1111);
        CalculateService calculateService = handler.getService(CalculateService.class);
        Student sta = new Student(1);
        Student stb = new Student(2);
        String result = calculateService.cal(sta, stb);
        System.out.println(result);
    }
}
複製程式碼

建立代理類遠端呼叫服務端釋出的服務

    public class CallProxyHandler implements InvocationHandler {

    private String ip;
    private int port;

    public CallProxyHandler(String ip, int port) {
        this.ip = ip;
        this.port = port;
    }

    /**
     * 獲取代理物件
     * @param clazz
     * @param <T>
     * @return
     */
    @SuppressWarnings("all")
    public <T> T getService(Class<T> clazz) {
        return (T) Proxy.newProxyInstance(CallProxyHandler.class.getClassLoader(),
                new Class<?>[] {clazz}, this);
    }
    
    /**
     * 將需要呼叫服務的方法名,引數型別,引數按照一定格式封裝傳送至服務端
     * 讀取服務端返回的結果
     * @param proxy
     * @param method
     * @param args
     * @return
     * @throws Throwable
     */
    @SuppressWarnings("all")
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        try (
                Socket socket = new Socket(ip, port);
                ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
                ObjectInputStream input = new ObjectInputStream(socket.getInputStream())
        ) {
            output.writeObject(proxy.getClass().getInterfaces()[0]);
            output.writeUTF(method.getName());
            output.writeObject(method.getParameterTypes());
            output.writeObject(args);
            output.flush();
            Object result = input.readObject();
            if (result instanceof Throwable) {
                throw (Throwable) result;
            }
            return result;
        }
    }
}
複製程式碼

至此,一個簡單的RPC服務呼叫框架完成。但是存在很多問題:

  1. 使用java自帶的序列化,效率不高,可以使用Hadoop Avro與protobuf
  2. 使用BIO方式進行網路傳輸,高併發情況無法應對,使用Netty框架進行網路通訊
  3. 缺少註冊中心,服務註冊可以使用Zookeeper進行管理。

相關文章