手写一个RPC框架

本文详细介绍了如何从头开始构建一个简单的RPC(远程过程调用)框架,包括服务消费方的透明调用、消息的编码与解码、服务的暴露与调用过程。通过Java动态代理实现客户端的本地调用方式,使用JDK序列化处理消息数据,最后展示了服务端如何接收和处理请求,以及客户端如何发起远程调用。整个流程清晰地揭示了RPC框架的基本工作原理。
摘要由CSDN通过智能技术生成

原文链接
想要手写出一个东西我们肯定要先知道一个东西的原理,我们先看看一个RPC框架需要哪些东西。

如何调用他人的远程服务?

由于各服务部署在不同机器,服务间的调用免不了网络通信过程,服务消费方每调用一个服务都要写一坨网络通信相关的代码,不仅复杂而且极易出错。要让网络通信细节对使用者透明,我们需要对通信细节进行封装,我们先看下一个RPC调用的流程涉及到哪些通信细节:

  1. 服务消费方(client)调用以本地调用方式调用服务;

  2. client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;

  3. client stub找到服务地址,并将消息发送到服务端;

  4. server stub收到消息后进行解码;

  5. server stub根据解码结果调用本地的服务;

  6. 本地服务执行并将结果返回给server stub;

  7. server stub将返回结果打包成消息并发送至消费方;

  8. client stub接收到消息,并进行解码;

  9. 服务消费方得到最终结果。

RPC的目标就是要2~8这些步骤都封装起来,让用户对这些细节透明。

1. 怎么做到透明化远程服务调用?

怎么封装通信细节才能让用户像以本地调用方式调用远程服务呢?对java来说就是使用代理!java代理有两种方式:1) jdk 动态代理;2)字节码生成。尽管字节码生成方式实现的代理更为强大和高效,但代码维护不易,大部分公司实现RPC框架时还是选择动态代理方式。我们这个最简易版的自然也是采用动态代理的方式。

2. 怎么对消息进行编码和解码?

2.1 确定消息数据结构
  1. 接口名称:在我们的例子里接口名是“HelloWorldService”,如果不传,服务端就不知道调用哪个接口了;

  2. 方法名:一个接口内可能有很多方法,如果不传方法名服务端也就不知道调用哪个方法;

  3. 参数类型&参数值参数类型有很多,比如有bool、int、long、double、string、map、list,甚至如struct(class)以及相应的参数值;超时时间

2.2 序列化

从RPC的角度上看,主要看三点:1)通用性,比如是否能支持Map等复杂的数据结构;2)性能,包括时间复杂度和空间复杂度,由于RPC框架将会被公司几乎所有服务使用,如果序列化上能节约一点时间,对整个公司的收益都将非常可观,同理如果序列化上能节约一点内存,网络带宽也能省下不少;3)可扩展性,对互联网公司而言,业务变化飞快,如果序列化协议具有良好的可扩展性,支持自动增加新的业务字段,而不影响老的服务,这将大大提供系统的灵活度。我们的是最简易版所以就采用了jdk序列化的方式来处理。

开始撸代码

  1. 初始化工程

    首先创建2个项目分别是server和client;server项目下两个模块分别是rpc-server-api和rpc-server-provider。

    为什么server项目要创建两个模块?

    client在调用服务端的服务时需要知道服务端的一些信息,client可以依赖于这个模块。我们的项目中SDK和契约包就是提供了这个功能。而真正的实现是放在rpc-server-provider中。

  2. rpc-server-api

public interface IHelloService {
    String sayHello(String content);

    String saveUser(User user);
}

请求参数类

private String className;
private String methodName;
private Object[] parameters;

  1. rpc-server-provider

    首先rpc-server-provider是依赖rpc-server-api的。我们写一个实现类,来实现api中定义的接口。

    public class HelloServiceImpl implements IHelloService{
       
        @Override
        public String sayHello(String content) {
            System.out.println("request in sayHello:"+content);
            return "Say Hello:"+content;
        }
    }
    
    

    我这么写好了实现远程要怎么才能调用的到呢?我们还需把服务暴露出去,那就需要一个服务暴露的方法。这里就是不断去接受请求,每一个socket交给一个processorHandler来处理。

    public class RpcProxyServer {
        ExecutorService executorService = Executors.newCachedThreadPool();
        public void publisher(Object service, int port) {
            ServerSocket serverSocket = null;
            try {
                serverSocket = new ServerSocket(port);
                while (true) {//不断接受请求
                    Socket socket = serverSocket.accept();//BIO
                    //每一个socket 交给一个processorHandler来处理
                    executorService.execute(new ProcessorHandler(socket, service));
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (serverSocket != null) {
                    try {
                        serverSocket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
    
        }
    }
    
    

    具体processorHandler的代码,从socket中获取请求对象,并是使用请求对象调用本服务方法,同时返回方法执行结果,将返回结果写入socket中。

    public class ProcessorHandler implements Runnable {
        private Socket socket;
        private Object service;
        
        public ProcessorHandler(Socket socket, Object service) {
            this.socket = socket;
            this.service = service;
        }
    
        @Override
        public void run() {
            try (InputStream inputStream = socket.getInputStream();
                 ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
                 ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream())) {
                //输入流中应该有什么东西?
                //请求哪个类,方法名称、参数
                RpcRequest rpcRequest = (RpcRequest) objectInputStream.readObject();
                Object result = invoke(rpcRequest); //反射调用本地服务
                objectOutputStream.writeObject(result);
                objectOutputStream.flush();
            } catch (IOException | ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    
        private Object invoke(RpcRequest request) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
            //反射调用
            Object[] args = request.getParameters(); //拿到客户端请求的参数
            Class<?>[] types = new Class[args.length]; //获得每个参数的类型
            for (int i = 0; i < args.length; i++) {
                types[i] = args[i].getClass();
            }
            Class clazz = Class.forName(request.getClassName()); //跟去请求的类进行加载
            Method method = clazz.getMethod(request.getMethodName(), types); //sayHello, saveUser找到这个类中的方法
            return method.invoke(service, args);
        }
    }
    
    

    大功告成,把服务发布出去。

    /**
     * Hello world!
     *
     */
    public class App {
        public static void main( String[] args ){
           IHelloService helloService=new HelloServiceImpl();
           RpcProxyServer proxyServer=new RpcProxyServer();
           // 发布到8080端口
           proxyServer.publisher(helloService,8080);
        }
    }
    
    
  2. 客户端代码开撸。我们现在在客户端依赖了服务端的api(SDK、契约包)如何才能实现调用远程方法呢?类似于服务端代理类。

    public class RpcProxyClient {
        public <T> T clientProxy(final Class<T> interfaceCls,final String host,final int port){
            return (T)Proxy.newProxyInstance(interfaceCls.getClassLoader(),
                    new Class<?>[]{interfaceCls},new RemoteInvocationHandler(host,port));
        }
    }
    
    

    public class RemoteInvocationHandler implements InvocationHandler {

    private String host;
    private int port;
    
    public RemoteInvocationHandler(String host, int port) {
        this.host = host;
        this.port = port;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //请求数据的包装
        RpcRequest rpcRequest=new RpcRequest();
        rpcRequest.setClassName(method.getDeclaringClass().getName());
        rpcRequest.setMethodName(method.getName());
        rpcRequest.setParameters(args);
        //远程通信
        RpcNetTransport netTransport=new RpcNetTransport(host,port);
        Object result=netTransport.send(rpcRequest);
    
        return result;
    }
    

    }

    
    处理网络传输的类
    
    

    public class RpcNetTransport {

    private String host;
    private int port;
    
    public RpcNetTransport(String host, int port) {
        this.host = host;
        this.port = port;
    }
    
    public Object send(RpcRequest request) {
        Object result = null;
        try (//建立连接
             Socket socket = new Socket(host, port);
             //网络socket
             ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
             ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream())) {
            
            outputStream.writeObject(request); //序列化()
            outputStream.flush();
            result = inputStream.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        return result;
    }
    
    
    最后使用这个远程调用:
    
    

    public class App {
    public static void main(String[] args) {
    RpcProxyClient rpcProxyClient = new RpcProxyClient();

        IHelloService iHelloService = rpcProxyClient.clientProxy(IHelloService.class,"localhost",8080);
    

    }
    }

    
    
    

最后画一张来总结一下整个流程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值