基於Socket實現PRC通信

前言

寫這篇博客的目的主要回顧一下學過的知識,避免遺忘,同時希望夥伴一起來指正一些不足,共同學習。文章是基於Socket來實現一個基本的RPC通信框架,並且實現版本控制。功能不會太複雜,主要是爲了疏通思路脈路。

背景環境

在分佈式中,我們經常會用到dubbo+zookeeper的框架來實現,由於本篇博客並沒有對zookeeper的實現,所以我們將由RPC-Server來統一對API進行管理,還請見諒!

一、RPC-Server創建

在這裏插入圖片描述

1.1 rpc-server-api
1.1.1基本創建

實體類的創建,並實現get&set方法。

//實體類
public class User {

    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

測試接口:
這裏隨便定義兩個方法,就不解釋方法了,看名字也能猜出來。

public interface IHelloService {

    String sayHello(String content);
    /**
     * 保存用戶
     * @param user
     * @return
     */
    String saveUser(User user);
}

RpcRequest:

關於Request對象的解釋:

Request對象,又稱爲請求對象,該對象派生自HTTPResponse類,是ASP中重要的服務器內置對象,它連接着Web服務器和Web客戶端程序。該對象用來獲取客戶端在請求一個頁面或者傳送一個Form時提供的所有信息,包括能夠標識瀏覽器和用戶的HTTP變量、存儲在客戶端Cookie信息以及附在URL後面的值、查詢字符串或頁面中Form段HTML控件內的值、Cookie、客戶端證書、查詢字符串等 。如瀏覽器和用戶的變量,客戶端表單中的數據、變量或者客戶端的cookie信息等,Request對象對應的類是System、Web、HttpRequest類。

我們這裏也簡單存儲一下請求的基本信息:

public class RpcRequest implements Serializable {
    //類名
    private String className;
    //方法名
    private String methodName;
    //參數
    private Object[] parameters;
    //版本號
    private String version;

    public String getVersion() {
        return version;
    }

    public void setVersion(String version) {
        this.version = version;
    }

    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.1.2 打jar包

到此處,api模塊的編寫就簡單的完成了。我們將此模塊進行打包,然後添加到prc-server-provider中。
在這裏插入圖片描述

1.2 rpc-server-provider

在這裏我們需要對接口的註冊和接收服務端的請求處理,

1.2.1 依賴
		<!-- rpc-server-api 的maven地址-->
        <dependency>
            <groupId>com.ccc</groupId>
            <artifactId>rpc-server-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.3.13.RELEASE</version>
        </dependency>
1.2.2 定義RpcService註解
@Target(ElementType.TYPE) //修飾範圍 //類或接口
@Retention(RetentionPolicy.RUNTIME)
@Component //被spring進行掃描
public @interface RpcService {
    
    Class<?> value(); //拿到服務的接口

    /**
     * 版本號
     */
    String version() default "";
}
1.2.3 對api接口實現

由於我們對版本進行了控制,所以此處簡單寫兩個實現類並標註上述聲明的註解

@RpcService(value = IHelloService.class,version = "v1.0")
public class HelloServiceImpl implements IHelloService{


    @Override
    public String sayHello(String content) {
        System.out.println("[v1.0] request in :"+content);
        return "[v1.0]say Hello:"+content;
    }

    @Override
    public String saveUser(User user) {
        System.out.println("request in saveUser :" +user);
        return "[v1.0]SUCCESS";
    }
}
@RpcService(value = IHelloService.class,version = "v2.0")
public class HelloServiceImpl2 implements IHelloService{


    @Override
    public String sayHello(String content) {
        System.out.println("[v2.0] request in :"+content);
        return "[v2.0]say Hello:"+content;
    }

    @Override
    public String saveUser(User user) {
        System.out.println("request in saveUser :" +user);
        return "[v2.0]SUCCESS";
    }
}
1.2.4 PrcServer編寫

創建MyRpcServer類,實現ApplicationContextAware,InitializingBean這兩個接口,當然這並不一定是必須需要實現這兩個接口,主要能實現功能就行。
ApplicationContextAware:加載Spring配置文件時,如果Spring配置文件中所定義或者註解自動注入的Bean類實現了ApplicationContextAware 接口,那麼在加載Spring配置文件時,會自動調用ApplicationContextAware 接口中的setApplicationContext方法。
InitializingBean:InitializingBean接口爲bean提供了初始化方法的方式,它只包括afterPropertiesSet方法,凡是繼承該接口的類,在初始化bean的時候都會執行該方法。
另外實現的通信是基於Socket套接字來實現的,我們這裏使用一個緩存線程池來處理每一次接收到的請求,另外請求的業務邏輯我們也交由ProcessorHandler線程來處理。

public class MyRpcServer implements ApplicationContextAware, InitializingBean {
     //緩存線程池
    ExecutorService executorService = Executors.newCachedThreadPool();
    //存放註解的容器
    private Map<String,Object> handlerMap = new HashMap<>();
    //端口號
    private int port;

    public MyRpcServer(int port){
        this.port = port;
    }

    /**
     * InitializingBean接口方法:
     * 初始化客戶端。
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {

        ServerSocket serverSocket = null;
        try {
        //創建對象並設置端口
            serverSocket = new ServerSocket(port);
            while (true){
                Socket socket = serverSocket.accept(); //此處會阻塞
                //每一個socket交給一個processorHandler處理
                executorService.execute(new ProcessorHandler(handlerMap,socket));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(serverSocket !=null){
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    /**
     * ApplicationContextAware接口方法
     * 將API進行初始化並存放於容器中
     * @param applicationContext
     * @throws BeansException
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    //獲取指定註解
        Map<String,Object> serviceMap = applicationContext.getBeansWithAnnotation(RpcService.class);
        if(! serviceMap.isEmpty()){
        //遍歷註解的value
            for(Object serviceBean : serviceMap.values()){
                //拿到註解
                RpcService rpcService = serviceBean.getClass().getAnnotation((RpcService.class));
                //獲取name
                String serviceName = rpcService.value().getName();
                //獲取版本號
                String version = rpcService.version();
                //添加版本號
                if(!StringUtils.isEmpty(version)){
                /**
                *	將serviceName拼接版本號,在此控制了版本號後,
                *	如果在註冊API時候添加了版本號,那麼客戶端調用接口的時候,就必須傳遞版本號信息
                *	否則無法進行調用。
                */
                    serviceName += "-"+version;
                }
                //將name作爲key class對象作爲value存放
                handlerMap.put(serviceName,serviceBean);

            }
        }
    }
}
1.2.5 ProcessorHandler

此類實現了Runnable接口,接收的每個請求都單獨用一個線程來處理,該類中主要是從socket中讀取客戶端發送過來的信息處理然後調用指定的方法。

public class ProcessorHandler implements Runnable {

    private Socket socket;

    private Map<String, Object> handlerMap;

    /**
     * 構造器
     * @param handlerMap 獲取註解信息
     * @param socket 獲取請求信息
     */
    public ProcessorHandler(Map<String, Object> handlerMap, Socket socket) {
        this.socket = socket;
        this.handlerMap = handlerMap;
    }

    @Override
    public void run() {
        ObjectInputStream objectInputStream = null;
        ObjectOutputStream objectOutputStream = null;
        try {
            // ------------------- InputStream ------------------
            //拿到客戶端信息 輸入流
            objectInputStream = new ObjectInputStream(socket.getInputStream());
            /**
             * 進行反序列化,將客戶端發送的Request信息讀取出來。
             * 包括請求哪個類,方法名稱,參數
             */
            RpcRequest rpcRequest = (RpcRequest) objectInputStream.readObject();
            //將請求進行處理
            Object result = invoke(rpcRequest);

            //-------------------- OutputStream --------------------
            //可以用於發送廣播消息。目前沒有使用
            objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
            objectOutputStream.writeObject(result);
            objectOutputStream.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (objectInputStream != null) {
                try {
                    objectInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (objectOutputStream != null) {
                try {
                    objectOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private Object invoke(RpcRequest rpcRequest) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        //得到類名
        String serviceName = rpcRequest.getClassName();
        //獲取版本號
        String version = rpcRequest.getVersion();
        //如果版本號不爲空,那麼將請求數據按照規定拼接
        if (!StringUtils.isEmpty(version)) {
            serviceName += "-" + version;
        }
        System.out.println(serviceName);
        System.out.println("map:"+handlerMap);
        //反射調用
        //根據key獲取類對象
        Object service = handlerMap.get(serviceName);

        //根據RpcRequest請求中的serviceName 如果沒有找到 拋出異常。
        if (service == null) {
            throw new RuntimeException("service not found:" + serviceName);
        }
        //獲取請求參數數組
        Object[] args = rpcRequest.getParameters(); //獲得請求參數
        Method method = null;
        if (args != null) { //如果有參數 對參數進行處理,如果沒有進行調用加載方法。
            //遍歷獲得每個參數的類型
            Class<?>[] types = new Class[args.length];
            for (int i = 0; i < args.length; i++) {
                //得到參數類型
                types[i] = args[i].getClass();
            }
            //根據請求的類去加載 //HelloServiceImpl
            Class clazz = Class.forName(rpcRequest.getClassName());
           method = clazz.getMethod(rpcRequest.getMethodName(), types);// sayHello saveUser 找到類中的方法
        }else {
            //根據請求的類去加載 //HelloServiceImpl
            Class clazz = Class.forName(rpcRequest.getClassName());
            // sayHello saveUser 找到類中的方法
            method = clazz.getMethod(rpcRequest.getMethodName());
        }

        //反射調用
        return method.invoke(service, args);
    }
}
1.2.6 注入

將代碼交由Spring來管理。

@Configuration
@ComponentScan(basePackages = "com.ccc")
public class SpringConfig {

    @Bean(name = "MyRpcServer")
    public MyRpcServer MyRpcServer(){
        return new MyRpcServer(9527);
    }

}
1.2.7 啓動類
public class App {
    public static void main(String[] args) {

        ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
        ((AnnotationConfigApplicationContext) context).start();

    }
}
1.2.8 打包

將rpc-server打jar包,方法和上圖一樣。 到這裏簡單的rpcserver也就完成了。下面寫個prc-client

二、RPC-Client 創建

創建個maven項目就行

2.1導入依賴

將RPC-Server的依賴導入進來

	<dependency>
      <groupId>com.ccc</groupId>
      <artifactId>rpc-server-api</artifactId>
      <version>1.0-SNAPSHOT</version>
    </dependency>
2.2 與Server進行連接
public class RpcNetTransport {
    private String host;
    private int prot;

    public RpcNetTransport(String host, int prot) {
        this.host = host;
        this.prot = prot;
    }

    public Object send(RpcRequest request) {
        Socket socket = null;
        Object result = null;
        ObjectOutputStream objectOutputStream = null;
        ObjectInputStream inputStream = null;

        try {
            socket = new Socket(host, prot); //建立連接
            /**
             * 將客戶的端的request信息進行輸出到Server端
             */
            objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
            objectOutputStream.writeObject(request); //序列化
            objectOutputStream.flush();

            // ----------------- InputStream ---------------
            //接受服務端發來的廣播消息,暫時沒用上。
            inputStream = new ObjectInputStream(socket.getInputStream());

            result = inputStream.readObject();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (objectOutputStream != null) {
                try {
                    objectOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return result;
    }
}
2.3 java動態代理來進行server之間交互。
2.3.1 RemoteInvocationHandler

在這裏我使用java的動態代理來實現創建一個RemoteInvocationHandler實現InvocationHandler,並實現invoke方法
InvocationHandler:是proxy代理實例的調用處理程序實現的一個接口,每一個proxy代理實例都有一個關聯的調用處理程序;在代理實例調用方法時,方法調用被編碼分派到調用處理程序的invoke方法,該方法有三個參數:
proxy:代理類代理的真實代理對象com.sun.proxy.$Proxy0
method:我們所要調用某個對象真實的方法的Method對象
args:指代代理對象方法傳遞的參數

public class RemoteInvocationHandler implements InvocationHandler {

    private String host;
    private int prot;

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //打樁
        System.out.println("come in");
        RpcRequest rpcRequest = new RpcRequest();
        // --------------- 開始 設置 RpcRequest 請求參數 ------------------
        //設置類名
        rpcRequest.setClassName(method.getDeclaringClass().getName());
        //設置方法名
        rpcRequest.setMethodName(method.getName());
        //參數
        rpcRequest.setParameters(args);
        //版本號
        rpcRequest.setVersion("v1.0");
        // --------------- 結束 設置 RpcRequest 請求參數 ------------------

        //遠程通信,將rpcRequest進行輸出。
        RpcNetTransport netTransport = new RpcNetTransport(host,prot);
        Object result = netTransport.send(rpcRequest);
        //返回結果
        return result;
    }
}
2.3.2 RpcClient

完成了上述操作,現在我就只需要對接口進行代理處理來實現RPC通信了。

public class RpcProxyClient {

    public <T> T clientProxy(final Class<T> interfaceCls,final String host,final int port){
        /**
        *	使用java自帶的代理類 將接口進行代理來進行 RPC遠程通信,該方法會自動去調用				 
        *	InvocationHandler 中的invok方法
        */
       return (T) Proxy.newProxyInstance(interfaceCls.getClassLoader(),
                new Class<?>[]{interfaceCls},new RemoteInvocationHandler(host,port));
    }
}
2.3.3 啓動類

我們只需要寫個main方法來啓動就行

public class App 
{
    public static void main( String[] args )
    {
        System.out.println( "Hello World!" );
        RpcProxyClient rpcProxyClient = new RpcProxyClient();
        //傳入需要調用的接口,主機地址,端口號
        IHelloService iHelloService = rpcProxyClient.clientProxy(IHelloService.class,"localhost",9527);
        //接口調用
        String result = iHelloService.sayHello("Ccc");
        //輸出返回結果
        System.out.println(result);
    }
}

三、測試。

啓動Rpc-Server…沒有報錯
在這裏插入圖片描述
啓動Rpc-Client…
我們查看一下server接收到的參數
發現客戶端的標識是 Ccc
在這裏插入圖片描述
客戶端接收的返回結果

在這裏插入圖片描述
接口調用成功。。

我們測試一下 不添加版本號調用:
在RemoteInvocationHandler 中取出對version的設置。。

客戶端報錯:
原因很簡單,因爲我們在Server端對接口的的註解@RpcService中進行了版本控制所以serverName應該是serverName-version的格式,而且我們是通過serverName作爲key來獲取類對象的。而在客戶端不傳遞的version版本的時候,我們默認是使用serverName的,所以服務端通過這個key就沒法找到對應的value。
在這裏插入圖片描述
我們在修改version爲v2.0
在這裏插入圖片描述
version2.0 輸出成功。

到此,簡單的通信功能也就完成,如果您發現有啥錯誤的地方,懇請指出批評。在此感謝!

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