Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: CI

on:
pull_request:
branches:
- main
- master
push:
branches:
- main
- master

permissions:
contents: read

jobs:
build-and-test:
name: build-and-test
runs-on: ubuntu-latest
container:
image: archlinux:latest
options: --security-opt seccomp=unconfined --ulimit memlock=-1:-1

steps:
- name: Install dependencies
run: |
pacman -Syu --noconfirm
pacman -S --noconfirm base-devel cmake gcc liburing pkgconf git nodejs

- name: Check out repository
uses: actions/checkout@v4

- name: Configure server tests
run: cmake -S server -B build/server -DCMAKE_BUILD_TYPE=Release

- name: Build server tests
run: cmake --build build/server -j

- name: Run tests
run: ctest --test-dir build/server --output-on-failure

- name: Configure example
run: cmake -S example -B build/example -DCMAKE_BUILD_TYPE=Release -DENABLE_ASAN=OFF

- name: Build example
run: cmake --build build/example -j
34 changes: 26 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,29 @@ cd ../benchmarks
DURATION=30s THREADS=4 CONNECTIONS=100 ./benchmark.sh
```

Output (2026-04-02 16:42:45 MSK, non-ASAN Release build):

- **GET /echo**
- **C++**: 427149 req/s
- **Tokio**: 500231 req/s
- **POST /echo**
- **C++**: 387572 req/s
- **Tokio**: 452589 req/s
Output (non-ASAN Release build):

#### GET /echo

| Server | Latency avg | Latency stdev | Latency max | +/- stdev | Requests/sec | Transfer/sec |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| C++ coroutine server | 132.16us | 66.78us | 4.97ms | 80.82% | 439792.17 | 29.78MB |
| Rust Tokio server | 150.12us | 119.34us | 5.53ms | 94.01% | 506996.06 | 40.61MB |

#### POST /echo

| Server | Latency avg | Latency stdev | Latency max | +/- stdev | Requests/sec | Transfer/sec |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| C++ coroutine server | 149.98us | 77.14us | 5.27ms | 81.01% | 402624.34 | 29.57MB |
| Rust Tokio server | 168.98us | 138.11us | 5.80ms | 91.79% | 465844.80 | 39.98MB |

## Pull Request Checks

Pull requests targeting `main` or `master` run the `CI / build-and-test`
GitHub Actions check. Configure both branches in GitHub branch protection or a
repository ruleset with:

- Require a pull request before merging
- Require status checks to pass before merging
- Required status check: `build-and-test` (shown in Actions as `CI / build-and-test`)
- Restrict direct pushes to matching branches
29 changes: 29 additions & 0 deletions server/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pkg_check_modules(LIBURING REQUIRED liburing)
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
add_library(coro_http_server
src/io_uring.cpp
src/http_parser.cpp
src/read_iterator.cpp
src/http_error.cpp
src/trie.cpp
Expand All @@ -42,3 +43,31 @@ include(GNUInstallDirs)
install(TARGETS coro_http_server EXPORT MyServerConfig DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(EXPORT MyServerConfig FILE coro_http_serverConfig.cmake NAMESPACE coro_http_server:: DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/coro_http_server)
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})

if (CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR)
set(CORO_HTTP_SERVER_BUILD_TESTS_DEFAULT ON)
else()
set(CORO_HTTP_SERVER_BUILD_TESTS_DEFAULT OFF)
endif()
option(CORO_HTTP_SERVER_BUILD_TESTS "Build coro_http_server tests" ${CORO_HTTP_SERVER_BUILD_TESTS_DEFAULT})

if (CORO_HTTP_SERVER_BUILD_TESTS)
include(CTest)
if (BUILD_TESTING)
find_package(Threads REQUIRED)

add_executable(http_parser_test tests/http_parser_test.cpp)
target_link_libraries(http_parser_test PRIVATE coro_http_server::coro_http_server)
set_target_properties(http_parser_test PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/tests
)
add_test(NAME http_parser_test COMMAND http_parser_test)

add_executable(http_integration_test tests/http_integration_test.cpp)
target_link_libraries(http_integration_test PRIVATE coro_http_server::coro_http_server Threads::Threads)
set_target_properties(http_integration_test PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/tests
)
add_test(NAME http_integration_test COMMAND http_integration_test)
endif()
endif()
6 changes: 3 additions & 3 deletions server/include/http_error.h
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
#pragma once
#include <exception>
#include <string_view>
#include <string>
namespace HTTP {
class HTTPError : public std::exception {
public:
std::string_view message;
std::string message;
const int status;
HTTPError(int status, std::string_view message);
HTTPError(int status, std::string message);
};
} // namespace HTTP
88 changes: 88 additions & 0 deletions server/include/http_parser.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#pragma once
#include "co_future.h"
#include "read_iterator.h"
#include "request_data.h"
#include <optional>
#include <string>
#include <string_view>

namespace HTTP {

enum class HttpParseStatus { NeedMoreData, NeedContinue, Complete, Error };
enum class RequestReadStatus { Complete, NeedContinue, Closed };

struct HttpParseResult {
HttpParseStatus status{HttpParseStatus::NeedMoreData};
int errorStatus{0};
std::string errorMessage;
};

class HttpParserState {
enum class State {
StartLine,
HeaderLine,
FixedBody,
ChunkSize,
ChunkData,
ChunkDataCrlf,
TrailerLine,
Complete,
Error
};

State state_{State::StartLine};
RequestData current_;
std::string line_;
std::string pending_;
bool sawCr_{false};
bool continueSent_{false};
bool waitingForContinue_{false};
bool continueCandidate_{false};
bool chunkNeedsLf_{false};
bool started_{false};
bool chunked_{false};
bool expectsContinue_{false};
bool hostEmpty_{false};
bool contentLengthInvalid_{false};
bool contentLengthConflict_{false};
bool unsupportedTransferEncoding_{false};
bool invalidExpect_{false};
bool hasContentLength_{false};
size_t hostCount_{0};
size_t headerBytes_{0};
size_t fixedRemaining_{0};
size_t chunkRemaining_{0};
size_t contentLength_{0};
int errorStatus_{0};
std::string errorMessage_;

void ResetForNext();
size_t ProcessBytes(std::string_view data);
void SetError(int status, std::string message);
void CompleteCurrent();
void ProcessLine();
void AppendLineData(std::string_view data);
void ProcessLineByte(char ch);
void FinishHeaders();
void TrackHeader(std::string_view name, std::string_view value);

public:
HttpParserState();
void Append(std::string_view data);
size_t Consume(std::string_view data);
bool Empty() const;
void MarkContinueSent();
HttpParseResult ParseNext(RequestData &request);
};

class HttpRequestParser {
ReadIterator iterator_;
HttpParserState state_;

public:
HttpRequestParser(IOUring &ring, int fd);
CoFuture<RequestReadStatus> ReadRequest(RequestData &request);
void MarkContinueSent();
};

} // namespace HTTP
7 changes: 5 additions & 2 deletions server/include/io_uring.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "co_future.h"
#include <array>
#include <atomic>
#include <cstddef>
#include <deque>
#include <functional>
#include <liburing.h>
Expand All @@ -12,6 +13,8 @@
#include <string_view>
#define QUEUE_DEPTH 1024
namespace HTTP {
inline constexpr size_t kReadBufferSize = 256;

class IOUring;

class IOUring {
Expand Down Expand Up @@ -44,8 +47,8 @@ class IOUring {
~IOUring();
IOUring();
IOUring &operator=(IOUring &&rhs);
void Read(int fileDescriptor, std::array<char, 256> &buffer, std::function<void(int)> complete);
CoFuture<int> ReadAsync(int fileDescriptor, std::array<char, 256> &buffer);
void Read(int fileDescriptor, std::array<char, kReadBufferSize> &buffer, std::function<void(int)> complete);
CoFuture<int> ReadAsync(int fileDescriptor, std::array<char, kReadBufferSize> &buffer);
void Write(int fileDescriptor, std::string_view data, size_t offset, size_t len,
std::function<void(int)> complete);
CoFuture<int> WriteAsync(int fileDescriptor, std::string_view data, size_t offset,
Expand Down
19 changes: 8 additions & 11 deletions server/include/read_iterator.h
Original file line number Diff line number Diff line change
@@ -1,27 +1,24 @@
#pragma once
#include "co_future.h"
#include "io_uring.h"
#include "request_data.h"
namespace HTTP {

class ReadIterator {
IOUring &ring_;
std::array<char, 256> buffer_;
std::array<char, kReadBufferSize> buffer_;
size_t length_{0};
size_t position_{0};
int fd_;
bool eof_{false};

public:
ReadIterator(IOUring &ring, int fd_);
CoFuture<void> Ensure();
CoFuture<void> operator++();
char operator*() const;
explicit operator bool() const;
bool Eof() const;
size_t Available() const;
const char *CurrentPtr() const;
void Advance(size_t n);
CoFuture<void> operator++();
char operator*();
operator bool();
CoFuture<void> ParseVariables(RequestData &data);
CoFuture<void> ParseHeaders(RequestData &data);
CoFuture<void> ParseMethod(RequestData &data);
CoFuture<void> ParseBody(RequestData &data);
};
};
} // namespace HTTP
24 changes: 21 additions & 3 deletions server/include/request_data.h
Original file line number Diff line number Diff line change
@@ -1,19 +1,37 @@
#pragma once
#include <cstddef>
#include <optional>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>
namespace HTTP {
enum Method { GET, PUT, POST, PATCH, DELETE };
enum Method { GET, PUT, POST, PATCH, DELETE, HEAD, OPTIONS, UNKNOWN };

constexpr size_t kRoutableMethodCount = 7;

struct RequestData {
std::unordered_map<std::string, std::string> headers;
std::unordered_map<std::string, std::string> params;
std::unordered_map<std::string, std::string> trailers;
std::vector<std::pair<std::string, std::string>> rawHeaders;
std::vector<std::string> urlVariables;
Method method;
Method method{UNKNOWN};
std::string methodToken;
std::string target;
std::string path;
std::string query;
std::string version;
std::string body;
bool connectionClose{false};
bool connectionKeepAlive{false};

std::optional<std::string> Header(std::string_view name) const;
};
struct ResponseData {
std::unordered_map<std::string, std::string> headers;
std::string body;
unsigned short status;
unsigned short status{200};
};
} // namespace HTTP
12 changes: 8 additions & 4 deletions server/include/server.h
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
#pragma once
#include "co_future.h"
#include "http_parser.h"
#include "io_uring.h"
#include "read_iterator.h"
#include "request_data.h"
#include "trie.h"
#include <atomic>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
namespace HTTP {
class Server {
Expand All @@ -20,9 +22,11 @@ class Server {

void WorkerLoop(IOUring &ring);
CoFuture<void> AcceptAndProcess(IOUring &ring);
CoFuture<void> GetHandler(RequestData &data, ReadIterator &iter, RespondType &handler);
CoFuture<void> WriteResponse(IOUring &ring, int connectionFD, const ResponseData &data,
bool keepAlive);
CoFuture<void> WriteRaw(IOUring &ring, int connectionFD, std::string_view data);
CoFuture<void> WriteResponse(IOUring &ring, int connectionFD,
const ResponseData &data,
const RequestData &request, bool keepAlive,
std::string &buffer);
CoFuture<void> Process(IOUring &ring, int connectionFD);
friend class ServerBuilder;

Expand Down
13 changes: 12 additions & 1 deletion server/include/trie.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,28 @@ class Trie {
StringEqual>
children;
std::unique_ptr<Node> wildcard;
std::optional<RespondType> handlers[5];
std::optional<RespondType> handlers[kRoutableMethodCount];
Node() = default;
Node &Move(std::string_view segment);
};
std::unique_ptr<Node> root_ = std::make_unique<Node>();

public:
struct RouteResult {
bool pathFound{false};
bool methodAllowed{false};
bool automaticOptions{false};
RespondType handler;
std::string allow;
};

Trie() = default;
Trie(Trie &&rhs);
Trie &operator=(Trie &&rhs);
void AddRequest(Method type, RespondType function, std::string_view path);
std::string AllAllowedMethods() const;
RouteResult Resolve(Method method, std::string_view path,
std::vector<std::string> &urlVariables) const;
RespondType Match(Method method, std::string_view path,
std::vector<std::string> &urlVariables) const;
};
Expand Down
Loading
Loading