在教程三中,我们已经完成了IdleState
的操作,那么接下来的核心就是完成其他所有类的操作。为了方便读者的阅读,我们继续给出在教程二中绘制出了的状态图。
stateDiagram-v2
[*] --> idle: server start
idle --> ehlo: EHLO
idle --> idle: RSET
ehlo --> mail: MAIL
mail --> rcpt: RCPT
rcpt --> rcpt: RCPT
rcpt --> dataStart: DATA
dataStart --> dataStart : except .
dataStart --> dataDone: .
ehlo --> ehlo: RSET, EHLO
mail --> ehlo: RSET, EHLO
rcpt --> ehlo: RSET, EHLO
dataStart --> ehlo: RSET, EHLO
dataDone --> ehlo: RSET, EHLO
dataDone --> mail: MAIL
idle --> [*]: QUIT
ehlo --> [*]: QUIT
mail --> [*]: QUIT
rcpt --> [*]: QUIT
dataStart --> [*]: QUIT
dataDone --> [*]: QUIT
EhloState 对于EhloState
,首先我们应该考虑其能够接收的额外命令。由状态图可知,我们需要MAIL
命令。因此我们需要在allowed
中添加额外的MAIL
。
1 2 EhloState::EhloState () { allowed.insert ("MAIL" ); }
同时,我们也需要在isCorrectParameters
中添加对MAIL
命令参数的判断。我们给出MAIL
命令请求和响应的ABNF范式。在这个范式中,我省略了对邮件格式正确性的描述,因为我认为这不是这个教程的重点,在本教程中,我们统一使用如下的正则表达式regex = "(\\w+)(\\.|_)?(\\w*)@(\\w+)(\\.(\\w+))+"
来表示邮件的格式。
1 2 mail-request = "MAIL <source> CRLF" mail-ok-reponse = "250" SP "Requested mail action okay, completed" CRLF
因此,我们就可以修改isCorrectParameters
函数,添加对MAIL
命令的判断:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <regex> ... std::optional<std::string> State::isCorrectParameters (std::vector<std::string> ¶meters) { const std::regex pattern{"(\\w+)(\\.|_)?(\\w*)@(\\w+)(\\.(\\w+))+" }; ... else if (parameters[0 ] == "MAIL" ) { if (parameters.size () != 2 || !std::regex_match (parameters[1 ], pattern)) { return "501 " + codeToMessages["501" ]; } } return std::nullopt ; }
然后,我们添加相应的单元测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 TEST (State, isCorrectParametersMAIL) { std::vector<std::vector<std::string>> tests{ {"MAIL" }, {"MAIL" , "MAIL" }, {"MAIL" , "NOOP" , "MAIL" }, {"MAIL" , "shejialuo@gamil..com" }, {"EHLO" , "shejialuo" }, {"EHLO" , "shejialuo@.com.com" }, {"EHLO" , "shejialuo@123.1.cn" }, }; auto state = std::make_unique <IdleState>(); for (auto &&test : tests) { auto result = state->isCorrectParameters (test); ASSERT_TRUE (result.has_value ()); ASSERT_EQ (result.value (), "501 " + codeToMessages["501" ]); } std::vector<std::string> successful{"MAIL" , "shejialuo@gmail.com" }; ASSERT_FALSE (state->isCorrectParameters (successful).has_value ()); }
然后,我们仍然按照测试驱动的方式完成EhloState
状态下的操作,测试的代码有许多可以重用TEST(State, IdleStateTransitive)
。RSET
和EHLO
命令都不会改变其状态,与其说不改变而是从EhloState
到EhloState
。所以我们只需要关心MAIL
命令,MAIL
命令会转化到MailState
中,根据上述分析,我们可以得出如下的测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 TEST (State, EhloStateTransitive) { std::vector<std::vector<std::string>> tests{ {"RSET" }, {"NOOP" }, {"QUIT" }, {"EHLO" , "127.0.0.1" }, {"MAIL" , "shejialuo@gmail.com" }, {"RSTE" }, {"DATA" }, {"." }, {"RCPT" }, }; std::vector<std::pair<std::string, std::unique_ptr<State> *>> expects{ {"250 " + codeToMessages["250" ], &States::ehloState}, {"250 " + codeToMessages["250" ], &States::ehloState}, {"221 " + codeToMessages["221" ], &States::idleState}, {"250 " + codeToMessages["250" ], &States::ehloState}, {"250 " + codeToMessages["250" ], &States::mailState}, {"500 " + codeToMessages["500" ], &States::ehloState}, {"503 " + codeToMessages["503" ], &States::ehloState}, {"503 " + codeToMessages["503" ], &States::ehloState}, {"503 " + codeToMessages["503" ], &States::ehloState}, }; for (int i = 0 ; i < tests.size (); ++i) { auto state = std::make_unique <EhloState>(); std::unique_ptr<State> *current = &States::ehloState; std::string result = state->transitive (tests[i], current); EXPECT_EQ (result, expects[i].first); EXPECT_EQ (current, expects[i].second); } }
然后我们开始实现EhloState::transitive
方法。
1 2 3 4 5 6 7 8 9 10 11 12 std::string EhloState::transitive (std::vector<std::string> ¶meters, std::unique_ptr<State> *¤t) { if (auto result = transitiveHelper (parameters, current); result.has_value ()) { return result.value (); } if (parameters[0 ] == "MAIL" ) { current = &States::mailState; } return "250 " + codeToMessages["250" ]; }
你可能已经发现了,我们的代码实际上非常的整洁,比起复杂的if-else
,我们通过层层地抽象让代码变得十分的简单。你可以编译代码并执行测试。如果你的代码有问题你可以执行如下的命令切换到现在的状态:
1 2 3 4 cd build && make -j12 && ctest --output-on-failuregit checkout ehlo-state
MailState 对于MailState
,首先我们应该考虑其能够接收的额外命令。由状态图可知,我们需要RCPT
命令。因此我们需要在allowed
中添加额外的RCPT
。
1 2 MailState::MailState () { allowed.insert ("RCPT" ); }
同时,我们也需要在isCorrectParameters
中添加对RCPT
命令参数的判断。我们给出RCPT
命令请求和响应的ABNF范式,其与MAIL
命令类似,此处不赘述。
1 2 rcpt-request = "RCPT <source> CRLF" rcpt-ok-reponse = "250" SP "Requested mail action okay, completed" CRLF
因此,我们就可以修改isCorrectParameters
函数,添加对RCPT
命令的判断:
1 2 3 4 5 6 7 8 9 10 11 12 #include <regex> std::optional<std::string> State::isCorrectParameters (std::vector<std::string> ¶meters) { ... else if (parameters[0 ] == "MAIL" || parameters[0 ] == "RCPT" ) { if (parameters.size () != 2 || !std::regex_match (parameters[1 ], pattern)) { return "501 " + codeToMessages["501" ]; } } return std::nullopt ; }
由于在MAIL
命令中我们已经给出了很详细的单元测试,此处就忽略了单元测试。然后,我们仍然按照测试驱动的方式完成MailState
状态下的操作,RSET
和EHLO
命令都改变其状态让其回到EhloState
状态。对于RCPT
命令,RCPT
命令会令其转化到RCPTState
中,根据上述分析,我们可以得出如下的测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 TEST (State, MailStateTransitive) { std::vector<std::vector<std::string>> tests{ {"RSET" }, {"NOOP" }, {"QUIT" }, {"EHLO" , "127.0.0.1" }, {"RCPT" , "shejialuo@gmail.com" }, {"MAIL" , "shejialuo@gmail.com" }, {"DATA" }, {"." }, }; std::vector<std::pair<std::string, std::unique_ptr<State> *>> expects{ {"250 " + codeToMessages["250" ], &States::ehloState}, {"250 " + codeToMessages["250" ], &States::mailState}, {"221 " + codeToMessages["221" ], &States::idleState}, {"250 " + codeToMessages["250" ], &States::ehloState}, {"250 " + codeToMessages["250" ], &States::rcptState}, {"503 " + codeToMessages["503" ], &States::mailState}, {"503 " + codeToMessages["503" ], &States::mailState}, {"503 " + codeToMessages["503" ], &States::mailState}, }; for (int i = 0 ; i < tests.size (); ++i) { auto state = std::make_unique <MailState>(); std::unique_ptr<State> *current = &States::mailState; std::string result = state->transitive (tests[i], current); EXPECT_EQ (result, expects[i].first); EXPECT_EQ (current, expects[i].second); } }
然后我们开始实现MailState::transitive
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 std::string MailState::transitive (std::vector<std::string> ¶meters, std::unique_ptr<State> *¤t) { if (auto result = transitiveHelper (parameters, current); result.has_value ()) { return result.value (); } if (parameters[0 ] == "RCPT" ) { current = &States::rcptState; } else { current = &States::ehloState; } return "250 " + codeToMessages["250" ]; }
你可以编译代码并执行测试。如果你的代码有问题你可以执行如下的命令切换到现在的状态:
1 2 3 4 cd build && make -j12 && ctest --output-on-failuregit checkout mail-state
RcptState 对于MailState
,首先我们应该考虑其能够接收的额外命令。由状态图可知,我们需要RCPT
命令和DATA
命令。因此我们需要在allowed
中添加额外的RCPT
和DATA
。
1 2 3 4 5 RcptState::RcptState () { allowed.insert ("RCPT" ); allowed.insert ("DATA" ); }
我们已经处理了RCPT
命令的判断,剩下就是DATA
命令的判断,根据DATA
命令的ABNF范式,我们可以做很简单的处理操作。
1 2 data-request = "DATA CRLF" data-ok-reponse = "250" SP "Requested mail action okay, completed" CRLF
1 2 3 4 5 6 7 8 9 std::optional<std::string> State::isCorrectParameters (std::vector<std::string> ¶meters) { ... if (command == "NOOP" || command == "QUIT" || command == "RSET" || command == "DATA" ) { if (parameters.size () != 1 ) { return "501 " + codeToMessages["501" ]; } } }
然后,我们仍然按照测试驱动的方式完成RCPTState
状态下的操作,RSET
和EHLO
命令都改变其状态让其回到EhloState
状态。对于RCPT
命令,RCPT
命令并不会改变其状态。当其接收到DATA
命令后,其会转化为DataStartState
。根据上述分析,我们可以得出如下的测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 TEST (State, RCPTStateTransitive) { std::vector<std::vector<std::string>> tests{ {"RSET" }, {"NOOP" }, {"QUIT" }, {"EHLO" , "127.0.0.1" }, {"RCPT" , "shejialuo@gmail.com" }, {"MAIL" , "shejialuo@gmail.com" }, {"DATA" }, {"." }, }; std::vector<std::pair<std::string, std::unique_ptr<State> *>> expects{ {"250 " + codeToMessages["250" ], &States::ehloState}, {"250 " + codeToMessages["250" ], &States::rcptState}, {"221 " + codeToMessages["221" ], &States::idleState}, {"250 " + codeToMessages["250" ], &States::ehloState}, {"250 " + codeToMessages["250" ], &States::rcptState}, {"503 " + codeToMessages["503" ], &States::rcptState}, {"250 " + codeToMessages["250" ], &States::dataStartState}, {"503 " + codeToMessages["503" ], &States::rcptState}, }; for (int i = 0 ; i < tests.size (); ++i) { auto state = std::make_unique <RcptState>(); std::unique_ptr<State> *current = &States::rcptState; std::string result = state->transitive (tests[i], current); EXPECT_EQ (result, expects[i].first); EXPECT_EQ (current, expects[i].second); } }
然后我们开始实现RcptState::transitive
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 std::string RcptState::transitive (std::vector<std::string> ¶meters, std::unique_ptr<State> *¤t) { if (auto result = transitiveHelper (parameters, current); result.has_value ()) { return result.value (); } if (parameters[0 ] == "DATA" ) { current = &States::dataStartState; } else if (parameters[0 ] == "RCPT" ) { current = &States::rcptState; } else { current = &States::ehloState; } return "250 " + codeToMessages["250" ]; }
你可以编译代码并执行测试。如果你的代码有问题你可以执行如下的命令切换到现在的状态:
1 2 3 4 cd build && make -j12 && ctest --output-on-failuregit checkout rcpt-state
DataStartState & DataDoneState 对于DataStartState
,首先我们应该考虑其能够接收的额外命令。由状态图可知,服务器端只能接收.
命令,此时我们必须转换我们的思路了。我们原先的思路是通过transitiveHelper
来处理,但由于在接收用户发送的数据阶段,哪怕用户发送MAIL
,我们也不能将其作为命令。所以当且仅当服务器接收.
命令时,我们才能进入到DataDoneState
阶段。我们不能够重用任何以前的方法,哪怕用户输入了. .
我们都必须认为这是邮件的内容。因此我们直接修改transitive
方法:
1 2 3 4 5 6 7 8 std::string DataStartState::transitive (std::vector<std::string> ¶meters, std::unique_ptr<State> *¤t) { if (parameters[0 ] == "." && parameters.size () == 1 ) { current = &States::dataDoneState; return "250 " + codeToMessages["250" ]; } return "354 " + codeToMessages["354" ]; }
对于该方法的测试,我们需要考虑到更多的边界条件,首先我们必须要思考的是如何构建我们的测试,首先我们必须测试会不会解析除了.
以外的命令,其次就是考虑类似于. .
是否会解析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 TEST (State, DataStartStateTransitive) { std::vector<std::vector<std::string>> tests{ {"RSET" }, {"RSET" , "1" }, {"NOOP" }, {"NOOP" , "NOOP" }, {"QUIT" }, {"EHLO" , "127.0.0.1" }, {"RCPT" , "shejialuo@gmail.com" }, {"MAIL" , "shejialuo@gmail.com" }, {"DATA" }, {".." }, {"." , "." }, {"." }, }; std::vector<std::pair<std::string, std::unique_ptr<State> *>> expects{ {"354 " + codeToMessages["354" ], &States::dataStartState}, {"354 " + codeToMessages["354" ], &States::dataStartState}, {"354 " + codeToMessages["354" ], &States::dataStartState}, {"354 " + codeToMessages["354" ], &States::dataStartState}, {"354 " + codeToMessages["354" ], &States::dataStartState}, {"354 " + codeToMessages["354" ], &States::dataStartState}, {"354 " + codeToMessages["354" ], &States::dataStartState}, {"354 " + codeToMessages["354" ], &States::dataStartState}, {"354 " + codeToMessages["354" ], &States::dataStartState}, {"354 " + codeToMessages["354" ], &States::dataStartState}, {"354 " + codeToMessages["354" ], &States::dataStartState}, {"250 " + codeToMessages["250" ], &States::dataDoneState}, }; for (int i = 0 ; i < tests.size (); ++i) { auto state = std::make_unique <DataStartState>(); std::unique_ptr<State> *current = &States::dataStartState; std::string result = state->transitive (tests[i], current); EXPECT_EQ (result, expects[i].first); EXPECT_EQ (current, expects[i].second); } }
对于DataDoneState
由状态图可以观察出来,实际上其与EhloState
是一致的,之所以用两个状态进行区分,是为了方便读者的理解。故这部分就不赘述了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 DataDoneState::DataDoneState () { allowed.insert ("MAIL" ); } std::string DataDoneState::transitive (std::vector<std::string> ¶meters, std::unique_ptr<State> *¤t) { if (auto result = transitiveHelper (parameters, current); result.has_value ()) { return result.value (); } if (parameters[0 ] == "MAIL" ) { current = &States::mailState; } else { current = &States::ehloState; } return "250 " + codeToMessages["250" ]; }
你可以编译代码并执行测试。如果你的代码有问题你可以执行如下的命令切换到现在的状态:
1 2 3 4 cd build && make -j12 && ctest --output-on-failuregit checkout finished-states
小结 你应该能够发现,这一节反而是简单的一节。我们所做的绝大多数工作都是重复。其实最重要的过程一直在我们是如何抽象出State
这个虚基类的。希望读者阅读到这儿能够有所启发,理解到良好的架构对于代码的整洁和拓展性拥有很大的作用。