javaSec-rmi详解
对于rmi这从很久前就接触到了利用,一直没有好好的梳理过,最近正好看到了好多相关的资料,顺便写下。
前置知识:RPC
理解RPC可以从他的名字开始,RPC的全称是Remote Method Call,顾名思义就是远程方法调用,RPC呢他不是一个框架也不是一个协议,是一个概念性的东西,只是远程通信的一种方式,区别其他远程通信的方式,他是其中的一种,这种概念性的东西,可能从定义说起来很烦,所以我尽量从代码的方式来理解。
从单机到分布式->分布式通信 :就是我原先只有一台主机,当时并不能实现我的需求,这时候我又加了一台,还不够,我用了个局域网的,还不够,这时候直接通过网络来建立各个主机之间的需求,来通过传递0和1这些二进制来建立联系,来通过tcpip来通信。
rpc的迭代过程
demo1.0
User实体类:
import java.io.Serializable;
public class User implements Serializable{
private static final long serialVersionUID = 1L;
private Integer id;
private String name;
public User(Integer id,String name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public String getName() {
return name;
}
public void setId(Integer id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
写的一个传参数的接口:
public interface IUserService {
public User findUserById(Integer id);
}
接口实现:
public class UserServiceImpl implements IUserService {
@Override
public User findUserById(Integer id) {
return new User(id,"wa1ki0g");
}
}
server端:
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
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(7777);
while (running) {
Socket s = ss.accept();
process(s);
s.close();
}
ss.close();
}
public 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);
System.out.println(1);
int id = dis.readInt();
IUserService service = new UserServiceImpl();
User user = service.findUserById(id);
dos.writeInt(user.getId());
dos.writeUTF(user.getName());
dos.flush();
System.out.println(2);
}
}
client端:
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket;
public class Client {
public static void main(String[] args) throws Exception{
Socket s = new Socket("127.0.0.1",7777);
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 = new User(id,name);
System.out.println(user);
dos.close();
s.close();
}
}
上述的client过程就是通过传递过去一个ID,server端传回一个对象属性,client端在根据属性来new这个对象,最终得到想要的对象,这种方式是极不方便实用的,假如我们的user类多了一个属性,那么代码要改变半天,并且在编辑client时的代码时候是极不方便的。注:传输的时候都是以底层二进制来传输的,在代码里也是用了ByteArrayOutputStream与DataOutputStream两个函数来进行二进制的转换与传递,运行结果:
demo2.0
由于demo1里的client端在进行传输使用的时候,需要先建立socket,又有io流之间的转换,这时候是极不方便的,我们可以把建立socket等的过程写成一个代理的方式,来供用户使用,这里只更改下client代码及新建一个Stub代理类,把一些重复的代码写到Stub类里,其他如上:
client端:
public class Client {
public static void main(String[] args) throws Exception{
Stub stub = new Stub();
System.out.println(stub.findUserById(123));
}
}
Stub类:
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket;
public class Stub {
public User findUserById(Integer id) throws Exception {
Socket s = new Socket("127.0.0.1",7777);
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 receivedId = dis.readInt();
String name = dis.readUTF();
User user = new User(receivedId,name);
dos.close();
s.close();
return user;
}
}
client运行,结果一样:
demo3.0
在使用我们的demo2.0的时候会发现,client的调用是极不合理的,Stub里只有findById这个方法,如果我们新加了findByName,findByAge这些新方法呢,我们又得对我们的代码进行改动,这里我们就要使用动态代理的方法,因此下面代码实现的就是可以支持在一个接口里面的随意访法的调用:
stub
import java.io.DataInputStream;
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",7777);
ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream());
String methodName = method.getName();
Class[] parametersTypes = method.getParameterTypes();
oos.writeUTF(methodName);
oos.writeObject(parametersTypes);
oos.writeObject(args);
oos.flush();
DataInputStream dis = new DataInputStream(s.getInputStream());
int receivedId = dis.readInt();
String name = dis.readUTF();
User user = new User(receivedId,name);
oos.close();
s.close();
return user;
}
};
Object o = Proxy.newProxyInstance(IUserService.class.getClassLoader(),new Class[]{IUserService.class},h);
System.out.println(12345);
return (IUserService)o;
}
}
client:
public class Client {
public static void main(String[] args) throws Exception{
IUserService service = Stub.getStub();
System.out.println(service.findUserById(123));
}
}
server:
import java.io.*;
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(7777);
while (running) {
Socket s = ss.accept();
process(s);
s.close();
}
ss.close();
}
public static void process(Socket s) throws Exception {
InputStream in = s.getInputStream();
OutputStream out = s.getOutputStream();
ObjectInputStream oos = new ObjectInputStream(in);
DataOutputStream dos = new DataOutputStream(out);
String methodName = oos.readUTF();
Class[] parameterTypes = (Class[]) oos.readObject();
Object[] args = (Object[]) oos.readObject();
IUserService service = new UserServiceImpl();
Method method = service.getClass().getMethod(methodName,parameterTypes);
User user = (User) method.invoke(service,args);
dos.writeInt(user.getId());
dos.writeUTF(user.getName());
dos.flush();;
}
}
其他的同上
demo4.0
虽然我们已经优化了很多,但是还不够,我们接下来要实现对随意接口中的随意方法的调用:
server:
import java.io.*;
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(7777);
while (running) {
Socket s = ss.accept();
process(s);
s.close();
}
ss.close();
}
public static void process(Socket s) throws Exception {
InputStream in = s.getInputStream();
OutputStream out = s.getOutputStream();
ObjectInputStream ois = new ObjectInputStream(in);
String clazzName = ois.readUTF();
String methodName = ois.readUTF();
Class[] parameterTypes = (Class[]) ois.readObject();
Object[] args = (Object[]) ois.readObject();
Class clazz = null;
//从服务注册表找到具体的实现类
clazz = UserServiceImpl.class;
Method method = clazz.getMethod(methodName,parameterTypes);
Object o = (Object)method.invoke(clazz.newInstance(),args);
ObjectOutputStream oos = new ObjectOutputStream(out);
oos.writeObject(o);
oos.flush();
}
}
client:
public class Client {
public static void main(String[] args) throws Exception{
IUserService service = (IUserService)Stub.getStub(IUserService.class);
System.out.println(service.findUserById(123));
}
}
stub:
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 Object getStub(Class 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",7777);
ObjectOutputStream oos = new ObjectOutputStream(s.getOutputStream());
String className = clazz.getName();
String methodName = method.getName();
Class[] parametersTypes = method.getParameterTypes();
oos.writeUTF(className);
oos.writeUTF(methodName);
oos.writeObject(parametersTypes);
oos.writeObject(args);
oos.flush();
ObjectInputStream ois = new ObjectInputStream(s.getInputStream());
Object o = ois.readObject();
oos.close();
s.close();
return o;
}
};
Object o = Proxy.newProxyInstance(IUserService.class.getClassLoader(),new Class[]{IUserService.class},h);
System.out.println(12345);
return o;
}
}
这样我们就简单的对rpc进行了一个小小的实现,这里面还有大部分可以改进的地方,在我们的代码中我们进行传递参数的方式是使用java中自带的序列化,这种可以说是最土的方法,他只支持java语言并且效率还很低,序列化完了以后的长度还是特别的长,所以仅仅在序列化传输方面就有很多很多可以代替的:
再或者我们可以对协议方面进行更改,在上述代码中我们传输数据的时候用到了最基本的socket编程,也就是通过tcp/ip协议来进行对我们的数据进行传输,当然我们还可以选择其他协议如http,以及rmi等等其他更高级的:
根据上面总结描述的,大家应该可以简单的理解了rmi是个什么东西
RMI 应用概述
RMI 应用程序通常包含两个独立的程序,一个服务器和一个客户端。典型的服务器程序创建一些远程对象,使对这些对象的引用可访问,并等待客户端调用这些对象上的方法。典型的客户端程序获取对服务器上一个或多个远程对象的远程引用,然后调用它们上的方法。RMI 提供了服务器和客户端通信和来回传递信息的机制。这样的应用程序有时被称为分布式对象应用程序。
总结来说,RMI(Remote Method Invocation),是一种跨JVM实现方法调用的技术。
在RMI的通信方式中,由以下三个大部分组成:
- Client
- Registry
- Server
其中Client是客户端,Server是服务端,而Registry是注册中心。
下图描述了一个 RMI 分布式应用程序,它使用 RMI 注册表来获取对远程对象的引用。服务器调用注册表以将名称与远程对象关联(或绑定)。客户端通过远程对象的名称在服务器的注册表中查找远程对象,然后调用它的方法。该图还显示 RMI 系统使用现有的 Web 服务器在需要时为对象加载类定义,从服务器到客户端以及从客户端到服务器。
他的概念的话其实还是比较好理解,就是一个远程的调用,就是一台机器想要执行另外一台机器上的java代码,我们看上面的图,我们的client是怎么来去调用这个服务端的呢,实际上就是服务端通过绑定这个远程对象,客户端通过客户端的代理去访问服务端的代理,最后再由服务端的代理来进行操作,但是这里有一个小问题,就是客户端怎么知道你这个服务端这个对象开到了哪里,因为socket通信基于端口的么,不可能每个对象都对应固定的端口吧,所以为了解决这个问题,rmi引入了注册中心,就是说你服务端绑定的这个远程对象不是开了个端口么,然后它绑定以后去告诉注册中心,这样我们的客户端通过查询注册中心,就知道了地址,在返回去找。
客户端的代理是由服务端生成放到注册中心的,再由客户端去拿的,服务端的代理是由注册中心反向生成的。
一句话来说就是:
客户端会Registry取得服务端注册的服务,从而调用服务端的远程方法。
实际上rmi还提供了一个大胆的想法,就是说当客户端去调用的时候,服务端没有我想调的这个对象,就是说允许服务端去远程的web server去加载一个类下来,可以类比UrlClassLoader,这是很危险的,这种机制在jdk8中以后就直接去掉了。
一个简单的demo:
这里为了看着方便写了两个项目,分别是client端和server端,他们都要定义一个相同的接口,这个接口都要继承这个remote,并且服务端有一个实现类,来实现接口里定义的方法。在接口中写的是一个sayHello方法,就是将小写转换成大写的。
client:
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",9999);
IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");
remoteObj.sayHello("walking");
}
}
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface IRemoteObj extends Remote {
// sayHello是客户端要调用的方法,要抛RemoteException异常
public String sayHello(String keywords) throws RemoteException;
}
server:
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
IRemoteObj remoteObj= new IRemoteObjImpl();
//这一步其实已经可以进行通信了,就是没有地址而已
Registry r= LocateRegistry.createRegistry(9999);
//把地址添加到注册中心
r.bind("remoteObj",remoteObj);
//绑定到注册中心
}
}
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface IRemoteObj extends Remote {
// sayHello是客户端要调用的方法,要抛RemoteException异常
public String sayHello(String keywords) throws RemoteException;
}
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class IRemoteObjImpl extends UnicastRemoteObject implements IRemoteObj {
public IRemoteObjImpl() throws RemoteException {
}
@Override
public String sayHello(String keywords) {
String upKeywords = keywords.toUpperCase();
System.out.println(upKeywords);
return upKeywords;
}
}
运行结果:
server端开始监听,client端运行:
server端成功执行:
rmi的流程看完了,那么我们说下这之间有什么安全问题,首先就是低版本中我们说的那个类加载问题,其次我们考虑server,client通信的问题,这实际是通过java的序列化与反序列化来实现的。
rmi调试
我们先来看从先知找的一个图:
可以看到在不算创建方面,在通信方面一共有六个,客户端->服务端。服务端->客户端,服务端->注册中心,注册中心->服务端,客户端->注册中心,
注册中心->客户端,我们接下来先分析服务创建的几个过程(远程对象创建,注册中心创建及绑定)其次再分析他们的通信
server端创建远程对象的流程
我们先来看下server端创建远程对象的流程:
跟进去,首先进到了远程对象的构造函数,我们有了这个对象之后呢,其实最后呢目的就是要把我们的这个远程对象发布到网络上,我们要调试的也是这个过程:
到他父类的构造函数里:
我们port传的是0,也就是默认值,那么他这部分他代表着他会将你的远程对象发布到随机的端口上,这里说的是远程对象的端口,并不是注册中心的那个默认端口,现在调试的是远程对象的创建流程,不要弄混了:
在一些静态赋值后会调用上图的exportObject函数,我们跟进去看下:
exportObject函数,是我们这个过程核心的一个函数,我们的实现类是一定要继承这个函数的,当然不继承也可以,那么必须在我们实现类的构造函数里要手动调用下这个静态函数,拿我们上述的demo举例:
ok,说完题外话接着往下看,看下exportObject函数中的两个参数,第一个obj就是我们的实现类对象,第二个参数实际上就可以猜到他是处理网络请求用的,仅仅传了一个端口进去,ip是自动获取的,我们跟下看下他真正处理网络请求的部分:
我们先记住 new的LiveRef这个类,这也是一个关键的点,我们跟到 LiveRef里面去看看:
objid,很明显就是与id相关的,暂时先不管了,我们看下this也就是他这个构造函数,跟进去:
可以看到第一个参数就是个objid,第二个参数里的函数应该是个处理网络请求的一个函数,看下这个函数,可以看见他的返回值是TCPEndpoint,一定是处理网络的一个模块:
看下TCPEndpoint这个类:
可以大致的猜到这应该是一个封装处理网络的一个模块,接着我们回来我们在进到LiveRef的this构造函数:
可以看到这有一个TcpTransport的这样一个东西,他才是真正处理网络了一个东西,可以说他是又封装的一层,一路上都是封装过来了,到了这里才发现真正有意义的东西,所以我们先去记住这个LiveRef,我们接着往下走,调用了他父类的构造函数:
就是个简单的赋值操作,将我们之前创造出的LiveRef赋值给UnicastRef的,这里的UnicastRef与UnicastServerRef其实就是一个对应着客户端,一个对应着服务端,这里我们要记住,从始至终我们只创建了一个LiveRet,剩下的一些操作只是赋值并不会创造新的,这样就不会很乱,接着往下走到这,我们出来了:
再进到exportObject里面,可以看到参数第二个参数是我们刚刚创建的那个UnicastServerRef,看一下他的参数可以发现,他包含了我们的LiveRef:
上图的判断是恒过的,然后又是一个赋值,主要就是LiveRef的赋值,下一步接着导出,我们跟进去:
可以发现,是进行了一个创建代理的操作,这个创建的var5就是上图中我们的客户端操作的那个stub代理,也就是会进行网络请求的那个东西,可能会想这个客户端的东西怎么会在服务端创建呢?因为他的流程是先在服务端创建好了在把它放到注册中心上去,然后客户端去注册中心上拿,然后在通过这个拿到的stub进行操作服务端的代理,服务端的代理就是Remote Skeleton,最后再通过服务端的代理来操作这个远程对象。
看一下他是怎么创建的呢,跟进去createProxy看一下:
看一下他的几个参数,第一个参数就是他的远程对象的这个类,就是我们的那个实现类,第二个参数就是封装起来了一个LiveRef,核心主要的就是LiveRef,接着往下走,走到判断这:
我们看下这个exists函数:
我们这里肯定是不会返回true的,这里应该是判断调用的是不是系统内置的几个,像jdk自带的那些,比如下图圈红框框这样的:
OK,这个if判断是进不去的,那我们接着往下走:
可以发现,接下来的步骤是一个创建动态代理的一个标准的流程,看他的hander参数就是var6可以发现,他的handler参数还是一个封装的一个LiveRef:
接着往下走,可以发现他的动态代理创建好了已经,然后下面有一个if这个if判断是不是系统自带的,就是和我们前面的那个jdk里带着的几个是对应的,这里条件是不满足的:
我们接着往下走,进到target那:
这个target就是起到了一个封装的作用,就是目前我们不是创建了一大堆的东西么,这个new target就是创建了一个总封装,把目前有用的都放到这个new出来的对象里面,可以进去看下:
我们往下走赋值完后看下:
我们看到上图中的客户端和服务端两个分别对应的LiveRef是相同的,这是因为我们客户端服务端要通信,所以用的是同一个东西,同一个LiveRef,我们走完这个Target函数,接着往下走:
我们再跟进这个exportObject发布函数,一直跟到这个核心代码这里:
发现调用了listen函数,listen函数凭常识应该可以猜到,是处理网络相关的,比如监听端口,我们在跟进去看看:
我们走到调用newServerSocket()函数处,跟进去看下再,看这:
那里就是当我们传的port参数是默认的0时,我们就会随机找一个端口进行监听,我们步出这个函数,看属性可以发现此时服务端口已被创建:
我们接着往下走,下一步创建了一个新的线程:
这个线程呢是用来处理当我们监听的端口被连接时应该做什么,处理逻辑在AcceptLoop函数里,就是进行一些网络的操作,这里就应该明白网络请求的线程,和真正的代码逻辑的线程是都是独立的,到目前为止我们的服务端就应该把远程对象发布出去了已经,发布在了一个随机的端口上面:
这个时候呢客户端是默认不知道发布到了哪里的,之后呢服务端还要做什么呢,他要记录一下,我们走出listen函数,再接着往下走:
一直走到exportObject函数,再跟进去,我们主要看下这putTarget()函数,跟进去,主要是这两行:
看下objTable和impTable:
其实就是系统创建的两个静态表,为了方便找寻,用来储存我们最终创建完成的target信息,我们的这个流程差不多就完事了,接下来就是监听在这等待连接,省略了一些繁琐无用的过程,我们的整个创建过程就完成了:
注册中心的创建
打个断点跟进去:
调用一个创建注册中心的静态方法,然后会传入一个端口参数,并new一个对象,出来:
我们跟进去看下有用的部分:
上面这里应该就是个安全检查的部分,接着往下走下看看:
new了一个LiveRef,看到这里应该有一点熟悉的感觉了,可以发现和我们上一部分的代码有点类似,其实这两部分从本质上来说是差不多的,都是在一个端口上开一个服务。所以好多步骤都是与上部分重复的。所以直接跟setup:
可以看到我们的var2属性与上部分还是雷同的,是一个新建的liveRef,包含网络的一些信息,包括我们创建UnicastServerRef对象都是差不多的代码,我们跟进去setup进去瞅瞅这里有不同的:
可以看到都比较像,他这也是调了UnicastServerRef对象的exportObject方法,但是值这里是不同的上一部分的三个参数分别是创建的远程对象,null,flase,但是呢与当前对比会发现,第三个参数不同,跟进去看看他是什么:
这个var3参数应该是判断是否jdk自带的,就是判断这个对象是永久的还是短期的,向我们之前自定义的那个远程对象是临时的一个对象,而这个是写在jdk里面的。我们仔细看看这段代码,会发现这完全就是之前创建代理的那段的代码,我们进到createProxy函数去看下:
可以看到有一个判断这里也是与上部分完全相同的,但是我们上部分讲了一个区别:是否是由jdk提供的,由于上部分我们使用的是我们自己创建的远程对象,所以并没有通过,但是在这里用的jdk自带的是可以通过的:
我们返回出去:
进到这个createStub方法:
这就是做了一个创建的操作,参数就放了ref,接着就走了出去,我们创建完看下这个stub参数:
我们的上部分的动态代理,当时里面也是放了一个ref,所以实际上基本就是一样的,接下来往下走:
这段if判断的是如果他是服务端定义好的就去调用他下面的那个方法,跟进去看下:
这里又调用了setSkeleton方法,这个Skeleton是什么呢,他指的是服务端的那个代理,就是说他会反向生成一个服务端代理,接着继续往下走:
上图和上部分一样也是进行生成封装,这大概就是注册中心的创建流程,所以只要知道大概的流程还是很容易去理解的,就是服务端生成stub放到注册中心,客户端去注册中心拿再利用这个stub进行通信,注册中心又会给服务端反向生成一个Skeleton,服务端再利用这个Skeleton进行应答通信。
绑定
打个断点:
跟进去瞅瞅:
看上图的bindings世界上就是可以理解为一个hash表,下面的那个if是判断被绑没绑定过,如果绑定过了,就会抛出个异常,如果没有就将他put进去,就是记录在这个表内。
未完待续。。。