Tutorial:Networking with UDP

This is an introduction to Networking, using luaSocket. Don't run away! Sockets are compiled into love (even if that fact isn't mentioned as often as it should be), and are really not that bad once you get used to them.

This tutorial assumes that you are familiar with Callbacks, and Lua in general. Networking should be considered a moderately advanced topic.

There two basic kinds of Sockets, We'll be covering UDP sockets here. UDP Networking is packet-based, meaning that its oriented around distinct (and otherwise independent) messages called packets.

In the long run it pays to have a solid understanding on how networking works, but for now lets just get cracking. :3 We'll start with the Love client, then follow with a stand-alone server written in lua

The Client

To start with, we need to require the 'socket' lib (which is compiled into love). socket provides low-level networking features.

local socket = require "socket"

-- the address and port of the server
local address, port = "localhost", 12345

local entity -- entity is what we'll be controlling
local updaterate = 0.1 -- how long to wait, in seconds, before requesting an update

local world = {} -- the empty world-state
local t

love.load

Hopefully you are familiar with it from the Callbacks tutorial.


First up, we need a udp socket, from which we'll do all out networking.

function love.load()

	udp = socket.udp()

Normally socket reads block until they have data, or a certain amout of time passes. that doesn't suit us, so we tell it not to do that by setting the 'timeout' to zero.

	udp:settimeout(0)

Unlike the server, we'll just be talking to the one machine, so we'll "connect" this socket to the server's address and port using setpeername.

(NOTE: UDP is actually connectionless, this is purely a convenience provided by the socket library, it doesn't actually change the 'bits on the wire', and in-fact we can change / remove this at any time.)

	udp:setpeername(address, port)

Seed the random number generator, so we don't just get the same numbers each time. entity will be what we'll be controlling, for the sake of this tutorial its just a number, but it'll do. we'll just use random to give us a reasonably unique identity for little effort.

(NOTE: random isn't actually a very good way of doing this, but the "correct" ways are beyond the scope of this article. the *simplest* is just an autocount, they get a *lot* more fancy from there on in)

	math.randomseed(os.time()) 
	entity = tostring(math.random(99999))

Here we do our first bit of actual networking: we set up a string containing the data we want to send (using 'string.format') and then send it using 'udp.send'. since we used 'setpeername' earlier we don't even have to specify where to send it.

Thats...it, really. the rest of this is just putting this context and practical use.

	local dg = string.format("%s %s %d %d", entity, 'at', 320, 240)
	udp:send(dg) -- the magic line in question.
	
	-- t is just a variable we use to help us with the update rate in love.update.
	t = 0 -- (re)set t to 0
end

love.update

Hopefully you are familiar with it from the Callbacks tutorial.

We start with a little bit of nonsense involving t we declared earlier; Its *very easy* to completely saturate a network connection if you aren't careful with the packets we send (or request!), so we hedge our chances by limiting how often we send (and request) updates.

(For the record, ten times a second is considered good for most normal games (including many MMOs), and you shouldn't ever really need more than 30 updates a second, even for fast-paced games.)

We could send updates for every little move, but we consolidate the last update-worth here into a single packet, drastically reducing our bandwidth use.

function love.update(deltatime)
	t = t + deltatime -- increase t by the deltatime
	
	if t > updaterate then
		local x, y = 0, 0
		if love.keyboard.isDown('up') then 	y=y-(20*t) end
		if love.keyboard.isDown('down') then 	y=y+(20*t) end
		if love.keyboard.isDown('left') then 	x=x-(20*t) end
		if love.keyboard.isDown('right') then 	x=x+(20*t) end

Again, we prepare a packet *payload* using string.format, then send it on its way with udp:send this one is the move update mentioned above.

		local dg = string.format("%s %s %f %f", entity, 'move', x, y)
		udp:send(dg)

And again! this is a request that the server send us an update for the world state.

O.png In most designs you don't request world-state updates, you just get them sent to you periodically.

Theres various reasons for this, but theres one *BIG* one you will have to solemnly take note of: 'anti-griefing'. World-updates are probably one of biggest things the average gameserver will pump out on a regular basis, and greifing with forged update requests would be simple effective. So they just don't support update requests, instead giving them out when they feel its appropriate

 


		local dg = string.format("%s %s $", entity, 'update')
		udp:send(dg)

		t=t-updaterate -- set t for the next round
	end


There could well be more than one message waiting for us, so we'll loop until we run out!

And here is something new, the much anticipated other end of udp:send! 'receive' will return a waiting packet (or nil, and an error message). data is a string, the payload of the far-end's udp:send. we can deal with it the same ways we could deal with any other string in lua (needless to say, getting familiar with lua's string handling functions is a must.)

	repeat
		data, msg = udp:receive()

		if data then -- you remember, right? that all values in lua evaluate as true, save nil and false?

string.match is our friend here, its part of string.*, and data is (or should be!) a string. that funky set of characters bares some explanation, though. (Which I haven't gotten to, but I'll leave you with a link to 5.4.1:Patterns)

			entity, cmd, parms = data:match("^(%S*) (%S*) (.*)")
			if cmd == 'at' then
				local x, y = parms:match("^(%-?[%d.e]*) (%-?[%d.e]*)$")

Confirming that the values you receved are what you expect is important, since you never known who or what is on the other end (or in between...). since this is just an example, we'll just use asserts.

And don't forget, even if you matched a "number", the result is still a string! thankfully conversion is easy in Lua.

				assert(x and y)
				x, y = tonumber(x), tonumber(y)
				world[entity] = {x=x, y=y}

This case shouldn't trigger often, but its always a good idea to check (and log!) any unexpected messages and events. It can help you find bugs in your code...or people trying to hack the server. Never forget, you can not trust the client!

			else
				print("unrecognised command:", cmd)
			end

If data was nil, then msg will contain a short description of the problem (which are also error id...). The most common will be 'timeout', since we settimeout() to zero, anytime there isn't data *waiting* for us, it'll timeout. But we should check to see if its a *different* error, and act accordingly. in this case we don't even try to save ourselves, we just error out.

		elseif msg ~= 'timeout' then 
			error("Network error: "..tostring(msg))
		end
	until not data 

end

love.draw

Hopefully you are familiar with it from the Callbacks tutorial.

Draw is stunningly simple, since its not really the meat of this example. it just just loops over the world table, and print the name (key) of everything in their, at its own stored coords.

function love.draw()
	-- pretty simple, we 
	for k, v in pairs(world) do
		love.graphics.print(k, v.x, v.y)
	end
end

And thats the end of the Client code.

See also



Other languages