namespace Supercell.Laser.Server.Networking { using Supercell.Laser.Server.Networking.Session; using System.Collections.Concurrent; using System; using System.IO; using System.Net; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; public static class TCPGateway { // Fix 4: единый реестр вместо двух параллельных List — здесь и в Connections.cs. // ConcurrentDictionary не требует внешнего лока для Add/Remove/Count. // Ключ — connection.Id (Interlocked-счётчик в Connection). private static ConcurrentDictionary ActiveConnections; private static ConcurrentDictionary PacketCounters; private static ConcurrentDictionary ConnectionAttempts; private static HashSet BannedIPs; private static readonly object BanLock = new object(); private static Socket Socket; private static Thread Thread; private static Timer CleanupTimer; private static int AcceptorCount; // Fix 4: публичный Count — Connections.cs делегирует сюда. public static int Count => ActiveConnections.Count; private const int MaxActiveConnections = 5000; // Fix 5: отдельный меньший лимит для соединений, не завершивших хендшейк. // Флуд, держащий сокеты открытыми без логина, упирается в этот предел, // а не в общий MaxActiveConnections — легитимные игроки всегда имеют слоты. private const int MaxPreAuthConnections = 1000; private const int CounterStaleSeconds = 300; public static void Init(string host, int port) { ActiveConnections = new ConcurrentDictionary(); PacketCounters = new ConcurrentDictionary(); ConnectionAttempts = new ConcurrentDictionary(); BannedIPs = new HashSet(); if (File.Exists("banned_ips.txt")) { foreach (string line in File.ReadAllLines("banned_ips.txt")) { string ip = line.Trim(); if (!string.IsNullOrEmpty(ip)) BannedIPs.Add(ip); } Logger.Print($"Загружено {BannedIPs.Count} забаненных IP."); } Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); Socket.Bind(new IPEndPoint(IPAddress.Parse(host), port)); Socket.Listen(99999999); AcceptorCount = Math.Max(Environment.ProcessorCount, 4); Thread = new Thread(Update); Thread.IsBackground = true; Thread.Start(); CleanupTimer = new Timer(CleanupInactiveConnections, null, 10000, 10000); Logger.Print($"Server listening on {host}:{port} ({AcceptorCount} parallel acceptors)"); } private static void Update() { for (int i = 0; i < AcceptorCount; i++) { try { Socket.BeginAccept(new AsyncCallback(OnAccept), null); } catch (Exception ex) { Logger.Print($"BeginAccept init error: {ex.Message}"); } } Thread.Sleep(Timeout.Infinite); } private static void OnAccept(IAsyncResult ar) { try { Socket.BeginAccept(new AsyncCallback(OnAccept), null); } catch (Exception ex) { Logger.Print($"BeginAccept rearm error: {ex.Message}"); return; } try { Socket client = Socket.EndAccept(ar); string clientIp = ((IPEndPoint)client.RemoteEndPoint).Address.ToString(); lock (BanLock) { if (BannedIPs.Contains(clientIp)) { client.Close(); return; } } // Fix 4: без ConnectionLock — ConcurrentDictionary.Count атомарен. if (ActiveConnections.Count >= MaxActiveConnections) { client.Close(); return; } var attemptCounter = ConnectionAttempts.GetOrAdd(clientIp, _ => new ConnectionAttemptCounter()); lock (attemptCounter) { attemptCounter.AttemptCount++; if ((DateTime.Now - attemptCounter.FirstAttemptTime).TotalSeconds > 10) { attemptCounter.FirstAttemptTime = DateTime.Now; attemptCounter.AttemptCount = 1; } if (attemptCounter.AttemptCount > 10) { BanIp(clientIp); Logger.Print($"IP {clientIp} blocked for spam"); return; } } // Fix 5: отдельный лимит для pre-auth соединений (PepperState < 5). // Count() — O(n), но n ≤ MaxActiveConnections и новые соединения // уже придушены AttemptCounter, поэтому это не горячий путь. int preAuthCount = 0; foreach (var kv in ActiveConnections) if (kv.Value.Messaging.PepperState < 5) preAuthCount++; if (preAuthCount >= MaxPreAuthConnections) { Logger.WriteServerLog($"[Limit] Pre-auth лимит {MaxPreAuthConnections} исчерпан, отклоняем {clientIp}"); client.Close(); return; } PacketCounters.GetOrAdd(clientIp, _ => new PacketCounter()); Connection connection = new Connection(client); ActiveConnections.TryAdd(connection.Id, connection); // Fix 4: Connections.AddConnection убран — единый реестр здесь. client.BeginReceive(connection.ReadBuffer, 0, Connection.ReadBufferSize, SocketFlags.None, new AsyncCallback(OnReceive), connection); // Fix 2: PepperState инициализируется в 2 (Messaging.cs), поэтому // старая проверка == 0 никогда не срабатывала — хендшейк не резался. // Теперь проверяем < 5: состояние 5 = хендшейк завершён полностью. Task.Delay(5000).ContinueWith(_ => { if (connection.Messaging.PepperState < 5) { Logger.WriteServerLog($"[Timeout] Хендшейк не завершён за 5 с от {clientIp}, PepperState={connection.Messaging.PepperState}"); RemoveConnection(connection); } }); } catch (Exception ex) { Logger.Print($"Ошибка при приёме подключения: {ex.Message}\n{ex.StackTrace}"); } } private static void OnReceive(IAsyncResult ar) { Connection connection = (Connection)ar.AsyncState; if (connection == null || connection.Socket == null || !connection.Socket.Connected) return; string clientIp = ((IPEndPoint)connection.Socket.RemoteEndPoint).Address.ToString(); try { int r = connection.Socket.EndReceive(ar); if (r <= 0) { Logger.WriteServerLog($"{clientIp} отключился. [Reason] 0 байт (EOF)."); RemoveConnection(connection); return; } if (PacketCounters.TryGetValue(clientIp, out var counter)) { lock (counter) { counter.PacketCount++; if ((DateTime.Now - counter.FirstPacketTime).TotalSeconds > 10) { counter.FirstPacketTime = DateTime.Now; counter.PacketCount = 1; } if (counter.PacketCount > 150) { BanIp(clientIp); Logger.Print($"IP {clientIp} заблокирован за превышение лимита пакетов."); RemoveConnection(connection); return; } } } connection.Memory.Write(connection.ReadBuffer, 0, r); connection.UpdateLastActiveTime(); if (connection.Messaging.OnReceive() != 0) { RemoveConnection(connection); Logger.WriteServerLog($"{clientIp} отключился. [Reason] ошибка обработки/расшифровки пакета."); return; } connection.Socket.BeginReceive(connection.ReadBuffer, 0, Connection.ReadBufferSize, SocketFlags.None, new AsyncCallback(OnReceive), connection); } catch (ObjectDisposedException) { Logger.WriteServerLog($"Сокет клиента {clientIp} уже был закрыт."); } catch (SocketException socketEx) { RemoveConnection(connection); Logger.WriteServerLog($"{clientIp} отключился. [Reason] {socketEx.SocketErrorCode} ({socketEx.Message})"); } catch (Exception exception) { Logger.Print($"Неожиданная ошибка от {clientIp}: {exception.Message}\n{exception.StackTrace}"); RemoveConnection(connection); } } // Fix 4: CleanupInactiveConnections поглощает логику из Connections.Update() // (IsAlive-проверку). Один таймер вместо двух отдельных потоков очистки. private static void CleanupInactiveConnections(object state) { var now = DateTime.Now; foreach (var kvp in ActiveConnections) { var connection = kvp.Value; bool shouldRemove = false; string reason = ""; if (connection.Socket == null || !connection.Socket.Connected) { shouldRemove = true; reason = "сокет закрыт"; } else if ((now - connection.LastActiveTime).TotalSeconds > 120) { shouldRemove = true; reason = "неактивность >120 с"; } else if (!connection.MessageManager.IsAlive()) { // Fix 4: эта проверка была в Connections.Update() — перенесена сюда. shouldRemove = true; reason = "IsAlive() == false (KeepAlive таймаут 30 с)"; } if (shouldRemove) { // TryRemove атомарен: только один поток выполнит Close + Sessions.Remove. if (ActiveConnections.TryRemove(kvp.Key, out _)) { Logger.WriteServerLog($"[Cleanup] Закрытие соединения {connection.Socket?.RemoteEndPoint}: {reason}"); bool hadSession = connection.MessageManager.HomeMode != null; long accountId = hadSession ? (connection.Avatar?.AccountId ?? 0) : 0; connection.Close(); if (hadSession && accountId != 0) { // Сохранение аккаунта — тяжёлая DB-операция, не на таймер-потоке. Task.Run(() => Sessions.Remove(accountId)); } } } } foreach (var kvp in PacketCounters) { if ((now - kvp.Value.FirstPacketTime).TotalSeconds > CounterStaleSeconds) PacketCounters.TryRemove(kvp.Key, out _); } foreach (var kvp in ConnectionAttempts) { if ((now - kvp.Value.FirstAttemptTime).TotalSeconds > CounterStaleSeconds) ConnectionAttempts.TryRemove(kvp.Key, out _); } } // Fix 1: лок убран совсем — ConcurrentDictionary.TryRemove атомарен. // Fix 1: Sessions.Remove (sync DB + рассылка друзьям) вынесен с IO-потока // в отдельный Task, чтобы не блокировать OnAccept для всех клиентов. private static void RemoveConnection(Connection connection) { if (!ActiveConnections.TryRemove(connection.Id, out _)) return; // уже удалён другим потоком (двойной вызов игнорируем) connection.Close(); if (connection.MessageManager.HomeMode != null) { long accountId = connection.Avatar.AccountId; // Сохранение аккаунта и нотификация друзей — вынесены с IO-потока. Task.Run(() => Sessions.Remove(accountId)); } } public static void OnSend(IAsyncResult ar) { try { Socket socket = (Socket)ar.AsyncState; socket.EndSend(ar); } catch (Exception ex) { Logger.WriteServerLog($"Error of sent: {ex.Message}"); } } private static void BanIp(string ip) { lock (BanLock) { if (BannedIPs.Add(ip)) { File.AppendAllText("banned_ips.txt", ip + Environment.NewLine); Logger.Print($"IP {ip} забанен и сохранён."); } } } } public class PacketCounter { public DateTime FirstPacketTime { get; set; } public int PacketCount { get; set; } public PacketCounter() { FirstPacketTime = DateTime.Now; PacketCount = 0; } } public class ConnectionAttemptCounter { public DateTime FirstAttemptTime { get; set; } public int AttemptCount { get; set; } public ConnectionAttemptCounter() { FirstAttemptTime = DateTime.Now; AttemptCount = 0; } } }