
Introduction
This example project contains four classes - TcpCommServer, TcpCommClient, clsAsyncUnbuffWriter and CpuMonitor. With these classes, you will not only be able to instantly
add TCP/IP functionality to your VB.NET applications, but also has most of the bells and whistles we're all looking for. With these classes, you will be able
to connect multiple clients to the server on the same port. You will be able to easily: throttle bandwidth to the clients, and send and receive files and data (text?)
along 250 provided channels simultaneously on a single connection.
Background
When I first started looking for information or example code for TCP/IP communication in VB.NET, I have to admit I was looking for it all. I was willing
to accept the bits I needed to get started, but I was hoping, like you probably are, that I would find it all... some easily transportable classes that would
allow me to do what I think most of us want when we first start looking into this - send and receive files and our own data or text, all over a single connection,
simultaneously. It's what we'll all need eventually, for one project or another, when the time comes to build a client/server application. We'll also need to control
the bandwidth at the server, or someone will use it all up downloading a large file... And oh - it would be nice if we could connect to the server on the same port from multiple clients, right?
I looked for a long time and never found anything right out of the box that would do what I needed, so I decided to build it myself. Along the way I began to understand
why people don't just freely give this kind of work away. It took me quite a bit of time coding, troubleshooting, and testing this - but it is, after all, just a tool.
It's what we do with it that will be worth something in the end.
So here it is.
Channels
We want to be able to do everything... right? And all over a single connection. You want to be able to send, say, a stream of screenshots,
or video, or text, and not have to code your own way of separating one from another. The channel was my answer. Every time you use the SendBytes method,
you send that data with a channel identifier. When it comes out the other end in the callback, it arrives with the channel identifier you sent it with so you can
tell your screenshot bytes from your text - if that's what you're sending. As I said above, you have 250 channels at your disposal (1 to 251).
OK, on to some code.
Using the code
First of all, both classes use delegates to do callbacks. All you have to do is pass the AddressOf YourCallbackSub while instantiating these classes,
and then call Start() or Connect(). I.e.:
Dim _Server As New tcpCommServer(AddressOf YourCallbackSub)
_Server.Start(60000)
Or:
Dim _Client As New tcpCommClient(AddressOf YourCallbackSub)
_Client.Connect("192.168.1.1", 60000)
YourCallbackSub has to have the right signature - you can see the right way to do it in the example project. You can also specify the max bps while
instantiating the server class. The default value is 9MBps.
To send a byte array, use sendBytes(). To get a file from the server, there is a GetFile() method. Simply supply it with the path
of the file on the server (a path local to the server). To send a file to the server, there's a SendFile() method.
Both GetFile and SendFile are handled by the classes without any other code needed by you - but if you'd like to track the progress
of the incoming or outgoing file, you can poll the GetPercentOfFileReceived and GetPercentOfFileSent methods.
These classes communicate with each other, and with you using a limited protocol language. You will be notified in your callback when your bytes
have been sent (using sendBytes), when your file has completed downloading or uploading, if a file transfer has been aborted, and if you receive an error either
locally or from the server. You will receive these messages from your client or server on channel 255. This is a quick look at these messages, and how I handled them
in the example project's client form's callback sub:
Public Sub UpdateUI(ByVal bytes() As Byte, ByVal dataChannel As Integer)
If Me.InvokeRequired() Then
Me.Invoke(_Client.ClientCallbackObject, bytes, dataChannel)
Else
Dim dontReport As Boolean = False
If dataChannel < 251 Then
Me.ListBox1.Items.Add(BytesToString(bytes))
ElseIf dataChannel = 255 Then
Dim msg As String = BytesToString(bytes)
Dim tmp As String = ""
If msg.Length > 15 Then tmp = msg.Substring(0, 15)
If tmp = "Receiving file:" Then
gbGetFilePregress.Text = "Receiving: " & _Client.GetIncomingFileName
dontReport = True
End If
If msg.Length > 13 Then tmp = msg.Substring(0, 13)
If tmp = "Sending file:" Then
gbSendFileProgress.Text = "Sending: " & _Client.GetOutgoingFileName
dontReport = True
End If
If msg = "->Done" Then
gbGetFilePregress.Text = "File->Client: Transfer complete."
btGetFile.Text = "Get File"
dontReport = True
End If
If msg = "<-Done" Then
gbSendFileProgress.Text = "File->Server: Transfer complete."
btSendFile.Text = "Send File"
dontReport = True
End If
If msg = "->Aborted." Then
gbGetFilePregress.Text = "File->Client: Transfer aborted."
dontReport = True
End If
If msg = "<-Aborted." Then
gbSendFileProgress.Text = "File->Server: Transfer aborted."
dontReport = True
End If
If msg = "UBS" Then btSendText.Enabled = True
dontReport = True
End If
If msg.Length > 4 Then tmp = msg.Substring(0, 5)
If tmp = "ERR: " Then
Dim msgParts() As String
msgParts = Split(msg, ": ")
MsgBox("" & msgParts(1), MsgBoxStyle.Critical, "Test Tcp Communications App")
dontReport = True
End If
If Not dontReport Then Me.ToolStripStatusLabel1.Text = BytesToString(bytes)
End If
End If
End Sub
Sendbytes will accept any size byte array you hand it. It can only send blockSize bytes at a time though, so if you hand
it a very large byte array, sendBytes will block until it's done sending it. You will want to wait until you receive the "UBS" notice
before handing sendBytes more data.
These classes will work the most efficiently if you only try to send a maximum of blockSize bytes at a time. BlockSize will be different
depending on your current throttle speed, and the server will change the client's blockSize immediately after connection. You can get the current blockSize by calling
the .GetBlocksize() method.
Throttling
Throttling works along a 4K boundary if you set the bandwidth to be less then 1 meg, so you can use the traditionally expected bandwidth limits - i.e.: 64K, 96K, 128K, 256K, etc.
After one meg, it's along a 5K boundary - so you can set it to more intuitive values: 2.5 meg, 5 meg, 9 meg, etc. The server counts all bytes coming in and going out, and checks
to make sure we're not processing more bytes then we should 4 times a second. For those who are interested, this is what the code looks like:
bandwidthUsedThisSecond = session.bytesSentThisSecond + session.bytesRecievedThisSecond
If bandwidthTimer.AddMilliseconds(250) >= Now And bandwidthUsedThisSecond >= (Mbps / 4) Then
While bandwidthTimer.AddMilliseconds(250) > Now
Thread.Sleep(1)
End While
End If
If bandwidthTimer.AddMilliseconds(250) <= Now Then
bandwidthTimer = Now
session.bytesRecievedThisSecond = 0
session.bytesSentThisSecond = 0
bandwidthUsedThisSecond = 0
End If
Throttling will be important for you because these classes will do as much work as you let them - as fast as they can. On my dev machine,
I was able to achieve transfer speeds of almost 300 Mbps copying one large file from the server to the client. But the cost of this was the client and the server's background threads
using 100% of their respective CPU core's resources - not a good scenario for a server. On my dev machine, 9MBps produced almost no perceptible CPU usage.
In a production environment, I'm sure you will want to set this even lower. Ideally, you will want to set the throttling when you instantiate your server.
Dim _Server As New tcpCommServer(AddressOf YourCallbackSub, 9000000)
You can also set the throttled bps using the ThrottleNetworkBps() method.
The CpuMonitor class
Included in this project, you'll find the CpuMonitor class. This class is used to monitor the CPU usage of a thread.
When running the example project, you will see some reporting of CPU usage in the status strip. This is the usage of the client's background thread - the thread actually doing
the work of communicating with the server.
The AsyncUnbuffWriter class
One of the issues I struggled with while testing was the sudden, unexplained loss of bandwidth. For no reason I could see, bandwidth while copying a file (with throttling off) would go from 300 MB / sec to very little after about 2 gig. A little more testing and I discovered that the slowdown was in my hard drive. A little more and I learned that it was related to the O/S Ram cache.
When you think about it, allowing Windows to cache file transfers is a very bad idea, specially for a server. It may provide the illusion of very fast file IO, but it is just an illusion. Too many simultaneous uploads, and your server runs out of available ram cache. Performance degrades... if this continues, the OS will eventually crash.
The solution is of course: unbuffered file IO. We want to leave Windows out of our file IO as much as possible. We also don't want to write to the hard drive every other time bytes arrive in the communications thread - we needed to do this asynchronously.
Microsoft doesn't provide a tool in managed code to do asynchronous unbuffered file IO. Filestream will do one or the other, but not both.
But what exactly is asynchronous file IO? Well, according to the wiki, it is: "...a form of input/output processing that permits other processing to continue before the transmission has finished."
That didn't sound so hard. Lots of people have been trying to get filestream's BeginRead / BeginWrite to do unbuffered IO, with almost no success. I abandoned that approach, and went another direction.
The AsyncUnbuffWriter class creates a second low priority thread thread and puts a filestream on it. When we fill it's external buffer, the filestream thread copies the buffer off for itself, signals that the external buffer is free again, and writes the bytes without letting windows buffer the IO. While the writer is writing the bytes, we're free to fill that external buffer again. The speed you get from this class depends entirely on the size of the buffer you choose. I was able to get sustained 100 MB / Sec file transfers using only one Sata 2 hard drive - but I had to use a very large buffer. Using two drives, the performance should be even better.
Of course, we don't want to use large buffers on the server - so our upload performance will always be limited. I've set the default buffer size in the client to 1 meg x your machine's page size (usually 4096). On my machine, that's 4 meg for the external buffer and 4 meg for the internal buffer - 8 total. This translates to 50 - 60 MB / sec file transfers to the client only. Uploads top out at about 35 MB / sec, as the write buffer is only 256k.
Again - we're talking about server software. We're never going to allow people 35MB file transfers anyway.
Points of interest
One thing worth noting here was an issue I noticed while testing these classes. As you would expect, I tested sending larger and larger files, and more and more connected
clients all sending or receiving large files. At one point, I had one client pulling a >3 gig file while two other clients were also doing largish file transfers,
and my bandwidth seemed to drop to almost nothing after 2 gig had been sent (almost nothing being between 1 and 30 MBps - I was testing without throttling to see this effect).
After much testing, I realized that this is a hardware limitation on my machine. I think it has to do with the hard drive cache. I was able to mitigate this effect
by inflating the filestream's buffers in the FileWriter object in the client for large file transfers to the client... but if you notice this effect on your system,
this is what it is. Yet another reason to throttle bandwidth. - Resolved.
History
- 12/30/11: Corrected a small problem with protocol reporting in
tcpCommServer. Added notification for when sendBytes() has completed sending data. - 6/19/12: Resolved file IO issues. Added the AsyncUnbuffWriter class to the example project. Resolved all warnings and rebuilt the project using Option Strict and Option Explicit.