Java消息机制
在长期的Java客户端开发过程中,一个常用的机制就是消息传送。无论是同步消息传送还是异步消息传送,应该说是建立在Observer设计模式基础上的。在Java中提供了基于这种模式的Observable/Observer事件框架,分别由java.util.Observable类和java.util.Observer接口组成,其中,Observer是观察者角色,Observable是被观察目标(subject)角色。
我们先简单的看一下这两个类(接口):Observable是一个封装了基本功能的类,比如注册observer(attach功能),注销observer(detatch功能)等。我们一般只需从Observalbe派生我们自己的观察者。应该注意的是,Observable必须是“有变化”才触发通知observer这一任务。即如果我们不主动设置changed属性为true,将不会有任何变化,也就是说不会有“通知”。因此,设置changed属性的值是我们应用jdk observer 设计模式的关键所在。Observable提供了setChange()来设置changed属性,符合了“只有observalbe才能直接或间接通知observer”(observable设计模式的)要求。
当然我们的实现中也不一定完全按Observer设计模式来做,也许我们通常会
1. 定义封装的消息类,作为消息数据的承载体,
2. 定义监听器,其中定义消息处理方法。
3. 定义消息发送类,增加注册和通知发送实现
当调用者实现监听器,并注册到消息发送类中,就可接收到消息了。这也就是Java的事件发送机制。示例如下:
public class MyObservable extends Observable {
private String data;
public void changeValue(String fValue) {
data = fValue;
setChanged();
}
}
public class ObserverTest {
public static void main(String[] args) {
MyObservable myOservable = new MyObservable();
myOservable.addObserver(new Observer() {
//注册匿名内部类Observer,当数据改变时将通知该类的update方法
public void update(Observable o, Object arg) {
System.out.println("This value has been changed to " + (String) arg);
}
});
String sValue = "Hello Msg";
myOservable.changeValue(sValue);
myOservable.notifyObservers(sValue + "!");//数据的改变由observable主动通知给Observer。
}
}
作为CS结构的消息发送而言,我们还需要再扩充Java的事件发送机制,以实现C和S间的消息传送。当然C和S之间的消息传送还需要一些基础设施。这里我们利用Java RMI结合回调机制来完成C和S之间的消息传送。我们知道一般RPC间的通讯是同步的,所以我们还可结合采用线程来实现异步调用。
简单介绍一下RMI,RMI supports two classes that implement the same interface. The first class is the implementation of the behavior, and it runs on the server. The second class acts as a proxy for the remote service and it runs on the client.
The Transport Layer makes the connection between JVMs. All connections are stream-based network connections that use TCP/IP.
On the client side, the RMI Registry is accessed through the static class Naming. It provides the method lookup() that a client uses to query a registry. The method lookup() accepts a URL that specifies the server host name and the name of the desired service. The method returns a remote reference to the service object. The URL takes the form:
rmi://<host_name> [:<name_service_port>]/<service_name>
再来看一下回调机制,回调实现了被调用一端在接口被调用时也会调用对端的接口。即客户端C调用服务端S中的某一函数fo,然后S又在某个时候反过来再调用C中的函数fn,对于C来说,这个fn就叫做回调函数。在CS间消息传送过程中,C端使用回调用实现注册到服务端。当服务端消息到达时,就会主动通知客户端接收消息。在CS结构中,按我们以上描述消息传送就需要经过RMI来实现,一个示例如下(本例参考了网上代码以说明消息传送的回调方式):
首先,定义Client与Server间交互接口:
public interface RemoteIntf extends Remote {
public void regist(DispTimeIntf client, int second) throws RemoteException;
public void unregist(DispTimeIntf client) throws RemoteException;
}
实现这个接口,代码如下:
public class RemoteService extends UnicastRemoteObject implements RemoteIntf {
//保存客服端远程对象
Map<DispTimeIntf, Runnable> clients = new HashMap<DispTimeIntf, Runnable>();
//必须显式写出构造方法并且抛出异常
public RemoteService() throws RemoteException {
}
//服务器端需要持有客户端的远程对象(接口),以便回调
@Override
public void regist(final DispTimeIntf client, final int seconds) {
Thread t;
t = new Thread(new Runnable() {
@Override
public void run() {
Timer t = new Timer();
TimerTask callback_task = new TimerTask() {
public void run() {
try {
Date now = new Date();
DateFormat d = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG);
String time = d.format(now);
//调用客户端应用
try {
client.dispTime(time);
} catch (Exception e) {
}
} catch (Exception e) {
e.printStackTrace();
}
}
};
t.schedule(callback_task, 0, seconds * 1000);
}
});
t.run();
clients.put(client, t);
}
@Override
public void unregist(DispTimeIntf client) {
Thread t = (Thread) clients.remove(client);
t.interrupt();
}
}
然后将这个接口实现绑定到端口上,如下:
public class Server {
public static void main(String[] args) {
try {
//创建接受对9001端口的远程调用的注册表
LocateRegistry.createRegistry(9001);
//创建远程对象的实例(向客户端提供的服务)
final RemoteIntf service = new RemoteService();
//将提供给客户端远端调用的远程对象根据指定名称加入远程调用注册表
Naming.bind("//localhost:9001/server", service);
} catch (final Exception e1) {
e1.printStackTrace();
}
}
}
以上过程是符合RMI开发的一般过程,但注意的是远程接口中定义的参数DispTimeIntf,这是同样是一个远程接口定义,用于服务端调用。定义如下:
public interface DispTimeIntf extends Remote {
//提供给Server端调用的回调接口
public String dispTime(String time) throws RemoteException;
}
这个远程接口的实现,如下:
class DispTimeImpl extends UnicastRemoteObject implements DispTimeIntf {
protected DispTimeImpl() throws RemoteException {
super();
}
public String dispTime(final String time) throws RemoteException {
System.err.println("get from Server: " + time);
return time;
}
}
最后看客户端如何调用服务端的RMI服务,如下:
public class Client {
public static void main(String[] args) {
try {
String ip = "127.0.0.1";
//在指定地址查找远程对象实例
RemoteIntf service = (RemoteIntf) Naming.lookup("//" + ip + ":9001/server");
DispTimeImpl dispTime = new DispTimeImpl();
service.regist(dispTime, 1);
} catch (RemoteException e1) {
e1.printStackTrace();
} catch (Exception e1) {
e1.printStackTrace();
}
}
}
这也是一个符合RMI客户端调用的过程,不同的是在调用服务端服务时,将一个用于回调的远程接口作为参数传递给服务器,服务端将调用回调接口进行消息处理。在RMI环境中,用于远程回调的接口必须是一个远程接口。在部署时,其stub和skeleton的部署方向正好相反。
这只是示例了一个简单的远程消息传送机制,而更复杂的实现就是实现一个消息服务。当我们尝试实现一个简单的Java消息服务机制时,还要考虑到一些基本的要素:消息数据类型、消息传送管道、消息接入点、Topic和filter、消息框架管理。
消息数据类型要定义远程传送过程中的消息包的数据结构,包括消息头和消息体。消息头主要定义消息的Topic、消息序列、消息类型(定义消息意图,比如一如命令消息、状态改变消息、告警警告等)。
消息传送管道则说明消息传送机制:一对一或是一对多消息传送、失效消息或无法传送到目的地的消息如何进行处理、消息传送管道故障后如何处理消息等等。
消息接入点要说明应用如何连接到消息服务中来发送或接收消息。消息接入点最关心的是接收消息的流量控制。在消息接收上分为推模型或拉模型,它们是使用轮询或事件驱动方式来进入消息接入。对消息服务而言,还需考虑消息消费模式:消息分派还是消息获取,消息的订阅与过滤机制、消息接入应用是同步处理还是异步处理。
消息框架管理要提供一些简单的debug或跟踪手段,进行部署前的测试与调试。
当前有不少消息服务的开源实现,当进行选择时,考虑这些基本要素,作出合适消息服务选择去做合适的事情尤为重要。