27

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

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

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

当前的服务器端实现使用class Workerwith 方法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负责解析请求和编写响应的 current )。可能有一个可互换的class ProtocolSyntax,它处理那些协议语法细节,但仍然使用我们新的闪亮的结构化命令处理程序。

4

6 回答 6

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);
  }
}
于 2012-08-12T17:36:44.563 回答
5

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

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));
于 2012-08-10T05:41:51.953 回答
3

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

对于协议消息实现的选择,我个人最喜欢与 C++ 配合得很好的google 协议缓冲区,我们现在将它用于几个项目。

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

编辑:

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

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

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

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

当然,您不应该以处理各种可能请求的单个接口结束,而应以多个专门处理消息类别(扩展点)的接口结束。

于 2012-08-10T17:31:03.247 回答
2

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

于 2012-08-10T14:54:32.280 回答
1

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

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

  1. 类中可能有一些数据字段Worker仅由一组方法或几个(但不是每个)组使用。例如,ifm_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_uniquenew SomeClass之类的模板来代替。或者,如果需要“可互换的协议实现”,其中一种可能性是使用工厂而不是一些(或全部)运算符。new SomeClass

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

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

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

于 2012-08-12T13:47:26.417 回答
1

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

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

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

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

于 2012-08-14T10:08:14.840 回答