從零實現一個RPC框架系列文章(二):11個類實現簡單RPC

wephone發表於2019-03-03

專案1.0版本原始碼

https://github.com/wephone/MeiZhuoRPC/tree/1.0


上一博文中 跟大家講了RPC的實現思路 思路畢竟只是思路 那麼這篇就帶著原始碼給大家講解下實現過程中的各個具體問題

讀懂本篇需要的基本知識 若尚未清晰請自行了解後再閱讀本文

  • java動態代理
  • netty框架的基本使用
  • spring的基本配置

最終專案的使用如下

/**
 *呼叫端程式碼及spring配置
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"file:src/test/java/rpcTest/ClientContext.xml"})
public class Client {

    @Test
    public void start(){
        Service service= (Service) RPC.call(Service.class);
        System.out.println("測試Integer,Double型別傳參與返回String物件:"+service.stringMethodIntegerArgsTest(233,666.66));
        //輸出string233666.66
    }

}

/**
 *Service抽象及其實現
 *呼叫與實現端共同依賴Service
 */
public interface Service {
    String stringMethodIntegerArgsTest(Integer a,Double b);
}
/**
 * ServiceImpl實現端對介面的具體實現
*/
public class ServiceImpl implements Service {
    @Override
    public String stringMethodIntegerArgsTest(Integer a, Double b) {
        return "String"+a+b;
    }
}
複製程式碼

1.0版本分3個包

  • Client 呼叫端
  • Server 實現端
  • Core 核心方法

首先看這句程式碼

呼叫端只需如此呼叫
定義介面 傳入介面類型別 後面呼叫的介面內的方法 全部是由實現端實現

Service service= (Service) RPC.call(Service.class);
複製程式碼

這句的作用其實就是生成呼叫端的動態代理

/**
     * 暴露呼叫端使用的靜態方法 為抽象介面生成動態代理物件
     * TODO 考慮後面優化不在使用時仍需強轉
     * @param cls 抽象介面的類型別
     * @return 介面生成的動態代理物件
     */
    public static Object call(Class cls){
        RPCProxyHandler handler=new RPCProxyHandler();
        Object proxyObj=Proxy.newProxyInstance(cls.getClassLoader(),new Class<?>[]{cls},handler);
        return proxyObj;
    }
複製程式碼

RPCProxyHandler為動態代理的方法被呼叫後的回撥方法 每個方法被呼叫時都會執行這個invoke

/**
     * 代理抽象介面呼叫的方法
     * 傳送方法資訊給服務端 加鎖等待服務端返回
     * @param proxy
     * @param method
     * @param args
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        RPCRequest request=new RPCRequest();
        request.setRequestID(buildRequestID(method.getName()));
        request.setClassName(method.getDeclaringClass().getName());//返回表示宣告由此 Method 物件表示的方法的類或介面的Class物件
        request.setMethodName(method.getName());
//        request.setParameterTypes(method.getParameterTypes());//返回形參型別
        request.setParameters(args);//輸入的實參
        RPCRequestNet.requestLockMap.put(request.getRequestID(),request);
        RPCRequestNet.connect().send(request);
        //呼叫用結束後移除對應的condition對映關係
        RPCRequestNet.requestLockMap.remove(request.getRequestID());
        return request.getResult();//目標方法的返回結果
    }
複製程式碼

也就是收集對應呼叫的介面的資訊 然後send給實現端
那麼這個requestLockMap又是作何作用的呢

  • 由於我們的網路呼叫都是非同步
  • 但是RPC呼叫都要做到同步 等待這個遠端呼叫方法完全返回後再繼續執行
  • 所以將每個請求的request物件作為物件鎖 每個請求傳送後加鎖 等到網路非同步呼叫返回後再釋放所
  • 生成每個請求的ID 這裡我用隨機數加時間戳
  • 將請求ID和請求物件維護在靜態全域性的一個map中 實現端通過ID來對應是哪個請求
  • 非同步呼叫返回後 通過ID notify喚醒對應請求物件的執行緒
    netty非同步返回的呼叫 釋放物件鎖
@Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        String responseJson= (String) msg;
        RPCResponse response= (RPCResponse) RPC.responseDecode(responseJson);
        synchronized (RPCRequestNet.requestLockMap.get(response.getRequestID())) {
            //喚醒在該物件鎖上wait的執行緒
            RPCRequest request= (RPCRequest) RPCRequestNet.requestLockMap.get(response.getRequestID());
            request.setResult(response.getResult());
            request.notifyAll();
        }
    }
複製程式碼

接下來是RPCRequestNet.connect().send(request);方法
connect方法其實是單例模式返回RPCRequestNet例項
RPCRequestNet構造方法是使用netty對實現端進行TCP連結
send方法如下

try {
            //判斷連線是否已完成 只在連線啟動時會產生阻塞
            if (RPCRequestHandler.channelCtx==null){
                connectlock.lock();
                //掛起等待連線成功
                System.out.println("正在等待連線實現端");
                connectCondition.await();
                connectlock.unlock();
            }
            //編解碼物件為json 傳送請求
            String requestJson= null;
            try {
                requestJson = RPC.requestEncode(request);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
            ByteBuf requestBuf= Unpooled.copiedBuffer(requestJson.getBytes());
            RPCRequestHandler.channelCtx.writeAndFlush(requestBuf);
            System.out.println("呼叫"+request.getRequestID()+"已傳送");
            //掛起等待實現端處理完畢返回 TODO 後續配置超時時間
            synchronized (request) {
                //放棄物件鎖 並阻塞等待notify
                request.wait();
            }
            System.out.println("呼叫"+request.getRequestID()+"接收完畢");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
複製程式碼

condition和lock同樣是為了同步等待非同步IO返回用的
send方法基本是編解碼json後傳送給實現端

呼叫端基本實現綜上所述 代理 傳送 同步鎖


下面是服務端的使用和實現

/**
 *實現端程式碼及spring配置
 */
 @RunWith(SpringJUnit4ClassRunner.class)
 @ContextConfiguration(locations={"file:src/test/java/rpcTest/ServerContext.xml"})
 public class Server {
 
     @Test
     public void start(){
         //啟動spring後才可啟動 防止容器尚未載入完畢
         RPC.start();
     }
 }
複製程式碼

出了配置spring之外 實現端就一句 RPC.start()
其實就是啟動netty伺服器
服務端的處理客戶端資訊回撥如下

@Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws IOException {
        String requestJson= (String) msg;
        System.out.println("receive request:"+requestJson);
        RPCRequest request= RPC.requestDeocde(requestJson);
        Object result=InvokeServiceUtil.invoke(request);
        //netty的write方法並沒有直接寫入通道(為避免多次喚醒多路複用選擇器)
        //而是把待傳送的訊息放到緩衝陣列中,flush方法再全部寫到通道中
//        ctx.write(resp);
        //記得加分隔符 不然客戶端一直不會處理
        RPCResponse response=new RPCResponse();
        response.setRequestID(request.getRequestID());
        response.setResult(result);
        String respStr=RPC.responseEncode(response);
        ByteBuf responseBuf= Unpooled.copiedBuffer(respStr.getBytes());
        ctx.writeAndFlush(responseBuf);
    }
複製程式碼

主要是編解碼json 反射對應的方法 我們看看反射的工具類

/**
     * 反射呼叫相應實現類並結果
     * @param request
     * @return
     */
    public static Object invoke(RPCRequest request){
        Object result=null;//內部變數必須賦值 全域性變數才不用
        //實現類名
        String implClassName= RPC.getServerConfig().getServerImplMap().get(request.getClassName());
        try {
            Class implClass=Class.forName(implClassName);
            Object[] parameters=request.getParameters();
            int parameterNums=request.getParameters().length;
            Class[] parameterTypes=new Class[parameterNums];
            for (int i = 0; i <parameterNums ; i++) {
                parameterTypes[i]=parameters[i].getClass();
            }
            Method method=implClass.getDeclaredMethod(request.getMethodName(),parameterTypes);
            Object implObj=implClass.newInstance();
            result=method.invoke(implObj,parameters);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return result;
    }
複製程式碼

解析Parameters getClass獲取他們的類型別 反射呼叫對應的方法

這裡需要注意一個點

  • 本文最初採用Gson處理json 但gson預設會把int型別轉為double型別 例如2變為2.0 不適用本場景 我也不想去專門適配
  • 所以換用了jackson
  • 常見json處理框架 反序列化為物件時 int,long等基本型別都會變成他們的包裝類Integer Long
  • 所以本例程中 遠端排程介面方法的形參不可以使用int等基本型別
  • 否則method.invoke(implObj,parameters);會找不到對應的方法報錯
  • 因為parameters已經是包裝類了 而method還是int這些基本類 所以找不到對應方法

最後是藉助spring配置基礎配置
我寫了兩個類 ServerConfig ClientConfig 作為呼叫端和服務端的配置
只需在spring中配置這兩個bean 並啟動IOC容器即可

呼叫端

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean class="org.meizhuo.rpc.client.ClientConfig">
        <property name="host" value="127.0.0.1"></property>
        <property name="port" value="9999"></property>
    </bean>
</beans>
複製程式碼

實現端

<?xml version="1.0" encoding="UTF-8"?>
 <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
     <bean class="org.meizhuo.rpc.server.ServerConfig">
         <property name="port" value="9999"></property>
         <property name="serverImplMap">
             <map>
                 <!--配置對應的抽象介面及其實現-->
                 <entry key="rpcTest.Service" value="rpcTest.ServiceImpl"></entry>
             </map>
         </property>
     </bean>
 </beans>
複製程式碼

最後有個小問題

我們的框架是作為一個依賴包引入的 我們不可能在我們的框架中讀取對應的spring xml
這樣完全是去了框架的靈活性
那我們怎麼在執行過程中獲得我們所處於的IOC容器 已獲得我們的正確配置資訊呢
答案是spring提供的ApplicationContextAware介面

/**
 * Created by wephone on 17-12-26.
 */
public class ClientConfig implements ApplicationContextAware {

    private String host;
    private int port;
    //呼叫超時時間
    private long overtime;

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }

    public long getOvertime() {
        return overtime;
    }

    public void setOvertime(long overtime) {
        this.overtime = overtime;
    }

    /**
     * 載入Spring配置檔案時,如果Spring配置檔案中所定義的Bean類
     * 如果該類實現了ApplicationContextAware介面
     * 那麼在載入Spring配置檔案時,會自動呼叫ApplicationContextAware介面中的
     * @param applicationContext
     * @throws BeansException
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        RPC.clientContext=applicationContext;
    }
}
複製程式碼

這樣我們在RPC類內部就維護了一個靜態IOC容器的context
只需如此獲取配置
RPC.getServerConfig().getPort()

 public static ServerConfig getServerConfig(){
        return serverContext.getBean(ServerConfig.class);
    }
複製程式碼

就這樣 這個RPC框架的核心部分 已經講述完畢了

本例程僅為1.0版本
後續部落格中 會加入異常處理 zookeeper支援 負載均衡策略等
部落格:zookeeper支援
歡迎持續關注 歡迎star 提issue

相關文章