LessonsExtensions: Built inLesson 66

Fast messages with ENet

Send messages between programs the way games do, with ENet: a server and a client, events you poll for, text and bytes, reliable and unreliable channels, the health of a connection, and a polite goodbye.

  • Extensions
  • Lesson 66 of 83
  • About 13 minutes
  • 6 examples

Messages for games

A socket (Sockets) carries a stream of bytes. A game wants something else: short packets that arrive whole, some of which must arrive (a chat line, a move) and some of which may be dropped if a newer one is coming (where a player is now). ENet gives exactly that, over UDP, and the xTalk Suite's enetxt brings it to xTalk with handlers that begin with en. Its helpers, such as enStateName and enFormatBytes, come with it.

ENet never calls your scripts by itself. You call enPoll again and again, on a timer in an app, and each call hands back what has happened since the last: a peer connected, a packet arrived, a peer left. Here the server and the client are in one script on one computer, talking through 127.0.0.1, and a loop polls both.

Hosts and holds

enInitialize takes a hold on the library before any other call, and enDeinitialize gives it back, once for each hold. A host is your end of the line: enHostCreateServer makes one that listens, given the address to listen on, a port (0 picks any free one), how many peers may join, how many channels each has, and speed limits in bytes a second (0 for none). enHostStatus says where it listens:

ExampleRun it in the playground
put enInitialize() into tStarted
put tStarted && "(0 is good)"
put enLibraryVersion() begins with "enet "
put enHostCreateServer("127.0.0.1", 0, 8, 2, 0, 0) into tServer
put enHostStatus(tServer) into tStatus
set the itemDelimiter to ":"
put item 1 of tStatus["address"]
put item 2 of tStatus["address"] > 0
put tStatus["peerCount"] && "peers"
get enHostDestroy(tServer)
put enHostStatus(tServer) is empty
enDeinitialize
Output
0 (0 is good)
true
127.0.0.1
true
0 peers
true

A closed host's handle is retired: asking about it gives empty, never another host's answer.

Connecting

A client host connects to a server. enConnect gives a peer handle at once, but the connection is real only when an event says so. Each event that enPoll returns is an array with a "name": enetConnect, enetReceive or enetDisconnect. The last parameter of enConnect is a number the server receives with the connection, such as the version of your game:

ExampleRun it in the playground
get enInitialize()
put enHostCreateServer("127.0.0.1", 0, 4, 1, 0, 0) into tServer
set the itemDelimiter to ":"
put enHostStatus(tServer) into tStatus
put item 2 of tStatus["address"] into tPort
put enHostCreateClient(1, 1, 0, 0) into tClient
put enConnect(tClient, "127.0.0.1", tPort, 1, 7) into tPeer
-- nothing happens until the hosts are polled
repeat 400 times
   get enPoll(tClient)
   put enPoll(tServer) into tEvents
   if tEvents[1]["name"] is "enetConnect" then exit repeat
   wait 5 milliseconds
end repeat
put "The server got" && tEvents[1]["name"] && "with" && tEvents[1]["data"]
put enPeerStatus(tPeer) into tStatus
put "The client is" && enStateName(tStatus["state"])
get enHostDestroy(tClient)
get enHostDestroy(tServer)
enDeinitialize
Output
The server got enetConnect with 7
The client is connected

Text and bytes

Once connected, enSendText sends text on a channel, and the other side gets an enetReceive event whose "payload" is the bytes: textDecode turns them back into text. enSend sends any bytes, up to 60000 a packet, which is how games keep their messages small. Both return 0 when the packet is queued, and a negative number when not (-4: too big). Packets leave at the next poll, or at once with enFlush:

ExampleRun it in the playground
get enInitialize()
put enHostCreateServer("127.0.0.1", 0, 4, 1, 0, 0) into tServer
set the itemDelimiter to ":"
put enHostStatus(tServer) into tStatus
put item 2 of tStatus["address"] into tPort
put enHostCreateClient(1, 1, 0, 0) into tClient
put enConnect(tClient, "127.0.0.1", tPort, 1, 0) into tPeer
put enSend(tPeer, 0, format("%60001s", ""), 0) && "for 60001 bytes"
repeat 400 times
   get enPoll(tClient)
   repeat for each element tEvent in enPoll(tServer)
      if tEvent["name"] is "enetConnect" then
         get enSendText(tPeer, 0, "Hello, server!", 0)
         get enSend(tPeer, 0, numToByte(200), 0)
      else if tEvent["name"] is "enetReceive" then
         put tEvent["payload"] & return after tReceived
      end if
   end repeat
   if the number of lines of tReceived is 2 then exit repeat
   wait 5 milliseconds
end repeat
put textDecode(line 1 of tReceived, "UTF-8")
put byteToNum(line 2 of tReceived)
get enHostDestroy(tClient)
get enHostDestroy(tServer)
enDeinitialize
Output
-4 for 60001 bytes
Hello, server!
200

Channels and delivery

A connection can have several channels, numbered from 0, and a late packet on one never holds up another. Each send also has a delivery flag:

  • 0, reliable: resent until it arrives, in order. For chat and moves.
  • 1, unreliable: in order, but it may be dropped. For where a player is now: a newer one comes soon.
  • 2: it may be dropped, or arrive out of order.
ExampleRun it in the playground
get enInitialize()
put enHostCreateServer("127.0.0.1", 0, 4, 2, 0, 0) into tServer
set the itemDelimiter to ":"
put enHostStatus(tServer) into tStatus
put item 2 of tStatus["address"] into tPort
put enHostCreateClient(1, 2, 0, 0) into tClient
put enConnect(tClient, "127.0.0.1", tPort, 2, 0) into tPeer
repeat 400 times
   get enPoll(tClient)
   put enPoll(tServer) into tEvents
   if tEvents[1]["name"] is "enetConnect" then
      -- where the player is: channel 1, unreliable
      get enSendText(tPeer, 1, "x=10 y=20", 1)
   end if
   if tEvents[1]["name"] is "enetReceive" then exit repeat
   wait 5 milliseconds
end repeat
put "channel" && tEvents[1]["channel"] & ":" && textDecode(tEvents[1]["payload"], "UTF-8")
get enHostDestroy(tClient)
get enHostDestroy(tServer)
enDeinitialize
Output
channel 1: x=10 y=20

Health, and goodbye

enPeerStatus tells a game how a connection is: its "state", the round trip in milliseconds ("rtt"), packets lost, and bytes sent and received, which enFormatBytes writes for people. enSetPeerTimeout decides when a quiet peer counts as gone. enDisconnect ends a connection politely: queued packets go first, then both sides get an enetDisconnect event, and the peer handle is retired, so a send to it is refused:

ExampleRun it in the playground
get enInitialize()
put enHostCreateServer("127.0.0.1", 0, 4, 1, 0, 0) into tServer
set the itemDelimiter to ":"
put enHostStatus(tServer) into tStatus
put item 2 of tStatus["address"] into tPort
put enHostCreateClient(1, 1, 0, 0) into tClient
put enConnect(tClient, "127.0.0.1", tPort, 1, 0) into tPeer
repeat 400 times
   get enPoll(tClient)
   put enPoll(tServer) into tEvents
   if tEvents[1]["name"] is "enetConnect" then
      put enPeerStatus(tPeer) into tStatus
      put enStateName(tStatus["state"]) & "," && enFormatBytes(tStatus["bytesSent"]) && "sent" into tHealth
      -- give up on a silent peer after 5 to 30 seconds
      get enSetPeerTimeout(tPeer, 0, 5000, 30000)
      get enDisconnect(tPeer, 0)
   end if
   if tEvents[1]["name"] is "enetDisconnect" then exit repeat
   wait 5 milliseconds
end repeat
put matchText(tHealth, "^connected, [0-9]+ B sent$")
put "The server got" && tEvents[1]["name"]
put enSendText(tPeer, 0, "still there?", 0) < 0
get enHostDestroy(tClient)
get enHostDestroy(tServer)
enDeinitialize
Output
true
The server got enetDisconnect
true

Try it yourself

Make the server answer: when it receives text, it sends it back in capitals to the peer that sent it (the event's "peer"). The client sends ping and puts what comes back.

Show a solution
ExampleRun it in the playground
get enInitialize()
put enHostCreateServer("127.0.0.1", 0, 4, 1, 0, 0) into tServer
set the itemDelimiter to ":"
put enHostStatus(tServer) into tStatus
put item 2 of tStatus["address"] into tPort
put enHostCreateClient(1, 1, 0, 0) into tClient
put enConnect(tClient, "127.0.0.1", tPort, 1, 0) into tPeer
repeat 400 times
   repeat for each element tEvent in enPoll(tServer)
      if tEvent["name"] is "enetReceive" then
         get enSendText(tEvent["peer"], 0, toUpper(textDecode(tEvent["payload"], "UTF-8")), 0)
      end if
   end repeat
   repeat for each element tEvent in enPoll(tClient)
      if tEvent["name"] is "enetConnect" then
         get enSendText(tPeer, 0, "ping", 0)
      else if tEvent["name"] is "enetReceive" then
         put textDecode(tEvent["payload"], "UTF-8") into tAnswer
      end if
   end repeat
   if tAnswer is not empty then exit repeat
   wait 5 milliseconds
end repeat
put tAnswer
get enHostDestroy(tClient)
get enHostDestroy(tServer)
enDeinitialize
Output
PING

Words in this lesson

Each one finds every lesson that uses it. The Dictionary has the whole story of each, with its syntax and more examples.