前言
这篇博客主要介绍RMI(Remote Method Invocation)远程方法调用,这个东西现在用的不多,只是作为分布式基础,进行一个简单了解,为了引出RPC的相关概念,并对RPC做一个简单认识,方便后面学习Dubbo
RMI简介
RMI全称是remote method invocation 远程方法调用,一种用于远程过程调用的应用程序编程接口,是纯java的网络分布式应用系统的核心解决方案之一。RMI使用Java远程消息交换协议JRMP(Java Remote Messaging Protocol)进行通信,JRMP是专门为Java对象制定的,对非Java语言开发的应用系统支持不足,不能与非java语言的对象进行通信。
RMI简单实现
目前了解的RMI好像有两种实现方式,一种是单独运行注册表,另一种是在服务器端运行注册表,由于本身这里的RMI只是为了方便自己理解RPC框架,这里不再纠结于那种方式,而是简单采用手动操作,能完成调用就算成功,至于深入理解,后面会补上。
1、建立服务端项目rmi-server,建立相应的包,然后编写对外的服务接口
/**
*
* @author liman
* @createtime 2018年8月16日
* @contract 15528212893
* @comment:
* 创建远程接口,需要继承至remote
*/
public interface PersonService extends Remote{
public List<PersonEntity> GetList() throws RemoteException;
}
该接口需要实现Remote接口,这个接口是个标记接口,由于远程方法调用的本质是网络通信,只是隐藏了底层实现。网络通信出现异常需要处理,所以所有的方法都必须要抛出RemoteException以说明该方法可能抛出网络通信异常。
2、编写远程方法接口实现类
package com.learn.rmi.self.JRMP;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.LinkedList;
import java.util.List;
/**
*
* @author liman
* @createtime 2018年8月16日
* @contract 15528212893
* @comment:
* 远程服务接口的实现类,这个实现类需要继承UnicastRemoteObject并实现PersonService接口
*/
public class PersonServiceImpl extends UnicastRemoteObject implements PersonService{
protected PersonServiceImpl() throws RemoteException {
super();
}
public List<PersonEntity> GetList() throws RemoteException {
System.out.println("Get Person Start!");
List<PersonEntity> personList=new LinkedList<PersonEntity>();
PersonEntity person1=new PersonEntity();
person1.setAge(25);
person1.setId(0);
person1.setName("Leslie");
personList.add(person1);
PersonEntity person2=new PersonEntity();
person2.setAge(25);
person2.setId(1);
person2.setName("Rose");
personList.add(person2);
return personList;
}
}
该方法的方法参数和返回值会在网络上传输,所以必须要实现序列化接口。
3、建立客户端项目,rmi-client
将前两部的服务端接口代码(PersonService)和实体(PersonEntity)拷贝到客户端中,需要保证package路径与服务端一致。首先承认这步操作有点骚气,一般这个步骤在之前的开发中都会有java编译器自带的RMI注册服务完成。
4、编写服务端的服务发布程序
package com.learn.rmi.self.JRMP;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
/**
*
* @author liman
* @createtime 2018年8月16日
* @contract 15528212893
* @comment:
* 服务端注册通讯端口
*/
public class ServerProgram {
public static void main(String[] args) {
try {
PersonService personService = new PersonServiceImpl();
//注册通讯端口
LocateRegistry.createRegistry(6600);
//注册通讯路径
Naming.rebind("rmi://127.0.0.1:6600/PersonService", personService);
System.out.println("Service start");
} catch (Exception e) {
e.printStackTrace();
}
}
}
LocateRegistry.createRegistry(6600);——注册通讯端口。Naming.rebind()——注册通讯路径。
5、编写客户端代码
在确认将服务端的service接口和对应的实体类拷贝过来之后,编写客户端代码
package com.learn.rmi.self.JRMP;
import java.rmi.Naming;
import java.util.List;
/**
*
* @author liman
* @createtime 2018年8月16日
* @contract 15528212893
* @comment:
* 客户端进行测试
*/
public class ClientProgram {
public static void main(String[] args) {
try {
PersonService personService = (PersonService)Naming.lookup("rmi://127.0.0.1:6600/PersonService");
List<PersonEntity> personList = personService.GetList();
for(PersonEntity person:personList) {
System.out.println("ID:"+person.getId()+" Age:"+person.getAge()+" Name:"+person.getName());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
6、测试运行
先运行服务端代码,完成之后控制台输出如下结果,表示服务端启动成功
然后启动客户端:
客户端没有维护PersonService的逻辑,只是保留了服务端的接口信息,客户端能成功调用相应的服务。不过有几个细节需要注意。
1、涉及到的序列化对象需要实现Serializable接口,这里的就是PersonEntity对象。
2、客户端和服务端PersonEntity中的serialVersionUID的值要一致,否则会抛出二者不一致的异常。关于serialVersionUID这个字段的作用,在序列化一文中有过总结,但是不够全面,这里可以参看这篇文章——serialVersionUID字段的作用。
RMI底层通信(取消注册中心)
其实RMI的原理和RPC框架很像,前几天在看源码的过程中,终于成功被源码绕晕,看来还是先只能简单实现,源码的理解后面再深入。
分为客户端和服务端,底层实质就是序列化和反序列化的通信。
客户端代码
1、客户端测试代码
主要通过获取服务端远程的对象和调用结果
package com.learn.rmi.rpc;
/**
* Created by liman on 2018/8/18.
* QQ:657271181
* e-mail:[email protected]
*/
public class ClientDemo {
public static void main(String[] args) {
RpcClientProxy rpcClientProxy = new RpcClientProxy();
ILearnHello learnHello = rpcClientProxy.clientProxy(ILearnHello.class,"localhost",8888);
System.out.println(learnHello.sayHello("liman"));
}
}
2、RpcClientProxy代理类
package com.learn.rmi.rpc;
import com.learn.rmi.ref.rpc.RemoteInvocationHandler;
import java.lang.reflect.Proxy;
/**
* Created by liman on 2018/8/18.
* QQ:657271181
* e-mail:[email protected]
*
* 客户端的代理
*/
public class RpcClientProxy {
public <T> T clientProxy(final Class<T> interfaceClass,final String host,final int port){
return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(),
new Class[]{interfaceClass}, new RemoteInvocationHandler(host,port));
}
}
3、RemoteInvocationHandler类,设置请求对象中需要调用的目标方法和参数,并完成请求的发送和结果获取
package com.learn.rmi.rpc;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
/**
* Created by liman on 2018/8/18.
* QQ:657271181
* e-mail:[email protected]
*
* 需要发起客户端和服务端的远程调用。
*/
public class RemoteInvocationHandler implements InvocationHandler{
private String host;
private int port;
public RemoteInvocationHandler(String host, int port) {
this.host = host;
this.port = port;
}
/**
* 需要跟远程发起调用
* 设置了需要调用的远程方法信息,然后发布到远程
* @param proxy
* @param method
* @param args
* @return
* @throws Throwable
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
RpcRequest request = new RpcRequest();
request.setClassName(method.getDeclaringClass().getName());
request.setMethodName(method.getName());
request.setParameters(args);
TCPTransport tcpTransport = new TCPTransport(this.host,this.port);
return tcpTransport.send(request);
}
}
4、TCPTransport,真正的请求发送
package com.learn.rmi.rpc;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
/**
* Created by liman on 2018/8/18.
* QQ:657271181
* e-mail:[email protected]
* <p>
* 专门管理socket
*/
public class TCPTransport {
private String host;
private int port;
public TCPTransport(String host, int port) {
this.host = host;
this.port = port;
}
//创建一个新的连接
private Socket newSocket() {
System.out.println("创建一个新连接");
Socket socket = null;
try {
socket = new Socket(host, port);
return socket;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("创建连接异常!");
}
}
public Object send(RpcRequest request) {
Socket socket = null;
try {
socket = newSocket();
ObjectOutputStream outputStream = new ObjectOutputStream
(socket.getOutputStream());
outputStream.writeObject(request);
outputStream.flush();
//处理返回的结果
ObjectInputStream inputStream = new ObjectInputStream
(socket.getInputStream());
Object result = inputStream.readObject();
inputStream.close();
outputStream.close();
return result;
} catch (Exception e) {
throw new RuntimeException("发起服务调用异常");
} finally {
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
5、远程目标的接口
package com.learn.rmi.rpc;
/**
* Created by liman on 2018/8/18.
* QQ:657271181
* e-mail:[email protected]
*/
public interface ILearnHello {
public String sayHello(String msg);
}
6、请求对象,客户端主要完成的任务就是封装一个RPCRequest请求对象,然后发送到远程服务器,这不操作在真正的RPC框架中并不是这么实现的,后续需要深入学习这一点。
package com.learn.rmi.rpc;
import java.io.Serializable;
/**
* Created by liman on 2018/8/18.
* QQ:657271181
* e-mail:[email protected]
*
* 传输对象
*
*/
public class RpcRequest implements Serializable{
private static final long serialVersionUID = 903071532920235263L;
private String className;
private String methodName;
private Object[] parameters;
public String getClassName() {
return className;
}
public void setClassName(String className) {
this.className = className;
}
public String getMethodName() {
return methodName;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
public Object[] getParameters() {
return parameters;
}
public void setParameters(Object[] parameters) {
this.parameters = parameters;
}
}
服务端代码
1、业务接口
package com.learn.rmi.rpc;
/**
* Created by liman on 2018/8/18.
* QQ:657271181
* e-mail:[email protected]
*/
public interface ILearnHello {
public String sayHello(String msg);
}
2、业务接口实现类,这个和接口都需要放在与客户端同一个目录名称下
package com.learn.rmi.rpc;
/**
* Created by liman on 2018/8/18.
* QQ:657271181
* e-mail:[email protected]
*/
public class LearnHelloImpl implements ILearnHello{
@Override
public String sayHello(String msg) {
return "Hello "+msg+" this is remote service";
}
}
3、RPCRequest,由于这里是最简陋的实现,需要服务端维护一个RPCRequest对象,同样需要与客户端放在相同目录下
package com.learn.rmi.rpc;
import java.io.Serializable;
/**
* Created by liman on 2018/8/18.
* QQ:657271181
* e-mail:[email protected]
*
* 传输对象
*
*/
public class RpcRequest implements Serializable{
private static final long serialVersionUID = 903071532920235263L;
private String className;
private String methodName;
private Object[] parameters;
public String getClassName() {
return className;
}
public void setClassName(String className) {
this.className = className;
}
public String getMethodName() {
return methodName;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
public Object[] getParameters() {
return parameters;
}
public void setParameters(Object[] parameters) {
this.parameters = parameters;
}
}
4、将服务发布到指定的端口上(即使在指定的端口上处理请求)
package com.learn.rmi.rpc;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created by liman on 2018/8/18.
* QQ:657271181
* e-mail:[email protected]
*
* 负责将服务发布出去
*/
public class RpcServer {
//定义一个线程池
private final ExecutorService executorService = Executors.newCachedThreadPool();
/**
* 将指定的服务发布出去
* @param service 待发布的服务
* @param port 端口
*/
public void publisher(final Object service,int port){
ServerSocket serverSocket = null;
try {
//启动一个服务监听
serverSocket = new ServerSocket(port);
System.out.println("服务开启,等待客户端请求");
//循环监听服务
while(true){
//循环处理客户端请求
Socket socket = serverSocket.accept();
executorService.execute(new ProcessorHandler(socket,service));
}
} catch (IOException e) {
e.printStackTrace();
}finally{
if(serverSocket!=null){
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
5、ProcessHandler类,真正的处理请求代码
package com.learn.rmi.rpc;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.Socket;
/**
* Created by liman on 2018/8/18.
* QQ:657271181
* e-mail:[email protected]
*/
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() {
//处理客户端的请求,这里采用多线程的方式完成
ObjectInputStream objectInputStream = null;
try {
objectInputStream = new ObjectInputStream(socket.getInputStream());
//获取远程传输的对象,通过反序列化获取
RpcRequest request = (RpcRequest) objectInputStream.readObject();
//处理结果
Object result = invoke(request);
//将结果输出到客户端
ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
outputStream.writeObject(result);
//各种流的简单关闭,这里就没有在finally中关闭
outputStream.flush();
objectInputStream.close();
outputStream.close();
} catch (Exception e) {
e.printStackTrace();
}finally{
if(objectInputStream!=null){
try {
objectInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 通过反射获取客户端需要调用的方法,并将结果返回
* @param request
* @return
*/
private Object invoke(RpcRequest request) throws 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();
}
//获取方法信息
Method method = service.getClass().getMethod(request.getMethodName(),types);
return method.invoke(service,args);
}
}
6、服务端测试类
package com.learn.rmi.rpc;
/**
* Created by liman on 2018/8/19.
* QQ:657271181
* e-mail:[email protected]
*/
public class ServerDemo {
public static void main(String[] args) {
ILearnHello iLearnHello = new LearnHelloImpl();
RpcServer rpcServer = new RpcServer();
rpcServer.publisher(iLearnHello,8889);
}
}
运行结果
服务端运行结果
客户端运行结果
说明
上述针对RMI的总结十分简单,后面针对RMI的实现也是十分简单,直接省去了注册中心这一步。后续需要学习深入