JavaScript Editor JavaScript Editor     JavaScript Debugger 



Team LiB
Previous Section Next Section

Downloads

The file-downloading process is similar to the file-uploading process. A FileDownloadQueue class creates FileDownloadClient instances to serve new user requests, provided the maximum number of simultaneous downloads hasn't been reached. Download progress information is written directly to the download ListView display, using the thread-safe ListViewItemWrapper. The whole process is diagrammed in Figure 9-9.

Click To expand
Figure 9-9: The downloading process

A download operation begins when a user double-clicks an item in the ListView search results, thereby triggering the ItemActivate event. The form code handles the event, checks that the requested file hasn't already been submitted to the FileDownloadQueue, and then adds it. This code demonstrates another advantage of using GUIDs to uniquely identify all files on the peer-to-peer network: it allows each peer to maintain a history of downloaded files.

The complete code for the ItemActivate event handler is shown here:

Private Sub lstSearchResults_ItemActivate(ByVal sender As Object, _
  ByVal e As System.EventArgs) Handles lstSearchResults.ItemActivate

    ' Retrieve information about the requested file.
    Dim File As SharedFile
    File = CType(CType(sender, ListView).SelectedItems(0).Tag, SharedFile)

    ' Check if the file is already downloaded, or in the process of being
    ' downloaded.
    If App.DownwnloadThread.CheckForFile(File) Then
        MessageBox.Show("You are already downloading this file.", "Error", _
          MessageBoxButtons.OK, MessageBoxIcon.Information)

    ' If you comment-out the following lines, you'll be able to test
    ' FileSwapper with a single active instance and download files
    ' from your own computer.
    ElseIf File.Peer.Guid.ToString() = Global.Identity.Guid.ToString() Then
        MessageBox.Show("This is a local file.", "Error", _
          MessageBoxButtons.OK, MessageBoxIcon.Information)

    Else
        ' Add the file to the download queue.
        App.DownwnloadThread.AddFile(File)

        ' Start the download queue thread if necessary (this is only performed
        ' once, the first time you download a file).
        If Not App.DownwnloadThread.Working Then
            App.DownwnloadThread.StartAllocateWork()
        End If

        ' Switch to the Downloads tab to see progress information.
        tbPages.SelectedTab = tbPages.TabPages(1)
    End If

End Sub

The FileDownloadQueue Class

The FileDownloadQueue tracks and schedules ongoing downloads. When the user requests a file, it's added to the QueuedFiles collection. If the maximum download thread count hasn't yet been reached, the file is removed from this collection and a new FileDownloadClient object is created to serve the request. All active FileDownloadClient objects are tracked in the DownloadThreads collection.

The FileDownloadQueue class creates new FileDownloadClient objects as needed in its private AllocateWork() method, which it executes on a separate thread. The application requests a new download by calling the StartAllocate Work() method, which creates the thread and invokes the AllocateWork() method asynchronously. The Abort() method stops the work allocation. This is the same design you saw with the FileServer class.

Public Class FileDownloadQueue

    ' The thread where downloads are scheduled.
    Private AllocateWorkThread As System.Threading.Thread

    ' The ListView where downloads are tracked.
    Private ListView As ListView

    ' The current state.
    Private _Working As Boolean
    Public ReadOnly Property Working() As Boolean
        Get
            Return _Working
        End Get
    End Property

    ' The collection of files that are waiting to be downloaded.
    Private QueuedFiles As New ArrayList()

    ' The threaded objects that are currently downloading files.
    Private DownloadThreads As New ArrayList()

    Public Sub New(ByVal linkedControl As ListView)
        ListView = linkedControl
    End Sub
    Public Sub StartAllocateWork()
        If _Working Then
            Throw New ApplicationException("Already in progress.")
        Else
            _Working = True

            AllocateWorkThread = New Threading.Thread(AddressOf AllocateWork)
            AllocateWorkThread.Start()
        End If
    End Sub

    Public Sub Abort()
        If _Working Then
            AllocateWorkThread.Abort()

            ' Abort all download threads.
            Dim DownloadThread As FileDownloadClient
            For Each DownloadThread In DownloadThreads
                DownloadThread.Abort()
            Next

            _Working = False
        End If
    End Sub

    Private Sub AllocateWork()
        ' (Code omitted.)
    End Sub
    Public Function CheckForFile(ByVal file As SharedFile) As Boolean
        ' (Code omitted.)
    End Function

    Public Sub AddFile(ByVal file As SharedFile)
        ' (Code omitted.)
    End Sub

End Class

The CheckForFile() method allows the application to verify that a file hasn't been downloaded before and isn't currently being downloaded. The code scans for the QueuedFiles and DownloadThreads collections to be sure.


Public Function CheckForFile(ByVal file As SharedFile) As Boolean

    ' Check the queued files.
    Dim Item As DisplayFile
    For Each Item In QueuedFiles
        If Item.File.Guid.ToString() = file.Guid.ToString() Then Return True
    Next

    ' Check the in-progress downloads.
    Dim DownloadThread As FileDownloadClient
    For Each DownloadThread In DownloadThreads
        If DownloadThread.File.Guid.ToString() = file.Guid.ToString() Then _
          Return True
    Next

    Return False

End Function

If this check succeeds, the AddFile() method is used to queue the file. Locking is used to ensure that no problem occurs if the FileDownloadClient is about to modify the QueuedFiles collection.

Public Sub AddFile(ByVal file As SharedFile)

    ' Add shared file.
    SyncLock QueuedFiles
        QueuedFiles.Add(New DisplayFile(file, ListView))
    End SyncLock

End Sub

The QueuedFile collection stores DisplayFile objects, not SharedFile objects. The DisplayFile object is a simple package that combines a SharedFile instance and a ListViewItemWrapper. The ListViewItemWrapper is used to update the status of the download on screen.

Public Class DisplayFile

    Private _ListViewItem As ListViewItemWrapper
    Private _File As SharedFile
    Public ReadOnly Property File() As SharedFile
        Get
            Return _File
        End Get
    End Property

    Public ReadOnly Property ListViewItem() As ListViewItemWrapper
        Get
            Return _ListViewItem
        End Get
    End Property

    Public Sub New(ByVal file As SharedFile, ByVal linkedControl As ListView)

        _ListViewItem = New ListViewItemWrapper(linkedControl, file.FileName, _
          "Queued")
        _File = file

    End Sub

End Class

As soon as the DisplayFile object is created, the underlying ListViewItem is created and added to the download list. That means that as soon as a download request is selected, it appears in the download status display, with the status "Queued." This differs from the approach used with file uploading, in which the ListViewItem is only created once the connection has been accepted.

The AllocateWork() method performs the real work for the FileDownloadQueue. It begins by scanning the collection for completed items and removing them for the collection. This is a key step, because the FileDownloadQueue relies on the Count property of the DownloadThreads collection to determine how many downloads are currently in progress. When scanning the collection, the code counts backward, which allows it to delete items without changing the index numbering for the remaining items.

Do

    ' Remove completed.
    Dim i As Integer
    For i = DownloadThreads.Count - 1 To 0 Step -1
        Dim DownloadThread As FileDownloadClient
        DownloadThread = CType(DownloadThreads(i), FileDownloadClient)
    If Not DownloadThread.Working Then
        SyncLock DownloadThreads
            DownloadThreads.Remove(DownloadThread)
        End SyncLock
    End If
Next

Next, new FileDownloadClient objects are created while threads are available.

Do While QueuedFiles.Count > 0 And _
DownloadThreads.Count < Global.Settings.MaxDownloadThreads

    ' Create a new FileDownloadClient.
    Dim DownloadThread As New FileDownloadClient(QueuedFiles(0))
    SyncLock DownloadThreads
        DownloadThreads.Add(DownloadThread)
    End SyncLock

    ' Remove the corresponding queued file.

    SyncLock QueuedFiles
        QueuedFiles.RemoveAt(0)
    End SyncLock

    ' Start the download (on a new thread).
    DownloadThread.StartDownload()

Loop

Finally, the thread doing the work allocation is put to sleep for a brief ten seconds, after which it continues through another iteration of the loop.

    Thread.Sleep(TimeSpan.FromSeconds(10))
Loop

The FileDownloadClient Class

The FileDownloadClient uses the same thread-wrapping design as the FileUpload class. The actual file transfer is performed by the Download() method. This method is launched asynchronously when the FileDownloadQueue calls the StartDownload() method, and canceled if the FileDownloadQueue calls Abort(). The current SharedFile and ListViewItem information is tracked using a private DisplayFile property.

Here's the basic structure:

Public Class FileDownloadClient

    ' The thread where the file download is performed.
    Private DownloadThread As System.Threading.Thread

    ' The current state.
    Private _Working As Boolean
    Public ReadOnly Property Working() As Boolean
        Get
            Return _Working
        End Get
    End Property

    ' The SharedFile and ListViewItem used for this download.
    Private DisplayFile As DisplayFile
    Public ReadOnly Property File() As SharedFile
        Get
            Return DisplayFile.File
        End Get
    End Property

    Public Sub New(ByVal file As DisplayFile)
        Me.DisplayFile = file
    End Sub

    ' The TCP/IP connection used to make the request.
    Private Client As TcpClient

    Public Sub StartDownload()
        If _Working Then
            Throw New ApplicationException("Already in progress.")
        Else
            _Working = True
            DownloadThread = New Threading.Thread(AddressOf Download)
            DownloadThread.Start()
        End If
    End Sub
    Public Sub Abort()
        If _Working Then
            Client.Close()
            DownloadThread.Abort()
            _Working = False
        End If
    End Sub

    Private Sub Download()
        ' (Code omitted.)
    End Sub

End Class

The Download() method code is lengthy, but straightforward. At first, the client attempts to connect with the remote peer by opening a TCP/IP connection to the indicated port and IP address. To simplify the code, no error handling is shown (although it is included with the online code).

DisplayFile.ListViewItem.ChangeStatus("Connecting...")

' Connect.
Dim Completed As Boolean = False

Do
    Client = New TcpClient()
    Dim Host As IPHostEntry = Dns.GetHostByAddress(DisplayFile.File.Peer.IP)
    Client.Connect(Host.AddressList(0), Val(DisplayFile.File.Peer.Port))

The next step is to define a new BinaryReader and BinaryWriter for the stream and check if the connection succeeded. If the connection doesn't succeed, the thread will sleep for ten seconds and the connection will be reattempted in a loop.

    Dim r As New BinaryReader(Client.GetStream())
    Dim w As New BinaryWriter(Client.GetStream())

    Dim Response As String = r.ReadString()
    If Response = Messages.Busy Then
        DisplayFile.ListViewItem.ChangeStatus("Busy - Will Retry")
        Client.Close()
    ElseIf Response = Messages.Ok Then
        DisplayFile.ListViewItem.ChangeStatus("Connected")

        ' (Download file here.)

    Else
        DisplayFile.ListViewItem.ChangeStatus("Error - Will Retry")
        Client.Close()

    End If

    If Not Completed Then Thread.Sleep(TimeSpan.FromSeconds(10))
Loop Until Completed

_Working = False

The actual file download is a multiple step affair. The first task is to request the file using its GUID.

' Request file.
w.Write(DisplayFile.File.Guid.ToString())

The server will then respond with the number of bytes for the file, or an error code if the file isn't found. Assuming no error is encountered, the FileSwapper will create a temporary file. Its name will be the GUID plus the extension .tmp.

Dim TotalBytes As Integer = r.ReadInt32()
If TotalBytes = Messages.FileNotFound Then
    DisplayFile.ListViewItem.ChangeStatus("File Not Found")

Else
    ' Write temporary file.
    Dim FullPath As String = Path.Combine(Global.Settings.SharePath, _
      File.Guid.ToString() & ".tmp")
    Dim Download As New FileInfo(FullPath)

The file transfer takes place 1KB at a time. The status for the in-progress download will be updated using the ListViewItem wrapper, no more than once per second.


    Dim TotalBytesRead, BytesRead As Integer

    Dim fs As FileStream = Download.Create()
    Dim Buffer(1024) As Byte
    Dim Percent As Single
    Dim LastWrite As DateTime = DateTime.Now
    Do
        ' Read a chunk of bytes.
        BytesRead = r.Read(Buffer, 0, Buffer.Length)
        fs.Write(Buffer, 0, BytesRead)
        TotalBytesRead += BytesRead

        ' Update the display once every second.
        If DateTime.Now.Subtract(LastWrite).TotalSeconds > 1 Then
            Percent = Math.Round((TotalBytesRead / TotalBytes) * 100, 0)
            LastWrite = DateTime.Now
            DisplayFile.ListViewItem.ChangeStatus( _
              Percent.ToString() & "% transferred")
        End If
    Loop While BytesRead > 0

When the file transfer is complete, the file must be renamed. The new name will be the same as the file name on the remote peer. However, special care is needed to handle duplicate file names. Before attempting the rename, the code checks for a name collision and adds a number (1, 2, 3, 4, and so on) to the file name to ensure uniqueness.

    fs.Close()

    ' Ensure that a unique name is chosen.
    Dim FileNames() As String = Directory.GetFiles(Global.Settings.SharePath)
    Dim FinalPath As String = Path.Combine(Global.Settings.SharePath, _
      File.FileName)

    Dim i As Integer
    Do While Array.IndexOf(FileNames, FinalPath) <> -1
        i += 1
        FinalPath = Path.Combine(Global.Settings.SharePath, _
          Path.GetFileNameWithoutExtension(File.FileName) & i.ToString() & _
           Path.GetExtension(File.FileName))
    Loop

    ' Rename file.
    System.IO.File.Move(FullPath, FinalPath)
    DisplayFile.ListViewItem.ChangeStatus("Completed")
End If

Client.Close()
Completed = True

Currently, the code doesn't add the newly downloaded file to the App.Shared Files collection, and it doesn't contact the discovery service to add it to the published catalog of files. However, you could easily add this code.

Figure 9-10 shows the upload status list with six entries. Two downloads are in progress while four are queued, because the maximum download thread count has been reached.

Click To expand
Figure 9-10: FileSwapper downloads

Team LiB
Previous Section Next Section


JavaScript Editor Free JavaScript Editor     JavaScript Editor


R7