C/C++ 思考:策略模式在协议解析中的应用

引出问题

在基于消息包的通信协议中,通常会通过一个id或命令名来标识该消息包,程序需要根据不同的标识进行不同的解析策略,提取出想要的内容。例如,一个典型的FTP请求命令是这样的:

USER anonymous\r\n

其中,"USER"是请求命令名,"anonymous"是该命令跟着的参数,"\r\n"是一条FTP命令结尾。

对应FTP请求格式:

命令<SP>参数<CRLF>

传统解析方式

如何从一条FTP请求命令中,解析出命令内容呢?
最直接地,根据空格字符(' ')将请求命令的字符串切分成两部分:命令名、命令参数。然后,将解析出的命令名与已知支持的命令名进行比较,如果匹配上,就调用对应的命令处理程序;如果没匹配上,就调用未知命令异常处理。

于是,我们写出如下代码,对FTP请求命令进行解析:

void handleCommand(const std::string& ftpText)
{
    std::string name; // 命令名
    std::string arg;  // 命令参数
    [name, arg] = str_split(ftpText, ' '); // 自定义切分函数, 将ftpText按空格切分成2个子字符串
    
    if (name == "USER") {
        handleUSER(arg);
    }
    else if (name == "PASS") {
        handlePASS(arg);
    }
    else if (name == "CWD") {
        handleCWD(arg);
    }
    ...
    else {
    // Unkown command
        handleUnknownCommand(arg);
    }
}

策略模式

上面代码能正常运行,但存在2个问题:
1)太繁琐,看起来很糟糕,实际上处理逻辑很简单,都是匹配到命令名就调用对应处理函数;
2)如果要添加对新命令的支持,就要到代码中去添加修改,如果涉及到多个命令,可能出错;

既然都是 “匹配命令 => 调用命令处理程序” 这种模式,能不能将这部分固定下来,而将可能变动的命令名、命令处理函数抽象出来?
答案是可以的,可以用策略模式进行简化。

先复习一下策略模式。

简介

策略模式是一种对象行为模式。

意图:定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。

适用场景:
1)许多相关的类,仅仅是行为不同,比如一种策略对应一个类。
2)需要使用一个算法的不同变体。
3)算法使用客户不应该知道的数据。
4)一个类定义多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现。

策略模式,特别适合用来解决if-else / switch 条件数目不确定的情况,便于用户扩展新的情形。

UML类图

UML类图如下:

其中,

  • Strategy(策略)
    定义所有支持的算法的公共接口。Context用该接口调用某个具体的算法ConcreteStrategy。

  • ConcreteStrategy(具体策略)
    实现Stragegy接口的某个具体算法。

  • Context(上下文)
    使用ConcreteStrategy对象,利用对应算法解决问题的上下文。

改进1:基于函数的代码结构改进

根据策略模式,我们用一个结构体将命令名、命令处理函数用结构体包装起来,然后将支持的命令名、处理函数放到一个数组中。改进代码如下:

typedef struct {
    std::string name;
    void (*handler)(const std::string&);
} FtpCommand_t;

#define COMMANDS_ARRAY_SIZE 10    // 根据实际情况决定数组大小
static FtpCommand_t commands[COMMANDS_ARRAY_SIZE] = {
    {"USER", handleUSER},
    {"PASS", handlePASS},
    {"CWD",  handleCWD},
    ...
};

void handleCommand(const std::string& ftpText)
{
    std::string name; // 命令名
    std::string arg;  // 命令参数
    [name, arg] = str_split(ftpText, ' ');

    for (int i = 0; i < COMMANDS_ARRAY_SIZE; ++i) {
        if (commands[i].name == name) { // 命令名匹配
            commands[i].handler(arg);   // 回调命令处理函数
            break;
        }
    }
}

当要添加、修改或删除一个命令及处理时,就可以直接在支持的命令数组commands[]中修改,而不用修改命令名匹配、回调代码。
注意:这种方式并不会提高程序运行效率,但会让程序结构清晰、易懂,维护更简易。

问题:为何改进中,没有任何策略类,却可称作策略模式?
策略模式的核心,是定义一系列算法,根据不同case,来选择使用不同的、可相互替换的算法。至于每个算法,是用类的形式,还是函数的形式,并非关键。而且,在C++中,函数也可以看作一种特殊的类(函数类)。
上面代码中,每种FTP命令,都对应了一种具体的解析策略,或称算法。策略模式的关键,是根据不同命令名的case,选择不同的解析策略(handlerxxx函数)对FTP请求消息进行解析。

改进2:基于对象的结构改进

C++中,更多时候,处理的代码是类的成员函数,而不是普通函数。
此时,可将前面的结构体数组 转换成一个static vector<> handlers_,然后遍历handleCommand,匹配到命令名就调用对应的命令处理函数即可。
也许,解析FTP消息功能,会包含在像下面FtpSession类中:

class FtpSession
{
public:
    using HandlerFuncType = void (FtpSession::*)(const std::string &);
    using HandlerItemType = std::pair<const std::string, HandlerFuncType>;
    ...
    void handleCommand(const std::string& ftpText);
    
private:
    void handleUSER(const std::string& arg);
    void handlePASS(const std::string& arg);
    void handleCWD(const std::string& arg);
    ...
    static std::vector<HandlerItemType> const handlers_; // 注意handlers_是static属性
};

static成员handlers_,是建立FTP命令名到解析命令的策略的映射的关键:

std::vector<FtpSession::HandlerItemType> const FtpSession::handlers_ = {
        { "USER", &FtpSession::handleUSER },
        { "PASS", &FtpSession::handlePASS },
        { "CWD",  &FtpSession::handleCWD },
        ...
}

策略模式本质,是在上下文中,根据不同case,选择不同算法的策略,对协议进行解析:

void FtpSession::handleCommand(const std::string& ftpText)
{
    std::string name; // 命令名
    std::string arg;  // 命令参数
    [name, arg] = str_split(ftpText, ' ');

    auto item = std::find_if(handlers_.begin(), handlers_.end(), [&name](const HandlerItemType& e) {
        return name == e.first;
    });
    if (item == handlers_.end()) { // fail
        handlerUnknownCommand();
    }
    else {
        auto const hanlder = item->second;
        (this->*handler)(arg); // Callback command handler // 注意这里的this->*handler
    }
}

为什么要用this->*handler进行回调?
因为handlers_.second存放的是void (FtpSession::)(const std::string &),也就是FtpSession成员函数地址,这是一个指针,this->handler表示对当前this对象的成员函数。
通过这种方式,巧妙地将对象的成员通过static成员,转发给当前对象的其他函数。

参考

https://github.com/mtheall/ftpd

posted @ 2023-03-29 21:11  明明1109  阅读(218)  评论(0编辑  收藏  举报