In einem früheren Post habe ich beschrieben, wie man einen einfachen Socket Server baut. Dieses mal will ich den Service konsumieren. Eine Socket Service Kommunikation ist extrem effizient. Allerdings müssen die verwendeten Ports in allen beteiligten Firewalls auch frei geschalten sein. Silverlight auferlegt sich zudem eine künstliche Beschränkung auf den Bereich 4502-4534. Noch viel schwerwiegender ist, das man bei der TCP Socket Kommunikation sehr schnell in Cross Domain Szenarien landet. Dann erhält man Security Exceptions, außer die Anwendung läuft OOB full trusted. Für allen jene denen diese Beschreibung noch nicht verwirrend genug ist: Wenn die Anwendung im Browser läuft muss eine ClientAccessPolicy.XML Datei angelegt werden, die dann von Silverlight ungefragt angefordert wird. Format und Inhalt ungefähr so.
<?xml version="1.0" encoding="utf-8"?>
<access-policy>
<cross-domain-access>
<policy>
<allow-from http-request-headers="*">
<domain uri="*" />
</allow-from>
<grant-to>
<socket-resource port="4502-4534" protocol="tcp" />
</grant-to>
</policy>
</cross-domain-access>
</access-policy>
Der vorläufige Höhepunkt ist, das Silverlight ab Version 4 versucht diese Datei auf Port 943 anzufordern. Dafür müsste man nun noch einen extra Socket Server schreiben der auf diesem Port lauscht und die Datei überträgt. Ist mühsam und deswegen gibt es wohl nun die Zweitmöglichkeit die Datei in das root des Webservers zu legen und per http auszuliefern. Allerdings muss man Silverlight das im Code noch mitteilen (SocketClientAccessPolicyProtocol.Http) wie wir gleich im folgenden Code sehen werden. Erlaubt sei noch der Hinweis das dies mit dem WebDev Server aus Visual Studio nicht geht, weil dieser auf einen zufälligen Port lauscht und nicht auf 80.
Weiter gehts mit der Initialisierung der Socket Verbindung. Die Sache mit der IP Adresse ist ein wenig mühsam und muss über den DNS Endpoint gelöst werden um rauszukriegen auf welcher Website die Anwendung läuft. Am Ende wird versucht die Verbindung herzustellen.
Dim sock As Socket
Private Sub Button1_Click(ByVal sender As System.Object,
ByVal e As System.Windows.RoutedEventArgs) Handles Button1.Click
Dim ep As DnsEndPoint =
New DnsEndPoint(Application.Current.Host.Source.DnsSafeHost, 4506)
sock = New Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp)
Dim args As SocketAsyncEventArgs = New SocketAsyncEventArgs()
args.RemoteEndPoint = ep
args.SocketClientAccessPolicyProtocol =
SocketClientAccessPolicyProtocol.Http 'Clientaccesspolicy
AddHandler args.Completed, AddressOf onSocketConnect
sock.ConnectAsync(args)
End Sub
Da das ganze asynchron läuft, wie bei Silverlight üblich, benötigt man die Funktion onSocketConnect, die aufgerufen wird sobald die Verbindung besteht. Jetzt wirds ein wenig Tricky. Um die eingehenden Daten zu erhalten, brauchen wir noch ein zweites Event OnSocketEmpfang. Da dies auch am Completed Event der SocketArgs hängt, habe ich die Events durch deregistrieren und registrieren ausgetauscht. Ob dies der beste Weg ist, ist mir nicht bekannt. Zumindest habe ich auf an anderer Stelle ähnliche Ansätze gefunden. Nun gehts ans Empfangen mit ReceiveAsync.
Private Sub onSocketConnect(ByVal sender As Object, ByVal e As SocketAsyncEventArgs)
If sock.Connected Then
RemoveHandler e.Completed, AddressOf onSocketConnect
Dim resp(1024) As Byte
e.SetBuffer(resp, 0, resp.Length)
AddHandler e.Completed, AddressOf onSocketEmpfang
sock.ReceiveAsync(e)
End If
End Sub
Wenn dann ab und zu mal Sekunden in der Form 40 oder so eintrudeln, landen wir im OnSocketEmpfang. Die Daten stehen zwar freundlicherweise im e.Buffer, aber leider befinden wir uns im Background Thread. Entsprechend muss per BeginInvoke ein Delegate definiert werden die eine Methode aufruft die im UI Thread läuft. Dabei werden die Argumente e als Parameter übergeben. Schlussendlich rufe ich wieder den ReceiveAsync Befehl auf, weil die Daten regelmäßig bis zum Sankt Nimmerleinstag kommen.
Private Sub onSocketEmpfang(ByVal sender As Object,
ByVal e As SocketAsyncEventArgs)
Dispatcher.BeginInvoke
(New Action(Of SocketAsyncEventArgs)(AddressOf updateUI), e)
sock.ReceiveAsync(e)
End Sub
Der Vollständigkeit halber noch der Teil der letztendlich eine TextBox im Frontend aktualisiert.
Private Sub updateUI(ByVal e As SocketAsyncEventArgs)
TextBlock1.Text =
Encoding.UTF8.GetString(e.Buffer, 0, e.BytesTransferred) +
Environment.NewLine
End Sub
Wer keine Erfahrung mit dem synchronsieren von Threads hat und das für Teufelszeug hält, soll mal in die app.xaml.vb oder app.xaml.cs blicken. Dort wird das globale Fehlerhandling genau so realisiert. Mit Hilfe einer Lambda Expression noch etwas eleganter codieren, obs lesbarer ist mag jeder selbst entscheiden.
Dispatcher.BeginInvoke(
New Action(Of SocketAsyncEventArgs)(
Sub()
TextBlock1.Text +=
Encoding.UTF8.GetString(e.Buffer, 0, e.BytesTransferred) +
Environment.NewLine
End Sub), e)