+----------------------------+
| server_main.cc +-----------------------+
+-------------+--------------+ get parsed|
| config |
| |
v v
+----------------------------+ +-------------+--------------+
+----+ server.h +----+ | config_parser.h |
| +----------------------------+ | +----------------------------+
| |
init a session | setup dispatcher|
object for each| based on parsed |
request v config v
+-------------+--------------+ +--------------+--------------+
| session.h +------->+ dispatcher.h |
+-------------+--------------+ get +--------------+--------------+
| hdlr |
get parsed | return an |
request v instance of v
+-------------+--------------+ +--------------+--------------+
| request_parser.h | | request_handler.h |
+----------------------------+ | ++request_handler_static.h |
| ||request_handler_echo.h |
| ||request_handler_error.h |
| ||request_handler_status.h |
| ||request_handler_proxy.h |
| ||request_handler_meme.h |
+-----------------------------+
The primary function of this program is to set up a server that can do simple echoing, serve a static file, return the request stats, and show the error page. The program is written in C++ and Boost library. The chart above shows the basic layout of the source code. The main file is server_main.cc, which sets up the server object and parses the config file. The config file parsing is done by config_parser.h. Upon the initialization of the server, the server object creates a dispatcher. The dispatcher is generated based on the config files, it maps a path to a specific handler. For each incoming requestion, a session would be generated. Each HTTP request is passed through the request_parser.h. And the dispatcher would return its corresponding handler to each session.
Although logging is not shown in the chart above, it is used in basically every file to record some important steps of the server. logger.h
contains the schema of the logger class. It adopts singleton design pattern to make sure there is only one running instance of the logger. The logger has a specific method for different level the of severity and it has a specific logging function for HTTP request.
void logTraceHTTPrequest(request http_request, tcp::socket& m_socket);
This method above the signature for logging HTTP class.
This folder contains several classes related to HTTP (not shown on the graph):
header.h
: Defines struct for common header formatrequest.h
: Defines struct for HTTP requestsreply.h
: Defines for HTTP repliesrequest_parser.h
: Parses HTTP requestsmime_types.h
: Provides methods for converting file extensions to MIME types
Defines class server
. A server object is initiated
Defines class session
. A session object is initiated by the server upon receiving a new request.
Defines class RequestHandlerDispatcher
. Each server object initializes and owns a RequestHandlerDispatcher
class. Given a request
object, the dispatcher returns a base class pointer to a new handler object that has been registered using longest prefix matching.
Defines virtual base class RequestHandler
. Given a request
object, the handlers return a pointer to a new reply
object. The current implementation includes several kinds of handlers that implement this interface:
request_handler_echo.h
: Defines classRequestHandlerEcho
. It returns aresponse
whose body field resembles therequest
.request_handler_static.h
: Defines classRequestHandlerEcho
. It returns aresponse
containing the requested file with corresponding MIME type formats.request_handler_status.h
: Defines classRequestHandlerStatus
. It returns aresponse
containing the current server status (request history, registered handlers, etc).request_handler_error.h
: Defiles classRequestHandlerError
. It returns aresponse
containing a 404 HTML page. This handler is registered with path/
, so this handler will be invoked for requests to unspecified paths.
Use cmake to perform out-of-source build:
$ mkdir -p build
$ cd build
$ cmake ..
$ make
There are two ways to run tests:
- go to the
build
directory of the project and runmake test
- go to the
tests
directory of the project and run../build/bin/<test_foo>
Currently, there are in total 6 test binary files, config_parser_test
, logger_test
, reply_test
, request_handler_test
, request_parser_test
, session_test
There are now unit tests for 6 classes. The following is a description about what each of them is about.
- reply_test: It tests whether the reply generated given a status code is valid and correct.
- request_parser_test: It tests whether the parser can report invalid HTTP request and convert valid request string to request object with the correct headers and body.
- config_parser_test: It tests whether the parser can correctly parse the config file to a NginxConfig object.
- logger_test: It tests whether the string output by logger is as expected in each severity.
- session_test: This is a mock test that tests whether the session object passes valid/invalid requests to request handler and shuts down afterwards correctly.
- request_handler_test: It tests whether the request handler handles all types of requests (echo, static, status) correctly and returns the correct HTTP response object.
The integration test for this project is implemented by shell script /tests/test.sh
. To run it separately, run it from the /build
directory:
cd build && ../tests/test.sh
The Integration test consists of three tests:
- A successful echo response of a valid request
- A failed echo response because of insufficient request headers
- A failed echo response because of invalid request body
To run the server, run the following command from the root directory
./build/bin/server ./conf/http.conf
You can use your own configuration file as the argument for the program.
Here we use the StatusHandler
class as an example to demonstrate how to add a new request handler to the server.
- Declare the new status handler, specify the url path and other attributes (if any) for
StatusHandler
in the HTTP config file
# File: conf/http.conf
handler status{
location /status;
}
- Define the class for the new request handler. The
StatusHandler
class here uses public inheritance to inherit theRequestHanlder
base class and overrides thehandleRequest
method. The header filerequest_handler_status.h
should be ininclude/request_handler
folder and the .cc filerequest_handler_status.cc
should be insrc/request_handler
folder.
/* File: include/request_handler/request_handler_status.h */
#include <iostream>
#include "request_handler.h"
// Inhertis the base class
class RequestHandlerStatus : public RequestHandler {
public:
// Define the constructor of RequestHandlerStatus class. In our implementation
// we simply do nothing, you can initialize attributes or call other methods
// in your own request handler
explicit RequestHandlerStatus(const NginxConfig &config) {}
// Override the handlerRequest method
std::unique_ptr<reply> handleRequest(const request &request_) noexcept override;
// Define other methods of status request handler
std::string requestToString(const request &request_);
...
};
In the .cc file we implement the methods of StatusHandler
:
/* File: request_handler_status.cc */
#include <iostream>
#include <cassert>
// Include the request_handler_status.h file
#include "request_handler/request_handler_status.h"
// Include other header files if needed
#include "request_handler_dispatcher.h"
#include "session.h"
std::unique_ptr<reply> RequestHandlerStatus::handleRequest(const request &request_) noexcept {
// Implement the handleRequest method here
...
}
std::string RequestHandlerStatus::requestToString(const request &request_) {
// Implement the requestToString method here
...
}
- In
request_handler_dispatcher.h
, add the handler type for the new added status handler
/* File: include/request_handler_dispatcher.h */
// Handler types already defined
static const HandlerType StaticHandler = "static";
static const HandlerType EchoHandler = "echo";
...
// Add HandlerType for status handler
static const HandlerType StatusHandler = "status";
- Include the
request_handler_status.h
header file inrequest_handler_dispatcher.cc
, add the option forStatusHandler
in the matching of prefixs and request handlers inRequestHandlerDispatcher::getRequestHandler
/* File: src/request_handler/request_handler_dispatcher.cc */
// Handlers already defined
#include "request_handler_dispatcher.h"
#include "request_handler/request_handler.h"
...
// Include the new added request handler
#include "request_handler/request_handler_status.h"
...
// Add an matching option in getRequestHandler method
std::unique_ptr<RequestHandler>
RequestHandlerDispatcher::getRequestHandler(const request &request_) const {
// Other codes
...
if (handler_configs_.find(matched_prefix) == handler_configs_.end()) {
// Failed to match to any handler
NginxConfigStatement empty_statement;
return std::make_unique<RequestHandlerError>(
NginxConfig()
);
}
else if (handler_configs_.find(matched_prefix)->second->tokens_[1] == StaticHandler) {
return std::make_unique<RequestHandlerStatic>(
*(handler_configs_.find(matched_prefix)->second->child_block_),
matched_prefix
);
}
...
// Add the matching of StatusHandler's prefix and class object
else if (handler_configs_.find(matched_prefix)->second->tokens_[1] == StatusHandler) {
return std::make_unique<RequestHandlerStatus>(
*(handler_configs_.find(matched_prefix)->second->child_block_)
);
}
// Other codes
...
}
- Append the new handler
src/request_handler_status.cc
to the argument list ofadd_library(request_handler ...)
inCMakeLists.txt
# File: CMakeLists.txt
add_library(request_handler
src/request_handler_dispatcher.cc
src/request_handler/request_handler_static.cc
src/request_handler/request_handler_echo.cc
src/request_handler/request_handler_error.cc
# link the new request handler class
src/request_handler/request_handler_status.cc
src/http/mime_types.cc)
- Add unit tests for the new request handler in
test/request_handler_test.cc
/* File: test/request_handler_test.cc */
// Other includes
...
// Include the header file of new request handler
#include "request_handler/request_handler_static.h"
...
// Other codes
...
class RequestHandlerTest : public :: testing::Test {
protected:
// Other member variables
...
// Add a NginxConfig object for the new handler
NginxConfig status_handler_config;
...
}
...
// Add unit test the new handler
Note that the instruction above is the minimum that you have to do to build successfully. If there are more dependencies for the class, you need to include them in each step as well. Also, these libs are required:
- Boost::system
- Boost::filesystem
- Boost::regex