Skip to content

Getting Started

irrld edited this page Jul 30, 2026 · 3 revisions

Adding znet to your project

As a submodule:

git submodule add https://github.com/teoncreative/znet.git external/znet
git submodule update --init --recursive

Then, 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)

Build options

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_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

A server

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() {
  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
  return 0;
}

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.

A client

Same shape. The difference is that a client has one session, handed to you when the connection completes rather than on accept.

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() {
  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();
  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.

Which transport those used

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.

Where to look next

Full programs live in the examples folder: basic for TCP, zdt for reliable UDP, userptr for attaching your own state to a connection, multiversion for negotiating packet versions between builds, and p2p for hole punching.

Clone this wiki locally