I noticed there was no UDP (FastConnection) tutorial included with EE.
I have been converting my game to RUDP and so decided to make a little tutorial using what I learned. Its useful to see how to use UDP in EE as well as a simple way to do reliable-ordered UDP. See the attached esenthelproject, I have also included the code for people who are not using the latest EE.
Features
Multiple clients
Auto dropping timed-out clients
Can send packets both reliably and unreliably
Ping calculation
Packet loss calculation
Project includes a ReliableFastConnectionClass, a Demo Client, and Demo Server.
Why use this over RakNet? Because its simpler! 100% EE Script code.
What it missing that would be nice:
Handshakes, Encryption, Packet combining, Limiting Packets to 1500 bytes or auto-fragmenting into smaller, sequential delivery rather than strict ordered etc.. etc..
I didn't put more effort into it than I needed to, I only use EE on the client so that is why the server isn't more complete (testing for malformed packets, buffer overrun etc), a malicious client could kill it pretty easy!
class ReliableFastConnection : FastConnection // UDP connection with optional reliable-ordered delivery
{
// things that can be tweaked..
Int pingFrequency = 10; //times per second to ping, 0 = server, > 0 = client
Int reliableTimeout = 100; // time in milliseconds from last reliable message to try sending it again
Int connectionTimeout = 15; // time in seconds that if no ping is received toremoved the ActiveDestination from remoteAddresses
// things not to be tweaked..
SockAddr localListenAddress;
Flt lastPingTime = 0; //seconds since app start, used to decide if we need to send another ping or not (client only)
Flt ping = 0; //round trip latency ms
Memc<Flt> ping_times; // container for storing the last 10 ping times
Flt packet_loss = 0; // percentage of ping packets that don't make it across the wire averaged over 10 seconds
Memc<Flt> pings_received_last_10_secs; // pings that have been recieved in last 10 seconds
ULong reliable_received = 0; // data in bytes received
ULong unreliable_received = 0;
ULong reliable_sent = 0;
ULong unreliable_sent = 0;
struct ActiveDestination
{
SockAddr address;
Flt received_ping_at_time = 0; // the last time we got a ping in local time, not remote tag time
ULong local_synchro_on_local = 0; // what our last reliable packet sent out was tagged with
ULong local_synchro_on_remote = 0; // the last confirmed received reliable packet from the remote connection
ULong remote_synchro_on_local = 0; // the last reliable packet recieved from the remote connection was tagged with this
Memc<PacketAddr> reliable_queue;
Flt lastReliableTime = 0;
}
Memc<ActiveDestination> remoteAddresses;
Memc<PacketAddr> received_queue; // this is currently not put into ActiveDestination like reliable_queue, but it certainly could be..
// connect(..) doesn't actually send anything to the remote host, its really just setting the remote address and opening the local UDP
// port for listening, after all, UDP is really connection-less, we establish a "connection" through keep-alive pinging in the update loop.
void connect(SockAddr remoteaddr, SockAddr listenaddr) // only the client uses this
{
ActiveDestination actdest;
actdest.address = remoteaddr;
remoteAddresses.add(actdest); // client will only have 1 destination address.
listen(listenaddr);
return;
}
void listen(SockAddr addr) // server will just listen, not use connect
{
create(addr);
localListenAddress = addr;
return;
}
void send_reliable(SockAddr addr, File &f) // this actually just adds to a queue
{
PacketAddr padd;
padd.from_file(f, addr);
if(remoteAddresses_has_remote(addr))
{
get_remoteAddress(addr).reliable_queue.add(padd); // add to end of queue
}
return;
}
void send_unreliable(SockAddr destination, File &f)
{
File f2;
f2.writeMem().putByte(RFC_UNRELIABLE).putUShort(f.size());
f.pos(0); // make sure it is at the beginning of the file before copying
f.copy(f2);
f2.pos(0);
send(destination, f2);
unreliable_sent += f.size(); // we ar enot including the RFC header and UShort size
}
Bool update()
{
Int bytes_received = 0;
Byte datarec[65536];
SockAddr sender_addr;
bytes_received = receive(sender_addr, datarec);
while(bytes_received > -1) // needs to be loop more than one command recieved at once is possible? seems like it shoudl onyl return 1 packet at a time, but maybe need multiple calls to receive if low framerate?
{
File f;f.readMem(datarec, bytes_received);
//while(!f.end()) // should not need this? we will only send 1 command per packet, doing multiple commands per packet makes it harder when we decide to ignore the rest of a packet like in RFS_RELIABLE case below, we would need to make sure to read out the rest of the data in the packet otherwise will will get buggy stuff
{
Byte command = f.getByte();
switch(command)
{
case RFC_PING: // only server will receive this, client pings server, not vice-versa
{
Flt ptime = f.getFlt();
ULong loc_synchro_on_remote = f.getULong(); // learn what was the last reliable client got from the server was, lets us know if we need to resend
if(remoteAddresses_has_remote(sender_addr))
{
ActiveDestination & adest = get_remoteAddress(sender_addr);
adest.local_synchro_on_remote = loc_synchro_on_remote;
adest.received_ping_at_time = Time.realTime();
File f2;
f2.writeMem().putByte(RFC_PING_REPLY).putFlt(ptime).putULong(adest.remote_synchro_on_local).pos(0); // let the client know what the last reliable the server got from it was
send(sender_addr, f2);
}
else
{
ActiveDestination actdest;
actdest.address = sender_addr;
actdest.received_ping_at_time = Time.realTime();
remoteAddresses.add(actdest);
File f2;
f2.writeMem().putByte(RFC_PING_REPLY).putFlt(ptime).putULong(actdest.remote_synchro_on_local).pos(0); // let the client know what the last reliable the server got from it was
send(sender_addr, f2);
}
}
break;
case RFC_PING_REPLY: // client will recieve this
{
Flt lastPingTimeReceived = f.getFlt();
// for packet loss calculation
pings_received_last_10_secs.add(lastPingTimeReceived);
// check to see if we need to increase our local synchro
test_local_synchro_on_remote(f.getULong(), remoteAddresses[0].local_synchro_on_remote, remoteAddresses[0]);
}
break;
case RFC_UNRELIABLE:
{
UShort length_of_packet = f.getUShort(); // datarec may contain more than one packet, so we need to only get one at a time
File f2;
f2.writeMem();
f.copy(f2, length_of_packet); // hopefully f.copy advances pos() of f.. need ot maek sure otherwise this wont work, it will just keep reading the same packet over and over
PacketAddr padd;
padd.from_file(f2, sender_addr);
received_queue.add(padd);
unreliable_received += length_of_packet;
}
break;
case RFC_RELIABLE:
{
ULong synchro = f.getULong();
if(remoteAddresses_has_remote(sender_addr))
{
ActiveDestination & adest = get_remoteAddress(sender_addr);
if(synchro == (adest.remote_synchro_on_local + 1)) // is it the next synchro message? we need to make sure its not a repeat of something we already received
{
UShort length_of_packet = f.getUShort(); // datarec may contain more than one packet, so we need to only get one at a time
File f2;
f2.writeMem();
f.copy(f2, length_of_packet); // hopefully f.copy advances pos() of f.. need ot maek sure otherwise this wont work, it will just keep reading the same packet over and over
PacketAddr padd;
padd.from_file(f2, sender_addr);
received_queue.add(padd);
adest.remote_synchro_on_local += 1;
reliable_received += length_of_packet;
// now we need to send a confirmation back immediately so remote doesn't have to wait for next ping
File f3;
f3.writeMem().putByte(RFC_CONFIRM_RELIABLE).putULong(adest.remote_synchro_on_local).pos(0); // let the remote know what the last reliable the server got from it was
send(sender_addr, f3);
}
}
}
case RFC_CONFIRM_RELIABLE:
{
REPA(remoteAddresses)if(remoteAddresses[i].address == sender_addr)
test_local_synchro_on_remote(f.getULong(), remoteAddresses[i].local_synchro_on_remote, remoteAddresses[i]); // check to see if we need to increase our local synchro
}
break;
default:
Exit("RFC Default, something went wrong, no matching command!");
break;
}
bytes_received = receive(sender_addr, datarec); // read the next packet, if it exists
}
}
send_ping(); // currently only works for client
send_from_reliable_queues();
if(pingFrequency == 0)remove_dead_remote_addresses(); // only server should do this for now, unless server->client pings are implemented
return true;
}
void send_from_reliable_queues()
{
REPA(remoteAddresses)
if(remoteAddresses[i].reliable_queue.elms()) // if there is stuff in the queue
if(remoteAddresses[i].local_synchro_on_local == remoteAddresses[i].local_synchro_on_remote) // if the destination address has received all previous reliable messages
{
actually_send_reliable(remoteAddresses[i].reliable_queue.first(),remoteAddresses[i].lastReliableTime, ++remoteAddresses[i].local_synchro_on_local);
}
else if((Time.realTime() - remoteAddresses[i].lastReliableTime) > (reliableTimeout/1000) ) // destination has not received our last sent reliable, and timeout has occured to send it again
{
actually_send_reliable(remoteAddresses[i].reliable_queue.first(),remoteAddresses[i].lastReliableTime, remoteAddresses[i].local_synchro_on_local);
}
}
void send_ping()
{
if(pingFrequency > 0) // is a client so ping
if((Time.realTime() - lastPingTime) > (1/pingFrequency))
{
File f;
f.writeMem().putByte(RFC_PING).putFlt(Time.realTime()).putULong(remoteAddresses[0].remote_synchro_on_local).pos(0); //remote_synchro_on_local let the server know what the last reliable the client got from it was
//send(destinationAddress, f);
send(remoteAddresses[0].address, f);
lastPingTime = Time.realTime();
// update pack loss stuff only for client
update_packet_loss();
packet_loss = ((pingFrequency * 10) - pings_received_last_10_secs.elms()) / 100.0;
if(packet_loss < 0) packet_loss = 0.0;
if(Time.realTime() < 10.0)packet_loss = 0; // jus tignor eit the first 10 secs of app opening
}
}
void actually_send_reliable(PacketAddr &padd, Flt &last_reliable_sent_time, ULong synchro_to_tag_packet_with)
{
File f;
f.writeMem().putByte(RFC_RELIABLE).putULong(synchro_to_tag_packet_with).putUShort(padd.length); // tag as reliable, put the synchro, add the length of the packet
f.put(padd.data, padd.length); // add packet
f.pos(0);
send(padd.address, f);
last_reliable_sent_time = Time.realTime();
reliable_sent += padd.length; // we are not including the RFC header and UShort size
}
void test_local_synchro_on_remote(ULong test_synchro, ULong &loc_synchro_on_remote, ActiveDestination &adest)
{
if(test_synchro == loc_synchro_on_remote + 1) // server has confirmed a new reliable
{
loc_synchro_on_remote += 1;
adest.reliable_queue.remove(0, true); // this will shift memory, is this too slow?
}
}
Bool remoteAddresses_has_remote(SockAddr address) // is the remote address in remoteAddresses?
{
Bool found_remote = false;
REPA(remoteAddresses)if(address == remoteAddresses[i].address){ found_remote = true; break;}
return found_remote;
}
This tutorial presents a sample Reliable UDP Client Connection.
It can be used together with second tutorial "Demo Server"
/******************************************************************************/
Str destinationIP = "127.0.0.1";
Str last_message_string = "nothing";
// destinationPort and listenPort must be different if server is run on localhost,
// because only one process can listen to a UDP port at one time,
// otherwise they can be the same port, and probably should be to help with
// NAT transversal (so that end users don't need to forward ports on router).
Int destinationPort = 8791;
//Int listenPort = 8790;
ReliableFastConnection connection;
/******************************************************************************/
void InitPre()
{
EE_INIT();
App.flag=APP_WORK_IN_BACKGROUND|APP_NO_PAUSE_ON_WINDOW_MOVE_SIZE; // specify work in background flag to work also when not focused
App.x=1;
D.mode(500, 400);
D.scale(1.25);
}
bool Init()
{
SockAddr remoteAddress;
remoteAddress.setIP(destinationIP, destinationPort);
if(Contains(destinationIP, "127.0.0.1")) //can't listen and send from 2 apps on same ip:port over UDP
{
SockAddr address;
address.setIP(destinationIP, destinationPort);
SockAddr localAddress;
localAddress.setLocalFast(destinationPort + Random(1, 1000)); // choose random port, hopefully no conflicts
connection.connect(remoteAddress, localAddress);
}
else
{
SockAddr localAddress;
localAddress.setLocalFast(destinationPort); // if we are not running server and client on same PC it is OK to use the same listen port on server and client
connection.connect(remoteAddress, localAddress);
}
return true;
}
/******************************************************************************/
void Shut()
{