Context

在教程四中,我们已经实现了状态机,接下来我们定义一个Context类,其仅仅简单地封装一下,维持一个current变量指向当前的状态即可。我们需要在miniSMTPServer/context目录下创建context.hppcontext.cpp文件并修改CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// miniSMTPServer/context/context.hpp
#pragma once

#include "state.hpp"

#include <memory>

class Context {
private:
std::unique_ptr<State> *current;

public:
Context();
std::string transitive(std::vector<std::string> &parameters);
~Context() = default;
};
1
2
3
4
5
6
7
8
9
// miniSMTPServer/context/context.cpp
#include "context.hpp"

#include "state.hpp"

Context::Context() { current = &States::idleState; }
std::string Context::transitive(std::vector<std::string> &parameters) {
return (*current)->transitive(parameters, current);
}
1
2
3
# miniSMTPServer/context/CMakeLists.txt
add_library(context STATIC state.cpp context.cpp)
...

可以看出Context类十分的简单,这是因为大部分工作都已经实现了。

SMTP Server

现在,我们终于可以开始写我们的Server了,我们只需要完成一个简单得不能再简单的工作:按照空格分割字符串。然而你可能会想到最直接的方法就是定义一个istringstream。然而这是不正确的,因为可能会存在如下的情况:

1
2
MAIL shejialuo@gmail.com OK
MAIL shejialuo@gmail.com NOT OK

ABNF中严格地定义了只允许拥有一个空格。而所有命令的参数最多不超过1个。我们采取的策略就可以非常简单了,直接以一个空格分割字符串即可。但是值得注意的是,我们必须删除接收到的字符串的最后两个,根据协议其最后两个字符为CRLF,即\r\n

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// miniSMTPServer/miniSMTPServer.cpp
std::vector<std::string> getParameters(std::string &request) {
request.pop_back();
request.pop_back();
int split = 0;
for (; split < request.size(); split++) {
if (request[split] == ' ') {
break;
}
}

std::string command = request.substr(0, split);
if (split != request.size()) {
std::string parameter = request.substr(split + 1, request.size() - split - 1);
return {command, parameter};
}

return {command};
}

最后我们只需要修改主函数就可以了,其逻辑十分的简单:

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
// miniSMTPServer/miniSMTPServer.cpp
int main() {
std::cout << "Hello, This is a simple SMTP server\n";

TCPSocket socket{};
socket.set_reuseaddr();
socket.bind();
socket.listen();

std::string request{};
Context context{};

while (true) {
auto s = socket.accept();

bool isDone = false;
while (!isDone) {
s.read(request);
if (request.empty()) {
std::cout << "S: Connection lost\n";
s.close();
break;
}
std::cout << "C: " << request;
std::string parameter{};
std::vector<std::string> parameters = getParameters(request);

std::string result = context.transitive(parameters);

s.write(result + "\r\n");
std::cout << "S: " << result << std::endl;
if (result.substr(0, 3) == "221") {
s.close();
isDone = true;
}
}
}

return 0;
}

最后我们需要修改miniSMTPServer/CMakeLists.txt文件如下所示:

1
2
3
4
5
6
7
8
9
10
11
# miniSMTPServer/CMakeLists.txt
add_executable(miniSMTP miniSMTPServer.cpp)

set_target_properties(miniSMTP PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/)

target_include_directories(miniSMTP PRIVATE ./util ./context)

target_link_libraries(miniSMTP util context)

add_subdirectory(./util)
add_subdirectory(./context)

然后进行编译最终运行./miniSMTP。你可以尝试许多的输入输出,如果你发现了Bug,欢迎给仓库提交miniSMTPServer

1
git checkout finish-all

总结

你可能会发现我们没有对邮件进行任何的处理,之所以我不打算做这样的处理是因为这并不是我们应该做的重点。如果你想保存用户发送的邮件,是一个非常简单的事情。你可能会想是不是需要在transitive函数中做这样的处理。实际上最合理的方式是新增加一个doAction的虚函数。对于每一个状态其存在一个处理函数:

  • IdleState:清空邮件信息的缓存。
  • EhloState:清空邮件信息的缓存。
  • MailState:记录邮件的发送人。
  • RcptState:记录邮件的接收人。
  • DataStartState:记录发送的信息。
  • DataStartDone:如果收到了QUIT命令,将邮件保存到硬盘。

希望能够阅读到这儿的你,或多或少能有所收获。这是我第一次尝试写一个教程,终于明白了写一个教程的艰辛。希望你能有一天用自己的知识帮助到他人。