From f1406501c6cf41e8faf971ab540d1d229cbc5394 Mon Sep 17 00:00:00 2001 From: Brychan Dempsey Date: Sat, 16 Oct 2021 20:01:49 +1300 Subject: [PATCH] Created initial connection logic for server and client --- PDGServer_WPF/App.xaml.cs | 1 + RBG_Server.Core/CommunicationHandler.cs | 206 +++++++++++++++++++++++- RBG_Server.Core/RBG_Server.csproj | 12 ++ 3 files changed, 218 insertions(+), 1 deletion(-) diff --git a/PDGServer_WPF/App.xaml.cs b/PDGServer_WPF/App.xaml.cs index 1c8b5ca..2fe763c 100644 --- a/PDGServer_WPF/App.xaml.cs +++ b/PDGServer_WPF/App.xaml.cs @@ -20,6 +20,7 @@ namespace RBG_Server { public GameServer BoardGameServer {get;} public CommunicationHandler communicationHandler { get; private set;} + protected static readonly SolidColorBrush RED_BRUSH = new(Color.FromRgb(255, 0, 0)); protected static readonly SolidColorBrush BLUE_BRUSH = new(Color.FromRgb(0, 0, 255)); protected static readonly SolidColorBrush GREEN_BRUSH = new(Color.FromRgb(0, 255, 0)); diff --git a/RBG_Server.Core/CommunicationHandler.cs b/RBG_Server.Core/CommunicationHandler.cs index 92fd9b8..737e4b9 100644 --- a/RBG_Server.Core/CommunicationHandler.cs +++ b/RBG_Server.Core/CommunicationHandler.cs @@ -1,7 +1,10 @@ -using System; +using NATUPNPLib; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Net; +using System.Net.Sockets; using System.Text; using System.Threading.Tasks; @@ -10,9 +13,13 @@ namespace RBG_Server /// /// Contains all communication data, from both the client's and server's perspective (should be able to switch between each mode as necessary) /// + /// public class CommunicationHandler { + UPnPNAT upnpnat = new(); + IStaticPortMapping portMapping; + /// /// Image data is stored in memory in a dictionary collection. Each byte[] represents an image file, compressed according to its file type; /// which helps save space in memory. Uses a CachedByteArray; essentially a normal byte array but automatically stored on disk if it is larger @@ -20,5 +27,202 @@ namespace RBG_Server /// This limit can be changed at any time /// public ConcurrentDictionary ImageCollection { get; } = new(); + public List ImageList { get; } = new(); + public string BoardName { get; set; } + + public IPAddress IpAddress { get; set; } + public short Port { get; set; } + + private TcpClient stateRetriever { get; set; } + private TcpClient dataRetriever { get; set; } + + + public int ColumnCount { get;private set; } + public int RowCount { get;private set; } + public int ColumnZoomStart { get;private set; } + public int RowZoomStart { get;private set; } + public int ColumnZoomSpan { get;private set; } + public int RowZoomSpan { get;private set; } + public int StartingColumn { get;private set; } + public int StartingRow { get;private set; } + + public void InitialiseServer() + { + // Find the server's active (reliable) network adapter, by creating a remote connection and retrieving our IP from it: + using (Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)) + { + s.Bind(new IPEndPoint(IPAddress.Any, 0)); + s.Connect("microsoft.com", 0); + // The IP is implicitly the one assigned to the interface the OS would use to connect to the remote address + IpAddress = (s.LocalEndPoint as IPEndPoint).Address; + } + // Create the upnp mapping + upnpnat.StaticPortMappingCollection.Add(Port, "TCP", Port, IpAddress.ToString(), true, "RBGServer"); + + TcpListener listener = new(IPAddress.Any, Port); // Allow local comms + + listener.Start(); + while (true) + { + TcpClient client = listener.AcceptTcpClient(); + // Delegate to another thread + _ = Task.Run(() => + { + AcceptConnections(client); + }); + } + // At this point, the ImageCollection will have already been initialised. + // The name of the board will also be set to {0}.. + // Server logic, such as accepting players and maintaining communication goes in here + } + + private void AcceptConnections(TcpClient client) + { + throw new NotImplementedException(); + } + + public void InitialiseClient(TcpClient client) + { + // At this point, no details about the game are loaded; we must load them from the server (to which we have already connected [no data should have been sent]). + NetworkStream stateStream = stateRetriever.GetStream(); + Task stateLoader = new Task(async () => + { + // Get game board details + byte[] buffer = new byte[] { 1, 0, 0, 0, 1 }; + stateStream.Write(buffer, 0, buffer.Length); + // Response size + var stateResponse = await GetResponse(stateStream, 4); + // Response data + stateResponse = await GetResponse(stateStream, GetInt32(stateResponse)); // Get the full response data + // Board state data + ColumnCount = GetInt32(stateResponse[..4]); + RowCount = GetInt32(stateResponse[4..8]); + ColumnZoomStart = GetInt32(stateResponse[8..12]); + RowZoomStart = GetInt32(stateResponse[12..16]); + ColumnZoomSpan = GetInt32(stateResponse[16..20]); + RowZoomSpan = GetInt32(stateResponse[20..24]); + StartingColumn = GetInt32(stateResponse[24..28]); + StartingRow = GetInt32(stateResponse[28..32]); + // Basic board data loaded; fetch players + buffer = new byte[] { 1, 0, 0, 0, 2}; + stateStream.Write(buffer, 0, buffer.Length); + stateResponse = await GetResponse(stateStream, 4); + stateResponse = await GetResponse(stateStream, GetInt32(stateResponse)); + // state response contains a player list + }); + + + + NetworkStream dataStream = dataRetriever.GetStream(); + Task dataLoader = new Task(async () => + { + byte[] buffer = new byte[] { 2, 0, 0, 0, 128, 0 }; // Get image collection names + dataStream.Write(buffer, 0, buffer.Length); + var dataResponse = await GetResponse(stateStream, 4); + + dataResponse = await GetResponse(dataStream, GetInt32(dataResponse)); + List data = new List(); + for (int i = 0; i < dataResponse.Length; i++) + { + if (dataResponse[i] != 0) + { + data.Add(dataResponse[i]); + } + else + { + ImageList.Add(Encoding.UTF8.GetString(data.ToArray())); + data.Clear(); + } + } + + // Load all low-resolution images + foreach (string item in ImageList) + { + byte[] strBytes = Encoding.ASCII.GetBytes(item); + buffer = new byte[strBytes.Length + 6]; + byte[] lenBytes = GetBytes(strBytes.Length + 6); + lenBytes.CopyTo(buffer, 0); + buffer[4] = 129; // Download image + buffer[5] = 0; // mip_low + strBytes.CopyTo(buffer, 6); + dataStream.Write(buffer, 0, buffer.Length); + // Read the length, then the data + dataResponse = await GetResponse(dataStream); + dataResponse = await GetResponse(dataStream, GetInt32(dataResponse)); + ImageCollection.TryAdd(item + "_mip_low", (CachedByteArray)dataResponse); + } + // At this point, the minimal amount of work required by the data thread has been done (load all thumbs) + // When an asset is needed from here, queue a load + }); + + dataLoader.Start(); + stateLoader.Start(); + + + byte[] buffer = new byte[] {0,0,0,1,1}; + // Writing to the stream is to be considered near constant-time, but reading is non-constant. + // This application model must be synchronous, but we execute other commands before expecting our response to have arrived (it can complete at any time in that period) + stateStream.Write(buffer, 0, buffer.Length); + buffer[0] = 1; + buffer[3] = 0; + dataStream.Write(buffer, 0, buffer.Length); + // A details request is [, ] + // The response is about the same format: + var stateResponse = GetResponse(stateStream, 4); + var dataResponse = GetResponse(dataStream, 4); + // First, load the board state (low mip-map, row definitions, column definitions, zoom position etc.) + // Retrieval command for the board + // Then load the player list, and use the low mip for their sprites + // Then check each loaded image and load the med, then large, then full sprite + // Then load the low of each unused image (for quick retrieval) + } + + /// + /// Idea is that each response is prefaced by a 4 byte stream length specifier. + /// This requires a busy wait to achieve, if not all recieved at once. + /// We retrieve the + /// + /// + /// + /// + private async Task GetResponse(NetworkStream stream, int targetLength=4) + { + if (stream.CanRead) + { + byte[] buffer = new byte[targetLength]; + StringBuilder myCompleteMessage = new StringBuilder(); + int numberOfBytesRead = 0; + + // Incoming message may be larger than the buffer size. + do + { + numberOfBytesRead += await stream.ReadAsync(buffer, numberOfBytesRead, targetLength - numberOfBytesRead); + } + while (numberOfBytesRead < targetLength); + return buffer; + } + return Array.Empty(); + } + + static internal byte[] GetBytes(int i) + { + byte[] bytes = BitConverter.GetBytes(i); + if (BitConverter.IsLittleEndian) + { + // We need data in big-endian format; so reverse the array + Array.Reverse(bytes); + } + return bytes; + } + + static internal int GetInt32(byte[] bytes) + { + if (BitConverter.IsLittleEndian) + { + // We need data in little-endian format; so reverse the array + Array.Reverse(bytes); + } + return BitConverter.ToInt32(bytes, 0); + } } } diff --git a/RBG_Server.Core/RBG_Server.csproj b/RBG_Server.Core/RBG_Server.csproj index 76214ae..7f15cb2 100644 --- a/RBG_Server.Core/RBG_Server.csproj +++ b/RBG_Server.Core/RBG_Server.csproj @@ -4,6 +4,18 @@ net5.0-windows10.0.19041.0 + + + tlbimp + 0 + 1 + 1c565858-f302-471e-b409-f180aa4abec6 + 0 + false + true + + +