first commit: echo server
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
/* ************************************************************************** */
|
||||
/* */
|
||||
/* ::: :::::::: */
|
||||
/* ByteBuffer.hpp :+: :+: :+: */
|
||||
/* +:+ +:+ +:+ */
|
||||
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
|
||||
/* +#+#+#+#+#+ +#+ */
|
||||
/* Created: 2026/08/25 09:30:45 by acossari #+# #+# */
|
||||
/* Updated: 2026/08/25 13:26:00 by acossari ### ########.fr */
|
||||
/* */
|
||||
/* ************************************************************************** */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
namespace webserv {
|
||||
|
||||
class ByteBuffer final {
|
||||
public:
|
||||
// The fixed 64 KiB capacity is also the maximum request-head size. It
|
||||
// leaves room for large headers while keeping memory bounded. The server
|
||||
// accepts at most 256 simultaneous connections, each with one receive and
|
||||
// one send buffer. In the worst-case scenario, their storage is 32 MiB, a
|
||||
// modest fixed ceiling for all connection buffers combined.
|
||||
// The C++23 uz suffix makes 64 a std::size_t, so multiplication occurs in
|
||||
// std::size_t instead of int and avoids implicit widening (clang-tidy).
|
||||
static constexpr std::size_t CAPACITY = 64uz * 1024;
|
||||
|
||||
std::span<const char> readable() const {
|
||||
return {storage_.data() + readPos_, writePos_ - readPos_};
|
||||
}
|
||||
|
||||
std::span<char> writableTail() {
|
||||
return {storage_.data() + writePos_, CAPACITY - writePos_};
|
||||
}
|
||||
|
||||
// Makes count bytes already written in writableTail() readable. count must
|
||||
// not exceed writableTail().size().
|
||||
void commit(std::size_t count);
|
||||
|
||||
// Discards count bytes from the start of readable(). count must not exceed
|
||||
// readable().size().
|
||||
void consume(std::size_t count);
|
||||
|
||||
void compact();
|
||||
|
||||
private:
|
||||
// C++17 guaranteed copy elision constructs this vector directly in
|
||||
// storage_, with no temporary vector or move.
|
||||
std::vector<char> storage_ = std::vector<char>(CAPACITY);
|
||||
|
||||
// Only bytes from readPos_ up to, but not including, writePos_ are live.
|
||||
// Values outside this range can remain in storage_ and are ignored.
|
||||
std::size_t readPos_ = 0;
|
||||
std::size_t writePos_ = 0;
|
||||
};
|
||||
|
||||
} // namespace webserv
|
||||
@@ -0,0 +1,71 @@
|
||||
/* ************************************************************************** */
|
||||
/* */
|
||||
/* ::: :::::::: */
|
||||
/* Connection.hpp :+: :+: :+: */
|
||||
/* +:+ +:+ +:+ */
|
||||
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
|
||||
/* +#+#+#+#+#+ +#+ */
|
||||
/* Created: 2026/08/28 09:21:04 by acossari #+# #+# */
|
||||
/* Updated: 2026/08/29 18:07:36 by acossari ### ########.fr */
|
||||
/* */
|
||||
/* ************************************************************************** */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "net/ByteBuffer.hpp"
|
||||
#include "net/FileDescriptor.hpp"
|
||||
|
||||
namespace webserv {
|
||||
|
||||
class Connection final {
|
||||
public:
|
||||
enum class Lifecycle { Alive, Done, Fatal };
|
||||
|
||||
struct Outcome {
|
||||
Lifecycle lifecycle;
|
||||
std::uint32_t desiredEvents;
|
||||
};
|
||||
|
||||
// ByteBuffer::CAPACITY is 64 KiB.
|
||||
// At most 16 KiB are copied per 48 KiB appended: 16 / 48 = 1 / 3.
|
||||
// A lower threshold reduces copying but leaves less unread work available.
|
||||
// The 16 KiB value is a fixed compromise, not a measured optimum.
|
||||
static constexpr std::size_t LOW_WATERMARK = 16uz * 1024;
|
||||
|
||||
explicit Connection(FileDescriptor socket);
|
||||
|
||||
// Each Connection has a unique identity and remains in its original slot.
|
||||
Connection(const Connection&) = delete;
|
||||
Connection& operator=(const Connection&) = delete;
|
||||
Connection(Connection&&) = delete;
|
||||
Connection& operator=(Connection&&) = delete;
|
||||
|
||||
int fd() const noexcept { return socket_.get(); }
|
||||
|
||||
Outcome onClientEvent(std::uint32_t events);
|
||||
|
||||
std::uint32_t desiredEvents() noexcept;
|
||||
|
||||
void close() noexcept { socket_.reset(); }
|
||||
|
||||
private:
|
||||
// recv() returning zero means the client has finished sending data.
|
||||
// readClosed_ records this. receive() still returns true because this
|
||||
// is not an error and the socket may still send its buffered response.
|
||||
// Only a recv() error returns false.
|
||||
bool receive();
|
||||
bool transmit();
|
||||
// TEMP echo path: queues received bytes unchanged for sending back to
|
||||
// the client.
|
||||
void forwardReceivedBytes();
|
||||
|
||||
FileDescriptor socket_;
|
||||
ByteBuffer receiveBuffer_;
|
||||
ByteBuffer sendBuffer_;
|
||||
bool readClosed_ = false;
|
||||
};
|
||||
|
||||
} // namespace webserv
|
||||
@@ -0,0 +1,61 @@
|
||||
/* ************************************************************************** */
|
||||
/* */
|
||||
/* ::: :::::::: */
|
||||
/* FileDescriptor.hpp :+: :+: :+: */
|
||||
/* +:+ +:+ +:+ */
|
||||
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
|
||||
/* +#+#+#+#+#+ +#+ */
|
||||
/* Created: 2026/08/24 16:24:03 by acossari #+# #+# */
|
||||
/* Updated: 2026/08/24 20:05:59 by acossari ### ########.fr */
|
||||
/* */
|
||||
/* ************************************************************************** */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <exception>
|
||||
#include <string>
|
||||
|
||||
namespace webserv {
|
||||
|
||||
// Sole owner of one file descriptor, move-only. Copying would give two owners
|
||||
// the same number, and the second close would land on whatever the kernel had
|
||||
// handed out in the meantime, killing an unrelated connection.
|
||||
class FileDescriptor final {
|
||||
public:
|
||||
class Error : public std::exception {
|
||||
public:
|
||||
explicit Error(std::string message);
|
||||
const char* what() const noexcept override;
|
||||
|
||||
private:
|
||||
std::string msg_;
|
||||
};
|
||||
|
||||
FileDescriptor() = default;
|
||||
|
||||
// Takes ownership and sets FD_CLOEXEC, so a successful execve() in a
|
||||
// forked child closes the descriptor before the new program starts. A
|
||||
// negative fd is accepted and gives an invalid object instead of throwing,
|
||||
// so the result of socket() can be passed in and checked with valid().
|
||||
explicit FileDescriptor(int fd);
|
||||
|
||||
~FileDescriptor();
|
||||
|
||||
FileDescriptor(const FileDescriptor&) = delete;
|
||||
FileDescriptor& operator=(const FileDescriptor&) = delete;
|
||||
FileDescriptor(FileDescriptor&& other) noexcept;
|
||||
FileDescriptor& operator=(FileDescriptor&& other) noexcept;
|
||||
|
||||
int get() const noexcept { return fd_; }
|
||||
bool valid() const noexcept { return fd_ >= 0; }
|
||||
|
||||
void setNonBlocking();
|
||||
|
||||
// Closes now instead of at destruction. Idempotent.
|
||||
void reset() noexcept;
|
||||
|
||||
private:
|
||||
int fd_ = -1;
|
||||
};
|
||||
|
||||
} // namespace webserv
|
||||
@@ -0,0 +1,48 @@
|
||||
/* ************************************************************************** */
|
||||
/* */
|
||||
/* ::: :::::::: */
|
||||
/* Listener.hpp :+: :+: :+: */
|
||||
/* +:+ +:+ +:+ */
|
||||
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
|
||||
/* +#+#+#+#+#+ +#+ */
|
||||
/* Created: 2026/08/27 15:52:02 by acossari #+# #+# */
|
||||
/* Updated: 2026/09/01 18:10:13 by acossari ### ########.fr */
|
||||
/* */
|
||||
/* ************************************************************************** */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <sys/socket.h>
|
||||
|
||||
#include <exception>
|
||||
#include <string>
|
||||
|
||||
#include "net/FileDescriptor.hpp"
|
||||
|
||||
namespace webserv {
|
||||
|
||||
class Listener final {
|
||||
public:
|
||||
class Error : public std::exception {
|
||||
public:
|
||||
explicit Error(std::string message);
|
||||
const char* what() const noexcept override;
|
||||
|
||||
private:
|
||||
std::string msg_;
|
||||
};
|
||||
|
||||
explicit Listener(const sockaddr_storage& address);
|
||||
|
||||
int fd() const noexcept { return fd_.get(); }
|
||||
|
||||
// Delegates the validity check without exposing the owned FileDescriptor.
|
||||
bool valid() const noexcept { return fd_.valid(); }
|
||||
|
||||
void close() noexcept { fd_.reset(); }
|
||||
|
||||
private:
|
||||
FileDescriptor fd_;
|
||||
};
|
||||
|
||||
} // namespace webserv
|
||||
@@ -0,0 +1,37 @@
|
||||
/* ************************************************************************** */
|
||||
/* */
|
||||
/* ::: :::::::: */
|
||||
/* PauseMask.hpp :+: :+: :+: */
|
||||
/* +:+ +:+ +:+ */
|
||||
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
|
||||
/* +#+#+#+#+#+ +#+ */
|
||||
/* Created: 2026/08/25 20:00:34 by acossari #+# #+# */
|
||||
/* Updated: 2026/08/25 20:00:38 by acossari ### ########.fr */
|
||||
/* */
|
||||
/* ************************************************************************** */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace webserv {
|
||||
|
||||
// PauseMask stores its flags in an 8-bit integer. Using the same type here
|
||||
// makes the compiler reject a ninth flag, which would not fit in the mask.
|
||||
enum class ListenerPause : std::uint8_t {
|
||||
PoolFull = 1U << 0,
|
||||
AcceptBackoff = 1U << 1,
|
||||
};
|
||||
|
||||
class PauseMask final {
|
||||
public:
|
||||
// set() returns true when the first reason is added; clear() returns true
|
||||
// when the last is removed. Those transitions disarm and rearm listeners.
|
||||
[[nodiscard]] bool set(ListenerPause reason);
|
||||
[[nodiscard]] bool clear(ListenerPause reason);
|
||||
|
||||
private:
|
||||
std::uint8_t bits_ = 0;
|
||||
};
|
||||
|
||||
} // namespace webserv
|
||||
@@ -0,0 +1,104 @@
|
||||
/* ************************************************************************** */
|
||||
/* */
|
||||
/* ::: :::::::: */
|
||||
/* SlotPool.hpp :+: :+: :+: */
|
||||
/* +:+ +:+ +:+ */
|
||||
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
|
||||
/* +#+#+#+#+#+ +#+ */
|
||||
/* Created: 2026/08/28 21:51:32 by acossari #+# #+# */
|
||||
/* Updated: 2026/08/29 20:33:13 by acossari ### ########.fr */
|
||||
/* */
|
||||
/* ************************************************************************** */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "event/EventToken.hpp"
|
||||
#include "net/Connection.hpp"
|
||||
#include "net/FileDescriptor.hpp"
|
||||
|
||||
namespace webserv {
|
||||
|
||||
// SlotPool uses fixed storage instead of growing a container for every new
|
||||
// connection. This representation provides three guarantees:
|
||||
//
|
||||
// 1. Every slot keeps a permanent index and stable storage. EventToken pairs
|
||||
// that index with an endpoint generation so stale events can be rejected.
|
||||
//
|
||||
// 2. The slot array is allocated once. Connections are constructed and
|
||||
// destroyed in place, so their slots are never relocated.
|
||||
//
|
||||
// 3. The free list limits the pool to CAPACITY connections. acquire() returns
|
||||
// no index when it is exhausted, and drain() makes retired slots reusable.
|
||||
class SlotPool final {
|
||||
public:
|
||||
// Slot indices use uint32_t to match their field in the 64-bit EventToken.
|
||||
// 256 is a calculated tradeoff, not an optimal value. It doubles the
|
||||
// target of 128 concurrent clients exercised by the provided tester. It
|
||||
// assumes a conservative limit of 1024 fds per process, common on Linux.
|
||||
//
|
||||
// fd budget = 4 base fds (standard streams and epoll)
|
||||
// + L listener fds
|
||||
// + 2 * 256 connection fds (socket + possible temporary file)
|
||||
// + 2 * 24 CGI pipe fds (the tester reaches 20, leaving 4 spare)
|
||||
// + 8 spare fds (temporary setup and general working margin)
|
||||
// = 572 + L
|
||||
//
|
||||
// This leaves space for up to 452 listeners, giving substantial headroom.
|
||||
// A capacity of 512 would require 1084 + L fds, already exceeding 1024.
|
||||
static constexpr std::uint32_t CAPACITY = 256;
|
||||
// Each slot tracks the client socket and the CGI stdin and stdout pipes.
|
||||
static constexpr std::size_t ENDPOINTS_PER_SLOT = 3;
|
||||
|
||||
struct Endpoint {
|
||||
std::uint32_t generation = 0;
|
||||
std::uint32_t registeredEvents = 0;
|
||||
bool registered = false;
|
||||
};
|
||||
|
||||
SlotPool();
|
||||
|
||||
bool full() const noexcept { return freeHead_ == CAPACITY; }
|
||||
|
||||
// Constructs a Connection for the socket in a free slot and returns its
|
||||
// index. Returns std::nullopt when the pool is full.
|
||||
std::optional<std::uint32_t> acquire(FileDescriptor socket);
|
||||
|
||||
// Returns true only if the token still identifies a registered endpoint
|
||||
// in an active slot with the same generation.
|
||||
bool isCurrent(const EventToken& token) const noexcept;
|
||||
|
||||
// Accesses the connection or endpoint metadata already stored in a slot.
|
||||
Connection& connection(std::uint32_t index);
|
||||
Endpoint& endpoint(std::uint32_t index, EndpointKind kind);
|
||||
|
||||
// Quarantines an active slot until the current event batch is complete.
|
||||
void retire(std::uint32_t index) noexcept;
|
||||
|
||||
// Recycles all quarantined slots and returns how many became available.
|
||||
std::size_t drain();
|
||||
|
||||
private:
|
||||
enum class SlotState { Free, InUse, Retired };
|
||||
|
||||
struct Slot {
|
||||
std::array<Endpoint, ENDPOINTS_PER_SLOT> endpoints;
|
||||
std::optional<Connection> connection;
|
||||
SlotState state = SlotState::Free;
|
||||
std::uint32_t nextFree = 0;
|
||||
};
|
||||
|
||||
std::array<Slot, CAPACITY> slots_;
|
||||
// Holds retired slot indices until they can return to the free list.
|
||||
// With storage reserved for CAPACITY entries, it acts as if it were a fixed
|
||||
// array while tracking how many entries are active without another counter.
|
||||
std::vector<std::uint32_t> pendingFree_;
|
||||
std::uint32_t freeHead_ = 0;
|
||||
};
|
||||
|
||||
} // namespace webserv
|
||||
Reference in New Issue
Block a user