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 *.csproj
*.sln *.sln
*.DotSettings *.DotSettings
*.user
.idea/ .idea/
# some random downloaded assets # some random downloaded assets
Assets/imported/ Assets/imported/

View File

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

View File

@ -2,37 +2,105 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Net; using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Threading;
using FastArena.Network.Packets;
using UnityEngine; using UnityEngine;
using Object = UnityEngine.Object; 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 public static class Client
{ {
private static TcpClient _tcp; private static TcpClient _tcp;
private static UdpClient _udp; 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() public static void SendUserAction()
{ {
throw new System.NotImplementedException(); throw new NotImplementedException();
} }
public static void Connect(IPAddress ip, short portFirst) public static void Connect(IPAddress ip, short portFirst)
{ {
_tcp = new TcpClient(); _tcp = new TcpClient();
_udp = new UdpClient(); _udp = new UdpClient();
_serverConnection = new ServerConnection()
{
Ip = ip, PortFirst = portFirst
};
_udp.Connect(ip, portFirst + 1);
_tcp.BeginConnect(ip, portFirst, TCPConnectCallback, null); _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) private static void TCPConnectCallback(IAsyncResult ar)
{ {
_tcp.EndConnect(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 prefab = Resources.Load(packet.prefabPath, typeof(GameObject));
var go = (GameObject)Object.Instantiate(prefab); 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; return;
component.UpdateTransform(ref packet); 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;
using System.Security.Cryptography; using System.Runtime.InteropServices;
using UnityEngine;
namespace FastArena namespace FastArena.Network.Packets
{ {
internal struct Hash256
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
internal readonly struct ConnectionRequestTCP internal readonly struct ConnectionRequestTCP
{ {
internal readonly PacketHeader header; 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); 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; using System.Runtime.InteropServices;
namespace FastArena; namespace FastArena.Network.Packets
[StructLayout(LayoutKind.Sequential)]
internal readonly struct CreateObjectPacket
{ {
[StructLayout(LayoutKind.Sequential)]
internal readonly struct CreateObjectPacket
{
internal readonly PacketHeader header; internal readonly PacketHeader header;
internal readonly int gameObjectId; internal readonly int gameObjectId;
internal readonly string prefabPath; internal readonly string prefabPath;
@ -15,4 +15,5 @@ internal readonly struct CreateObjectPacket
gameObjectId = _gameObjectId; gameObjectId = _gameObjectId;
prefabPath = _prefabPath; prefabPath = _prefabPath;
} }
}
} }

View File

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

View File

@ -1,7 +1,7 @@
namespace FastArena; namespace FastArena.Network.Packets
internal static class PacketParser
{ {
internal static class PacketParser
{
internal static PacketType ReadHeader(byte[] data) internal static PacketType ReadHeader(byte[] data)
{ {
var h = StructBinaryConverter.ReadStruct<PacketHeader>(data); var h = StructBinaryConverter.ReadStruct<PacketHeader>(data);
@ -12,4 +12,5 @@ internal static class PacketParser
{ {
return StructBinaryConverter.ReadStruct<T>(data); 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 System.Runtime.InteropServices;
using UnityEngine; using UnityEngine;
namespace FastArena; namespace FastArena.Network.Packets
[StructLayout(LayoutKind.Sequential)]
internal readonly struct TransformUpdatePacket
{ {
[StructLayout(LayoutKind.Sequential)]
internal readonly struct TransformUpdatePacket
{
internal readonly PacketHeader header; internal readonly PacketHeader header;
internal readonly int gameObjectId; internal readonly int gameObjectId;
internal readonly Vector3 position; internal readonly Vector3 position;
@ -18,4 +18,5 @@ internal readonly struct TransformUpdatePacket
position = transform.position; position = transform.position;
rotation = transform.rotation; rotation = transform.rotation;
} }
}
} }

View File

@ -2,9 +2,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Net; using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using FastArena.Network.Packets;
using UnityEngine; using UnityEngine;
namespace FastArena namespace FastArena.Network
{ {
public class ServerConfig 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) public static void Start(ServerConfig config)
{ {
@ -49,7 +50,7 @@ namespace FastArena
{ {
var tcpClient = _tcp.EndAcceptTcpClient(ar); var tcpClient = _tcp.EndAcceptTcpClient(ar);
var connection = new ClientConnection(tcpClient); var connection = new ClientConnection(tcpClient);
connections.Add(connection); Connections.Add(connection);
} }
private static void UdpReceiveCallback(IAsyncResult ar) private static void UdpReceiveCallback(IAsyncResult ar)
@ -83,7 +84,7 @@ namespace FastArena
public static void SendTransformUpdate(int id, Transform transform) public static void SendTransformUpdate(int id, Transform transform)
{ {
byte[] data = StructBinaryConverter.GetBytes(new TransformUpdatePacket(id, transform)); byte[] data = StructBinaryConverter.GetBytes(new TransformUpdatePacket(id, transform));
foreach(var p in connections) foreach(var p in Connections)
{ {
UdpSendTo(data, p); 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; 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> /// <summary>
/// Component fetching GameObject's Transform from the server every tick. /// Component fetching GameObject's Transform from the server every tick.
/// </summary> /// </summary>

View File

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

View File

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