ENet doesn’t actually guarantee reliability through UDP itself; it builds a reliable layer on top of UDP by implementing its own packet sequencing, acknowledgments, and retransmissions.

Let’s watch ENet in action. Imagine a simple game where a player moves and we want to send that movement data to another player.

Here’s a basic ENet server setup:

#include <enet/enet.h>
#include <stdio.h>

int main(void) {
    ENetAddress address;
    ENetEvent event;
    ENetHost *server;

    /* Initialize ENet */
    if (enet_initialize() != 0) {
        fprintf(stderr, "An error occurred while initializing ENet.\n");
        return EXIT_FAILURE;
    }
    atexit(enet_deinitialize);

    /* Bind the server to the local port 1234 */
    enet_address_set_host(&address, ENET_HOST_ANY);
    address.port = 1234;
    server = enet_host_create(&address, 32, 2, 0, 0); // Max clients, channels, upload/download bandwidth

    if (server == NULL) {
        fprintf(stderr, "An error occurred while trying to create an ENet server host.\n");
        return EXIT_FAILURE;
    }

    printf("Server started on port 1234.\n");

    /* Wait for events */
    while (1) {
        while (enet_host_service(server, &event, 1000) > 0) { // Service for 1000ms
            switch (event.type) {
                case ENET_EVENT_TYPE_CONNECT:
                    printf("A new client connected from %u:%u.\n",
                           event.peer->address.host,
                           event.peer->address.port);
                    break;

                case ENET_EVENT_TYPE_RECEIVE:
                    printf("A packet was received from client %u:%u.\n",
                           event.peer->address.host,
                           event.peer->address.port);
                    // Echo the received packet back to the client
                    enet_packet_destroy(event.packet); // Destroy the received packet
                    // Create a new packet to send
                    ENetPacket *packet = enet_packet_create(event.packet->data, event.packet->dataLength, ENET_PACKET_RELIABLE);
                    enet_peer_send(event.peer, 0, packet); // Send on channel 0
                    break;

                case ENET_EVENT_TYPE_DISCONNECT:
                    printf("Client %u:%u disconnected.\n",
                           event.peer->address.host,
                           event.peer->address.port);
                    break;

                case ENET_EVENT_TYPE_NONE:
                    // Timeout, no event occurred
                    break;
            }
        }
    }

    enet_host_destroy(server);
    return EXIT_SUCCESS;
}

And a corresponding client:

#include <enet/enet.h>
#include <stdio.h>

int main(void) {
    ENetAddress address;
    ENetEvent event;
    ENetHost *client;
    ENetPeer *server_peer;

    /* Initialize ENet */
    if (enet_initialize() != 0) {
        fprintf(stderr, "An error occurred while initializing ENet.\n");
        return EXIT_FAILURE;
    }
    atexit(enet_deinitialize);

    /* Create a client host */
    client = enet_host_create(NULL, 1, 0, 0, 0); // Max clients, channels, upload/download bandwidth
    if (client == NULL) {
        fprintf(stderr, "An error occurred while trying to create an ENet client host.\n");
        return EXIT_FAILURE;
    }

    /* Connect to the server */
    enet_address_set_host(&address, "127.0.0.1"); // Server IP
    address.port = 1234;
    server_peer = enet_host_connect(client, &address, 2, 0); // Max channels, data

    if (server_peer == NULL) {
        fprintf(stderr, "No available servers connected.\n");
        return EXIT_FAILURE;
    }

    printf("Attempting to connect to server...\n");

    /* Wait for connection */
    if (enet_host_service(client, &event, 5000) > 0 && event.type == ENET_EVENT_TYPE_CONNECT) {
        printf("Successfully connected to server.\n");

        /* Send a message to the server */
        const char *message = "Hello Server!";
        ENetPacket *packet = enet_packet_create(message, strlen(message) + 1, ENET_PACKET_RELIABLE);
        enet_peer_send(server_peer, 0, packet); // Send on channel 0
        printf("Sent message to server.\n");

        /* Wait for server response */
        while (enet_host_service(client, &event, 1000) > 0) {
            switch (event.type) {
                case ENET_EVENT_TYPE_RECEIVE:
                    printf("Received from server: %s\n", (char *)event.packet->data);
                    enet_packet_destroy(event.packet);
                    goto cleanup; // Exit loop after receiving
                case ENET_EVENT_TYPE_DISCONNECT:
                    printf("Disconnected from server.\n");
                    goto cleanup;
                default:
                    break;
            }
        }
    } else {
        /* Connection failed */
        printf("Connection to server failed.\n");
    }

cleanup:
    /* Clean up */
    enet_host_destroy(client);
    return EXIT_SUCCESS;
}

This demonstrates the core loop: enet_host_create sets up the network interface, enet_host_connect initiates a connection, enet_host_service polls for network events (like new connections, received data, disconnections), and enet_peer_send dispatches packets. enet_packet_create allocates memory for a packet, and enet_packet_destroy frees it. The enet_host_destroy call cleans up the network resources.

ENet’s "reliability" means it handles the heavy lifting of ensuring data arrives. When you create a packet with ENET_PACKET_RELIABLE, ENet will:

  1. Assign a sequence number to the packet.
  2. Send it over UDP.
  3. If it doesn’t receive an acknowledgment (ACK) from the other side within a timeout, it will retransmit the packet.
  4. It continues retransmitting until an ACK is received or a maximum retry count is reached, at which point it might signal a disconnection.

This is crucial for game state updates where losing a single packet could mean a player’s position is stale or incorrect. For less critical data, like occasional chat messages, you might use ENET_PACKET_UNRELIABLE or ENET_PACKET_UNRELIABLE_SEQUENCED to reduce overhead, but for core gameplay, reliability is king.

The concept of channels is also important. ENet supports up to 256 channels per connection (indexed 0-255). When you send a packet, you specify a channel. This allows you to prioritize traffic. For instance, you could send critical movement updates on channel 0 (which might have a smaller queue or higher retransmission priority) and less critical data like chat messages on channel 1. If the network gets congested, ENet can prioritize sending packets from lower-numbered channels.

What most people don’t realize is that ENet’s reliability is actually a form of enforced ordering for reliable packets. Even if you send packet A, then packet B, and packet B arrives first, ENet will hold onto packet B until packet A has been acknowledged and delivered to the application. This is why you can enet_packet_destroy(event.packet) and then enet_packet_create with the same data to echo it back reliably; ENet ensures the echo arrives in the same order the original was intended.

Want structured learning?

Take the full Udp course →