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:
- Assign a sequence number to the packet.
- Send it over UDP.
- If it doesn’t receive an acknowledgment (ACK) from the other side within a timeout, it will retransmit the packet.
- 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.