TCP-Server & TCP-Clients mit PowerShell

Heute möchte ich euch zeigen, wie man mit der PowerShell eine Client-Server Verbindung über das Netzwerk herstellt. Dabei verwende ich TCP. Sicher, es gibt dafür schon etliche Anleitungen im Netz – aber ich habe keine gefunden, mit der ein TCP-Server auf EINEM Port mehrere Client-Sessions bedienen kann. Und den Code für dieses Szenario bekommt ihr bei mir 🙂

Verbindung zwischen einem Server und einem Client (Basic)

Für diese Verbindung muss auf dem Server ein TCP-Listener eingerichtet und gestartet werden. Das Argument ist die Portnummer. Ist der Listener gestartet, dann wartet der Server auf einen Client (Zeile 3):

$TcpServer = New-Object System.Net.Sockets.TcpListener -ArgumentList 9005
$TcpServer.Start()
$TCPClient = $TcpServer.AcceptTcpClient()

Der Client startet seine TCP-Session ähnlich. In Zeile 1 wähle ich einen zufälligen LocalPort und baue dann bis zur Zeile 3 den TCPClient zusammen. In der Do-While-Schleife wartet mein Client nun auf eine Server-Verbindung von localhost:9005 und probiert es im Abstand von 1 Sekunde erneut:

$SourcePort = Get-Random -Minimum 48086 -Maximum 53042
$Endpoint   = New-Object System.Net.IPEndpoint ([ipaddress]::any,$SourcePort) 
$TcpClient  = [Net.Sockets.TCPClient]$Endpoint
do{
    try   { $TcpClient.Connect("localhost",9005) } 
    catch { Sleep 1 }
} while (-not $TcpClient.Connected)

Haben sich der Client und der Server gefunden muss nun jeder noch eine Verbindung zum Datenstrom auf der Session aufnehmen. Sowohl der Server als auch der Client erledigt das mit dieser Zeile:

$DataStream = $TCPClient.GetStream()

Danach kann der Server einen Text an den Client senden:

$Data = [text.Encoding]::Ascii.GetBytes('Message an Client 1')    
$DataStream.Write($Data,0,$Data.length)

Diesen nimmt der Client über folgenden Code entgegen:

[byte[]]$byte = New-Object byte[] 64512
$byteCount = $DataStream.read($byte, 0, $byte.Length)
$Message += [text.encoding]::ASCII.GetString( (1..$byteCount | ForEach-Object { $byte[$_-1] } ) )

Die Nachricht ist dann auf dem Client in der Variable $Message gespeichert.

Wird die Verbindung nicht mehr benötigt, dann kann sie mit diesen Zeilen auf dem Server beendet werden:

$TcpServer.stop()
$TcpServer.Server.Dispose()

Der Client dagegen baut die Verbindung so ab:

$TcpClient.Close()
$TcpClient.Dispose()

Das sind die Basics – ein alter Hut!

Verbindung zwischen einem Server und VIELEN Clients

Dieses Szenario ist nicht viel komplizierter. Auf der Clientseite ist eigentlich alles wie im ersten Szenario. Den Code des Clients habe ich aber um eine Schleife für den Empfang von Nachrichten erweitert. Erst, wenn er den Text ‚close‘ empfängt, wird die Schleife unterbrochen und die Sitzung wird beendet:

# Variablen
    $ServerName = 'localhost'
    $Port       = 9005

# erstelle TCP-Client    
    $SourcePort = Get-Random -Minimum 48086 -Maximum 53042
    $Endpoint   = New-Object System.Net.IPEndpoint ([ipaddress]::any,$SourcePort) 
    $TcpClient  = [Net.Sockets.TCPClient]$Endpoint

# warte auf Verbindung zum TCPServer            
    do{
        try   { $TcpClient.Connect("$ServerName",$Port) } 
        catch { Sleep 1 }
    } while (-not $TcpClient.Connected)
    $DataStream = $TcpClient.GetStream()

# solange kein 'close' gesendet wird: empfange Messages
    $Message = ""
    do {        
        if ($DataStream.DataAvailable) {
            $Message = ""  
            do {    
                [byte[]]$byte = New-Object byte[] 64512
                $byteCount = $DataStream.read($byte, 0, $byte.Length)
                $Message += [text.encoding]::ASCII.GetString( (1..$byteCount | ForEach-Object { $byte[$_-1] } ) )
            } while ($DataStream.DataAvailable)
                                  
            write-host "$Message"
        } else {            
            Start-Sleep -Milliseconds 100
        }
    } while ($Message -ne "close")

# beende TcpClient        
    $TcpClient.Close()
    $TcpClient.Dispose()

Die Clients werden einfach gestartet:

TCP-Server & TCP-Clients mit PowerShell

Der TCP-Server muss sich mehrere Verbindungen „merken“, um diese korrekt anzusprechen. Da bietet sich meine Variable in Zeile 2 (ein Array) an. Dazu wird wie im ersten Beispiel der TCP-Server gestartet:

# Variablen
    $Sessions = @()
    $Port     = 9005

# TCP-Server starten
    $TcpServer = New-Object System.Net.Sockets.TcpListener -ArgumentList $Port
    $TcpServer.Start()

Die erste Clientverbindung wird nun in einem neuen Objekt mit definierten Eigenschaften gespeichert. In der ersten merke ich mir einfach eine aufsteigende Nummer (AnzahlVorhandeneSessions + 1), dann die RemoteIP und den RemotePort (beides ist optional) und zuletzt den wichtigen DataStream. Ist das Objekt zusammengestellt, wird es an der ArrayVariable $Sessions angefügt. Es entsteht somit eine tabellarische Struktur:

$IncomingClient = $TcpServer.AcceptTcpClient() 
    $NewSession = New-Object -TypeName PSObject
    $NewSession | Add-Member -MemberType NoteProperty -Name SessionID  -Value ($Sessions.Count + 1)
    $NewSession | Add-Member -MemberType NoteProperty -Name RemoteIP   -Value $IncomingClient.Client.RemoteEndPoint.Address.IPAddressToString
    $NewSession | Add-Member -MemberType NoteProperty -Name RemotePort -Value $IncomingClient.Client.RemoteEndPoint.Port
    $NewSession | Add-Member -MemberType NoteProperty -Name DataStream -Value $IncomingClient.GetStream()
    $Sessions += $NewSession

Analog kann eine 2te, 3te, …xte Session entgegengenommen werden. 🙂 Das sieht dann so aus:

TCP-Server & TCP-Clients mit PowerShell

Nur wie spricht man nun eine Session konkret an? Ganz einfach: über den Index in der Arrayvariable Sessions:

$Data = [text.Encoding]::Ascii.GetBytes('Message an Client 1')
$Sessions[0].DataStream.Write($Data,0,$Data.length)

$Sessions[0] enthält die erste TCP-Verbindung. Diese umfasst eine Eigenschaft DataStream (siehe oben: Add-Member!). Die Eigenschaft – und damit der gemerkte Datenstrom – kann durch Objekt.Eigenschaft angesprochen werden: $sessions[0].DataStream. Und ab hier geht’s genauso weiter wie im 1:1-Beispiel:

TCP-Server & TCP-Clients mit PowerShell

Soll nun ein Text an mehrere Clients gesendet werden, dann könnte dies so aussehen:

$Data = [text.Encoding]::Ascii.GetBytes('Message an viele Clients')
$Sessions[0].DataStream.Write($Data,0,$Data.length)
$Sessions[1].DataStream.Write($Data,0,$Data.length)
$Sessions[2].DataStream.Write($Data,0,$Data.length)

TCP-Server & TCP-Clients mit PowerShell

Das Beenden einer Session ist mit diesen Zeilen möglich:

$Data = [text.Encoding]::Ascii.GetBytes('close')
$Sessions[0].DataStream.Write($Data,0,$Data.length)
$Sessions[0].DataStream.Close()

Sind alle Sessions beendet, dann kann der TCP-Server und der Listener wieder abgeschaltet werden:

$TcpServer.stop()
$TcpServer.Server.Dispose()

Der vollständige Code mit einem Beispiel für den Server könnte nun so aussehen:

# Variablen
    $Sessions = @()
    $Port     = 9005

# TCP-Server starten
    $TcpServer = New-Object System.Net.Sockets.TcpListener -ArgumentList $Port
    $TcpServer.Start() 

# 1. Session annehmen
    $IncomingClient = $TcpServer.AcceptTcpClient() 
    $NewSession = New-Object -TypeName PSObject
    $NewSession | Add-Member -MemberType NoteProperty -Name SessionID  -Value ($Sessions.Count + 1)
    $NewSession | Add-Member -MemberType NoteProperty -Name RemoteIP   -Value $IncomingClient.Client.RemoteEndPoint.Address.IPAddressToString
    $NewSession | Add-Member -MemberType NoteProperty -Name RemotePort -Value $IncomingClient.Client.RemoteEndPoint.Port
    $NewSession | Add-Member -MemberType NoteProperty -Name DataStream -Value $IncomingClient.GetStream()
    $Sessions += $NewSession

# Daten an 1. Session senden
    $Data = [text.Encoding]::Ascii.GetBytes('Message an Client 1')
    $Sessions[0].DataStream.Write($Data,0,$Data.length)

# 2. Session annehmen
    $IncomingClient = $TcpServer.AcceptTcpClient() 
    $NewSession = New-Object -TypeName PSObject
    $NewSession | Add-Member -MemberType NoteProperty -Name SessionID  -Value ($Sessions.Count + 1)
    $NewSession | Add-Member -MemberType NoteProperty -Name RemoteIP   -Value $IncomingClient.Client.RemoteEndPoint.Address.IPAddressToString
    $NewSession | Add-Member -MemberType NoteProperty -Name RemotePort -Value $IncomingClient.Client.RemoteEndPoint.Port
    $NewSession | Add-Member -MemberType NoteProperty -Name DataStream -Value $IncomingClient.GetStream()
    $Sessions += $NewSession

# Daten an beide Sessions senden
    $Data = [text.Encoding]::Ascii.GetBytes('Message an beide Clients')
    $Sessions[0].DataStream.Write($Data,0,$Data.length)
    $Sessions[1].DataStream.Write($Data,0,$Data.length)

# 1. Session beenden
    $Data = [text.Encoding]::Ascii.GetBytes('close')
    $Sessions[0].DataStream.Write($Data,0,$Data.length)
    $Sessions[0].DataStream.Close()

# 2. Session beenden
    $Data = [text.Encoding]::Ascii.GetBytes('close')
    $Sessions[1].DataStream.Write($Data,0,$Data.length)
    $Sessions[1].DataStream.Close()

# TCP-Server beenden
    $TcpServer.stop()
    $TcpServer.Server.Dispose()

So einfach kann es sein. 🙂 Den vollständigen Code als Beispiel gibt es hier als zip.

Stay Tuned!