进行对象池优化发送部分,减少GC.

This commit is contained in:
wuyanchen 2025-12-25 15:48:18 +08:00
parent 0daff632f2
commit 2b28969d27
6 changed files with 280 additions and 73 deletions

View File

@ -1,6 +1,4 @@
using MessagePack;
using System.Numerics;
using System.Text.Json;
namespace XNet.Business
{
@ -28,7 +26,7 @@ namespace XNet.Business
[Key("agentIdx")]
public int AgentIdx { get; set; }
[Key("position")]
public Vector3 Position { get; set; }
public Vector3Msg Position { get; set; }
[Key("rotation")]
public float Rotation { get; set; } // 简化为绕Y轴旋转弧度
}
@ -40,6 +38,6 @@ namespace XNet.Business
[Key("type")]
public WsMsgType Type { get; set; } = WsMsgType.SubscribeInstance;
[Key("data")]
public byte[] Data { get; set; } = string.Empty;
public byte[] Data { get; set; } = [];
}
}

View File

@ -12,6 +12,7 @@ namespace XNet.Business
private const int TARGET_FPS = 30;
private const int FRAME_TIME_MS = 1000 / TARGET_FPS;
private readonly List<string> _instanceIds = new List<string>();
public GameLoopService(NavMeshManager navMeshManager, SceneAgent sceneAgent, WsConnectionManager wsManager)
{
@ -20,6 +21,19 @@ namespace XNet.Business
_wsManager = wsManager;
}
public void CreateInstance(string instanceId, string mapId)
{
_navMeshManager.CreateInstance(instanceId, mapId);
_instanceIds.Add(instanceId);
}
public void RemoveInstance(string instanceId)
{
_navMeshManager.RemoveInstance(instanceId);
_instanceIds.Remove(instanceId);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Console.WriteLine("=== Server Initializing Resources... ===");
@ -35,8 +49,8 @@ namespace XNet.Business
string instanceGuid_B = "Instance_TeamB_" + Guid.NewGuid();
// 注册 NavMesh 映射
_navMeshManager.CreateInstance(instanceGuid_A, "Map_Forest");
_navMeshManager.CreateInstance(instanceGuid_B, "Map_Forest"); // 复用同一份 NavMesh 内存
CreateInstance(instanceGuid_A, "Map_Forest");
CreateInstance(instanceGuid_B, "Map_Forest"); // 复用同一份 NavMesh 内存
// 为这两个副本创建独立的物理/避障模拟器
_sceneAgent.CreateCrowdForInstance(instanceGuid_A);
@ -74,19 +88,36 @@ namespace XNet.Business
// 这行代码利用 SceneAgent 内部的 Parallel.ForEach效率极高
_sceneAgent.UpdateAll(deltaTime);
// 2. 收集所有需要同步的Agent状态
List<AgentPositionSyncMsg> allSyncMsgs =
[
// 遍历所有实例可从_navMeshManager获取实例列表或SceneAgent维护
.. _sceneAgent.GetAgentsNeedSync(instanceGuid_A),
.. _sceneAgent.GetAgentsNeedSync(instanceGuid_B),
];
//// 2. 收集所有需要同步的Agent状态
//List<AgentPositionSyncMsg> allSyncMsgs = [];
//foreach (var instanceId in _instanceIds)
//{
// var syncMsgs = _sceneAgent.GetAgentsNeedSync(instanceId);
// allSyncMsgs.AddRange(syncMsgs);
//}
// 3. 异步发送WebSocket消息不阻塞游戏循环
//// 3. 异步发送WebSocket消息不阻塞游戏循环
//if (allSyncMsgs.Count > 0)
//{
// _ = _wsManager.SendAgentPositionBatchAsync(allSyncMsgs);
//}
// 2. 异步收集+推送消息IO密集型提交到专用线程池不阻塞主循环
_ = Task.Run(async () =>
{
List<AgentPositionSyncMsg> allSyncMsgs = [];
foreach (var instanceId in _instanceIds)
{
var syncMsgs = _sceneAgent.GetAgentsNeedSync(instanceId);
allSyncMsgs.AddRange(syncMsgs);
}
if (allSyncMsgs.Count > 0)
{
_ = _wsManager.SendAgentPositionBatchAsync(allSyncMsgs);
await _wsManager.SendAgentPositionBatchAsync(allSyncMsgs);
}
}, stoppingToken);
// 帧率控制
long frameWorkTime = stopwatch.ElapsedMilliseconds - currentTime;

View File

@ -113,7 +113,7 @@ namespace XNet.Business
{
InstanceId = instanceId,
AgentIdx = agentIdx,
Position = currPos,
Position = new Vector3Msg { X = currPos.X, Y = currPos.Y, Z = currPos.Z },
Rotation = currRot
});
// 更新缓存
@ -127,7 +127,7 @@ namespace XNet.Business
{
InstanceId = instanceId,
AgentIdx = agentIdx,
Position = currPos,
Position = new Vector3Msg { X = currPos.X, Y = currPos.Y, Z = currPos.Z },
Rotation = currRot
});
lastStates.TryAdd(agentIdx, new AgentState { Position = currPos, Rotation = currRot });
@ -303,7 +303,10 @@ namespace XNet.Business
public void UpdateAll(float deltaTime)
{
Parallel.ForEach(_crowdInstances.Values, ci =>
Parallel.ForEach(_crowdInstances.Values, new ParallelOptions
{
MaxDegreeOfParallelism = Environment.ProcessorCount // 限制并行数=CPU核心数避免过载
}, ci =>
{
lock (ci.SyncRoot)
{

View File

@ -1,38 +1,113 @@
using MessagePack;
using Microsoft.Extensions.ObjectPool;
using NanoidDotNet;
using System.Collections.Concurrent;
using System.Net.Sockets;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
namespace XNet.Business
{
public class WsConnectionManager
{
// 存储所有活跃的WebSocket连接
// ========== 原有核心字段 ==========
private readonly ConcurrentDictionary<string, WebSocket> _connections = new();
// 存储实例ID -> 订阅该实例的连接ID列表
private readonly ConcurrentDictionary<string, HashSet<string>> _instanceSubscribers = new();
// 线程安全的随机数生成器生成连接ID
private readonly Random _random = new();
// 序列化选项(复用避免重复创建)
private readonly JsonSerializerOptions _jsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
// 新增WebSocket连接
// ========== 新增:对象池配置 ==========
// 对象池(使用自定义重写的策略)
private readonly ObjectPool<List<AgentPositionSyncMsg>> _syncMsgListPool;
private readonly ObjectPool<List<string>> _deadConnListPool;
private readonly ObjectPool<byte[]> _byteArrayPool;
// ========== 核心:继承 DefaultPooledObjectPolicy 并重写 Create/Return ==========
/// <summary>
/// 自定义 Agent 消息列表池化策略(重写 Create/Return
/// </summary>
public sealed class SyncMsgListPolicy : DefaultPooledObjectPolicy<List<AgentPositionSyncMsg>>
{
// 重写 Create 方法:指定初始容量,减少扩容开销
public override List<AgentPositionSyncMsg> Create()
{
return new List<AgentPositionSyncMsg>(100); // 初始容量100
}
// 重写 Return 方法:清空列表数据,保留容量
public override bool Return(List<AgentPositionSyncMsg> obj)
{
obj.Clear(); // 关键:归还前清空数据,避免脏数据
return true; // 返回true表示可复用
}
}
/// <summary>
/// 自定义失效连接列表池化策略
/// </summary>
public sealed class DeadConnListPolicy : DefaultPooledObjectPolicy<List<string>>
{
public override List<string> Create()
{
return new List<string>(50); // 初始容量50
}
public override bool Return(List<string> obj)
{
obj.Clear();
return true;
}
}
// ========== 2. byte[] 直接实现 IPooledPolicy绕过 new() 约束) ==========
public sealed class ByteArrayPolicy : IPooledObjectPolicy<byte[]>
{
private readonly int _defaultSize;
public ByteArrayPolicy(int defaultSize = 4096)
{
_defaultSize = defaultSize;
}
// 直接实现 Create 方法,自定义数组创建逻辑(指定长度)
public byte[] Create()
{
return new byte[_defaultSize]; // 合法:数组创建必须指定长度
}
// 直接实现 Return 方法,自定义归还逻辑
public bool Return(byte[] obj)
{
if (obj.Length != _defaultSize) return false; // 仅复用指定大小的数组
Array.Clear(obj); // 清空数据,避免泄露
return true;
}
}
public WsConnectionManager()
{
// 配置对象池参数
var maximumRetained = 200;
// 初始化对象池(传入自定义策略)
_syncMsgListPool = new DefaultObjectPool<List<AgentPositionSyncMsg>>(new SyncMsgListPolicy(), maximumRetained);
_deadConnListPool = new DefaultObjectPool<List<string>>(new DeadConnListPolicy(), maximumRetained);
_byteArrayPool = new DefaultObjectPool<byte[]>(new ByteArrayPolicy(4096), maximumRetained);
}
// ========== 业务方法(无修改) ==========
public string AddConnection(WebSocket socket)
{
string connId = GenerateConnId();
string connId = $"Conn_{Nanoid.Generate()}";
_connections.TryAdd(connId, socket);
Console.WriteLine($"[WS] 新连接:{connId},当前连接数:{_connections.Count}");
Console.WriteLine($"[WS .NET 10] 新连接:{connId},当前连接数:{_connections.Count}");
return connId;
}
// 移除WebSocket连接清理订阅关系
public void RemoveConnection(string connId)
{
if (_connections.TryRemove(connId, out _))
{
// 清理该连接的所有订阅
foreach (var instanceId in _instanceSubscribers.Keys)
{
lock (_instanceSubscribers[instanceId])
@ -40,64 +115,98 @@ namespace XNet.Business
_instanceSubscribers[instanceId].Remove(connId);
}
}
Console.WriteLine($"[WS] 连接断开:{connId},当前连接数:{_connections.Count}");
Console.WriteLine($"[WS .NET 10] 连接断开:{connId},当前连接数:{_connections.Count}");
}
}
// 客户端订阅实例
public bool SubscribeInstance(string connId, string instanceId)
{
if (!_connections.ContainsKey(connId))
{
Console.WriteLine($"[WS] 订阅失败:连接 {connId} 不存在");
Console.WriteLine($"[WS .NET 10] 订阅失败:连接 {connId} 不存在");
return false;
}
// 初始化实例的订阅列表(不存在则创建)
_instanceSubscribers.GetOrAdd(instanceId, _ => new HashSet<string>());
lock (_instanceSubscribers[instanceId])
{
_instanceSubscribers[instanceId].Add(connId);
}
Console.WriteLine($"[WS] 连接 {connId} 订阅实例 {instanceId}");
return true;
}
// 批量发送Agent位置同步消息异步不阻塞游戏循环
public async Task SendAgentPositionBatchAsync(List<AgentPositionSyncMsg> syncMsgs)
{
if(syncMsgs.Count == 0) return;
// 按实例ID分组减少重复发送
var groupedMsgs = syncMsgs.GroupBy(m => m.InstanceId);
foreach (var group in syncMsgs.GroupBy(m => m.InstanceId))
{
if (!_instanceSubscribers.TryGetValue(group.Key, out var subscriberConnIds)) continue;
foreach (var group in groupedMsgs)
// 1. 复用:从池获取消息列表
var msgList = _syncMsgListPool.Get();
try
{
string instanceId = group.Key;
if (!_instanceSubscribers.TryGetValue(instanceId, out var subscriberConnIds))
msgList.AddRange(group);
// 2. 序列化逻辑优化(完全复用池化数组)
int msgBytesLength = 0; // 记录有效长度
byte[]? msgBytes = null;
// 2. 复用:从池获取字节数组(核心使用 _byteArrayPool
var pooledBytes = _byteArrayPool.Get();
bool needReturnPooledBytes = false;
try
{
continue; // 无订阅者,跳过
using (var tempMs = new MemoryStream())
{
MessagePackSerializer.Serialize(tempMs, msgList);
msgBytesLength = (int)tempMs.Position;
if (msgBytesLength <= pooledBytes.Length)
{
tempMs.Position = 0;
tempMs.Read(pooledBytes, 0, msgBytesLength);
msgBytes = pooledBytes; // 直接复用池化数组
needReturnPooledBytes = true;
}
else
{
msgBytes = tempMs.ToArray();
needReturnPooledBytes = false;
}
}
}
finally
{
if (needReturnPooledBytes)
{
_byteArrayPool.Return(pooledBytes);
}
}
// 序列化该实例的所有同步消息
byte[] bytesData = MessagePackSerializer.Serialize(group.ToList());
var wsMsg = new WsMessage
{
Type = WsMsgType.AgentPositionSync,
Data = bytesData
};
// 封装WebSocket消息
var wsMsg = new WsMessage { Type = WsMsgType.AgentPositionSync, Data = msgBytes };
byte[] sendBytes = MessagePackSerializer.Serialize(wsMsg);
// 异步发送给所有房间订阅者(逐个发送,失败则清理连接)
List<string> deadConnIds = new();
// 3. 复用:从池获取失效连接列表
var deadConnIds = _deadConnListPool.Get();
try
{
lock (subscriberConnIds)
{
foreach (var connId in subscriberConnIds)
{
if (_connections.TryGetValue(connId, out var socket))
{
_ = SendToSingleConnAsync(socket, sendBytes, connId, deadConnIds);
// 3. 发送时传递有效长度
if (needReturnPooledBytes)
{
_ = SendToSingleConnAsync(socket, msgBytes, msgBytesLength, connId, deadConnIds);
}
else
{
_ = SendToSingleConnAsync(socket, msgBytes, msgBytes.Length, connId, deadConnIds);
}
}
}
}
@ -108,16 +217,32 @@ namespace XNet.Business
RemoveConnection(deadConnId);
}
}
finally
{
_deadConnListPool.Return(deadConnIds); // 归还,自动清空
}
}
finally
{
_syncMsgListPool.Return(msgList); // 归还,自动清空
}
}
}
// 内部:发送消息给单个连接(异步)
private async Task SendToSingleConnAsync(WebSocket socket, byte[] data, string connId, List<string> deadConnIds)
// 1. 定义带长度的发送方法
private async Task SendToSingleConnAsync(WebSocket socket, byte[] data, int dataLength, string connId, List<string> deadConnIds)
{
try
{
if (socket.State == WebSocketState.Open)
{
await socket.SendAsync(new ArraySegment<byte>(data), WebSocketMessageType.Text, true, CancellationToken.None);
// 只发送有效长度的字节而非全量4KB
await socket.SendAsync(
new ArraySegment<byte>(data, 0, dataLength),
WebSocketMessageType.Binary,
true,
CancellationToken.None
);
}
else
{
@ -131,10 +256,59 @@ namespace XNet.Business
}
}
// 生成唯一连接ID
private string GenerateConnId()
private async Task SendToSingleConnAsync(WebSocket socket, byte[] data, string connId, List<string> deadConnIds)
{
return $"Conn_{Nanoid.Generate()}";// {DateTime.Now.Ticks}_{_random.Next(1000, 9999)}";
try
{
if (socket.State == WebSocketState.Open)
{
await socket.SendAsync(new ArraySegment<byte>(data), WebSocketMessageType.Binary, true, CancellationToken.None);
}
else
{
deadConnIds.Add(connId);
}
}
catch (Exception ex)
{
Console.WriteLine($"[WS .NET 10] 发送失败 {connId}{ex.Message}");
deadConnIds.Add(connId);
}
}
public async ValueTask DisposeAsync()
{
foreach (var (_, socket) in _connections)
{
if (socket.State == WebSocketState.Open)
{
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Server shutdown", CancellationToken.None);
}
socket.Dispose();
}
_connections.Clear();
_instanceSubscribers.Clear();
}
}
// 补充 Vector3 的 MessagePack 序列化(否则会序列化失败)
[MessagePackObject]
public struct Vector3Msg
{
[Key("x")]
public float X { get; set; }
[Key("y")]
public float Y { get; set; }
[Key("z")]
public float Z { get; set; }
public Vector3Msg(float x, float y, float z)
{
X = x;
Y = y;
Z = z;
}
}
}

View File

@ -15,6 +15,7 @@
<PackageReference Include="Microsoft.ClearScript.V8" Version="7.5.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="10.0.1" />
<PackageReference Include="Nanoid" Version="3.1.0" />
</ItemGroup>

View File

@ -18,7 +18,7 @@ namespace XNet.Api.Controllers
[HttpGet]
public void MapInit()
{
_mapAgent!.FindPath("Instance_TeamA_1", new Vector3(-4, 0, -4), new Vector3(5, 0, 3));
//_mapAgent!.FindPath("Instance_TeamA_1", new Vector3(-4, 0, -4), new Vector3(5, 0, 3));
}
}
}