Difference between revisions of "Tutorial:Networking with UDP"
(Client Exmaple...) |
m (their -> there) |
||
(19 intermediate revisions by 9 users not shown) | |||
Line 1: | Line 1: | ||
− | This is an introduction to | + | This is an introduction to networking, using ''Luasocket''. Don't run away! Luasocket is compiled into LÖVE, and is really not that bad once you get used to it. |
This tutorial assumes that you are familiar with [[Tutorial:Callback Functions|Callbacks]], and Lua in general. Networking should be considered a moderately advanced topic. | This tutorial assumes that you are familiar with [[Tutorial:Callback Functions|Callbacks]], and Lua in general. Networking should be considered a moderately advanced topic. | ||
− | There two basic kinds of | + | There are two basic kinds of sockets, and we'll be covering UDP in this tutorial. UDP networking is message-oriented (as opposed to TCP being stream-oriented), meaning that it's oriented around distinct (and otherwise independent) messages called ''datagrams''. |
In the long run it pays to have a solid understanding on how networking works, but for now lets just get cracking. :3 | 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 | + | We'll start with the LÖVE client, then follow up with a stand-alone server written in Lua. |
− | =The Client= | + | ==The Client== |
− | To start | + | To start, we need to require the "socket" lib. ''socket'' provides low-level networking features. |
<source lang="lua"> | <source lang="lua"> | ||
local socket = require "socket" | local socket = require "socket" | ||
Line 22: | Line 22: | ||
local t | local t | ||
</source> | </source> | ||
− | |||
− | |||
− | + | ===[[love.load]]=== | |
− | First up, we need a | + | First up, we need a UDP socket, from which we'll do all our networking. |
<source lang="lua"> | <source lang="lua"> | ||
function love.load() | function love.load() | ||
− | |||
udp = socket.udp() | udp = socket.udp() | ||
</source> | </source> | ||
− | Normally socket reads block until they have data | + | Normally socket reads block (cause your game to stop and wait) until they have data. That doesn't suit us, so we tell it not to do that by setting the ''timeout'' to zero. |
− | <source lang=lua> | + | <source lang="lua"> |
udp:settimeout(0) | udp:settimeout(0) | ||
</source> | </source> | ||
− | 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. | + | 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 ''udp:setpeername''. |
+ | |||
+ | {{notice|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.}} | ||
− | |||
<source lang=lua> | <source lang=lua> | ||
udp:setpeername(address, port) | udp:setpeername(address, port) | ||
</source> | </source> | ||
− | Seed the | + | Seed the [[wikipedia:PRNG|PRNG]], 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. |
+ | It's just a number, but it'll do. We'll just use ''math.random'' to give us a reasonably unique identity for little effort. | ||
− | + | {{notice|A random number to identify yourself isn't actually a very good way of doing this, but the "correct" ways are beyond the scope of this article.)}} | |
− | <source lang=lua> | + | |
+ | <source lang="lua"> | ||
math.randomseed(os.time()) | math.randomseed(os.time()) | ||
entity = tostring(math.random(99999)) | entity = tostring(math.random(99999)) | ||
</source> | </source> | ||
− | 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'. | + | 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 ''udp:setpeername'' earlier we don't even have to specify where to send it. | ||
− | Thats...it, really. | + | Thats... it, really. The rest of this is just putting this context and practical use. |
<source lang=lua> | <source lang=lua> | ||
local dg = string.format("%s %s %d %d", entity, 'at', 320, 240) | local dg = string.format("%s %s %d %d", entity, 'at', 320, 240) | ||
Line 62: | Line 63: | ||
end | end | ||
</source> | </source> | ||
− | |||
− | |||
+ | ===[[love.update]]=== | ||
We start with a little bit of nonsense involving ''t'' we declared earlier; | We start with a little bit of nonsense involving ''t'' we declared earlier; | ||
− | + | It's '''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 | + | ''(For the record, ten times a second is considered good for most normal games (including many [[wikipedia:Massively Multiplayer Online game|MMO]]s), 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. | + | We could send updates for every little move, but we'll consolidate the last update-worth here into a single packet, drastically reducing our bandwidth use. |
− | <source lang=lua> | + | <source lang="lua"> |
function love.update(deltatime) | function love.update(deltatime) | ||
t = t + deltatime -- increase t by the deltatime | t = t + deltatime -- increase t by the deltatime | ||
Line 83: | Line 83: | ||
</source> | </source> | ||
− | Again, we prepare a packet | + | Again, we prepare a packet ''payload'' using string.format, then send it on its way with udp:send. This is the move update mentioned above. |
<source lang=lua> | <source lang=lua> | ||
local dg = string.format("%s %s %f %f", entity, 'move', x, y) | local dg = string.format("%s %s %f %f", entity, 'move', x, y) | ||
Line 89: | Line 89: | ||
</source> | </source> | ||
− | And again! | + | And again! This is a request that the server send us an update for the world state. |
+ | |||
{{notice|In most designs you don't request world-state updates, you just get them sent to you periodically. | {{notice|In most designs you don't request world-state updates, you just get them sent to you periodically. | ||
− | + | There's various reasons for this, but there's one '''BIG''' one you will have to solemnly take note of: "anti-griefing". World-updates are probably one of biggest things the average game-server will pump out on a regular basis, and griefing 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}} | So they just don't support update requests, instead giving them out when they feel its appropriate}} | ||
+ | |||
<source lang=lua> | <source lang=lua> | ||
local dg = string.format("%s %s $", entity, 'update') | local dg = string.format("%s %s $", entity, 'update') | ||
Line 104: | Line 106: | ||
There could well be more than one message waiting for us, so we'll loop until we run out! | 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! | + | And here is something new, the much anticipated other end of <code>udp:send</code>! |
− | + | <code>udp:receive</code> will return a waiting packet (or nil, and an error message). | |
− | ''data'' is a string, the '''payload''' of the far-end's ''udp:send''. | + | ''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 [http://www.lua.org/manual/5.1/manual.html#5.4 string handling functions] is a must.) |
<source lang=lua> | <source lang=lua> | ||
repeat | repeat | ||
Line 114: | Line 116: | ||
</source> | </source> | ||
− | + | <code>string.match</code> 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 [http://www.lua.org/manual/5.1/manual.html#5.4.1 5.4.1:Patterns]) | |
<source lang=lua> | <source lang=lua> | ||
− | + | ent, cmd, parms = data:match("^(%S*) (%S*) (.*)") | |
if cmd == 'at' then | if cmd == 'at' then | ||
local x, y = parms:match("^(%-?[%d.e]*) (%-?[%d.e]*)$") | local x, y = parms:match("^(%-?[%d.e]*) (%-?[%d.e]*)$") | ||
</source> | </source> | ||
− | Confirming that the values you | + | Confirming that the values you received 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! | + | And don't forget, even if you matched a "number", the result is still a '''string'''! Thankfully conversion is easy in Lua using <code>tonumber()</code>. |
<source lang=lua> | <source lang=lua> | ||
assert(x and y) | assert(x and y) | ||
x, y = tonumber(x), tonumber(y) | x, y = tonumber(x), tonumber(y) | ||
− | world[ | + | world[ent] = {x=x, y=y} |
</source> | </source> | ||
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. | 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. | ||
Line 135: | Line 137: | ||
end | end | ||
</source> | </source> | ||
− | If ''data'' was | + | If ''data'' was <code>nil</code>, then ''msg'' will contain a short description of the problem (which are also double as error IDs...). |
− | The most common will be 'timeout', since we settimeout() to zero, | + | The most common will be <code>'timeout'</code>, since we <code>socket:settimeout()</code> to zero, any time there isn't data ''waiting'' for us, it'll <code>'timeout'</code>. 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. | ||
<source lang=lua> | <source lang=lua> | ||
Line 142: | Line 145: | ||
error("Network error: "..tostring(msg)) | error("Network error: "..tostring(msg)) | ||
end | end | ||
− | until not data | + | until not data |
− | |||
end | end | ||
</source> | </source> | ||
− | |||
− | |||
+ | ===love.draw=== | ||
Draw is stunningly simple, since its not really the meat of this example. | Draw is stunningly simple, since its not really the meat of this example. | ||
− | + | It just loops over the ''world'' table, and print the name (key) of everything in there, at its own stored co-ords. | |
<source lang=lua> | <source lang=lua> | ||
Line 161: | Line 162: | ||
</source> | </source> | ||
− | And | + | And that's the end of the Client code. |
+ | ==The Server== | ||
+ | The server is a little different, for starters its a stand-alone Lua program: it doesn't run in LÖVE. | ||
+ | |||
+ | Once again we begin by <code>require</code>ing socket, and creating a UDP socket. | ||
+ | |||
+ | ''(LuaSocket ''isn't'' compiled into Lua by default. If you are on Windows just get the all-in-one [https://github.com/rjpcomputing/luaforwindows/releases] installer, I wouldn't know for Mac, and Linux? you guys know what to do :3)'' | ||
+ | <source lang=lua> | ||
+ | local socket = require "socket" | ||
+ | local udp = socket.udp() | ||
+ | </source> | ||
+ | |||
+ | And once again, we set the <code>'timeout'</code> to zero. | ||
+ | |||
+ | But next we do something a little different; unlike the client, the server has to be specific about where its 'bound', or the poor clients will never find it. Thus while we can happily let the client auto-bind to whatever it likes, we have to tell the server to bind to something known. | ||
+ | |||
+ | The first part is which ''interface'' we should bind to, <code>'*'</code> basically means "all of them". ''port'' is simple, the system maintains a list of up to 65535 (!) "ports" ... really just numbers. | ||
+ | |||
+ | Point is that if you send to a particular port, then only things "listening" to that port will be able to receive it, and likewise you can only read data sent to ports you are listening too. | ||
+ | |||
+ | Generally speaking, if an address is which machine you want to talk to, then a port is what program on that machine you want to talk to. | ||
+ | |||
+ | {{notice| On some operating systems, ports between ''0'' and ''1024'' are "reserved for privileged processes". Its a security precaution for those system. Generally speaking, just not using ports in that range avoids a lot of problems}} | ||
+ | |||
+ | <source lang=lua> | ||
+ | udp:settimeout(0) | ||
+ | udp:setsockname('*', 12345) | ||
+ | </source> | ||
+ | |||
+ | We declare a whole bunch of local variables that we'll be using the in main server loop below. you probably recognise some of them from the client example, but you are also probably wondering what's with the fruity names, ''msg_or_ip''? ''port_or_nil''? | ||
+ | |||
+ | Well, we're using a slightly different function this time, you'll see when we get there. | ||
+ | |||
+ | <source lang=lua> | ||
+ | local world = {} -- the empty world-state | ||
+ | local data, msg_or_ip, port_or_nil | ||
+ | local entity, cmd, parms | ||
+ | </source> | ||
+ | Indefinite loops are probably not something you are used to if you only know love, but they are quite common. And in fact love has one at its heart, you just don't see it. Regardless, we'll be needing one for our server. And this little variable lets us ''stop'' it :3 | ||
+ | <source lang=lua> | ||
+ | local running = true | ||
+ | |||
+ | print "Beginning server loop." | ||
+ | while running do | ||
+ | </source> | ||
+ | This next line looks familiar, I'm sure, but we're using <code>udp:receivefrom()</code> this time. its similar to receive, but returns the data, sender's ip address, and the sender's port (which you'll hopefully recognise as the two things we need to send messages to someone). We didn't have to do this in the client example because we just bound the socket to the server, but that also ignores messages from sources other than what we've bound to, which obviously won't do at all as a server. | ||
+ | |||
+ | ''(Strictly, we could have just used <code>udp:receivefrom()</code> (and its counterpart, <code>udp:sendto()</code>) in the client. there's nothing special about the functions to prevent it, however <code>send</code>/<code>receive</code> are convenient and perform slightly better.)'' | ||
+ | |||
+ | <source lang=lua> | ||
+ | data, msg_or_ip, port_or_nil = udp:receivefrom() | ||
+ | if data then | ||
+ | -- more of these funky match paterns! | ||
+ | entity, cmd, parms = data:match("^(%S*) (%S*) (.*)") | ||
+ | </source> | ||
+ | The server implements a few more commands than the client does, the 'move' command updates the position of an entity ''relative to its current position'', 'at' simply sets an entity's location (which we saw in the client), then there's ''update'', which loops through the server's world-state and sends 'at' commands back to the client. and finally there's 'quit', which kills the server. | ||
+ | <source lang=lua> | ||
+ | if cmd == 'move' then | ||
+ | local x, y = parms:match("^(%-?[%d.e]*) (%-?[%d.e]*)$") | ||
+ | assert(x and y) -- validation is better, but asserts will serve. | ||
+ | -- don't forget, even if you matched a "number", the result is still a string! | ||
+ | -- thankfully conversion is easy in lua. | ||
+ | x, y = tonumber(x), tonumber(y) | ||
+ | -- and finally we stash it away | ||
+ | local ent = world[entity] or {x=0, y=0} | ||
+ | world[entity] = {x=ent.x+x, y=ent.y+y} | ||
+ | elseif cmd == 'at' then | ||
+ | local x, y = parms:match("^(%-?[%d.e]*) (%-?[%d.e]*)$") | ||
+ | assert(x and y) -- validation is better, but asserts will serve. | ||
+ | x, y = tonumber(x), tonumber(y) | ||
+ | world[entity] = {x=x, y=y} | ||
+ | elseif cmd == 'update' then | ||
+ | for k, v in pairs(world) do | ||
+ | udp:sendto(string.format("%s %s %d %d", k, 'at', v.x, v.y), msg_or_ip, port_or_nil) | ||
+ | end | ||
+ | elseif cmd == 'quit' then | ||
+ | running = false; | ||
+ | </source> | ||
+ | There's nothing much left to see, other than the ''socket.sleep' call, which helps reduce the CPU load of the server. | ||
+ | Generally, people seem to prefer to just rely on blocking behaviour (which we turned off) to keep CPU usage down, but its nice to know how do this without blocking. | ||
+ | <source lang=lua> | ||
+ | else | ||
+ | print("unrecognised command:", cmd) | ||
+ | end | ||
+ | elseif msg_or_ip ~= 'timeout' then | ||
+ | error("Unknown network error: "..tostring(msg)) | ||
+ | end | ||
+ | |||
+ | socket.sleep(0.01) | ||
+ | end | ||
+ | |||
+ | print "Thank you." | ||
+ | </source> | ||
+ | |||
+ | ==Conclusion== | ||
+ | * [[Tutorial:Networking with UDP-TheClient | The Complete Client Source]] | ||
+ | * [[Tutorial:Networking with UDP-TheServer | The Complete Server Source]] | ||
+ | |||
+ | [[wikipedia:User Datagram Protocol|UDP]] is simple in its usage, but also relies on the developer to get a lot more right, since UDP doesn't make any assurances about the order that datagrams arrive, or even that they arrive at all. These are things that you obviously have to take into account when designing your protocol. | ||
+ | |||
+ | Additionally UDP datagrams have limited sizes; the luasocket Documentation specifically notes that it doesn't support anything over 8k, and frankly you should assume much less. | ||
+ | |||
==See also== | ==See also== | ||
* [[Networking with TCP]] | * [[Networking with TCP]] | ||
− | + | * [http://w3.impa.br/~diego/software/luasocket/reference.html luaSocket Reference] | |
+ | * [http://www.beej.us/guide/bgnet/ Beej's Networking Tutorial] | ||
[[Category:Tutorials]] | [[Category:Tutorials]] | ||
Line 172: | Line 275: | ||
== Other languages == | == Other languages == | ||
− | {{i18n|Tutorial: | + | {{i18n|Tutorial:Networking with UDP}} |
Latest revision as of 18:35, 5 October 2023
This is an introduction to networking, using Luasocket. Don't run away! Luasocket is compiled into LÖVE, and is really not that bad once you get used to it.
This tutorial assumes that you are familiar with Callbacks, and Lua in general. Networking should be considered a moderately advanced topic.
There are two basic kinds of sockets, and we'll be covering UDP in this tutorial. UDP networking is message-oriented (as opposed to TCP being stream-oriented), meaning that it's oriented around distinct (and otherwise independent) messages called datagrams.
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 LÖVE client, then follow up with a stand-alone server written in Lua.
Contents
The Client
To start, we need to require the "socket" lib. 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
First up, we need a UDP socket, from which we'll do all our networking.
function love.load()
udp = socket.udp()
Normally socket reads block (cause your game to stop and wait) until they have data. 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 udp:setpeername.
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 PRNG, 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. It's just a number, but it'll do. We'll just use math.random to give us a reasonably unique identity for little effort.
A random number to identify yourself isn't actually a very good way of doing this, but the "correct" ways are beyond the scope of this article.) |
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 udp: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
We start with a little bit of nonsense involving t we declared earlier; It's 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'll 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 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.
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
!
udp: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)
ent, cmd, parms = data:match("^(%S*) (%S*) (.*)")
if cmd == 'at' then
local x, y = parms:match("^(%-?[%d.e]*) (%-?[%d.e]*)$")
Confirming that the values you received 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 using tonumber()
.
assert(x and y)
x, y = tonumber(x), tonumber(y)
world[ent] = {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 double as error IDs...).
The most common will be 'timeout'
, since we socket:settimeout()
to zero, any time 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
Draw is stunningly simple, since its not really the meat of this example. It just loops over the world table, and print the name (key) of everything in there, at its own stored co-ords.
function love.draw()
-- pretty simple, we
for k, v in pairs(world) do
love.graphics.print(k, v.x, v.y)
end
end
And that's the end of the Client code.
The Server
The server is a little different, for starters its a stand-alone Lua program: it doesn't run in LÖVE.
Once again we begin by require
ing socket, and creating a UDP socket.
(LuaSocket isn't compiled into Lua by default. If you are on Windows just get the all-in-one [1] installer, I wouldn't know for Mac, and Linux? you guys know what to do :3)
local socket = require "socket"
local udp = socket.udp()
And once again, we set the 'timeout'
to zero.
But next we do something a little different; unlike the client, the server has to be specific about where its 'bound', or the poor clients will never find it. Thus while we can happily let the client auto-bind to whatever it likes, we have to tell the server to bind to something known.
The first part is which interface we should bind to, '*'
basically means "all of them". port is simple, the system maintains a list of up to 65535 (!) "ports" ... really just numbers.
Point is that if you send to a particular port, then only things "listening" to that port will be able to receive it, and likewise you can only read data sent to ports you are listening too.
Generally speaking, if an address is which machine you want to talk to, then a port is what program on that machine you want to talk to.
udp:settimeout(0)
udp:setsockname('*', 12345)
We declare a whole bunch of local variables that we'll be using the in main server loop below. you probably recognise some of them from the client example, but you are also probably wondering what's with the fruity names, msg_or_ip? port_or_nil?
Well, we're using a slightly different function this time, you'll see when we get there.
local world = {} -- the empty world-state
local data, msg_or_ip, port_or_nil
local entity, cmd, parms
Indefinite loops are probably not something you are used to if you only know love, but they are quite common. And in fact love has one at its heart, you just don't see it. Regardless, we'll be needing one for our server. And this little variable lets us stop it :3
local running = true
print "Beginning server loop."
while running do
This next line looks familiar, I'm sure, but we're using udp:receivefrom()
this time. its similar to receive, but returns the data, sender's ip address, and the sender's port (which you'll hopefully recognise as the two things we need to send messages to someone). We didn't have to do this in the client example because we just bound the socket to the server, but that also ignores messages from sources other than what we've bound to, which obviously won't do at all as a server.
(Strictly, we could have just used udp:receivefrom()
(and its counterpart, udp:sendto()
) in the client. there's nothing special about the functions to prevent it, however send
/receive
are convenient and perform slightly better.)
data, msg_or_ip, port_or_nil = udp:receivefrom()
if data then
-- more of these funky match paterns!
entity, cmd, parms = data:match("^(%S*) (%S*) (.*)")
The server implements a few more commands than the client does, the 'move' command updates the position of an entity relative to its current position, 'at' simply sets an entity's location (which we saw in the client), then there's update, which loops through the server's world-state and sends 'at' commands back to the client. and finally there's 'quit', which kills the server.
if cmd == 'move' then
local x, y = parms:match("^(%-?[%d.e]*) (%-?[%d.e]*)$")
assert(x and y) -- validation is better, but asserts will serve.
-- don't forget, even if you matched a "number", the result is still a string!
-- thankfully conversion is easy in lua.
x, y = tonumber(x), tonumber(y)
-- and finally we stash it away
local ent = world[entity] or {x=0, y=0}
world[entity] = {x=ent.x+x, y=ent.y+y}
elseif cmd == 'at' then
local x, y = parms:match("^(%-?[%d.e]*) (%-?[%d.e]*)$")
assert(x and y) -- validation is better, but asserts will serve.
x, y = tonumber(x), tonumber(y)
world[entity] = {x=x, y=y}
elseif cmd == 'update' then
for k, v in pairs(world) do
udp:sendto(string.format("%s %s %d %d", k, 'at', v.x, v.y), msg_or_ip, port_or_nil)
end
elseif cmd == 'quit' then
running = false;
There's nothing much left to see, other than the socket.sleep' call, which helps reduce the CPU load of the server. Generally, people seem to prefer to just rely on blocking behaviour (which we turned off) to keep CPU usage down, but its nice to know how do this without blocking.
else
print("unrecognised command:", cmd)
end
elseif msg_or_ip ~= 'timeout' then
error("Unknown network error: "..tostring(msg))
end
socket.sleep(0.01)
end
print "Thank you."
Conclusion
UDP is simple in its usage, but also relies on the developer to get a lot more right, since UDP doesn't make any assurances about the order that datagrams arrive, or even that they arrive at all. These are things that you obviously have to take into account when designing your protocol.
Additionally UDP datagrams have limited sizes; the luasocket Documentation specifically notes that it doesn't support anything over 8k, and frankly you should assume much less.
See also
Other languages
Dansk –
Deutsch –
English –
Español –
Français –
Indonesia –
Italiano –
Lietuviškai –
Magyar –
Nederlands –
Polski –
Português –
Română –
Slovenský –
Suomi –
Svenska –
Türkçe –
Česky –
Ελληνικά –
Български –
Русский –
Српски –
Українська –
עברית –
ไทย –
日本語 –
正體中文 –
简体中文 –
Tiếng Việt –
한국어
More info