IPScanner mit Threading

    • VB.NET

    Es gibt 4 Antworten in diesem Thema. Der letzte Beitrag () ist von Lukas.

      IPScanner mit Threading

      Hi!

      Ich beziehe mich hier auf die IPv4 - Notation.
      Beispiel1: 192.168.1.3
      Beispiel2: 192.168.1.0/28
      Beispiel3: 192.168.1.3/28 (bisserl sinnlos)
      Beispiel4: 192.168.1.3/32 (bisserl umständlich)

      Beispiel1 addressiert genau eine IP. Eine IP besteht aus 4 Bytes, die bei IPv4-Notation dezimal geschrieben werden, durch "." segmentiert
      Beispiel2 definiert zusätzlich eine Subnetzmaske der Länge 28. Das heist: Von der IP sind nur die ersten 28 Bits relevant, die letzten 4 Bits können frei gewählt werden. Damit ist ein Bereich von IPs addressiert.
      Deshalb ist in Beispiel3 das ".3" bisserl sinnlos, denn das spezifiziert die 2 letzten Bits, die ja gar nicht relevant sind.
      Beispiel4 ist umständlich, denn wenn alle 32 Bits relevant sind, kann man die NetzMasken-Angabe auch weglassen (s. Beispiel1).

      Für einen IPScanner sind 2 Probleme zu lösen:
      1. Aus obiger Notation müssen alle im Bereich liegenden IPs ermittelt werden.
      2. Die müssen angepingt werden, und zwar mit Threading, denn ein erfolgloser Ping dauert lange - nämlich bis zum Timeout.

      Alle IPs im Bereich ermitteln

      IP konvertieren
      Kernstück sind 2 Konverter-Methoden, die Integer und IPv4-Notation ineinander überführen können:

      VB.NET-Quellcode

      1. ''' <summary>gibt die Integer-Darstellung einer IP zurück</summary>
      2. Public Shared Function IP2Int(ByVal ip As String) As Int32
      3. 'split zerteilt ip in 4 strings, die je eine Zahl darstellen
      4. 'Byte.Parse wandelt je ein Segment in ein Byte um
      5. 'Select sammelt diese Bytes in ein IEnumerable(Of Bytes)
      6. 'ToArray macht daraus ein Byte-Array
      7. Dim bytes = (From s In ip.Split("."c) Select Byte.Parse(s)).ToArray
      8. Array.Reverse(bytes) 'dreht die Reihenfolge um
      9. Return BitConverter.ToInt32(bytes, 0)
      10. End Function
      11. ''' <summary>bildet aussm Integer die String-Darstellung einer IP</summary>
      12. Public Shared Function Int2IP(ByVal n As Int32) As String
      13. Dim bytes = BitConverter.GetBytes(n)
      14. Array.Reverse(bytes) 'reihenfolge umdrehen
      15. 'Byte-Array in ein String-Array konvertieren
      16. Dim strings = bytes.Select(Function(b) b.ToString)
      17. 'Strings verketten und returnen
      18. Return String.Join(".", strings)
      19. End Function

      Damit kann man eine IP nach Integer konvertieren, und zum "scannen" den Integer dann einfach hochzählen.
      Müssen wir noch die Untergrenze des Bereiches bestimmen, sowie die Länge.

      Länge des IP-Bereichs und untere Grenze
      Die Länge des Bereiches ist 2^AnzahlFreierBits. Also bei einer NetMaskLength von 28 ergäbe sich 2^4 = 16.
      ZweierPotenzen lassen sich sehr performant als BitShift-coden:

      VB.NET-Quellcode

      1. Length = 1 << (32 - NetMaskLength)

      Für die Untergrenze muß man halt die Subnetzmaske auf die IP anwenden ("maskieren"). Bekannt ist aber nur NetMaskLength - wir brauchen aber ein Bitmuster als Maske. Dieses ist sehr einfach gestrickt, es ist nämlich ein Integer, dessen höherwertige Bits gesetzt sind, bis zur angegebenen Subnetzmasken-Länge (Beispiel2: die 28 höherwertigen Bits sind gesetzt, die restlichen 4 Bits nicht). Wieder gibt es eine überaus performante Bit-Operation:

      VB.NET-Quellcode

      1. _NetBitMask = Not (Length - 1)
      Erstaunlich, nicht? Aber vergegenwärtigt man sich schnell am Beispiel, zB eine Length von 16: Length - 1 ergibt 15, also die 4 niederen Bits gesetzt. Davon die Inversion ist genau unsere NetzMaske mit den 28 gesetzten höheren Bits :)

      Die Untergrenze erhält man dann, indem man die IP-Addresse mit der _NetBitMask maskiert

      VB.NET-Quellcode

      1. LBound = Address And _NetBitMask


      Alternative Notationen der SubnetMask
      Es ist vlt. etwas irreführend, die /28 aus Beispiel2 als SubnetzMaske zu bezeichnen, denn es ist ja nur die Länge der eigentlichen Bit-Maske. Aber es ist die kürzeste Definition - vgl:
      11111111.11111111.11111111.11110000 (Binäre Darstellung)
      255.255.255.240 (Dezimale Darstellung)
      Die dezimale Darstellung ist ebenfalls sehr häufig

      erneuerte Betrachtung des IP-Addressraumes
      Erst wenn man die bitweise Betrachtung einer IP verstanden hat, erschließt sich auch das listige Prinzip, wie man eine IP-Vergabe hierarchisch strukturieren kann:
      Einem Büro kann etwa folgende 16 IPs zugeteilt bekommen:
      192.168.1.0/28 - das geht von 192.168.1.0 bis 192.168.1.15.
      Das nächste Büro kriegt die IPs:
      192.168.1.16/28 - das geht von 192.168.1.16 bis 192.168.1.31.
      Die übergeordnete Abteilung bekommt logisch:
      192.168.1.0/27 - das geht von 192.168.1.0 bis 192.168.1.31...
      ...Und umfasst damit diese beiden Büros.
      Geschnackelt? Da die Subnetzmaske um ein Bit verkleinert wurde, hat sich der frei wählbare Bereich verdoppelt.
      Dieses hierarchische Prinzip wird genutzt einerseits um verschachtelte Addressräume zu definieren, aber auch die Server hangeln sich iwie bitweise die IP längs, um ein Datenpaket gewissermaßen "in Richtung" Addressat weiterzuleiten, an einen dem Addressaten näherliegenden Server.

      Was es nicht geben kann, wäre ein Bereich etwa von 192.168.1.3 bis 192.168.1.18 (wären ja auch 16 IPs). Denn aus der Bitweisen Betrachtung ergibt sich, dass Bereichsgrenzen der binären Logik folgen: Also bei 32 als Netmask ist jede Zahl Bereichsgrenze, bei 31 jede 2. Zahl, bei 30 jede 4., bei 29 jede 8. usw..
      Daher kann 192.168.1.3 nicht die Untergrenze eines Bereichs der Länge 16 sein, denn die 3 liegt nicht auf der 16er Schrittweite (vgl: 192.168.1.0/28, 192.168.1.16/28)

      Ich hab das ganze Zeug in eine Klasse "IPData" gekapselt, wo man alle diese Informationen bequem abrufen kann:
      Spoiler anzeigen

      VB.NET-Quellcode

      1. Public Class IPData
      2. ''' <summary>Länge der SubnetzMaske</summary>
      3. Public NetMaskLength As Int32 = Nothing
      4. Public Address As Int32 = Nothing
      5. ''' <summary>SubnetzMaske in segmentierter Darstellung</summary>
      6. Public ReadOnly Property NetBitMask As String
      7. Get
      8. Return Int2IP(_NetBitMask)
      9. End Get
      10. End Property
      11. Private ReadOnly Property _NetBitMask As Int32
      12. Get
      13. Return Not Length - 1
      14. End Get
      15. End Property
      16. ''' <summary>IP ohne Masken-Angabe</summary>
      17. Public Property Name As String
      18. Get
      19. Return Int2IP(Address)
      20. End Get
      21. Set(ByVal value As String)
      22. Address = IP2Int(value)
      23. End Set
      24. End Property
      25. ''' <summary>IP mit Maske, zB 168.230.16.3/24</summary>
      26. Public Property FullName As String
      27. Get
      28. Return Name & "/" & NetMaskLength
      29. End Get
      30. Set(ByVal value As String)
      31. Dim splits = value.Split("/"c)
      32. Name = (splits(0))
      33. NetMaskLength = If(splits.Length = 1, 32, Int32.Parse(splits(1)))
      34. End Set
      35. End Property
      36. ''' <summary>Untergrenze der SubnetMaske</summary>
      37. Public ReadOnly Property LBound As String
      38. Get
      39. Return Int2IP(LBoundAddress)
      40. End Get
      41. End Property
      42. ''' <summary>Obergrenze der SubnetMaske</summary>
      43. Public ReadOnly Property UBound As String
      44. Get
      45. Return Int2IP(LBoundAddress + Length - 1)
      46. End Get
      47. End Property
      48. ''' <summary>Untergrenze der SubnetMaske in Integer-Darstellung</summary>
      49. Public ReadOnly Property LBoundAddress As Integer
      50. Get
      51. Return Address And (Not Length - 1)
      52. End Get
      53. End Property
      54. ''' <summary>Größe des durch die SubnetMaske definierten Addressraumes</summary>
      55. Public ReadOnly Property Length As Integer
      56. Get
      57. Return 1 << (32 - NetMaskLength)
      58. End Get
      59. End Property
      60. ''' <summary>gibt die Integer-Darstellung einer IP zurück</summary>
      61. Public Shared Function IP2Int(ByVal ip As String) As Int32
      62. 'split zerteilt ip in 4 strings, die je eine Zahl darstellen
      63. 'Byte.Parse wandelt je ein Segment in ein Byte um
      64. 'Select sammelt diese Bytes in ein IEnumerable(Of Bytes)
      65. 'ToArray macht daraus ein Byte-Array
      66. Dim bytes = (From s In ip.Split("."c) Select Byte.Parse(s)).ToArray
      67. Array.Reverse(bytes) 'dreht die Reihenfolge um
      68. Return BitConverter.ToInt32(bytes, 0)
      69. End Function
      70. ''' <summary>bildet aussm Integer die String-Darstellung einer IP</summary>
      71. Public Shared Function Int2IP(ByVal n As Int32) As String
      72. Dim bytes = BitConverter.GetBytes(n)
      73. Array.Reverse(bytes) 'reihenfolge umdrehen
      74. 'Byte-Array in ein String-Array konvertieren
      75. Dim strings = bytes.Select(Function(b) b.ToString)
      76. 'Strings verketten und returnen
      77. Return String.Join(".", strings)
      78. End Function
      79. End Class

      Threading
      Unabdingbar.
      Will man einen Bereich von 256 IPs durchpingen, und gesteht jedem Ping einen Timeout von 2s zu - hmm - ich glaub, etwas über 7min, richtig?
      Verwendet man 256 Threads sinds nur 2s :)
      Man muß aber auch die Anzahl der Threads begrenzen, die belegen nämlich jeder über 1MB Speicher. Da habichne Weile dran rumprobiert, um hinzukriegen, wie mehrere Thread-Methoden sich parallel immer die nächste IP abholen und verarbeiten. Herausgekommen ist etwas mit 2 Delegaten (addSuccess und addFail) und der anonymen Methode consume():

      VB.NET-Quellcode

      1. Private Sub btStart_Click(ByVal sender As Object, ByVal e As EventArgs) Handles btStart.Click
      2. UpdateGui(False)
      3. Array.ForEach({lstSuccess, lstFail}, Sub(lst) lst.Items.Clear())
      4. Dim ipData = New IPData With {.FullName = txtIP.Text}
      5. Dim threadCount = Integer.Parse(txtThreadCount.Text)
      6. Dim nIP = ipData.LBoundAddress - 1
      7. Dim ubound = nIP + ipData.Length
      8. Dim addSuccess = DirectCast(AddressOf lstSuccess.Items.Add, Action(Of String))
      9. Dim addFail = DirectCast(AddressOf lstFail.Items.Add, Action(Of String))
      10. Dim consume As ThreadStart = _
      11. Sub()
      12. Using _Ping = New Ping
      13. Do
      14. Dim exceedsIpRange As Boolean
      15. 'exklusiv für diesen Thread nIP inkrementieren und gegen ubound checken
      16. SyncLock Me
      17. nIP += 1
      18. exceedsIpRange = nIP > ubound
      19. End SyncLock
      20. If exceedsIpRange Then 'Thread läuft aus
      21. 'der letzte auslaufende Thread reEnabled das Gui
      22. If nIP = ubound + threadCount Then Me.BeginInvoke(UpdateGui, True)
      23. Exit Do 'auf jd. Fall aussteigen - Thread auslaufen lassen
      24. End If
      25. 'String-Darstellung von nIP pingen, Result-spezifisch adden
      26. Dim sIP = ipData.Int2IP(nIP)
      27. If _Ping.Send(sIP, 2000).Status = IPStatus.Success Then
      28. Me.BeginInvoke(addSuccess, sIP)
      29. Else
      30. Me.BeginInvoke(addFail, sIP)
      31. End If
      32. Loop
      33. End Using
      34. End Sub
      35. 'viele Threads starten
      36. For i = 0 To threadCount - 1
      37. Call New Thread(consume) With {.Priority = ThreadPriority.Lowest}.Start()
      38. Next
      39. End Sub

      Die Delegaten sind erforderlich, um aus dem Nebenthread das Adden von ListboxItems an den GuiThread delegieren zu können. Die anonyme Methode macht die ganze Arbeit: instanziert ein Ping-Objekt, und holt sich im Do-Loop die jeweils aktuelle ID und pingt die mittm Ping.
      Listig die Reaktivierung des Guis:

      VB.NET-Quellcode

      1. SyncLock Me
      2. nIP += 1
      3. exceedsIpRange = nIP > ubound
      4. End SyncLock
      5. If exceedsIpRange Then 'Thread läuft aus
      6. 'der letzte auslaufende Thread reEnabled das Gui
      7. If nIP = ubound + threadCount Then Me.BeginInvoke(UpdateGui, True)
      8. Exit Do 'auf jd. Fall aussteigen - Thread auslaufen lassen
      9. End If
      Also er incrementiert auf jeden Fall die aktuell zu testende nIP, auch wenn die den ubound schon überschritten hat. Ist die Überschreitung nun genauso hoch, wie threadCount vorgesehen waren, dann "weiß" der Thread, dass er der letzte ist, der ausläuft, und reaktiviert daher das Gui.

      Aber das steht zunächstmal nur in der anonymen Methode, also ist nur deklariert, wird von sich aus nicht ausgeführt.
      Erst ganz am Schluß werden Threads erzeugt, die diese Methode dann auch abfahren - und zwar mehrfach parallel.
      Dateien
      • IPScanner.zip

        (16,83 kB, 377 mal heruntergeladen, zuletzt: )

      Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „ErfinderDesRades“ ()

      Sorry - das Threading war Blödsinn

      Die Ping-Klasse verfügt über eine Methode .SendAsync, und solche Async-Methoden sind "hartem Threading" mit diesen teuren Thread-Objekten unbedingt vorzuziehen.
      Eine Async-Methode schafft es irgendwie, aufgerufen zu werden, ohne den aktuellen Thread zu blockieren. Und das eben ohne diese teuren Thread-Objekte.
      Ursprünglich hatte ich Ping.SendAsync verworfen, weil ein Ping nur einen Aufruf davon verträgt, und beim zweiten einen "IsBusy"-Fehler wirft.
      Aber zwischenzeitlich denkichmir "ist doch egal: mw. auch bei einem IPBereich von 16384 Addressen - erstellenwa halt ebenso viele Pings und gut is.". Und so liegt auch einen Bereich von 16384 Ips zu checken noch im Sekundenbereich :).

      Hier also der wahre IPScanner - ist auch viel leichter verständlich:

      VB.NET-Quellcode

      1. Private Counter As Integer
      2. Private Sub btStart_Click(ByVal sender As Object, ByVal e As EventArgs) Handles btStart.Click
      3. UpdateGui(False)
      4. Array.ForEach({lstSuccess, lstFail}, Sub(lst) lst.Items.Clear())
      5. Dim ipData = New IPData With {.FullName = txtIP.Text}
      6. Counter = ipData.Length
      7. For i = ipData.LBoundAddress To ipData.LBoundAddress + ipData.Length
      8. Dim png = New Ping
      9. AddHandler png.PingCompleted, AddressOf Ping_PingCompleted
      10. Dim sIP = ipData.Int2IP(i)
      11. png.SendAsync(sIP, 2000, sIP)
      12. Next
      13. End Sub
      14. Private Sub Ping_PingCompleted(ByVal sender As Object, ByVal e As PingCompletedEventArgs)
      15. If e.Reply.Status = IPStatus.Success Then
      16. lstSuccess.Items.Add(e.Reply.Address.ToString)
      17. Else
      18. 'bei Fail ist die Addresse gelöscht, also stattdessen den UserState anzeigen
      19. lstFail.Items.Add(e.UserState.ToString)
      20. End If
      21. DirectCast(sender, Ping).Dispose() ' wichtig: Resourcebereinigung
      22. Counter -= 1
      23. If Counter = 0 Then UpdateGui(True)
      24. End Sub
      Dateien
      • IPScanner05.zip

        (21,7 kB, 351 mal heruntergeladen, zuletzt: )

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „ErfinderDesRades“ ()

      Hey ErfinderdesRates xD

      Für reinen Massenping ist ein Thread pro Ping definitiv quatsch.
      Möchte man aber mehr, als nur einen Ping durchführen, und man möchte nur mithilfe des Pings prüfen, ob es überhaupt Sinn macht, weiter zu machen, dann kann das Erzeugen von Threads schon sinnvoll sein.

      Ansonsten ist Variante 2 definitiv schöner :)


      Btw.:
      Ich verwende immer folgende Funktion für IpToInt:

      VB.NET-Quellcode

      1. Shared Function IPAddressToNumber(ByVal address As String) As Int32
      2. Return System.Net.IPAddress.NetworkToHostOrder(BitConverter.ToInt32(System.Net.IPAddress.Parse(address).GetAddressBytes(), 0))
      3. End Function


      Ansonsten schön beschrieben :)
      Das ist meine Signatur und sie wird wunderbar sein!
      Hallo,

      erstmal Positives: Dein Programm funktioniert gut und schnell (schneller als manche andere) ABER wenn man das Programm per Task Manager oder VisualStudio (Stopp-Button) killt gibts 'nen Bluescreen. Ich konnte leider nicht lesen was das für ein Fehler war, da er zu schnell rebootet hat.
      Mit freundlichen Grüßen,
      Thunderbolt
      Könnte/dürfte hiermit im Zusammenhang stehen: connect.microsoft.com/VisualSt…for-pingreply-causes-bsod
      „Was daraus gefolgert werden kann ist, dass jeder intelligentere User sein Geld lieber für Bier ausgibt, um einen schönen Rausch zu haben, und nicht dieses Ranzprodukt.“

      -Auszug aus einer Unterhaltung über das iPhone und dessen Vermarktung.