- Introduction to lua
- Basic Setup
- Estrela for Luxinia
- Scenegraphs
- Scene Setup
- Models/Meshes
- Input handling
- Basic physics
- Physics surface properties
- Vehicle physics
- Using models
- Picking
- A Simple UDP Server
- GUI for the UDP Server
- Tetris attack clone prototype
- Stencil Shadows
- Sprite animation with matobject
- Project archives
- Estrela for Cg
- Specular mapping shader
- Material and Matobject
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)
If a connection cannot be established, there are various reasons why this could be so:
- 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. - 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. - 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
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 endThe 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.
