Skip to content

Getting Started

irrld edited this page Jul 30, 2026 · 3 revisions

Getting Started

Adding znet to your project

As a submodule:

git submodule add https://github.com/irrld/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

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.

Switching to reliable UDP

One field, and nothing above it changes:

ClientConfig config{"127.0.0.1", 25000, std::chrono::seconds(10),
                    ConnectionType::ZDT};

The server needs the same, and both ends must agree. What that buys, and what it costs, is in Choosing a Transport.

Where to look next

Full programs live in the examples folder: basic for TCP, zdt for reliable UDP, multiversion for negotiating packet versions between builds, and p2p for hole punching.

Clone this wiki locally