using System.Diagnostics;
using System.Net;
using MoShiro.Protocol;
namespace MoShiro.Measurement;
///
/// 单向数据流(UDP train),监听方(Agent,行的主导方):按声明的码率均匀发包,发出的每个包 (seq, size, t_send) 记在按序号的记录里,
/// Probe 的接收时刻之后按序号合并(没有到达的就是丢失)。发送在专用线程上完成,不占用线程池。
/// 等不到对端的地址(5 秒内没有任何认证过的包)就以 结束。
///
public sealed class TrainListener : BlockBase
{
private static readonly TimeSpan s_endpointWait = TimeSpan.FromSeconds(5);
private readonly RowStore _records;
private long _sent;
public TrainListener(BlockEnv env, BlockSpec spec)
: base(env, spec)
{
double pps = spec.Params.GetNumber("mbps") * 1_000_000 / 8 / Math.Max(1, spec.Int("size"));
_records = new RowStore(spec.Kind.FindStream("pkt")!, Side.Agent, (int)Math.Clamp(pps * spec.Int("duration_s") * 1.25, 1024, 4_000_000));
}
public override long Produced => Interlocked.Read(ref _sent);
public override RowStore? Records(string streamName) => streamName == "pkt" ? _records : null;
public override async Task RunAsync(CancellationToken ct)
{
// Probe 开始前发来的探活包(Data)要被接受,准入策略才能据此学到它当前的源地址(NAT 映射);它们本身没有内容
Env.Udp!.Register(Spec.StreamId, static (in UdpDelivery _) => { });
try
{
DataSession session = await WaitForPeerAsync(ct);
IPEndPoint target = session.UdpEndpoint!;
TimeSpan duration = TimeSpan.FromSeconds(Spec.Int("duration_s"));
var done = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var thread = new Thread(() =>
{
try
{
Send(session, target, duration, ct);
done.SetResult();
}
catch (Exception ex)
{
done.SetException(ex);
}
})
{
IsBackground = true,
Name = "udp-train",
};
thread.Start();
await done.Task;
}
finally
{
Env.Udp.Unregister(Spec.StreamId);
}
}
private async Task WaitForPeerAsync(CancellationToken ct)
{
DateTimeOffset deadline = Env.Time.GetUtcNow() + s_endpointWait;
while (true)
{
DataSession? session = Env.Session();
if (session?.UdpEndpoint is not null)
{
return session;
}
if (Env.Time.GetUtcNow() >= deadline)
{
throw new BlockFaultException("the peer's address is unknown");
}
await Task.Delay(TimeSpan.FromMilliseconds(50), Env.Time, ct);
}
}
private void Send(DataSession session, IPEndPoint target, TimeSpan duration, CancellationToken ct)
{
double packetsPerSecond = Spec.Params.GetNumber("mbps") * 1_000_000 / 8 / Spec.Int("size");
int size = (int)Spec.Int("size");
byte[] packet = new byte[size];
using var pause = new ManualResetEventSlim(false);
uint seq = 0;
long startTicks = Stopwatch.GetTimestamp();
long endTicks = startTicks + (long)(duration.TotalSeconds * Stopwatch.Frequency);
while (!ct.IsCancellationRequested)
{
long now = Stopwatch.GetTimestamp();
if (now >= endTicks)
{
break;
}
// 到目前为止应该发出的包数(均匀):落后了就追,领先了就等;追赶的上限是 50 ms 的量,避免长时间停顿之后的突发
double elapsed = (now - startTicks) / (double)Stopwatch.Frequency;
long due = Math.Min((long)(elapsed * packetsPerSecond) + 1, seq + Math.Max(1, (long)(packetsPerSecond * 0.05)));
if (seq >= due)
{
// 离下一个包还早就让出 CPU(至少 1 ms 的粒度),临近了再自旋,保证发送间隔均匀
if ((seq / packetsPerSecond) - elapsed > 0.002)
{
pause.Wait(1, ct);
}
else
{
Thread.SpinWait(50);
}
continue;
}
for (; seq < due; seq++)
{
long sendUs = Env.Clock.NowUs();
UdpPacket.Write(packet, new UdpHeader(UdpPacketType.Data, session.SessionId, Spec.StreamId, seq, sendUs), session.Key);
Env.Udp!.Send(target, packet);
Span row = [size, sendUs];
_records.SetRow(seq, row);
Interlocked.Exchange(ref _sent, seq + 1L);
}
}
}
}
///
/// 单向数据流,拨号方(Probe,行的跟随方):接收,记录每个序号第一次到达的时刻(按序号索引,热路径不分配);
/// 序号超过本地包速率上限推算出的范围时忽略。阶段结束时按序号范围切成多个列块交给 Agent,每块不超过 的记录。
/// 开始时先发几个探活包,让 Agent 学到我们当前的源地址(NAT 映射)再开始发送。
///
public sealed class TrainDialer : BlockBase
{
private const int PerFragment = 40_000;
private readonly RowStore _received;
private readonly RowStore _step;
private readonly int _tRecv;
private readonly long _maxSeq;
private long _startUs;
private long _endUs;
public TrainDialer(BlockEnv env, BlockSpec spec)
: base(env, spec)
{
double pps = spec.Params.GetNumber("mbps") * 1_000_000 / 8 / Math.Max(1, spec.Int("size"));
long seconds = Math.Max(1, spec.Int("duration_s"));
_maxSeq = Math.Max(1024, Math.Min((long)(pps * seconds * 1.25), env.MaxPacketsPerSecond * seconds * 3 / 2));
_received = new RowStore(spec.Kind.FindStream("pkt")!, Side.Probe, (int)_maxSeq);
_tRecv = _received.ColumnIndex("t_recv");
_step = new RowStore(spec.Kind.FindStream("step")!, Side.Probe, 16);
}
public override async Task RunAsync(CancellationToken ct)
{
UdpMux udp = Env.Udp!;
DataSession session = Env.Session() ?? throw new BlockFaultException("no data session");
_startUs = Env.Clock.NowUs();
udp.Register(Spec.StreamId, OnPacket);
try
{
byte[] hello = new byte[UdpPacket.MinBytes + 20];
IPEndPoint target = PeerEndpoint(Spec.Port);
for (uint seq = 0; seq < 3; seq++)
{
UdpPacket.Write(hello, new UdpHeader(UdpPacketType.Data, session.SessionId, Spec.StreamId, seq, Env.Clock.NowUs()), session.Key);
udp.Send(target, hello);
}
await WaitUntilCancelledAsync(ct);
}
finally
{
_endUs = Env.Clock.NowUs();
udp.Unregister(Spec.StreamId);
}
}
public override int Flush(FlushMode mode, long peerProduced = 0)
{
if (mode != FlushMode.All)
{
return 0;
}
long end = _endUs == 0 ? Env.Clock.NowUs() : _endUs;
long step = _step.Append();
_step.Set(step, _step.ColumnIndex("index"), 0);
_step.Set(step, _step.ColumnIndex("mbps"), (long)Spec.Params.GetNumber("mbps"));
_step.Set(step, _step.ColumnIndex("start_ts"), _startUs);
_step.Set(step, _step.ColumnIndex("end_ts"), end);
long total = Math.Min(peerProduced, _maxSeq);
int fragments = 0;
for (long from = 0; from < total || fragments == 0; from += PerFragment)
{
long to = Math.Min(from + PerFragment, total);
RawStream pkt = _received.Cut(from, to, includeKey: true);
var parameters = new Dictionary(Spec.Params.Values)
{
[FragmentKeys.SeqFrom] = Number(from),
[FragmentKeys.SeqTo] = Number(to),
};
Env.Fragments!.Ship(
"udp_train",
Spec.Scope,
fragments == 0 ? [pkt, _step.Cut(0, 1, includeKey: false)] : [pkt],
parameters,
Env.Clock.UtcMsAt(_startUs),
(int)((end - _startUs) / 1000),
Spec.Port);
fragments++;
if (total == 0)
{
break;
}
}
return fragments;
}
private static System.Text.Json.JsonElement Number(long value) =>
System.Text.Json.JsonDocument.Parse(value.ToString(System.Globalization.CultureInfo.InvariantCulture)).RootElement.Clone();
private void OnPacket(in UdpDelivery delivery)
{
long seq = delivery.Header.Seq;
if (delivery.Header.Type == UdpPacketType.Data && seq < _maxSeq && !_received.IsSet(seq, _tRecv))
{
_received.Set(seq, _tRecv, delivery.ReceiveUs); // 只记第一次到达(重复的包不覆盖)
}
}
}