mirror of
https://github.com/ACEmulator/ACE
synced 2026-08-17 12:26:06 -04:00
* Network performance improvements, limit boxing * Polygon comment out redundant test code * Transition remove duplicate Init() * Creature_BodyPart and Monster_Melee comment out unused var
393 lines
16 KiB
C#
393 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
using log4net;
|
|
|
|
using ACE.Common;
|
|
using ACE.Server.Entity;
|
|
using ACE.Server.Entity.Actions;
|
|
using ACE.Server.Managers;
|
|
using ACE.Server.Network.Packets;
|
|
using ACE.Server.Network.Handlers;
|
|
using ACE.Server.Network.Enum;
|
|
|
|
namespace ACE.Server.Network.Managers
|
|
{
|
|
public static class NetworkManager
|
|
{
|
|
private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
|
|
private static readonly ILog packetLog = LogManager.GetLogger(System.Reflection.Assembly.GetEntryAssembly(), "Packets");
|
|
|
|
// Hard coded server Id, this will need to change if we move to multi-process or multi-server model
|
|
public const ushort ServerId = 0xB;
|
|
|
|
/// <summary>
|
|
/// Seconds until a session will timeout.
|
|
/// Raising this value allows connections to remain active for a longer period of time.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If you're experiencing network dropouts or frequent disconnects, try increasing this value.
|
|
/// </remarks>
|
|
public static uint DefaultSessionTimeout = ConfigManager.Config.Server.Network.DefaultSessionTimeout;
|
|
|
|
private static readonly ReaderWriterLockSlim sessionLock = new ReaderWriterLockSlim();
|
|
private static readonly Session[] sessionMap = new Session[ConfigManager.Config.Server.Network.MaximumAllowedSessions];
|
|
|
|
/// <summary>
|
|
/// Handles ClientMessages in InboundMessageManager
|
|
/// </summary>
|
|
public static readonly ActionQueue InboundMessageQueue = new ActionQueue();
|
|
|
|
public static void ProcessPacket(ConnectionListener connectionListener, ClientPacket packet, IPEndPoint endPoint)
|
|
{
|
|
if (connectionListener.ListenerEndpoint.Port == ConfigManager.Config.Server.Network.Port + 1)
|
|
{
|
|
//ServerPerformanceMonitor.RestartEvent(ServerPerformanceMonitor.MonitorType.ProcessPacket_1);
|
|
if (packet.Header.Flags.HasFlag(PacketHeaderFlags.ConnectResponse))
|
|
{
|
|
if (packetLog.IsDebugEnabled)
|
|
packetLog.DebugFormat("{0}, {1}", packet, endPoint);
|
|
PacketInboundConnectResponse connectResponse = new PacketInboundConnectResponse(packet);
|
|
|
|
// This should be set on the second packet to the server from the client.
|
|
// This completes the three-way handshake.
|
|
sessionLock.EnterReadLock();
|
|
Session session = null;
|
|
try
|
|
{
|
|
session =
|
|
(from k in sessionMap
|
|
where
|
|
k != null &&
|
|
k.State == SessionState.AuthConnectResponse &&
|
|
k.Network.ConnectionData.ConnectionCookie == connectResponse.Check &&
|
|
k.EndPointC2S.Address.Equals(endPoint.Address)
|
|
select k).FirstOrDefault();
|
|
}
|
|
finally
|
|
{
|
|
sessionLock.ExitReadLock();
|
|
}
|
|
if (session != null)
|
|
{
|
|
session.SetS2CEndpoint(endPoint);
|
|
session.State = SessionState.AuthConnected;
|
|
session.Network.sendResync = true;
|
|
AuthenticationHandler.HandleConnectResponse(session);
|
|
}
|
|
|
|
}
|
|
else if (packet.Header.Id == 0 && packet.Header.HasFlag(PacketHeaderFlags.CICMDCommand))
|
|
{
|
|
// TODO: Not sure what to do with these packets yet
|
|
}
|
|
else
|
|
{
|
|
log.ErrorFormat("Packet from {0} rejected. Packet sent to listener 1 and is not a ConnectResponse or CICMDCommand", endPoint);
|
|
}
|
|
//ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.ProcessPacket_1);
|
|
}
|
|
else // ConfigManager.Config.Server.Network.Port + 0
|
|
{
|
|
//ServerPerformanceMonitor.RestartEvent(ServerPerformanceMonitor.MonitorType.ProcessPacket_0);
|
|
if (packet.Header.HasFlag(PacketHeaderFlags.LoginRequest))
|
|
{
|
|
if (packetLog.IsDebugEnabled)
|
|
packetLog.DebugFormat("{0}, {1}", packet, endPoint);
|
|
if (GetAuthenticatedSessionCount() >= ConfigManager.Config.Server.Network.MaximumAllowedSessions)
|
|
{
|
|
log.InfoFormat("Login Request from {0} rejected. Server full.", endPoint);
|
|
SendLoginRequestReject(connectionListener, endPoint, CharacterError.LogonServerFull);
|
|
}
|
|
else if (ServerManager.ShutdownInProgress)
|
|
{
|
|
log.InfoFormat("Login Request from {0} rejected. Server is shutting down.", endPoint);
|
|
SendLoginRequestReject(connectionListener, endPoint, CharacterError.ServerCrash1);
|
|
}
|
|
else if (ServerManager.ShutdownInitiated && (ServerManager.ShutdownTime - DateTime.UtcNow).TotalMinutes < 2)
|
|
{
|
|
log.InfoFormat("Login Request from {0} rejected. Server shutting down in less than 2 minutes.", endPoint);
|
|
SendLoginRequestReject(connectionListener, endPoint, CharacterError.ServerCrash1);
|
|
}
|
|
else
|
|
{
|
|
log.DebugFormat("Login Request from {0}", endPoint);
|
|
|
|
var ipAllowsUnlimited = ConfigManager.Config.Server.Network.AllowUnlimitedSessionsFromIPAddresses.Contains(endPoint.Address.ToString());
|
|
if (ipAllowsUnlimited || ConfigManager.Config.Server.Network.MaximumAllowedSessionsPerIPAddress == -1 || GetSessionEndpointTotalByAddressCount(endPoint.Address) < ConfigManager.Config.Server.Network.MaximumAllowedSessionsPerIPAddress)
|
|
{
|
|
var session = FindOrCreateSession(connectionListener, endPoint);
|
|
if (session != null)
|
|
{
|
|
if (session.State == SessionState.AuthConnectResponse)
|
|
{
|
|
// connect request packet sent to the client was corrupted in transit and session entered an unspecified state.
|
|
// ignore the request and remove the broken session and the client will start a new session.
|
|
RemoveSession(session);
|
|
log.Warn($"Bad handshake from {endPoint}, aborting session.");
|
|
}
|
|
|
|
session.ProcessPacket(packet);
|
|
}
|
|
else
|
|
{
|
|
log.InfoFormat("Login Request from {0} rejected. Failed to find or create session.", endPoint);
|
|
SendLoginRequestReject(connectionListener, endPoint, CharacterError.LogonServerFull);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
log.InfoFormat("Login Request from {0} rejected. Session would exceed MaximumAllowedSessionsPerIPAddress limit.", endPoint);
|
|
SendLoginRequestReject(connectionListener, endPoint, CharacterError.LogonServerFull);
|
|
}
|
|
}
|
|
}
|
|
else if (sessionMap.Length > packet.Header.Id)
|
|
{
|
|
var session = sessionMap[packet.Header.Id];
|
|
if (session != null)
|
|
{
|
|
if (session.EndPointC2S.Equals(endPoint))
|
|
session.ProcessPacket(packet);
|
|
else
|
|
log.DebugFormat("Session for Id {0} has IP {1} but packet has IP {2}", packet.Header.Id, session.EndPointC2S, endPoint);
|
|
}
|
|
else
|
|
{
|
|
log.DebugFormat("Unsolicited Packet from {0} with Id {1}", endPoint, packet.Header.Id);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
log.DebugFormat("Unsolicited Packet from {0} with Id {1}", endPoint, packet.Header.Id);
|
|
}
|
|
//ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.ProcessPacket_0);
|
|
}
|
|
}
|
|
|
|
private static void SendLoginRequestReject(ConnectionListener connectionListener, IPEndPoint endPoint, CharacterError error)
|
|
{
|
|
var tempSession = new Session(connectionListener, endPoint, (ushort)(sessionMap.Length + 1), ServerId);
|
|
|
|
SendLoginRequestReject(tempSession, error);
|
|
}
|
|
|
|
public static void SendLoginRequestReject(Session session, CharacterError error)
|
|
{
|
|
// First we must send the connect request response
|
|
var connectRequest = new PacketOutboundConnectRequest(
|
|
Timers.PortalYearTicks,
|
|
session.Network.ConnectionData.ConnectionCookie,
|
|
session.Network.ClientId,
|
|
session.Network.ConnectionData.ServerSeed,
|
|
session.Network.ConnectionData.ClientSeed);
|
|
session.Network.ConnectionData.DiscardSeeds();
|
|
session.Network.EnqueueSend(connectRequest);
|
|
|
|
// Then we send the error
|
|
session.SendCharacterError(error);
|
|
|
|
session.Network.Update();
|
|
}
|
|
|
|
public static int GetSessionCount()
|
|
{
|
|
sessionLock.EnterReadLock();
|
|
try
|
|
{
|
|
return sessionMap.Count(s => s != null);
|
|
}
|
|
finally
|
|
{
|
|
sessionLock.ExitReadLock();
|
|
}
|
|
}
|
|
|
|
public static int GetAuthenticatedSessionCount()
|
|
{
|
|
sessionLock.EnterReadLock();
|
|
try
|
|
{
|
|
return sessionMap.Count(s => s != null && s.AccountId != 0);
|
|
}
|
|
finally
|
|
{
|
|
sessionLock.ExitReadLock();
|
|
}
|
|
}
|
|
|
|
public static int GetUniqueSessionEndpointCount()
|
|
{
|
|
sessionLock.EnterReadLock();
|
|
try
|
|
{
|
|
var ipAddresses = new HashSet<IPAddress>();
|
|
|
|
foreach (var s in sessionMap)
|
|
{
|
|
if (s != null)
|
|
ipAddresses.Add(s.EndPointC2S.Address);
|
|
}
|
|
|
|
return ipAddresses.Count;
|
|
}
|
|
finally
|
|
{
|
|
sessionLock.ExitReadLock();
|
|
}
|
|
}
|
|
|
|
public static int GetSessionEndpointTotalByAddressCount(IPAddress address)
|
|
{
|
|
sessionLock.EnterReadLock();
|
|
try
|
|
{
|
|
int result = 0;
|
|
|
|
foreach (var s in sessionMap)
|
|
{
|
|
if (s != null && s.EndPointC2S.Address.Equals(address))
|
|
result++;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
finally
|
|
{
|
|
sessionLock.ExitReadLock();
|
|
}
|
|
}
|
|
|
|
public static Session FindOrCreateSession(ConnectionListener connectionListener, IPEndPoint endPoint)
|
|
{
|
|
Session session;
|
|
|
|
sessionLock.EnterUpgradeableReadLock();
|
|
try
|
|
{
|
|
session = sessionMap.SingleOrDefault(s => s != null && endPoint.Equals(s.EndPointC2S));
|
|
if (session == null)
|
|
{
|
|
sessionLock.EnterWriteLock();
|
|
try
|
|
{
|
|
for (ushort i = 0; i < sessionMap.Length; i++)
|
|
{
|
|
if (sessionMap[i] == null)
|
|
{
|
|
log.DebugFormat("Creating new session for {0} with id {1}", endPoint, i);
|
|
session = new Session(connectionListener, endPoint, i, ServerId);
|
|
sessionMap[i] = session;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
sessionLock.ExitWriteLock();
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
sessionLock.ExitUpgradeableReadLock();
|
|
}
|
|
|
|
return session;
|
|
}
|
|
|
|
public static Session Find(uint accountId)
|
|
{
|
|
sessionLock.EnterReadLock();
|
|
try
|
|
{
|
|
return sessionMap.SingleOrDefault(s => s != null && s.AccountId == accountId);
|
|
}
|
|
finally
|
|
{
|
|
sessionLock.ExitReadLock();
|
|
}
|
|
}
|
|
|
|
public static Session Find(string account)
|
|
{
|
|
sessionLock.EnterReadLock();
|
|
try
|
|
{
|
|
return sessionMap.SingleOrDefault(s => s != null && s.Account == account);
|
|
}
|
|
finally
|
|
{
|
|
sessionLock.ExitReadLock();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes a session, network client and network endpoint from the various tracker objects.
|
|
/// </summary>
|
|
public static void RemoveSession(Session session)
|
|
{
|
|
sessionLock.EnterWriteLock();
|
|
try
|
|
{
|
|
log.DebugFormat("Removing session for {0} with id {1}", session.EndPointC2S, session.Network.ClientId);
|
|
if (sessionMap[session.Network.ClientId] == session)
|
|
sessionMap[session.Network.ClientId] = null;
|
|
}
|
|
finally
|
|
{
|
|
sessionLock.ExitWriteLock();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes all inbound GameAction messages.<para />
|
|
/// Dispatches all outgoing messages.<para />
|
|
/// Removes dead sessions.
|
|
/// </summary>
|
|
public static int DoSessionWork()
|
|
{
|
|
int sessionCount = 0;
|
|
|
|
sessionLock.EnterUpgradeableReadLock();
|
|
try
|
|
{
|
|
// The session tick outbound processes pending actions and handles outgoing messages
|
|
ServerPerformanceMonitor.RestartEvent(ServerPerformanceMonitor.MonitorType.DoSessionWork_TickOutbound);
|
|
Parallel.ForEach(sessionMap, ConfigManager.Config.Server.Threading.NetworkManagerParallelOptions, s => s?.TickOutbound());
|
|
ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.DoSessionWork_TickOutbound);
|
|
|
|
// Removes sessions in the NetworkTimeout state, including sessions that have reached a timeout limit.
|
|
ServerPerformanceMonitor.RestartEvent(ServerPerformanceMonitor.MonitorType.DoSessionWork_RemoveSessions);
|
|
foreach (var session in sessionMap.Where(k => !Equals(null, k)))
|
|
{
|
|
if (session.PendingTermination != null && session.PendingTermination.TerminationStatus == SessionTerminationPhase.SessionWorkCompleted)
|
|
{
|
|
session.DropSession();
|
|
session.PendingTermination.TerminationStatus = SessionTerminationPhase.WorldManagerWorkCompleted;
|
|
}
|
|
|
|
sessionCount++;
|
|
}
|
|
ServerPerformanceMonitor.RegisterEventEnd(ServerPerformanceMonitor.MonitorType.DoSessionWork_RemoveSessions);
|
|
}
|
|
finally
|
|
{
|
|
sessionLock.ExitUpgradeableReadLock();
|
|
}
|
|
return sessionCount;
|
|
}
|
|
|
|
public static void DisconnectAllSessionsForShutdown()
|
|
{
|
|
foreach (var session in sessionMap)
|
|
{
|
|
session?.Terminate(SessionTerminationReason.ServerShuttingDown, new GameMessages.Messages.GameMessageCharacterError(CharacterError.ServerCrash1));
|
|
}
|
|
}
|
|
}
|
|
}
|