Implementing a Socket Server to push data to a Silverlight client

Collapse
X
 
  • Time
  • Show
Clear All
new posts
  • Frinavale
    Recognized Expert Expert
    • Oct 2006
    • 9749

    Implementing a Socket Server to push data to a Silverlight client

    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:
    • 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
    Helper:
    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
    EventArgs:
    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
    The console application:
    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
    This needs major modification.
Working...