Introduction
This article is part of the article about Using Silverlight Sockets. If you haven't already reviewed this article, please do so before continuing because it explains more about why a Policy Server is required.
Walkthrough
This solution has 4 components:
The PusherServer contains an instance of the DataGetter and a list of AsyncClientConn ections. When a new socket connection is made to the PusherServer, it creates a new AsyncClientConn ection and passes it a reference to the socket connection and a reference to the DataGetter.
When a new AsyncClientConn ection is created, it specifies a method that is to be used to handle the DataGetter's DataRetrieved event.
Now, when the DataGetter instance raises the DataRetrieved event all connected AsyncClientConn ections' push the data retrieved to the clients. There is no more enumerating through the list of AsyncClientConn ections, no need to lock the list of AsyncClientConn ections while pushing data, and the connections are established more quickly and smoother than the first solution.
The push server:
The following manage a connection to the push server:
Helper:
EventArgs:
The console application:
This needs major modification.
This article is part of the article about Using Silverlight Sockets. If you haven't already reviewed this article, please do so before continuing because it explains more about why a Policy Server is required.
Walkthrough
This solution has 4 components:
- AsyncClientConn ection: a class that manages the asynchronous socket connection logic. It is responsible for sending (pushing) the data to the connected client. This class contains a reference to a DataGetter.
- PusherServer: a class that manages the connected AsyncClientConn ections. It accepts incoming asynchronous socket connections and creates new AsyncClientConn ections that manage these sockets. It also removes any AsyncClientConn ections that are no longer connected.
- DataGetter: a class used to retrieve the data that is to be sent to the connected clients. The data retrieved is a picture file. The DataGetter raises a DataRetrieved event every time it's finished retrieving a picture.
- TAsyncClientEve ntArgs: a class that inherits from EventArgs. It's used to transfer the data retrieved by the DataGetter class when it raises the DataRetrieved event.
The PusherServer contains an instance of the DataGetter and a list of AsyncClientConn ections. When a new socket connection is made to the PusherServer, it creates a new AsyncClientConn ection and passes it a reference to the socket connection and a reference to the DataGetter.
When a new AsyncClientConn ection is created, it specifies a method that is to be used to handle the DataGetter's DataRetrieved event.
Now, when the DataGetter instance raises the DataRetrieved event all connected AsyncClientConn ections' push the data retrieved to the clients. There is no more enumerating through the list of AsyncClientConn ections, no need to lock the list of AsyncClientConn ections while pushing data, and the connections are established more quickly and smoother than the first solution.
The push server:
Code:
Imports System.Net Imports System.Net.Sockets Imports System.Threading Imports System.IO Imports System Imports System.Text Public Class PusherServer Private _listener As Socket Private _endpoint As IPEndPoint Private _connectedClients As List(Of AsyncClientConnection) Private WithEvents _pictureGetter As PictureGetter Private connections As Boolean = False ''' <summary> ''' Initializes a new instance of the PusherServer. ''' Starts the server. ''' </summary> ''' <remarks></remarks> Public Sub New() _connectedClients = New List(Of AsyncClientConnection) _pictureGetter = New PictureGetter _endpoint = New IPEndPoint(IPAddress.Any, 4509) _listener = New Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) _listener.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, 0) _listener.Bind(_endpoint) _listener.Listen(10) Console.WriteLine("Waiting for request...") _listener.BeginAccept(New AsyncCallback(AddressOf OnConnection), Nothing) End Sub ''' <summary> ''' Called when a new connection is made with the server. ''' Adds the connection to the list of managed connections and sends intial ''' data to the client. ''' </summary> ''' <param name="res"></param> ''' <remarks></remarks> Private Sub OnConnection(ByVal res As IAsyncResult) SyncLock _connectedClients Console.WriteLine("Connection with client made.") Dim newClient As New AsyncClientConnection(_listener.EndAccept(res), _pictureGetter) AddHandler newClient.Closed, AddressOf ConnectionClosed _connectedClients.Add(newClient) connections = True _pictureGetter.StartGettingPictures() End SyncLock 'look for more connections. _listener.BeginAccept(New AsyncCallback(AddressOf OnConnection), Nothing) End Sub Private Sub ConnectionClosed(ByVal sender As Object, ByVal e As EventArgs) SyncLock _connectedClients _connectedClients.Remove(sender) If _connectedClients.Count = 0 And _pictureGetter.IsGettingPictures Then _pictureGetter.StopGettingPictures() Console.WriteLine() Console.WriteLine("Waiting for request...") Console.WriteLine() End If End SyncLock End Sub End Class
The following manage a connection to the push server:
Code:
Imports System.Net Imports System.Net.Sockets Imports System.Runtime.Serialization Imports System.Runtime.Serialization.Formatters.Binary ''' <summary> ''' Manages a single socket connection ''' </summary> ''' <remarks></remarks> Public Class AsyncClientConnection Implements IDisposable Private _disposedOf As Boolean = False ' To detect redundant calls Private _service As PictureGetter 'A reference to the class responsible for retriving the data to send Private _client As Socket 'The socket Public Event Closed As EventHandler 'Used to indicate when the connection has been closed ''' <summary> ''' Indicates whether or not the client/socket is connected. ''' </summary> ''' <value>Whether or not the client/socket is connected.</value> ''' <returns>Whether or not the client/socket is connected.</returns> ''' <remarks></remarks> Private ReadOnly Property IsConnected() As Boolean Get If _disposedOf = False AndAlso _client IsNot Nothing Then Try Return _client.Connected Catch ex As ObjectDisposedException Me.Dispose() Return False End Try End If Me.Dispose() Return False End Get End Property ''' <summary> ''' Initializes a new instance of the AsyncClient ''' </summary> ''' <param name="client">The Socket that the AsyncClient manages</param> ''' <remarks></remarks> Public Sub New(ByVal client As Socket, ByVal service As PictureGetter) _client = client _service = service AddHandler _service.PictureRetrieved, AddressOf DataRetrieved End Sub ''' <summary> ''' Disables and release the resources used by the socket. ''' </summary> ''' <remarks></remarks> Public Sub Close() If Not _disposedOf Then Try _client.Close() Catch ex As ObjectDisposedException Me.Dispose() Catch ex As SocketException Console.WriteLine("There was a problem closing the connection:") ProcessError(ex) End Try End If End Sub ''' <summary> ''' Handles the PictureGetter's PictureRetrieved event. ''' Pushes the picture to the client. ''' </summary> ''' <param name="sender">The PictureGetter</param> ''' <param name="e">The TAsyncClientEventArgs containing the byte array representation of the picture to send</param> ''' <remarks></remarks> Private Sub DataRetrieved(ByVal sender As Object, ByVal e As TAsyncClientEventArgs) Send(e.PushData) End Sub '''' <summary> '''' Sends the String supplied to the client '''' </summary> '''' <param name="data">A String message to send to the client</param> '''' <remarks></remarks> 'Public Sub Send(ByVal data As String) ' If Not _disposedOf Then ' If IsConnected Then ' Dim encoder As New System.Text.ASCIIEncoding ' Dim message() As Byte = encoder.GetBytes(data) ' Console.WriteLine("Sending data to client...") ' _client.BeginSend(message, 0, message.Length, SocketFlags.None, New AsyncCallback(AddressOf OnSendCompleted), Nothing) ' Else ' Console.WriteLine("Client is closed: cannot send data to client") ' ProcessClose() ' End If ' End If 'End Sub ''' <summary> ''' Sends the String supplied to the client ''' </summary> ''' <param name="data">A String message to send to the client</param> ''' <remarks></remarks> Public Sub Send(ByVal data() As Byte) 'If Not _disposedOf Then ' If IsConnected Then ' Try ' SendInitPackage(data.Length) ' Dim dataWithSize() As Byte = data ' Console.WriteLine("Sending data to client...") ' _client.BeginSend(dataWithSize, 0, dataWithSize.Length, SocketFlags.None, New AsyncCallback(AddressOf OnSendCompleted), Nothing) ' Catch ex As Exception ' If IsConnected Then ' ProcessError(ex) ' Else ' ProcessClose() ' End If ' End Try ' Else ' Console.WriteLine("Client is closed: cannot send data to client") ' ProcessClose() ' End If 'End If If Not _disposedOf Then If IsConnected Then 'An integer is 32 bits = 4 bytes Dim intSize As Integer = data.Length Dim size() As Byte = BitConverter.GetBytes(intSize) Dim dataWithSize(size.Length + data.Length - 1) As Byte size.CopyTo(dataWithSize, 0) data.CopyTo(dataWithSize, size.Length) Try Console.WriteLine("Sending data to client...") _client.BeginSend(dataWithSize, 0, dataWithSize.Length, SocketFlags.None, New AsyncCallback(AddressOf OnSendCompleted), Nothing) Catch ex As Exception If IsConnected Then ProcessError(ex) Else ProcessClose() End If End Try Else Console.WriteLine("Client is closed: cannot send data to client") ProcessClose() End If End If End Sub ''' <summary> ''' Sends an initalizing package to the client with the size of the data about to be sent. ''' </summary> ''' <param name="size">The size of the data about to be sent.</param> ''' <remarks></remarks> Private Sub SendInitPackage(ByVal size As Integer) If Not _disposedOf Then If IsConnected Then Try Dim sizeInBytes() As Byte = BitConverter.GetBytes(size) Console.WriteLine("Sending init package to client...") _client.BeginSend(sizeInBytes, 0, sizeInBytes.Length, SocketFlags.None, New AsyncCallback(AddressOf OnInitSendComplete), Nothing) Catch ex As Exception If IsConnected Then ProcessError(ex) Else ProcessClose() End If End Try Else Console.WriteLine("Client is closed: cannot init package to client") ProcessClose() End If End If End Sub ''' <summary> ''' Called when the data has been sent to the client. ''' </summary> ''' <param name="res">Stores state information and any user defined data for this asynchronous operation</param> ''' <remarks></remarks> Private Sub OnSendCompleted(ByVal res As IAsyncResult) If Not _disposedOf Then If IsConnected Then Try Console.WriteLine("Data Sent to client.") _client.EndSend(res) Catch ex As ObjectDisposedException Me.Dispose() Catch ex As Exception Console.WriteLine("Could not send data to client:") ProcessError(ex) End Try Else ProcessClose() End If End If End Sub ''' <summary> ''' Called when the initialize package has been sent to client. ''' </summary> ''' <param name="res">Stores state information and any user defined data for this asynchronous operation</param> ''' <remarks></remarks> Private Sub OnInitSendComplete(ByVal res As IAsyncResult) If Not _disposedOf Then If IsConnected Then Try Console.WriteLine("Init package sent to client.") _client.EndSend(res) Catch ex As ObjectDisposedException Me.Dispose() Catch ex As Exception Console.WriteLine("Could not send init package to client:") ProcessError(ex) End Try Else ProcessClose() End If End If End Sub ''' <summary> ''' Processes closing the client ''' </summary> ''' <remarks></remarks> Private Overloads Sub ProcessClose() Try _client.Close() Me.Dispose() Catch ex As Exception 'client already closed/disposed of End Try RaiseEvent Closed(Me, New EventArgs) End Sub ''' <summary> ''' Processes the case of an error and closes the client. ''' </summary> ''' <param name="ex">The error to process</param> ''' <remarks></remarks> Private Overloads Sub ProcessError(ByVal ex As Exception) Console.WriteLine(ex.Message) Console.WriteLine(ex.StackTrace) Console.WriteLine() ProcessClose() End Sub ' IDisposable Protected Overridable Sub Dispose(ByVal disposing As Boolean) If Not Me._disposedOf Then If disposing Then ' TODO: free other state (managed objects). End If ' TODO: free your own state (unmanaged objects). ' TODO: set large fields to null. If _client IsNot Nothing Then Try _client.Close() Catch ex As Exception 'client already closed/disposed of End Try End If End If Me._disposedOf = True End Sub #Region " IDisposable Support " ' This code added by Visual Basic to correctly implement the disposable pattern. Public Sub Dispose() Implements IDisposable.Dispose ' Do not change this code. Put cleanup code in Dispose(ByVal disposing As Boolean) above. Dispose(True) GC.SuppressFinalize(Me) End Sub #End Region End Class
Code:
Imports System.IO ''' <summary> ''' Responsible for retrieving the data to send. ''' When the data has been retrieved, it raises an event handled by the AsyncClient's which push the ''' data to the connected clients. ''' </summary> ''' <remarks></remarks> Public Class PictureGetter Private _timer As System.Timers.Timer Private _currentImageNumber As Integer Private _retrieveData As Boolean Public Event PictureRetrieved As EventHandler(Of TAsyncClientEventArgs) Public Sub New() _retrieveData = False _currentImageNumber = 1 Dim timerInterval As Double = 33.3 ' 30pics/sec 'Dim timerInterval As Double = 10 _timer = New System.Timers.Timer _timer.Enabled = False _timer.Interval = timerInterval AddHandler _timer.Elapsed, AddressOf timer_elapsed End Sub ''' <summary> ''' Handles the timer tick event. ''' </summary> ''' <param name="sender"></param> ''' <param name="e"></param> ''' <remarks></remarks> Private Sub timer_elapsed(ByVal sender As Object, ByVal e As EventArgs) GetData() End Sub Public Sub StartGettingPictures() _retrieveData = True GetData() If _timer.Enabled = False Then _timer.Enabled = True _timer.Start() End If End Sub Public Sub StopGettingPictures() _retrieveData = False _timer.Enabled = False _timer.Stop() End Sub Public ReadOnly Property IsGettingPictures() Get Return _retrieveData End Get End Property ''' <summary> ''' Returns the data to be sent. ''' </summary> ''' <remarks></remarks> Private Sub GetData() 'While _retrieveData 'Loading the policy file into memory Dim appExecutionPath As String = System.Reflection.Assembly.GetEntryAssembly().Location Dim path As String = appExecutionPath.Remove(appExecutionPath.LastIndexOf("\"), appExecutionPath.Length - appExecutionPath.LastIndexOf("\")) Dim pathToPicture As String = path + "\Images\" + _currentImageNumber.ToString + ".JPG" 'the path to the policy file _currentImageNumber += 1 If _currentImageNumber > 24 Then _currentImageNumber = 1 End If Try Dim fs As New FileStream(pathToPicture, FileMode.Open) Dim picture(fs.Length) As Byte fs.Read(picture, 0, picture.Length) fs.Close() RaiseEvent PictureRetrieved(Me, New TAsyncClientEventArgs(picture)) Catch ex As Exception Console.WriteLine(ex.Message + Environment.NewLine + ex.StackTrace) End Try 'End While End Sub End Class
Code:
Public Class TAsyncClientEventArgs Inherits EventArgs Private _pushData() As Byte 'will contain the byte array of data to push to the client ''' <summary> ''' Gets or sets the byte array of data to push to the client. ''' </summary> ''' <value>The byte array used as data to push to the client.</value> ''' <returns>The byte array used as data to push to the client.</returns> ''' <remarks></remarks> Public Property PushData() As Byte() Get Return _pushData End Get Set(ByVal value As Byte()) _pushData = value End Set End Property ''' <summary> ''' Initializes a new instance of the TAsyncClientEventArgs. ''' </summary> ''' <param name="pushData"></param> ''' <remarks></remarks> Public Sub New(ByVal pushData As Byte()) _pushData = pushData End Sub End Class
Code:
Imports System.Threading Module Module1 Sub Main() Console.WriteLine("Picture Pusher Server Starting") Dim ps As New PusherServer System.Threading.Thread.Sleep(System.Threading.Timeout.Infinite) End Sub End Module