SmoOnlineServer/Server/Server.cs

335 lines
16 KiB
C#
Raw Normal View History

2022-02-04 09:45:38 +00:00
using System.Buffers;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using Shared;
using Shared.Packet;
using Shared.Packet.Packets;
namespace Server;
public class Server {
public readonly List<Client> Clients = new List<Client>();
public IEnumerable<Client> ClientsConnected => Clients.Where(client => client.Metadata.ContainsKey("lastGamePacket") && client.Connected);
2022-02-04 09:45:38 +00:00
public readonly Logger Logger = new Logger("Server");
2022-02-10 01:44:50 +00:00
private readonly MemoryPool<byte> memoryPool = MemoryPool<byte>.Shared;
2022-02-16 00:35:38 +00:00
public Func<Client, IPacket, bool>? PacketHandler = null!;
public event Action<Client, ConnectPacket> ClientJoined = null!;
2022-02-10 01:44:50 +00:00
2022-02-22 04:05:13 +00:00
public async Task Listen(CancellationToken? token = null) {
2022-02-04 09:45:38 +00:00
Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
2022-04-03 02:58:57 +00:00
serverSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
serverSocket.Bind(new IPEndPoint(IPAddress.Parse(Settings.Instance.Server.Address), Settings.Instance.Server.Port));
2022-02-04 09:45:38 +00:00
serverSocket.Listen();
2022-02-10 01:44:50 +00:00
Logger.Info($"Listening on {serverSocket.LocalEndPoint}");
2022-02-04 09:45:38 +00:00
2022-02-22 04:05:13 +00:00
try {
while (true) {
Socket socket = token.HasValue ? await serverSocket.AcceptAsync(token.Value) : await serverSocket.AcceptAsync();
2022-04-03 02:14:35 +00:00
socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true);
2022-02-10 01:44:50 +00:00
2022-02-22 04:05:13 +00:00
Logger.Warn($"Accepted connection for client {socket.RemoteEndPoint}");
2022-02-04 09:45:38 +00:00
2022-02-22 04:05:13 +00:00
try {
#pragma warning disable CS4014
Task.Run(() => HandleSocket(socket))
.ContinueWith(x => { if (x.Exception != null) { Logger.Error(x.Exception.ToString()); } });
#pragma warning restore CS4014
}
2022-03-12 02:29:17 +00:00
catch (Exception e) {
Logger.Error($"Error occured while setting up socket handler? {e}");
2022-02-22 04:05:13 +00:00
}
}
}
catch (OperationCanceledException) {
2022-02-22 04:05:13 +00:00
// ignore the exception, it's just for closing the server
Logger.Info("Server closing");
2022-02-22 04:05:13 +00:00
try {
serverSocket.Shutdown(SocketShutdown.Both);
2022-03-22 09:20:49 +00:00
}
catch {
// ignored
2022-03-22 09:20:49 +00:00
}
finally {
serverSocket.Close();
}
Logger.Info("Server closed");
2022-07-28 05:56:50 +00:00
Console.WriteLine("\n\n\n"); //for the sake of the restart command.
}
2022-02-04 09:45:38 +00:00
}
2022-02-16 00:35:38 +00:00
public static void FillPacket<T>(PacketHeader header, T packet, Memory<byte> memory) where T : struct, IPacket {
2022-02-04 09:45:38 +00:00
Span<byte> data = memory.Span;
2022-04-27 20:17:02 +00:00
header.Serialize(data[..Constants.HeaderSize]);
2022-02-14 22:07:42 +00:00
packet.Serialize(data[Constants.HeaderSize..]);
2022-02-04 09:45:38 +00:00
}
// broadcast packets to all clients
public delegate void PacketReplacer<in T>(Client from, Client to, T value); // replacer must send
2022-04-27 20:02:26 +00:00
public void BroadcastReplace<T>(T packet, Client sender, PacketReplacer<T> packetReplacer) where T : struct, IPacket {
2022-04-28 03:32:52 +00:00
foreach (Client client in Clients.Where(client => client.Connected && sender.Id != client.Id)) packetReplacer(sender, client, packet);
}
2022-02-16 00:35:38 +00:00
public async Task Broadcast<T>(T packet, Client sender) where T : struct, IPacket {
IMemoryOwner<byte> memory = MemoryPool<byte>.Shared.RentZero(Constants.HeaderSize + packet.Size);
2022-02-04 09:45:38 +00:00
PacketHeader header = new PacketHeader {
Id = sender?.Id ?? Guid.Empty,
Type = Constants.PacketMap[typeof(T)].Type,
PacketSize = packet.Size
2022-02-04 09:45:38 +00:00
};
FillPacket(header, packet, memory.Memory);
await Broadcast(memory, sender);
}
public Task Broadcast<T>(T packet) where T : struct, IPacket {
return Task.WhenAll(Clients.Where(c => c.Connected).Select(async client => {
IMemoryOwner<byte> memory = MemoryPool<byte>.Shared.RentZero(Constants.HeaderSize + packet.Size);
PacketHeader header = new PacketHeader {
Id = client.Id,
Type = Constants.PacketMap[typeof(T)].Type,
PacketSize = packet.Size
};
FillPacket(header, packet, memory.Memory);
await client.Send(memory.Memory, client);
memory.Dispose();
}));
}
2022-02-04 09:45:38 +00:00
/// <summary>
2022-02-10 01:44:50 +00:00
/// Takes ownership of data and disposes once done.
2022-02-04 09:45:38 +00:00
/// </summary>
/// <param name="data">Memory owner to dispose once done</param>
/// <param name="sender">Optional sender to not broadcast data to</param>
public async Task Broadcast(IMemoryOwner<byte> data, Client? sender = null) {
2022-02-10 08:42:35 +00:00
await Task.WhenAll(Clients.Where(c => c.Connected && c != sender).Select(client => client.Send(data.Memory, sender)));
2022-02-04 09:45:38 +00:00
data.Dispose();
}
/// <summary>
2022-02-10 01:44:50 +00:00
/// Broadcasts memory whose memory shouldn't be disposed, should only be fired by server code.
2022-02-04 09:45:38 +00:00
/// </summary>
/// <param name="data">Memory to send to the clients</param>
/// <param name="sender">Optional sender to not broadcast data to</param>
public async void Broadcast(Memory<byte> data, Client? sender = null) {
2022-02-10 08:42:35 +00:00
await Task.WhenAll(Clients.Where(c => c.Connected && c != sender).Select(client => client.Send(data, sender)));
2022-02-04 09:45:38 +00:00
}
public Client? FindExistingClient(Guid id) {
return Clients.Find(client => client.Id == id);
}
private async void HandleSocket(Socket socket) {
Client client = new Client(socket) {Server = this};
2022-02-04 09:45:38 +00:00
IMemoryOwner<byte> memory = null!;
2022-05-08 22:04:57 +00:00
await client.Send(new InitPacket {
MaxPlayers = Settings.Instance.Server.MaxPlayers
});
2022-02-04 09:45:38 +00:00
bool first = true;
try {
while (true) {
memory = memoryPool.Rent(Constants.HeaderSize);
2022-03-22 09:20:49 +00:00
async Task<bool> Read(Memory<byte> readMem, int readSize, int readOffset) {
readSize += readOffset;
while (readOffset < readSize) {
int size = await socket.ReceiveAsync(readMem[readOffset..readSize], SocketFlags.None);
2022-02-15 21:17:18 +00:00
if (size == 0) {
// treat it as a disconnect and exit
Logger.Info($"Socket {socket.RemoteEndPoint} disconnected.");
if (socket.Connected) await socket.DisconnectAsync(false);
return false;
2022-02-15 21:17:18 +00:00
}
readOffset += size;
}
return true;
2022-02-04 09:45:38 +00:00
}
if (!await Read(memory.Memory[..Constants.HeaderSize], Constants.HeaderSize, 0))
break;
PacketHeader header = GetHeader(memory.Memory.Span[..Constants.HeaderSize]);
Range packetRange = Constants.HeaderSize..(Constants.HeaderSize + header.PacketSize);
if (header.PacketSize > 0) {
IMemoryOwner<byte> memTemp = memory; // header to copy to new memory
memory = memoryPool.Rent(Constants.HeaderSize + header.PacketSize);
memTemp.Memory.Span[..Constants.HeaderSize].CopyTo(memory.Memory.Span[..Constants.HeaderSize]);
memTemp.Dispose();
if (!await Read(memory.Memory, header.PacketSize, Constants.HeaderSize))
break;
}
2022-02-04 09:45:38 +00:00
// connection initialization
if (first) {
first = false;
2022-02-10 01:44:50 +00:00
if (header.Type != PacketType.Connect) throw new Exception($"First packet was not init, instead it was {header.Type}");
2022-02-04 09:45:38 +00:00
2022-02-14 20:40:42 +00:00
ConnectPacket connect = new ConnectPacket();
connect.Deserialize(memory.Memory.Span[packetRange]);
2022-02-04 09:45:38 +00:00
lock (Clients) {
2022-03-22 09:20:49 +00:00
if (Clients.Count(x => x.Connected) == Settings.Instance.Server.MaxPlayers) {
2022-03-22 09:17:48 +00:00
client.Logger.Error($"Turned away as server is at max clients");
memory.Dispose();
goto disconnect;
}
2022-03-22 09:20:49 +00:00
2022-02-04 09:45:38 +00:00
bool firstConn = false;
switch (connect.ConnectionType) {
2022-03-13 11:14:01 +00:00
case ConnectPacket.ConnectionTypes.FirstConnection: {
2022-02-04 09:45:38 +00:00
firstConn = true;
2022-03-22 09:17:48 +00:00
if (FindExistingClient(header.Id) is { } newClient) {
if (newClient.Connected) {
newClient.Logger.Info($"Disconnecting already connected client {newClient.Socket?.RemoteEndPoint} for {client.Socket?.RemoteEndPoint}");
newClient.Dispose();
}
2022-03-22 09:17:48 +00:00
newClient.Socket = client.Socket;
client = newClient;
}
2022-03-22 09:20:49 +00:00
2022-02-04 09:45:38 +00:00
break;
}
2022-03-13 11:14:01 +00:00
case ConnectPacket.ConnectionTypes.Reconnecting: {
client.Id = header.Id;
2022-02-04 09:45:38 +00:00
if (FindExistingClient(header.Id) is { } newClient) {
if (newClient.Connected) {
newClient.Logger.Info($"Disconnecting already connected client {newClient.Socket?.RemoteEndPoint} for {client.Socket?.RemoteEndPoint}");
newClient.Dispose();
}
2022-02-04 09:45:38 +00:00
newClient.Socket = client.Socket;
client = newClient;
} else {
firstConn = true;
2022-03-13 11:14:01 +00:00
connect.ConnectionType = ConnectPacket.ConnectionTypes.FirstConnection;
2022-02-04 09:45:38 +00:00
}
2022-02-10 01:44:50 +00:00
2022-02-04 09:45:38 +00:00
break;
}
default:
throw new Exception($"Invalid connection type {connect.ConnectionType}");
}
2022-02-09 21:56:57 +00:00
client.Name = connect.ClientName;
2022-02-09 21:56:57 +00:00
client.Connected = true;
2022-02-04 09:45:38 +00:00
if (firstConn) {
// do any cleanup required when it comes to new clients
List<Client> toDisconnect = Clients.FindAll(c => c.Id == header.Id && c.Connected && c.Socket != null);
Clients.RemoveAll(c => c.Id == header.Id);
2022-02-10 01:44:50 +00:00
2022-02-04 09:45:38 +00:00
client.Id = header.Id;
Clients.Add(client);
Parallel.ForEachAsync(toDisconnect, (c, token) => c.Socket!.DisconnectAsync(false, token));
// done disconnecting and removing stale clients with the same id
ClientJoined?.Invoke(client, connect);
2022-02-04 09:45:38 +00:00
}
}
2022-02-10 01:44:50 +00:00
List<Client> otherConnectedPlayers = Clients.FindAll(c => c.Id != header.Id && c.Connected && c.Socket != null);
await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => {
IMemoryOwner<byte> tempBuffer = MemoryPool<byte>.Shared.RentZero(Constants.HeaderSize + (other.CurrentCostume.HasValue ? Math.Max(connect.Size, other.CurrentCostume.Value.Size) : connect.Size));
2022-02-10 01:44:50 +00:00
PacketHeader connectHeader = new PacketHeader {
Id = other.Id,
Type = PacketType.Connect,
PacketSize = connect.Size
};
connectHeader.Serialize(tempBuffer.Memory.Span[..Constants.HeaderSize]);
2022-02-10 01:44:50 +00:00
ConnectPacket connectPacket = new ConnectPacket {
2022-03-13 11:14:01 +00:00
ConnectionType = ConnectPacket.ConnectionTypes.FirstConnection, // doesn't matter what it is
2022-04-27 05:24:40 +00:00
MaxPlayers = Settings.Instance.Server.MaxPlayers,
2022-02-15 20:39:34 +00:00
ClientName = other.Name
};
2022-02-14 22:06:49 +00:00
connectPacket.Serialize(tempBuffer.Memory.Span[Constants.HeaderSize..]);
await client.Send(tempBuffer.Memory[..(Constants.HeaderSize + connect.Size)], null);
2022-02-12 23:37:03 +00:00
if (other.CurrentCostume.HasValue) {
2022-02-11 04:25:47 +00:00
connectHeader.Type = PacketType.Costume;
connectHeader.PacketSize = other.CurrentCostume.Value.Size;
connectHeader.Serialize(tempBuffer.Memory.Span[..Constants.HeaderSize]);
other.CurrentCostume.Value.Serialize(tempBuffer.Memory.Span[Constants.HeaderSize..(Constants.HeaderSize + connectHeader.PacketSize)]);
await client.Send(tempBuffer.Memory[..(Constants.HeaderSize + connectHeader.PacketSize)], null);
2022-02-11 04:25:47 +00:00
}
2022-02-11 04:25:47 +00:00
tempBuffer.Dispose();
});
2022-02-10 01:44:50 +00:00
Logger.Info($"Client {client.Name} ({client.Id}/{socket.RemoteEndPoint}) connected.");
} else if (header.Id != client.Id && client.Id != Guid.Empty) {
throw new Exception($"Client {client.Name} sent packet with invalid client id {header.Id} instead of {client.Id}");
}
2022-02-04 09:45:38 +00:00
2022-02-11 04:25:47 +00:00
if (header.Type == PacketType.Costume) {
2022-02-12 23:37:03 +00:00
CostumePacket costumePacket = new CostumePacket {
BodyName = ""
};
costumePacket.Deserialize(memory.Memory.Span[Constants.HeaderSize..(Constants.HeaderSize + costumePacket.Size)]);
2022-02-12 23:37:03 +00:00
client.CurrentCostume = costumePacket;
2022-02-11 04:25:47 +00:00
}
2022-02-14 21:17:36 +00:00
try {
2022-02-14 20:40:42 +00:00
IPacket packet = (IPacket) Activator.CreateInstance(Constants.PacketIdMap[header.Type])!;
packet.Deserialize(memory.Memory.Span[Constants.HeaderSize..(Constants.HeaderSize + packet.Size)]);
if (PacketHandler?.Invoke(client, packet) is false) {
memory.Dispose();
continue;
}
}
catch (Exception e) {
2022-02-16 00:35:38 +00:00
client.Logger.Error($"Packet handler warning: {e}");
2022-02-14 20:40:42 +00:00
}
#pragma warning disable CS4014
Broadcast(memory, client)
.ContinueWith(x => { if (x.Exception != null) { Logger.Error(x.Exception.ToString()); } });
#pragma warning restore CS4014
2022-02-04 09:45:38 +00:00
}
}
catch (Exception e) {
if (e is SocketException {SocketErrorCode: SocketError.ConnectionReset}) {
2022-03-12 02:16:10 +00:00
client.Logger.Info($"Disconnected from the server: Connection reset");
2022-02-04 09:45:38 +00:00
} else {
2022-03-12 02:16:10 +00:00
client.Logger.Error($"Disconnecting due to exception: {e}");
if (socket.Connected) {
#pragma warning disable CS4014
Task.Run(() => socket.DisconnectAsync(false))
.ContinueWith(x => { if (x.Exception != null) { Logger.Error(x.Exception.ToString()); } });
#pragma warning restore CS4014
}
2022-02-04 09:45:38 +00:00
}
memory?.Dispose();
}
2022-03-22 09:20:49 +00:00
2022-03-22 09:17:48 +00:00
disconnect:
2022-03-12 02:16:10 +00:00
Logger.Info($"Client {socket.RemoteEndPoint} ({client.Name}/{client.Id}) disconnected from the server");
2022-02-04 09:45:38 +00:00
close:
bool wasConnected = client.Connected;
2022-03-15 20:44:46 +00:00
// Clients.Remove(client)
client.Connected = false;
try {
client.Dispose();
2022-03-22 09:20:49 +00:00
}
2022-04-28 03:32:52 +00:00
catch { /*lol*/ }
2022-03-22 09:20:49 +00:00
#pragma warning disable CS4014
if (wasConnected) {
Task.Run(() => Broadcast(new DisconnectPacket(), client))
.ContinueWith(x => { if (x.Exception != null) { Logger.Error(x.Exception.ToString()); } });
}
#pragma warning restore CS4014
2022-02-04 09:45:38 +00:00
}
private static PacketHeader GetHeader(Span<byte> data) {
//no need to error check, the client will disconnect when the packet is invalid :)
PacketHeader header = new PacketHeader();
header.Deserialize(data[..Constants.HeaderSize]);
return header;
2022-02-04 09:45:38 +00:00
}
}