December 7, 2007

TCP Socket War (C# and TcpListener)

I have just spent the better part of two days fighting a socket bug that turned out to be a simple fix. Isn't it funny how the time consuming ones always turn out to be something simple? I have been using .Net's TcpListener to create a server/client applications. The advantage of using the TcpListener is that it does all of the standard socket setup for you. You also still have access to the socket and the network stream that is used in case you still need the extra power that comes with manipulating them yourself. The way I set this thing up is by having both the client and server loop forever to listen for incoming data. This is done using BeginRead(...) on the network stream.

myTcpClient.GetStream().BeginRead(data, 0, size, new AsyncCallback(dataReceived), state);

This will have the app listen on another thread so that I can do my other business. When data is received, the callback method that was passed in will be called. What is cool about this is that you can still send on the socket while you are listening in the background! Gotta love it.

I had been developing my server and my client and all seemed to be going well. Then I decided to put my client on another computer so that i could do more testing. The computer of choice happened to be connected via a wireless connection. When I started up my client application, my client started to break! Further investigation yielded that my data was being corrupt on the way to my client (or so I thought). It made no sense that my data was getting corrupt because TCP is made to assure that all of your data arrives, that it is the right data, and that it arrives in order. I would later discover that if I sent enough data in a single send, that my client would also break on a wired connection... even if both the client and server were on the localhost. I tried everything I could think of to fix this problem, but nothing seemed to be working out. I even tried breaking my data up into smaller packets with no luck.

Well after wasting the better part of two days on this, I was doing yet another debug session to try to figure things out. Again, I realized that all of my data was not arriving at the client. For some reason, this time a light bulb went off. You see, I had been operating on the assumption that since I was using an asynchronous read (BeginRead) instead of a synchronous read (Read) that the thread would not return until either an error had occurred or until 'size' number of bytes were read. I don't feel so bad because I had others tell me that this is how it works too. Don't expect the MS documentation to spell this out for you either. The truth of the matter is that because I was working on my local network with technically small amounts of data, I was receiving all of it before BeginRead was returning... that is until I switched to a slower wireless connection.

The solution is simple. All you have to do is check the number of bytes received and see if you got the number of bytes you were expecting. If you didn't then you will have to call BeginRead again and pass the number of remaining expected bytes in as your size. You get the number of bytes read when you call EndReceive.

int readBytes = myTcpClient.GetStream().EndRead(callbackAsyncResult);

This is basically the same technique as used when you implement synchronous reads. I was also using an abnormal technique to get my data, which probably helped delay my figuring this out. The way my app works is that I take a serializable object and serialize it with BinaryFormatter. I then take the length of the byte array produced and convert it to bytes with BitConverter. I then append the length bytes to the front of my object's bytes. This new byte array is what I actually send over my TCP socket stream. When the data arrives on the other side, I immediately pop off 4 bytes (the size of an integer) so that I get my length. I then create a buffer that is the size of my length and read in length bytes on my next call to BeginRead. Typically people have a fixed buffer size and read continuously until they get their expected length. I guess thats what I get for doing things my own way ;-)

Boy do I feel better!

No comments: