一起实现RPC,超详细~~~ 第一篇

一起实现RPC,超详细!!! 第一篇

一个简单的RPC通信逻辑如下:

  • 服务端暴露其要提供服务的接口
  • 客户端通过动态代理调用服务端的接口方法,通过代理将要传输的信息发送到服务端(代理是为了让客户端不去关注远程的细节,就像调用本地方法一样)
  • 服务端通过Socket进行不间断监听,如接收到数据后,就创建一个线程去执行
  • 服务端通过反射从客户端传来的信息中去找到对应的方法
  • 服务端将执行得到的数据结果封装到response中返回给客户端
  • 客户端收到数据并进行输出

项目的整体框架如下:

通过maven进行管理,父工程为My-RPC

  • My-RPC
    • RPC-API:接口相关的类
    • RPC-COMMON:通用类
    • RPC-CORE:RPC核心框架
    • Client:客户端
    • Server:服务端

实现过程如下:

一、定义接口

1.定义服务端要提供的接口

该接口位于RPC-API下,其提供一个方法hello,参数为一个HelloObject对象(后续会创建)。

package com.t598.api;

/**
 * 声明一个提供给外界的,可以被客户端去调用的接口。
 */
public interface HelloService {
    /**
     * 声明一个接口方法:hello
     * 参数用HelloObject对象
     * @param helloObject
     * @return
     */
    String hello(HelloObject helloObject);
}
2.创建hello方法中需要的HelloObject
package com.t598.api;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.io.Serializable;

/**
 * HelloObject需要实现Serializable接口,因为后面传输时需要接口类型,因此需要序列化,没有序列化会报错。
 */
@Data
@AllArgsConstructor
public class HelloObject implements Serializable {
    private Integer id;
    private String message;
}

二、创建传输对象

1.定义一个用于客户端与服务端之间传输信息的对象

该对象可以让服务端来唯一的确定客户端要调用的方法(通过接口名、方法名、参数列表、参数类型来唯一确定)。该对象位于RPC-COMMON中。

package com.t598.common.entity;

import lombok.Builder;
import lombok.Data;

import java.io.Serializable;

/**
 * 声明一个传输对象
 * 客户端想要让服务端唯一确定调用哪个方法,必须告知服务端相关参数:
 *  1.接口名字 2.方法名字 3.参数列表 4.参数类型
 * 将这四个需要传输的信息封装为一个传输对象RpcRquest,并实现Serializable接口
 * 这样,数据传输时服务端和客户端通过该对象来确定客户端调用的是哪个方法
 */
@Data
@Builder
public class RpcRequest implements Serializable {
    /**
     * 想要调用的接口名
     */
    private String interfaceName;

    /**
     * 想要调用的方法名
     */
    private String methodName;

    /**
     * 需要传输的参数列表
     */
    private Object[] parameters;

    /**
     * 参数的类型
     */
    private Class<?>[] paramTypes;
}

2.创建返回消息的对象

服务端通过RpcRequest对象找到对应的方法并执行之后,需要给客户端返还一个消息(成功或者失败),我们将执行结果封装到RpcResponse中。该对象位于RPC-COMMON中。

package com.t598.common.entity;

import com.t598.common.enumeration.ResponseCode;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * 通过传输RpcRequest对象之后,找到需要调用的方法,需要将执行的结果来返还给客户端
 * 这里将返还的结果封装为RpcResponse对象(如成功还是失败)
 * 同样需要实现Serializable接口
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RpcResponse<T> implements Serializable {
    /**
     * 响应返回时候的状态
     */
    private Integer statusCode;

    /**
     * 对响应状态的补充信息
     */
    private String message;

    /**
     * 响应的数据
     */
    private T data;


    /**
     * 声明响应成功时候的返回信息
     */
    public static <T> RpcResponse success(T data){
        //声明响应对象
        RpcResponse<T> response = new RpcResponse<>();
        //从响应代码的枚举类ResponseCode中获取成功的代码,并将其写入当前的response响应状态
        response.setStatusCode(ResponseCode.SUCCESS.getCode());
        //将响应数据写入当前的response响应数据
        response.setData(data);
        //返回响应对象response
        return response;
    }

    public static <T> RpcResponse fail(ResponseCode code){
        //声明响应对象
        RpcResponse<T> response = new RpcResponse<>();
        //写入当前的response响应状态
        response.setStatusCode(code.getCode());
        //写入当前的response补充信息
        response.setMessage(code.getMessage());
        //返回响应对象response
        return response;
    }
}

三、动态代理

1.创建客户端的动态代理对象

客户端通过动态代理来与服务端通信,客户端只需要调用,不用管具体的怎么实现,就像调用本地方法一样。位于RPC-COMMON中。

package com.t598.core.transport;
import com.t598.common.entity.RpcRequest;
import com.t598.common.entity.RpcResponse;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * 为客户端设置一个动态代理:
 *      使得客户端将想要调用的数据传输给服务端,
 *      让客户端只管调用,而不用管具体的怎么实现,
 *      就像调用本地方法一样。
 */
public class RpcClientProxy implements InvocationHandler {
    /**
     * 地址
     */
    private String host;
    /**
     * 端口号
     */
    private int port;

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

    /**
     * 封装了getProxy()方法,来返回代理对象。
     * @param clazz
     * @param <T>
     * @return
     */
    @SuppressWarnings("unchecked")
    public <T> T getProxy(Class<T> clazz) {
        return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, this);
    }

    /**
     * 重写invoke方法,实现代理类在被客户端调用时候的动作
     *      客户端通过代理将RPCRequest对象发送给服务端
     *      服务端返回数据给代理,客户端在代理处获得数据
     *
     *      客户端只需要调用,代理来实现与服务端的传输
     * @param proxy
     * @param method
     * @param args
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //声明传输的rpcRequest对象
        RpcRequest rpcRequest = RpcRequest.builder()
                .interfaceName(method.getDeclaringClass().getName())
                .methodName(method.getName())
                .parameters(args)
                .paramTypes(method.getParameterTypes())
                .build();
        RpcClient rpcClient = new RpcClient();
        return ((RpcResponse) rpcClient.sendRequest(rpcRequest, host, port)).getData();
    }
}

2.实现具体的通信逻辑

创建一个RpcClient类来实现具体的通信逻辑。位于RPC-COMMON中。

package com.t598.core.transport;

import com.t598.common.entity.RpcRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

public class RpcClient {
    private static final Logger logger = LoggerFactory.getLogger(RpcClient.class);

    /**
     * sendRequest具体实现与服务端通信的逻辑。
     * 通过socket发送给服务端,并接受到返回的数据
     * 通过Java的序列化方式在socket中传输
     * @param rpcRequest
     * @param host
     * @param port
     * @return
     */
    public Object sendRequest(RpcRequest rpcRequest, String host, int port){
        try (Socket socket = new Socket(host, port)) {
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
            ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
            objectOutputStream.writeObject(rpcRequest);
            objectOutputStream.flush();
            return objectInputStream.readObject();
        } catch (IOException | ClassNotFoundException e) {
            logger.error("调用时有错误发生:" + e);
            return null;
        }
    }
}

四、反射调用

1.服务端通过反射找到客户端要调用的方法

服务端一直监听客户端与之通信的端口(这里设置为8888)端口,当有请求连接时通过线程池创建线程让其执行通信的逻辑。位于RPC-COMMON中。

package com.t598.core.transport;

import com.t598.common.entity.RpcRequest;
import com.t598.common.entity.RpcResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;

/**
 * 服务端通过反射进行调用对应的方法
 * 服务端一直监听对应端口,当有请求连接时通过线程池创建线程让其执行通信的逻辑
 */
public class RpcServer {
    private final ExecutorService threadPool;
    private static final Logger logger = LoggerFactory.getLogger(RpcServer.class);

    /**
     * 初始化线程池,当有连接时就新建一个线程去执行
     */
    public RpcServer(){
        // 核心线程数
        int corePoolSize = 5;
        // 最大线程数
        int maximumPoolSize = 50;
        // 空闲线程的等待时间
        long keepAliveTime = 60;
        // 阻塞队列
        BlockingQueue<Runnable> workingQueue = new ArrayBlockingQueue<>(100);
        // 线程工厂
        ThreadFactory threadFactory = Executors.defaultThreadFactory();

        threadPool = new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                TimeUnit.SECONDS,
                workingQueue,
                threadFactory
        );
    }

    public void register(Object service, int port){
        try(ServerSocket serverSocket = new ServerSocket(port)){
            logger.info("服务器正在启动...");
            Socket socket;
            while ((socket = serverSocket.accept()) != null) {
                logger.info("客户端连接!Ip为:" + socket.getInetAddress());
                threadPool.execute(new WorkerThread(socket, service));
            }
        } catch (IOException e) {
            logger.error("连接时有错误发生:", e);
        }
    }

    private class WorkerThread implements Runnable {
        private final Socket socket;
        private final Object service;

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

        @Override
        public void run() {
            try (ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
                 ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream())) {
                RpcRequest rpcRequest = (RpcRequest) objectInputStream.readObject();
                Method method = service.getClass().getMethod(rpcRequest.getMethodName(), rpcRequest.getParamTypes());
                Object returnObject = method.invoke(service, rpcRequest.getParameters());
                objectOutputStream.writeObject(RpcResponse.success(returnObject));
                objectOutputStream.flush();
            } catch (IOException | ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                logger.error("调用或发送时有错误发生:", e);
            }
        }

    }
}

五、简单测试

1.客户端测试

位于Client中。

package com.t598.client;

import com.t598.api.HelloObject;
import com.t598.api.HelloService;
import com.t598.core.transport.RpcClientProxy;

public class TestClient {
    public static void main(String[] args) {
        //创建客户端的动态代理,由代理来负责与服务端通信
        RpcClientProxy rpcClientProxy = new RpcClientProxy("127.0.0.1", 8888);
        //返回代理对象,需要通信的接口是HelloService
        HelloService helloService = rpcClientProxy.getProxy(HelloService.class);
        //创建需要传输的对象,id为12,message为"this is a message"
        HelloObject helloObject = new HelloObject(12, "this is a message");
        //与服务端的HelloService接口通信,调用hello方法,并传入传输对象helloObject,返回字符串hello
        String hello = helloService.hello(helloObject);
        System.out.println(hello);
    }
}
2.服务端测试

位于Server中。

package com.t598.server;

import com.t598.core.transport.RpcServer;

public class TestServer {
    public static void main(String[] args) {
        HelloServiceImpl helloService = new HelloServiceImpl();
        RpcServer rpcServer = new RpcServer();
        rpcServer.register(helloService, 8888);
    }
}

PS:需要实现服务端提供的HelloService接口

package com.t598.server;

import com.t598.api.HelloObject;
import com.t598.api.HelloService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HelloServiceImpl implements HelloService {
    private static final Logger logger = LoggerFactory.getLogger(HelloServiceImpl.class);
    @Override
    public String hello(HelloObject helloObject) {
        logger.info("接收到:{}", helloObject.getMessage());
        return "这是调用的返回值,id=" + helloObject.getId();
    }
}

六、执行结果

服务端启动,等待连接:

在这里插入图片描述

客户端启动:

在这里插入图片描述

客户端接收消息:

在这里插入图片描述

介绍RCP的实现原理 目录 1. 前言 2 2. 基本概念 3 2.1. IDL 3 2.2. 代理(Proxy) 3 2.3. 存根(Stub) 4 3. 三要素 4 3.1. 网络通讯 4 3.2. 消息编解码 5 3.3. IDL编译器 5 4. flex和bison 5 4.1. 准备概念 5 4.1.1. 正则表达式(regex/regexp) 6 4.1.2. 符号∈ 6 4.1.3. 终结符/非终结符/产生式 6 4.1.4. 记号(Token) 6 4.1.5. 形式文法 7 4.1.6. 上下文无关文法(CFG) 7 4.1.7. BNF 8 4.1.8. 推导 8 4.1.9. 语法树 8 4.1.10. LL(k) 9 4.1.11. LR(k) 9 4.1.12. LALR(k) 9 4.1.13. GLR 9 4.1.14. 移进/归约 9 4.2. flex和bison文件格式 9 4.2.1. 定义部分 10 4.2.2. 规则部分 10 4.2.3. 用户子例程部分 10 4.3. flex基础 10 4.3.1. flex文件格式 11 4.3.2. 选项 11 4.3.3. 名字定义 11 4.3.4. 词法规则 12 4.3.5. 匹配规则 12 4.3.6. %option 13 4.3.7. 全局变量yytext 13 4.3.8. 全局变量yyval 13 4.3.9. 全局变量yyleng 13 4.3.10. 全局函数yylex 13 4.3.11. 全局函数yywrap 13 4.4. bison基础 14 4.4.1. bison文件格式 14 4.4.2. %union 14 4.4.3. %token 15 4.4.4. 全局函数yyerror() 15 4.4.5. 全局函数yyparse() 15 4.5. 例1:单词计数 15 4.5.1. 目的 15 4.5.2. flex词法文件wc.l 16 4.5.3. Makefile 16 4.6. 例2:表达式 17 4.6.1. 目的 17 4.6.2. flex词法exp.l 17 4.6.3. bison语法exp.y 17 4.6.4. Makefile 19 4.6.5. 代码集成 19 4.7. 例3:函数 20 4.7.1. 目的 20 4.7.2. func.h 20 4.7.3. func.c 21 4.7.4. IDL代码func.idl 22 4.7.5. flex词法func.l 22 4.7.6. bison语法func.y 24 4.7.7. Makefile 27 5. 进阶 27 5.1. 客户端函数实现 27 5.2. 服务端函数实现 28 5.2.1. Stub部分实现 28 5.2.2. 用户部分实现 29 6. 参考资料 29
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值