Bootstrap

透彻解析RPC以代码为主

目录
第一版:简单实现
第二版:静态代理
第三版:动态代理
第四版:动态代理封装请求参数以及返回值,实现该接口所有方法调用
第五版:动态代理所有接口所有方法调用
序列化框架介绍
最终版:优化序列化框架
总结

简单介绍一下然后主要看代码。
RPC(Remote Procedure Call) 远程过程调用,也可以称作远程方法调用。RPC只是一个概念,具体的实现有很多种。可以调用restful风格的接口,也可以使用Socket长连接来调用。
这里主要讲解使用Socket来调用接口,对于服务器内部通信来说通过Socket会比http的方式效率更高。

第一版

在公告服务中定义公共的类,客户端和服务端都要需要使用

package com.zxh.rpc.common;
import java.io.Serializable;
public interface IUserService extends Serializable {
    public User findUserById(Integer id);
}
package com.zxh.rpc.common;
import lombok.Data;
import lombok.ToString;
import java.io.Serializable;
@Data
@ToString
public class User implements Serializable {
    private Integer id;
    private String name;
    public User(Integer id, String name) {
        this.id = id;
        this.name = name;
    }
}

服务端实现公共接口

import com.zxh.rpc.common.IUserService;
import com.zxh.rpc.common.User;
public class UserServiceImpl implements IUserService {
    @Override
    public User findUserById(Integer id) {
        return  new User(id,"Lucy");
    }
}

服务端要做的事很简单,监听8888端口等待客户端连接。连接上后,通过InputStream获取传入的id.通过OutputStream返回User对象

import com.zxh.rpc.common.User;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
 * Created by zxh on 2022/1/25
 */
public class Server {
    private static boolean running = true;
    public static void main(String[] args) throws Exception {
        ServerSocket ss = new ServerSocket(8888);
        while (running){
            Socket s = ss.accept();  // 接收一个客户端连接
            process(s);
            s.close();
        }
    }
    // 处理该客户端链接的输入输出流
    private static void process(Socket s) throws Exception{
        InputStream in = s.getInputStream();
        OutputStream out = s.getOutputStream();
        DataInputStream dis = new DataInputStream(in);
        DataOutputStream dos = new DataOutputStream(out);

        int id = dis.readInt();  // 通过输入流读取数据
        System.out.println("客户端发送的id:"+id);
        UserServiceImpl service = new UserServiceImpl();
        User user = service.findUserById(id);
        dos.writeInt(user.getId());
        dos.writeUTF(user.getName());
        dos.flush();
    }
}

然后看一下客户端如何调用

import com.zxh.rpc.common.User;

import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket;
public class Client {
    // 客户端:将id=123 传给服务器并获取返回值
    public static void main(String[] args) throws Exception{
        Socket s = new Socket("127.0.0.1", 8888);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();  // 字节数组输出流,就是一块内存空间
        DataOutputStream dos = new DataOutputStream(baos);  // 转换成数据输出流, 在外面包装了一层 DataOutputStream 专门用来写各种各样基础数据类型的
        dos.writeInt(123);
        // 通过socket 输出流 输出
        s.getOutputStream().write(baos.toByteArray()); // toByteArray 最终将数据转成真正的字节数组
        s.getOutputStream().flush();

        // 通过socket 输入流 获取返回数据
        DataInputStream dis = new DataInputStream(s.getInputStream());
        int id = dis.readInt();
        String name = dis.readUTF();

        User user = new User(id,name);
        System.out.println(user);
        dos.close();
        s.close();
    }
}

第二版

对于客户端来说每次调用接口都要写网络连接的方法过于麻烦。关注业务的同时还要关注网络,方法的调用过于复杂不利于开发。于是变想到使用代理的方式帮我远程方法调用的问题。

新增一个Stub代理类实现了IUserService接口

import com.zxh.rpc.common.IUserService;
import com.zxh.rpc.common.User;

import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket;
// 使用一个代理
public class Stub implements IUserService{
    public User findUserById(Integer id){
        User user = null;
        try {
            Socket s = new Socket("127.0.0.1", 8888);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();  // 字节数组输出流 , 就是一块内存空间
            DataOutputStream dos = new DataOutputStream(baos);  // 转换成数据输出流, 在外面包装了一层 DataOutputStream 专门用来写各种各样基础数据类型的
            dos.writeInt(11);
            // 通过socket 输出流 输出
            s.getOutputStream().write(baos.toByteArray()); // toByteArray 最终将数据转成真正的字节数组
            s.getOutputStream().flush();


            // 通过socket 输入流 获取返回数据
            DataInputStream dis = new DataInputStream(s.getInputStream());
            int receivedId = dis.readInt();
            String name = dis.readUTF();

            user = User.builder().id(receivedId).name(name).build();
            dos.close();
            s.close();

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

客户端修改,只要调用代理类方法即可

public class Client {
    public static void main(String[] args) throws Exception {
        Stub stub = new Stub();
        System.out.println(stub.findUserById(123));
    }
}

第三版

可以发现第二版其实就是将原来写在Client的内容封装到了一个类中。好处就是IUserService下的所有接口都可以在代理类Stub中直接调用。但这样还是有缺陷,就是如果IUserService新增接口,那么Stub就需要再写新的实现方法依然是十分麻烦。有没有什么办法可以代理所有接口呢?有!那就是动态代理。
对动态代理不太了解的可以看我这篇文章【Java】代理模式(Proxy模式)详解
同样的服务端代码无需修改
客户端调用

package com.zxh.rpc03;
import com.zxh.rpc.common.IUserService;
public class Client {
    public static void main(String[] args) throws Exception {
        IUserService stub = Stub.getStub();
        System.out.println(stub.findUserById(123));
    }
}

Stub代理类

package com.zxh.rpc03;

import com.zxh.rpc.common.IUserService;
import com.zxh.rpc.common.User;

import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.Socket;

// 使用动态代理
public class Stub  {
    // 通过该方法可以让客户端获取到 一个IUserService的实现类!!!
    public static IUserService getStub(){
        InvocationHandler h = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Socket s = new Socket("127.0.0.1", 8888);

                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                DataOutputStream dos = new DataOutputStream(baos);
                dos.writeInt(123);

                s.getOutputStream().write(baos.toByteArray());
                s.getOutputStream().flush();

                DataInputStream dis = new DataInputStream(s.getInputStream());
                int id = dis.readInt();
                String name = dis.readUTF();

                User user = User.builder().id(id).name(name).build();
                dos.close();
                s.close();
                return user;
            }
        };
        // 代理IUserService,实现IUserService接口
        IUserService service = (IUserService)Proxy.newProxyInstance(IUserService.class.getClassLoader(), new Class[]{IUserService.class}, h);
        return service;
    }
}

第四版

虽然第三版帮我们生成了IUserService的实现类,但还是无法代理所有的方法。因为请求参数,返回值,调用的方法都没有动态。这一版将对这些进行动态生成。
如何确定一个方法?
方法名,方法参数类型(以防有重载方法)即可确定一个方法,然后传入参数即可调用。

package com.zxh.rpc05;
import com.zxh.rpc.common.IUserService;
import com.zxh.rpc.common.User;

import java.io.DataInputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.Socket;

// 使用动态代理
public class Stub  {
    public static IUserService getStub(){
        InvocationHandler h = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Socket s = new Socket("127.0.0.1", 8888);
                ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream());

                String methodName = method.getName(); // 方法名
                Class<?>[] parameterTypes = method.getParameterTypes(); // 方法参数类型
                oos.writeUTF(methodName);
                oos.writeObject(parameterTypes);
                oos.writeObject(args);
                oos.flush();

                ObjectInputStream ois = new ObjectInputStream(s.getInputStream());

                Object o = ois.readObject();  // 返回值类型不做转换,直接返回Object
                oos.close();
                s.close();
                return o;
            }
        };
        // 代理IUserService,实现IUserService接口
        IUserService service = (IUserService)Proxy.newProxyInstance(IUserService.class.getClassLoader(), new Class[]{IUserService.class}, h);
        return service;
    }
}

当然服务端也需要进行一定的修改

package com.zxh.rpc05;
import com.zxh.rpc.common.User;

import java.io.DataOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    private static boolean running = true;
    public static void main(String[] args) throws Exception {
        ServerSocket ss = new ServerSocket(8888);
        while (running){
            Socket s = ss.accept();  // 接收一个客户端连接
            process(s);
            s.close();
        }
    }
    // 处理该客户端链接的输入输出流
    private static void process(Socket s) throws Exception{
        ObjectInputStream ois = new ObjectInputStream(s.getInputStream());  // 输入流
        ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream());  // 输出流

        String methodName = ois.readUTF();
        Class[] parameterType = (Class[]) ois.readObject();
        Object[] args = (Object[]) ois.readObject();

        UserServiceImpl service = new UserServiceImpl();
        Method method = service.getClass().getMethod(methodName, parameterType); //通过反射获取到方法
        Object o = method.invoke(service, args); // 调用方法返回Object
        System.out.println("调用的方法:"+methodName+"("+parameterType+")"+"参数:"+args );
        
        oos.writeObject(o);  // 通过输出流写出
        oos.flush();
    }
}

如上就实现了IUserService的所有方法调用

第五版

当然还可以进行优化上面只实现了IUserService定义的方法调用,那是否可以实现拿到任意类型的接口,能够帮我生成更多的代理?同样可以!那就是告诉服务端那个类型的接口。
先看一下客户端的调用

package com.zxh.rpc06;
import com.zxh.rpc.common.IUserService;
import com.zxh.rpc.common.User;
public class Client {
    public static void main(String[] args) throws Exception {
        IUserService stub = Stub.getStub(IUserService.class); // 传入需要代理的接口类型
        User user = stub.findUserById(123);
        System.out.println(user);
    }
}
package com.zxh.rpc06;

import com.zxh.rpc.common.IUserService;
import com.zxh.rpc.common.User;

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.Socket;
// 使用动态代理
public class Stub{
    // v05: 传入什么类型就返回该类型的动态代理实现类
    // 通过泛型来定义 返回值类型
    public static <T> T getStub(Class<T> clazz){
        InvocationHandler h = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Socket s = new Socket("127.0.0.1", 8888);
                ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream());

                String clazzName = clazz.getName();  // 获取到接口类型
                String methodName = method.getName(); // 方法名
                Class<?>[] parameterTypes = method.getParameterTypes();

                oos.writeUTF(clazzName);
                oos.writeUTF(methodName);
                oos.writeObject(parameterTypes);
                oos.writeObject(args);
                oos.flush();

                ObjectInputStream ois = new ObjectInputStream(s.getInputStream());
                Object o = ois.readObject();

                oos.close();
                s.close();
                return o;
            }
        };
        // 生成代理,强转成T
        T service = (T)Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, h);
        return service;
    }
}

服务端同样要做修改

package com.zxh.rpc06;
import com.zxh.rpc.common.User;

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    private static boolean running = true;

    public static void main(String[] args) throws Exception {
        ServerSocket ss = new ServerSocket(8888);
        while (running){
            Socket s = ss.accept();  // 接收一个客户端连接
            process(s);
            s.close();
        }
    }
    // 处理该客户端链接的输入输出流
    private static void process(Socket s) throws Exception{
        ObjectInputStream ois = new ObjectInputStream(s.getInputStream());  // 输入流
        ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream());  // 输出流

        String clazzName = ois.readUTF();  // 管道是队列 先入先出。客户端先写的clazzName 这边就先读clazzName
        String methodName = ois.readUTF();
        Class<?>[] parameterType = (Class<?>[]) ois.readObject();
        Object[] args = (Object[]) ois.readObject();

        Class clazz = null;
        // 这里需要注意,我们只能拿到接口的类型,而具体的实现类对象需要自己定义用哪个
        if("com.zxh.rpc.common.IProductService".equals(clazzName)){
            clazz = ProductServiceImpl.class;  // 这里可以使用springIOC注入
        }else if ("com.zxh.rpc.common.IUserService".equals(clazzName)){
            clazz = UserServiceImpl.class;
        }

        Method method = clazz.getMethod(methodName, parameterType);
        Object o = method.invoke(clazz.newInstance(), args);  // 第一个参数是调用该方法的对象,第二个参数是调用该方法要传入的属性
        System.out.println("调用的方法:"+methodName+"("+parameterType+")"+"参数:"+args );

        oos.writeObject(o);
        oos.flush();
        oos.close();
        ois.close();
    }
}

这样就实现了所有暴露出来的接口都可以进行动态代理。

序列化框架

还有什么可以优化的呢?那就是序列化框架!
jdk自带的序列化框架java.io.Serializable有一些缺点:

  1. 序列化后的大小较大
  2. 序列化的时间较长

市面上的一些序列化框架

  1. java.io.Serializable
  2. Hessian
  3. google protobuf
  4. facebook Thrift
  5. kyro
  6. fst
  7. xmlrpc(xstream)
  8. json序列化框架
    1. Jackson
    2. google Gson
    3. Ali FastJson

下面是JDK和Hessian序列化对比的程序
引入依赖

<dependency>
   <groupId>com.caucho</groupId>
   <artifactId>hessian</artifactId>
   <version>4.0.65</version>
</dependency>
package com.zxh.rpc09_Hessian02;

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.zxh.rpc.common.User;

import java.io.*;

public class HessianVSJDK {
   // Hessian 序列化的长度比JDK端,序列化的时间也比JDK短
   public static void main(String[] args) throws Exception {
       User user = new User("zhangsan", 1);

       System.out.println("Hessian:"+hessianSerialize(user).length);
       System.out.println("JDK:"+jdkSerialize(user).length);
   }

   public static byte[] hessianSerialize(Object o) throws Exception{
       ByteArrayOutputStream baos = new ByteArrayOutputStream();
       Hessian2Output output = new Hessian2Output(baos);
       output.writeObject(o);
       output.flush();
       byte[] bytes = baos.toByteArray();
       baos.close();
       output.close();
       return bytes;
   }
   public static <T> T deserialize(byte[] bytes,Class<T> clazz) throws IOException {
       ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
       Hessian2Input input = new Hessian2Input(bais);
       Object o = input.readObject();
       bais.close();
       input.close();
       return (T)o;
   }

   public static byte[] jdkSerialize(Object o) throws Exception{
       ByteArrayOutputStream baos = new ByteArrayOutputStream();
       ObjectOutputStream output = new ObjectOutputStream(baos);
       output.writeObject(o);
       output.flush();
       byte[] bytes = baos.toByteArray();
       baos.close();
       output.close();
       return bytes;
   }
   
   public static <T> T jdkDeserialize(byte[] bytes,Class<T> clazz) throws Exception {
       ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
       ObjectInputStream input = new ObjectInputStream(bais);
       Object o = input.readObject();
       bais.close();
       input.close();
       return (T)o;
   }
}

最终输出结果,可见JDK序列化后的字节码长度要远大于Hessian的序列化。且时间也比Hessian长。这里就不做继续比较了

Hessian:45
JDK:187

最终版

改进序列化框架
首先封装一个请求对象

package com.zxh.rpc10;
import lombok.Data;
import java.io.Serializable;

@Data
public class RPCReq implements Serializable {

   private String clazzName;  // 接口类型名
   private String methodName; // 方法名
   private Class[] parameterTypes; // 方法参数类型
   private Object[] args; // 方法实参

   public RPCReq() {
   }
   public RPCReq(String clazzName, String methodName, Class[] parameterTypes, Object[] args) {
       this.clazzName = clazzName;
       this.methodName = methodName;
       this.parameterTypes = parameterTypes;
       this.args = args;
   }
}

封装一个HessianUitl

package com.zxh.rpc10;

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

public class HessianUtil {

   public static byte[] serialize(Object o) throws IOException {
       ByteArrayOutputStream baos = new ByteArrayOutputStream();
       Hessian2Output output = new Hessian2Output(baos);
       output.writeObject(o);
       output.flush();
       byte[] bytes = baos.toByteArray();
       baos.close();
       output.close();
       return bytes;
   }

   public static <T> T deserialize(byte[] bytes,Class<T> clazz) throws IOException {
       ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
       Hessian2Input input = new Hessian2Input(bais);
       Object o = input.readObject();
       bais.close();
       input.close();
       return (T)o;
   }

   public static Object deserialize(byte[] bytes) throws IOException {
       ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
       Hessian2Input input = new Hessian2Input(bais);
       Object o = input.readObject();
       bais.close();
       input.close();
       return o;
   }
}

Stub生成代理方法修改

package com.zxh.rpc10;

import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.Socket;

public class Stub{
   public static <T> T getStub(Class<T> clazz){
       InvocationHandler h = new InvocationHandler() {
           @Override
           public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
               Socket s = new Socket("127.0.0.1", 8888);
               OutputStream output = s.getOutputStream();
               InputStream input = s.getInputStream();

               String clazzName = clazz.getName();  // 接口类型
               String methodName = method.getName(); // 方法名
               Class[] parameterTypes = method.getParameterTypes(); // 参数类型
   			   // 封装请求对象
               RPCReq req = new RPCReq(clazzName,methodName,parameterTypes,args); 
               byte[] serialize = HessianUtil.serialize(req); // 序列化请求对象
               output.write(serialize);
               output.flush();


               byte[] bytes = new byte[1024];
               input.read(bytes);
               Object o = HessianUtil.deserialize(bytes);  // 反序列化返回值

               s.close();
               output.close();
               input.close();
               return o;
           }
       };
       // 代理IUserService,实现IUserService接口
       T service = (T)Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, h);
       return service;
   }
}

服务端代码修改

package com.zxh.rpc10;

import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
   private static boolean running = true;

   public static void main(String[] args) throws Exception {
       ServerSocket ss = new ServerSocket(8888);
       while (running){
           Socket s = ss.accept();  // 接收一个客户端连接
           process(s);
           s.close();
       }
   }
   // 处理该客户端链接的输入输出流
   private static void process(Socket s) throws Exception{
       InputStream input = s.getInputStream();
       OutputStream output = s.getOutputStream();


       byte[] bytes = new byte[1024];
       input.read(bytes);  // 将字节流数据读入字节数组
       RPCReq req = HessianUtil.deserialize(bytes, RPCReq.class);  // 反序列化成RPCReq对象

       String clazzName = req.getClazzName();
       String methodName = req.getMethodName();
       Class[] parameterTypes = req.getParameterTypes();
       Object[] args = req.getArgs();

       Class clazz = null;
       // 根据接口类型调用对应的接口实现
       if("com.zxh.rpc.common.IProductService".equals(clazzName)){
           clazz = ProductServiceImpl.class;
       }else if ("com.zxh.rpc.common.IUserService".equals(clazzName)){
           clazz = UserServiceImpl.class;
       }

       Method method = clazz.getMethod(methodName, parameterTypes);
       Object o = method.invoke(clazz.newInstance(), args);  // 第一个参数是调用该方法的对象,第二个参数是调用该方法要传入的属性

       byte[] serialize = HessianUtil.serialize(o);  // 序列化返回值
       output.write(serialize);
       output.flush();

       output.close();
       input.close();
       System.out.println("调用的方法:"+methodName+"("+parameterTypes+")"+"参数:"+args );
   }
}

总结

RPC远程过程调用。其实就是客户端通过网络传递数据告诉服务端要调用哪个方法。而对于Java来说如何唯一确定一个方法呢?那就是接口类型,方法名,方法参数类型就可以唯一确定一个方法!再加上实参就可以调用方法了。
通过公共模块定义好一个接口以及其他参数类型和返回值类型。客户端和服务端都依赖于这个公共模块。
客户端通过JDK动态代理生成接口实现类。主要就是负责传递参数以及处理返回值。
服务端获取客户端的参数,根据接口类型找到对应的实现类,调用方法并返结果。
而其中在网络传输部分可以有Hessian等序列化框架进行优化。

;