手把手教你写一个简单的SMTP服务器(三)
Contents
- 手把手教你写一个简单的SMTP服务器(一)
- 手把手教你写一个简单的SMTP服务器(二)
- 手把手教你写一个简单的SMTP服务器(三)
- 手把手教你写一个简单的SMTP服务器(四)
- 手把手教你写一个简单的SMTP服务器(五)
在教程二中我们已经完成了最重要的工作,构建了最基本的虚基类State。回顾在教程二中我们对于状态的定义,我们拟定义如下6个类来表征状态机中的状态:
IdleStateEhloStateMailStateRcptStateDataStartStateDataDoneState
1 | // miniSMTPServer/context/state.hpp |
然后我们需要实现其方法,在现在这个阶段,我们当让其做空操作,或者返回默认值。
1 | // miniSMTPServer/context/state.cpp |
根据教程二中设计,我们可以知道当服务器端收到客户端的命令时,其会调用transitive方法进行状态的转换,一个思路是我们可以使用make_unique方法构造一个完整的新类来表述状态,但是这样不是很高效。为了提高效率,我们应该使用全局的生命周期的变量,因此我们可以构造一个新类States包含指向6个已经存在的类的指针,我们使用unique_ptr管理这些指针,从而避免内存的泄漏。
1 | // miniSMTPServer/context/state.hpp |
1 | // miniSMTPServer/context/state.cpp |
完成了这一步,我们就可以修改State类的transitiveFromQuit方法:
1 | // miniSMTPServer/context/state.cpp |
至此,我们完成了我们的状态机的雏形。同时你也可以通过执行命令git checkout all-states获得上面的代码。
IdleState
当状态机一启动时,其应该位于IdleState状态,其能够接收4个命令:RSET,NOOP,QUIT以及EHLO命令。其中NOOP和QUIT已经统一处理了,我们并不需要关心。在后面的状态中,我们会忽略这两个命令。对于RSET命令来说,其在这个状态没有任何的作用(严格来说,不是没有作用,而是从IdleState转化为IdleState)。对于EHLO命令来说,其应该从IdleState状态转化为EhloState。在完成这个工作之前,我们首先使用ABNF范式定义RSET等命令的请求和响应,为了简单起见,本教程直接设置了一个静态的”127.0.0.1”。
1 | rset-request = "RSET" CRLF |
因此,我们首先更新State基类中的isCorrectParameters方法。其操作很简单,对于NOOP,QUIT和RSET而言,这些命令都不需要任何的参数。而对于EHLO命令,我们直接严格按照ABNF定义即可。
1 | std::optional<std::string> State::isCorrectParameters(std::vector<std::string> ¶meters) { |
你可能觉得这个功能很简单,但是就我个人而言,我认为此处我们应该写一点单元测试保证这个函数的正确性。显然,用cpp写单元测试也是相当的繁琐。首先,你需要在miniSMTPServer/context目录下创建一个新的目录tests。然后在tests目录下添加如下的文件:
CMakeLists.txtstateTest.cpp
对于位于miniSMTPServer/context/tests目录下的CMakeLists.txt,添加如下的代码:
1 | # miniSMTPServer/context/tests/CMakeLists.txt |
然后修改位于miniSMTPServer/context目录下的CMakeLists.txt文件,添加如下的代码:
1 | # miniSMTPServer/context/CMakeLists.txt |
然后我们就可以开始写单元测试了,我们目前的单元测试主要关心State::isCorrectParameters方法。
1 | // miniSMTPServer/context/tests/stateTest.cpp |
上述代码提供的单元测试十分简单。不需要进行讲解,从后面开始,我们会以测试驱动来撰写代码,也就是我们先写测试文件再写相应的功能,当我们的代码能够通过测试后也就证明我们的代码是正确的。
现在我们要开始实现IdleState类中的方法。首先我们应该思考IdleState中允许存在什么命令,由上面的讲述可知,IdleState允许的命令与State中的allowed一致,所以对其构造函数我们可以不做任何的处理,那么关键的地方就在于transitive方法的实现。我们首先实现如下的单元测试的代码:
1 | // miniSMTPServer/context/tests/stateTest.cpp |
测试的代码很简单,你可以编译然后使用ctest --output-on-failure可以发现很多错误,那么就让我们来更新IdleState::transitive方法,其更新的很简单,首先使用基类的方法transitiveHelper。然后只需要处理EHLO命令即可,因为QUIT和NOOP命令已经在transitiveHelper方法实现了,而RSET方法对于idleState没有任何作用。同时我也添加了许多其他测试。
1 | // miniSMTPServer/context/tests/stateTest.cpp |
再完成了测试代码的编写后,我们开始修改IdleState::transitive方法:
1 | // miniSMTPServer/context/state.cpp |
然后我们编译代码运行测试:
1 | cd build && make -j12 && ctest --output-on-failure |
其结果如下:
1 | 80% tests passed, 1 tests failed out of 5 |
产生这样的结果并不是我们代码的问题,而是由于commands变量的定义在教程二中没有完全定义,因此修改其定义如下:
1 | // miniSMTPServer/context/state.cpp |
然后我们重新编译代码运行测试:
1 | cd build && make -j12 && ctest --output-on-failure |
你会发现我们能够通过所有的测试。
小结
本节我们主要完善了我们的状态机并实现了第一个状态IdleState的操作,并通过测试驱动的方式写出了我们的代码。在后续的教程中我们将继续完善其他的状态操作。如果你产生了任何的问题,你可以运行如下的命令:
1 | git checkout idle-state |