Apache MINA --- [状态机]

若是你正在使用MINA来开发复杂的网络交互应用,你可能会发现本身在寻找一些良好的状态模式设计方案来解决其中的一些复杂性.那么在你那么作以前,咱们能够先来看看MINA状态机能为咱们作点什么再作决定.java


下面用一个简单的示例(磁带)展现MINA状态机的工做方式:(每一个节点表明状态,箭头表明转换操做)apache

接下来看代码:安全

//对外接口
public interface TapeDeck {
    void load(String nameOfTape);
    void eject();
    void start();
    void pause();
    void stop();
}

// 事件处理类(不须要实现TapeDeck接口)
public class TapeDeckHandler {
    //使用@State注解来声明状态
    @State public static final String EMPTY = "Empty";
    @State public static final String LOADED = "Loaded";
    @State public static final String PLAYING = "Playing";
    @State public static final String PAUSED = "Paused";

    //使用@Transition注解来声明转换(on:触发的转换事件ID,in:事件起始状态,next:事件目标状态)
    @Transition(on = "load", in = EMPTY, next = LOADED)
    public void loadTape(String nameOfTape) {
        System.out.println("Tape '" + nameOfTape + "' loaded");
    }

    //当一个事件可以基于多个起始状态而被触发时,必须使用@Transitions注解
    //(如上图所示,磁带在LOADED,PAUSED的状态下,都能触发"play"事件)
    @Transitions({
        @Transition(on = "play", in = LOADED, next = PLAYING),
        @Transition(on = "play", in = PAUSED, next = PLAYING)
    })
    public void playTape() {
        System.out.println("Playing tape");
    }
    
    //代表当磁带处于PLAYING状态而发生"pause"事件时候,这个方法会被调用而且状态将转变成PAUSED状态
    @Transition(on = "pause", in = PLAYING, next = PAUSED)
    public void pauseTape() {
        System.out.println("Tape paused");
    }

    @Transition(on = "stop", in = PLAYING, next = LOADED)
    public void stopTape() {
        System.out.println("Tape stopped");
    }

    @Transition(on = "eject", in = LOADED, next = EMPTY)
    public void ejectTape() {
        System.out.println("Tape ejected");
    }
    
    /**
     * 关于@Transition参数的额外说明
     *
     * 若是省略参数"on",会默认使用"*",表示会匹配到全部事件
     * 若是省略参数"next",会默认使用"_self_",它表明当前状态的一个别名,这种方式能够用来创建循环
     * 参数"weight"可用来定义转换将以什么索引值被搜索,状态的转换将依据它们的"weight"值来升序排列,默认值是"0"
     *
     */
}

//MAIN
public static void main(String[] args) {
    TapeDeckHandler handler = new TapeDeckHandler();
    //使用TapeDeckHandler来建立一个状态机实例,并指定起始状态为EMPTY,每一个@Transition注解对应一个Transition实例
    StateMachine sm = StateMachineFactory.getInstance(Transition.class).create(TapeDeckHandler.EMPTY, handler);
    //建立TapeDeck接口的代理实现
    TapeDeck deck = new StateMachineProxyBuilder().create(TapeDeck.class, sm);

    deck.load("The Knife - Silent Shout");
    deck.play();
    deck.pause();
    deck.play();
    deck.stop();
    deck.eject();
    
    /**
     * @Transition和Transition的区别
     *
     * @Transition只是一个注解,它用来标记当一个转换事件触发时会被调用的方法,在幕后,MINA状态机将会为每个被
     * 该注解声明的方法建立一个MethodTransition实例,而MethodTransition则实现了Transition接口.做为一个
     * MINA用户,你永远不须要直接使用Transiton或MethodTransition类型.
     *
     */
}


让咱们仔细看看当一个代理的方法被执行时都发生了些什么:网络

StateContext对象是很重要的,由于它保存了当前状态.当一个方法在代理中被调用时,它要求StateContextLookup实例从方法参数中得到StateContext.通常来讲,StateContextLookup实现会遍历方法参数来寻找一个特定类型的对象并用它来找回StateContext对象,若是尚未StateContext被分配,它会新建一个并保存起来.session

当咱们使用MINA的IoHandler时,咱们将会使用IoSessionStateContextLookup实例来从方法参数中查找IoSession.它将会使用IoSession的属性来为每一个MINA session储存一个独立的StateContext实例.这样一来全部MINA session均可以互不干扰的使用同一个状态机.并发

(备注:在上面的例子中,当咱们使用StateMachineProxyBuilder来建立代理的时候并无指定具体哪一个StateContextLookup的实现.那么默认就使用SingletonStateContextLookup,它彻底忽视传递给它的方法参数而始终返回同一个StateContext对象,很显然,这种方式在多个客户端并发使用同一个状态机的状况下是没用的)
app

全部在代理对象上的方法调用将会被代理转换成事件对象.每一个事件都有一个ID以及0+参数.这个ID对应了方法的名字而事件参数对应了方法的参数.例如方法deck.load("XXX")对应了事件{id="load",arguments=["XXX"]}.事件对象也预包含了一个指向StateContext对象的引用.ui

一旦事件对象被建立代理类将会调用StateContext.handle(Event)方法.该方法遍历当前状态的Transition对象来查找接收当前事件的Transition实例.当Transition对象被找到后,这个操做会中止.查找过程会按照属性"weight"的升序顺序来执行(该属性在@Transition注解中指定)this

最后一步是调用匹配到的Transition对象的execute(Event)方法,执行完成后,状态机将会变动当前状态到"next"属性指定的状态.编码


MethodTransition的匹配规则:

考虑该事件: {id = "messageReceived", arguments = [ArrayList a = [...], Integer b = 1024]}

//----------------------------------能够被匹配的方法--------------------------------------
// All method arguments matches all event arguments directly
// 彻底匹配
@Transition(on = "messageReceived")
public void messageReceived(ArrayList l, Integer i) { ... }

// Matches since ((a instanceof List && b instanceof Number) == true)
// 匹配 由于((a是List的实现 && b是Number的实现) == true)
@Transition(on = "messageReceived")
public void messageReceived(List l, Number n) { ... }

// Matches since ((b instanceof Number) == true)
// 匹配 由于((b是Number的实现) == true)
@Transition(on = "messageReceived")
public void messageReceived(Number n) { ... }

// Methods with no arguments always matches
// 匹配 无参方法老是匹配
@Transition(on = "messageReceived")
public void messageReceived() { ... }

// Methods only interested in the current Event or StateContext always matches
// 匹配 直插入了Event或者StateContext的老是匹配
@Transition(on = "messageReceived")
public void messageReceived(StateContext context) { ... }

// Matches since ((a instanceof Collection) == true)
// 匹配 由于((a是Collection的实现) == true)
@Transition(on = "messageReceived")
public void messageReceived(Event event, Collection c) { ... }

//匹配 由于MyStateContext是StateContext的实现
@Transition(on = "messageReceived")
public void messageReceived(MyStateContext context) { ... }

//----------------------------------不能被匹配的方法--------------------------------------
// Incorrect ordering
// 不匹配 顺序错误
@Transition(on = "messageReceived")
public void messageReceived(Integer i, List l) { ... }

// ((a instanceof LinkedList) == false)
// 不匹配 ((a是LinkedList的实现) == false)
@Transition(on = "messageReceived")
public void messageReceived(LinkedList l, Number n) { ... }

// Event must be first argument
// 不匹配 Event必须位于首位
@Transition(on = "messageReceived")
public void messageReceived(ArrayList l, Event event) { ... }

// StateContext must be second argument if Event is used
// 不匹配 若是存在Event,StateContext必须位于第二位
@Transition(on = "messageReceived")
public void messageReceived(Event event, ArrayList l, StateContext context) { ... }

// Event must come before StateContext
// 不匹配 StateContext必须在Event以后
@Transition(on = "messageReceived")
public void messageReceived(StateContext context, Event event) { ... }


//额外的说明
//若是同时拥有Event和StateContext,Event必须位于首位,StateContext必须位于第二位
//若是只出现Event和StateContext中的一个,它们都必须位于首位
//自定义的参数顺序也是被严格要求的
//Integer,Double,Float等也能匹配到对应的基础类型int,double,float


状态继承:

状态实例也许有父类.若是StateMachine.handle(Event)没有在当前状态下找到可以匹配当前事件的Transition,它会查找父状态直到顶层.利用这一特性,咱们能够轻易编写一些通用代码而不须要为每一个状态指定全部Transition(若是在某状态下找不到对应当前事件的转换,会抛出异常).

来看具体例子:

public static void main(String[] args) {
    ...
    deck.load("The Knife - Silent Shout");
    deck.play();
    deck.pause();
    deck.play();
    deck.stop();
    deck.eject();
    deck.play();//异常
}

//...
//Tape stopped
//Tape ejected
//Exception in thread "main" o.a.m.sm.event.UnhandledEventException: 
//Unhandled event: org.apache.mina.statemachine.event.Event@15eb0a9[id=play,...]
//    at org.apache.mina.statemachine.StateMachine.handle(StateMachine.java:285)
//    at org.apache.mina.statemachine.StateMachine.processEvents(StateMachine.java:142)
    
//咱们能够这么作    
@Transitions({
    @Transition(on = "*", in = EMPTY, weight = 100),
    @Transition(on = "*", in = LOADED, weight = 100),
    @Transition(on = "*", in = PLAYING, weight = 100),
    @Transition(on = "*", in = PAUSED, weight = 100)
})
public void error(Event event) {
    System.out.println("Cannot '" + event.getId() + "' at this time");
}

//...
//Tape stopped
//Tape ejected
//Cannot 'play' at this time.

//可是这里只是简单的示例,只有4种状态,若是有几十种状态?因此咱们这么作
//使用状态继承来处理异常
public static class TapeDeckHandler {
    @State public static final String ROOT = "Root";
    @State(ROOT) public static final String EMPTY = "Empty";
    @State(ROOT) public static final String LOADED = "Loaded";
    @State(ROOT) public static final String PLAYING = "Playing";
    @State(ROOT) public static final String PAUSED = "Paused";

    ...

    @Transition(on = "*", in = ROOT)
    public void error(Event event) {
        System.out.println("Cannot '" + event.getId() + "' at this time");
    }
}

//...
//Tape stopped
//Tape ejected
//Cannot 'play' at this time.


MINA状态机之IoHandler:

完整代码:http://mina.apache.org/mina-project/xref/org/apache/mina/example/tapedeck/

如今咱们把磁带示例转变成一个TCP服务.服务端接收命令如:load,play,stop等.响应+或-.协议是基于UTF-8文本的.

telnet localhost 12345
S: + Greetings from your tape deck!
C: list
S: + (1: "The Knife - Silent Shout", 2: "Kings of convenience - Riot on an empty street")
C: load 1
S: + "The Knife - Silent Shout" loaded
C: play
S: + Playing "The Knife - Silent Shout"
C: pause
S: + "The Knife - Silent Shout" paused
C: play
S: + Playing "The Knife - Silent Shout"
C: info
S: + Tape deck is playing. Current tape: "The Knife - Silent Shout"
C: eject
S: - Cannot eject while playing
C: stop
S: + "The Knife - Silent Shout" stopped
C: eject
S: + "The Knife - Silent Shout" ejected
C: quit
S: + Bye! Please come back!

这里咱们不会过多的详细描述代码细节,而只选择其中重要的一部分,若是要看完整代码请访问上面URL.

如今我么看看服务端是如何工做的.核心类是实现了状态机的TapeDeckServer:

//咱们再也不使用@Transitions和@Transition注解而用@IoHandlerTransitions和@IoHandlerTransition来取代它们.
//当咱们为IoHandler接口建立状态机时,它们是最优选项,由于它们为事件ID提供了枚举类型而非咱们以前所用的字符串.
//也有相应的IoFilter接口的注解.
public class TapeDeckServer {
    
    //使用状态继承来实现通用逻辑代码
    @State
    public static final String ROOT = "Root";
    @State(ROOT)
    public static final String EMPTY = "Empty";
    @State(ROOT)
    public static final String LOADED = "Loaded";
    @State(ROOT)
    public static final String PLAYING = "Playing";
    @State(ROOT)
    public static final String PAUSED = "Paused";

    private final String[] tapes = {"The Knife - Silent Shout", "Kings of convenience - Riot on an empty street"};

    //咱们使用自定义的StateContext实现:TapeStateContext.这个类用来跟踪当前磁带的名称
    //咱们为何不把磁带名做为属性设置在IoSession中?由于自定义的StateContext提供了类型安全
    static class TapeDeckContext extends AbstractStateContext {
        private String tapeName;
    }

    @IoHandlerTransition(on = SESSION_OPENED, in = EMPTY)
    public void connect(IoSession session) {
        session.write("+ Greetings from your tape deck!");
    }

    //该方法中的最后一个参数是LoadCommand类型,这意味着只有当messageReceived(IoSession session, Object message)
    //中的message能被解码成LoadCommand的时候该方法才会被匹配执行. 
    @IoHandlerTransition(on = MESSAGE_RECEIVED, in = EMPTY, next = LOADED)
    public void loadTape(TapeDeckContext context, IoSession session, LoadCommand cmd) {
        if (cmd.getTapeNumber() < 1 || cmd.getTapeNumber() > tapes.length) {
            session.write("- Unknown tape number: " + cmd.getTapeNumber());
            //这一行代码使用StateControl来覆盖目标状态,若是磁带不能识别,则没法进入LOADED状态
            StateControl.breakAndGotoNext(EMPTY);
        } else {
            context.tapeName = tapes[cmd.getTapeNumber() - 1];
            session.write("+ \"" + context.tapeName + "\" loaded");
        }
    }

    @IoHandlerTransitions({@IoHandlerTransition(on = MESSAGE_RECEIVED, in = LOADED, next = PLAYING), @IoHandlerTransition(on = MESSAGE_RECEIVED, in = PAUSED, next = PLAYING)})
    public void playTape(TapeDeckContext context, IoSession session, PlayCommand cmd) {
        session.write("+ Playing \"" + context.tapeName + "\"");
    }

    @IoHandlerTransition(on = MESSAGE_RECEIVED, in = PLAYING, next = PAUSED)
    public void pauseTape(TapeDeckContext context, IoSession session, PauseCommand cmd) {
        session.write("+ \"" + context.tapeName + "\" paused");
    }

    @IoHandlerTransition(on = MESSAGE_RECEIVED, in = PLAYING, next = LOADED)
    public void stopTape(TapeDeckContext context, IoSession session, StopCommand cmd) {
        session.write("+ \"" + context.tapeName + "\" stopped");
    }

    @IoHandlerTransition(on = MESSAGE_RECEIVED, in = LOADED, next = EMPTY)
    public void ejectTape(TapeDeckContext context, IoSession session, EjectCommand cmd) {
        session.write("+ \"" + context.tapeName + "\" ejected");
        context.tapeName = null;
    }

    //in=ROOT:在任何状态下都能调用
    @IoHandlerTransition(on = MESSAGE_RECEIVED, in = ROOT)
    public void listTapes(IoSession session, ListCommand cmd) {
        StringBuilder response = new StringBuilder("+ (");
        for (int i = 0; i < tapes.length; i++) {
            response.append(i + 1).append(": ");
            response.append('"').append(tapes[i]).append('"');
            if (i < tapes.length - 1) {
                response.append(", ");
            }
        }
        response.append(')');
        session.write(response);
    }

    @IoHandlerTransition(on = MESSAGE_RECEIVED, in = ROOT)
    public void info(TapeDeckContext context, IoSession session, InfoCommand cmd) {
        String state = context.getCurrentState().getId().toLowerCase();
        if (context.tapeName == null) {
            session.write("+ Tape deck is " + state + "");
        } else {
            session.write("+ Tape deck is " + state + ". Current tape: \"" + context.tapeName + "\"");
        }
    }

    @IoHandlerTransition(on = MESSAGE_RECEIVED, in = ROOT)
    public void quit(TapeDeckContext context, IoSession session, QuitCommand cmd) {
        session.write("+ Bye! Please come back!").addListener(IoFutureListener.CLOSE);
    }

    @IoHandlerTransition(on = MESSAGE_RECEIVED, in = ROOT, weight = 10)
    public void error(Event event, StateContext context, IoSession session, Command cmd) {
        session.write("- Cannot " + cmd.getName() + " while " + context.getCurrentState().getId().toLowerCase());
    }

    //编码异常,输出
    @IoHandlerTransition(on = EXCEPTION_CAUGHT, in = ROOT)
    public void commandSyntaxError(IoSession session, CommandSyntaxException e) {
        session.write("- " + e.getMessage());
    }

    //其余异常,关闭会话,weight=10代表它的匹配顺序在commandSyntaxError以后
    @IoHandlerTransition(on = EXCEPTION_CAUGHT, in = ROOT, weight = 10)
    public void exceptionCaught(IoSession session, Exception e) {
        e.printStackTrace();
        session.close(true);
    }

    //这个方法用来处理全部其余的状况,咱们不能舍弃它由于咱们并无用@IoHandlerTransition注解来声明全部状态下
    //全部可能的事件.没有了这个方法,MINA状态机将会抛出异常若是那个事件能被状态机处理的话(如:messageSent事件)
    @IoHandlerTransition(in = ROOT, weight = 100)
    public void unhandledEvent() {
    }

}

来看MAIN方法:

private static IoHandler createIoHandler() {
    //由于咱们用@IoHandlerTransition注解取代了@Transition注解,因此这里也作相应的改变
    StateMachine sm = StateMachineFactory.getInstance(IoHandlerTransition.class).create(EMPTY, new TapeDeckServer());
    
    //这里咱们指定了IoSessionStateContextLookup做为StateContextLookup实现
    //若是不这么作,StateContext对象始终是单例
    return new StateMachineProxyBuilder().setStateContextLookup(
            new IoSessionStateContextLookup(new StateContextFactory() {
                public StateContext create() {
                    return new TapeDeckContext();
                }
            })).create(IoHandler.class, sm);
}

public static void main(String[] args) throws Exception {
    SocketAcceptor acceptor = new NioSocketAcceptor();
    acceptor.setReuseAddress(true);
    ProtocolCodecFilter pcf = new ProtocolCodecFilter(
            new TextLineEncoder(), new CommandDecoder());
    acceptor.getFilterChain().addLast("codec", pcf);
    acceptor.setHandler(createIoHandler());
    acceptor.setLocalAddress(new InetSocketAddress(PORT));
    acceptor.bind();
}
相关文章
相关标签/搜索