Networking with UDP

This is just the introduction and does not cover advanced networking techniques that are quite complex. It just shows how to

  • Open an UDP server
  • Connect with clients to it
  • Sending messages from one client to all clients (including itself)

Note on networking

If a connection cannot be established, there are various reasons why this could be so:

  1. The firewall
    Firewalls protect the operating system from intruders. If a firewall is on, it will naturally reject incoming packets if the port has not been opened.
  2. A router
    Many routers act like a firewall: If an packet from outside (Internet) is received, it is only forwarded to a computer in the LAN, if the recipient is known. This is the case if either that computer has established the connection or if the router is configured to forward packages that are received at a certain port to a certain computer. If a server should be present, the router must be configured to forward the incoming packets to the server, since the connection is established from outside.
    In order for a correct communication, make sure that the sending socket of a server is also the one that the client has connected to.
  3. The software
    If the first points can be ruled out (check this first) the bug might be in the sourcecode of your software.

Step 1: UDP Server

Network communication using TCP/UDP is done by using sockets. If a serversocket is opened on your computer, a port must be specified which ranges from 1-~65000. Using Port numbers <1000 is generally not adviced, since these are used for other purposes. Using a quite random port number like 14923 is quite safe, but in any case, before publishing something to the public, make a search if that port is really unique. You could also use ports that are already used by other games, since it is unlikely that another game is running at the same time. The advantage here is, that sometimes routers open ports that are used by games per default.

The documentation on luasocket is included in the luxinia API, but not completly, for a more detailed documentation, you can read it here:

UDP Connections are the easiest way to communicate but it is also unsecure since there is no check if a message really arrived, nor is the order of sent data guaranteed.

First, we'll need a server. We will start and end it using a normal timer function and yield the execution in regular intervals.

Our basic setup is the same just as always, including two variables specifying the server's address and its port:

view = UtilFunctions.simplerenderqueue()
view.rClear:colorvalue(0.0,0.0,0.0,0) 

svrport = 14285
svraddr = "localhost"

The server is run as a function and we start creating an udp socket:

local function server ()
  local udp = assert(socket.udp())
  assert(udp:setsockname("*",svrport))
  udp:settimeout(0)

putting asserts around the initializers will make sure that we stop the execution in case of errors. For the server, we define to listen on the serverport for just any address the computer has.

Setting the timeout to 0 is necessary, otherwise our script would just sleep as long as no message is received. Since receiving no messages for a frame is just ok, we don't want it to sleep. We will filter timeout messages and handle them as what they really are: no data received.

Since we can receive more than one message per update, we need to read out all data that was received till now. For this cause, we write a function that collects all messages together, ignoring timeout errors and throwing an error in case of other errors:

local function server ()
  (...)
  local function readall ()
    local list = {}
    while true do 
      local data,from,port = udp:receivefrom()
      if data == nil and from == "timeout" then return list end
      assert(data,from) -- another error was thrown!
      table.insert(list,{data = data, from = from, port = port})
    end
  end

Once a timeout was produced, we will return a list of all collected messages. We could freeze the server by sending nonstop udp messages, but as the security does not play a role here now, we ignore this case now.

Further, we need to loop now - receiving all messages over the time and reacting on the inputs. As we do not have defined a protocol right now, we want to print out what the clients have sent:

local function server ()
  (...)
  local clientlist = {}

  while true do
    local msglist = readall()
    for i,msg in ipairs(msglist) do 
      print(msg.data,msg.from,msg.port)
    end
    coroutine.yield() -- sleep
  end

We would stop the loop later by defining an exit case, so let's clodse the udp connection, once the loop has been stoped and close our server function:

  udp:close()
end

Step 2: The client

We program our client in a similiar way and design it as write only at the moment, so we just keep on sending a message each frame. This is not very complicated, as this script is now very short:

local function client()
  local udp = assert(socket.udp())
  udp:settimeout(0)
  assert(udp:setsockname("*",0))
  udp:setpeername(svraddr,svrport)

  while true do
    udp:send("Hello")
    coroutine.yield()
  end

  udp:close()
end

Step 3: Running the server and client

Before starting the server, we want to modify it to send back any data to our "clients":

function server ()
  (...)
  while true do
    local msglist = readall()
    for i,msg in ipairs(msglist) do 
      clientlist[msg.from] = msg.port
      for to,port in pairs(clientlist) do
        udp:sendto(msg.data,to,port)
      end      
    end
    coroutine.yield() -- sleep
  end

We just need to call both functions as timer functions now:

Timer.set("server",server,50)
Timer.set("client",client,50)

Our server will just enter any sender of data in our clientlist and we will send any received data to each "client" that we have collected. Of course, we should define later conditions how to remove clients from that list, including a verification if a client wants to connect, but for simplicity, we just ignore that now.

We haven't defined any print out result for received messages now, but this will be handled in our client method, as it should always receive what it has sent:

local function client()
  (...)
  while true do
    udp:send("Hello")

    while true do 
      local data,err = udp:receive()
      if not data and err=="timeout" then break end
      assert(data,err)
      print("Received",data)
    end
    coroutine.yield()
  end
end 

The code for receiving data looks quite similiar here, but we don't need to know who has sent it, since we defined that the server is our peer - we won't need to track this now.

We could start now other instances of luxinia (however, only one server can be opened, so once the server is started, the other instances will complain that they can't open the server, but that's ok), and see, that each client will receive what the other have sent.

Step 4: A more sophisticated handling of the clients

What we need now is a kind of a protocol, that defines how a client connects and how it says goodbye. We also want to define a chat system to broadcast chat messages. Our protocol supports then:
  • Connecting validation
  • Chatting
  • Disconnecting

For a connect, the server awaits the string "hello". It will answer it with "connected "+id, telling the client that it has been connected. It will ignore "hello" connects from connected clients. Further, we will handle "disconnect" from connected clients the instruction to remove it from the clientlist. A message that starts with "chat" will be delegated to each client, telling what the client has said, prepending the client's id, that we also need to specify in case of a connect.

Server clientmanagement:

  local clientlist = {}
  local clientids = {}
  local clientidcnt = 1

  local function broadcast (msg)
    -- just send a message to everyone
    for i,client in pairs(clientlist) do
      udp:sendto(msg,client.from,client.port)
    end
  end
The clientlist contains a list of tables that contain the client's address and port of its socket. The broadcast function will simply forward a message to each client. We ignore for now that this method of communication is quite inefficient - it would be better to collect all messages for each client until the end of the frame and send each message as a block. However, the protocol is too simple to support this and this technique is not part of this tutorial, as it should only show the basic principle.

Additionally, a received function is created which handles client inputs:

  local function received (from,port,data)
    local clientid = clientids[from..port]
    if not clientid then -- unkown client
      if data == "hello" then -- let it "connect"
        clientid,clientidcnt = clientidcnt,clientidcnt + 1
        local client = {
          id = clientid, 
          from = from, 
          port = port
        }
        clientlist[client.id] = client
        clientids[from..port] = client.id
        print("SERVER: NEW CLIENT")
        udp:sendto("connected "..client.id,from,port)
        broadcast("chat New client connected ("..client.id..")")
      end
      return -- in any case, just terminate her now 
    end

In first place, we try to identify the client and if we know him. We keep a list where the keys are made of the client's address and its port, identifying it with an ID. If the client is not known now, we check if the client wants to connect - this is the case if the client sent a "hello" message. In that case, we create a new clientid and store the client's data in the clientlist. We also send immediatly a message to the client that he is now connected and after that we broadcast a chat message that a new client has connected.

If the client is already known, we need to figure out what his message means:

    if data == "disconnect" then
      clientlist[clientid] = nil -- delete it from the list
      udp:sendto("disconnected",client.id,from,port)
      broadcast("chat Client "..client.id.." disconnected")
    end

    if data:match "^chat" then
      broadcast("chat ("..client.id.."): "..(data:sub(5)))
    end
  end

In case of a disconnect we remove the client from the list and broadcast aa chat message around that the client has left us.

If it is a chat message, we forward it to all participants in our list.

The server runnin code is now fairly simple:

  while true do
    local msglist = readall()
    for i,msg in ipairs(msglist) do 
      received(msg.from,msg.port,msg.data)
    end
    coroutine.yield() -- sleep
  end

Since the client needs a connect code, it is a bit more complicated. It might also happen that a server is not yet installed during the connect (i.e. if we start server and client at the same time just like here) and then the operating system might send a message that the requested port is not opened - which causes our client's udp socket to be closed. In that case, we want to try again to connect to the server:

local function client()
  print "starting client"
  local udp,myid

  local function connect ()
    udp = assert(socket.udp())
    udp:settimeout(0)
    assert(udp:setpeername(svraddr,svrport))
    print(udp:getsockname())
    udp:send("hello")
    while true do
      coroutine.yield()
      local answer,err = udp:receive()
      if not answer then
        if err ~= "timeout" then
          -- something went wrong ...
          udp:close()
          return connect() -- let's try again
        end
      else
        if answer:match "^connected" then 
          myid = answer:match("^connected (.*)")
          return 
        end
      end
    end
  end

Additionally, we could need now a receiving function similiar to the server's receive function (splitting stuff up in functions like that simplifies reading the code):

  local function receive ()    
    while true do 
      local data,err = udp:receive()
      if not data and err=="timeout" then 
        coroutine.yield() -- nothing received, sleep now
      else
        assert(data,err)
        return data
      end
    end
  end

An additional "say" function will create a chat message:

  local function say (str)
    udp:send("chat "..str)
  end

Once we have all the functions that we need, we just need to call them in order:

  connect() 
  print ("Connected and got id "..myid)

  say ("HELLO")
  while true do
    local msg = receive()
    if msg:match "^chat" then -- chatmessage
      print("CLIENT "..myid.." hears: "..msg)
    end
    coroutine.yield()
  end

  udp:send("disconnect")
  udp:close()

And that is pretty much all of it. We can start multiple clients now:

Timer.set("server",server,50)
TimerTask.new(
  function ()
    Timer.set("1stclient",function () client() end,50)
    Timer.set("2ndclient",function () client() end,50)
  end,200)

Both will start after an delay of 200 milliseconds and will connect and send a "HELLO" and print out any incoming messages.

The console output looks like this then:

 
starting client
127.0.0.1       2716
starting client
127.0.0.1       2717
SERVER: NEW CLIENT
SERVER: NEW CLIENT
Connected and got id 1
CLIENT 1 hears: chat New client connected (1)
Connected and got id 2
CLIENT 2 hears: chat New client connected (2)
CLIENT 1 hears: chat New client connected (2)
CLIENT 2 hears: chat (1):  HELLO
CLIENT 1 hears: chat (1):  HELLO
CLIENT 2 hears: chat (2):  HELLO
CLIENT 1 hears: chat (2):  HELLO

So every client listens what any other did say. This would also work over any network connection, as long as the server is visible in the network (switch of the firewall!).

UDP is as said not very reliable due to the possible packet loss and missing order of received packages. However UDP is much cheaper than TCP since there is not as much overhead. It is also quite simple to establish an UDP connection. You can combine TCP and UDP, sending all the really important data over the TCP channel (connect/disconnect, chat, gamelevel setup) while sending redundant data (position/movement information) over the UDP sockets.