ace/Source/ACE.Server/Network/NetworkSession.cs
Mag-nus c2ed2eab9a
Performance improvements with the help of Claude (#4472)
* Network performance improvements, limit boxing

* Polygon comment out redundant test code

* Transition remove duplicate Init()

* Creature_BodyPart and Monster_Melee comment out unused var
2026-08-03 11:48:37 +00:00

1010 lines
47 KiB
C#

using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using ACE.Common.Cryptography;
using ACE.Entity.Enum;
using ACE.Server.Entity;
using ACE.Server.Entity.Actions;
using ACE.Server.Network.Enum;
using ACE.Server.Network.GameMessages;
using ACE.Server.Network.GameMessages.Messages;
using ACE.Server.Network.Handlers;
using ACE.Server.Network.Managers;
using ACE.Server.Network.Packets;
using log4net;
namespace ACE.Server.Network
{
public class NetworkSession
{
private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
private static readonly ILog packetLog = LogManager.GetLogger(System.Reflection.Assembly.GetEntryAssembly(), "Packets");
private const int minimumTimeBetweenBundles = 5; // 5ms
private const int timeBetweenTimeSync = 20000; // 20s
private const int timeBetweenAck = 2000; // 2s
private readonly Session session;
private readonly ConnectionListener connectionC2S; // This is the connection the client transmits on. In retail this would be port 9000 for GLS; For world servers, examples would be port 9002 / 9004 / 9006 / 9008.
private readonly ConnectionListener connectionS2C; // This is the connection the server transmits on. In retail this would be port 9001 for GLS; For world servers, examples would be port 9003 / 9005 / 9007 / 9009.
private readonly Object[] currentBundleLocks = new Object[(int)GameMessageGroup.QueueMax];
private readonly NetworkBundle[] currentBundles = new NetworkBundle[(int)GameMessageGroup.QueueMax];
private ConcurrentDictionary<uint, ClientPacket> outOfOrderPackets = new ConcurrentDictionary<uint, ClientPacket>();
private ConcurrentDictionary<uint, MessageBuffer> partialFragments = new ConcurrentDictionary<uint, MessageBuffer>();
private ConcurrentDictionary<uint, ClientMessage> outOfOrderFragments = new ConcurrentDictionary<uint, ClientMessage>();
private DateTime nextSend = DateTime.UtcNow;
// Resync will be started after ConnectResponse, and should immediately be sent then, so no delay here.
// Fun fact: even though we send the server time in the ConnectRequest, client doesn't seem to use it? Therefore we must TimeSync early so client doesn't see a skew when we send it later.
public bool sendResync;
private DateTime nextResync = DateTime.UtcNow;
// Ack should be sent after a 2 second delay, so start enabled with the delay.
// Sending this too early seems to cause issues with clients disconnecting.
private bool sendAck = true;
private DateTime nextAck = DateTime.UtcNow.AddMilliseconds(timeBetweenAck);
private uint lastReceivedPacketSequence = 1;
private uint lastReceivedFragmentSequence;
/// <summary>
/// This is referenced from many threads:<para />
/// ConnectionListener.OnDataReceieve()->Session.HandlePacket()->This.HandlePacket(packet), This path can come from any client or other thinkable object.<para />
/// WorldManager.UpdateWorld()->Session.Update(lastTick)->This.Update(lastTick)
/// </summary>
private readonly ConcurrentDictionary<uint /*seq*/, ServerPacket> cachedPackets = new ConcurrentDictionary<uint /*seq*/, ServerPacket>();
private static readonly TimeSpan cachedPacketPruneInterval = TimeSpan.FromSeconds(5);
private DateTime lastCachedPacketPruneTime;
/// <summary>
/// Number of seconds to retain cachedPackets
/// </summary>
private const int cachedPacketRetentionTime = 120;
/// <summary>
/// This is referenced by multiple thread:<para />
/// [ConnectionListener Thread + 0] WorldManager.ProcessPacket()->SendLoginRequestReject()<para />
/// [ConnectionListener Thread + 0] WorldManager.ProcessPacket()->Session.ProcessPacket()->NetworkSession.ProcessPacket()->DoRequestForRetransmission()<para />
/// [ConnectionListener Thread + 1] WorldManager.ProcessPacket()->Session.ProcessPacket()->NetworkSession.ProcessPacket()-> ... AuthenticationHandler<para />
/// [World Manager Thread] WorldManager.UpdateWorld()->Session.Update(lastTick)->This.Update(lastTick)<para />
/// </summary>
private readonly ConcurrentQueue<ServerPacket> packetQueue = new ConcurrentQueue<ServerPacket>();
public readonly SessionConnectionData ConnectionData = new SessionConnectionData();
/// <summary>
/// Stores the tick value for the when an active session will timeout. If this value is in the past, the session is dead/inactive.
/// </summary>
public long TimeoutTick { get; set; }
public ushort ClientId { get; }
public ushort ServerId { get; }
public NetworkSession(Session session, ConnectionListener connectionListener, ushort clientId, ushort serverId)
{
this.session = session;
connectionC2S = connectionListener;
connectionS2C = SocketManager.GetMatchedConnectionListener(connectionC2S);
ClientId = clientId;
ServerId = serverId;
// New network auth session timeouts will always be low.
TimeoutTick = DateTime.UtcNow.AddSeconds(AuthenticationHandler.DefaultAuthTimeout).Ticks;
for (int i = 0; i < currentBundles.Length; i++)
{
currentBundleLocks[i] = new object();
currentBundles[i] = new NetworkBundle();
}
}
/// <summary>
/// Enequeues a GameMessage for sending to this client.
/// This may be called from many threads.
/// </summary>
/// <param name="messages">One or more GameMessages to send</param>
public void EnqueueSend(params GameMessage[] messages)
{
if (isReleased) // Session has been removed
return;
foreach (var message in messages)
{
var grp = message.Group;
var currentBundleLock = currentBundleLocks[(int) grp];
lock (currentBundleLock)
{
var currentBundle = currentBundles[(int) grp];
currentBundle.EncryptedChecksum = true;
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Enqueuing Message {1}", session.LoggingIdentifier, message.Opcode);
currentBundle.Enqueue(message);
}
}
}
/// <summary>
/// Enequeues a GameMessage for sending to this client.
/// This may be called from many threads.
/// </summary>
/// <param name="messages">One or more GameMessages to send</param>
public void EnqueueSend(IEnumerable<GameMessage> messages)
{
if (isReleased) // Session has been removed
return;
foreach (var message in messages)
{
var grp = message.Group;
var currentBundleLock = currentBundleLocks[(int)grp];
lock (currentBundleLock)
{
var currentBundle = currentBundles[(int)grp];
currentBundle.EncryptedChecksum = true;
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Enqueuing Message {1}", session.LoggingIdentifier, message.Opcode);
currentBundle.Enqueue(message);
}
}
}
/// <summary>
/// Enqueues a ServerPacket for sending to this client.
/// Currently this is only used publicly once during login. If that changes it's thread safety should be re
/// </summary>
/// <param name="packets"></param>
public void EnqueueSend(params ServerPacket[] packets)
{
if (isReleased) // Session has been removed
return;
foreach (var packet in packets)
{
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Enqueuing Packet {1}", session.LoggingIdentifier, packet.GetHashCode());
packetQueue.Enqueue(packet);
}
}
/// <summary>
/// Prunes the cachedPackets dictionary
/// Checks if we should send the current bundle and then flushes all pending packets.
/// </summary>
public void Update()
{
if (isReleased) // Session has been removed
return;
if (DateTime.UtcNow - lastCachedPacketPruneTime > cachedPacketPruneInterval)
PruneCachedPackets();
for (int i = 0; i < currentBundles.Length; i++)
{
NetworkBundle bundleToSend = null;
var group = (GameMessageGroup)i;
var currentBundleLock = currentBundleLocks[i];
lock (currentBundleLock)
{
var currentBundle = currentBundles[i];
if (group == GameMessageGroup.InvalidQueue)
{
if (sendResync && !currentBundle.TimeSync && DateTime.UtcNow > nextResync)
{
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Setting to send TimeSync packet", session.LoggingIdentifier);
currentBundle.TimeSync = true;
currentBundle.EncryptedChecksum = true;
nextResync = DateTime.UtcNow.AddMilliseconds(timeBetweenTimeSync);
}
if (sendAck && !currentBundle.SendAck && DateTime.UtcNow > nextAck)
{
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Setting to send ACK packet", session.LoggingIdentifier);
currentBundle.SendAck = true;
nextAck = DateTime.UtcNow.AddMilliseconds(timeBetweenAck);
}
if (currentBundle.NeedsSending && DateTime.UtcNow >= nextSend)
{
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Swapping bundle", session.LoggingIdentifier);
// Swap out bundle so we can process it
bundleToSend = currentBundle;
currentBundles[i] = new NetworkBundle();
}
}
else
{
if (currentBundle.NeedsSending && DateTime.UtcNow >= nextSend)
{
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Swapping bundle", session.LoggingIdentifier);
// Swap out bundle so we can process it
bundleToSend = currentBundle;
currentBundles[i] = new NetworkBundle();
}
}
}
// Send our bundle if we have one
// We should be able to execute this outside the lock as Sending is single threaded
// and all future writes from other threads will go to the new bundle
if (bundleToSend != null)
{
SendBundle(bundleToSend, group);
nextSend = DateTime.UtcNow.AddMilliseconds(minimumTimeBetweenBundles);
}
}
FlushPackets();
}
private void PruneCachedPackets()
{
lastCachedPacketPruneTime = DateTime.UtcNow;
var currentTime = (ushort)Timers.PortalYearTicks;
// Make sure our comparison still works when ushort wraps every 18.2 hours.
var removalList = cachedPackets.Values.Where(x => (currentTime >= x.Header.Time ? currentTime : currentTime + ushort.MaxValue) - x.Header.Time > cachedPacketRetentionTime);
foreach (var packet in removalList)
cachedPackets.TryRemove(packet.Header.Sequence, out _);
}
// This is called from ConnectionListener.OnDataReceieve()->Session.ProcessPacket()->This
/// <summary>
/// Processes and incoming packet from a client.
/// </summary>
/// <param name="packet">The ClientPacket to process.</param>
public void ProcessPacket(ClientPacket packet)
{
if (isReleased) // Session has been removed
return;
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Processing packet {1}", session.LoggingIdentifier, packet.Header.Sequence);
NetworkStatistics.C2S_Packets_Aggregate_Increment();
if (!packet.VerifyCRC(ConnectionData.CryptoClient))
{
return;
}
// If the client sent a NAK with a cleartext CRC then process it
if ((packet.Header.Flags & PacketHeaderFlags.RequestRetransmit) == PacketHeaderFlags.RequestRetransmit
&& !((packet.Header.Flags & PacketHeaderFlags.EncryptedChecksum) == PacketHeaderFlags.EncryptedChecksum))
{
List<uint> uncached = null;
foreach (uint sequence in packet.HeaderOptional.RetransmitData)
{
if (!Retransmit(sequence))
{
if (uncached == null)
uncached = new List<uint>();
uncached.Add(sequence);
}
}
if (uncached != null)
{
// Sends a response packet w/ PacketHeader.RejectRetransmit
var packetRejectRetransmit = new PacketRejectRetransmit(uncached);
EnqueueSend(packetRejectRetransmit);
}
NetworkStatistics.C2S_RequestsForRetransmit_Aggregate_Increment();
return; //cleartext crc NAK is never accompanied by additional data needed by the rest of the pipeline
}
#region order-insensitive "half-processing"
if (packet.Header.HasFlag(PacketHeaderFlags.Disconnect))
{
session.Terminate(SessionTerminationReason.PacketHeaderDisconnect);
return;
}
if (packet.Header.HasFlag(PacketHeaderFlags.NetErrorDisconnect))
{
session.Terminate(SessionTerminationReason.ClientSentNetworkErrorDisconnect);
return;
}
// depending on the current session state:
// Set the next timeout tick value, to compare against in the WorldManager
// Sessions that have gone past the AuthLoginRequest step will stay active for a longer period of time (exposed via configuration)
// Sessions that in the AuthLoginRequest will have a short timeout, as set in the AuthenticationHandler.DefaultAuthTimeout.
// Example: Applications that check uptime will stay in the AuthLoginRequest state.
session.Network.TimeoutTick = (session.State == SessionState.AuthLoginRequest) ?
DateTime.UtcNow.AddSeconds(AuthenticationHandler.DefaultAuthTimeout).Ticks : // Default is 15s
DateTime.UtcNow.AddSeconds(NetworkManager.DefaultSessionTimeout).Ticks; // Default is 60s
#endregion
#region Reordering stage
// Reordering stage
// Check if this packet's sequence is a sequence which we have already processed.
// There are some exceptions:
// Sequence 0 as we have several Seq 0 packets during connect. This also cathes a case where it seems CICMDCommand arrives at any point with 0 sequence value too.
// If the only header on the packet is AckSequence. It seems AckSequence can come in with the same sequence value sometimes.
if (packet.Header.Sequence <= lastReceivedPacketSequence && packet.Header.Sequence != 0 &&
!(packet.Header.Flags == PacketHeaderFlags.AckSequence && packet.Header.Sequence == lastReceivedPacketSequence))
{
packetLog.WarnFormat("[{0}] Packet {1} received again", session.LoggingIdentifier, packet.Header.Sequence);
return;
}
// Check if this packet's sequence is greater then the next one we should be getting.
// If true we must store it to replay once we have caught up.
var desiredSeq = lastReceivedPacketSequence + 1;
if (packet.Header.Sequence > desiredSeq)
{
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Packet {1} received out of order", session.LoggingIdentifier, packet.Header.Sequence);
if (!outOfOrderPackets.ContainsKey(packet.Header.Sequence))
outOfOrderPackets.TryAdd(packet.Header.Sequence, packet);
if (desiredSeq + 2 <= packet.Header.Sequence && DateTime.UtcNow - LastRequestForRetransmitTime > new TimeSpan(0, 0, 1))
DoRequestForRetransmission(packet.Header.Sequence);
return;
}
#endregion
#region Final processing stage
// Processing stage
// If we reach here, this is a packet we should proceed with processing.
HandleOrderedPacket(packet);
// Process data now in sequence
// Finally check if we have any out of order packets or fragments we need to process;
CheckOutOfOrderPackets();
CheckOutOfOrderFragments();
#endregion
}
const uint MaxNumNakSeqIds = 115; //464 + header = 484; (464 - 4) / 4
/// <summary>
/// request retransmission of lost sequences
/// </summary>
/// <param name="rcvdSeq">the sequence of the packet that was just received.</param>
private void DoRequestForRetransmission(uint rcvdSeq)
{
var desiredSeq = lastReceivedPacketSequence + 1;
List<uint> needSeq = new List<uint>();
needSeq.Add(desiredSeq);
uint bottom = desiredSeq + 1;
if (rcvdSeq < bottom || rcvdSeq - bottom > CryptoSystem.MaximumEffortLevel)
{
session.Terminate(SessionTerminationReason.AbnormalSequenceReceived);
return;
}
uint seqIdCount = 1;
for (uint a = bottom; a < rcvdSeq; a++)
{
if (!outOfOrderPackets.ContainsKey(a))
{
needSeq.Add(a);
seqIdCount++;
if (seqIdCount >= MaxNumNakSeqIds)
{
break;
}
}
}
ServerPacket reqPacket = new ServerPacket();
byte[] reqData = new byte[4 + (needSeq.Count * 4)];
MemoryStream msReqData = new MemoryStream(reqData, 0, reqData.Length, true, true);
msReqData.Write(BitConverter.GetBytes((uint)needSeq.Count), 0, 4);
needSeq.ForEach(k => msReqData.Write(BitConverter.GetBytes(k), 0, 4));
reqPacket.Data = msReqData;
reqPacket.Header.Flags = PacketHeaderFlags.RequestRetransmit;
EnqueueSend(reqPacket);
LastRequestForRetransmitTime = DateTime.UtcNow;
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Requested retransmit of {1}", session.LoggingIdentifier, needSeq.Select(k => k.ToString()).Aggregate((a, b) => a + ", " + b));
NetworkStatistics.S2C_RequestsForRetransmit_Aggregate_Increment();
}
private DateTime LastRequestForRetransmitTime = DateTime.MinValue;
/// <summary>
/// Handles a packet<para />
/// Packets at this stage are already verified, "half processed", and reordered
/// </summary>
/// <param name="packet">ClientPacket to handle</param>
private void HandleOrderedPacket(ClientPacket packet)
{
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Handling packet {1}", session.LoggingIdentifier, packet.Header.Sequence);
// If we have an EchoRequest flag, we should flag to respond with an echo response on next send.
if (packet.Header.HasFlag(PacketHeaderFlags.EchoRequest))
{
FlagEcho(packet.HeaderOptional.EchoRequestClientTime);
VerifyEcho(packet.HeaderOptional.EchoRequestClientTime);
}
// If we have an AcknowledgeSequence flag, we can clear our cached packet buffer up to that sequence.
if (packet.Header.HasFlag(PacketHeaderFlags.AckSequence))
AcknowledgeSequence(packet.HeaderOptional.AckSequence);
if (packet.Header.HasFlag(PacketHeaderFlags.TimeSync))
{
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Incoming TimeSync TS: {1}", session.LoggingIdentifier, packet.HeaderOptional.TimeSynch);
// Do something with this...
// Based on network traces these are not 1:1. Server seems to send them every 20 seconds per port.
// Client seems to send them alternatingly every 2 or 4 seconds per port.
// We will send this at a 20 second time interval. I don't know what to do with these when we receive them at this point.
}
// This should be set on the first packet to the server indicating the client is logging in.
// This is the start of a three-way handshake between the client and server (LoginRequest, ConnectRequest, ConnectResponse)
// Note this would be sent to each server a client would connect too (Login and each world).
// In our current implimenation we handle all roles in this one server.
if (packet.Header.HasFlag(PacketHeaderFlags.LoginRequest))
{
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] LoginRequest", session.LoggingIdentifier);
AuthenticationHandler.HandleLoginRequest(packet, session);
return;
}
// Process all fragments out of the packet
foreach (ClientPacketFragment fragment in packet.Fragments)
ProcessFragment(fragment);
// Update the last received sequence.
if (packet.Header.Sequence != 0 && packet.Header.Flags != PacketHeaderFlags.AckSequence)
lastReceivedPacketSequence = packet.Header.Sequence;
}
/// <summary>
/// Processes a fragment, combining split fragments as needed, then handling them
/// </summary>
/// <param name="fragment">ClientPacketFragment to process</param>
private void ProcessFragment(ClientPacketFragment fragment)
{
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Processing fragment {1}", session.LoggingIdentifier, fragment.Header.Sequence);
ClientMessage message = null;
// Check if this fragment is split
if (fragment.Header.Count != 1)
{
// Packet is split
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Fragment {1} is split, this index {2} of {3} fragments", session.LoggingIdentifier, fragment.Header.Sequence, fragment.Header.Index, fragment.Header.Count);
if (partialFragments.TryGetValue(fragment.Header.Sequence, out var buffer))
{
// Existing buffer, add this to it and check if we are finally complete.
buffer.AddFragment(fragment);
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Added fragment {1} to existing buffer. Buffer at {2} of {3}", session.LoggingIdentifier, fragment.Header.Sequence, buffer.Count, buffer.TotalFragments);
if (buffer.Complete)
{
// The buffer is complete, so we can go ahead and handle
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Buffer {1} is complete", session.LoggingIdentifier, buffer.Sequence);
message = buffer.TryGetMessage();
MessageBuffer removed = null;
partialFragments.TryRemove(fragment.Header.Sequence, out removed);
}
}
else
{
// No existing buffer, so add a new one for this fragment sequence.
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Creating new buffer {1} for this split fragment", session.LoggingIdentifier, fragment.Header.Sequence);
var newBuffer = new MessageBuffer(fragment.Header.Sequence, fragment.Header.Count);
newBuffer.AddFragment(fragment);
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Added fragment {1} to the new buffer. Buffer at {2} of {3}", session.LoggingIdentifier, fragment.Header.Sequence, newBuffer.Count, newBuffer.TotalFragments);
partialFragments.TryAdd(fragment.Header.Sequence, newBuffer);
}
}
else
{
// Packet is not split, proceed with handling it.
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Fragment {1} is not split", session.LoggingIdentifier, fragment.Header.Sequence);
if (fragment.Data.Length >= 4) // ClientMessage must be a minimum of 4 bytes in length
message = new ClientMessage(fragment.Data);
}
// If message is not null, we have a complete message to handle
if (message != null)
{
// First check if this message is the next sequence, if it is not, add it to our outOfOrderFragments
if (fragment.Header.Sequence == lastReceivedFragmentSequence + 1)
{
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Handling fragment {1}", session.LoggingIdentifier, fragment.Header.Sequence);
HandleFragment(message);
}
else
{
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Fragment {1} is early, lastReceivedFragmentSequence = {2}", session.LoggingIdentifier, fragment.Header.Sequence, lastReceivedFragmentSequence);
outOfOrderFragments.TryAdd(fragment.Header.Sequence, message);
}
}
}
/// <summary>
/// Handles a ClientMessage by calling using InboundMessageManager
/// </summary>
/// <param name="message">ClientMessage to process</param>
private void HandleFragment(ClientMessage message)
{
InboundMessageManager.HandleClientMessage(message, session);
lastReceivedFragmentSequence++;
}
/// <summary>
/// Checks if we now have packets queued out of order which should be processed as the next sequence.
/// </summary>
private void CheckOutOfOrderPackets()
{
while (outOfOrderPackets.TryRemove(lastReceivedPacketSequence + 1, out var packet))
{
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Ready to handle out-of-order packet {1}", session.LoggingIdentifier, packet.Header.Sequence);
HandleOrderedPacket(packet);
}
}
/// <summary>
/// Checks if we now have fragments queued out of order which should be handled as the next sequence.
/// </summary>
private void CheckOutOfOrderFragments()
{
while (outOfOrderFragments.TryRemove(lastReceivedFragmentSequence + 1, out var message))
{
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Ready to handle out of order fragment {1}", session.LoggingIdentifier, lastReceivedFragmentSequence + 1);
HandleFragment(message);
}
}
//private List<EchoStamp> EchoStamps = new List<EchoStamp>();
private static int EchoLogInterval = 5;
private static int EchoInterval = 10;
private static float EchoThreshold = 2.0f;
private static float DiffThreshold = 0.01f;
private float lastClientTime;
private DateTime lastServerTime;
private double lastDiff;
private int echoDiff;
private void VerifyEcho(float clientTime)
{
if (session.Player == null || session.logOffRequestTime != DateTime.MinValue)
return;
var serverTime = DateTime.UtcNow;
if (lastClientTime == 0)
{
lastClientTime = clientTime;
lastServerTime = serverTime;
return;
}
var serverTimeDiff = serverTime - lastServerTime;
var clientTimeDiff = clientTime - lastClientTime;
var diff = Math.Abs(serverTimeDiff.TotalSeconds - clientTimeDiff);
if (diff > EchoThreshold && diff - lastDiff > DiffThreshold)
{
lastDiff = diff;
echoDiff++;
if (echoDiff >= EchoLogInterval)
log.Warn($"{session.Player.Name} - TimeSync error: {echoDiff} (diff: {diff})");
if (echoDiff >= EchoInterval)
{
log.Error($"{session.Player.Name} - disconnected for speedhacking");
var actionChain = new ActionChain();
actionChain.AddAction(session.Player, () =>
{
//session.Network.EnqueueSend(new GameMessageBootAccount(session));
session.Network.EnqueueSend(new GameMessageSystemChat($"TimeSync: client speed error", ChatMessageType.Broadcast));
session.LogOffPlayer();
echoDiff = 0;
lastDiff = 0;
lastClientTime = 0;
});
actionChain.EnqueueChain();
}
}
else if (echoDiff > 0)
{
if (echoDiff > EchoLogInterval)
log.Warn($"{session.Player.Name} - Diff: {diff}");
lastDiff = 0;
echoDiff = 0;
}
}
//is this special channel
private void FlagEcho(float clientTime)
{
var currentBundleLock = currentBundleLocks[(int)GameMessageGroup.InvalidQueue];
lock (currentBundleLock)
{
var currentBundle = currentBundles[(int)GameMessageGroup.InvalidQueue];
// Debug.Assert(clientTime == -1f, "Multiple EchoRequests before Flush, potential issue with network logic!");
currentBundle.ClientTime = clientTime;
currentBundle.EncryptedChecksum = true;
}
}
private void AcknowledgeSequence(uint sequence)
{
// TODO Sending Acks seems to cause some issues. Needs further research.
// if (!sendAck)
// sendAck = true;
var removalList = cachedPackets.Keys.Where(x => x < sequence);
foreach (var key in removalList)
cachedPackets.TryRemove(key, out _);
}
private bool Retransmit(uint sequence)
{
if (cachedPackets.TryGetValue(sequence, out var cachedPacket))
{
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Retransmit {1}", session.LoggingIdentifier, sequence);
if (!cachedPacket.Header.HasFlag(PacketHeaderFlags.Retransmission))
cachedPacket.Header.Flags |= PacketHeaderFlags.Retransmission;
SendPacketRaw(cachedPacket);
return true;
}
if (packetLog.IsDebugEnabled)
{
if (cachedPackets.Count > 0)
{
// This is to catch a race condition between .Count and .Min() and .Max()
try
{
packetLog.DebugFormat("Session {0}\\{1} ({2}:{3}) retransmit requested packet {4} not in cache. Cache range {5} - {6}.", session.Network?.ClientId, session.EndPointC2S, session.Account, session.Player?.Name, sequence, cachedPackets.Keys.Min(), cachedPackets.Keys.Max());
}
catch
{
packetLog.DebugFormat("Session {0}\\{1} ({2}:{3}) retransmit requested packet {4} not in cache. Cache is empty. Race condition threw exception.", session.Network?.ClientId, session.EndPointC2S, session.Account, session.Player?.Name, sequence);
}
}
else
packetLog.DebugFormat("Session {0}\\{1} ({2}:{3}) retransmit requested packet {4} not in cache. Cache is empty.", session.Network?.ClientId, session.EndPointC2S, session.Account, session.Player?.Name, sequence);
}
return false;
}
private void FlushPackets()
{
while (packetQueue.TryDequeue(out var packet))
{
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Flushing packets, count {1}", session.LoggingIdentifier, packetQueue.Count);
if (packet.Header.HasFlag(PacketHeaderFlags.EncryptedChecksum) && ConnectionData.PacketSequence.CurrentValue == 0)
ConnectionData.PacketSequence = new Sequence.UIntSequence(1);
bool isNak = packet.Header.Flags.HasFlag(PacketHeaderFlags.RequestRetransmit);
// If we are only ACKing, then we don't seem to have to increment the sequence
if (packet.Header.Flags == PacketHeaderFlags.AckSequence || isNak)
packet.Header.Sequence = ConnectionData.PacketSequence.CurrentValue;
else
packet.Header.Sequence = ConnectionData.PacketSequence.NextValue;
packet.Header.Id = ServerId;
packet.Header.Iteration = 0x01;
packet.Header.Time = (ushort)Timers.PortalYearTicks;
if (packet.Header.Sequence >= 2u && !isNak)
cachedPackets.TryAdd(packet.Header.Sequence, packet);
SendPacket(packet);
}
}
private void SendPacket(ServerPacket packet)
{
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Sending packet {1}", session.LoggingIdentifier, packet.GetHashCode());
NetworkStatistics.S2C_Packets_Aggregate_Increment();
if (packet.Header.HasFlag(PacketHeaderFlags.EncryptedChecksum))
{
uint issacXor = ConnectionData.IssacServer.Next();
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Setting Issac for packet {1} to {2}", session.LoggingIdentifier, packet.GetHashCode(), issacXor);
packet.IssacXor = issacXor;
}
SendPacketRaw(packet);
}
private void SendPacketRaw(ServerPacket packet)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent((int)(PacketHeader.HeaderSize + (packet.Data?.Length ?? 0) + (packet.Fragments.Count * PacketFragment.MaxFragementSize)));
try
{
// On connection to server, client expects response on the connection it initiated, once that occurs, the client connects to the +1 port and then the server transmits on that connection, while the client continues to transmit on the initial port.
var socket = (session.EndPointS2C is null) ? connectionC2S.Socket : connectionS2C.Socket;
var endPoint = (session.EndPointS2C is null) ? session.EndPointC2S : session.EndPointS2C;
packet.CreateReadyToSendPacket(buffer, out var size);
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("{0}", packet);
if (packetLog.IsDebugEnabled)
{
var listenerEndpoint = (System.Net.IPEndPoint)socket.LocalEndPoint;
var sb = new StringBuilder();
sb.AppendLine(String.Format("[{5}] Sending Packet (Len: {0}) [{1}:{2}=>{3}:{4}]", size, listenerEndpoint.Address, listenerEndpoint.Port, endPoint.Address, endPoint.Port, session.Network.ClientId));
sb.AppendLine(buffer.BuildPacketString(0, size));
packetLog.DebugFormat("{0}", sb);
}
try
{
socket.SendTo(buffer, size, SocketFlags.None, endPoint);
}
catch (SocketException ex)
{
// Unhandled Exception: System.Net.Sockets.SocketException: A message sent on a datagram socket was larger than the internal message buffer or some other network limit, or the buffer used to receive a datagram into was smaller than the datagram itself
// at System.Net.Sockets.Socket.UpdateStatusAfterSocketErrorAndThrowException(SocketError error, String callerName)
// at System.Net.Sockets.Socket.SendTo(Byte[] buffer, Int32 offset, Int32 size, SocketFlags socketFlags, EndPoint remoteEP)
var listenerEndpoint = (System.Net.IPEndPoint)socket.LocalEndPoint;
var sb = new StringBuilder();
sb.AppendLine(ex.ToString());
sb.AppendLine(String.Format("[{5}] Sending Packet (Len: {0}) [{1}:{2}=>{3}:{4}]", buffer.Length, listenerEndpoint.Address, listenerEndpoint.Port, endPoint.Address, endPoint.Port, session.Network.ClientId));
log.Error(sb.ToString());
session.Terminate(SessionTerminationReason.SendToSocketException, null, null, ex.Message);
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
/// <summary>
/// This function handles turning a bundle of messages (representing all messages accrued in a timeslice),
/// into 1 or more packets, combining multiple messages into one packet or spliting large message across
/// several packets as needed.
/// </summary>
/// <param name="bundle"></param>
private void SendBundle(NetworkBundle bundle, GameMessageGroup group)
{
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Sending Bundle", session.LoggingIdentifier);
bool writeOptionalHeaders = true;
List<MessageFragment> fragments = new List<MessageFragment>();
// Pull all messages out and create MessageFragment objects
while (bundle.HasMoreMessages)
{
var message = bundle.Dequeue();
var fragment = new MessageFragment(message, ConnectionData.FragmentSequence++);
fragments.Add(fragment);
}
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Bundle Fragment Count: {1}", session.LoggingIdentifier, fragments.Count);
// Loop through while we have fragements
while (fragments.Count > 0 || writeOptionalHeaders)
{
ServerPacket packet = new ServerPacket();
PacketHeader packetHeader = packet.Header;
if (fragments.Count > 0)
packetHeader.Flags |= PacketHeaderFlags.BlobFragments;
if (bundle.EncryptedChecksum)
packetHeader.Flags |= PacketHeaderFlags.EncryptedChecksum;
int availableSpace = ServerPacket.MaxPacketSize;
// Pull first message and see if it is a large one
var firstMessage = fragments.FirstOrDefault();
if (firstMessage != null)
{
// If a large message send only this one, filling the whole packet
if (firstMessage.DataRemaining >= availableSpace)
{
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Sending large fragment", session.LoggingIdentifier);
ServerPacketFragment spf = firstMessage.GetNextFragment();
packet.Fragments.Add(spf);
availableSpace -= spf.Length;
if (firstMessage.DataRemaining <= 0)
fragments.Remove(firstMessage);
}
// Otherwise we'll write any optional headers and process any small messages that will fit
else
{
if (writeOptionalHeaders)
{
writeOptionalHeaders = false;
WriteOptionalHeaders(bundle, packet);
if (packet.Data != null)
availableSpace -= (int)packet.Data.Length;
}
// Create a list to remove completed messages after iterator
List<MessageFragment> removeList = new List<MessageFragment>();
foreach (MessageFragment fragment in fragments)
{
bool fragmentSkipped = false;
// Is this a large fragment and does it have a tail that needs sending?
if (!fragment.TailSent && availableSpace >= fragment.TailSize)
{
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Sending tail fragment", session.LoggingIdentifier);
ServerPacketFragment spf = fragment.GetTailFragment();
packet.Fragments.Add(spf);
availableSpace -= spf.Length;
}
// Otherwise will this message fit in the remaining space?
else if (availableSpace >= fragment.NextSize)
{
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Sending small message", session.LoggingIdentifier);
ServerPacketFragment spf = fragment.GetNextFragment();
packet.Fragments.Add(spf);
availableSpace -= spf.Length;
}
else
fragmentSkipped = true;
// If message is out of data, set to remove it
if (fragment.DataRemaining <= 0)
removeList.Add(fragment);
// UIQueue messages must go out in order. Otherwise, you might see an NPC's tells in an order that doesn't match their defined emotes.
if (fragmentSkipped && group == GameMessageGroup.UIQueue)
break;
}
// Remove all completed messages
fragments.RemoveAll(x => removeList.Contains(x));
}
}
// If no messages, write optional headers
else
{
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] No messages, just sending optional headers", session.LoggingIdentifier);
if (writeOptionalHeaders)
{
writeOptionalHeaders = false;
WriteOptionalHeaders(bundle, packet);
if (packet.Data != null)
availableSpace -= (int)packet.Data.Length;
}
}
EnqueueSend(packet);
}
}
private void WriteOptionalHeaders(NetworkBundle bundle, ServerPacket packet)
{
PacketHeader packetHeader = packet.Header;
if (bundle.SendAck) // 0x4000
{
packetHeader.Flags |= PacketHeaderFlags.AckSequence;
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Outgoing AckSeq: {1}", session.LoggingIdentifier, lastReceivedPacketSequence);
packet.InitializeDataWriter();
packet.DataWriter.Write(lastReceivedPacketSequence);
}
if (bundle.TimeSync) // 0x1000000
{
packetHeader.Flags |= PacketHeaderFlags.TimeSync;
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Outgoing TimeSync TS: {1}", session.LoggingIdentifier, Timers.PortalYearTicks);
packet.InitializeDataWriter();
packet.DataWriter.Write(Timers.PortalYearTicks);
}
if (bundle.ClientTime != -1f) // 0x4000000
{
packetHeader.Flags |= PacketHeaderFlags.EchoResponse;
if (packetLog.IsDebugEnabled)
packetLog.DebugFormat("[{0}] Outgoing EchoResponse: {1}", session.LoggingIdentifier, bundle.ClientTime);
packet.InitializeDataWriter();
packet.DataWriter.Write(bundle.ClientTime);
packet.DataWriter.Write((float)Timers.PortalYearTicks - bundle.ClientTime);
}
}
private bool isReleased;
/// <summary>
/// This will empty out arrays, collections and dictionaries, and mark the object as released.
/// Any further work assigned to this object will be ignored.
/// </summary>
public void ReleaseResources()
{
isReleased = true;
for (int i = 0; i < currentBundles.Length; i++)
currentBundles[i] = null;
outOfOrderPackets.Clear();
partialFragments.Clear();
outOfOrderFragments.Clear();
cachedPackets.Clear();
packetQueue.Clear();
ConnectionData.CryptoClient.ReleaseResources();
}
}
}