connection code for client

This commit is contained in:
Timerix 2025-07-11 19:08:41 +03:00
parent 7b809a2c60
commit 3738f802c5
19 changed files with 282 additions and 131 deletions

1
.gitignore vendored
View File

@ -9,6 +9,7 @@ UserSettings
*.csproj
*.sln
*.DotSettings
*.user
.idea/
# some random downloaded assets
Assets/imported/

View File

@ -27,7 +27,7 @@ namespace FastArena
public void Kill()
{
Destroy(this.gameObject);
Destroy(gameObject);
}
}
}

View File

@ -2,37 +2,105 @@
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Threading;
using FastArena.Network.Packets;
using UnityEngine;
using Object = UnityEngine.Object;
namespace FastArena
namespace FastArena.Network
{
public enum ClientStatus
{
Initialized, Connected, Disconnected
}
public class ServerConnection
{
public IPAddress Ip;
public short PortFirst;
public Guid SessionId;
public byte[] ServerKey;
}
public static class Client
{
private static TcpClient _tcp;
private static UdpClient _udp;
private static byte[] _clientKey;
private static ServerConnection _serverConnection;
public static ClientStatus Status { get; private set; } = ClientStatus.Initialized;
public static void SendUserAction()
{
throw new System.NotImplementedException();
throw new NotImplementedException();
}
public static void Connect(IPAddress ip, short portFirst)
{
_tcp = new TcpClient();
_udp = new UdpClient();
_serverConnection = new ServerConnection()
{
Ip = ip, PortFirst = portFirst
};
_udp.Connect(ip, portFirst + 1);
_tcp.BeginConnect(ip, portFirst, TCPConnectCallback, null);
_udp.BeginSend(StructBinaryConverter.GetBytes(new ConnectionRequestUDP()))
}
private static void ReadExactly(this TcpClient tcp, byte[] buffer, int offset, int exactSize, int timeoutMs = 5000)
{
int msPassed = 0;
while (tcp.Available < exactSize)
{
Thread.Sleep(1);
msPassed += 1;
if (msPassed > timeoutMs)
{
throw new TimeoutException(
$"couldn't get {exactSize} bytes from socket, {tcp.Available} avaliable");
}
}
int readN = _tcp.GetStream().Read(buffer, offset, exactSize);
if (readN != exactSize)
throw new Exception($"expected to read {exactSize} bytes, got {readN}");
}
private static void TCPConnectCallback(IAsyncResult ar)
{
_tcp.EndConnect(ar);
// receive first response from server
byte[] buffer = new byte[4096];
_tcp.ReadExactly(buffer, 0, Marshal.SizeOf<ConnectionResponseTCP>());
//TODO: implement decryption
var firstResponse = StructBinaryConverter.ReadStruct<ConnectionResponseTCP>(buffer);
firstResponse.ValidateResponseCode();
_serverConnection.SessionId = firstResponse.sessionId;
_serverConnection.ServerKey = firstResponse.serverKey;
// connect udp socket associated with current session
byte[] byteMessage = StructBinaryConverter.GetBytes(new ConnectionRequestUDP(_serverConnection.SessionId));
//TODO: implement encryption
_udp.Send(byteMessage, byteMessage.Length);
// receive second response from server
IPEndPoint remoteEndpoint = new IPEndPoint(IPAddress.None, 0);
byteMessage = _udp.Receive(ref remoteEndpoint);
//TODO: implement decryption
var secondResponse = StructBinaryConverter.ReadStruct<ConnectionResponseUDP>(byteMessage);
secondResponse.ValidateResponseCode();
//TODO: start tcp read loop
//TODO: start udp read loop
}
private static Dictionary<int, SyncTransform> _syncTransforms = new();
private static void CreateGameObject(int id, ref CreateObjectPacket packet)
private static readonly Dictionary<int, SyncTransform> _syncTransforms = new Dictionary<int, SyncTransform>();
private static void CreateGameObject(ref CreateObjectPacket packet)
{
var prefab = Resources.Load(packet.prefabPath, typeof(GameObject));
var go = (GameObject)Object.Instantiate(prefab);
@ -44,9 +112,9 @@ namespace FastArena
}
}
private static void UpdateObjectTransform(int id, ref TransformUpdatePacket packet)
private static void UpdateObjectTransform(ref TransformUpdatePacket packet)
{
if(!_syncTransforms.TryGetValue(id, out var component))
if(!_syncTransforms.TryGetValue(packet.gameObjectId, out var component))
return;
component.UpdateTransform(ref packet);
}

View File

@ -0,0 +1,17 @@
using System;
using UnityEngine;
namespace FastArena.Network
{
public class GameObjectId : MonoBehaviour
{
public int Id { get; private set; } = -1;
internal void _SetId(int packetGameObjectId)
{
if (Id != -1)
throw new Exception($"GameObjectId is already set to {Id}");
Id = packetGameObjectId;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a0274d4d086949a880c5e0b4a9e5cf20
timeCreated: 1752248822

View File

@ -1,21 +1,75 @@
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using UnityEngine;
using System;
using System.Runtime.InteropServices;
namespace FastArena
namespace FastArena.Network.Packets
{
internal struct Hash256
[StructLayout(LayoutKind.Sequential)]
internal readonly struct ConnectionRequestTCP
{
internal readonly PacketHeader header;
private SHA256 _sha256;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)]
internal readonly byte[] clientKey;
public ConnectionRequestTCP()
public ConnectionRequestTCP(byte[] clientKey)
{
header = new PacketHeader(PacketType.ConnectionRequestTCP);
this.clientKey = clientKey;
}
}
[StructLayout(LayoutKind.Sequential)]
internal readonly struct ConnectionResponseTCP
{
internal readonly PacketHeader header;
internal readonly StatusCode statusCode;
internal readonly Guid sessionId;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)]
internal readonly byte[] serverKey;
public ConnectionResponseTCP(StatusCode statusCode, Guid sessionId, byte[] serverKey)
{
header = new PacketHeader(PacketType.ConnectionResponseTCP);
this.statusCode = statusCode;
this.sessionId = sessionId;
this.serverKey = serverKey;
}
public void ValidateResponseCode()
{
if (statusCode != StatusCode.OK)
throw new StatusCodeException(statusCode);
}
}
[StructLayout(LayoutKind.Sequential)]
internal readonly struct ConnectionRequestUDP
{
internal readonly PacketHeader header;
internal readonly Guid sessionId;
public ConnectionRequestUDP(Guid sessionId)
{
header = new PacketHeader(PacketType.ConnectionRequestUDP);
this.sessionId = sessionId;
}
}
[StructLayout(LayoutKind.Sequential)]
internal readonly struct ConnectionResponseUDP
{
internal readonly PacketHeader header;
internal readonly StatusCode statusCode;
public ConnectionResponseUDP(StatusCode statusCode)
{
header = new PacketHeader(PacketType.ConnectionResponseUDP);
this.statusCode = statusCode;
}
public void ValidateResponseCode()
{
if (statusCode != StatusCode.OK)
throw new StatusCodeException(statusCode);
}
}
}

View File

@ -1,10 +1,10 @@
using System.Runtime.InteropServices;
namespace FastArena;
[StructLayout(LayoutKind.Sequential)]
internal readonly struct CreateObjectPacket
namespace FastArena.Network.Packets
{
[StructLayout(LayoutKind.Sequential)]
internal readonly struct CreateObjectPacket
{
internal readonly PacketHeader header;
internal readonly int gameObjectId;
internal readonly string prefabPath;
@ -15,4 +15,5 @@ internal readonly struct CreateObjectPacket
gameObjectId = _gameObjectId;
prefabPath = _prefabPath;
}
}
}

View File

@ -1,6 +1,6 @@
using System.Runtime.InteropServices;
namespace FastArena
namespace FastArena.Network.Packets
{
internal enum PacketType : ushort
{

View File

@ -1,7 +1,7 @@
namespace FastArena;
internal static class PacketParser
namespace FastArena.Network.Packets
{
internal static class PacketParser
{
internal static PacketType ReadHeader(byte[] data)
{
var h = StructBinaryConverter.ReadStruct<PacketHeader>(data);
@ -12,4 +12,5 @@ internal static class PacketParser
{
return StructBinaryConverter.ReadStruct<T>(data);
}
}
}

View File

@ -0,0 +1,46 @@
using System.Runtime.InteropServices;
namespace FastArena.Network.Packets
{
public static class StructBinaryConverter
{
public static byte[] GetBytes<T>(T s) where T : struct
{
return GetBytes(ref s);
}
public static byte[] GetBytes<T>(ref T s) where T : struct
{
int size = Marshal.SizeOf<T>();
// TODO: implement array pool
byte[] buffer = new byte[size];
// tel GC to not move the buffer in memory
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try
{
Marshal.StructureToPtr(s, handle.AddrOfPinnedObject(), false);
}
finally
{
handle.Free();
}
return buffer;
}
public static T ReadStruct<T>(byte[] buffer) where T : struct
{
T s;
// tel GC to not move the buffer in memory
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try
{
s = (T)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T));
}
finally
{
handle.Free();
}
return s;
}
}
}

View File

@ -1,11 +1,11 @@
using System.Runtime.InteropServices;
using UnityEngine;
namespace FastArena;
[StructLayout(LayoutKind.Sequential)]
internal readonly struct TransformUpdatePacket
namespace FastArena.Network.Packets
{
[StructLayout(LayoutKind.Sequential)]
internal readonly struct TransformUpdatePacket
{
internal readonly PacketHeader header;
internal readonly int gameObjectId;
internal readonly Vector3 position;
@ -18,4 +18,5 @@ internal readonly struct TransformUpdatePacket
position = transform.position;
rotation = transform.rotation;
}
}
}

View File

@ -2,9 +2,10 @@
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using FastArena.Network.Packets;
using UnityEngine;
namespace FastArena
namespace FastArena.Network
{
public class ServerConfig
{
@ -33,7 +34,7 @@ namespace FastArena
}
}
public static List<ClientConnection> connections = new List<ClientConnection>();
public static readonly List<ClientConnection> Connections = new List<ClientConnection>();
public static void Start(ServerConfig config)
{
@ -49,7 +50,7 @@ namespace FastArena
{
var tcpClient = _tcp.EndAcceptTcpClient(ar);
var connection = new ClientConnection(tcpClient);
connections.Add(connection);
Connections.Add(connection);
}
private static void UdpReceiveCallback(IAsyncResult ar)
@ -83,7 +84,7 @@ namespace FastArena
public static void SendTransformUpdate(int id, Transform transform)
{
byte[] data = StructBinaryConverter.GetBytes(new TransformUpdatePacket(id, transform));
foreach(var p in connections)
foreach(var p in Connections)
{
UdpSendTo(data, p);
}

View File

@ -0,0 +1,22 @@
using System;
namespace FastArena.Network
{
public enum StatusCode
{
OK,
ServerError,
AccessNotAllowed
}
public class StatusCodeException : Exception
{
public StatusCode Code;
public StatusCodeException(StatusCode code) :
base($"Status code {(int)code} ({code})")
{
Code = code;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6ef3bf843e27438cbaae508f73654ec6
timeCreated: 1752246290

View File

@ -1,55 +0,0 @@
using System;
using System.Runtime.InteropServices;
namespace FastArena
{
public static class StructBinaryConverter
{
public static byte[] GetBytes<T>(T s) where T : unmanaged
{
return GetBytes(ref s);
}
public static byte[] GetBytes<T>(ref T s) where T : unmanaged
{
int size = Marshal.SizeOf(typeof(T));
byte[] buffer = System.Buffers.ArrayPool<byte>.Shared.Rent(Marshal.SizeOf<T>());
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
unsafe
{
fixed (T* s_ptr = &s)
{
Marshal.Copy((IntPtr)s_ptr, buffer, 0, size);
Marshal.StructureToPtr();
}
}
try
{
Marshal.StructureToPtr(msg_struct, handle.AddrOfPinnedObject(), false);
return buffer;
}
finally
{
handle.Free();
ArrayPool<byte>.Shared.Return(buffer);
}
}
public static T ReadStruct<T>(byte[] buffer) where T : unmanaged
{
GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try
{
T s = (T)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T));
return s;
}
finally
{
handle.Free();
}
}
}
}

View File

@ -1,20 +1,8 @@
using System;
using FastArena.Network.Packets;
using UnityEngine;
namespace FastArena
namespace FastArena.Network
{
public class GameObjectId : MonoBehaviour
{
public int Id { get; private set; } = -1;
internal void _SetId(int packetGameObjectId)
{
if (Id != -1)
throw new Exception($"GameObjectId is already set to {Id}");
Id = packetGameObjectId;
}
}
/// <summary>
/// Component fetching GameObject's Transform from the server every tick.
/// </summary>

View File

@ -1,7 +1,7 @@
{
"dependencies": {
"com.unity.ide.rider": "3.0.28",
"com.unity.ide.visualstudio": "2.0.22",
"com.unity.ide.rider": "3.0.36",
"com.unity.ide.visualstudio": "2.0.23",
"com.unity.ide.vscode": "1.2.5",
"com.unity.textmeshpro": "2.1.6",
"com.unity.ugui": "1.0.0",

View File

@ -8,7 +8,7 @@
"url": "https://packages.unity.com"
},
"com.unity.ide.rider": {
"version": "3.0.28",
"version": "3.0.36",
"depth": 0,
"source": "registry",
"dependencies": {
@ -17,7 +17,7 @@
"url": "https://packages.unity.com"
},
"com.unity.ide.visualstudio": {
"version": "2.0.22",
"version": "2.0.23",
"depth": 0,
"source": "registry",
"dependencies": {