Difference between revisions of "Tutorial:Networking with UDP (Français)"
(→love.update) |
(→Other languages) |
||
(2 intermediate revisions by the same user not shown) | |||
Line 72: | Line 72: | ||
''(Pour information, dix fois par seconde est considéré comme bon pour la majortié des jeux normaux (incluant de nombreux [[wikipedia:fr:Jeu_en_ligne_massivement_multijoueur|MMO]]s), et vous ne devriez jamais vraiment avoir besoin de plus que 30 mises à jour par seconde, même pour les jeu avec déplacements rapides.)'' | ''(Pour information, dix fois par seconde est considéré comme bon pour la majortié des jeux normaux (incluant de nombreux [[wikipedia:fr:Jeu_en_ligne_massivement_multijoueur|MMO]]s), et vous ne devriez jamais vraiment avoir besoin de plus que 30 mises à jour par seconde, même pour les jeu avec déplacements rapides.)'' | ||
− | Nous pourrions envoyer des mises à jour pour chaque petit mouvement, mais nous allons | + | Nous pourrions envoyer des mises à jour pour chaque petit mouvement, mais nous allons ici consolider la valeur de la dernière mise à jour dans un seul paquet, réduisant drastiquement notre utilisation de la bande passante. |
<source lang="lua"> | <source lang="lua"> | ||
function love.update(deltatime) | function love.update(deltatime) | ||
− | t = t + deltatime -- | + | t = t + deltatime -- Augmente t par le delta (différence) temps |
if t > updaterate then | if t > updaterate then | ||
Line 85: | Line 85: | ||
</source> | </source> | ||
− | + | De nous vous, nous préparons un paquet ''payload'' en utilisant string.format, puis l'envoyons sur son chemin à l'aide de udp:send. C'est la mise à jour de déplacement mentionné au dessus. | |
<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 266: | Line 266: | ||
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. | 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. | ||
− | == | + | ==Voir également== |
− | * [[Networking with TCP]] | + | * [[Networking with TCP (Français)]] |
* [http://w3.impa.br/~diego/software/luasocket/reference.html luaSocket Reference] | * [http://w3.impa.br/~diego/software/luasocket/reference.html luaSocket Reference] | ||
* [http://beej.us/guide/bgnet/output/html/singlepage/bgnet.html Beej's Networking Tutorial] | * [http://beej.us/guide/bgnet/output/html/singlepage/bgnet.html Beej's Networking Tutorial] | ||
− | [[Category:Tutorials]] | + | [[Category:Tutorials (Français)]] |
{{#set:LOVE Version=0.7.1}} | {{#set:LOVE Version=0.7.1}} | ||
{{#set:Description=Networking with UDP}} | {{#set:Description=Networking with UDP}} | ||
− | == | + | == Autres langues == |
{{i18n|Tutorial:Networking with UDP}} | {{i18n|Tutorial:Networking with UDP}} |
Latest revision as of 01:03, 31 December 2020
Traduction en cours, vous êtes les bienvenus pour y participer |
Ceci est une introduction au réseau en utilisant Luasocket. De vous enfuyez pas ! Luasocket est compilé dans LÖVE, et il vaudra le coup, lorsque vous vous y serez habitué.
Ce tutoriel assume que vous êtes familier avec les fonctions de rappel voir Tutoriel:Fonctions de rappel, et Lua en général. Le réseau devrait être considéré comme un sujet modérément avancé.
Il y a deux types de sockets (chaussettes de connexion) basiques, et nous allons couvrir UDP dans ce tutoriel. les échanges UDP sont orienté message (par opposition à TCP qui est orienté flux), cela signifie qu'il est orienté autour de messages distincts (et également indépendants) appelés des « datagrammes » (datagrams).
Dans le long parcours, une compréhension solide du fonctionnement du réseau est payante, mais pour le moment laissons nous craquer. :3 Nous allons commencer par le client LÖVE, puis poursuivre par un serveur indépendant écrit en Lua.
Contents
Le client
Pour commencer, nous avons besoin de demander la bibliothèque « socket ». socket fournit les fonctionnalités réseau de bas niveau.
local socket = require "socket"
-- L'adresse et port du serveur
local address, port = "localhost", 12345
local entity -- Une entité est ce que nous contrôlons
local updaterate = 0.1 -- Temps d'attente en secondes, avant de demander une mise à jour
local world = {} -- L'état Monde vide
local t
love.load
Premièrement, nous avons besoin d'un socket UDP, depuis lequel nous ferons toutes nos opérations réseau.
function love.load()
udp = socket.udp()
Habituellement, un socket lit des blocs (provoquant l'arrêt et l'attente de votre jeu) jusqu'à l'obtention des données. Cela ne nous convientpas, nous lui disons de ne pas faire cela en réglant le temps avant expiration (timeout) à zéro.
udp:settimeout(0)
Contrairement au serveur, nous allons uniquement échanger avec une seule machine, nous allons donc « connecter » ce socket à l'adresse et port du serveur en utilisant udp:setpeername.
udp:setpeername(address, port)
Semons le générateur de nombres pseudo-aléatoires (PRNG), de façon à ne pas obtenir les mêmes nombres à chaque fois. L'entité (entity) sera ce que nous contrôlons, dans l'interêt de ce tutoriel. C'est simplement un nombre, mais nous allons le faire. Nous utilisons simplement la fonction math.random (math.hasard) qui nous donne une identité raisonnablement unique avec peu d'éfforts.
Un nombre aléatoire pour s'identifier soi-même n'est pas une très bonne méthode de fonctionnement, mais les méthodes correctes vont au delà du but de cet article.) |
math.randomseed(os.time())
entity = tostring(math.random(99999))
Nous faisons ici, notre premier action réelle de réseau : Nous créeons une chaîne de caractères contenant les données que nous voulons envoyer (en utilisant string.format) puis nous les envoyons en utilisant udp.send. Comme nous avons utilisé udp:setpeername précédemment, nous n'avons même pas besoin de spécifier où nous l'envoyons.
C'est... tout, réellement. Le reste ne sert qu'à placer ce contexte et l'utilisation pratique.
local dg = string.format("%s %s %d %d", entity, 'at', 320, 240)
udp:send(dg) -- La ligne magique en question.
-- t n'est qu'une variable que nous utilisons pour nous aider avec la fréquence de mise à jour dans love.update.
t = 0 -- (re)set t to 0
end
love.update
Nous commençons avec un peu de non-sens en impliquant t que nous avons déclaré plus tôt ; Il est très facile de saturer complétement une connexion réseau si nous ne sommes pas attentif aux paquets que nous envoyons (ou demandons !), nous mettons donc des barrières hedge à nos chances en limitant la fréquence à laquelle nous envoyons (ou demandons) des mises à jour.
(Pour information, dix fois par seconde est considéré comme bon pour la majortié des jeux normaux (incluant de nombreux MMOs), et vous ne devriez jamais vraiment avoir besoin de plus que 30 mises à jour par seconde, même pour les jeu avec déplacements rapides.)
Nous pourrions envoyer des mises à jour pour chaque petit mouvement, mais nous allons ici consolider la valeur de la dernière mise à jour dans un seul paquet, réduisant drastiquement notre utilisation de la bande passante.
function love.update(deltatime)
t = t + deltatime -- Augmente t par le delta (différence) temps
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
De nous vous, nous préparons un paquet payload en utilisant string.format, puis l'envoyons sur son chemin à l'aide de udp:send. C'est la mise à jour de déplacement mentionné au dessus.
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 their, 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 Lua for Windows 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.
Voir également
Autres langues
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