Skip to content

Latest commit

 

History

History
204 lines (165 loc) · 7.63 KB

README.md

File metadata and controls

204 lines (165 loc) · 7.63 KB

Serverbox

test

Serverbox is a framework for networking in C++. The purpose is similar to boost::asio. The focus is on these key points:

  • Simplicity - the API and algorithm is hard to misuse and is easy to understand.
  • Compactness - the framework is small, both in code and binary size.
  • Speed - extreme optimizations and efficiency for run-time and even compile-time.
  • Fairness - huge accent on algorithms' fairness and even utilization of the CPU.

Being tens if not hundreds of times smaller than Boost, this small framework outperforms the former more than 50% in RPS on the same highload benchmark, while being on par in small loads.

The framework consists of several modules implementing things most needed on the backend: network IO, task scheduling, fast data structures and containers, and some smaller utilities.

The core features in the framework are IOCore - a networking layer to accept clients, to send and receive data, and TaskScheduler - task processing engine. More about them below.

TaskScheduler

TaskScheduler is a multi-threaded task processing engine for asynchronous code execution. It is the heart of the framework which is valuable on its own and its algorithm is the base for IOCore. The scheduler can be used for things from simple one-shot async callback execution to multistep complex pipelines with yields in between.

A very simple example:

TaskScheduler sched("name", thread_count, queue_size);
Task* t = new Task([](Task *self) {
	printf("Executed in scheduler!\n");
	delete self;
});
sched.Post(t);

The Task object is a very light context (< 100 bytes) which can be just deleted right after single callback invocation, or can be attached to your own data and re-used across multiple steps of your pipeline, and can be used for deadlines, wakeups, signaling, etc.

It can also be used with C++20 coroutines:

Task* t = new Task();
t->SetCallback([](Task *self) -> mg::box::Coro {
	printf("Executed in scheduler!\n");
	self->SetDeadline(someDeadline);
	bool ok = co_await self->AsyncReceiveSignal();
	if (ok)
		printf("Received a signal");
	else
		printf("No signal");
	co_await self->AsyncExitDelete();
	assert(!"unreachable");
	co_return;
}(t));
sched.Post(t);

See more in src/mg/sch/README.md.

IOCore

IOCore is a multi-threaded event-loop for asynchronous code execution and network IO. It is like TaskScheduler, the internal architecture and the API are very similar, but IOCore's main use case is work with sockets and efficient IO. You can think of it as if each of your sockets (server, accepted sockets, client sockets) is like a very lightweight coroutine.

See more in src/mg/aio/README.md.

A simple example:

// Global or not - you choose. The type is not a singleton.
IOCore theCore;

class MyClient : private TCPSocketSubscription
{
public:
	MyClient(const Host& host) : myHost(host)
	{
		mySock = new TCPSocket(theCore);
		mySock->Open(TCPSocketParams{});
		// Receive with a 16KB buffer first time.
		mySock->PostRecv(16 * 1024);
		TCPSocketConnectParams connParams;
		connParams.myHost = &host;
		mySock->PostConnect(connParams, this);
	}

	void Send(const Message& msg)
	{
		BufferStream stream;
		msg.ToStream(stream);
		// Thread-safe send() from anywhere.
		mySock->PostSendRef(stream.TakeData());
	}

private:
	void OnRecv(BufferReadStream& stream) override
	{
		Message msg;
		while (msg.FromStream(stream))
		{
			// Your handling function.
			HandleMyMessage(msg);
		}
		// Keep receiving until close happens.
		mySock->Recv(16 * 1024);
	}

	void OnClose() override
	{
		// Reconnect with backoff 10ms after disconnect. Easy way to avoid too frequent
		// reconnects. If reconnect is needed at all.
		TCPSocketConnectParams connParams;
		connParams.myHost = &myHost;
		connParams.myDelay = 10;
		// Can change socket params between reconnects if want.
		mySock->Open(TCPSocketParams{});
		mySock->PostConnect(connParams, this);
	}

	TCPSocket* mySock;
	const Host myHost;
};

IOCore core can be used as a task scheduler - IOTasks don't need to have sockets right from the start or at all. It can also be combined with TaskScheduler to execute your business logic in there, and do just IO in IOCore. This is actually the preferable usage.

Getting Started

Dependencies

  • At least C++17;
  • A standard C++ library. On non-Windows also need pthread library.
  • Compiler support:
    • Windows MSVC;
    • Clang;
    • GCC;
  • Kernel support:
    • Windows (10 and later guaranteed);
    • WSLv1 (v2 as well, since this is basically a VM);
    • Linux (any version);
    • MacOS (earliest tested Catalina 11.7.10);
  • Architecture support:
    • x86
    • ARM
  • CMake. Compatible with cmake CLI and with VisualStudio CMake.

Build and test

Configuration options

It is possible to choose certain things at the CMake configuration stage. Each option can be given to CMake using -D<name>=<value> syntax. For example, -DMG_AIO_USE_IOURING=1.

  • MG_ENABLE_TEST - 1/0 = enable or disable tests compilation. Handy, when building and installing it regularly and want to save time. Default is 1.
  • MG_ENABLE_BENCH - 1/0 = same as above for benchmarks. Disabling them also makes sense because they might be not compatible with certain boost versions. Default is 0.
  • MG_AIO_USE_IOURING - 1/0 = enable/disable io_uring on Linux, 0 = use epoll, 1 = use io_uring. Default is 0.
  • MG_BOOST_USE_IOURING - 1/0 = same for boost::asio used in the benchmarks. Default is 0.
  • MG_IS_CI - 1/0 = whether is running in CI. Is used to reduce duration of some tests which are more about perf than correctness. Default is 0.

Visual Studio

  • Open VisualStudio;
  • Select "Open a local folder";
  • Select serverbox/ folder where the main CMakeLists.txt is located;
  • Wait for CMake parsing and configuration to complete;
  • Build and run as a normal VS project.

CMake CLI

From root of the project:

mkdir build
cd build
cmake ../

Now can run the tests: ./test/test.

Useful tips (for clean project, from the build folder created above):

  • Compiler switch:
    • clang:
      CC=clang CXX=clang++ cmake ../;
    • GCC:
      CC=gcc CXX=g++ cmake ../;
  • Build type switch:
    • Release, all optimizations, no debug info:
      cmake -DCMAKE_BUILD_TYPE=Release ../;
    • Release, all optimizations:
      cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo ../;
    • Debug, no optimization at all:
      cmake -DCMAKE_BUILD_TYPE=Debug ../;
  • Change C++ standard: cmake -DCMAKE_CXX_STANDARD=17/20/...;

Installation

mkdir build
cd build
# Your own install directory is optional.
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=$(pwd)/installed ../
make install

This creates a subfolder installed/ in the current directory, which contains the library binaries and headers. The libraries are self-sufficient and isolated. It means you can take just TaskScheduler headers and static library, or just IOCore's, or just basic libmgbox and its headers. Or any combination of those.

Stubs

Some of the libraries might have intentionally undefined symbols. For example, mg/box doesn't have logging functions defined (mg::box::LogV()). That is done so as you could define them in your code to intercept logging output, statistics, contention tracking, etc.

If none of the proposed interception endpoints are needed, you can simply link with a stub-library to use the defaults, such as libmgboxstub.

Further info

See more details here: