The file uploading and downloading logic represents the heart of the FileSwapper application. The user needs the ability not only to perform both of these operations at the same time, but also to serve multiple upload requests or download multiple files in parallel. To accommodate this requirement, we must use a two-stage design, in which one class is responsible for creating new upload or download objects as needed. In the case of an upload, this is the FileServer class. The FileServer waits for requests and creates a FileUpload object for each new file upload. The diagrams in Figure 9-7 show how the FileServer and FileUpload classes interact.
The FileServer class listens for connection requests on the defined port using a TcpListener. It follows the same pattern as the asynchronous Search class:
The thread used to monitor the port is stored in a private member variable.
The thread is created with a call to StartWaitForRequest(), and aborted with a call to Abort(). The actual monitoring code exists in the WaitForRequest() method.
The ListView that tracks uploads is stored in a private member variable.
This framework is shown in the following code listing. One of the differences you'll notice is that an additional member variable is used to track individual upload threads. The Abort() method doesn't just stop the thread that's waiting for connection requests—it also aborts all the threads that are currently transferring files.
Public Class FileServer ' The thread where the port is being monitored. Private WaitForRequestThread As System.Threading.Thread ' The TcpListener used to monitor the port. Private Listener As TcpListener ' The ListView that tracks current uploads. 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 threads that are allocated to transfer files. Private UploadThreads As New ArrayList() Public Sub New(ByVal linkedControl As ListView) ListView = linkedControl End Sub Public Sub StartWaitForRequest() If _Working Then Throw New ApplicationException("Already in progress.") Else _Working = True WaitForRequestThread = New Threading.Thread(AddressOf WaitForRequest) WaitForRequestThread.Start() End If End Sub Public Sub Abort() If _Working Then Listener.Stop() WaitForRequestThread.Abort() ' Abort all upload threads. Dim UploadThread As FileUpload For Each UploadThread In UploadThreads UploadThread.Abort() Next _Working = False End If End Sub Public Sub WaitForRequest() ' (Code omitted.) End Sub End Class
The WaitForRequest() method contains some more interesting code. First, it instantiates a TcpListener object and invokes the AcceptTcpClient() method, blocking the thread until it receives a connection request. Once a connection request is received, the code creates a new FileUpload object, starts it, and adds the FileUpload object to the UploadThreads collection.
The WaitForRequest() code doesn't create threads indiscriminately, however. Instead, it examines the Global.MaxUploadThreads setting to determine how many upload threads can exist at any one time. If there's already that number of items in the UploadThreads collection, new requests will receive a busy message instructing them to try again later. The connection will be closed and no new FileUpload object will be created. To ensure that the server is always ready to serve new clients, it automatically scans the UploadThreads collection for objects that have finished processing every time it receives a request. Once it removes these, it decides whether the new request can be accommodated.
.NET is quite efficient when destroying and creating new threads. However, you could optimize performance even further by reusing upload and download threads and maintaining a thread pool, rather than by creating new threads. One way to do this is to use the ThreadPool class that was introduced in Chapter 5.
Public Sub WaitForRequest() Listener = New TcpListener(Global.Settings.Port) Listener.Start() Do ' Block until connection received. Dim Client As TcpClient = Listener.AcceptTcpClient() ' Check for completed requests. ' This will free up space for new requests. Dim UploadThread As FileUpload Dim i As Integer For i = (UploadThreads.Count - 1) To 0 Step -1 UploadThread = CType(UploadThreads(i), FileUpload) If UploadThread.Working = False Then UploadThreads.Remove(UploadThread) End If Next Try Dim s As NetworkStream = Client.GetStream() Dim w As New BinaryWriter(s) If UploadThreads.Count > Global.Settings.MaxUploadThreads Then w.Write(Messages.Busy) s.Close() Else w.Write(Messages.Ok) Dim Upload As New FileUpload(s, ListView) UploadThreads.Add(Upload) Upload.StartUpload() End If Catch Err As Exception ' Errors are logged for future reference, but ignored, so that the ' peer can continue serving clients. Trace.Write(Err.ToString()) End Try Loop End Sub
FileSwapper peers communicate using simple string messages. A peer requests a file for downloading by submitting its GUID. The server responds with a string "OK" or "BUSY" depending on its state. These values are written to the stream using the BinaryWriter. To ensure that the correct values are always used, they aren't hard-coded in the WaitForRequest() method, but defined as constants in a class named Messages. As you can see from the following code listing, FileSwapper peers only support a very limited vocabulary.
Public Class Messages ' The server will respond to the request. Public Const Ok = "OK" ' The server has reached its upload limit. Try again later. Public Const Busy = "BUSY" ' The requested file isn't in the shared collection. Public Const FileNotFound = -1 End Class
The FileUpload class uses the same thread-wrapping design as the FileServer and Search classes. The actual file transfer is performed by the Upload() method. This method is launched asynchronously when the FileServer calls the StartUpload() method and canceled if the FileServer calls Abort(). A reference is maintained to the ListView control with the upload listings in order to provide real-time progress information.
Public Class FileUpload ' The thread where the file transfer takes place. Private UploadThread As System.Threading.Thread ' The underlying network stream. Private Stream As NetworkStream ' The current state. Private _Working As Boolean Public ReadOnly Property Working() As Boolean Get Return _Working End Get End Property ' The ListView where results are recorded. Private ListView As ListView Public Sub New(ByVal stream As NetworkStream, ByVal listView As ListView) Me.Stream = stream Me.ListView = listView End Sub Public Sub StartUpload() If _Working Then Throw New ApplicationException("Already in progress.") Else _Working = True UploadThread = New Threading.Thread(AddressOf Upload) UploadThread.Start() End If End Sub Public Sub Abort() If _Working Then UploadThread.Abort() _Working = False End If End Sub Private Sub Upload() ' (Code omitted) End Sub End Class
We'll dissect the code in the Upload() method piece by piece. The first task the Upload() method undertakes is to create a BinaryWriter and BinaryReader for the stream, and then it reads the GUID of the requested file into a string.
' Connect. Dim w As New BinaryWriter(Stream) Dim r As New BinaryReader(Stream) ' Read file request. Dim FileRequest As String = r.ReadString()
It then walks through the collection of shared files, until it finds the matching GUID.
Dim File As SharedFile Dim Filename For Each File In Global.SharedFiles If File.Guid.ToString() = FileRequest Then Filename = File.FileName Exit For End If Next
Download requests use a GUID instead of a file name. This design allows you to enhance the FileSwapper program to allow sharing in multiple directories, in which case the file name may no longer be unique. The GUID approach also makes it easy to validate a user request before starting a transfer. This is a key step, which prevents a malicious client from trying to trick a FileSwapper peer into downloading a sensitive file that it isn't sharing.
' Check file is shared. If Filename = "" Then w.Write(Messages.FileNotFound)
If the file is found, a new ListViewItem is added to the upload display, using a helper class named ListViewItemWrapper. The ListViewItemWrapper handles the logic needed to create the ListViewItem and change the status text in a thread-safe manner, by marshaling these operations to the correct thread.
Else ' Create ListView. Dim ListViewItem As New ListViewItemWrapper(ListView, Filename, _ "Initializing")
The next step is to open the file and write the file size (in bytes) to the network stream. This information allows the remote peer to determine progress information while downloading the file.
Try ' Open file. Dim Upload As New FileInfo(Path.Combine(Global.Settings.SharePath, _ Filename)) ' Read file. Dim TotalBytes As Integer = Upload.Length w.Write(TotalBytes)
Next, the file is opened, and the data is written to the network stream 1KB at a time. The ListViewItem.ChangeStatus method is used to update the status display in the loop, but a time limit is used to ensure that no more than one update is made every second. This reduces on-screen flicker for fast downloads.
Dim TotalBytesRead, BytesRead As Integer Dim fs As FileStream = Upload.OpenRead() Dim Buffer(1024) As Byte Dim Percent As Single Dim LastWrite As DateTime = DateTime.MinValue Do ' Write a chunk of bytes. BytesRead = fs.Read(Buffer, 0, Buffer.Length) w.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 ListViewItem.ChangeStatus(Percent.ToString() & "% transferred") End If Loop While BytesRead > 0 fs.Close() ListViewItem.ChangeStatus("Completed") Catch Err As Exception Trace.Write(Err.ToString) ListViewItem.ChangeStatus("Error") End Try End If Stream.Close() _Working = False
In this case, the client simply disconnects when it stops receiving data and notices that the connection has been severed. Alternatively, you could use a special signal (such as a specific byte sequence) to indicate that the file is complete or, more practically, you could precede every 1KB chunk with an additional byte describing the status (last chunk, more to come, and so on). The client would have to remove this byte before writing the data to the file.
Figure 9-8 shows the upload status list with three entries. Two uploads have completed, while one is in progress.