JAVA EE十三大规范(1)RMI(全网最深入浅出)

简介: 1.概述RPC:RPC(Remote Procedure Call),一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议,RPC可以用HTTP协议实现,并且用HTTP是建立在 TCP 之上最广泛使用的 RPC,但是互联网公司往往用自己的私有协议,比如鹅厂的JCE协议,私有协议不具备通用性但是相比于HTTP协议,RPC采用二进制字节码传输,更加高效也更加安全。用一个比较形象的例子来形容,你老婆出去打麻将,然后想起家里洗衣机里的衣服还没洗,于是打电话给在家里的你,叫你把洗衣机里的衣服洗了,这就是远程过程调用。微服务中服务的调用底层就是使用的RPC机制。

1.概述

RPC:

RPC(Remote Procedure Call),一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议,RPC可以用HTTP协议实现,并且用HTTP是建立在 TCP 之上最广泛使用的 RPC,但是互联网公司往往用自己的私有协议,比如鹅厂的JCE协议,私有协议不具备通用性但是相比于HTTP协议,RPC采用二进制字节码传输,更加高效也更加安全。


用一个比较形象的例子来形容,你老婆出去打麻将,然后想起家里洗衣机里的衣服还没洗,于是打电话给在家里的你,叫你把洗衣机里的衣服洗了,这就是远程过程调用。微服务中服务的调用底层就是使用的RPC机制。

RMI:

RMI(Remote Method Invocation),在JDK1.2中推出,RPC的Java版本,官方的说法是RMI 支持存储于不同地址空间的程序级对象之间彼此进行通信,实现远程对象之间的无缝远程调用。直白的说法RMI其实就是支持一个JVM去调用另一个JVM中的对象中的方法。其底层的其实就是靠socket以及序列化和反序列化支撑起来的,使用分布式垃圾收集器(DGC)进行GC。


RMI是一个分布式的架构,由三部分组成:


客户端


远程对象的调用者


服务器


定义、发布远程对象


注册中心


JDK提供的一个可以独立运行的程序,在bin目录下,其运行在服务器端的一个固定端口上。

2.详述

2.1.流程分析

2.1.1.手写实现

为了方便理解RMI的整个流程,我们首先基于网络通信和序列化、反序列化来手写一个远程方法调用的demo:


核心为两个代理:


Skeleton


骨架,服务器端的远程对象的代理,封装远程对象及网络通信能力。


stub


存根,客户端的远程对象的代理,搭建一个远程对象的骨架,具体的方法调用通过网络通信来访问远程对象的对应方法。


接口:

public interface Person {
    int getAge() throws Throwable;
    String getName() throws Throwable;
}

服务端:

//实现类
public class PersonServer implements Person{
    private int age;
    private String name;
    public PersonServer(String name,int age){
        this.age=age;
        this.name=name;
    }
    public int getAge() throws Throwable {
        return age;
    }
    public String getName() throws Throwable {
        return name;
    }
}
//骨架
public class Person_Skeleton extends Thread{
    private PersonServer myServer;
    public Person_Skeleton(PersonServer server) {
        // get reference of object server
        this.myServer = server;
    }
    public void run() {
        try {
            // new socket at port 9000
            ServerSocket serverSocket = new ServerSocket(9000);
            // accept stub's request
            Socket socket = serverSocket.accept();
            while (socket != null) {
                // get stub's request
                ObjectInputStream inStream =
                        new ObjectInputStream(socket.getInputStream());
                String method = (String)inStream.readObject();
                // check method name
                if (method.equals("age")) {
                    // execute object server's business method
                    int age = myServer.getAge();
                    ObjectOutputStream outStream =
                            new ObjectOutputStream(socket.getOutputStream());
                    // return result to stub
                    outStream.writeInt(age);
                    outStream.flush();
                }
                if(method.equals("name")) {
                    // execute object server's business method
                    String name = myServer.getName();
                    ObjectOutputStream outStream =
                            new ObjectOutputStream(socket.getOutputStream());
                    // return result to stub
                    outStream.writeObject(name);
                    outStream.flush();
                }
            }
        } catch(Throwable t) {
            t.printStackTrace();
            System.exit(0);
        }
    }
}

客户端:

//存根
public class Person_Stub implements Person{
    private Socket socket;
    public Person_Stub() throws Throwable {
        // connect to skeleton
        socket = new Socket("127.0.0.1", 9000);
    }
    public int getAge() throws Throwable {
        // pass method name to skeleton
        ObjectOutputStream outStream =
                new ObjectOutputStream(socket.getOutputStream());
        outStream.writeObject("age");
        outStream.flush();
        ObjectInputStream inStream =
                new ObjectInputStream(socket.getInputStream());
        return inStream.readInt();
    }
    public String getName() throws Throwable {
        // pass method name to skeleton
        ObjectOutputStream outStream =
                new ObjectOutputStream(socket.getOutputStream());
        outStream.writeObject("name");
        outStream.flush();
        ObjectInputStream inStream =
                new ObjectInputStream(socket.getInputStream());
        return (String)inStream.readObject();
    }
}

2.1.2.JDK实现

结合前文的手写实现来看JDK给出的实现整个流程一目了然:

服务器端生产远程对象和骨架代理,将远程对象注册到注册中心中,客户端找注册中心拿远程对象的时候会获取到远程对象的存根代理,通过存根代理和骨架代理之间的网络通信就实现了远程方法调用

9d82a19839d145ea922924a464ffffbb.png

e3efe41d801544419fecf02264e545c1.png

代码示例:

1.服务器

//继承远程调用接口,定义方法模板
public interface IRemoteObj extends Remote {
    String sayHello(String keyWords) throws RemoteException;
}
//实现业务方法
public class RemoteObjImpl extends UnicastRemoteObject implements  IRemoteObj {
    protected RemoteObjImpl() throws RemoteException {
        //如果不继承UnicastRemoteObject就需要手动导出
        //UnicastRemoteObject.exportObject(this,0);
    }
    public String sayHello(String keyWords) throws RemoteException {
        System.out.println(keyWords);
        return "hello "+keyWords;
    }
}
//启动入口
public class RMIServer {
    public static void main(String[] args) throws Exception{
        IRemoteObj remoteObj=new RemoteObjImpl();
        //创建注册中心
        Registry registry= LocateRegistry.createRegistry(1099);
        //向注册中心中注册对象
        registry.bind("remoteObj",remoteObj);
    }
}

2.客户端

public interface IRemoteObj extends Remote {
    String sayHello(String keyWords) throws RemoteException;
}
public class RMIClient {
    public static void main(String[] args) throws Exception {
        //连接注册中心
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
        //调用对象
        IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");
        remoteObj.sayHello("world!");
    }
}

2.2.安全漏洞

2.2.1.漏洞成因

由于RMI底层使用了序列化和反序列化,因此存在序列化相关的漏洞。

Serializable接口其实隐式的提供了两个方法writeObject、readObject用来自定义序列化时的读写动作,这两个方法在重写快捷键中是看不到的,但是只要定义了这两个方法就会生效。


反序列化由于需要从外部去加载类,这样给一些恶意代码的注入提供了机会,以下为一个反序列化注入攻击的例子:

public class MyObject implements Serializable {
    private static final long serialVersionUID = -6554051283690579548L;
    public String name;
    //重写readObject()方法
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
        //执行默认的readObject()方法
        in.defaultReadObject();
        //执行指定程序
        Runtime.getRuntime().exec("calc.exe");
    }
}
public class test {
    public static void main(String[] args) throws Exception {
        //定义myObj对象
        MyObject myObj = new MyObject();
        myObj.name = "hello world";
        //创建一个包含对象进行反序列化信息的”object”数据文件
        FileOutputStream fos = new FileOutputStream("object");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        //writeObject()方法将myObj对象写入object文件
        os.writeObject(myObj);
        os.close();
        //从文件中反序列化obj对象
        FileInputStream fis = new FileInputStream("object");
        ObjectInputStream ois = new ObjectInputStream(fis);
        //恢复对象
        MyObject objectFromDisk = (MyObject)ois.readObject();
        System.out.println(objectFromDisk.name);
        ois.close();
    }
}

2.2.2.防御方法

  • 认知和签名
  • 禁止JVM执行外部命令Runtime.exec
  • RASP监测

序列化、反序列化漏洞攻击与防御是一个很大的话题,历年来JAVA开源社区中爆出的各类组件的安全漏洞中大多数与其相关,此处暂不展开做详细论述,后面会写专门的文章来讨论相关内容。

目录
相关文章
|
2月前
|
缓存 算法 Java
【Java引用规范】强软引用
本文详细介绍了Java中引用的概念和作用,包括强引用、软引用、弱引用和虚引用,并探讨了不同引用类型在内存管理和垃圾回收中的特性与用途。强引用是最常见的引用类型,对象只要被引用就不会被垃圾回收;软引用适用于内存敏感的缓存场景,在内存不足时会被回收;弱引用在更早的垃圾回收阶段被清除;虚引用主要用于对象的finalize过程。文章通过示例代码和内存分析工具展示了软引用的具体应用和回收机制。
【Java引用规范】强软引用
|
1月前
|
Java 编译器 Android开发
java作业的提交规范与要求
java作业的提交规范与要求
24 0
|
2月前
|
Java API 开发者
Java 注释规范
Java中的注释规范包括单行注释(`//`)、多行注释(`/* ... */`)和文档注释(`/** ... */`)。单行注释适用于简短说明,多行注释用于较长描述,文档注释则专为自动生成API文档设计。注释应清晰明了、及时更新,避免冗余,并详细说明参数和返回值。遵循这些规范有助于提高代码的可读性和可维护性。
|
3月前
|
Java
Java应用结构规范问题之在UnitConvertUtils工具类将千米转换为米的问题如何解决
Java应用结构规范问题之在UnitConvertUtils工具类将千米转换为米的问题如何解决
|
3月前
|
Java 应用服务中间件 HSF
Java应用结构规范问题之配置Logback以仅记录错误级别的日志到一个滚动文件中的问题如何解决
Java应用结构规范问题之配置Logback以仅记录错误级别的日志到一个滚动文件中的问题如何解决
|
3月前
|
Java 应用服务中间件 HSF
Java应用结构规范问题之配置Logback以在控制台输出日志的问题如何解决
Java应用结构规范问题之配置Logback以在控制台输出日志的问题如何解决
|
3月前
|
Java 开发者
Java 编程风格与规范:跟上时代热点,打造高质量代码,为开发者梦想保驾护航
【8月更文挑战第30天】本文强调了Java编程中代码质量和可维护性的重要性,详细介绍了命名规范、代码格式和注释的最佳实践,如使用描述性的命名、适当的缩进及空行,以及关键代码部分的注释说明,同时还提供了避免魔法值和减少代码重复的建议与示例,帮助提升团队协作效率和项目长期发展。
66 2
|
3月前
|
Java
编写规范JAVA代码
本文档制定了Java编程规范,旨在确保系统源程序的可读性和可维护性,适用于所有Java开发、测试及维护过程。规范包括命名规则(如Package、Class及其成员等)与样式规定,强调统一风格以提高协作效率,并列举了具体示例与注意事项,如避免单字符变量名及使用有意义的反义词组命名等。
58 1
|
3月前
|
Java 应用服务中间件 HSF
Java应用结构规范问题之AllLoggers接口获取异常日志的Logger实例的问题如何解决
Java应用结构规范问题之AllLoggers接口获取异常日志的Logger实例的问题如何解决
|
3月前
|
Java 应用服务中间件 HSF
Java应用结构规范问题之dal层中的mapper数据源类型进行组织的问题如何解决
Java应用结构规范问题之dal层中的mapper数据源类型进行组织的问题如何解决