Das Programm „pingcs“
Bei der Erstellung des Programms sind einige Aspekte zu beachten.
Grundsätzlich ist die Struktur von .Net-Programmen so, das die relevanten, externen, Abhängigkeiten sowie Build-Direktiven in der Projekt-Datei (.csproj) abgelegt sind.
Da wir jedoch nicht mit dem Standard-.Net-Compiler Roslyn sondern bflat kompilieren, ist hier die Angabe einer .csproj-Datei nicht notwendig.
In meiner Projektstruktur ist sie dennoch vorhanden, da dass kompilieren mit bflat genauso möglich ist wie mit Roslyn, da es ja schließlich valider C# Code ist.
Dadurch lässt sich die Anwendung in der IDE (ich verwende Jetbrains Rider) ganz normal editieren und bei Bedarf debuggen und ausführen.
Bei der Erstellung der nativen Anwendung mit bflat müssen jedoch ein paar grundsätzliche Dinge beachtet werden, da sonst der Build nicht funktioniert.
Lustige Features, wie das weglassen der Main()-Methode funktioniert auch nicht, da bflat einen Einstiegspunkt in das Programm kennen muss.
Wir müssen ebenfalls using-Deklarativen angeben, obwohl das in der Roslyn gewohnten Welt nicht mehr nötig ist.
Parameter
Wird das Programm komplett ohne Parameter bzw. mit dem Parameter help ausgeführt, erfolgt die Ausgabe einer Hilfe-Sequenz.
Programmablauf ist easy…
Vom Ablauf her ist das Programm „as simple as it could be“ gestaltet. Wie starten eine Konsolenanwendung, werten die Programm-Argumente aus. Erzeugen daraus einen IcmpRequest, den wir an den IcmpConnector übergeben. Das daraus entstehende IcmpResponse-Objekt verwenden wir für die Ausgabe in der Konsole.readonly Strukturen die auf die Unveränderlichkeit (Immutability) von Delegaten zielen. Delegaten in C# sind Referenztypen, die auf Methoden verweisen. Sie ermöglichen es, Methoden als Parameter zu übergeben, Ereignisse zu implementieren und Callbacks zu definieren. readonly wird festgelegt, dass die dazugehörige Variable über die Laufzeit der Anwendung, nach der Initialisierung, unveränderlich ist.Beispiel für den traditionellen Ansatz
public string PrintHelloWorld(string variable){
return $"Hallo {variable}";
}
Dies ist eine herkömmliche Instanzmethode einer Klasse. Sie erfordert eine Instanz der Klasse, um aufgerufen zu werden (es sei denn, sie wird als static deklariert). Die Methode nimmt einen string als Eingabeparameter und gibt einen modifizierten string zurück.
Vorteile:
- Lesbarkeit: Die Verwendung einer benannten Methode kann die Lesbarkeit und Verständlichkeit des Codes für Menschen verbessern, insbesondere wenn der Methodenname die Aktion klar beschreibt.
- Wiederverwendbarkeit: Als Teil einer Klasse kann diese Methode leicht in verschiedenen Teilen des Programms wiederverwendet werden, insbesondere wenn sie Teil einer gemeinsam genutzten Klasse oder Bibliothek ist.
- Debugging: Traditionelle Methoden sind in der Regel einfacher zu debuggen, da Debugger gut darauf ausgelegt sind, mit dem Aufrufstapel von Methodenaufrufen umzugehen.
Nachteile:
- Flexibilität: Weniger flexibel im Vergleich zu Delegaten oder Lambda-Ausdrücken, wenn es darum geht, die Methode als Parameter zu übergeben oder sie dynamisch zu ändern.
Beispiel für den funktionalen (in meinen Augen moderneren) Ansatz
public static readonly Func PrintHelloWorld = variable => $"Hallo {variable}";
Hier wird ein Func<string, string>-Delegat verwendet, der auf einen Lambda-Ausdruck zeigt. Der Delegat ist static, was bedeutet, dass er auf Klassenebene verfügbar ist, und readonly, was bedeutet, dass seine Zuweisung nach der Initialisierung unveränderlich ist. Der Lambda-Ausdruck nimmt einen string als Eingabe und gibt einen modifizierten string zurück, ähnlich der traditionellen Methode.
Vorteile:
- Flexibilität: Delegaten können als Argumente für Methoden übergeben werden, was sie für bestimmte Entwurfsmuster wie Callbacks, Ereignishandler und LINQ-Operationen sehr nützlich macht.
- Kompaktheit: Lambda-Ausdrücke und Delegaten erlauben eine kompakte Darstellung von Methodenlogik, was den Code oft kürzer und prägnanter machen kann.
- Unveränderlichkeit: Die Verwendung von
readonlymacht den Delegaten sicher gegen unbeabsichtigte Neuzuweisungen (gerade in Multithreaded Umgebungen), was die Stabilität des Codes erhöht.
Nachteile:
- Lesbarkeit: Für Entwickler, die mit Delegaten und Lambda-Ausdrücken weniger vertraut sind, kann dieser Ansatz weniger intuitiv sein.
- Debugging: Das Debuggen von Lambda-Ausdrücken kann etwas umständlicher sein, besonders wenn sie komplex sind oder in tief verschachtelten Aufrufen verwendet werden.
Die Frage, welches Konzept moderner oder zeitgemäßer ist, hängt stark vom Kontext und den spezifischen Anforderungen des Projekts ab. Lambda-Ausdrücke und Delegaten bieten eine große Flexibilität und sind besonders nützlich in funktionalen Programmiermustern, Ereignisbehandlungen und bei der Arbeit mit APIs, die höhere Ordnungsfunktionen erwarten. Sie gelten als modernes Feature von C# und sind in vielen modernen C#-Codebasen weit verbreitet.
Traditionelle Methoden bleiben jedoch für viele Anwendungsfälle relevant, insbesondere wenn Klarheit, Lesbarkeit und die Organisation von Geschäftslogik in wiederverwendbaren Komponenten im Vordergrund stehen.
Anyway, ich kanns lesen, also Delegates!
Die Liste der Parameter
Ruft man das Programm ohne Parameter oder mit dem Parameter help auf, erhält man, wie man das von anderen Konsolen-Tools auch gewohnt ist eine kurze Hilfestellung zur Anwendung.
Hier werden auch die Parameter, ihre Bedeutung und Angabe der Parameter-Variablen beschrieben.
pingcs, a ping alternative in c#, native for various platforms.
written 2024 by L. Lueck
it use bflat for native os compiling and upx for executable compression
Bflat: https://flattened.net/
UPX: https://upx.github.io/
usage of pingcs:
pingcs [IPAddress or HostName or help]
help - displays this help text (same as pingcs without any parameter)
option t [int 1..n] count of pings | if not given, 3 ping per default
option c [dec [0,0 | 0.0] .. [n,n | n.n]] default wait time in seconds between ping commands, default is 0
option a [int 0..n] time in millisecond how long the ping tries to connect to the endpoint, default is 1000
option v [byte 0..255] ttl for ping, defines the rfc-792 ttl
option x [int 4 or 6] sets the address type to be used manually 4 is IpV4 6 is IpV6 not given is best default
--------------------
simple usage: pingcs 1.1.1.1 (pings the cloudflare dns three times)
Ein paar Worte zu Enums
Enums in C# sind pain in the ass. Komplett unflexibel. Und wie ich lernen musste, wird die key –> value Zuordnung über Reflection gelöst.
Das heißt
enum YesNoMaybe
{
Yes = 1,
No = 2,
Maybe = 3
}
bedeutet, dass die Auflösung vom Wert (Yes, No, Maybe) zum entsprechenden Schlüssel (1, 2, 3) kein Problem darstellt, die Umwandlung vom Schlüssel zum jeweiligen Wert über Reflektion gelöst wird.
Dies wird zur Compile-Zeit gelöst und hat zur Folge, dass bspw. bflat mit dem Schalter –no-reflection die Zuordnung Key zu Wert nicht mehr auflösen kann und statt dem Wert („Yes“, „No“ oder „Maybe“) den Schlüssel 1, 2 oder 3 zurückgibt als Fallback.
Ich könnte jetzt den Schalter –no-reflection weglassen, dann wird mir allerdings die kompilierte Executable zu groß.
Watt nu?
Naja, ganz einfach, ich schmeiße die internen enums über Board und schreibe einfach selber welche.
Ausgehend von der Basisklasse „Enumeration“
internal abstract record Enumeration(string name)
{
private string Name { get; } = name;
public override string ToString() => Name;
}
definieren wir für jeden Enumeration-Typ eine abgeleitete Klasse. Bspw. IpAddressFamilyEnum
internal record IpAddressFamilyEnum(string name) : Enumeration(name)
{
public static readonly IpAddressFamilyEnum IpV4 = new(nameof(IpV4));
public static readonly IpAddressFamilyEnum IpV6 = new(nameof(IpV6));
public override string ToString()
{
return base.ToString();
}
}
Oder MessageTypeV4Enum, bei dem auch gezeigt wird, wie von einem Key der entsprechende Wert ermittelt wird. Über die entsprechende (überschriebene) ToString-Methode können wir dann auch den Bezeichner ausgeben.
internal record MessageTypeV4Enum(string name) : Enumeration(name)
{
public static MessageTypeV4Enum EchoReply = new(nameof(EchoReply));
public static MessageTypeV4Enum DestinationUnreachable = new(nameof(DestinationUnreachable));
public static MessageTypeV4Enum SourceQuench = new(nameof(SourceQuench));
public static MessageTypeV4Enum Redirect = new(nameof(Redirect));
public static MessageTypeV4Enum EchoRequest = new(nameof(EchoRequest));
public static MessageTypeV4Enum TimeExceeded = new(nameof(TimeExceeded));
public static MessageTypeV4Enum ParameterProblem = new(nameof(ParameterProblem));
public static MessageTypeV4Enum Timestamp = new(nameof(Timestamp));
public static MessageTypeV4Enum TimestampReply = new(nameof(TimestampReply));
public static MessageTypeV4Enum InformationRequest = new(nameof(InformationRequest));
public static MessageTypeV4Enum InformationReply = new(nameof(InformationReply));
public static MessageTypeV4Enum GetValueById(int id)
{
return id switch
{
0 => EchoReply,
3 => DestinationUnreachable,
4 => SourceQuench,
5 => Redirect,
8 => EchoRequest,
11 => TimeExceeded,
12 => ParameterProblem,
13 => Timestamp,
14 => TimestampReply,
15 => InformationRequest,
16 => InformationReply,
_ => throw new ArgumentOutOfRangeException(nameof(id), id, null)
};
}
public override string ToString()
{
return base.ToString();
}
}
Main-Methode
Hier erfolgt die Auswertung der übergebenen Parameter und die Übergabe der Parameter an das IcmpRequest-Objekt. Wir verwenden an dieser Stelle zwar Sprachfeatures von .NET 8, können aber auf eine statische Main-Methode nicht verzichten, da wir ja keinen Roslyn-Compiler nutzen, der letztendlich die Main-Methode bei der Kompilierung hinzufügt.
bflat benötigt zwingend einen Einstiegspunkt in das Programm über eine Main-Methode mit der Entgegennahme der Argumente als string[].
Nach erfolgreichem Ping wird, entsprechend der Address-Family (IPv4 oder IPv6) die Zeichenkette der Konsolenausgabe aufbereitet und ausgegeben.
Die Ping-Methode wird in einer Schleife (ausgehend von der konfigurierten Anzahl an Durchläufen) ausgeführt.
Die einzige nicht Delegates-Methode ist die Main-Methode.
Warum?
Sowohl bflat als auch Roslyn benötigen als Einstiegspunkt für ein Programm eine klassische void Main(string[] args){} oder Task Main(string[] args){} Methode.
Action<string[]> Main = args => {} ist im klassischen Sinne keine Methode, sondern eine Variable Main, die eine Funktion (Lambda) enthält. Variable != Methode.
Nuja!
internal abstract class PingC
{
static void Main(string[] args)
{
var argumentParameter = StaticUtils.ResolveArgs(args);
if (argumentParameter is null)
{
WriteLine(StaticUtils.WriteHelpText());
Environment.Exit(0);
}
var request = new IcmpRequest(argumentParameter.Ttl, argumentParameter.IpOrHostName,
argumentParameter.TimeoutInMs, true, argumentParameter.GivenAddressType);
WriteLine($"pinging {argumentParameter.IpOrHostName} ({request.RealTargetHostName}) from {request.IpEndPointSource?.Address.ToString() ?? "---"} for {argumentParameter.Count} times with {argumentParameter.TimeBetweenPing}s delay");
for (var i = 1; i <= argumentParameter.Count; i++)
{
var response = IcmpConnector.Ping(request);
switch (response.AddressFamily)
{
case var value when value == IpAddressFamilyEnum.IpV4:
var respV4 = (IcmpResponseV4)response;
WriteLine(
$"[{i}] :: {respV4.TypeV4Enum} from {respV4.Origin?.Address.ToString() ?? "---"} to {respV4.FromIp?.Address.ToString() ?? "---"} in {respV4.RoundTripTime} ms with TTL {respV4.Ttl}");
break;
case var value when value == IpAddressFamilyEnum.IpV6:
var respV6 = (IcmpResponseV6)response;
WriteLine(
$"[{i}] :: {respV6.TypeV6Enum} from {respV6.Origin?.Address.ToString() ?? "---"} to {respV6.FromIp?.Address.ToString() ?? "---"} in {respV6.RoundTripTime} ms");
break;
default:
throw new ArgumentException("invalid ip address family type", nameof(response.AddressFamily));
}
if (argumentParameter.TimeBetweenPing > 0)
Thread.Sleep((int)(argumentParameter.TimeBetweenPing * 1000));
}
Environment.Exit(0);
}
}
IcmpRequest
Diese Klasse dient in erster Linie dazu, das Request-Objekt aufzubauen, den Netzwerkpayload und die erforderliche Checksumme bereitzustellen, sowie im Vorfeld die Namensauflösung vorzunehmen und die korrekte Address-Family auszuhandeln bzw. zu validieren ob die vorgegebene Address-Family für die Adresse möglich ist.
Über den Konstruktor der Klasse werden die übergebenen Variablen verarbeitet und weitere Felder gefüllt, die dann bei der Ausführung von Ping benötigt werden.
Die Felder können nur innerhalb der Klasse verändert jedoch von außerhalb der Klasse gelesen werden.
Auf die komplette Beschreibung jeder einzelnen Funktion verzichte ich an dieser Stelle. Jeder, der sich dafür interessiert kann sich den Source-Code beispielsweise für diese Klasse auf Gitlab ansehen.
Eine Sache vielleicht doch.
Falls irgendjemand mal die Checksummenberechnung für ICMP-Frames in C# benötigt: Here it is:
private readonly Func _getChecksum = (messageSize, payload) =>
{
var packetSize = messageSize + 8;
var checksum = Enumerable.Range(0, packetSize / 2)
.Select(index => BitConverter.ToUInt16(payload, index * 2))
.Aggregate(0u, (sum, value) => sum + value);
checksum = (checksum >> 16) + (checksum & 0xffff);
checksum += (checksum >> 16);
return (ushort) ~checksum;
};
IcmpConnector
Im IcmpConnector wird die eigentliche Ping-Anfrage realisiert und die Laufzeit zwischen dem senden des Requests und dem empfangen des Response ermittelt. Das Ergebnis wird als IcmpResponse-Objekt an die aufrufende Methode zurückgegeben werden.
Bei der Verwendung von Pingcs wird bewusst auf async / await – Funktionalität, gerade bei der Verwendung des Netzwerkstacks verzichtet.
Warum? Na, wenn ich in einer Konsolen-Awendung mit Enter starte, erwarte ich ein Ergebnis oder Timeout bzw. eine Fehlermeldung diesbezüglich. Da muss ich keine Tasks auslagern bzw. non-blocking gestalten, da die Konsolen-Anwendung eh der einzige Prozess ist und sich max. über die Timeout-Zeit selbst blockieren kann.
IcmpResponse
In dieser Klasse wird das Ergebnis eines Ping an die aufrufende Methode zurückgegeben. IcmpResponse ist dabei der zurückgegebene Basistyp. Konkret wird ein IcmpResponseV4 oder ein IcmpResponseV6 zurückgegeben und entsprechend diesem Subtyp bei der Ausgabe auf der Konsole berücksichtigt.
Der Rest ist codetechnischer Firlefanz. Wie bereits weiter oben erwähnt kann man sich die Sourcen zu pingcs gerne auf meinem Gitlab-Projekt ansehen. Der Link dahin ist hier.