接入会话和协议


注:这是高级内容,适用于打算实现自定义客户端协议的用户。

接入会话代表了连接到Evennia接入程序的外部连接,连接者通常是运行某种mud客户端的玩家。他们的连接方式,即玩家客户端与Evennia交互所用的语言,被称为连接协议。MUD最常见的协议是Telnet协议。所有的接入会话都由接入程序的会话处理程序负责存储和管理。

有时在技术上难以将会话和协议的概念分开,因为它们都严重依赖对方。


添加自定义协议

Evennia有一套插件系统,可以让你在不编辑 src/ 中任何文件的情况下添加新的自定义协议。要做到这一点,你需要将协议作为新的“服务”添加到应用程序。

查看 src/server/portal.py 中的实例,特别是文件的结尾部分。各种内置服务,如 telnet、ssh、webclient 等都在哪里被添加到接入程序中(在 src/server.server.py 中有一个等价的但更简短的清单)。

要添加自己的新服务(比如要将自定义的客户端协议添加到接入程序中),需要在 game/gamesrc/conf/ 中创建一个新模块,让我们把它命名为 myproc_plugin.py。我们要告诉服务器或接入程序需要导入这个模块。在 game/settings.py 中加入以下内容中的一个:
    # 添加到服务器
    SERVER_SERVICES_PLUGIN_MODULES.append('game.gamesrc.conf.myproc_plugin')
    # 或者,如果你想添加到接入程序
    PORTAL_SERVICES_PLUGIN_MODULES.append('game.gamesrc.conf.myproc_plugin')
在该模块中可以写入定义协议所需的任何东西,但它必须包含一个函数 start_plugin_services(app)。接入程序会将它作为启动程序的一部分来调用。start_plugin_services 函数必须包含启动服务所需的全部代码。参数 app 指向接入程序自身,这样自定义的服务就可以添加到它上面。这个函数应该不返回任何东西。

这是它可能的样子:
    # game/gamesrc/conf/myproc_plugin.py

    # 在这里定义新的接入Twisted协议
    class MyOwnFactory( ... ):
        [...]

    # 一些设置
    MYPROC_ENABLED = True # 直接做一些设置,省得每次运行都要修改设置
    MY_PORT = 6666

    def start_plugin_services(portal):
        "这在接入程序启动时调用"
        if not MYPROC_ENABLED:
            return
        # 在启动时将它和其他服务列在一起
        print "  myproc: %s" % MY_PORT

        # 一些设置(简单的例子)
        factory = MyOwnFactory()
        my_service = internet.TCPServer(MY_PORT, factory)
        # 所有Evennia服务的名字都必须唯一
        my_service.setName("MyService")
        # 添加到主接入应用程序
        portal.services.addService(my_service)
当你定义完模块并在 settings 中指定好之后,只需重新加载服务器,你的新协议/服务就会和其它服务一同加载了。


编写你自己的协议

我们不会在这里介绍如何从头开始编写一个稳定的通信协议,这不是一个简单的任务。还好Twisted提供了大量常用协议的实现方式可供你使用。

在 Twisted 中,编写协议的实现方式通常需要创建一个从适当 Twisted 父类继承的类,然后重载协议所需的特定方法,让它可以与 Evennia 交互。当有新连接通过这个协议建立时,这个类的实例会被调用。当各种状态变化时,类中具有特定名字的方法会被调用(他们的名字由 Twisted 的实现决定)。

直连协议(如Telnet)至少要由以下几部分组成(实际的方法名可能会有所不同):
  • connectionMade —— 在新连接建立时调用。它必须调用 self.init_session() 并提供三个参数:协议类型的标识符(如字符串“telnet”或“ssh”)、所连客户端的IP地址、以及会话处理程序的引用。按惯例,会话处理程序通常存储在 src/server/portal.py 的工厂中,具体例子请参阅该文件。这样做可以避免许多循环导入的问题。
  • connectionLost —— 不论因何种原因连接断开时都会调用它。它必须调用 self.sessionhandler.disconnect(self),会话处理程序可以确保将连接断开的消息通知到系统的其余部分。
  • getData —— 从玩家传来的数据抵达Evennia。它应该对该协议所需的任何格式的数据都使用,然后要将数据转发到 self.sessionhandler.data_in(self, msg, data)。
  • sendLine —— 数据从服务器发送到玩家。这由下面所说的 data_out() 钩子函数调用。
参见 server/telnet.py 中的例子。

这些内容可能不是在所有协议中都那么鲜明,但原则是相同的。不论访问方式如何,这四个基本部件都是与接入会话相连的,这是各种低级别协议与Evennia之间实际的通用接口。


接入会话

接入会话是Evennia的专有事物,它必须是从 src.server.session.Session 继承的类。如果你看过了上面的 Telnet 例子就会知道,有时协议和会话是通过多重继承在同一个类中实现的。在启动时接入程序会创建接入会话,并将它添加到其会话处理程序中。与此同时,这个会话处理程序也会赋给会话的 sessionhandler 属性。这很重要,因为该处理程序拥有所有与发送、接收数据服务器有关的方法。

尽管我们无法知道 Twisted 协议的方法名称(它会因协议的不同而不同),但会话有着严格的命名规则,不会改变,它(和其他一些便利的功能一起)是将协议与Evennia连接起来的纽带。

会话类必须实现以下的方法钩子(名称必须严格一致):
  • disconnect() —— 手动断开时调用。必须调用协议中特定的断开连接方法(如上述的 connectionLost)
  • data_in(text=None **kwargs) —— 数据从玩家到Evennia。通常它只是将协议实际使用的输入方法包装一下,以保持协议的一致性。通常它会将数据传到 self.sessionhandler.data_in。
  • data_out(text=None, **kwargs) —— 数据从Evennia到玩家。该方法应该先对数据做与特定协议相关的处理,然后再将数据传给类似于上述 self.sendLine() 的发送方法。可选的关键字参数可以用来将选项之类的信息传给协议。在 telnet 的例子中,关键字参数是如何将文字解析为ANSI序列的标识。在Evennia内部,通常使用 data_out 的别名 msg。


杂项笔记

再看一个例子,除了telnet协议,Evennia还支持webclient,这是一个自定义的ajax协议。你会发现,虽然上面看到的 telnet 是 Twisted 协议的典型例子,但ajax客户端的协议看起来却非常不同,因为它与网页服务器是通过长轮询(comet)的方式交互的。上面提到的所有必要部分都在,但实现方式非常不同。