十分钟写一个简易版的RPC

944 阅读8分钟

RPC介绍

大家好,我是jack xu,我们知道RPC(Remote Procedure Call)就是远程过程调用,它是一种通过网络从远程计算机程序请求服务。调用远程计算机上的服务,就像调用本地服务一样丝滑。

下面是RPC的演进历史,一开始是RMI,但是局限Java与Java之前的通信,不能跨语言;接下来是http+xml,即webservice,可以跨语言调用,但是我们知道xml是很大的,很占网络资源;接下来就是http+json,很轻量级,很是要写很多重复的非业务代码;接下来就是框架阶段了,Google 的GRPC,Facebook的Thrift(现在交给了Apache),阿里的Dubbo,最后到 SpringCloud 用到的 Restful。 image.png

这里补充说下,不要说RPC好,也不要说HTTP好,两者各有千秋。本质上,两者是可读性和效率之间的抉择,通用性和易用性之间的抉择。最终谁能发展更好,很难说。

本文用到的知识:

《java核心基础之反射》

《帮我们找房租房买房的代理模式》

《java核心基础之自定义注解》

《从根儿上认识线程池》 这些文章也是我写的,感兴趣的小伙伴可以看下。

流程图

这是一个网上的通用流程图,当发起请求的时候,调用方通过动态代理,然后把请求的参数进行序列化,通过网络到达被调用方,被调用方拿到参数,进行反序列化,然后在本地进行反射调用方法,最后再将计算出来的结果进行序列化返回给调用方,调用法反序列化取得值。整体就是这样一个流程。 image.png

下面是本次手写RPC的一个流程图,用户发起请求访问客户端rpc-user-service服务,rpc-user-service再去调用服务端rpc-order-service服务查询订单信息。当中也会经过序列化和反序列化流程。

image.png

代码实现

服务端rpc-order-service

订单服务rpc-order-service,这是一个maven项目,这是一个父pom,然后创建两个子项目,order-api和order-provider,这两个也是maven项目,项目结构如下。

image.png

order-api

order-api是契约,也就是定义接口的,order-provider 需要实现它。然后把它打成一个jar包,上传到nexus私服,因为 rpc-user-service 也需要引用它,调用order服务提供的契约。

image.png

RpcRequest 类就是定义 rpc-user-service 请求 rpc-order-service 时,告诉 order 调用哪个类里的哪个方法以及传入的参数是什么。这里我没有搭建私服,一般公司是有私服的,在自己电脑上用 install 安装到maven 本地仓库即可。

@Data
public class RpcRequest implements Serializable {

    private String className;

    private String methodName;

    private Object[] args;


}

order-provider

先看下项目中的类,类很多,然后我们接下来分别讲解。

image.png

首先是service层实现契约,既然是实现,先引用一下order-api的pom。

<dependency>
    <groupId>com.jack</groupId>
    <artifactId>order-api</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

实现类OrderServiceImpl.class

//该注解bean加载以后会将bean信息保存到哈希表
@JackRemoteService
public class OrderServiceImpl implements IOrderService {

    @Override
    public String queryOrderList() {
        return "this is rpc-order-service queryOrderList method";
    }

    @Override
    public String orderById(String id) {
        return "this is rpc-order-service orderById method,param  is " + id;
    }

}

细心的小伙伴发现,这里打了一个自定义注解@JackRemoteService,打这个注解的作用是当bean加载完以后把该bean的信息保存到哈希表,以供后面的反射调用。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface JackRemoteService {

}

注解就是一个打标记的作用,打了标记就需要有人去识别它。这里就需要实现BeanPostProcessor接口,重写里面的postProcessAfterInitialization方法。这个方法里干的事就是检查加载的当前bean有没有打JackRemoteService这个注解,如果打了就把bean里面的所有方法添加到哈希表里。

/**
 * @author jackxu
 * bean加载以后将bean的信息保存到哈希表
 */
@Component
public class InitialMerdiator implements BeanPostProcessor {
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

        if (bean.getClass().isAnnotationPresent(JackRemoteService.class)) {
            Method[] methods = bean.getClass().getDeclaredMethods();
            for (Method method : methods) {
                //接口名.方法名
                String key = bean.getClass().getInterfaces()[0].getName() + "." + method.getName();
                BeanInfo beanInfo = new BeanInfo();
                beanInfo.setBean(bean);
                beanInfo.setMethod(method);
                Mediator.getInstance().put(key, beanInfo);
            }
        }
        return bean;
    }

}

哈希表的定义是Mediator.class,key是类名.方法名

public class Mediator {

    public Map<String, BeanInfo> map = new ConcurrentHashMap<>();

    private Mediator() {
    }


    private static volatile Mediator instance;


    public static Mediator getInstance() {
        if (instance == null) {
            synchronized (Mediator.class) {
                if (instance == null) {
                    instance = new Mediator();
                }
            }
        }
        return instance;
    }

    public Map<String, BeanInfo> getMap() {
        return map;
    }

    public void put(String key, BeanInfo beanInfo) {
        map.put(key, beanInfo);
    }
    
}

最后在所有bean都加载完以后,启动一个socket的监听,这样服务端就写好了,等待客户端的请求。

spring有一些内置的事件,当完成某种操作时会发出某些事件动作。比如监听ContextRefreshedEvent事件,当所有的bean都初始化完成并被成功装载后会触发该事件,实现ApplicationListener < ContextRefreshedEvent >接口可以收到监听动作,然后写自己的逻辑。

SocketServerInitial.class

//spring容器启动完成之后,会发布一个ContextRefreshedEvent
@Component
public class SocketServerInitial implements ApplicationListener<ContextRefreshedEvent> {
    //线程池
    private final ExecutorService executorService = new ThreadPoolExecutor(5, 10, 0L,
            TimeUnit.MILLISECONDS,
            new ArrayBlockingQueue<Runnable>(10), Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy());

    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
        //启动服务
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(8888);
            while (true) {
                Socket socket = serverSocket.accept();
                executorService.execute(new ProcessorHandler(socket));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //关闭socket
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

线程池里执行的方法,就是把接收到的socket请求,先把RpcRequest进行反序列化,然后按照传递过来的接口、方法在哈希表中找到该方法,然后通过反射进行调用,最终将结果返回去。

/**
 * @author jack xu
 */
public class ProcessorHandler implements Runnable {

    private Socket socket;

    public ProcessorHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        ObjectOutputStream outputStream = null;
        ObjectInputStream inputStream = null;
        try {
            inputStream = new ObjectInputStream(socket.getInputStream());
            //反序列化
            RpcRequest request = (RpcRequest) inputStream.readObject();
            //根据传过来的参数执行方法
            System.out.println("request :" + request);
            Object result = processor(request);
            System.out.println("response :" + result);
            //将计算结果写入输出流
            outputStream = new ObjectOutputStream(socket.getOutputStream());
            outputStream.writeObject(result);
            outputStream.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //关闭流
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public Object processor(RpcRequest request) {
        try {
            Map<String, BeanInfo> map = Mediator.getInstance().getMap();
            //接口名.方法名
            String key = request.getClassName() + "." + request.getMethodName();
            //取出方法
            BeanInfo beanInfo = map.get(key);
            if (beanInfo == null) {
                return null;
            }
            //bean对象
            Object bean = beanInfo.getBean();
            //方法
            Method method = beanInfo.getMethod();
            //反射
            return method.invoke(bean, request.getArgs());
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

采用BIO的传输方式,必须需要执行完毕一个请求后才可以执行下一个请求,这样就会导致效率很低,所以采用线程池的方式解决这个问题,但是如果请求非常多,依然会出现堵塞,最好的方式是用netty的方式来实现rpc。

客户端rpc-user-service

rpc-user-service 是一个 spring boot 项目,因为最终我们要通过 restful 来调用的,如果用 ssm 搭建太慢了,还是先看下项目整体结构。

image.png

我们从controller层开始看,首先是引用了接口order-api,因为我们已经安装到本地的maven仓库了,所以直接引用下pom即可。

<dependency>
    <groupId>com.jack</groupId>
    <artifactId>order-api</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
@RestController
public class UserController {

    //这里的作用是将接口封装成一个代理对象
    @JackReference
    private IOrderService orderService;

    @JackReference
    private IGoodService goodService;

    @GetMapping("/test")
    public String test() {
        return orderService.queryOrderList();
    }

    @GetMapping("/get")
    public String get() {
        return goodService.getGoodInfoById(1L);
    }
}

我们看到这里也有一个自定义注解JackReference,它的作用是将打上该注解的接口变为代理对象。

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface JackReference {

}

我们还是依葫芦画瓢,当bean加载前,这里是postProcessBeforeInitialization方法,将打上JackReference注解的接口设置为代理对象。

@Component
public class ReferenceInvokeProxy implements BeanPostProcessor {

    @Autowired
    RemoteInvocationHandler invocationHandler;

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        //获取所有字段
        Field[] fields = bean.getClass().getDeclaredFields();
        for (Field field : fields) {
            if (field.isAnnotationPresent(JackReference.class)) {
                field.setAccessible(true);
                Object proxy = Proxy.newProxyInstance(field.getType().getClassLoader(), new Class<?>[]{field.getType()}, invocationHandler);
                try {
                    field.set(bean, proxy);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
        return bean;
    }

}

我们知道orderService.queryOrderList()在本地我们是没有这个实例的,也执行不了,所以代理对象里干的就是把要执行的方法、参数封装成RpcRequest,然后通过Socket发送到服务端,然后拿到返回的数据,让我们看起来就像在本地执行一样,实际是代理对象帮我们干了很多事。

@Component
public class RemoteInvocationHandler implements InvocationHandler {

    @Value("${rpc.host}")
    private String host;

    @Value("${rpc.port}")
    private int port;


    @Override
    public Object invoke(Object proxy, Method method, Object[] args) {
        RpcRequest request = new RpcRequest();
        request.setArgs(args);
        request.setClassName(method.getDeclaringClass().getName());
        request.setMethodName(method.getName());
        return send(request);
    }

    public Object send(RpcRequest request) {
        ObjectOutputStream outputStream = null;
        ObjectInputStream inputStream = null;
        try {
            Socket socket = new Socket(host, port);
            //IO操作
            outputStream = new ObjectOutputStream(socket.getOutputStream());
            outputStream.writeObject(request);
            outputStream.flush();
            inputStream = new ObjectInputStream(socket.getInputStream());
            return inputStream.readObject();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        } finally {
            //关闭流
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

测试

首先启动服务端,服务端的代码是这样写的,需要加上ComponentScan扫包

/**
 * @author jack xu
 */
@Configuration
@ComponentScan("com.jack")
public class Bootstrap {

    public static void main(String[] args) {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Bootstrap.class);
    }

}

已经跑起来了,等待客户端请求 image.png

客户端是spring boot项目,正常启动即可

@SpringBootApplication
public class RpcUserServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(RpcUserServiceApplication.class, args);
    }

}

也跑起来了 image.png 然后打开浏览器访问一下,成功拿到结果了

image.png

服务端也打印出来对应的日志,一次完整的RPC请求结束。 image.png

结尾

本文的源码在github上

rpc-user-service

rpc-order-service

最后总结下我们这用的是多线程+BIO的模式,感兴趣的小伙伴可以改成 Netty 的方式。另外请求的地址我们在这里也是写死的,也没有做负载均衡,一般是要搭配注册中心使用的,更完善的还会有监控等功能,真正的Dubbo做了很多东西,本文只是探讨研究两个服务间的通信,谢谢观看~