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.
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 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 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.
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.