【问题标题】:Better structure for request based protocol implementation基于请求的协议实现的更好结构
【发布时间】:2012-08-05 01:33:05
【问题描述】:

我正在使用一种协议,它基本上是基于 TCP 的请求和响应协议,类似于其他基于行的协议(SMTP、HTTP 等)。

该协议有大约 130 种不同的请求方法(例如登录、用户添加、用户更新、日志获取、文件信息、文件信息……)。所有这些方法都不能很好地映射到 HTTP 中使用的广泛方法(GET、POST、PUT、...)。如此宽泛的方法会引入一些不相关的实际含义的曲解。

但协议方法可以按类型分组(例如,用户管理、文件管理、会话管理……)。

当前的服务器端实现使用 class Worker 和方法 ReadRequest()(读取请求,由方法和参数列表组成)、HandleRequest()(见下文)和 WriteResponse()(写入响应代码和实际响应数据) .

HandleRequest() 将为实际请求方法调用一个函数 - 使用方法名称的哈希映射到指向实际处理程序的成员函数指针。

实际的处理程序是一个普通的成员函数,每个协议方法都有一个:每个方法验证其输入参数,执行它必须做的任何事情并设置响应代码(成功是/否)和响应数据。

示例代码:

class Worker {
    typedef bool (Worker::*CommandHandler)();
    typedef std::map<UTF8String,CommandHandler> CommandHandlerMap;

    // handlers will be initialized once
    //   e.g. m_CommandHandlers["login"] = &Worker::Handle_LOGIN;
    static CommandHandlerMap m_CommandHandlers;

    bool HandleRequest() {
        CommandHandlerMap::const_iterator ihandler;
        if( (ihandler=m_CommandHandlers.find(m_CurRequest.instruction)) != m_CommandHandler.end() ) {
            // call actual handler
            return (this->*(ihandler->second))();
        }
        // error case:
        m_CurResponse.success = false;
        m_CurResponse.info = "unknown or invalid instruction";
        return true;
    }

    //...


    bool Handle_LOGIN() {
        const UTF8String username = m_CurRequest.parameters["username"];
        const UTF8String password = m_CurRequest.parameters["password"];

        // ....

        if( success ) {

            // initialize some state...
            m_Session.Init(...);
            m_LogHandle.Init(...);
            m_AuthHandle.Init(...);

            // set response data
            m_CurResponse.success = true;
            m_CurResponse.Write( "last_login", ... );
            m_CurResponse.Write( "whatever", ... );
        } else {
            m_CurResponse.Write( "error", "failed, because ..." );
        }
        return true;
    }


};

所以。问题是:我的工人班现在有大约 130 个“命令处理程序方法”。每个人都需要访问:

  1. 请求参数
  2. 响应对象(写入响应数据)
  3. 不同的其他会话本地对象(如数据库句柄、授权/权限查询句柄、日志记录、服务器各种子系统的句柄等)

什么是更好地构造这些命令处理程序方法的好策略?

一个想法是每个命令处理程序有一个类,并使用对请求、响应对象等的引用对其进行初始化-但恕我直言,开销是不可接受的(实际上,它会为任何单个访问添加间接处理程序需要的一切:请求、响应、会话对象……)。如果它能够提供实际优势,它是可以接受的。但是,这听起来不太合理:

class HandlerBase {
protected:
    Request &request;
    Response &response;
    Session &session;
    DBHandle &db;
    FooHandle &foo;
    // ...
public:
    HandlerBase( Request &req, Response &rsp, Session &s, ... )
    : request(req), response(rsp), session(s), ...
    {}
    //...
    virtual bool Handle() = 0;
};

class LoginHandler : public HandlerBase {
public:
    LoginHandler( Request &req, Response &rsp, Session &s, ... )
    : HandlerBase(req,rsp,s,..)
    {}
    //...
    virtual bool Handle() {
        // actual code for handling "login" request ...
    }
};

好的,HandlerBase 可以只获取工作对象本身的引用(或指针)(而不是请求、响应等的引用)。但这也会增加另一个间接性(this->worker->session 而不是 this->session)。这种间接性是可以的,如果它最终能买到一些优势的话。

有关整体架构的一些信息

worker 对象代表一个用于与某个客户端的实际 TCP 连接的单个工作线程。每个线程(因此,每个工作线程)都需要自己的数据库句柄、授权句柄等。这些“句柄”是每个线程的对象,允许访问服务器的某些子系统。

整个架构基于某种依赖注入:例如要创建会话对象,必须为会话构造函数提供“数据库句柄”。然后会话对象使用这个数据库句柄来访问数据库。它永远不会调用全局代码或使用单例。因此,每个线程都可以不受干扰地独立运行。

但是代价是——而不是仅仅调用单例对象——worker 及其命令处理程序必须通过这种特定于线程的句柄访问系统的任何数据或其他代码。这些句柄定义了它的执行上下文。

总结和澄清:我的实际问题

我正在寻找一个优雅的替代当前(“具有大量处理程序方法列表的工作对象”)解决方案:它应该是可维护的,具有低开销并且不需要编写太多的胶水代码。此外,它必须仍然允许每个方法控制其执行的非常不同的方面(这意味着:如果方法“superflurry foo”想要在满月时失败,那么该实现必须有可能这样做) .这也意味着,我不希望在我的代码的这个架构层(它存在于我的代码的不同层)有任何类型的实体抽象(创建/读取/更新/删除 XFoo 类型)。这个架构层是纯协议,没有别的。

最终肯定是妥协,但我对任何想法都感兴趣!

AAA 奖励:具有可互换协议实现的解决方案(而不仅仅是当前的 class Worker,它负责解析请求和编写响应)。 也许可能是一个可互换的class ProtocolSyntax,它处理那些协议语法细节,但仍然使用我们新的闪亮的结构化命令处理程序。

【问题讨论】:

  • 这对 C++ 来说似乎是一项糟糕的工作。我会重新考虑您的设计方法,并可能在 Java 中设计一个 HTTP servlet。哪些具体的事情你不能用 HTTP 做而你想用 FRUNSTP(你的协议)做?
  • HTTP 方法和 REST 样式接口适用于大多数事情。但是,还有一些命令,例如“文件元更新”,它应该在一堆不同的文件上设置不同的元数据字段。这将打破 REST 概念(每个文件都有其单个 URL)。当然,可以只对单个文件使用“文件元更新”。但是多文件更新(除了许多其他“多实体”查询)在我的软件中是性能关键的事情。
  • 哦,当然,我的协议不是无状态的,就像 HTTP。一个典型的会话包括登录、查询不同的数据、更新一些东西,最后关闭连接。
  • 那么您只需将 REST 与 POST / PUT 一起使用,该 POST / PUT 详细描述了您正在执行的操作,例如像这样的路径:POST http://myserver.com/files/update-meta?token=XX_XXXX_XX, [ 'fileName' : { meta-attribute : 'meta-value' }, ... ] 其中token 是访问令牌。既然已经有了很棒的工具,为什么还要重新发明轮子呢?
  • 简单地将请求参数和响应对象作为输入参数传递给每个处理程序有什么问题?

标签: c++ design-patterns


【解决方案1】:

你已经有了大部分正确的想法,下面是我将如何继续。

让我们从您的第二个问题开始:可互换协议。如果你有通用的请求和响应对象,你可以有一个读取请求和写入响应的接口:

class Protocol {
  virtual Request *readRequest() = 0;
  virtual void writeResponse(Response *response) = 0;
}

例如,您可以有一个名为 HttpProtocol 的实现。

对于您的命令处理程序,“每个命令处理程序一个类”是正确的方法:

class Command {
  virtual void execute(Request *request, Response *response, Session *session) = 0;
}

请注意,我将所有常见的会话句柄(DB、Foo 等)汇总到一个对象中,而不是传递一大堆参数。同样,使用这些方法参数而不是构造函数参数意味着您只需要每个命令的一个实例。

接下来,您将拥有一个 CommandFactory,其中包含命令名称到命令对象的映射:

class CommandFactory {
  std::map<UTF8String, Command *> handlers;

  Command *getCommand(const UTF8String &name) {
    return handlers[name];
  }
}

如果您完成了所有这些操作,Worker 会变得非常纤细,并且可以简单地协调所有内容:

class Worker {
  Protocol *protocol;
  CommandFactory *commandFactory;
  Session *session;

  void handleRequest() {
    Request *request = protocol->readRequest();
    Response response;

    Command *command = commandFactory->getCommand(request->getCommandName());
    command->execute(request, &response, session);

    protocol->writeResponse(&response);
  }
}

【讨论】:

  • +1 因为我真的很喜欢这个解决方案。我自己在生产代码中使用过它。但是,这里有一个建议。请注意,当使用“每个命令处理程序一个类”的方法时,您最终会得到超过一百个主要是样板的类。我曾经编写过一个 XMLRPC 服务器,其中“只有”大约两打处理程序类,但过了一段时间,这变得非常痛苦。如果我可以再做一次,我肯定会三思而后行,可能会选择多个成员函数。也许您可以将相关函数重构为几个单独的类。尝试找到一个好的中间道路。
  • @djf:如果设计得当,拥有许多小班应该不会是一件痛苦的事。但通常这表明服务接口本身有问题——为什么单个服务会暴露这么多方法?
  • 我完全同意。 Frunsi 正在查看界面中的 130 种方法!我认为应该在重构服务器实现之前重新设计接口。只是我的 2 美分。
  • @djf:我没有意识到 OP 在他的问题中实际上提到了 130 种方法,所以是的,您的评论更加相关。 :)
  • @djf:拥有这 130 种方法本身就是一个主题。是的,重新设计会有所帮助(例如,更类似于 OOP 的协议),但仍然会有很多方法和许多特殊情况。它的软件复杂,不仅仅是抽象的“管理一些对象的东西”,也不是纯粹的 CRUD 东西。
【解决方案2】:

如果是我,我可能会在您的问题中使用两者的混合解决方案。
有一个可以处理多个相关命令的工作基类,并且可以允许您的主“调度”类探测支持的命令。对于胶水,您只需要告诉调度类每个工人类。

class HandlerBase
{
public:
    HandlerBase(HandlerDispatch & dispatch) : m_dispatch(dispatch) {
        PopulateCommands();
    }
    virtual ~HandlerBase();

    bool CommandSupported(UTF8String & cmdName);

    virtual bool HandleCommand(UTF8String & cmdName, Request & req, Response & res);
    virtual void PopulateCommands();

protected:
    CommandHandlerMap m_CommandHandlers; 
    HandlerDispatch & m_dispatch;
};

class AuthenticationHandler : public HandlerBase
{
public:
    AuthenticationHandler(HandlerDispatch & dispatch) : HandlerBase(dispatch) {}

    bool HandleCommand(UTF8String & cmdName, Request & req, Response & res) {
        CommandHandlerMap::const_iterator ihandler;                     
        if( (ihandler=m_CommandHandlers.find(req.instruction)) != m_CommandHandler.end() ) {                     
            // call actual handler                     
            return (this->*(ihandler->second))(req,res);                     
        }                     
        // error case:                     
        res.success = false;                     
        res.info = "unknown or invalid instruction";                     
        return true; 
    }

    void PopulateCommands() {
        m_CommandHandlers["login"]=Handle_LOGIN;
        m_CommandHandlers["logout"]=Handle_LOGOUT;
    }

    void Handle_LOGIN(Request & req, Response & res) {
        Session & session = m_dispatch.GetSessionForRequest(req);
        // ...
    }
};

class HandlerDispatch
{
public:
    HandlerDispatch();
    virtual ~HandlerDispatch() {  
        // delete all handlers 
    }

    void AddHandler(HandlerBase * pHandler);
    bool HandleRequest() {
        vector<HandlerBase *>::iterator i;
        for ( i=m_handlers.begin() ; i < m_handlers.end(); i++ ) {
            if ((*i)->CommandSupported(m_CurRequest.instruction)) {
                return (*i)->HandleCommand(m_CurRequest.instruction,m_CurRequest,m_CurResponse);
            }
        }
        // error case:                                                            
        m_CurResponse.success = false;
        m_CurResponse.info = "unknown or invalid instruction";

        return true; 
    }
protected:
    std::vector<HandlerBase*> m_handlers;
}

然后将它们粘合在一起,您将执行以下操作:

// Init
m_handlerDispatch.AddHandler(new AuthenticationHandler(m_handlerDispatch));

【讨论】:

  • 谢谢,看起来很有希望。有两个地图查找(而不是一个)应该是可以接受的权衡。
【解决方案3】:

至于传输 (TCP) 特定部分,您是否看过通过消息套接字/队列支持各种分布式计算模式的 ZMQ 库?恕我直言,您应该在他们的 Guide 文档中找到满足您需求的合适模式。

对于协议消息实现的选择,我个人最喜欢google protocol buffers,它与 C++ 配合得非常好,我们现在正在几个项目中使用它。

至少您将归结为特定请求及其参数 + 必要返回参数的调度程序和处理程序实现。 Google protobuf 消息扩展以通用方式允许这样做。

编辑:

更具体一点,使用 protobuf 消息,调度程序模型与您的主要区别在于您不需要在调度之前进行完整的消息解析,但是您可以注册处理程序,告诉自己是否可以通过消息的扩展名处理或不处理特定消息。 (主)调度程序类不需要知道要处理的具体扩展,只需询问已注册的处理程序类。您可以轻松扩展此机制,让某些子调度程序覆盖更深的消息类别层次结构。

因为 protobuf 编译器已经可以完全看到您的消息传递数据模型,所以您不需要任何类型的反射或动态类多态性测试来确定具体的消息内容。您的 C++ 代码可以静态地请求消息的可能扩展,如果不存在则不会编译。

我不知道如何以更好的方式解释这一点,或者展示一个具体示例如何使用这种方法改进现有代码。恐怕您已经在消息格式的反序列化代码上花费了一些精力,使用 google protobuf 消息(或者 RequestResponse 是什么类?)可以避免这种情况。

ZMQ 库可能有助于实现您的 Session 上下文以通过基础架构分派请求。

当然,您最终不应该只使用一个接口来处理各种可能的请求,而应该使用多个专门处理消息类别(扩展点)的接口。

【讨论】:

  • 谢谢,但我的实际协议很好。我知道谷歌协议缓冲区,但在这里不需要它们。 ZMQ 及其指南也无济于事。
  • @Frunsi 好的,这只是一个想法,这将是我解决问题的第一种方法,因为我已经成功使用了几次。从抽象的角度来看,我认为您的问题将在 Chain of Responsibility 模式内解决。
  • 我认为责任链模式对我的用例来说效率低下(如果多个处理程序参与处理单个请求会很有意义)...
  • 是的,我的代码已经花费了一些精力进行反序列化(协议很简单,可以有效地解析和编写)。 RequestResponse 对象基本上只包含一个键值对列表。发送/接收二进制数据的模式将其四舍五入(它可以使用sendfile()系统调用进行零拷贝...)
  • 你能举一个分派扩展的小例子吗?例如,消息为Animal,扩展为CatDogCat 的处理程序打印 "Meow"Dog 的处理程序打印 "Bark"
【解决方案4】:

我认为这是类 REST 实现的理想案例。另一种方法也可以是根据类别/任何其他标准将处理程序方法分组到几个工人类。

【讨论】:

    【解决方案5】:

    如果协议方法只能按类型分组,但同一组的方法在其实现中没有任何共同点,那么提高可维护性的唯一方法可能是在不同文件之间分配方法,一个文件一组.

    但很可能同一组的方法具有以下一些共同特征:

    1. Worker 类中的某些数据字段可能仅由一组方法或多个(但不是每个)组使用。例如,如果 m_AuthHandle 只能由用户管理和会话管理方法使用。
    2. 可能有一些输入参数组,被某个组的每个方法使用。
    3. 可能有一些公共数据,通过某个组的每个方法写入响应。
    4. 可能有一些常用方法,被某个组的多个方法调用。

    如果其中一些事实是真实的,那么就有充分的理由将这些特征归为不同的类别。不是每个命令处理程序一个类,而是每个事件组一个类。或者,如果有多个组共有的特征,则为类的层次结构。

    将所有这些组类的实例分组到一个位置可能会很方便:

    classe UserManagement: public IManagement {...};
    classe FileManagement: public IManagement {...};
    classe SessionManagement: public IManagement {...};
    struct Handlers {
      smartptr<IManagement> userManagement;
      smartptr<IManagement> fileManagement;
      smartptr<IManagement> sessionManagement;
      ...
      Handlers():
        userManagement(new UserManagement),
        fileManagement(new FileManagement),
        sessionManagement(new SessionManagement),
        ...
      {}
    };
    

    可以使用make_unique 等模板来代替new SomeClass。或者,如果需要“可互换协议实现”,其中一种可能性是使用工厂而不是部分(或全部)new SomeClass 运算符。

    m_CommandHandlers.find() 应该分成两个映射搜索:一个 - 在此结构中找到适当的处理程序,另一个(在 IManagement 的适当实现中) - 找到指向实际处理程序的成员函数指针。

    除了查找成员函数指针之外,任何IManagement 实现的HandleRequest 方法都可以为其事件组提取公共参数并将它们传递给事件处理程序(如果只有几个,则一个接一个,或者分组如果有很多,则在一个结构中)。

    另外IManagement 实现可能包含WriteCommonResponce 方法以简化所有事件处理程序通用的响应字段的编写。

    【讨论】:

      【解决方案6】:

      命令模式是解决这个问题的两个方面的方法。

      使用它来实现您的协议处理程序,该处理程序具有一个通用的 IProtocol 接口(和/或抽象基类),以及协议处理程序的不同实现,具有专门用于每个协议的不同类。

      然后使用 ICommand 接口以相同的方式实现您的命令,并在单独的类中实现每个命令方法。你快到了。将现有方法拆分为新的专用类。

      将您的请求和响应包装为 Mememento 对象

      【讨论】:

      • 来吧 - 如果这就像将想到的第一个图案扔进水壶并搅拌一个小时一样简单,那么我根本不会问。命令模式在这里不适用(它不能解决我的任何问题),备忘录模式使情况变得更糟,也无济于事。
      猜你喜欢
      • 1970-01-01
      • 2011-03-19
      • 1970-01-01
      • 2013-11-17
      • 1970-01-01
      • 1970-01-01
      • 2010-10-10
      • 1970-01-01
      • 2016-04-14
      相关资源
      最近更新 更多