1、简述
RPC即远程过程调用。在分布式环境中,客户机和服务器在不同的机器上运行,客户端通过网络通信,调用在服务器端运行的过程,并把结果发送回客户端。
程序调用本地方法时,在同一个进程,共享内存区域,但远程过程调用则发生在不过的机器,不同进程,客户端要调用服务端的方法,就需要解决一下几个关键点:
寻址
分布式应用一般都会部署在不同的服务器上,而且同一个应用会部署多个节点。这样客户端调用服务端就需要知道服务端的地址,端口等信息。在并发量较大的情况下,需要将大量请求均匀分发到多个服务端,以提升系统吞吐量。所以PRC框架需要注册中心来完成服务注册与发现,以及负载均衡的功能。常见的注册中心就是zookeeper、Eureka。因为寻址其实就是基于观察者模式来实现,所以原则上redis,消息队列等提供发布订阅功能的中间件也能做注册中心。
序列化、反序列化
要实现远程服务调用就需要通过网络来传输请求对象信息,那就要实现对象的序列化反序列化方法。序列化方法的实现方式很大程度上影响RPC框架的性能优劣。
网络协议
服务调用的网络通讯协议多种多样,有基于socket的、有用NIO的、也有用http、tcp协议的。网络协议也是RPC框架的性能决定性因素之一。所以有许多优秀的网络通讯的框架,比如netty、mina。
2、主流RPC框架介绍
目前流行的RPC框架有
dubbo
阿里巴巴2012年开源的优秀轻量级RPC框架,后捐献到Apache基金会,2019年5月毕业,成为Apache顶级项目。dubbo在国内被广泛使用。支持的注册中心有Muticast、Zookeeper、Redis、Simple。默认采用Hessian做序列化策略,网络通讯框架使用Netty,默认使用dubbo协议。适用于服务消费者数量远大于消费提供者的场景。
thrift
由facebook推出,使用socket进行数据传输,数据以特定的格式发送,接收方进行解析,支持多种协议。是一个支持多语言的RPC框架。
gRPC
Google开源的一个高性能、通用的RPC框架,主要面向移动应用开发并基于HTTP/2协议标准,基于ProtoBuf(Protocol Buffers)序列化协议开发,且支持众多开发语言。
另外还有各大互联网公司开源的优秀RPC框架,诸如Twitter的Finagle、新浪的Motan、腾讯的Tars、当当在dubbo基础上开源dubbox、百度的brpc等。
3、手写简单RPC框架
为了深入理解rpc基本原理,下面手写一个最简单的rpc框架,用java的BIO做序列化、socket网络通讯。负载均衡,服务注册功能就不写了,主要是理解RPC底层如何通过动态代理、反射技术实现远程服务调用。
代码结构:
核心代码就是框架服务端和客户端的实现。
请求类:RpcRequest
public class RpcRequest implements Serializable {
//接口全限定名
private String className;
//方法名
private String methodName;
//方法参数类型
private Class<?>[] paramTypes;
//方法参数
private Object[] params;
/*getter and setter*/
}
客户端通过该请求告诉服务端要调用哪个服务、哪个方法、方法参数类型及参数。
响应类:RpcResponse
public class RpcResponse implements Serializable {
//异常
private Throwable error;
//调用结果
private Object result;
/*getter and setter*/
}
响应类保存了服务调用的结果,和调用失败的错误信息。请求和响应都要进行网络传输,所以要实现Serializable接口。以便序列化。
客户端:RpcClient
public class RpcClient {
public Object execute(RpcRequest request,String host,int port) throws Throwable {
Socket server = new Socket(host, port);
ObjectInputStream ois = null;
ObjectOutputStream oos = null;
try{
//将请求写到连接服务端socket的输出流
oos = new ObjectOutputStream(server.getOutputStream());
oos.writeObject(request);
oos.flush();
//读取输入流的内容
ois = new ObjectInputStream(server.getInputStream());
Object res = ois.readObject();
RpcResponse response = null;
if (!(res instanceof RpcResponse)){
throw new InvalidClassException("相应类型不正确,应当为"+RpcResponse.class+"类型");
}else{
response = (RpcResponse) res;
}
if (response.getError()!=null){
throw response.getError();
}
return response.getResult();
} catch (ClassNotFoundException e) {
e.printStackTrace();
throw e;
} finally {
if (ois!=null)ois.close();
if (oos != null) oos.close();
if (server != null) server.close();
}
}
}
客户端通过socket连接服务端,将request序列化发送到服务端,等待响应,然后反序列化,读取调用结果。这里要通过动态代理来发送请求,实现基于接口远程调用。客户端本地定义接口,具体的实现类在服务端。客户端代理类:
客户端代理类:RpcClientProxy
public class RpcClientProxy implements InvocationHandler {
private String host;
private int port;
public RpcClientProxy(String host, int port) {
this.host = host;
this.port = port;
}
@SuppressWarnings("unchecked")
public <T> T getProxy(Class<T> clazz){//clazz必须是接口类型
return (T) Proxy.newProxyInstance(clazz.getClassLoader(),new Class[]{clazz},RpcClientProxy.this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//这里可以功能增强,比如负载均衡,过滤器
System.out.println("调用远程方法前,xxxxxx");
RpcRequest request = new RpcRequest();
request.setClassName(method.getDeclaringClass().getName());
request.setMethodName(method.getName());
request.setParamTypes(method.getParameterTypes());
request.setParams(args);
Object result = new RpcClient().execute(request, host, port);
//功能增强,比如记录流水信息
System.out.println("调用远程方法后,xxxxxx");
return result;
}
}
通过getProxy方法获取服务接口代理对象,执行目标方法时会调用invoke方法,invoke方法调用了客户端execute方法,发送请求到服务端。
自定义注解:Service
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {
/**
* 注解所属接口类型
* @return
*/
Class<?> value();
}
使用这个注解标记类为服务类,有这个注解的类将被实例化,存进map。value参数为服务的接口类型。
服务端:RpcServer
对于服务端来说,就是五个步骤:
-
根据指定端口号,创建socket连接
-
通过参数获取要扫描的包路径,扫描之。也就是遍历目录,找到所有有@Service注解的类,实例化它,存在map里。key是类的全限定名,value是实例。
-
阻塞等待获取客户端连接,
-
拿到连接后,具体的操作放到线程池中去执行
代码如下:
public class RpcServer {
public void start(int port,String clazz){
ServerSocket server = null;
try {
//1、创建socket连接
server = new ServerSocket(port);
//2、获取所有服务类
Map<String,Object> services = getService(clazz);
//3、创建线程池
Executor executor = new ThreadPoolExecutor(5, 10, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>() {
});
while(true){
//4、获取客户端连接
Socket client = server.accept();
//5、将服务端被调用的服务放到线程池中异步执行
RpcServerHandler service = new RpcServerHandler(client,services);
executor.execute(service);
}
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
if (server!=null){
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/*扫描包路径获取rpc服务类,并构建实例,与全限定名对应存在map里*/
private Map<String, Object> getService(String clazz) throws ClassNotFoundException {
if (clazz == null) {
throw new ClassNotFoundException("扫描包名为空");
}
Map<String,Object> services = new HashMap<>();
//全限定名数组
String[] clazzes = clazz.split(",");
try {
List<Class<?>> classes = new ArrayList<>();
for (String cl:clazzes) {
List<Class<?>> classList = getClasses(cl);
classes.addAll(classList);
}
//通过反射循环创建示例
for (Class<?> cla:classes) {
Object object = cla.newInstance();
services.put(cla.getAnnotation(Service.class).value().getName(),object);
}
}catch (InstantiationException | IllegalAccessException | ClassNotFoundException e ) {
e.printStackTrace();
}
return services;
}
private List<Class<?>> getClasses(String packageName) throws ClassNotFoundException {
//定义返回的列表
List<Class<?>> classes = new ArrayList<>();
//找到指定的包目录
File directory = null;
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if (contextClassLoader == null) {
throw new ClassNotFoundException("无法获取到ClassLoader");
}
String path = packageName.replace(".","/");
//TODO 处理目录含中文或者空格的问题
URL resource = contextClassLoader.getResource(path);
if (resource == null) {
throw new ClassNotFoundException("无法获取该资源("+packageName+")");
}
directory = new File(resource.getFile());
if(directory.exists()){
//获取包目录留下所有文件
String[] files = directory.list();
File[] fileList = directory.listFiles();
for (int i = 0; fileList!=null&&i<fileList.length; i++) {
if ( null == files[i]) {break;}
File file = fileList[i];
//判断是否为class文件
if (file.isFile()&&file.getName().endsWith(".class")){
Class<?> clazz = Class.forName(packageName+"."+files[i].substring(0,files[i].length()-6));
if (clazz.getAnnotation(Service.class)!=null) {//如果有@Service注解,添加到列表
classes.add(clazz);
}
}else if(file.isDirectory()){ //如果是目录,则递归查找
List<Class<?>> classList = getClasses(packageName+"."+file.getName());
if (classList != null && classList.size()!=0) {
classes.addAll(classList);
}
}
}
}else{
throw new ClassNotFoundException("资源不存在"+directory);
}
return classes;
}
}
核心就是start方法,getService方法主要运用IO操作和反射,找到类的目录判断注解,实例化,注意异常的处理以及资源的关闭。
注意:代码没有处理URL编码问题,如果项目所在文件夹名有中文或空格会有错误。
服务端任务类:RpcServerHandler
现在看一下服务端任务类RpcServerHandler,
public class RpcServerHandler implements Runnable {
private Socket clientSocket;
private Map<String,Object> serviceMap;
public RpcServerHandler(Socket client, Map<String, Object> services) {
this.clientSocket = client;
this.serviceMap = services;
}
@Override
public void run() {
ObjectInputStream ois = null;
ObjectOutputStream oos = null;
RpcResponse response = new RpcResponse();
try{
ois = new ObjectInputStream(clientSocket.getInputStream());
oos = new ObjectOutputStream(clientSocket.getOutputStream());
//反序列化
Object object = ois.readObject();
RpcRequest request = null;
if (!(object instanceof RpcRequest)){
response.setError(new Exception("请求类型错误"));
oos.writeObject(response);
oos.flush();
return;
}else {
request = (RpcRequest) object;
}
//查找并执行服务
Object service = serviceMap.get(request.getClassName());
Class<?> clazz = service.getClass();
Method method = clazz.getMethod(request.getMethodName(),request.getParamTypes());
Object result = method.invoke(service,request.getParams());
response.setResult(result);
oos.writeObject(response);
oos.flush();
} catch (Exception e) {
if (oos != null) {
response.setError(e);
try {
oos.writeObject(response);
oos.flush();
} catch (IOException e1) {
e1.printStackTrace();
}
}
} finally {
try {
if (ois!=null) ois.close();
if (oos != null) oos.close();
if (clientSocket != null) clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
该类实现Runnable接口,作为一个任务放在线程池中执行 。收到客户端的连接后,从连接的输入流中读取信息,并反序列化为RpcRequest类。根据request中的className字段,到存放服务实例的map中去找到对应的实例,以及request中的方法有关信息,就可以通过反射调用服务实例的对应方法,最后将执行结果放到RpcResponse类中通过socket连接范送给客户端。
一次远程服务调用完成。
4、测试
定义服务接口:StudentService
定义两个方法,一个在客户端输出,一个在服务端输出。
public interface StudentService {
Student getInfo();
boolean printInfo(Student student);
}
服务实现类:StuentServiceImpl
简单写一个实现类
@Service(StudentService.class)
public class StudentServiceImpl implements StudentService {
@Override
public Student getInfo() {
Student stu = new Student();
stu.setId(1);
stu.setName("zhangsan");
stu.setAge(20);
return stu;
}
@Override
public boolean printInfo(Student student) {
if (student == null) {
return false;
}
System.out.println(student);
return true;
}
}
测试类
public class ServerTest {
public static void main(String[] args) {
RpcServer server= new RpcServer();
server.start(9999,"com.youzi.test.rpc.demo");
}
}
启动服务端,定义端口号和要扫描的包。
public class ClientTest {
public static void main(String[] args) {
RpcClientProxy clientProxy = new RpcClientProxy("127.0.0.1", 9999);
StudentService proxy = clientProxy.getProxy(StudentService.class);
// System.out.println(proxy.getInfo());
System.out.println(proxy.printInfo(new Student(2,"lisi",18)));
}
}
启动客户端,连接本机服务端开的9999端口,拿到服务接口类型StudentService的代理对象,调用目标方法会出发客户端的execute方法,进行远程调用。
测试结果:
以上就是一个最简单的RPC框架,不包含负载均衡,注册中心,服务治理,集群容错等高级用法。主要运用socket、bio、动态代理、线程池、反射等基本知识点,实现RPC的基本功能,远程过程调用。