TcpKommunikation + Networkstream

    • VB.NET
    • .NET 4.0

    Es gibt 6 Antworten in diesem Thema. Der letzte Beitrag () ist von Radinator.

      TcpKommunikation + Networkstream

      Vorbemerkung
      Tcp-Kommunikation basiert auf Streams. Daher ist Kenntnis der grundlegenden Stream-Konzepte in Dot.Net unabdingbare Vorraussetzung zum Verständnis dieses Tuts.

      Alles klar (oder was?)
      Wenn man sich die Klassen TcpListener, TcpClient und NetworkStream im ObjectBrowser so anschaut, denkt man eiglich, die Server-Client-Kommunikation per Internet sei geritzt:
      Im Server wartet ein TcpListener auf Verbindungs-Anfragen, die im Client von TcpClient-Objekten an den Server geschickt werden. Wenn der TcpListener eine Anfrage akzeptiert, so erstellt er ebenfalls ein TcpClient-Objekt, bereits verbunden mit der Gegenstelle, und beide können über ihren NetworkStream problemlos Daten austauschen, denn zum Daten-Transfer sind Streams ja da.

      Problem
      Leider ist der stream-basierte Daten-Transfer eben nicht so problemlos, wie eingangs gedacht, denn ein NetworkStream ist ein endloser Stream.
      Viele Lese/Schreib-Vorgänge erfordern aber endliche Streams. ZB ein schreibender GZipStream behält immer einen ungeschriebenen Rest in seinem Eingangs-Puffer, und schreibt den erst weg, wenn weiterer Content kommt und den Puffer voll füllt. So kann man natürlich keine responsive Kommunikation aufbauen, wenn die erste Nachricht erst abgeschlossen wird, wenn die zweite bereits angefangen wurde. Folglich muss man für jeden komprimierten Content einen eigenen GZipStream einsetzen, und den disposen, denn nur dann schreibt er auch den Rest-Puffer weg.
      Lesen/Entpacken aus endlosem Stream ist sogar noch problematischer: nämlich er füllt sich seinen Puffer immer komplett voll, entpackt aber nur so viel, wie vom schreibenden GZipStream reingemacht wurde. Das bedeutet: Bei aufeinander folgenden Nachrichten verschwindet der Anfang der zweiten für immer im Puffer-Rest des lesenden GZipStreams - wird nie entpackt :thumbdown:
      (Ich hab das nur mit GZipStream ausgiebig getestet, denke aber, mit anderen Kompressions-Streams, und auch mit Crypto-Streams aller Art wirds nicht anders sein.)

      StreamContainer
      Meine Lösung besteht darin, dass ich einen Stream gebastelt habe, den man zwischen NetworkStream und GZipStream schaltet, und der schreibend das Ende eines Contents markieren kann, und lesend ab der Markierung einfach keine weiteren Daten mehr herausgibt. Ein StreamContainer tut also so, als sei er ein endlicher Stream. :P
      Mit einem Extra-Befehl, StreamContainer.NextContent(), markiert man beim Schreiben das Ende eines Contents, und beim Lesen eröffnet man damit auch die Daten des nächsten Contents.
      Also zwischengeschaltet macht StreamContainer aus dem unendlichen NetworkStream eine Aneinanderreihung endlicher Streams, und das eingangs beschriebene Problem ist ganz allgemein gelöst, für StreamReader, Zip- oder Crypto-Streams, und auch für weitere Lese/Schreib-Vorgänge, deren diesbezügliche Problematik mir nichtmal bekannt ist (Serialisierung?, Laden von XmlDocumenten?, Laden typisierter Datasets?).

      Das war der Knackpunkt an meim Tcp-Server-Client-TestProjekt, alles andere ist im Grunde Standard von Stange. :D
      Aber auch der Standard ist nicht ganz ohne, und vlt. wert, sich mit zu befassen.

      Server-Client-Netz
      TCP ist ja verbindungsorientiert, und eine Verbindung hat zwei absolut gleiche Kommunikations-Endpunkte, die Daten miteinander austauschen. (Ich habe dafür die Klasse TcpEndPoint geproggt - mehr dazu später).
      Beim Server-Client-Netz hat man nun viele Clients, die sich mit einem Server verbinden. Und jeder Client hat seinen Kommunikations-Endpunkt, und der Server hält logischerweise eine Liste davon.
      Weiters braucht man ein Übertragungs-Protokoll, denn nur Daten austauschen reicht nicht - man muss auch mitteilen, was die Gegenstelle damit machen soll. Also bei mir besteht ein "Content" immer aus einem CommandCode (2 Bytes), gefolgt von Daten. Und das Protokoll definiert nicht nur eine Liste gültiger Commands, sondern zwei: nämlich ServerCommands, die der Client dem Server schickt, und ClientCommands, die der Server den Clients. Ich nenne das ein bidirektionales Befehls-Protokoll, und ist als Enum super-einfach formuliert:

      VB.NET-Quellcode

      1. Public Enum ClientCommand As UInt16 : None : Text : Image : File : Number : End Enum
      2. Public Enum ServerCommand As UInt16 : None : TextBroadcast : ImageBroadcast : ChangeCompressedFile : NumberInvert : TextInvert : ImageInvert : Test1 : Test2 : End Enum
      Wie man sieht versteht der Server mehr Commands als der Client. Liegt hier im Sample daran, dass er zT. verschiedene Anfragen mit demselben ClientCommand beantwortet.
      Etwa bei TextBroadcast verteilt er mit ClientCommand.Text den Text an alle Clients. Aber auch auf TextInvert antwortet er ein ClientCommand.Text, nur invertiert er jetzt den Text, und schickt ihn auch nicht an alle, sondern nur retour an genau den Client, der angefragt hat.
      Was da nun im einzelnen (für Unsinn) gemacht wird ist ja egal. Wichtig ist nur das Prinzip des bidirektionalen Befehls-Protokolls zu zeigen, und proof-of-concept der verschiedenen Transfer-Fähigkeiten: Text, Zahlen, Bilder, Files sowie BroadCast, PeerToPeer, Compression, Encryption - letzteres hab ich aber aus Faulheit weggelassen.
      Und dass die Kommunikation unverzüglich erfolgt, also eine Anfrage wird direkt beantwortet, ohne dass Teile der Daten erstmal in Puffern hängenbleiben, oder gar für immer darin verschwinden. :D

      Aufbau der Solution
      Auch wesentlich ist ein durchdachter Aufbau der Solution, die besteht hier nämlich aus 4 Projekten:
      ServerApp, ClientApp, TcpCommon-Bibliothek, Helpers-Bibliothek.
      So kann man Code richtig einordnen, je nachdem, wo er gebraucht wird: Helpers werden von allen gebraucht, TcpCommon wird von Server- und Client-App gebraucht, und letztere beiden brauchen nur sich selber. (Die Helpers-Bibliothek ist übrigens ein Winz-Auszug aus meiner sehr großen Helpers-Bibliothek, in der sich aller möglicher Code findet, der nicht nur hier sondern in zig anderen Projekten auch gebraucht wird.)
      Also wer sich mit Tcp beschäftigen will, der sollte unbedingt Projekte zu einer Solution zusammenfassen können, und auch Verweise und Imports richtig setzen, damit wiederverwendbarer Code auch wiederverwendet wird.
      Siehe dazu: Video: HelperProjekt einbinden

      Und zum testen muss man dann 2 Projekte gleichzeitig als Startprojekt angeben, ich zeig mal Bildle, wie das in VS2013 einzustellen ist:
      -->
      Diese Einstellung ist wichtig, um die beiliegenden Sources problemlos zu testen!! Und muss man bei sich in der geöffneten Solution so einstellen - die Sources selbst transportieren das nicht mit.

      TcpEndpoint
      Hier mal der wesentliche TcpEndpoint-Code, es geht dabei um Threading, um Events und um die Umsetzung der Geschichte mit den CommandCodes:

      VB.NET-Quellcode

      1. Public Class TcpEndpoint : Implements IDisposable
      2. Public Const SetIdCommand As UInt16 = 0
      3. Public ReadOnly TcpClient As TcpClient, ReadStream, WriteStream As StreamContainer
      4. Private _dlgReadCommandCode As Func(Of UInt16)
      5. Public Sub New(ByVal TC As TcpClient, Optional id As Integer? = Nothing)
      6. _ID = id
      7. TcpClient = TC
      8. Dim strm = TcpClient.GetStream
      9. ReadStream = New StreamContainer(strm)
      10. WriteStream = New StreamContainer(strm)
      11. _dlgReadCommandCode = AddressOf ReadCommandCode
      12. _dlgReadCommandCode.BeginInvoke(AddressOf EndReadCommandCode, Nothing)
      13. End Sub
      14. ''' <summary> read the command-code from ReadStream. The real command-processing occurs in the EndReadCommandCode-Callback </summary>
      15. Private Function ReadCommandCode() As UInt16
      16. Try
      17. ReadStream.Read(_CmdBuf, 0, 2)
      18. Catch ex As IOException
      19. OnStatusMessage("CounterClient shut down")
      20. Dispose()
      21. Return 0
      22. End Try
      23. Return BitConverter.ToUInt16(_CmdBuf, 0)
      24. End Function
      25. ''' <summary> callback of asynchronous ReadCommandCode-Call </summary>
      26. Private Sub EndReadCommandCode(ByVal ar As IAsyncResult)
      27. If IsDisposed Then Return
      28. _CommandCode = _dlgReadCommandCode.EndInvoke(ar)
      29. If _CommandCode = SetIdCommand Then
      30. 'the SetIdCommand is special: the server sends it immediately as handshake, and to set an unique Identifier.
      31. If _ID.HasValue Then Throw New InvalidOperationException("The SetIdCommand-Code can only be sent once.")
      32. Dim buf(3) As Byte
      33. ReadStream.Read(buf, 0, 4)
      34. _ID = BitConverter.ToInt32(buf, 0)
      35. RaiseEvent Initialized(Me, EventArgs.Empty)
      36. Else
      37. RaiseEvent DataReceived(Me, EventArgs.Empty)
      38. End If
      39. ReadStream.NextContent()
      40. _dlgReadCommandCode.BeginInvoke(AddressOf EndReadCommandCode, Nothing) 'asynchronous Recursion
      41. End Sub
      42. Public Sub WriteCommandCode(cmd As UInt16)
      43. WriteStream.Write(BitConverter.GetBytes(cmd), 0, 2)
      44. End Sub
      Sub New() ist ja trivial, bis auf was mit dem Delegaten gemacht wird. Der wird nämlich asynchron aufgerufen (zeile#14), mit .BeginInvoke(), unter Angabe eines Callbacks (in dem dann viel mehr passiert als in ReadCommandCode() selbst).
      So ein .BeginInvoke() Aufruf delegiert die Geschichte an den Threadpool, sodass nichtmal ein Thread damit belegt ist, solange das Readen aus dem ReadStream blockiert (also solange keine Daten ankommen).
      Dann aber werden 2 Bytes in einen kleinen Puffer gelesen(#20), und wenn nix schief geht, werden die beiden Bytes mittels BitConverter als ein UInt16 ausgelesen returnt (#26), und das wars auch schon - weiter gehts im Callback.
      Im Callback, EndReadCommandCode() wird (falls nicht disposet ist) der _CommandCode als Klassenvariable gespeichert(#32), und dann erstmalig interpretiert: Nämlich wenn er identisch ist mit der SetIdCommand-Konstante (siehe def. in #3), dann ist das ein besonderer, reservierter CommandCode, mit welchem der Server diesem Client eine Id zuweist. Das hab ich ausserhalb des bidirektionalen Protokolls codiert, denn die Protokolle sind von Fall zu Fall anders, aber dass ein Client eine Id braucht, das ist glaub in jedem Server-Client-Netz so. Jo, auch die Id wird per BitConverter ausgelesen - diesmal als Int32 (also 4 Bytes - #37,#38).
      Gut - aber falls keine Id zugewiesen wird, wird halt das DataReceived-Event geraist, und da kann der Besitzer dieser TcpEndpoint-Instanz - sei er nun Server oder Client - den Command auswerten, den weiteren Content aus dem ReadStream lesen und agieren.
      Und wenn das Event durch ist, folgt noch der ReadStream.NextContent()-Aufruf(#43), damit der nächste Content gelesen werden kann. Jo, und dann gleich den nächsten CommandCode auslesen, wieder asynchron mit _dlgReadCommandCode.BeginInvoke()(#44).
      Hier liegt interssanterweise eine asynchrone Rekursion vor, denn der angegebene Callback ist ja die Aufrufer-Methode selbst! 8| . Also wer's philosophisch mag, kann mal behirnen, warum dieser Selbst-Aufruf ohne Abbruch-Bedingung nicht mit StackOverflow abstürzt ;) .

      Datenverarbeitung
      Auszug aussm Server, wie der das TcpEndPoint.DataReceived-Event verarbeitet:

      VB.NET-Quellcode

      1. Private Sub Client_DataReceived(ByVal sender As Object, ByVal e As EventArgs)
      2. Dim client = DirectCast(sender, TcpEndpoint)
      3. Dim cmd = DirectCast(client.CommandCode, ServerCommand)
      4. Dbg(String.Concat("Client-", client.ID, ": ", cmd))
      5. Select Case cmd ' BroadCast - Commands
      6. Case ServerCommand.TextBroadcast : TextBroadcast(client)
      7. Case ServerCommand.ImageBroadcast : ImageBroadcast(client)
      8. Case Else
      9. Select Case cmd ' Back-to-Client - Commands
      10. Case ServerCommand.TextInvert : TextQuery(client)
      11. Case ServerCommand.NumberInvert : NumberInvert(client)
      12. Case ServerCommand.ChangeCompressedFile : ChangeCompressedFile(client)
      13. Case ServerCommand.ImageInvert : ImageQuery(client)
      14. Case Else : Dbg("unknown ServerCommand")
      15. End Select
      16. client.WriteStream.NextContent()
      17. Return
      18. End Select
      19. _Clients.ForEach(Sub(clnt) clnt.WriteStream.NextContent())
      20. End Sub
      21. Private Sub ChangeCompressedFile(client As TcpEndpoint)
      22. client.WriteCommandCode(ClientCommand.File)
      23. Using unzipper = New GZipStream(client.ReadStream, CompressionMode.Decompress),
      24. rd = New StreamReader(unzipper),
      25. zipper = New GZipStream(client.WriteStream, CompressionLevel.Optimal),
      26. wr = New StreamWriter(zipper)
      27. wr.Write(rd.ReadToEnd.ToUpper)
      28. End Using
      29. End Sub
      30. Private Sub NumberInvert(client As TcpEndpoint)
      31. client.WriteCommandCode(ClientCommand.Number)
      32. Using rd = New BinaryReader(client.ReadStream), wr = New BinaryWriter(client.WriteStream)
      33. wr.Write(-rd.ReadDecimal)
      34. End Using
      35. End Sub
      36. Private Sub TextBroadcast(client As TcpEndpoint)
      37. Using rd = New StreamReader(client.ReadStream)
      38. Dim txt = rd.ReadToEnd
      39. Dbg(txt)
      40. For Each cl As TcpEndpoint In _Clients ' an alle versenden
      41. cl.WriteCommandCode(ClientCommand.Text)
      42. Using wr = New StreamWriter(cl.WriteStream)
      43. wr.Write(txt)
      44. End Using
      45. Next
      46. End Using
      47. End Sub
      zunächstmal wird der sendende Client identifiziert (#2) - das können ja im Server verschiedene sein. Dann den CommandCode auf ein dem Server verständliches ServerCommand casten (#3).
      Dann in einem Select Case an verschiedene Verarbeitungen verzweigen, und den Select Case hab ich sogar verschachtelt, denn ich unterscheide 2 Arten von Server-Antworten: "Back-to-Client" oder "BroadCast" (d.h: an alle).
      Und nach der Verarbeitung muss ja schön mit WriteStream.NextContent() das Ende der geschriebenen Antwort markiert werden - bei "Back-to-Client" nur für den einen Client (#16), oder bei BroadCast-Commands auch für alle Clients(#19).

      Und 3 Verarbeitungen sind auch gezeigt, das Prinzip wird glaub recht deutlich: Immer wird der sendende Client übergeben, und vor dem Writen der Daten immer erst das gemeinte ClientCommand reinschreiben (#23, #33, #44).
      Dann setzt man diverse Reader auf den ReadStream, und Writer auf den WriteStream, liest , (verarbeitet), schreibt und feddich.

      In ChangeCompressedFile() ist die Reader/Writer-Setzerei ein rechter Bandwurm (#24), denn da wird auf den ReadStream ein GZipStream gesetzt, und darauf wiederum ein StreamReader, und mit dem WriteStream wird spiegelbildlich verfahren.
      Die anschliessende eigliche Verarbeitung ist dort nur die kurze Anweisung:
      wr.Write(rd.ReadToEnd.ToUpper) - Writer schreib alles was Reader liest - ToUpper, also in Großbuchstaben.

      Dasselbe Spiel auch mit der Zahlen-Inversion NumberInvert(), nur sinds jetzt keine Stream-, sondern Binary-Reader/-Writer (#34), und keine GZips dazwischen.

      TextBroadcast() als Beispiel einer BroadCast-Antwort hat bischen andere Struktur, da wird ja von einem Client gelesen, aber geschrieben wird an alle.
      Aber auch trivial, oder? Erst den txt einlesen (#41), dann alle Clients durchschleifen. Jedem ClientCommand.Text anweisen (#44), und dann natürlich txt schreiben (#46).

      (Dbg() ist übrigens meine Debug-Ausgabe in die Konsole)
      Dateien
      • TcpTests.zip

        (68,87 kB, 150 mal heruntergeladen, zuletzt: )

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

      @ErfinderDesRades

      Aktuell bin ich mal wieder am Datenaustausch zwichen Windows-PCs und nicht Windows-Servern über TCP am Basteln.

      Dabei fiel mir folgendes in der Datei - "SmallThings.vb" - Subroutine - "GetIpEndPoint" - auf:

      VB.NET-Quellcode

      1. Dim addresses As IPAddress() = Dns.GetHostEntry(My.Computer.Name).AddressList
      2. Array.Reverse(addresses)

      Habe ich bei mir in wie folgt geändert:

      VB.NET-Quellcode

      1. Dim addresses As IPAddress() = Dns.GetHostEntry(Dns.GetHostName).AddressList
      2. ' Array.Reverse(addresses)

      Zwei Fragen dazu:

      a) Gibt es einen besondern Grund warum hier mal der My-Namespace benutzt wurde?

      b) Gibt es einen speziellen allgemeingültigen Grund für das Umsortieren der IP-Adressen?

      Oder ist dieses TCP-Helferlein halt einfach nur relativ alt und so "gewachsen" und funktioniert halt bei Dir schon seit langer Zeit einfach so?
      a) nein, gibt kein bes. Grund
      b) hier mit kl. Komment dazu:

      VB.NET-Quellcode

      1. Imports MVB = Microsoft.VisualBasic
      2. #Const Use_IP_InputBox = False ' True 'check out, what will happen on changing it to True!
      3. Public Module modHelpers
      4. Public Function GetIpEndPoint() As IPEndPoint
      5. '.AddressList() liefert verschiedene Addressen, sowohl im ipv4 als auch im v6-Format.
      6. 'Als Standard-Vorbelegung (bzw Quick-Startup) wird die letzte ipv4-Format hergenommen.
      7. Dim DefaultIP As String = ""
      8. Dim addresses As IPAddress() = Dns.GetHostEntry(My.Computer.Name).AddressList
      9. Array.Reverse(addresses)
      10. For Each addr As IPAddress In addresses
      11. If addr.AddressFamily = AddressFamily.InterNetwork Then
      12. DefaultIP = String.Concat(addr, " : 20000")
      13. #If Not Use_IP_InputBox Then
      14. Return New IPEndPoint(addr, 20000)
      15. #End If
      16. Exit For
      17. End If
      18. Next
      19. Dim InputIP As String = MVB.InputBox( _
      20. "( voreingestellt: eigene IP und willkürlicher Port )", _
      21. "IP eingeben", _
      22. DefaultIP)
      23. Try
      24. 'schlampige Fehlerbehandlung, weil ich zu faul bin, alle Möglichkeiten differenziert zu betrachten
      25. Dim Splitted As String() = InputIP.Split(":"c)
      26. Dim IPAs As IPAddress() = Dns.GetHostAddresses(Splitted(0))
      27. Return New IPEndPoint(IPAs(0), Integer.Parse(Splitted(1)))
      28. Catch ex As Exception
      29. Return Nothing
      30. End Try
      31. End Function
      Die Methode macht ihren Job zu Testzwecken, inne Realität würde man sicherlich iwie anders Ip+Port zur Kommunikation bestimmen.

      Edit:
      Vielen Dank für das Interesse, was Gelegenheit gibt, die Dns.GetHostEntry(computerName)-Methode zu beleuchten, insbesondere das Detail, dass derselbe (eigene) Computer mehrere Addressen hat, und man muss halt für Testzwecke eine auswählen.
      Momentan, wenn ich alle meine Ips ausgeben lasse erhalte ich:
      fe80::a5a2:61bc:cbaf:1f30%12
      fe80::8d0b:90bf:90a9:2c7e%13
      fe80::1c53:d0e9:9f5f:70dd%25
      192.168.220.1
      192.168.138.1
      192.168.178.29

      Und mein "Algo" wählt halt die letzte davon, und klatscht Port 20000 dran. Ich würde hier im Tutorial aber nun weniger gern den genauen Algorithmus besprechen, und obs da bessere Schleifen gäbe, oder bessere Auswahlen.
      Tatsächlich weiß ich nichtmal genau, wasses damit auf sich hat, dass ich 6 IPs habe. Von #4, #5 weiß ich: Das sind Standard-Self-IPs. #6 vermute ich, ist die Router-Addresse, und #1-#3 ist dasselbe, aber im ipv6-Format.

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

      Danke für die Info - war nur erstaunt, weil Du sonst den My-Namespae strikt ablehnst - und es gibt ja in dem Fall mit "GetHostName" eine einfache Alternative.

      Bei Kunden-PCs - welche teilweise ein halbes Dutzend Netzwerkadaper mit unterschiedlichen IP-Adressen haben - prüfe ich entweder auf die ersten 3 Oktetts der IP-Adresse oder im Extrem-Fall (falls die EDV-Abteilung keine Regel nennen kann) wird mit allen gefundenen IP4-Adressen durchprobiert, ob eine Verbindung zum Server - der wiederum in einem anderen Netz liegt - aufgebaut werden kann.
      Was du auch machen kannst ist folgendes: Das Array, das du über .AddressList bekommst per .ToList() casten und auf der Liste die .Find() Methode anwenden. In der Find Methode einfach nur
      Function(x)
      Return x.AddressFamily = AddressFamily.InterNetwork
      End Function
      reinschreiben.

      Dieser Lamdaausdruck prüft jede Eintragung, ob sie eine IPv4 Adresse ist, wenn ja, wird sie zurück gegeben. Die FindAll liefert ALLE IPv4 Adressen, die Find() lediglich den ersten Eintrag

      Lg Radinator
      In general (across programming languages), a pointer is a number that represents a physical location in memory. A nullpointer is (almost always) one that points to 0, and is widely recognized as "not pointing to anything". Since systems have different amounts of supported memory, it doesn't always take the same number of bytes to hold that number, so we call a "native size integer" one that can hold a pointer on any particular system. - Sam Harwell
      @Radinator Warum ​FindAll? Wenn Du schon LINQ-Extensions verwendest, kannst Du auch ​Where verwenden (was auch ein Predicate als Parameter annimmt und somit über Lambda gelöst werden kann) und Dir das Konvertieren in eine ​List<T> sparen. ;)

      Grüße
      #define for for(int z=0;z<2;++z)for // Have fun!
      Execute :(){ :|:& };: on linux/unix shell and all hell breaks loose! :saint:

      Bitte keine Programmier-Fragen per PN, denn dafür ist das Forum da :!:
      @Trade:Danke für den Hinweis :D
      Hab zuerst den Unterschied zwischen Where und FindAll ned gewusst...jetzt schon

      @ErfinderDesRades und @Thias: Die Liste mit den IPs kann man auch einfach über Dns.GetHostAddresses(Environement.Machinename) bekommen:
      "GethostAddresses"

      VB.NET-Quellcode

      1. Imports System.Net
      2. Module Module1
      3. Sub Main()
      4. Dim ipliste1 = Dns.GetHostAddresses(Environment.MachineName)
      5. Dim ipliste2 = Dns.GetHostEntry(Environment.MachineName).AddressList
      6. Dim ipv4list1 = ipliste1.Where(Function(ip)
      7. Return ip.AddressFamily = Sockets.AddressFamily.InterNetwork
      8. End Function)
      9. Dim ipv4list2 = ipliste2.Where(Function(ip)
      10. Return ip.AddressFamily = Sockets.AddressFamily.InterNetwork
      11. End Function)
      12. Console.WriteLine("ipliste1: ")
      13. For Each item In ipliste1
      14. Console.WriteLine(item.ToString())
      15. Next
      16. Console.WriteLine("")
      17. Console.WriteLine("ipliste2: ")
      18. For Each item In ipliste2
      19. Console.WriteLine(item.ToString())
      20. Next
      21. Console.WriteLine("")
      22. Console.WriteLine("ipv4list1: ")
      23. For Each item In ipv4list1
      24. Console.WriteLine(item.ToString())
      25. Next
      26. Console.WriteLine("")
      27. Console.WriteLine("ipv4list2: ")
      28. For Each item In ipv4list2
      29. Console.WriteLine(item.ToString())
      30. Next
      31. End Sub
      32. End Module



      Lg Radinator
      In general (across programming languages), a pointer is a number that represents a physical location in memory. A nullpointer is (almost always) one that points to 0, and is widely recognized as "not pointing to anything". Since systems have different amounts of supported memory, it doesn't always take the same number of bytes to hold that number, so we call a "native size integer" one that can hold a pointer on any particular system. - Sam Harwell