基于请求的协议实现的更好结构

时间:2012-08-08 00:35:37

标签: c++ design-patterns

我正在使用协议,这基本上是一个请求& 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;
    }


};

因此。问题是:我的worker类现在有大约130个“命令处理程序方法”。每个人都需要访问:

  1. 请求参数
  2. 响应对象(写入响应数据)
  3. 不同的其他会话本地对象(如数据库句柄,授权/权限查询句柄,日志记录,服务器各种子系统的句柄等)。
  4. 更好地构建这些命令处理程序方法的好策略是什么?

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

    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可以只引用一个引用(或指针)到worker对象本身(而不是引用请求,响应等)。但这也会增加另一个间接(this-&gt; worker-&gt; session而不是this-&gt; session)。这种间接是好的,如果它会购买一些优势。

    有关整体架构的一些信息

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

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

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

    摘要&amp;澄清:我的实际问题

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

    最后,它肯定会是妥协,但我对任何想法感兴趣!

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

6 个答案:

答案 0 :(得分:16)

你已经掌握了大部分正确的想法,这就是我将如何进行的。

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

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 :(得分:5)

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

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));

答案 2 :(得分:4)

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

对于协议消息实现的选择,我个人最喜欢google protocol buffers,它与C ++非常兼容,我们现在正在将它用于几个项目。

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

修改

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

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

我不知道如何以更好的方式解释这个问题,或者用这种方法展示如何改进现有代码的具体示例。我担心你已经在消息格式的de / /序列化代码上做了一些努力,使用谷歌protobuf消息(或者RequestResponse是什么类型的消息可以避免)

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

当然,你不应该在一个处理各种可能请求的接口中,而是一些专门处理消息类别(扩展点)的接口。

答案 3 :(得分:2)

我认为这是类似REST的实现的理想情况。另一种方法也可以是基于category / any-other-criteria将处理程序方法分组到多个worker类。

答案 4 :(得分:1)

如果协议方法只能按类型分组,但同一组的方法在实现中没有任何共同点,那么可能唯一可以做的就是在不同文件之间分配方法,一组文件用于组

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

  1. Worker类中可能有一些数据字段仅由一组方法或多个(但不是每个)组使用。例如,如果m_AuthHandle只能由用户管理和会话管理方法使用。
  2. 可能有一些输入参数组,由某些组的每个方法使用。
  3. 可能有一些常见数据,通过某些组的每种方法写入响应。
  4. 可能有一些常见的方法,由某些组的几种方法调用。
  5. 如果其中一些事实属实,则有充分的理由将这些功能分组到不同的类中。每个命令处理程序不是一个类,而是每个事件组一个类。或者,如果存在多个组共有的特征,则为类的层次结构。

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

    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。或者,如果&#34;可互换协议实施&#34;需要,其中一种可能性是使用工厂而不是一些(或所有)new SomeClass运算符。

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

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

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

答案 5 :(得分:1)

命令模式是解决此问题的两个方面的解决方案。

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

然后使用ICommand接口以及在单独的类中实现的每个Command方法以相同的方式实现命令。你几乎就在那里。将现有方法拆分为新的专业类。

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