From d3d83903f44b7cb03161cc7a7281bae807159622 Mon Sep 17 00:00:00 2001 From: Rackover Date: Thu, 30 Jul 2020 17:44:13 +0200 Subject: [PATCH] Added UDP hole punching, updated protocol --- .editorconfig | 4 + Broadcast.sln | 5 + Broadcast/Broadcast.csproj | 4 + Broadcast/Server.cs | 96 +++++++++- BroadcastClient/Client.cs | 172 ++++++++++++++---- BroadcastClient/Hole.cs | 36 ++++ ...{EProtocol.cs => EInternetworkProtocol.cs} | 26 +-- BroadcastShared/ETransportProtocol.cs | 13 ++ BroadcastShared/Lobby.cs | 22 ++- BroadcastShared/Networking.cs | 7 +- 10 files changed, 326 insertions(+), 59 deletions(-) create mode 100644 .editorconfig create mode 100644 BroadcastClient/Hole.cs rename BroadcastShared/{EProtocol.cs => EInternetworkProtocol.cs} (80%) create mode 100644 BroadcastShared/ETransportProtocol.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..80c2344 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# CS4014: Because this call is not awaited, execution of the current method continues before the call is completed +dotnet_diagnostic.CS4014.severity = suggestion diff --git a/Broadcast.sln b/Broadcast.sln index 9d25c98..5b54775 100644 --- a/Broadcast.sln +++ b/Broadcast.sln @@ -11,6 +11,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BroadcastClient", "Broadcas EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BroadcastTester", "BroadcastTester\BroadcastTester.csproj", "{73A3F722-B492-4226-ACBE-45D7E525BB08}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C965B5AD-84D5-4F3C-A4C4-7F139431FEAA}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/Broadcast/Broadcast.csproj b/Broadcast/Broadcast.csproj index a90efa1..ea46f0a 100644 --- a/Broadcast/Broadcast.csproj +++ b/Broadcast/Broadcast.csproj @@ -7,6 +7,10 @@ true true + + + + diff --git a/Broadcast/Server.cs b/Broadcast/Server.cs index 89869ce..54e2c68 100644 --- a/Broadcast/Server.cs +++ b/Broadcast/Server.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; @@ -20,6 +21,8 @@ public class Server Dictionary lastHeardAbout = new Dictionary(); Logger logger; + Dictionary pendingPunchRequests = new Dictionary(); + public Server() { logger = new Logger(programName:"B_Server", outputToFile:true); @@ -30,7 +33,8 @@ public Server() {Networking.PROTOCOL_SUBMIT, HandleSubmit }, {Networking.PROTOCOL_QUERY, HandleQuery }, {Networking.PROTOCOL_DELETE, HandleDelete }, - {Networking.PROTOCOL_HELLO, HandleHello } + {Networking.PROTOCOL_HELLO, HandleHello }, + {Networking.PROTOCOL_PUNCH, HandlePunch } }; @@ -68,12 +72,12 @@ public Server() } } catch (IOException e) { - logger.Info("Client ["+clientId+"] had an IOException (see debug)"); + logger.Info("Client ["+clientId+"] triggered an IOException (see debug)"); logger.Debug(e.ToString()); break; } catch (SocketException e) { - logger.Info("Client [" + clientId + "] had an SocketException (see debug)"); + logger.Info("Client [" + clientId + "] triggered a SocketException (see debug)"); logger.Debug(e.ToString()); break; } @@ -121,8 +125,29 @@ void CleanLobbies() void HandleHello(byte[] _, TcpClient client, uint clientId) { var helloMsg = Encoding.UTF8.GetBytes("Hello!"); - client.GetStream().WriteData(helloMsg); - logger.Info("Sent a HELLO to client [{0}] ({1} bytes)".Format(clientId, helloMsg.Length)); + var msg = new List() { Networking.PROTOCOL_HELLO}; + msg.AddRange(helloMsg); + + var byteMsg = msg.ToArray(); + bool isRequestingPunch = false; + + if (pendingPunchRequests.ContainsKey(client)) + { + byteMsg = pendingPunchRequests[client]; + pendingPunchRequests.Remove(client); + isRequestingPunch = true; + } + + client.GetStream().WriteData(byteMsg); + + if (isRequestingPunch) + { + logger.Info("Sent a PUNCH request client [{0}] ({1} bytes)".Format(clientId, byteMsg.Length)); + } + else + { + logger.Info("Sent a HELLO to client [{0}] ({1} bytes)".Format(clientId, byteMsg.Length)); + } } void HandleQuery(byte[] deserializable, TcpClient client, uint clientId) @@ -175,7 +200,12 @@ void HandleSubmit(byte[] deserializable, TcpClient client, uint clientId) logger.Debug("Auto-filled lobby submission address with v4:{1}/v6:{2} for client [{0}]".Format(clientId, string.Join(".", lobby.address), lobby.strAddress)); } - var index = lobbies.FindIndex(o => o.id == lobby.id || (o.port == lobby.port && o.address.IsSameAs(lobby.address) && o.strAddress == lobby.strAddress)); + var index = lobbies.FindIndex(o => + o.GetOwner() == client || + o.id == lobby.id || + (o.port == lobby.port && o.address.IsSameAs(lobby.address) && o.strAddress == lobby.strAddress) + ); + uint uIntId = 0; if (index > -1) { uIntId = lobbies[index].id; @@ -192,6 +222,7 @@ void HandleSubmit(byte[] deserializable, TcpClient client, uint clientId) lastHeardAbout[lobby] = DateTime.UtcNow; var id = BitConverter.GetBytes(uIntId); client.GetStream().WriteData(id); // I return the ID of the lobby + lobby.SetOwner(client); logger.Info("Finished processing lobby submission from client [{0}] (gave them ID {1} for their lobby)!".Format(clientId, uIntId)); } @@ -200,12 +231,57 @@ void HandleDelete(byte[] deserializable, TcpClient client, uint clientId) { logger.Debug("Preparing to remove a lobby requested by client [{0}]".Format(clientId)); - var targetLobby = BitConverter.ToUInt32(deserializable, 0); + var targetLobbyId = BitConverter.ToUInt32(deserializable, 0); + var targetLobby = lobbies.Find(o => o.id == targetLobbyId); + + if (targetLobby == null) + { + logger.Info("Client [{0}] tried to remove lobby {1} but it does not exist. Doing nothing.".Format(clientId, targetLobbyId)); + return; + } + + if (targetLobby.GetOwner() != client) + { + logger.Info("Client [{0}] tried to remove lobby {1} but they're not the owner. Doing nothing.".Format(clientId, targetLobbyId)); + return; + } + lock (lobbies) { - var removed = lobbies.RemoveAll(o => o.id == targetLobby); - logger.Debug("Removed {1} lobbies (every lobby with ID {2}) as requested by client [{0}]".Format(clientId, removed, targetLobby)) ; + var removed = lobbies.RemoveAll(o => o.id == targetLobby.id); + logger.Debug("Removed {1} lobbies (every lobby with ID {2}) as requested by client [{0}]".Format(clientId, removed, targetLobbyId)) ; + } + logger.Info("Finished removing lobby {1} as requested by client [{0}]".Format(clientId, targetLobbyId)); + } + + void HandlePunch(byte[] deserializable, TcpClient client, uint clientId) + { + logger.Debug("Preparing to punch a lobby for client [{0}]".Format(clientId)); + + var targetLobby = BitConverter.ToUInt32(deserializable, 0); + var lobby = lobbies.Find(o => o.id == targetLobby); + + if (lobby == null) + { + logger.Warn("Asked to punch a NON-EXISTENT lobby {1} as requested by client [{0}]. Doing nothing.".Format(clientId, targetLobby)); + return; } - logger.Info("Finished removing lobby {1} as requested by client [{0}]".Format(clientId, targetLobby)); + + var portBytes = BitConverter.GetBytes(lobby.port); + + var endpoint = ((IPEndPoint)lobby.GetOwner().Client.RemoteEndPoint); + + pendingPunchRequests[lobby.GetOwner()] = new byte[] + { + Networking.PROTOCOL_PUNCH, + lobby.address[0], + lobby.address[1], + lobby.address[2], + lobby.address[3], + portBytes[0], + portBytes[1] + }; + + logger.Info("Added pending PUNCH paquet to {0}:{1} (the owner of lobby {2})".Format(endpoint.Address, endpoint.Port, lobby.id)); } } } diff --git a/BroadcastClient/Client.cs b/BroadcastClient/Client.cs index 21d0ed3..465fee2 100644 --- a/BroadcastClient/Client.cs +++ b/BroadcastClient/Client.cs @@ -22,6 +22,8 @@ public class Client TcpClient client; Logger logger; + List executionQueue = new List(); + public Client(string masterAddress, string gameName, bool allowOnlyInterNetworkAddress=false) { logger = new Logger(programName: "B_Client", outputToFile: true); @@ -43,17 +45,44 @@ public Client(string masterAddress, string gameName, bool allowOnlyInterNetworkA address = interAddr; + + // Main execution queue for async tasks + Task.Run(async ()=> + { + while (true) + { + var tasks = executionQueue.ToArray(); + foreach (var task in tasks) + { + task.Invoke(); + executionQueue.Remove(task); + } + if (tasks.Length != 0) logger.Trace("Executed " + tasks.Length + " tasks this loop"); + await Task.Delay(100); + } + }); + Start(); } + void Enqueue(Action action) + { + lock (executionQueue) { + executionQueue.Add(action); + } + } + void Start() { - if (client!= null) { + if (client != null) + { logger.Debug("Disposing previous client"); - try { + try + { client.GetStream().Dispose(); } - catch(InvalidOperationException) { + catch (InvalidOperationException) + { // Nothing to do } client.Close(); @@ -62,8 +91,9 @@ void Start() client = new TcpClient(address.ToString(), Networking.PORT); logger.Debug("Done! Setting receive timeout..."); client.ReceiveTimeout = SECONDS_BEFORE_EMPTY_READ * 1000; - logger.Debug("Set client ReceiveTimeout to " + (SECONDS_BEFORE_EMPTY_READ * 1000)+"ms"); - logger.Info("Client connected to "+address+":"+Networking.PORT); + + logger.Debug("Set client ReceiveTimeout to " + (SECONDS_BEFORE_EMPTY_READ * 1000) + "ms"); + logger.Info("Client connected to " + address + ":" + Networking.PORT); } public IReadOnlyList GetLobbyList() @@ -71,14 +101,17 @@ public IReadOnlyList GetLobbyList() return lobbies.AsReadOnly(); } + // Async public void UpdateLobbyList(Query query = null) { if (!ConnectIfNotConnected()) return; NetworkStream stream = client.GetStream(); // Send the message to the connected TcpServer. - if (query == null) { - query = new Query() { + if (query == null) + { + query = new Query() + { game = game }; } @@ -87,18 +120,20 @@ public void UpdateLobbyList(Query query = null) message.AddRange(query.Serialize()); stream.WriteData(message.ToArray()); - try { + try + { var data = stream.Read(); lobbies = Lobby.DeserializeList(data); logger.Info("Currently {0} lobbies", lobbies.Count); } - catch(Exception e) { // Gonna be IOException or SocketException + catch (Exception e) + { // Gonna be IOException or SocketException logger.Error("Could not update the lobby list:"); logger.Error(e.ToString()); } - } + // Sync public Lobby CreateLobby(string title, byte[] address, uint maxPlayers=8, string gameVersion="???", string map="Unknown") { var lobby = new Lobby() { @@ -112,10 +147,14 @@ public Lobby CreateLobby(string title, byte[] address, uint maxPlayers=8, string return lobby; } + // Async public void UpdateLobby(Lobby lobby) { - logger.Debug("Updating lobby " + lobby.id); - CreateLobby(lobby); // Same thing + Enqueue(() => + { + logger.Debug("Updating lobby " + lobby.id); + CreateLobby(lobby); // Same thing + }); } public uint CreateLobby(Lobby lobby) @@ -162,30 +201,69 @@ uint SubmitLobby(Lobby lobby) return 0; // Means this has had an exception of any kind } - public void DestroyLobby( uint lobbyId) + // Async + public void DestroyLobby(uint lobbyId) { - if (!ConnectIfNotConnected()) return; + Enqueue(() => + { + if (!ConnectIfNotConnected()) return; - NetworkStream stream = client.GetStream(); + NetworkStream stream = client.GetStream(); - try { - List message = new List(); - message.Add(Networking.PROTOCOL_DELETE); - message.AddRange(BitConverter.GetBytes(lobbyId)); + try + { + List message = new List(); + message.Add(Networking.PROTOCOL_DELETE); + message.AddRange(BitConverter.GetBytes(lobbyId)); - logger.Debug("Destroying lobby {4}: {0} {1} {2} {3}", message[0], message[1], message[2], message[3], lobbyId); + logger.Debug("Destroying lobby {4}: {0} {1} {2} {3}", message[0], message[1], message[2], message[3], lobbyId); - stream.WriteData(message.ToArray()); - } - catch (IOException e) { - logger.Error("Could not destroy the lobby:"); - logger.Error(e.ToString()); - } - catch (SocketException e) { - logger.Error("Could not destroy the lobby:"); - logger.Error(e.ToString()); - } + stream.WriteData(message.ToArray()); + } + catch (IOException e) + { + logger.Error("Could not destroy the lobby:"); + logger.Error(e.ToString()); + } + catch (SocketException e) + { + logger.Error("Could not destroy the lobby:"); + logger.Error(e.ToString()); + } + }); + } + + // Async + public void PunchLobby(uint lobbyId) + { + logger.Trace("Enqueuing lobby punch instruction for lobby " + lobbyId); + Enqueue(() => + { + if (!ConnectIfNotConnected()) return; + + NetworkStream stream = client.GetStream(); + try + { + List message = new List(); + message.Add(Networking.PROTOCOL_PUNCH); + message.AddRange(BitConverter.GetBytes(lobbyId)); + + logger.Debug("Punching lobby {0}", lobbyId); + + stream.WriteData(message.ToArray()); + } + catch (IOException e) + { + logger.Error("Could not punch the lobby:"); + logger.Error(e.ToString()); + } + catch (SocketException e) + { + logger.Error("Could not punch the lobby:"); + logger.Error(e.ToString()); + } + }); } bool ConnectIfNotConnected() @@ -220,6 +298,31 @@ bool IsConnected() var data = stream.Read(); if (data.Length > 0) { + + switch (data[0]) + { + // PROTOCOL--IP1--IP2--IP3--IP4--PORT--PORT + case Networking.PROTOCOL_PUNCH: + if (data.Length < 7) + { + logger.Warn("Ignored punch request from TCP server because of a malformed request (read " + data.Length + " bytes)"); + return true; + } + var address = data[1] + "." + data[2] + "." + data[3] + "." + data[4]; + var port = BitConverter.ToUInt16(new byte[] { data[5], data[6] }, 0); + logger.Debug("Preparing to punch " + address + ":" + port + "..."); + Hole.PunchUDP(address, port, logger); + break; + + case Networking.PROTOCOL_HELLO: + logger.Debug("Received hello from server (" + data.Length + " bytes)"); + break; + + default: + logger.Warn("Received garbage TCP data from broadcast server (read " + data.Length + " bytes, controller " + data[0] + ")"); + break; + } + return true; } else { @@ -254,14 +357,21 @@ public void Test() logger.Trace("TEST --> Creating new lobby"); var myLobby = CreateLobby(new Lobby() { game = "test" }); createdLobbies.Add(myLobby); + logger.Trace("TEST --> Successfully created lobby " + myLobby + ""); Thread.Sleep(5000); } - else if (new Random().Next(0, 3) < 2 && createdLobbies.Count > 0) { + else if (new Random().Next(0, 3) < 1 && createdLobbies.Count > 0) { logger.Trace("TEST --> Destroying a created lobby"); DestroyLobby(createdLobbies[0]); createdLobbies.RemoveAt(0); Thread.Sleep(5000); } + else if (createdLobbies.Count > 0) + { + logger.Trace("TEST --> Punching a created lobby ("+ createdLobbies[0]+")"); + PunchLobby(createdLobbies[0]); + Thread.Sleep(2000); + } logger.Trace("TEST --> End of decision loop"); } catch (IOException) { diff --git a/BroadcastClient/Hole.cs b/BroadcastClient/Hole.cs new file mode 100644 index 0000000..8455a2c --- /dev/null +++ b/BroadcastClient/Hole.cs @@ -0,0 +1,36 @@ +using Broadcast.Shared; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace Broadcast.Client +{ + public static class Hole + { + public async static Task PunchUDP(string ipAddress, ushort port, Logger logger) + { + while (true) + { + try + { + using (UdpClient c = new UdpClient(port)) + { + c.Send(Enumerable.Range(0, 255).Select(o => (byte)Networking.PROTOCOL_GARBAGE_PUNCH).ToArray(), 255, ipAddress, port); + logger.Info("Finished punching " + ipAddress + ":" + port); + break; + } + } + catch (SocketException) + { + // Do nothing, wait for next opportunity + logger.Trace("Could not punch {0}:{1}, waiting for next opportunity".Format(ipAddress, port)); + await Task.Delay(100); + } + } + } + } +} diff --git a/BroadcastShared/EProtocol.cs b/BroadcastShared/EInternetworkProtocol.cs similarity index 80% rename from BroadcastShared/EProtocol.cs rename to BroadcastShared/EInternetworkProtocol.cs index e2c74a3..355558b 100644 --- a/BroadcastShared/EProtocol.cs +++ b/BroadcastShared/EInternetworkProtocol.cs @@ -1,13 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Broadcast.Shared -{ - public enum EProtocol - { - IPv4, - IPv6, - Custom - } -} +using System; +using System.Collections.Generic; +using System.Text; + +namespace Broadcast.Shared +{ + public enum EInternetworkProtocol + { + IPv4, + IPv6, + Custom + } +} diff --git a/BroadcastShared/ETransportProtocol.cs b/BroadcastShared/ETransportProtocol.cs new file mode 100644 index 0000000..2de803e --- /dev/null +++ b/BroadcastShared/ETransportProtocol.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Broadcast.Shared +{ + public enum ETransportProtocol + { + UDP, + TCP, + CUSTOM + } +} diff --git a/BroadcastShared/Lobby.cs b/BroadcastShared/Lobby.cs index c32be96..678dca5 100644 --- a/BroadcastShared/Lobby.cs +++ b/BroadcastShared/Lobby.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Text; using System.IO; +using System.Net.Sockets; namespace Broadcast.Shared { @@ -27,10 +28,13 @@ public class Lobby public byte[] address = new byte[4]; public string strAddress = string.Empty; public ushort port = 1; - public EProtocol protocol = EProtocol.IPv4; + public EInternetworkProtocol internetProtocol = EInternetworkProtocol.IPv4; + public ETransportProtocol transportProtocol = ETransportProtocol.CUSTOM; public byte[] rawData = new byte[] { }; + TcpClient owner; + public byte[] Serialize(){ byte[] span; using (MemoryStream ms = new MemoryStream()){ @@ -52,7 +56,8 @@ public byte[] Serialize(){ bw.Write(address); bw.Write(strAddress); bw.Write(port); - bw.Write((byte)protocol); + bw.Write((byte)internetProtocol); + bw.Write((byte)transportProtocol); bw.Write(rawData.Length); bw.Write(rawData); } @@ -89,7 +94,8 @@ static void FillLobby(Lobby lobby, BinaryReader br){ lobby.address = br.ReadBytes(4); lobby.strAddress = br.ReadString(); lobby.port = br.ReadUInt16(); - lobby.protocol = (EProtocol)br.ReadByte(); + lobby.internetProtocol = (EInternetworkProtocol)br.ReadByte(); + lobby.transportProtocol = (ETransportProtocol)br.ReadByte(); lobby.rawData = br.ReadBytes(br.ReadInt32()); } @@ -126,5 +132,15 @@ public bool HasAddress() { return (address != null && !address.IsSameAs(new byte[4])) || !string.IsNullOrEmpty(strAddress); } + + public void SetOwner(TcpClient client) + { + this.owner = client; + } + + public TcpClient GetOwner() + { + return owner; + } } } diff --git a/BroadcastShared/Networking.cs b/BroadcastShared/Networking.cs index 5e767da..d9d2ba8 100644 --- a/BroadcastShared/Networking.cs +++ b/BroadcastShared/Networking.cs @@ -15,13 +15,16 @@ public MessageBuildingException(string msg, byte[] constructed) : base(msg + "\n } } - public const ushort PORT = 4004; + public const ushort PORT = 1000+VERSION; public const byte PROTOCOL_QUERY = 0; public const byte PROTOCOL_SUBMIT = 1; public const byte PROTOCOL_DELETE = 2; public const byte PROTOCOL_HELLO = 3; + public const byte PROTOCOL_PUNCH = 4; + + public const byte PROTOCOL_GARBAGE_PUNCH = 255; public const ushort MESSAGE_BITE_SIZE = 1024; - public const byte VERSION = 5; + public const byte VERSION = 6; static Dictionary isStreamBusy = new Dictionary();