Planning on writing a set of blog articles on how to write a webs server.
So each phase will have a new version of the server with extra features. But starting very basic and working up to a reasonable (not apache good) server written in C++ that people can take and play with.
Before I go and show the code in an article I hope I can get some feedback here to make sure I don't do anything to stupid.
Version 3: Server Class
Version 2 started to test my bounds in having all the code in one file. So I am going to use this version to distribute the code into multiple classes. So that in subsequent articles I don't need to show code that is not changing.
Lets do the things that are not going to change much first:
Nissa.cpp
Server is called "Nissa" after Scandinavian Folklore.
#include "Server.h"
#include "PintHTTP.h"
#include <ThorsLogging/ThorsLogging.h>
#include <string>
#include <iostream>
int main(int argc, char* argv[])
{
loguru::g_stderr_verbosity = 9;
std::cout << PACKAGE_STRING << " Server\n";
int port = 8080;
if (argc > 1) {
port = std::stoi(argv[1]);
}
using ThorsAnvil::Nissa::Server;
using ThorsAnvil::Nissa::PintHTTP;
PintHTTP pintHTTP;
#define CERTIFICATE_INFO "/etc/letsencrypt/live/thorsanvil.dev/"
#ifdef CERTIFICATE_INFO
// If you have site certificate set CERTIFICATE_INFO to the path
// This will then create an HTTPS server.
using ThorsAnvil::ThorsSocket::CertificateInfo;
CertificateInfo certificate{CERTIFICATE_INFO "fullchain.pem",
CERTIFICATE_INFO "privkey.pem"
};
Server server{certificate, port, pintHTTP};
#else
// Without a site certificate you should only use an HTTP port.
// But most modern browsers are going to complain.
// See: https://letsencrypt.org/getting-started/
// On how to get a free signed site certificate.
Server server{port, pintHTTP};
#endif
server.run();
}
Pint.h
Which means the things they work with are "Pints" as the scoundrals like a tipple over Christmas.
#ifndef THORSANVIL_NISSA_PINT_H
#define THORSANVIL_NISSA_PINT_H
#include <ThorsSocket/SocketStream.h>
#include <functional>
namespace ThorsAnvil::Nissa
{
enum class PintResult {Done, More};
class Pint
{
public:
using SocketStream = ThorsAnvil::ThorsSocket::SocketStream;
using ServerAction = std::function<bool(SocketStream&&)>;
virtual ~Pint() {}
virtual PintResult handleRequest(SocketStream& stream) = 0;
};
}
#endif
PintHTTP.h
#ifndef THORSANVIL_NISSA_PINT_HTTP_H
#define THORSANVIL_NISSA_PINT_HTTP_H
#include "Pint.h"
namespace ThorsAnvil::Nissa
{
class PintHTTP: public Pint
{
using SocketStream = Pint::SocketStream;
using ServerAction = Pint::ServerAction;
public:
virtual PintResult handleRequest(SocketStream& stream) override;
};
}
#endif
PintHTTP.cpp
#include "PintHTTP.h"
#include <charconv>
using namespace ThorsAnvil::Nissa;
PintResult PintHTTP::handleRequest(SocketStream& stream)
{
std::size_t bodySize = 0;
bool closeSocket = true;
std::string line;
while (std::getline(stream, line))
{
std::cout << "Request: " << line << "\n";
if (line == "\r") {
break;
}
if (line == "Connection: keep-alive\r") {
closeSocket = false;
}
if (line.compare("Content-Length: ") == 0) {
std::from_chars(&line[0] + 16, &line[0] + line.size(), bodySize);
}
}
stream.ignore(bodySize);
if (stream)
{
stream << "HTTP/1.1 200 OK\r\n"
<< "Content-Length: 135\r\n"
<< "\r\n"
<< R"(<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html>
<head><title>Nissa server 1.1</title></head>
<body>Hello world</body>
</html>)";
stream.flush();
}
std::cerr << "Done\n";
return closeSocket ? PintResult::Done : PintResult::More;
}
JobQueue.h
The first interesting piece of code that pops out is the JobQueue. This basically controls the background threads that processes any accepted request.
I am deliberately keeping this class simple as I know that the next step is going to involve storing some state. So I don't want any state in the queue explicitly. So the JobQueue holds only actions that need to be taken.
#ifndef THORSANVIL_NISSA_JOB_QUEUE_H
#define THORSANVIL_NISSA_JOB_QUEUE_H
#include <functional>
#include <thread>
#include <mutex>
#include <condition_variable>
namespace ThorsAnvil::Nissa
{
using Work = std::function<void()>;
class JobQueue
{
using WorkQueue = std::queue<Work>;
std::vector<std::thread> workers;
std::mutex workMutex;
std::condition_variable workCV;
WorkQueue workQueue;
public:
JobQueue(int workerCount);
~JobQueue();
void addJob(Work&& action);
private:
Work getNextJob();
void processWork();
};
}
#endif
JobQueue.cpp
#include "JobQueue.h"
using namespace ThorsAnvil::Nissa;
JobQueue::JobQueue(int workerCount)
{
for (int loop = 0; loop < workerCount; ++loop) {
workers.emplace_back(&JobQueue::processWork, this);
}
}
JobQueue::~JobQueue()
{
for (auto& worker: workers) {
worker.join();
}
}
void JobQueue::addJob(Work&& action)
{
std::unique_lock lock(workMutex);
workQueue.emplace(std::move(action));
workCV.notify_one();
}
Work JobQueue::getNextJob()
{
std::unique_lock lock(workMutex);
workCV.wait(lock, [&](){return !workQueue.empty();});
Work work = std::move(workQueue.front());
workQueue.pop();
return work;
}
void JobQueue::processWork()
{
while (true)
{
Work work = getNextJob();
work();
}
}
Server.h
This leaves a very simple Server class definition.
#ifndef THORSANVIL_NISSA_SERVER_H
#define THORSANVIL_NISSA_SERVER_H
#include "Pint.h"
#include "JobQueue.h"
#include <ThorsSocket/Server.h>
#include <ThorsSocket/SocketStream.h>
#include <functional>
#include <thread>
#include <mutex>
#include <condition_variable>
namespace ThorsAnvil::Nissa
{
class Server
{
using SSLctx = ThorsAnvil::ThorsSocket::SSLctx;
using Listener = ThorsAnvil::ThorsSocket::Server;
using SocketStream = ThorsAnvil::ThorsSocket::SocketStream;
using CertificateInfo = ThorsAnvil::ThorsSocket::CertificateInfo;
SSLctx ctx;
Listener listener;
Pint& pint;
JobQueue jobQueue;
public:
Server(int port, Pint& pint, int workerCount = 1);
Server(CertificateInfo& certificate, int port, Pint& pint, int workerCount = 1);
void run();
private:
void connectionHandler(SocketStream& stream);
};
}
#endif
Server.cpp
#include "Server.h"
#include <ThorsSocket/Socket.h>
#include <ThorsSocket/SocketStream.h>
using namespace ThorsAnvil::Nissa;
Server::Server(int port, Pint& pint, int workerCount)
: ctx{ThorsAnvil::ThorsSocket::SSLMethodType::Server}
, listener{ThorsAnvil::ThorsSocket::ServerInfo{port}}
, pint{pint}
, jobQueue{workerCount}
{}
Server::Server(CertificateInfo& certificate, int port, Pint& pint, int workerCount)
: ctx{ThorsAnvil::ThorsSocket::SSLMethodType::Server, certificate}
, listener{ThorsAnvil::ThorsSocket::SServerInfo{port, ctx}}
, pint{pint}
, jobQueue{workerCount}
{}
template<typename T>
struct CopyOnMove
{
mutable T value;
CopyOnMove(T&& init)
: value(std::move(init))
{}
CopyOnMove(CopyOnMove const& copy)
: value{std::move(copy.value)}
{}
};
void Server::run()
{
using ThorsAnvil::ThorsSocket::Socket;
using ThorsAnvil::ThorsSocket::Blocking;
while (true)
{
Socket accept = listener.accept(Blocking::No);
SocketStream stream(std::move(accept));
jobQueue.addJob([&, StreamRef = CopyOnMove{std::move(stream)}]() mutable
{
SocketStream stream{std::move(StreamRef.value)};
connectionHandler(stream);
});
}
}
void Server::connectionHandler(ThorsAnvil::ThorsSocket::SocketStream& stream)
{
for (bool closeSocket = false; !closeSocket && stream.good();)
{
closeSocket = true;
if (pint.handleRequest(stream) == PintResult::More) {
closeSocket = false;
}
}
}