系统间通信:基于TCP协议的RPC实现范例
一、RPC名词解释
RPC的全称是Remote Process Call,即远程过程调用,它应用广泛,实现方式也很多,拥有RMI、WebService等诸多成熟的方案,在业界得到了广泛的使用。单台服务的处理能力受到硬件成本的限制,不可能无限制地提升。RPC将原来的本地调用转变为调用远端的服务器上的方法,给系统的处理能力和吞吐量带来了近似于无限制提升的可能,这是系统发展到一定阶段必然性的变革,也是实现分布式计算的基础。
RPC的实现包括客户端和服务端,即服务的调用方与服务的提供方。服务调用方发送RPC请求到服务提供方,服务提供方根据调用方提供的参数执行请求方法,将执行结果返回给调用方,一次RPC调用完成。这里涉及到网络通信、序列化与反序列化的操作,如果服务提供者是多台或者服务提供者进行了分组,还会涉及到负载均衡与路由。
二、RPC调用过程
随着业务的发展,服务调用者的规则发展到一定阶段,对服务提供方的压力也日益增加,因此,服务需要进行扩容。而随着服务提供者的增加与业务的发展,不同的服务之间还需要进行分组,以隔离不同的业务,避免相互影响,在这种情况下,服务的路由和负载均衡成为必须考虑的问题。服务消费者通过获取服务提供者的分组信息和地址信息进行路由,如果服务提供者为一个集群而非单台机器,则需要根据相应的负载均衡策略,选取其中一台进行调研,有关服务的路由和负载均衡,后续章节会进行详细介绍RPC调用过程(不包括负载均衡和路由)如下图所示:
(1) 服务消费方(client)调用以本地调用方式调用服务;;
(2) clientstub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体,并将消息发送到服务端(对象序列化);
(3) serverstub收到消息后进行解码(对象反序列化);
(4) serverstub根据解码结果调用本地的服务;
(5) 本地服务执行并将结果返回给server stub;
(6) serverstub将返回结果打包成消息并发送至消费方;
(7) clientstub接收到消息,并进行解码;
(8) 服务消费方得到最终结果。
RPC的目标就是要2~7这些步骤都封装起来,让用户对这些细节透明。(注意:无论是何种类型的数据,最终都需要转换二进制流在网络上进行传输,数据的发送方需要将对象转换为二进制流,而数据的接收方则需要把二进制流再恢复为对象。目前成熟的方案有多种,比如Protocol Buffers、Java 自带方式、Hessian、JSON、XML等)。各种序列化方式性能对比图如下所示:
三、范例代码
基于Java的Socket API,我实现一个简单的RPC调用的范例,在这个范例中,包括服务的接口、接口的远程实现、服务的提供方以及服务的消费方。为了方便大家下载代码运行,我把代码放到了Github(https://github.com/dreamerkr/RPCDemo ),其类图如下所示:
(1) 服务接口SayHelloservice
package com.primeton.RPCDemo; public interface SayHelloService { public String sayHello(String msg); }
(2) 接口远端实现
<span style= "font-size:14px;" > package com.primeton.RPCDemo; public class SayHelloServiceImpl implements SayHelloService { @Override public String sayHello(String msg) { if ( "hello" .equals(msg)){ return "hello client" ; }else { return "bye bye" ; } } }</span>
(3) 服务消费者
<span style= "font-size:14px;" > package com.primeton.RPCDemo; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Method; import java.net.Socket; public class Consumer { public static void main(String[] args) throws Exception { String interfaceName = SayHelloService.class .getName(); Method method = SayHelloService.class .getMethod( "sayHello" , java.lang.String.class ); Object[] argments = {"hello" }; Socket socket = new Socket( "127.0.0.1" , 1234 ); ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream()); outputStream.writeUTF(interfaceName); outputStream.writeUTF(method.getName()); outputStream.writeObject(method.getParameterTypes()); outputStream.writeObject(argments); System.out.println("发送信息到服务端,发送的信息为:" +argments[ 0 ]); ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream()); Object object = inputStream.readObject(); System.out.println("服务返回的结果为" +object); } }</span>
(4) 服务提供者
<span style= "font-size:14px;" > package com.primeton.RPCDemo; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Method; import java.net.ServerSocket; import java.net.Socket; import java.util.HashMap; import java.util.Map; public class Provider { public static void main(String[] args) throws Exception { ServerSocket serverSocket = new ServerSocket( 1234 ); Map<Object,Object> services = new HashMap<Object,Object>(); services.put(SayHelloService.class , new SayHelloServiceImpl()); while ( true ){ System.out.println("服务提供者启动,等待客户端调用…………" ); Socket socket = serverSocket.accept(); ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream()); String interfaceName = objectInputStream.readUTF(); String methodName = objectInputStream.readUTF(); Class<?>[] paramterTypes = (Class<?>[])objectInputStream.readObject(); Object[] argments = (Object[])objectInputStream.readObject(); System.out.println("客户端调用服务端接口" +interfaceName+ "的" + methodName+ "方法" ); Class serviceClass = Class.forName(interfaceName); Object serivce = services.get(serviceClass); Method method = serviceClass.getMethod(methodName, paramterTypes); Object result = method.invoke(serivce, argments); ObjectOutputStream stream = new ObjectOutputStream(socket.getOutputStream()); stream.writeObject(result); System.out.println("服务端返回结果为" +result); } } }</span>
服务提供者端事先将服务实例化好后放在services这个Map中(此处涉及服务的路由被简化处理了,后面章节会详细讲解),通过一个while循环,不断地接收到来的请求,得到所需的参数,包括接口名称、方法名称、参数类型和参数,通过Java的反射取得接口所需调用的方法,执行后将结果返回给服务的消费者。
在真实的生产环境中,常常是多个客户端同时发送多个请求到服务端,服务端则需要同时接收和处理多个客户端请求,涉及并发处理、路由、负载均衡、监控、日志记录等功能,这个是无法满足的。下一章我将给大家分享基于HTTP协议的RPC实现的范例。