23 September 2014
Tutorial: Local multiplayer with UDP/TCP
Today’s guest tutorial comes to you courtesy of Mark Steelman, founder of Steelman Games LLC. Mark is currently working on a turn-based RPG incorporating local multiplayer called Legend of Us Roleplaying Game. Before becoming an indie developer, Mark worked for four years at Electronic Arts as a game designer. You can follow the progress of his projects on Facebook, Google+, or by subscribing to his newsletter.
Make devices find each other with UDP
As noted in the introduction, I’m building a turn-based role-playing game using Corona SDK. As part of the design, I wanted local multiplayer and I also wanted it to be cross-platform. After a lot of research, I figured out how to accomplish this and, in this tutorial, I will share some of what I learned. Even better: this architecture will work for both turn-based and action games, so stick around if you’re making an action game!
This tutorial assumes that you have a basic understanding of Corona SDK, Lua, and peer-to-peer networking. We are going to use LuaSocket which is included in Corona SDK under the socket library. Because this design is intended for cross-platform multiplayer, it’s not going to use any native systems. While it does assume that the player has a Local Area Network (LAN), that network doesn’t need to be connected to the internet.
Including LuaSocket
In order to use LuaSocket, you must first include it:
1 |
local socket = require( "socket" ) |
That’s the easy part — let’s continue…
UDP and TCP
First, let me give a brief explanation of UDP and TCP. Both of these are protocols which allow computers to talk to each other and each one has certain advantages and disadvantages. However, I’m only going to cover features that are relevant to the tutorial.
UDP has the ability to send messages to an address without knowing if anything is there. It doesn’t check to see if the message made it anywhere — it simply transmits the message. UDP also allows you to listen to an address without being connected to a computer at that address.
TCP is a reliable protocol. It sends messages to another computer that it’s connected to via a TCP socket. If the other computer responds that there was a problem with the message, it sends the message again.
Knowing this, you might ask “Why would anyone use UDP when TCP is so reliable?” Well, there are several reasons, but the reason most relevant to this tutorial is the fact that you must know the IP address of the server in order to use TCP. Any network which you can join without visiting the LAN administrator is assigning you a random IP address using DHCP. This means that we’ll need to discover the IP address of the server on our own. Fortunately, UDP can help with that.
Advertise the server
For local multiplayer to work, one of the devices must act as the server/host. The host doesn’t need to have special privileges in your game, but the primary record of the game in progress will be stored on this device. In the language of role-playing games, let’s call this player the game master. This game master is going to announce his/her presence to the local network via some method. I utilize an “Invite” button which calls the following function upon being pressed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
local advertiseServer = function( button ) local send = socket.udp() send:settimeout( 0 ) --this is important (see notes below) local stop local counter = 0 --using this, we can advertise our IP address for a limited time local function broadcast() local msg = "AwesomeGameServer" --multicast IP range from 224.0.0.0 to 239.255.255.255 send:sendto( msg, "228.192.1.1", 11111 ) --not all devices can multicast so it's a good idea to broadcast too --however, for broadcast to work, the network has to allow it send:setoption( "broadcast", true ) --turn on broadcast send:sendto( msg, "255.255.255.255", 11111 ) send:setoption( "broadcast", false ) --turn off broadcast counter = counter + 1 if ( counter == 80 ) then --stop after 8 seconds stop() end end --pulse 10 times per second local serverBroadcast = timer.performWithDelay( 100, broadcast, 0 ) button.stopLooking = function() timer.cancel( serverBroadcast ) --cancel timer button.stopLooking = nil end stop = button.stopLooking end |
Here are some notes about this code:
- Multicast is a device feature which allows one device to communicate with several. iPhones and iPads have it, but I’ve been told that iPods do not. I haven’t tried it on any Android devices, so maybe somebody can test it and report their results in the comments section. As a result of this inconsistency, we also use broadcast. “Why don’t we just use broadcast?” you might ask. Well, the catch with broadcast is that the LAN has to allow broadcasts. By using both, we are maximizing the chance of finding each other.
- The “pulse” of the timer is ten times per second. I don’t recommend setting your timer pulse faster than that unless you have a good reason — after all, your game needs time to do other things. This is a standard pulse speed for most action games including MMOs.
- The port you choose can be anything between
1
and65535
, however, applications almost always block the port that they use and you’ll get an error if you try to bind to a port that is currently in use. Likewise, if you bind to a port, you need to unbind/close the port when you end the game so you don’t block it indefinitely on the players device. Lower number ports are used by commonly run applications, so it’s best to use a port between1024
and65535
. - The
settimeout()
function allows you to tell the socket how long to wait for a message before moving on. Default is to wait indefinitely, meaning that your game freezes until it gets a message. Setting it to0
tells it to just check and if there’s nothing to receive and move on to the next task.
Finding the server
The client will need to know its own IP address for the next step. Fortunately, UDP in LuaSocket can help with that:
1 2 3 4 5 6 7 |
local getIP = function() local s = socket.udp() --creates a UDP object s:setpeername( "74.125.115.104", 80 ) --Google website local ip, sock = s:getsockname() print( "myIP:", ip, sock ) return ip end |
The IP address in the above function is arbitrary — I used the Google address because I know it. You don’t even need to be connected to the internet for this function to return your IP address, but you must at least be on a local network.
Listening for the server
Now we are prepared to listen for the server. We will recognize the server because of the message AwesomeGameServer
. Obviously, this could be any string; we are just going to match strings.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
local function findServer( button ) local newServers = {} local msg = "AwesomeGameServer" local listen = socket.udp() listen:setsockname( "226.192.1.1", 11111 ) --this only works if the device supports multicast local name = listen:getsockname() if ( name ) then --test to see if device supports multicast listen:setoption( "ip-add-membership", { multiaddr="226.192.1.1", interface = getIP() } ) else --the device doesn't support multicast so we'll listen for broadcast listen:close() --first we close the old socket; this is important listen = socket.udp() --make a new socket listen:setsockname( getIP(), 11111 ) --set the socket name to the real IP address end listen:settimeout( 0 ) --move along if there is nothing to hear local stop local counter = 0 --pulse counter local function look() repeat local data, ip, port = listen:receivefrom() --print( "data: ", data, "IP: ", ip, "port: ", port ) if data and data == msg then if not newServers[ip] then print( "I hear a server:", ip, port ) local params = { ["ip"]=ip, ["port"]=22222 } newServers[ip] = params end end until not data counter = counter + 1 if counter == 20 then --stop after 2 seconds stop() end end --pulse 10 times per second local beginLooking = timer.performWithDelay( 100, look, 0 ) function stop() timer.cancel( beginLooking ) button.stopLooking = nil evaluateServerList( newServers ) --do something with your found servers listen:close() --never forget to close the socket! end button.stopLooking = stopLooking end |
I put a lot of inline comments above, but I’ll elaborate on a few things:
- Notice that we account for the fact that not all devices have Multicast.
- The
receivefrom()
function is going to just pull in anything that’s at that address, so we need to filter it. This is why we have the string message to compare with. - When two devices find each other, it can get painful if they both have a short duration. I like to make the server wait much longer than the clients. If the server is advertising, the client finds them pretty quick. Basically, I just want to avoid “Can you try that again? I missed it.”
- In this example, I’m passing in a reference to the button that the player pressed to activate the function. I do this because so the player can push it again and stop broadcasting. If you don’t want to do that, you don’t need the button reference.
So, at this point, we know how to let the game master be discovered by the players. The essential IP address required to use TCP is attached to the UDP message. Now that we have the game master’s IP address, we can connect to their device using TCP.
Swapping strings
Now we’ll discuss how to create a TCP server, connect to it, and send messages back and forth.
First, let’s discuss what TCP will provide and what it won’t. Like I said above, once we have a connection between devices, they’ll be able to send messages back and forth. These messages will just be strings. Imagine it like a text message app — in this case, the app on one device sends texts to the app on another device. These messages are then interpreted by the apps on each device and some action occurs.
Security
This tutorial will not go in depth about security, but a couple points should be covered:
- The server and client can only control each other as far as you allow it. As I’ve iterated several times now, TCP just sends and receives text strings. For a Pac-Man clone that could be controlled by a second device, about the only information the server would need is “BEGIN”, “UP”, “DOWN”, “LEFT”, and “RIGHT” — all else could simply be ignored.
- You should never try to make your app accept functions that have been turned into a string. Let the client and the server have their own functions and just use the transmitted text to call the functions. If your app accepts functions, you open up a very serious security vulnerability, so don’t do it! Instead, just pass commands with parameters.
In any case, don’t lie awake at night worrying about this. Neither iOS nor Android will let you damage someone’s device with such foolishness, but it may ruin the install of your game!
Starting the server
The server runs in a periodic loop. On each iteration of the loop, it checks to see if any clients want to join and whether connected clients sent a message. If the buffer has any messages to send out, it sends them. Here’s a basic TCP server module with further explanation following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
local S = {} local socket = require( "socket" ) local clientList = {} local clientBuffer = {} S.getIP = function() local s = socket.udp() s:setpeername( "74.125.115.104", 80 ) local ip, sock = s:getsockname() print( "myIP:", ip, sock ) return ip end S.createServer = function() local tcp, err = socket.bind( S.getIP(), 22222 ) --create a server object tcp:settimeout( 0 ) local function sPulse() repeat local client = tcp:accept() --allow a new client to connect if client then print( "found client" ) client:settimeout( 0 ) --just check the socket and keep going --TO DO: implement a way to check to see if the client has connected previously --consider assigning the client a session ID and use it on reconnect. clientList[#clientList+1] = client clientBuffer[client] = { "hello_client\n" } --just including something to send below end until not client local ready, writeReady, err = socket.select( clientList, clientList, 0 ) if err == nil then for i = 1, #ready do --list of clients who are available local client = ready[i] local allData = {} --this holds all lines from a given client repeat local data, err = client:receive() --get a line of data from the client, if any if data then allData[#allData+1] = data end until not data if ( #allData > 0 ) then --figure out what the client said to the server for i, thisData in ipairs( allData ) do print( "thisData: ", thisData ) --do stuff with data end end end for sock, buffer in pairs( clientBuffer ) do for _, msg in pairs( buffer ) do --might be empty local data, err = sock:send( msg ) --send the message to the client end end end end --pulse 10 times per second local serverPulse = timer.performWithDelay( 100, sPulse, 0 ) local function stopServer() timer.cancel( serverPulse ) --cancel timer tcp:close() for i, v in pairs( clientList ) do v:close() end end return stopServer end return S |
And that is a basic server. Let’s start at the top with some explanation:
socket.bind()
creates a server object which you bind to the port of your choice. I used11111
, but you can use any that we listed in the earlier section. Remember to close the TCP object when you shut down the server via thestopServer()
function!settimeout( 0 )
tells LuaSocket to move on if there’s no information waiting at the socket.accept()
returns a client object which represents the connection to the other device. Each client will get their own object and each one will need to be closed when the game is done. We do this in the function at the bottom calledstopServer()
.socket.select()
goes through our list of client connections to see which are available. Any that are not available are ignored but not closed.receive()
receives one line of data. You can designate a line of data in a string by putting\n
at the end. It’s simple and you’ll be able to create bite-sized pieces of data. This function is structured so that you end up with a numbered table of string lines. They are numbered in the order that they were received, but you can’t rely on the lines you send being received in the order you sent them. If this is important, and it often is, you’ll need to create a way for the server to know if a line is in the right order.- Next we go through the list of lines and interpret them. This is usually just a series of
if
–then
statements with a liberal use of the string library. - Finally, we send whatever is in the buffer. The buffer is another list of strings. Again, you can’t absolutely control the order in which they are received. You don’t have to use a buffer, but when you are using a multi-use device like a phone as a server, it’s a good idea. You may just
:send()
to a client socket at any time but the only way the device knows that the message didn’t go through is if the other device responds. If the other device is taking a call, it will ignore your message and the message will be lost. If you implement a buffer, it sends the message every pulse until something happens that removes the message from the buffer, however you’ll need to implement a way of knowing when to remove items from the buffer.
Connecting to the server
Connecting to the server is much simpler:
1 2 3 4 5 6 7 8 9 10 |
local function connectToServer( ip, port ) local sock, err = socket.connect( ip, port ) if sock == nil then return false end sock:settimeout( 0 ) sock:setoption( "tcp-nodelay", true ) --disable Nagle's algorithm sock:send( "we are connected\n" ) return sock end |
To elaborate on this slightly:
socket.connect
is pretty self explanatory: attempt to connect to the server at that address.settimeout( 0 )
again lets the socket know that you want it to just check the socket and move on if there’s no incoming message.- Nagle’s algorithm is a standard function that causes the socket to aggregate data until the data is of a certain size, then send it. If you are just going to send “UP” and you want it sent right away, you’ll want this off.
What’s not included in this example is a method to determine if the client is connecting for the first time or reconnecting (return session). This is outside the scope of this tutorial, but one option is to use a session ID which the client gets the first time it connects to the server. In this case, both the client and the server save the ID. Then, if the client loses the connection, this ID is sent upon reconnection and the server can update the client’s data with the new client socket.
Client loop
The final piece of the puzzle is the client loop. This will look very much like the server loop, but it never tries to accept connections.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
local function createClientLoop( sock, ip, port ) local buffer = {} local clientPulse local function cPulse() local allData = {} local data, err repeat data, err = sock:receive() if data then allData[#allData+1] = data end if ( err == "closed" and clientPulse ) then --try again if connection closed connectToServer( ip, port ) data, err = sock:receive() if data then allData[#allData+1] = data end end until not data if ( #allData > 0 ) then for i, thisData in ipairs( allData ) do print( "thisData: ", thisData ) --react to incoming data end end for i, msg in pairs( buffer ) do local data, err = sock:send(msg) if ( err == "closed" and clientPulse ) then --try to reconnect and resend connectToServer( ip, port ) data, err = sock:send( msg ) end end end --pulse 10 times per second clientPulse = timer.performWithDelay( 100, cPulse, 0 ) local function stopClient() timer.cancel( clientPulse ) --cancel timer clientPulse = nil sock:close() end return stopClient end |
Note that the client is always responsible for making the connection to the server. The server never tries to reach the client — it has enough to handle already. Beyond that, there isn’t anything clarify that wasn’t already covered in the server loop section.
Conclusion
I hope this tutorial helps you achieve your multiplayer dreams! While it’s not intended to be a comprehensive tutorial on networking, what I accomplished should help you get two devices talking with each other using TCP.
Thank you for reading and remember to check out Legend of Us Roleplaying Game which inspired me to figure all of this out.
Lerg
Posted at 16:41h, 23 SeptemberWow. Great topic!
egruttner
Posted at 19:01h, 23 SeptemberA-M-A-Z-I-N-G !!!
Thank you Rob!
Anton
Posted at 05:05h, 24 SeptemberIt would be great to see working example
simpleex
Posted at 01:01h, 26 September+1
Bob
Posted at 05:39h, 25 SeptemberMark,
Thanks for sharing your knowledge and experience. This should be helpful to many developers.
Bob
CK
Posted at 09:14h, 25 SeptemberThank you for the tutorial.
Does this also work with 2 devices connected via bluetooth?
Mark Steelman
Posted at 18:21h, 26 SeptemberBluetooth is a different protocol, it’s like TCP or UDP except it doesn’t need a LAN. As of right now, I don’t know how to get that to work with Corona but I’ll let you know if I figure it out.
As for demonstrating this code, just take all the code above and put it in a “main.lua” file. Then make two buttons, one to call the function to advertise the server and one to call the function to look for the server. Set up the server loop to kick off when you start the app. Now load the app on a device and run it. Also run it in the emulator. Push the server button in the emulator and the client button on the device (or vis versa) and you will see print messages in the console of each that will confirm it is working. This is assuming that both the emulator and the device are on the same network.
I didn’t explain that in detail in the tutorial because there are other tutorials on how to make a button and this tutorial is long enough as it is. You don’t have to use buttons of course, that is just an easy way to demonstrate the code.
Anton
Posted at 03:00h, 29 SeptemberThis does not work please make an example
Mark Steelman
Posted at 07:30h, 29 SeptemberPost what you did and I or someone else in the community will see if we can help you figure out why your code isn’t working.
Anton
Posted at 01:53h, 01 Octoberhttp://forums.coronalabs.com/topic/51548-local-multiplayer-with-udptcp/
Rob Miracle
Posted at 17:00h, 25 SeptemberAll thanks go to Mark for this!
CK: I doubt this will work over bluetooth unless you’re doing TCP/IP networking over Bluetooth.
Rob
Mark Steelman
Posted at 06:36h, 08 OctoberI looked at Anton’s example linked above and I need to make a couple of corrections to my tutorial. Rob is going to fix them inline but I will point them out here because it might help you not make the same mistake.
First, you need to use a different port for your server and for finding other devices. The server will block the port it is using. We are changing the port that the server uses in the tutorial to 22222 but any legal port is fine. Be sure in your code that you don’t try to start the server more than once or you are going to get a port blocked error.
Second, when you send a string over TCP you need to put “n” after it. The receive function in its default state reads a line. You need to tell it when the line has ended or it will keep waiting for the rest of the line before it prints.
Jv
Posted at 13:20h, 13 MayDoes this work for people connecting to your server outside your LAN?
Anish Krishna
Posted at 06:17h, 22 Julyi was making an app needing to send and receive just data bytes instead of string…
how can i do it
strings are going ok on UDP in my program…
AK
Posted at 07:07h, 22 Julyhow can i send a byte value (specifically from 0-255) in UDP and decode the same
I am able to send strings successfully in UDP
stevon8ter
Posted at 12:34h, 06 AugustI looked over this tutorial, and it turned out to be really useful.
I’m coding in CODEA, but this also has LuaSocket and thus most part could be reused, thanks for the great tutorial 🙂
stevon8ter
Posted at 10:19h, 10 AugustOne small thing tho, in “advertising the server”, there’s this:
send:sendto( msg, “228.192.1.1”, 11111 )
but the IP should be 226. …
since the client is listening to the 226. … as well 🙂
Adi
Posted at 00:17h, 19 SeptemberVery useful. Thanks for sharing.
Wesley
Posted at 14:14h, 03 NovemberI had to use the following code to get the local broadcast working. It basically replaces the last octet of your IP address with 255. A more correct solution would probably be to use the correct broadcast address based on the subnet mask but I was unsure of how to do this in CoronaSDK and I believe that this will work on most home networks (hopefully other networks support multicast or 255.255.255.255 broadcast).
--http://stackoverflow.com/questions/20459943/find-the-last-index-of-a-character-in-a-string
function findLast(haystack, needle)
local i=haystack:match(".*"..needle.."()")
if i==nil then return nil else return i-1 end
end
-- replace last octet of our ip address with '255'
function get_local_broadcast_address()
local ip = getIP()
local index = findLast(ip, "%.")
local substr = string.sub(ip,1,index) .. "255"
return substr
end
Adi
Posted at 23:31h, 05 NovemberGreat tutorial. Its been very helpful in our current game.
awais
Posted at 21:32h, 16 Novemberwhen i run on device Runtime error occurs ( attempt to index local “UDPBroadcast”(a nil value).)
Mohammad Fakhreddin
Posted at 14:07h, 21 JanuaryI checked your code change this part of your code to work correctly:
Your Main.lua
—————–
sendbF = function(event)
— Get a session ID (“sock”) from server
if(server_ip~=0) then
local sock = connectToServer(server_ip,1235)
createClientLoop(sock,server_ip,1235)
else
findServer(sendbF)
end
end
Mohammad Fakhreddin
Posted at 14:05h, 21 JanuaryThanks for your tutorial
I have to say it needs a few changes for working correctly on android devices :
I changed your client listener to 0.0.0.0
——————————————
Client:
———————————————————
listen = socket.udp() –make a new socket
listen:setsockname( “0.0.0.0”, 1119 ) –set the socket name to the real IP address
———————————————————
Server
————————————————-
print(“Advertise server”)
assert(send:setoption( “broadcast”, true )) –turn on broadcast
assert(send:sendto( msg, “255.255.255.255”, 1119 ))
assert(send:setoption( “broadcast”, false )) –turn off broadcast
Tapas
Posted at 11:21h, 22 FebruaryIts a great help. But in my case in the following code in createServer method
local ready, writeReady, err = socket.select( clientList, clientList, 0 )
Table ready is empty and writeReady contains the client data. Can you let me why is that.
Adi
Posted at 22:59h, 26 JuneSeems this code is no longer working in latest build 2016.2907. Perhaps it is due to the update of LuaSocket lib in build 2016.2883. Appreciate any advise on how to fix it.