This commit is contained in:
FangkuaiYa committed 2026-08-17 01:06:56 +08:00
1 parent 50a159f8cf
commit a4de6a83d9
4 files changed
+65 -8

No files matched your search

@@ -9,7 +9,11 @@ namespace Empostor.Server
{
public static void AddEventPools(this IServiceCollection services)
{
services.TryAddSingleton<ObjectPoolProvider>(new DefaultObjectPoolProvider());
// Larger object-pool retention so high-frequency packet allocation
// (MessageReader, event objects) is served from the pool instead of
// hammering the GC under load (e.g. 40+ players).
services.TryAddSingleton<ObjectPoolProvider>(
new DefaultObjectPoolProvider { MaximumRetained = 512 });
services.AddSingleton(serviceProvider =>
{
@@ -4,6 +4,7 @@ using Empostor.Api;
using Empostor.Api.Config;
using Empostor.Api.Net;
using Next.Hazel;
using Next.Hazel.Udp;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -21,6 +22,16 @@ namespace Empostor.Server.Net.Hazel
InnerConnection = innerConnection;
innerConnection.DataReceived = ConnectionOnDataReceived;
innerConnection.Disconnected = ConnectionOnDisconnected;
if (innerConnection is UdpConnection udp)
{
// Tolerate UDP packet loss under load (e.g. 40+ players). A few
// dropped reliable packets or keepalive pings are retransmitted
// for longer before the connection is declared dead, so a burst
// of loss no longer disconnects players.
udp.DisconnectTimeout = 10000;
udp.MissingPingsUntilDisconnect = 12;
}
}
public Connection InnerConnection { get; }
@@ -33,10 +33,17 @@ public abstract partial class UdpConnection : NetworkConnection
_readerPool = readerPool;
_stoppingCts = new CancellationTokenSource();
Pipeline = Channel.CreateUnbounded<byte[]>(new UnboundedChannelOptions
// Bound the inbound queue so a slow/overloaded connection cannot let
// packets accumulate without limit. Once full, the oldest queued packet
// is dropped (DropOldest) — reliable packets are retransmitted by the
// sender anyway and unreliable ones carry fresh state each tick, so this
// keeps memory flat under sustained load instead of letting it balloon
// until the GC pressure starts disconnecting everyone.
Pipeline = Channel.CreateBounded<byte[]>(new BoundedChannelOptions(1024)
{
SingleReader = true,
SingleWriter = true
SingleWriter = true,
FullMode = BoundedChannelFullMode.DropOldest,
});
}
@@ -46,8 +53,13 @@ public abstract partial class UdpConnection : NetworkConnection
public Task StartAsync()
{
// Store the task we're executing
_executingTask = Task.Factory.StartNew(ReadAsync, TaskCreationOptions.LongRunning);
// Run the read loop on the thread pool instead of a dedicated
// long-running thread per connection. ReadAsync is fully async (it
// awaits the pipeline channel), so it only occupies a thread-pool
// thread while there is actually data to process. With hundreds of
// connections this avoids hundreds of dedicated threads (1 MB stack
// each) and the resulting context-switch overhead.
_executingTask = Task.Run(ReadAsync);
// If the task is completed then return it, this will bubble cancellation and failure to the caller
if (_executingTask.IsCompleted) return _executingTask;
@@ -1,5 +1,6 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;
@@ -65,6 +66,20 @@ public class UdpConnectionListener : NetworkConnectionListener
{
}
try
{
// Under high load (many players / high tick rate) the OS-level UDP
// receive queue can overflow and silently drop packets; reliable
// retransmission / keepalive then misreads that as a dead connection
// and disconnects players. Use a large buffer so bursts are absorbed
// instead of dropped.
_socket.Client.ReceiveBufferSize = 4 * 1024 * 1024;
_socket.Client.SendBufferSize = 4 * 1024 * 1024;
}
catch (SocketException)
{
}
_reliablePacketTimer = new Timer(ManageReliablePackets, null, 100, Timeout.Infinite);
_allConnections = new ConcurrentDictionary<EndPoint, UdpServerConnection>();
@@ -79,10 +94,25 @@ public class UdpConnectionListener : NetworkConnectionListener
private async void ManageReliablePackets(object? state)
{
foreach (var kvp in _allConnections)
// Resend reliably-sent packets for every connection. Run them
// concurrently instead of serially awaiting each connection: with many
// players one slow connection used to stall retransmission for everyone
// else, which made packet loss snowball into mass disconnects.
var connections = new List<UdpServerConnection>(_allConnections.Values);
var tasks = new Task[connections.Count];
for (var i = 0; i < connections.Count; i++)
{
var sock = kvp.Value;
await sock.ManageReliablePackets();
var conn = connections[i];
tasks[i] = conn.ManageReliablePackets().AsTask();
}
try
{
await Task.WhenAll(tasks);
}
catch
{
// ignored
}
try