2

我正在编写我的第一个 Go 程序,一个 SMTP 服务器,我认为使用 FSM 来表示网络协议的状态转换会很优雅。我真的很喜欢这个 haskell SMTP FSM 示例,所以我在此之后对其进行了松散的建模。

创建了一个简单的 FSM 类型,它接受一个转换表作为其构造函数参数,并有一个 Run 方法,该方法接受一个事件并将调用它在状态表中匹配的适当处理函数。然后我有一个“会话”类型,这是在处理来自其连接的传入 SMTP 命令后需要使用 FSM 的类型。

这是 FSM 转换的样子:

type Transition struct {
    from    State
    event   Event
    to      State
    handler func() string
}

然后,在我的 Session 对象中,我被迫在其构造函数中定义转换表,以便我可以访问其用于转换操作的方法:

func (s *SmtpSession) NewSession() {
    transitions := []Transition{    
        {Initial, Rset, Initial, sayOk},
        {HaveHelo, Rset, HaveHelo, sayOk},
        {AnyState, Rset, HaveHelo, resetState},

         ...

        {Initial, Data, Initial, needHeloFirst},
        {HaveHelo, Data, HaveHelo, needMailFromFirst},
        {HaveMailFrom, Data, HaveMailFrom, needRcptToFirst},
        {HaveRcptTo, Data, HaveData, startData},
    }
    smtpFsm = StateMachine.NewMachine(transitions)
}

当所有会话将具有基本相同的 FSM 时,必须在每个会话中创建此 FSM 的实例似乎很浪费。我宁愿只拥有某种“静态” FSM,它可以被赋予一个转换表,然后 Run 方法将采用当前状态和一个事件并返回结果“动作”函数。

然而,这就是我遇到麻烦的地方。因为所有的处理函数实际上都是 Session 对象的方法,所以我必须在 Session 中定义它。我想不出一种方法可以只为所有会话定义一次转换表,并且仍然可以适当地访问我需要的会话处理程序函数。

如果我以直接的程序风格编写这个程序,那么我就不会遇到任何这些问题。FSM 可以直接访问所有处理函数。

我唯一能想到的是将我的 FSM 更改为不返回函数指针,而是返回一些任意常量,然后 Session 将映射到适当的函数:

var transitions = []Transition{
    {Initial, Rset, Initial, "sayOk"},
    {HaveHelo, Rset, HaveHelo, "sayOk"},
    {AnyState, Rset, HaveHelo, "resetState"},
    ...
}

var smtpFsm = NewStateMachine(transitions)

func (s *Session) handleInput(cmd string) {
    event := findEvent(cmd)
    handler := findHandler(smtpFsm.Run(s.currentState, event))
    handler(cmd)   
}

func (s *Session) findHandler(handlerKey string) {
    switch handlerKey {
    case "sayOk":
        return sayOk
    case "resetState":
        return resetState
    }
}

这将解决必须为每个会话重新初始化一个新的 FSM 的问题,但它也感觉有点 hackish。有人对我如何避免这个问题有任何建议吗?这是演示问题的不完整 Session.go 的链接。

4

2 回答 2

3

如果您的处理程序不是方法,而是将实例作为参数的函数,那么整个事情会变得容易得多。这大致相同,但是您可以进行某种type State func(Session) State设置,并且可以更轻松地考虑它。

于 2012-11-03T01:30:35.563 回答
2

我不确定这个总体想法有多骇人听闻。随着关注点的分离,您的状态机只是发出一个令牌,并且不知道稍后将如何使用它。在您的handleInput方法中,您将使用该令牌来执行一个操作,在这种情况下是在您的会话对象上找到适当的方法。

但整件事对我来说似乎结束了。您有一个想要了解的转换表Session。我猜你有一个包fsm,我猜它做的很少,但也想知道,Session因为它依赖于转换表以使包有任何用途。

我会说要么切断链接并发出一个constSession用于找到合适方法的方法,走更程序化的路线,或者将位合并得更近(可能涉及放弃转换表)。

或者,如果您真的想走低效的路线,请findHandler按名称反映适当的方法并删除该 switch 语句,但这只是为了好玩:)

您还可以考虑一些替代实施方案。标准库中有很多很好的例子。查看文本/模板的包文件:

http://golang.org/src/pkg/text/template/parse/lex.go

http://golang.org/src/pkg/text/template/parse/parse.go

我所说的要点是,正如您已经指出的那样,如果您希望在函数之外定义转换表,您将需要引用令牌或函数,而不是你没有的实例。但是为了好玩,使用类似下面的东西,然后你可以Action("sayOk")在你的转换表中说,并传递Session给交付的结果函数。

package main

import (
    "fmt"
    "reflect"
)

type Foo struct{}

func (f *Foo) Bar() string {
    return "hello"
}

func Action(name string) func(f *Foo) string {
    return func(f *Foo) string {    
        s := reflect.ValueOf(f).MethodByName(name).Call([]reflect.Value{})
        return s[0].String()
    }
}


func main() {
    f := &Foo{}
    a := Action("Bar")
    fmt.Println(a(f))
}
于 2012-11-02T08:36:51.020 回答