-
Notifications
You must be signed in to change notification settings - Fork 5
Getting Started
As a submodule:
git submodule add https://github.com/teoncreative/znet.git external/znet
git submodule update --init --recursiveThen, in your CMakeLists.txt, using the bundled zstd:
add_subdirectory(external/znet/vendor/zstd/build/cmake ${CMAKE_CURRENT_BINARY_DIR}/zstd)
add_subdirectory(external/znet/znet ${CMAKE_CURRENT_BINARY_DIR}/znet)
target_link_libraries(your_target PRIVATE znet)Or with a system zstd from vcpkg, brew or your package manager:
set(ZNET_USE_EXTERNAL_ZSTD ON)
add_subdirectory(external/znet/znet)
target_link_libraries(your_target PRIVATE znet)| Option | Default | Effect |
|---|---|---|
ZNET_CXX_STANDARD |
20 |
14, 17, 20 or 23
|
ZNET_USE_EXTERNAL_ZSTD |
OFF |
Use a zstd you provide instead of the bundled one |
ZNET_ENABLE_METRICS |
ON |
OFF compiles the counters out entirely |
ZNET_ENABLE_LTO |
ON |
Link-time optimization for release builds, silently skipped where unsupported |
ZNET_MAX_READ_ELEMENTS |
65536 |
Largest element count a vector, map or array read accepts. 0 removes the ceiling |
ZNET_MAX_READ_STRING_LENGTH |
65536 |
Longest string a read accepts, in bytes. 0 removes the ceiling |
ZNET_ENABLE_STRICT_WARNINGS |
ON |
znet's own warning set for its own sources, -Werror included. Nothing reaches your targets |
ZNET_PREFER_IPV4 |
OFF |
Take the first IPv4 result when a hostname resolves to both families |
ZNET_PREFER_STD_SLEEP |
OFF |
std::this_thread::sleep_for between ticks instead of the precise spin-and-sleep: less CPU, coarser timing |
ZNET_BUILD_EXTENSIONS |
ON |
The optional extensions. Each skips itself when its dependency is missing |
ZNET_EXT_ALLOW_FETCH |
ON |
OFF stops extensions downloading a missing dependency; they skip instead |
ZNET_EXT_<NAME> |
ON |
One extension off, e.g. -DZNET_EXT_BULLET=OFF. Names are in the extensions page |
ZNET_BUILD_BENCHMARKS |
OFF |
The benchmark suite. Slow; each comparison library is a further opt-in (ZNET_BENCH_ENET, ZNET_BENCH_RAKNET, ZNET_BENCH_GNS) and is fetched when turned on |
ZNET_BUILD_EXTENSIONS and ZNET_BUILD_BENCHMARKS are declared by the
top-level CMakeLists.txt, which also pulls in the examples and the tests, so
they do nothing for a consumer adding external/znet/znet. To get the
extensions that way, add their directory yourself after the library:
add_subdirectory(external/znet/extensions ${CMAKE_CURRENT_BINARY_DIR}/znet-ext)
target_link_libraries(your_target PRIVATE znet-reflect)Three things happen for every connection: give the session a codec so it can read and write your packets, give it a handler so something receives them, and send.
enum : PacketId { kChatMessage = 1 };
class ChatMessage : public Packet {
public:
ChatMessage() : Packet(kChatMessage) {}
std::string text;
};
class ChatSerializer : public PacketSerializer<ChatMessage> {
public:
std::shared_ptr<Buffer> SerializeTyped(std::shared_ptr<ChatMessage> packet,
std::shared_ptr<Buffer> buffer) override {
buffer->WriteString(packet->text);
return buffer;
}
std::shared_ptr<ChatMessage> DeserializeTyped(std::shared_ptr<Buffer> buffer) override {
auto packet = std::make_shared<ChatMessage>();
packet->text = buffer->ReadString();
return packet;
}
};
class ChatHandler : public PacketHandler<ChatHandler, ChatMessage> {
public:
explicit ChatHandler(std::shared_ptr<PeerSession> session)
: session_(std::move(session)) {}
void OnPacket(std::shared_ptr<ChatMessage> packet) {
auto reply = std::make_shared<ChatMessage>();
reply->text = "echo: " + packet->text;
session_->SendPacket(reply);
}
private:
std::shared_ptr<PeerSession> session_;
};
// one codec for every session: serializers are stateless and shared
std::shared_ptr<Codec> g_codec;
bool OnClientConnected(IncomingClientConnectedEvent& event) {
event.session()->SetCodec(g_codec);
event.session()->SetHandler(std::make_shared<ChatHandler>(event.session()));
return false; // false lets other handlers see the event too
}
void OnEvent(Event& event) {
EventDispatcher dispatcher{event};
dispatcher.Dispatch<IncomingClientConnectedEvent>(
ZNET_BIND_GLOBAL_FN(OnClientConnected));
}
int RunServer() {
if (znet::Init() != Result::Success) { // once, before anything else
return 1;
}
g_codec = std::make_shared<Codec>();
g_codec->Add(kChatMessage, std::make_unique<ChatSerializer>());
ServerConfig config{"0.0.0.0", 25000};
Server server{config};
server.SetEventCallback(ZNET_BIND_GLOBAL_FN(OnEvent));
if (server.Bind() != Result::Success) {
return 1;
}
server.Listen(); // returns immediately; the server runs on its own thread
server.Wait(); // blocks until it stops
znet::Cleanup(); // after the last znet object is gone
return 0;
}znet::Init() does the global setup, WSAStartup among it, and every later
call is a cheap no-op, so a component may call it defensively. Cleanup()
releases what it took.
Listen() returns as soon as the listener is up. Wait() is what blocks, so a
program that has other work to do simply does not call it.
Same shape. The difference is that a client has one session, handed to you when the connection completes rather than on accept.
class ClientHandler : public PacketHandler<ClientHandler, ChatMessage> {
public:
void OnPacket(std::shared_ptr<ChatMessage> packet) {
ZNET_LOG_INFO("server said: {}", packet->text);
}
};
bool OnConnected(ClientConnectedToServerEvent& event) {
auto codec = std::make_shared<Codec>();
codec->Add(kChatMessage, std::make_unique<ChatSerializer>());
event.session()->SetCodec(codec);
event.session()->SetHandler(std::make_shared<ClientHandler>());
auto hello = std::make_shared<ChatMessage>();
hello->text = "hello";
event.session()->SendPacket(hello);
return false;
}
void OnEvent(Event& event) {
EventDispatcher dispatcher{event};
dispatcher.Dispatch<ClientConnectedToServerEvent>(ZNET_BIND_GLOBAL_FN(OnConnected));
}
int RunClient() {
if (znet::Init() != Result::Success) {
return 1;
}
ClientConfig config{"127.0.0.1", 25000, std::chrono::seconds(10)};
Client client{config};
client.SetEventCallback(ZNET_BIND_GLOBAL_FN(OnEvent));
if (client.Bind() != Result::Success) {
return 1;
}
client.Connect(); // returns immediately
client.Wait();
znet::Cleanup();
return 0;
}The third ClientConfig field is the connection timeout. Zero disables it,
which means a client dialing an address that never answers waits forever.
SendPacket() only queues, and it returns a Result rather than void.
QueueFull is the backpressure signal: nothing was encoded, the packet is
still yours, and retrying is the right response. NotReady means the handshake
has not settled yet, which is why both snippets above send from the connected
event rather than straight after Connect().
Neither config above named a transport, so both got the default: ZDT, znet's reliable-UDP transport. One field switches to TCP, and nothing above it changes:
ClientConfig config{"127.0.0.1", 25000, std::chrono::seconds(10),
ConnectionType::TCP};The server needs the same, and both ends must agree. What each one buys, and what it costs, is in Choosing a Transport. The short version: stay on ZDT unless something in your environment only passes TCP.
- Messages of your own: Packets and Serialization
- What else you can respond to: Events
- Keeping per-connection state: Session State
- Before you call znet from your own threads: Threading Model
Full programs live in the examples
folder: basic for the smallest server and client pair, zdt for the delivery
modes, userptr for attaching your own state to a connection, multiversion
for negotiating packet versions between builds, auth for service-signed
tokens and a session-bound client signature, chat-tui for a chat room with
fan-out, and p2p for gathering, hole punching and the relay fallback through
a rendezvous server. All of them run on ZDT, p2p included once its punch
lands; only its hop to the rendezvous server is TCP.