从王者荣耀看设计模式(远程代理模式)
一.简介
每位王者荣耀玩家都有一个属于自己的游戏账号。为了提升游戏等级或者增加游戏体验感,会存在多个游戏玩家同时共享一个游戏账号的情况。当一位玩家使用账号正在游戏中时,另一玩家登陆同一账号会导致前一玩家强制退出登陆!(ಥ_ಥ)
模式动机
在本实例中,通过远程代理,我们可以实现远程控制。当我处于在线状态时,使用代理让同一账号的使用者下线
二.远程代理(帮助我们控制访问远程对象)
远程代理可以作为另一个JVM上对象的本地代表。调用代理的方法,会被代理利用网络转发到远程执行,并且结果会通过网络返回给代理,再由代理将结果返回给客户。
代理,就是代表某一真实对象,假装它是真正的对象。但是其实一切的动作是代理对象利用网络和真正的对象沟通o(´^`)o 代理之所以需要控制访问,是因为我们的客户不知道如何和远程对象沟通。从某些方面来看,远程代理控制访问,可以帮助我们处理网络上的细节
变量只能引用和当前代码语句在同一堆空间中的对象,Java有内置远程调用的功能可以帮助我们实现远程代理
三.结构图
四.设计类图
五.代码实现
AccountStatus类(远程接口类)
package com.practice.MyRemote;
import java.rmi.Remote;
import java.rmi.RemoteException;
/*
* 客户端和服务端统一的接口,只需要服务端根据这个接口实现相应的功能,然后注册上去,
* 客户端就可以根据这个接口来使用相应的功能
*
* 定义一个远程接口,必须继承Remote接口,其中需要远程调用的方法必须抛出RemoteException异常
*/
public interface AccountStatus extends Remote {
public void OnlineStatus() throws RemoteException;//玩家在线状态
public void OfflineStatus() throws RemoteException;//玩家下线状态
}
AccountStatusHelper(接口实现类<代理>)
package com.practice.MyRemote.Imple;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import com.practice.MyRemote.AccountStatus;
public class AccountStatusHelper extends UnicastRemoteObject implements AccountStatus {
private static final long serialVersionUID = 1L;
@SuppressWarnings("unused")
private boolean Online = false;
public AccountStatusHelper(boolean _Online) throws RemoteException {
this.Online = _Online;
}
@Override
public void OnlineStatus() throws RemoteException {
System.out.println("玩家1正使用账号游戏中……");
}
@Override
public void OfflineStatus() throws RemoteException {
System.out.println("玩家1退出王者荣耀账号!");
}
}
Server(服务类)
package com.practice.MyRemote.Server;
import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import com.practice.MyRemote.AccountStatus;
import com.practice.MyRemote.Imple.AccountStatusHelper;
/**
* 创建RMI注册表,启动RMI服务,并将远程对象注册到RMI注册表中。
*/
public class Server {
private static final String HOST = "localhost";
private static final int PORT = 9090;
public static void main(String [] args) {
try {
//创建对象, 准备将这个对象作为远程对象注册
AccountStatus accountStatusHelper = new AccountStatusHelper(true);
LocateRegistry.createRegistry(PORT);
//绑定的URL标准格式为:rmi://host:port/name(其中协议名可以省略,下面两种写法都是正确的)
Naming.bind("rmi://" + HOST + ":" + PORT + "/AccountStatusHelper",accountStatusHelper);
System.out.println("---->远程对象绑定成功!");
}catch(RemoteException e) {
System.out.println("创建远程对象发生异常!");
e.printStackTrace();
}catch(AlreadyBoundException e) {
System.out.println("发生重复绑定对象异常!");
e.printStackTrace();
}catch(MalformedURLException e) {
System.out.println("发生URL畸形异常!");
e.printStackTrace();
}
}
}
Client类(客户类)
package com.practice.Client;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import com.practice.MyRemote.AccountStatus;
/*
* 用于调用远程服务
*
* 客户端测试,在客户端调用远程对象上的远程方法,并返回结果
*/
public class Client {
public static int flag = 0;
public static void main(String[] args){
new Thread(new Runnable() {
public void run() {
try {
AccountStatus accountStatus;
accountStatus = (AccountStatus)Naming.lookup("rmi://127.0.0.1:9090/AccountStatusHelper");
accountStatus.OnlineStatus();
Thread.sleep(1000);
if(flag == 1) {
accountStatus.OfflineStatus();
}
} catch (NotBoundException | MalformedURLException | RemoteException | InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}).start();
System.out.println("玩家2通过QQ登陆王者荣耀");
flag = 1;
}
}
六.运行结果
⑴先运行Server类
运行结果:
⑵运行Client类
JAVA RMI(Remote Method Invoke)
RMI提供了客户辅助对象(Stub)和服务器辅助对象(Skeleton),为客户辅助对象创建和服务对象相同的方法。RMI的好处在于你不必亲自写任何网络或I/O的代码。客户程序调用远程方法(即真正的服务所在)就和运行在客户自己的本地JVM上对对象进行正常方法调用一样。RMI提供了所有运行时的基础设施
RMI将客户辅助对象称为stub(桩),服务辅助对象称为skeleton(骨架)
RMI方法调用是如何发生的
①客户对象调用客户辅助对象的doSomeThing()方法。
②客户辅助对象打包调用信息(变量、方法名等),然后通过网络将它运给服务辅助对象
③服务辅助对象把来自客户辅助对象的信息解包,找出被调用的方法(以及在哪个对象内),然后调用真正的服务对象上的真正方法
④服务对象上的方法被调用,将结果返回给服务辅助对象
⑤服务辅助对象把调用的返回信息打包,然后通过网络运回给客户辅助对象
⑥服务辅助对象把返回值解包,返回给客户对象,对于客户对象来说,这是完全透明的
制作远程服务
第一步:制作远程接口
远程接口定义出可以让客户远程调用的方法。客户将用它作为服务的类类型。Stub和实际的服务都实现此接口。
⑴ 扩展java.rmi.Remote。Remote是一个"记号"接口,所以Remote不具有方法,对于RMI来说,Remote接口具有特别的意义,所以我们必须遵守规则
import java.rmi.Remote;
import java.RemoteException;
public interface MyRemote extends Remote{
……
}
⑵声明所有的方法都会抛出RemoteException
客户使用远程接口调用服务。换句话说,客户会调用实现远程接口的Stub上的方法,而Stub底层用到了网络和I/O,所以所有坏事情都有可能发生。客户必须认识到此风险,通过处理或声明远程异常来解决。如果接口的方法声明了异常,任何在接口类型的引用上调用方法的代码也必须处理或声明异常。
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface MyRemote extends Remote{
public String sayHello() throws RemoteException;
}
⑶确定变量和返回值是否属于原语(primitive)类型或者可序列化(Serializable)类型
远程方法的变量和返回值,必须属于源于类型或Serializable类型。远程方法的变量必须被打包并通过网络运送,这要靠序列化实现。如果使用原语类型、字符串和许多API中内定的类型(包括数组和集合),都不会有问题。如果传送的是定义的类,就必须保证类实现了Serializable接口
第二步:制作远程实现
这是做实际工作的类,为远程接口中的定义的远程方法提供真正的实现
⑴扩展UnicastRemoteObject
为了要成为远程对象,对象需要某些"远程的"功能。最简单的方式就是扩展java.rmi.server.UnicastRemoteObject,让超类完成基本工作
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {
……
}
⑵实现客户想要调用的接口的方法
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {
private static final long serialVersionUID = 1L;
/*
/*UnicastRemoteObject的构造器会抛出RemoteException.当类被实例化时
/*候,如果超类的构造器抛出异常,那么只能声明子类的构造器也抛出异常
*/
protected MyRemoteImpl() throws RemoteException {}
public String sayHello() throws RemoteException {
return "server says:'Hello'";
}
}
⑶用RMI Registry注册此服务
目前,我们已经有了MyRemoteImpl这个远程服务,必须让它可以被远程客户调用。我们要做的就是将此服务实例化,然后放进RMI Registry中(要先确定RMI Registry正在运行,否则注册失败)。当注册这个对象时,RMI系统其实注册的是Stub,因为这是客户真正需要的。注册服务使用了java.rmi.Naming类的静态bind方法
package com.practice.SayHello;
import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
/**
* 创建RMI注册表,启动RMI服务,并将远程对象注册到RMI注册表中。
*/
public class Server {
private static final String HOST = "localhost";
private static final int PORT = 9090;
public static void main(String args[]) {
try {
//创建2个对象, 准备将这个两个对象作为远程对象注册
MyRemote MyRemoteHelper = new MyRemoteImpl();
LocateRegistry.createRegistry(PORT);
//绑定的URL标准格式为:rmi://host:port/name(其中协议名可以省略,下面两种写法都是正确的)
Naming.bind("rmi://" + HOST + ":" + PORT + "/MyRemoteImpl", MyRemoteHelper);
System.out.println("---->远程对象绑定成功!");
} catch (RemoteException e) {
System.out.println("创建远程对象发生异常!");
e.printStackTrace();
} catch (AlreadyBoundException e) {
System.out.println("发生重复绑定对象异常!");
e.printStackTrace();
} catch (MalformedURLException e) {
System.out.println("发生URL畸形异常!");
e.printStackTrace();
}
}
}
⑷运行Main程序
main方法首先会实例化一个服务对象,然后到RMI registry中注册,注册后,这个服务就可以供客户调用了。
第三步.客户端程序
客户取得stub对象(我们的代理)以调用其中的方法。所以我们需要RMI Registry的帮
忙。客户从Registry中寻找(lookup)代理,就像根据名字在电话簿里寻找一样
⑴客户到RMI Register中查找
Naming.lookup("rmi://127.0.0.1/RemoteHelper");//RemoteHelper是在服务端绑定时用的名称
⑵RMI Register返回Stub对象
作为lookup方法的返回值,RMI会自动对stub反序列化(在客户端必须有stub类)
MyRemote accountStatus=(MyRemote)Naming.lookup("rmi:/127.0.0.1:9090/MyRemoteImpl");
⑶客户调用stub的方法,就像stub就是真正的服务对象一样
完整的客户端代码:
import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
/**
* 创建RMI注册表,启动RMI服务,并将远程对象注册到RMI注册表中。
*/
public class Server {
private static final String HOST = "localhost";
private static final int PORT = 9090;
public static void main(String args[]) {
try {
//创建2个对象, 准备将这个两个对象作为远程对象注册
MyRemote MyRemoteHelper = new MyRemoteImpl();
LocateRegistry.createRegistry(PORT);
//绑定的URL标准格式为:rmi://host:port/name(其中协议名可以省略,下面两种写法都是正确的)
Naming.bind("rmi://" + HOST + ":" + PORT + "/MyRemoteImpl", MyRemoteHelper);
System.out.println("---->远程对象绑定成功!");
} catch (RemoteException e) {
System.out.println("创建远程对象发生异常!");
e.printStackTrace();
} catch (AlreadyBoundException e) {
System.out.println("发生重复绑定对象异常!");
e.printStackTrace();
} catch (MalformedURLException e) {
System.out.println("发生URL畸形异常!");
e.printStackTrace();
}
}
}
**⑷运行Main方法