When TCP Goes Bad





When TCP Goes Bad

The simple example programs studied so far in this chapter assumed a lot when sending and receiving data across the TCP channel. There are, however, plenty of possible glitches that weren’t taken into consideration. In terms of connection-oriented client and server programs, the two problems that hit novice network programmers most often are as follows:

  • Improper data buffer handling in programs

  • Improper message handling across the network

Thankfully, each of these situations can be overcome with proper programming techniques.

Problems with Data Buffers

In the simple client and server samples shown, a byte array was used as a data buffer for sending and receiving data in the socket. Because the programs were operating in a controlled environment, and all of the messages were known to be a controlled size (small) as well as a controlled format (text), this kind of buffer was not a problem.

In the real world, however, you may not know the size or type of the incoming data when you’re communicating with unknown remote servers or clients. What happens in the data buffer as messages of various sizes are received? How about when more data arrives than what you have defined in the data buffer? The following sections offer answers to these important questions.

Properly Using the Data Buffer

As data is received via TCP, it is stored in an internal buffer on the system. Each call to the Receive() method attempts to remove data from that TCP buffer. The amount of data read by the Receive() method is controlled by either of these factors:

  • The size of the data buffer supplied to the Receive() method

  • The buffer size parameter supplied to the Receive() method

In the SimpleTcpSrvr.cs program (Listing 5.1), the data variable was defined as a byte array with 1024 elements:

byte[] data = new byte[1024];

The simple form of the Receive() method was used to specify the buffer to store the incoming data. Because no data size was specified in the Receive() call, the buffer size was automatically set to the default size of the defined data buffer, 1024 bytes.

The Receive() method attempts to read 1024 bytes of data at a time and place it in the data variable:

recv = client.Receive(data);

At the time the Receive() method is called, if the TCP buffer contains less than 1024 bytes of data, the method will return with whatever data is available and set the recv variable to the amount of data actually read. This value is important because it represents the amount of valid data in the data buffer. You must always use it when referencing the data in the data buffer, or you may get surprised.

For example, the following command creates a string object using the first recv bytes of the data array:

stringData = Encoding.ASCII.GetString(data, 0, recv);

This is the proper way to reference the data in the buffer. The GetString() method only uses the number of bytes most recently placed in the buffer. If a larger data buffer were received before this Receive() method call, and you did not use the recv value to create the string, the extra bytes would be appended to the string, producing an incorrect data value.

You can see this problem occur if you remove the last two parameters of the GetString() method in the SimpleTcpClient.cs program and send varying sizes of strings to the server. The output from the SimpleTcpClient program will look like this (I removed the extra blank lines to shorten the output):

C:\>SimpleTcpClient
Welcome to my test server
test
testome to my test server
a bad test
a bad test my test server
exit
Disconnecting from server...
C:\>

Because the data buffer size was not defined in the GetString() method, the entire data buffer (all 1024 bytes) is displayed for each message, even though the majority of it was not part of the received data. Obviously, this is not a good thing. Without specifying the recv variable to show the amount of valid data, the data buffer will produce erroneous results.

Dealing with Small Buffers

The opposite of having too large a buffer is having one that’s too small. A whole new set of challenges arises if the data buffer used in the Receive() call is too small for the incoming data.

As shown in Figure, the TCP subsystem on Windows contains its own data buffer, used to buffer both incoming and outgoing data. This is required because TCP must be able to retransmit data at any time. Once the data has been acknowledged, it can safely be removed from the buffer.

Click To expand
Figure: The TCP buffers

The incoming data works the same way. It will stay in the buffer until a Receive() method is used to read it. If the Receive() method does not read all the data in the buffer, the remainder stays there, waiting for another Receive() call. No data will be lost as long as more Receive() methods are used to read the data from the buffer, but you will not get the data chunks you want.

Let’s set up a situation so you can see what happens. Set the data buffer size to a small number, such as 20 bytes, in the SimpleTcpClient.cs program:

byte data = new byte[20];

After recompiling the program, you can run the test. Here’s what you get:

C:\>SimpleTcpClient
Welcome to my test s
test
erver
another test
test
oops
another test
exit
Disconnecting from server...
C:\>

Because the data buffer is not large enough to scoop up all the TCP buffer data in the Receive() method, it takes only what can fit in the buffer and leaves the rest for the next Receive() call. Thus, getting the entire server welcome banner required two calls to Receive(). On the next transmission, the next chunk of data from the buffer was read from the TCP buffer, and from then on the client was behind in the data transmission.

Because of this behavior, you must take care to ensure that all of the data is read from the TCP buffer properly. Too small a buffer can result in mismatched messages; on the other hand, too large a buffer can result in mixed messages. The hard part is trying to distinguish between messages as data is being read from the socket. The next section describes how to deal with message problems and ensure that the data is properly handled.

Problems with TCP Messages

One of the biggest pitfalls for novice network programmers is using message-oriented communications with TCP connections. The most important point to remember about using TCP for network communications is that TCP does not respect message boundaries. This section demonstrates this behavior using a badly written TCP program and some ways to fix it.

A Typical TCP Unprotected Message Boundary

The typical novice network programmer, having just read about the wonderful benefits of TCP programming, proceeds to create a client/server application that passes messages back and forth between two devices on the network using TCP sockets. However, not realizing the inherent disregard of message boundaries suffered by stream protocols such as TCP, the programmer writes a series of Send() methods on the client, along with a corresponding series of Receive() methods on the server. The processing setup is as illustrated in Figure:

Click To expand
Figure: An improper TCP communication technique

The critical drawback to using this way to communicate is that you are not guaranteed that the data from each individual Send() method will be read by each individual Receive() method. As shown in the preceding section on buffer issues, all of the data read by the Receive() method is not actually read directly from the network. Rather, the Receive() method reads data from the TCP buffer internal to the system. As new TCP packets are received from the network, they are placed sequentially into the TCP buffer. When the Receive() method is called, it reads all the data available in the TCP buffer, not just the first packet’s worth. This exact behavior is what is occurring in Figure.

Let’s demonstrate this problem using a couple of defective programs. First, Listing 5.3 is the bad server program.

Listing 5.3: The BadTcpSrvr.cs program
Start example
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
class BadTcpSrvr
{
  public static void Main()
  {
   int recv;
   byte[] data = new byte[1024];
   IPEndPoint ipep = new IPEndPoint(IPAddress.Any, 9050);
   Socket newsock = new Socket(AddressFamily.InterNetwork,
           SocketType.Stream, ProtocolType.Tcp);
   newsock.Bind(ipep);
   newsock.Listen(10);
   Console.WriteLine("Waiting for a client...");
   Socket client = newsock.Accept();
   string welcome = "Welcome to my test server";
   data = Encoding.ASCII.GetBytes(welcome);
   client.Send(data, data.Length,
           SocketFlags.None);
   IPEndPoint newclient = (IPEndPoint)client.RemoteEndPoint;
   Console.WriteLine("Connected with {0} at port {1}",
           newclient.Address, newclient.Port);
   for (int i = 0; i < 5; i++)
   {
     recv = client.Receive(data); 
     Console.WriteLine(Encoding.ASCII.GetString(data, 0, recv));
   }
   Console.WriteLine("Disconnecting from {0}", newclient.Address);
   client.Close();
   newsock.Close();
  }
}
End example

This server program establishes a normal TCP socket to listen for incoming connections. When a connection is received, the server sends out a standard greeting banner message and then attempts to receive five separate messages from the client:

for (int i = 0; i < 5; i++)
{
  recv = client.Receive(data); 
  Console.WriteLine(Encoding.ASCII.GetString(data, 0, recv));
}

Each call to the Receive() method attempts to read all the data available in the TCP buffer. After receiving the fifth message, the connection is closed, along with the original server socket.

Now we’ll examine a bad client program to go along with the bad server program. Listing 5.4, the BadTcpClient.cs program, creates a socket and attempts to connect to the remote server (remember to replace the IP address or hostname shown with that of your server device). After connecting to the remote server, the client uses five separate Send() calls to transmit a text message to the server program.

Listing 5.4: The BadTcpClient.cs program
Start example
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
class BadTcpClient
{
  public static void Main()
  {
   byte[] data = new byte[1024];
   string stringData;
   IPEndPoint ipep = new IPEndPoint(
           IPAddress.Parse("127.0.0.1"), 9050);
   Socket server = new Socket(AddressFamily.InterNetwork,
           SocketType.Stream, ProtocolType.Tcp);
   try
   {
     server.Connect(ipep);
   } catch (SocketException e)
   {
     Console.WriteLine("Unable to connect to server.");
     Console.WriteLine(e.ToString());
     return;
   }
   int recv = server.Receive(data);
   stringData = Encoding.ASCII.GetString(data, 0, recv);
   Console.WriteLine(stringData);
   server.Send(Encoding.ASCII.GetBytes("message 1"));
   server.Send(Encoding.ASCII.GetBytes("message 2"));
   server.Send(Encoding.ASCII.GetBytes("message 3"));
   server.Send(Encoding.ASCII.GetBytes("message 4"));
   server.Send(Encoding.ASCII.GetBytes("message 5"));
   Console.WriteLine("Disconnecting from server...");
   server.Shutdown(SocketShutdown.Both);
   server.Close();
  }
}
End example

Try running these programs multiple times from various clients and servers on the network. The results you will see are unpredictable. It is possible that each Receive() call on the server will read a single message sent by the Send() call on the client, which is what might be expected. But this behavior is not guaranteed. I tried it a few times, with different results. It appears that when the client and server are on the same system, the expected result usually occurs. However, when using the client on a system other than the server’s (as you would assume in a normal network program environment), some very interesting things happen.

Here are two representative results from the server output:

C:\>BadTcpSrvr
Waiting for a client...
Connected with 192.168.1.6 at port 2345
message 1message 2
message 3message 4
message 5
Disconnecting from 192.168.1.6
C:\>BadTcpSrvr
Waiting for a client...
Connected with 192.168.1.6 at port 1065
message 1
message 2message 3message
 4message 5
Disconnecting from 192.168.1.6
C:\>

In the first try, the first server Receive() call picks up the complete messages from the first two client Send() calls. Then, the second server Receive() call gets the next two client Send() calls. Finally, the third server Receive() call grabs the data from the last client Send() call. Once the client disconnects, the remaining server Receive() calls return with no data, and the server properly closes the connection.

The second try is even more interesting. The first server Receive() call picks up a complete message from the first client Send() call—so far, so good. Then, the second server Receive() picks up the next two client Send() calls, plus part of the fourth client Send() call. This is a classic example of a message being split between Receive() calls. Finally, the third server Receive() call gets the rest of the data from the fourth client Send() call, plus the data from the last Send() call. What a mess!

There are two lessons that can be learned from this example:

  • Never test your network programs only on a local client and server and expect those programs to work the same way on a network.

  • Never assume that TCP will send message data in the same way you are employing in your program.

The next section offers some programming tips for getting your data to a remote system via TCP and being able to make sense out of it.

Solving the TCP Message Problem

To solve the unprotected boundary problems of our bad server and bad client programs, you must devise some technique to distinguish between separate messages on the remote system. There are three common ways to distinguish messages sent via TCP:

  • Always send fixed-sized messages

  • Send the message size with each message

  • Use a marker system to separate messages

Each of these techniques offers a way for the remote system to separate out individual messages as they are received across the network. These techniques also have their own set of pros and cons. This section takes you through these techniques and their idiosyncrasies when they’re employed in C# network programs.

Using Fixed-Sized Messages

The easiest but most costly way to solve the TCP message problem is to create a protocol that always transmits messages of a fixed size. By setting all messages to the same size, the receiving TCP program can know without doubt when an entire message has been received from the remote device.

Another advantage to using fixed-size messages is that when multiple messages are received, they can be clearly separated based on the number of bytes in the message. This way, messages received together can be separated with some accuracy. The following sections describe how to implement fixed-size messages for the Send() and Receive() methods.

Sending Fixed-Size Messages

When sending fixed-size messages, you must ensure that the entire message is sent from the Send() method. Don’t assume that the complete message was sent by the TCP system. On a simple Send() method, you might see the following code:

byte[] data = new byte[1024];
.
.
int sent = socket.Send(data);

On the basis of this code, you might be tempted to presume that the entire 1024-byte data buffer was sent to the remote device and go on with your program. But this might be a bad assumption.

Depending on the size of the internal TCP buffer and how much data is being transferred, it is possible that not all of the data supplied to the Send() method was actually sent. The Send() return value sent indicates how many bytes of the data were actually sent to the TCP buffer. It is your job to determine whether all of the data was sent, however. If it wasn’t, it is also your job to try and resend the rest of it. This is often done using a simple while() statement loop, checking the value returned from the Send() method against the original size of the data buffer. Of course, if the size does not match, you must provide a way to send the rest of the data.

The code using this method looks something like this:

int SendData(Socket s, byte[] data)
{
  int total = 0;
  int size = data.Length;
  int dataleft = size;
  int sent;
  while (total < size)
  {
   sent = s.Send(data, total, dataleft, SocketFlags.None);
   total += sent;
   dataleft -= sent;
  }
  return total;
}

Because you know the size of the data buffer before sending it, you can loop through the Send() method until you know that all of the expected bytes have been sent. This code snippet demonstrates how to loop through the Send() method, sending chunks of the data buffer until all of it has been properly sent. Each new call to the Send() method must obviously pick up where the previous call left off. Of course, it is entirely possible that all of the data will be sent on the first attempt, which is also fine.

Here is the more complicated format of the Send() method that does all this:

sent = s.Send(data, total, dataleft);

The first parameter points to the entire data buffer to be sent. The second parameter indicates at what position in the buffer to start sending, and the last parameter indicates how many bytes to attempt to send. Like the plain-vanilla Send() method, this more-capable version returns the total number of bytes sent to the remote device.

Receiving Fixed-Size Messages

As is so with sending data, you are not always guaranteed to receive all of the data message in a single Receive() method call. Looping through the Receive() method can also be done to ensure that you have received the entire amount of data expected. Of course, this works only when you know exactly how much data to expect to receive from the server. The following code snippet demonstrates the ReceiveData() method:

byte[] ReceiveData(Socket s, int size)
  {
   int total = 0;
   int dataleft = size;
   byte[] data = new byte[size];
   int recv;
   while(total < size)
   {
     recv = s.Receive(data, total, dataleft, 0);
     if (recv == 0)
     {
      data = Encoding.ASCII.GetBytes("exit ");
      break;
     }
     total += recv;
     dataleft -= recv;
   }
   return data;
  }

The ReceiveData() method requires the socket to receive data on and the set size of the data messages. In turn, it loops through the Receive() method, reading data until the set number of bytes have been received. Once finished, ReceiveData()can ensure that the proper number of bytes has been received before the message is returned to the calling application.

Testing the Fancy Send() and Receive() Methods

Using the SendData() and ReceiveData() methods is simple. Listing 5.5 presents the FixedTcpSrvr.cs program, in which the earlier bad TCP server example is re-created using the new methods.

Listing 5.5: The FixedTcpSrvr.cs program
Start example
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
class FixedTcpSrvr
{
  private static int SendData(Socket s, byte[] data)
  {
   int total = 0;
   int size = data.Length;
   int dataleft = size;
   int sent;
   while (total < size)
   {
     sent = s.Send(data, total, dataleft, SocketFlags.None);
     total += sent;
     dataleft -= sent;
   }
   return total;
  }
  private static byte[] ReceiveData(Socket s, int size)
  {
   int total = 0;
   int dataleft = size;
   byte[] data = new byte[size];
   int recv;
   while(total < size)
   {
     recv = s.Receive(data, total, dataleft, 0);
     if (recv == 0)
     {
      data = Encoding.ASCII.GetBytes("exit");
      break;
     }
     total += recv;
     dataleft -= recv;
   }
   return data;
  }
  public static void Main()
  {
   byte[] data = new byte[1024];
   IPEndPoint ipep = new IPEndPoint(IPAddress.Any, 9050);
   Socket newsock = new Socket(AddressFamily.InterNetwork,
           SocketType.Stream, ProtocolType.Tcp);
   newsock.Bind(ipep);
   newsock.Listen(10);
   Console.WriteLine("Waiting for a client...");
   Socket client = newsock.Accept();
   IPEndPoint newclient = (IPEndPoint)client.RemoteEndPoint;
   Console.WriteLine("Connected with {0} at port {1}",
           newclient.Address, newclient.Port);
   string welcome = "Welcome to my test server";
   data = Encoding.ASCII.GetBytes(welcome);
   int sent = SendData(client, data);
   for (int i = 0; i < 5; i++)
   {
     data = ReceiveData(client, 9);
     Console.WriteLine(Encoding.ASCII.GetString(data));
   }
   Console.WriteLine("Disconnected from {0}", newclient.Address);
   client.Close();
   newsock.Close();
  }
}
End example

The “fixed” version of the server now incorporates the SendData() and ReceiveData() methods in order to guarantee that the proper number of bytes are sent and received. The section of the program that receives the five data packets from the client now ensures that only 9 bytes of data are received for each message:

for (int i = 0; i < 5; i++)
{
  data = ReceiveData(client, 9);
  Console.WriteLine(Encoding.ASCII.GetString(data));
}

Now that the server is expecting each TCP message to be exactly 9 bytes, you need to create a client that sends only 9-byte messages. Listing 5.6, the FixedTcpClient.cs program, does this.

Listing 5.6: The FixedTcpClient.cs program
Start example
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
class FixedTcpClient
{
  private static int SendData(Socket s, byte[] data)
  {
   int total = 0;
   int size = data.Length;
   int dataleft = size;
   int sent;
   while (total < size)
   {
     sent = s.Send(data, total, dataleft, SocketFlags.None);
     total += sent;
     dataleft -= sent;
   }
   return total;
  }
  private static byte[] ReceiveData(Socket s, int size)
  {
   int total = 0;
   int dataleft = size;
   byte[] data = new byte[size];
   int recv;
   while(total < size)
   {
     recv = s.Receive(data, total, dataleft, 0);
     if (recv == 0)
     {
      data = Encoding.ASCII.GetBytes("exit ");
      break;
     }
     total += recv;
     dataleft -= recv;
   }
   return data;
  }
  public static void Main()
  {
   byte[] data = new byte[1024];
   int sent;
   IPEndPoint ipep = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9050);
   Socket server = new Socket(AddressFamily.InterNetwork,
           SocketType.Stream, ProtocolType.Tcp);
   try
   {
     server.Connect(ipep);
   } catch (SocketException e)
   {
     Console.WriteLine("Unable to connect to server.");
     Console.WriteLine(e.ToString());
     return;
   }
   int recv = server.Receive(data);
   string stringData = Encoding.ASCII.GetString(data, 0, recv);
   Console.WriteLine(stringData);
   sent = SendData(server, Encoding.ASCII.GetBytes("message 1"));
   sent = SendData(server, Encoding.ASCII.GetBytes("message 2"));
   sent = SendData(server, Encoding.ASCII.GetBytes("message 3"));
   sent = SendData(server, Encoding.ASCII.GetBytes("message 4"));
   sent = SendData(server, Encoding.ASCII.GetBytes("message 5"));
   Console.WriteLine("Disconnecting from server...");
   server.Shutdown(SocketShutdown.Both);
   server.Close();
  }
}
End example

With the new version of the client, each message sent to the server is always 9 bytes:

sent = SendData(server, Encoding.ASCII.GetBytes("message 1"), 9);
sent = SendData(server, Encoding.ASCII.GetBytes("message 2"), 9);
sent = SendData(server, Encoding.ASCII.GetBytes("message 3"), 9);
sent = SendData(server, Encoding.ASCII.GetBytes("message 4"), 9);
sent = SendData(server, Encoding.ASCII.GetBytes("message 5"), 9);
Note 

You can test the new FixedTcpSrvr and FixedTcpClient programs with various clients on your network and with various fixed-message sizes (although each program can only use one set message size). Using the new versions of the programs, I experienced no message difficulties, even when running them on separate machines on a network: each message sent by the SendData() method on the client was properly received by the ReceiveData() method on the server.

Using the Message Size

As you have seen in this discussion of TCP problems with message boundaries, using a fixed-size message protocol does solve the problem of separating messages—but it’s an extremely clunky solution. You have to make sure that all of your messages are exactly the same length. This means having to pad shorter messages, which means wasted network bandwidth.

The simple solution to this problem is to allow variable-length messages. The only drawback to that idea is that the remote device must somehow know the size of each of the variable-length messages. This can be accomplished by sending a message size indicator within each message. Determining how to place the message size indicator within the message is somewhat of a problem.

There are many ways to include the message size within the message packet. The simplest way is to create a text representation of the message size and append it to the beginning of the message. For example, as shown in Listing 5.6, message 1 would be 9 bytes long, and it would need the text value 9 placed before the message. This would make the transmitted message look like this:

9message 1

The 9 at the front of the message indicates how many bytes are in the actual message. The receiving device can read the first byte of each message and instantly know how many bytes to read for complete the message.

It is not too hard to see where this scheme falls apart, however. With the solution now in place, both the client and server programs must know how many bytes will be used for the message size text. Obviously, one character would not be able to handle large packets. Using three or four characters would be better but would waste some bytes for smaller packets.

What’s the answer? Instead of using the text representation of the message size, you transmit the actual integer value for the message size. This requires converting the integer size of the packet to a byte array and sending it before the message. The following sections describe how to use this technique to add the message size to a variable-length message.

Sending Variable-Length Messages

The SendVarData() method is a modified version of the SendData() method presented earlier. In SendVarData(), you find the size of the outbound message and add it to the beginning of the message as a 4-byte integer value. By using a set 4-byte value, any size message (up to 65KB) can be accommodated using the same coding scheme. The new method should look like this:

private static int SendVarData(Socket s, byte[] data)
  {
   int total = 0;
   int size = data.Length;
   int dataleft = size;
   int sent;
   byte[] datasize = new byte[4];
   datasize = BitConverter.GetBytes(size);
   sent = s.Send(datasize);
   while (total < size)
   {
     sent = s.Send(data, total, dataleft, SocketFlags.None);
     total += sent;
     dataleft -= sent;
   }
   return total;
  }

The SendVarData() method first converts the integer value of the data size to 4 bytes in a byte array, using the BitConverter class available in the System namespace. The GetBytes() method converts the 32-bit integer value to the 4-byte array. The array is then sent to the remote system. After sending the message size, the method then attempts to send the message itself using the standard technique of looping until it knows that all of the bytes have been sent.

Receiving Variable-Length Messages

The next step is to create a method that can interpret the received message format that includes the message size bytes and the entire message. The ReceiveData() method must be modified to accept 4 bytes of data from the socket and convert them into the integer value that represents the size of the message. After determining the size of the message, the entire message should be read into the data buffer using the standard technique shown in ReceiveData(). The ReceiveVarData() method looks like this:

private static byte[] ReceiveVarData(Socket s)
  {
   int total = 0;
   int recv;
   byte[] datasize = new byte[4];
   recv = s.Receive(datasize, 0, 4, 0);
   int size = BitConverter.ToInt32(datasize);
   int dataleft = size;
   byte[] data = new byte[size];
   while(total < size)
   {
     recv = s.Receive(data, total, dataleft, 0);
     if (recv == 0)
     {
      data = Encoding.ASCII.GetBytes("exit ");
      break;
     }
     total += recv;
     dataleft -= recv;
   }
   return data;
  }

ReceiveVarData()reads the first 4 bytes of the message and converts them to an integer value using the GetInt32() method of the BitConverter class:

recv = s.Receive(datasize, 0, 4, 0);
int size = BitConverter.ToInt32(datasize);

The long version of the Receive() method must be used to ensure that only the first 4 bytes of the message are read from the buffer. Next, the normal Receive() method loop is performed, reading data from the socket until the proper number of bytes indicated by the message size has been read.

Testing the New Methods

Now that both the SendVarData() and ReceiveVarData() methods have been determined, you can modify the bad TCP programs using the new methods. Armed with this new technique of sending and receiving variable-length messages, you can create TCP message applications that are capable of sending large text messages without worrying about messages overlapping. Listing 5.7 presents an example of a TCP server program that uses the SendVarData() and ReceiveVarData() methods.

Listing 5.7: The VarTcpSrvr.cs program
Start example
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
class VarTcpSrvr
{
  private static int SendData(Socket s, byte[] data)
  {
   int total = 0;
   int size = data.Length;
   int dataleft = size;
   int sent;
   byte[] datasize = new byte[4];
   datasize = BitConverter.GetBytes(size);
   sent = s.Send(datasize);
   while (total < size)
   {
     sent = s.Send(data, total, dataleft, SocketFlags.None);
     total += sent;
     dataleft -= sent;
   }
   return total;
  }
  private static byte[] ReceiveVarData(Socket s)
  {
   int total = 0;
   int recv;
   byte[] datasize = new byte[4];
   recv = s.Receive(datasize, 0, 4, 0);
   int size = BitConverter.ToInt32(datasize, 0);
   int dataleft = size;
   byte[] data = new byte[size];
   while(total < size)
   {
     recv = s.Receive(data, total, dataleft, 0);
     if (recv == 0)
     {
      data = Encoding.ASCII.GetBytes("exit ");
      break;
     }
     total += recv;
     dataleft -= recv;
   }
   return data;
  }
  public static void Main()
  {
   byte[] data = new byte[1024];
   IPEndPoint ipep = new IPEndPoint(IPAddress.Any, 9050);
   Socket newsock = new Socket(AddressFamily.InterNetwork,
           SocketType.Stream, ProtocolType.Tcp);
   newsock.Bind(ipep);
   newsock.Listen(10);
   Console.WriteLine("Waiting for a client...");
   Socket client = newsock.Accept();
   IPEndPoint newclient = (IPEndPoint)client.RemoteEndPoint;
   Console.WriteLine("Connected with {0} at port {1}",
           newclient.Address, newclient.Port);
   string welcome = "Welcome to my test server";
   data = Encoding.ASCII.GetBytes(welcome);
   int sent = SendVarData(client, data);
   for (int i = 0; i < 5; i++)
   {
     data = ReceiveVarData(client);
     Console.WriteLine(Encoding.ASCII.GetString(data));
   }
   Console.WriteLine("Disconnected from {0}", newclient.Address);
   client.Close();
   newsock.Close();
  }
}
End example

This program performs the same functionality as the original BadTcpSrvr.cs program (Listing 5.3): it sends out a banner message when a client connects and then attempts to accept five separate messages from the client before closing the connection. Of course, the difference between the bad program and VarTcpSrvr.cs is that it expects to receive each message using the new message format, which adds the 4-byte message size to the beginning of each message.

Because of the new message format, you cannot use just any TCP client program to communicate with this server. Instead, you need a special client program that can send the message size as well as the text message, using the new format. A TCP client counterpart program is shown in Listing 5.8. The client program uses the new SendVarData() method to create messages formatted with the message size to send variable length messages to the server. Once again, if you are testing this program on a network, don’t forget to replace the loopback IP address with the appropriate IP address of your server machine.

Listing 5.8: The VarTcpClient.cs program
Start example
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
class VarTcpClient
{
  private static int SendVarData(Socket s, byte[] data)
  {
   int total = 0;
   int size = data.Length;
   int dataleft = size;
   int sent;
   byte[] datasize = new byte[4];
   datasize = BitConverter.GetBytes(size);
   sent = s.Send(datasize);
   while (total < size)
   {
     sent = s.Send(data, total, dataleft, SocketFlags.None);
     total += sent;
     dataleft -= sent;
   }
   return total;
  }
  private static byte[] ReceiveVarData(Socket s)
  {
   int total = 0;
   int recv;
   byte[] datasize = new byte[4];
   recv = s.Receive(datasize, 0, 4, 0);
   int size = BitConverter.ToInt32(datasize, 0);
   int dataleft = size;
   byte[] data = new byte[size];
   while(total < size)
   {
     recv = s.Receive(data, total, dataleft, 0);
     if (recv == 0)
     {
      data = Encoding.ASCII.GetBytes("exit ");
      break;
     }
     total += recv;
     dataleft -= recv;
   }
   return data;
  }
  public static void Main()
  {
   byte[] data = new byte[1024];
   int sent;
   IPEndPoint ipep = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9050);
   Socket server = new Socket(AddressFamily.InterNetwork,
           SocketType.Stream, ProtocolType.Tcp);
   try
   {
     server.Connect(ipep);
   } catch (SocketException e)
   {
     Console.WriteLine("Unable to connect to server.");
     Console.WriteLine(e.ToString());
     return;
   }
   data = ReceiveVarData(server);
   string stringData = Encoding.ASCII.GetString(data);
   Console.WriteLine(stringData);
   string message1 = "This is the first test";
   string message2 = "A short test";
   string message3 = "This string is an even longer test. The quick brown Â_     fox jumps over the lazy dog.";
   string message4 = "a";
   string message5 = "The last test";
   sent = SendVarData(server, Encoding.ASCII.GetBytes(message1));
   sent = SendVarData(server, Encoding.ASCII.GetBytes(message2));
   sent = SendVarData(server, Encoding.ASCII.GetBytes(message3));
   sent = SendVarData(server, Encoding.ASCII.GetBytes(message4));
   sent = SendVarData(server, Encoding.ASCII.GetBytes(message5));
   Console.WriteLine("Disconnecting from server...");
   server.Shutdown(SocketShutdown.Both);
   server.Close();
  }
}
End example

You can test the new client and server programs by starting VarTcpSrvr and then starting VarTcpClient in either a separate command prompt window, or on a separate machine on the network. The server program should output the appropriate messages:

C:\>VarTcpSrvr
Waiting for a client...
Connected with 127.0.0.1 at port 1044
This is the first test
A short test
This string is an even longer test. The quick brown fox jumps over the lazy dog.
a
The last test
Disconnected from 127.0.0.1
C:\>

You now have a complete TCP client and server program that can accurately send text messages back and forth between a client and a server.

Warning 

When using the BitConverter() methods, the integer values are converted to a byte array based on the byte order of the local machine. As long as the client and server are both Intel-based Windows machines, that will work fine. If one of them is from another type of system that uses a different byte order, you can run into difficulty. Chapter 7, "Using The C# Socket Helper Classes," discusses this problem and how to solve it.

Using Message Markers

Still another way to improve control when sending TCP messages is a message marker system. This system separates each message by a predetermined character (or characters) to specify the end of the message. As messages are received from the socket, the data is checked character-by-character for the occurrence of the marker character(s). When a marker is detected, the preceding data is known to contain a complete message and is passed to the application as a message. The data following the marker is tagged as the start of a new message.

One of the drawbacks to using message markers is that each character received in the data stream must be checked to determine if it is the marker. For large messages, this could greatly slow down performance of the system. Also, it means that some character must be designated as a marker, and that character has to be restricted from the normal data (or things get really complicated).

Instead of creating your own message marker system, The C# language provides some classes that can be used to simplify the process. The next section describes how to use the System.IO namespace stream classes to easily read and write text messages in a TCP connection.


 Python   SQL   Java   php   Perl 
 game development   web development   internet   *nix   graphics   hardware 
 telecommunications   C++ 
 Flash   Active Directory   Windows