Click here to Skip to main content
Click here to Skip to main content

Testing TCP and UDP socket servers using C# and .NET

, 20 Jul 2002 CPOL
Rate this:
Please Sign up or sign in to vote.
When you're developing a TCP/IP server application it's easy to test it poorly. In this article we develop a test framework that does most of the hard work for you.

The following source was built using Visual Studio .NET. The pre-compiled binary release should run on any system which has the .NET framework installed.

Overview

When you're developing a TCP/IP server application it's easy to test it poorly. It's easy to fire requests into a server, check the responses and assume that's enough. Even if you're testing using the actual production client application you may find that you are failing to fully test the server under heavy load or unusual network conditions. You may be using two or more machines, but your development network probably doesn't cause the kind of packet fragmentation and delays that you might encounter in the wild. Often when testing in the development environment your server only ever receives complete, distinct messages, and this can lead novice developers to assume that this is how it always is. As we pointed out in a previous article, the server developer is always responsible for breaking up the TCP byte stream into protocol specific chunks.

Luckily it's quite easy to create a generic TCP/IP test framework that you can then plug protocol specific knowledge into. The framework can ensure that the byte stream being sent to the server suffers from fragmented messages, multiple messages and delays, if appropriate. The protocol specific knowledge can then test the server at the application level whilst the testing framework tests the server at the byte stream processing level.

We developed this testing framework using C# and .NET because we were liked the way that the .NET framework made it easy to develop socket client code using XML for configuration and allowing easy dynamic loading of plug-ins for protocol specific code. First we'll present the design of the framework, then we'll cover how we implemented this design and finally we'll present plug-in test harnesses that allow you to test all of the servers that we've developed in the other articles in this series.

The conversation - an abstract protocol

Client/server systems use many different protocols to communicate. Our challenge, in writing a protocol independent test, tool is to determine the features common to all protocols and write our test tool in terms of those features. By thinking about how protocols are put together in an abstract sense we can provide all of the facilities that any protocol could require. To anchor these abstract concepts in our design we need to name them.

Client/server systems communicate by sending sequences of bytes to each other. Each distinct sequence of bytes is a message. Testing such systems involves sending sequences of messages from peer to peer and ensuring that the reply to the message and the action taken by the peer on receipt of the message is as expected. In the abstract, clients and servers have conversations consisting of one or more exchanges of messages and replies. A test consists of one or more conversations. Messages are sent from the test tool to the program being tested and replies are sent from the program being tested to the test tool. Some conversations may begin with a reply rather than a message and some messages do not generate a reply. In an exchange of messages both the message and the reply are optional.

Interface based programming

Our requirements state that we should be protocol agnostic and support plug-in code to allow the user of the test tool to develop the protocol specific code. We have abstracted all protocols to the concept of a conversation and now need to move that abstract concept nearer to the code. Since the concrete detail will be protocol specific the best way to capture the abstract concepts is to define interfaces that the test tool can use to manipulate the protocol specific plug-in code in terms of the abstract definition of a protocol. The test tool can then work with any code that implements the appropriate interfaces, it's not concerned about the actual classes involved as long as they implement the correct interfaces in the correct way.

Often people find it difficult to move from the abstract design to the slightly more concrete world of interfaces. One of the best ways to do this is to look at the abstract design and note each of the nouns. These are usually a good choice for interfaces. So:

A test consists of one or more conversations consisting of one or more exchanges of messages and replies.

So, we have the following potential interfaces:

  • Test
  • Conversation
  • Exchange
  • Message
  • Reply

We could draw a diagram or two at this point if we felt it would help, something like this, perhaps:

Iterative design

Now that we know the interfaces involved we can start to flesh them out in code. Defining the interfaces will be an iterative thing, it needs to be because we don't know everything about the system we're designing yet. Expect the interfaces to change during the design, expect to add new interfaces and remove ones that don't actually end up adding any value. Also, expect to write code that uses these interfaces whilst they're still in a state of flux, you need to write some code to work out if the interfaces allow you to do what you need to do. Interfaces can and do change whilst you're developing the application, but they should stay fixed once you have released to ensure the compatibility of future releases.

A first attempt at our interfaces could look something like this:

   public interface IMessage
   {
   }

   public interface IReply
   {
   }

   public interface IMessageExchange
   {
      IMessage GetMessage();
      IReply GetReply();
   }

   public interface IConversation
   {
      IMessageExchange GetMessageExchange();
   }

   public interface ISocketServerTest
   {
      IConversation GetConversation();
   }

We've made some assumptions in the interfaces shown above. We assume that you can call GetConversation() multiple times and that, eventually, it will return null to indicate that no further conversations should take place. Likewise we assume that we can call GetMessageExchange() multiple times until all messages exchanges for a conversation have been retrieved. Each MessageExchange object consists of an optional message and an optional reply. If the message exists then GetMessage() returns it, if it doesn't then GetMessage() returns null. Likewise with the reply.

Dealing with TCP and UDP servers

Obviously these interfaces are far from complete. To be able to send the message we need to access it as a sequence of bytes. Processing a reply is more complex. As all good TCP/IP programmers know, TCP presents a byte stream interface. It's up to the application to take data from that byte stream and break it into blocks that are meaningful to the application. This is protocol specific, so must be represented as an abstract concept in our interfaces. It would also be good to be able to support both TCP and UDP with one test tool. UDP works in terms of distinct datagrams rather than a stream. This means that TCP message exchanges consist of a message and a response stream to process and UDP message exchanges consist of a message and a response datagram. Revising our interfaces to cater for these changes gives us something like this:

   public interface IMessage
   {
      byte[] GetAsBytes();
   }

   public interface IMessageExchange
   {
      IMessage GetMessage();
   }

   public interface IResponseStreamHandler
   {
      void HandleResponse(IResponseStream responseStream);
   }   

   public interface ITcpMessageExchange : IMessageExchange
   {
      IResponseStreamHandler GetResponseStreamHandler();
   }

   public interface IResponseDatagramHandler
   {
      void HandleResponse(byte[] responseDatagram);
   }

   public interface IUdpMessageExchange : IMessageExchange
   {
      IResponseDatagramHandler GetResponseDatagramHandler();
   }

Notice how we've created an abstraction around the the TCP response stream. IResponseStream is an interface that the test tool can implement and which can provide controlled access to the read side of a .NET NetworkStream object. We could simply pass the NetworkStream object to the plug-in, but then the code that is supposed to only be able to process the reply data stream could write to the NetworkStream, or close it, etc. In this kind of situation it's better to provide a custom interface that allows the user only the access that we want to allow them. This tends to result in more robust software.

The IResponseStream interface might look something like this:

   public interface IResponseStream
   {
      int DefaultTimeoutMillis
      {
         get;
         set;
      }

      string ReadLine();
      string ReadLine(int timeoutMillis);

      int Read(byte[] buffer, int offset, int length);
      int Read(byte[] buffer, int offset, int length, int timeoutMillis);

      byte ReadByte();
      byte ReadByte(int timeoutMillis);

      void Close();
   }

To make it easy for the developer writing the protocol specific plug-ins we provide various methods of accessing the data in the response stream. We also provide timeout functionality on all reads, this ensures that the test can't hang if there's no data sent back. The protocol specific code can specify a timeout for each read operation, and simply set a default timeout for all read operations that don't explicitly supply a timeout. We allow the user to close the response stream, but note that this merely shuts down the receive side of the TCP connection.

Configuring the test

We will need to be able to pass configuration data to the protocol specific plug-in. This configuration can be in the form of an XML document, to isolate the plug-in from how we come by the XML we'll just pass it the root node of the XML that it is to process. This data is completely specific to the plug-in and the test tool doesn't need to understand it at all. Since protocol plug-ins can be for TCP or UDP (or perhaps both) we need to configure the plug-in to use the correct protocol. If the plug-in doesn't support the protocol then it can throw an exception.

Munging the byte stream

Our test tool will attempt to send messages as a non distinct byte stream. This tests that the server is correctly handling the incoming data as a byte stream and not assuming that it will receive distinct messages. There are three ways that a message could arrive:

  • A single, complete, message. Just by chance, the message arrives intact and there are no other messages available at the time we call read.
  • A fragment of a message. Only the first x bytes have arrived, the rest will arrive later.
  • Multiple messages. The results of the read contains more than one message, or a few messages and a message fragment.

To aid us in debugging situations where our server doesn't behave as expected we need to be able to control how the messages are sent from the test tool. Since we may wish to do this at a protocol specific level we will allow the protocol specific plug-in to indicate if it supports fragmented messages and multiple messages. If the plug-in doesn't support these options then the test tool will not use them. When tranmitting multiple messages together as a single block, it may make sense to the protocol that some messages in a conversation could be transmitted together and some can not be. Imagine a protocol which requires a user to log in and then, once logged in, the user can upload a file. The login message will never form part of a multiple message block as further data will never be sent until the reply has been processed. The file upload however could consist of multiple messages that could quite easilly form part of a multiple message block. To allow the plug-in to control which messages can and cannot form part of a multiple message block we can adjust the conversation interface a little. Rather than simply returning messages until all messages in the conversation have been processed it could, instead, return an array of messages that could form part of a multiple message block. Thus the conversation now consists of one or more blocks of message exchanges. If the test tool is operating in multiple message mode then messages in a block may be sent as one transmission. As before, GetMessages() returns null when there are no more blocks in the conversation.

Note that for UDP testing the concept of sending fragmented messages is not relevant and sending multiple messages tests a different aspect of the server. In TCP sending multiple messages together tests how the server determines message boundaries. In UDP message boundaries are fixed and sending multiple messages merely tests how the server handles multiple messages concurrently.

Allowing for multi-threading

Our test tool will simulate multiple concurrent connections by operating in a multi-threaded manner. There seems little point in loading and configuring the plug-in seperately for each thread, but our existing design doesn't provide for requesting "the first" conversation multiple times. By adding the concept of a ConversationCreator we can obtain the ConversationCreator interface once in each thread and then request conversations from it until it returns null.

Some extra hooks

It's possible that some protocols may need exact timing information about when messages were transmitted. To cater for this possibility we can add a MessageTransmitted() method to the IMessage interface. This method will be called as soon after the message is transmitted as is possible, if it's critical that this method is called exactly after the message is transmitted then the plug-in should state that it doesn't support multiple messages, or present the message to the test tool as a single message block. Likewise, it may be useful to know when a Conversation is complete, or when all conversations provided by a ConversationCreation are complete.

The resulting interfaces look something like this, note that we've also added a method to enable the test tool to display some information about the plug-in that it's using.

   public interface IMessage
   {
      byte[] GetAsBytes();

      void MessageTransmitted();
   }

   public interface IMessageExchange
   {
      IMessage GetMessage();
   }

   public interface IResponseStreamHandler
   {
      void HandleResponse(IResponseStream responseStream);
   }   

   public interface ITcpMessageExchange : IMessageExchange
   {
      IResponseStreamHandler GetResponseStreamHandler();
   }

   public interface IResponseDatagramHandler
   {
      void HandleResponse(byte[] responseDatagram);
   }

   public interface IUdpMessageExchange : IMessageExchange
   {
      IResponseDatagramHandler GetResponseDatagramHandler();
   }

   public interface IConversation
   {
      IMessageExchange [] GetMessages();

      void ConversationComplete();
   }

   public interface IConversationCreator
   {
      IConversation GetConversation();

      void Completed();
   }

   public interface ISocketServerTest
   {
      void Initialise(XmlNode parameters, Protocol protocol);
      
      bool AllowFragments();

      bool AllowMultipleMessages();

      void DumpInformation(TextWriter outputStream);

      IConversationCreator GetConversationCreator();
   }

The test tool uses these interfaces as follows:

  • Load test configuration. The configuration details which plug-in to use, and other configurable test parameters.
  • Load the specified plug-in.
  • Locate the entry point object. This object must implement the ISocketServerTest interface.
  • Call Initialise() on the ISocketServerTest interface.
  • Adjust the test configuration based on the results of calls to AllowFragments() and AllowMultiplMessages().
  • Create worker threads and pass the ISocketServerTest interface to each.
  • Wait for all worker threads to complete.

In each worker thread:

  • Obtain the ConversationCreator.
  • While GetConversation() doesn't return null.
  • Create the correct type of connection (TCP or UDP) using the conversation, and converse.
  • Call Completed()

Conversing via TCP consists of:

  • While GetMessages() doesn't return null.
  • If configured to send multiple messages, randomly decide how many messages to send, else send 1.
  • Calculate the size of buffer required to send all of the message data.
  • If configured to send fragmented messages, randomly decide how much of the message data to actually send.
  • Create a buffer of the required size.
  • For each message that we're going to send, copy as many of its bytes into the buffer as we can.
  • Send the full buffer.
  • For each message that we sent, call MessageTransmitted().
  • For each message that we sent call GetResponseStreamHandler() and if non null, HandleResponse()
  • If configured for delays, add a delay and then process more messages

Conversing via UDP consists of:

  • While GetMessages() doesn't return null.
  • If configured to send multiple messages, randomly decide how many messages to send, else send 1.
  • For each message that we're going to send, send it and call MessageTransmitted().
  • For each message that we sent call GetResponseStreamHandler() and if non null, HandleResponse()
  • If configured for delays, add a delay and then process more messages

Implementation

The implementation of the test tool in C# is fairly straight forward. The .NET framework library provides us with easy to use networking classes in System.Net.Sockets. We use the TcpClient and the UdpClient as the basis of our client connection classes. The multi-threading is equally easy. We use System.Threading.Thread and simply have to provide the function that we wish our threads to execute. The main thread waits for the worker threads to complete using a ManualResetEvent. The threads decrement a counter using the Interlocked class. Use of the Interlocked class means that each thread is guaranteed to decrement the counter as a single atomic operation. Without it, two threads accessing the counter at the same time could lead to unexpected behaviour. The thread that moves the counter to 0 sets the event and the main thread shuts down. Configuration is a breeze using the classes from System.Xml to load an XML configuration document and walk the nodes of the tree.

Dynamically loading plug-ins

One of our major requirements of the tool is that the protocol specific work can be done by plug-ins that can be loaded and configured per test run. We took great care in the design of the interfaces to make it as easy as possible to create the plug-ins. Each plug-in is developed as a stand-alone DLL assembly. The entry-point to a plug-in is an object that implements ISocketServerTest. Plug-ins can contain multiple entry-points and the entry-point used by a particular test run is dependent on the configuration file. Loading and configuring the plug-in is made easy using the classes in System.Reflection.

      Assembly assembly = Assembly.LoadFrom(Y); // path to assembly dll

      Object obj = assembly.CreateInstance(X);  // name of object that implements 
                                                // ISocketServerTest

      if (obj == null)
      {
         throw new Exception("Entry point not found");
      }

      ISocketServerTest serverTest = (ISocketServerTest)obj;

      serverTest.Initialise(config.TestParameters, config.Protocol);

      return serverTest;
   }

We were pleasantly surprised at how easy it was to translate our design into code using C# and .NET and the only gripe we have is the lack of true multiple inheritance. It would have been nice to be able to have the interfaces, or a class derived from the interfaces, provide default implementations for some methods, such as the MessageTransmitted() and ConversationComplete() methods and effectively give the user of the interface the option to implement if they require behaviour other than the default.

Configuring the test tool

The tool is configured using XML files. The name of the configuration file to use is passed to the tool on the command line. A configuration file might look something like this:

<xml version="1.0" encoding="utf-8" ?>
<SocketServerTest>
   <Threads>
      <Number>350</Number>
      <Batch>10</Batch>
      <DelayMillis>1000</DelayMillis>
   </Threads>
   <Protocol>TCP</Protocol>
   <Messages>
      <Fragments>true</Fragments>
      <Multiple>true</Multiple>
      <DelayMillis>1000</DelayMillis>
   <Messages>
   <RandomSeed>112</RandomSeed>
   <Test>
      <Host>localhost</Host>
      <Port>5001</Port>
      <Name>LargePacketEchoServerTest.dll</Name>
      <EntryPoint>ServerTest</EntryPoint>
      <Parameters>
         <Conversations>10</Conversations>
         <Blocks>10</Blocks>
         <Messages>10</Messages>
         <MessageSize>8000</MessageSize>
         <ShowDebug>false</ShowDebug>
      </Parameters>
   </Test>
</SocketServerTest>

As you can see, we can configure the number of worker threads to start, and start these threads in batches with a delay between each batch, if required. We can configure the protocol, TCP or UDP, and whether the test tool should send message fragments and multiple messages and if it should delay between message blocks. We can also specify the seed of the random number generator, this allows us to be able to reply test runs exactly even though they contain a 'random' element. This is especially useful in tracking down bugs which only show up under certain message fragmentation situations.

The elements within the <Test> specify the details of the test itself. The host and port to connect to define the server that will be tested. The <Name> node details the plug-in protocol test assembly to use and the <EntryPoint> node details the name of the class within the test assembly that is the entry point for the test. The contents of the <Parameters> node is treated as opaque data by the test tool and is simply passed to the plug-in during initialisation.

Structuring the code

We decided to place all of the interfaces in their own assembly. This allows both the plug-ins and the test tool to reference them without requiring the plug-ins to reference the tool itself. The code for the test tool lives in the JetByte.SocketServerTest namespace and the interfaces in the JetByte.SocketServerTest.Interfaces namespace.

Writing a plug-in

Now that we have a test tool that allows for plugging in protocol specific tests we need to write some plug-ins. The packet-based echo server that we developed in an earlier article provides quite a good test of the test tool and its interfaces. The packet echo server works with a very simple protocol. Upon connection, it sends a welcome message. From then on it expects to receive a single byte header which contains the length of the data packet (including the one byte header). The server reads the number of bytes specified by the header and then echoes the packet back to the client. We can use the test tool to ensure that the server obeys the protocol correctly by sending fragmented messages and multiple message blocks, we can check that the server operates correctly by comparing every byte of data sent with every byte received.

The first thing to do when writing a protocol test plug-in is to create a new dll assembly. Select File New, C# class library (although of course you could use any .NET language to create the plug-in). We need to add a reference to the JetByte.SocketServerTest.Interfaces assembly so that we can refer to the interfaces that are defined within it.

The entry point into the protocol specific plug-in is any class that derives from the ISocketServerTest interface. A plug-in can have multiple entry points and the one used by a particular test is determined by the configuration file. We could place the entry point in a namespace, but configuration is easier if we don't, so we'll simply define a class at global scope. Our entry point may look something like this:

   public class ServerTest : ISocketServerTest
   {
      public ServerTest()
      {
      }

      // Implement ISocketServerTest

      public void Initialise(XmlNode parameters, Protocol protocol)
      {
         if (protocol != Protocol.TCP)
         {
            throw new Exception("PacketEchoServerTest only supports TCP");
         }
         config = new PacketEchoServerTest.Configuration(parameters);
      }

      public bool AllowFragments()
      {
         return true;
      }

      public bool AllowMultipleMessages()
      {
         return config.MultipleMessages;
      }

      public void DumpInformation(TextWriter outputStream)
      {
         outputStream.WriteLine("Packet echo server test");
         outputStream.WriteLine("Fragmented Messages: {0}", AllowFragments());
         outputStream.WriteLine("Multiple Messages: {0}", AllowMultipleMessages());
         outputStream.WriteLine("Conversations {0}", config.Conversations);
         outputStream.WriteLine("Blocks per conversation {0}", config.Blocks);
         outputStream.WriteLine("Messages per block {0}", config.Messages);
         outputStream.WriteLine("Message Size {0} bytes", config.MessageSize);
      }

      public IConversationCreator GetConversationCreator()
      {
         return new ConversationCreator(config, Conversation.Type.Simple);
      }

      private PacketEchoServerTest.Configuration config;
   }

The rest of the classes involved will be defined in the PacketEchoServerTest namespace. Notice that we have a Configuration class which deals with the XML document for us and provides validation and a property-based access method for our configuration. The Configuration class uses a base class defined in the JetByte.SocketServerTest.Interfaces namespace that provides helper functions for accessing data from the XML nodes. Notice that we always allow fragmented messages, but are configurable as to whether we allow multiple message blocks. The reason for this is that we can then use this test harness to test both a packet echo server that ensures that packets are echoed in the sequence that they are received and one that doesn't.

Our ConversationCreator is pretty simple. Remember that this is merely an extra layer of indirection so that each test thread can create conversations concurrently. The ConversationCreator is the place to hold any state that needs to be passed between the Conversations that make up a test.

   internal class ConversationCreator : IConversationCreator
   {
      public ConversationCreator(Configuration config)
      {
         this.config = config;
      }
		
      // Implement IConversationCreator

      public IConversation GetConversation()
      {
         if (numConversations++ < config.Conversations)
         {
            return new Conversation(config);
         }

         return null;
      }

      public void Completed()
      {
         // Nothing to do here
      }
		
      private int numConversations = 0;

      private Configuration config;
   }

Our Conversation is fairly straight forward. When GetMessages() is called it generates the appropriate number of MessageExchange objects and returns them as an array. Notice how we deal with the fact that for this particular protocol the Conversation starts with one kind of message and then continues with another kind of message. The ServerSignOn message is a MessageExchange class that doesn't send a Message, it just waits for a "reply". This lets us deal with the fact that the server sends data to us first. When We connect and begin conversing, the first thing we do is wait for the server's sign on "reply". Once we have received the reply we then move to sending and receiving echo data packets. The packets themselves are of varying sizes.

   internal class Conversation : IConversation
   {
      public Conversation(Configuration config)
      {
         this.config = config;
      }

      // Implement IConversation

      public IMessageExchange [] GetMessages()
      {
         IMessageExchange [] messages = null;

         if (blocksSent == 0)
         {
            messages = new IMessageExchange[1];

            messages[0] = new ServerSignOn();
         }
         else if (blocksSent < config.Blocks + 1)
         {
            messages = new IMessageExchange[config.Messages];

            for (int i = 0; i < config.Messages; ++i)
            {
               int messageSize = config.MessageSize + (i % 55);

               messages[i] = new MessageExchange(messageSize);
            }
         }

         blocksSent++;

         return messages;
      }
		
      public void ConversationComplete()
      {
         // Nothing to do here	
      }

      private int blocksSent = 0;

      Configuration config;
   }

The ServerSignOn class is typical of how MessageExchange objects are structured. It implements both the ITcpMessageExchange interface and the IResponseStreamHandler interface. When the GetResponseStreamHandler() method of the ITcpMessageExchange interface is called it simply returns itself. As we'll see in the echo MessageExchange class, it's convenient for the MessageExchange object to implement both the Message and reply interface. Of course the interface definition doesn't mandate this, it just happens to be a convenient implementation strategy which places the response handler and message generator in the same class. As we'll see with the echo MessageEchange object, this is convenient as the response handler associated with a message has easy access to the original message data. The ServerSignOn object simply reads a line from the response stream and discards it. We could do some checking here to validate that the server response is as we expect it, but we don't bother.

   internal class ServerSignOn : ITcpMessageExchange, IResponseStreamHandler
   {
      // Implement IMessageExchange

      public IMessage GetMessage()
      {
         return null;
      }

      public IResponseStreamHandler GetResponseStreamHandler()
      {
         return this;
      }

      // Implement IResponseStreamHandler

      public void HandleResponse(IResponseStream responseStream)
      {
         responseStream.ReadLine();
      }
   }

The echo MessageExchange class is a little more complex. First we create a message of the size specified, the message consists of the single byte header and the data bytes. When the GetMessage() method of IMessageExchange is called we simply return ourselves as we are the Message. We can then return our message bytes when asked by a call to GetAsBytes(). To process the server's respose GetResponseStreamHandler() will be called on our ITcpMessageExchange interface. Again we simply return ourself as we also handle the response. When HandleResponse() is called we read a single byte from the response stream, check that the header specifies the correct number of bytes and then attempt to read the message body. We loop until we have read the correct number of bytes. If any read times out and returns 0 bytes then the Read() method of the IResponseStream interface will throw an exception for us. Finally we compare the contents of the reply with the contents of the original message.

   internal class MessageExchange : ITcpMessageExchange, IMessage, 
                                    IResponseStreamHandler
   {
      public MessageExchange(int size)
      {
         if (size > 256)
         {
            throw new Exception("Size must be <= 256");
         }

         this.size = size;
       
         message = new byte[size];

         message[0] = (byte)size;

         for (int i = 1; i < size; ++i)
         {
            message[i] = (byte)(i + 1);
         }
      }

      // Implement IMessageExchange

      public IMessage GetMessage()
      {
         return this;
      }

      // Implement ITcpMessageExchange

      public IResponseStreamHandler GetResponseStreamHandler()
      {
         return this;
      }

      // Implement IMessage

      public byte[] GetAsBytes()
      {
         return message;
      }

      public void MessageTransmitted()
      {
         // Nothing to do
      }

      // Implement IResponseStreamHandler

      public void HandleResponse(IResponseStream responseStream)
      {
         // Now, read in size bytes from the stream and compare them to the message

         byte[] response = new byte[size];

         response[0] = responseStream.ReadByte();

         if ((int)response[0] != size)
         {
            throw new Exception("packetSize != size");
         }

         int bytesRead = 1;

         while (bytesRead != size)
         {
            bytesRead += responseStream.Read(response, bytesRead, size - bytesRead);
         }

         for (int i = 0; ok && i < size; ++i)
         {
            if (message[i] != response[i])
            {
               throw new Exception("response != message");
            }
         }
      }      

      private byte[] message;
      private int size;
   }

The way that we've structured the Packet Echo Server's test plug-in bears little relationship to the structure of the interfaces. We've managed to condense the objects required for message exchange into a single object which handles the message, message exchange and response interfaces. For more complex protocols the message exchange object may link back to the conversation object, thus allowing the results of one message exchange to affect future message exchanges - such as passing the number of available mail messages between message exchanges used to test a POP3 server.

Testing

The plug-in that we've just described can be used to test the Packet Echo Server that's available here. The server listens on two ports and has slightly different semantics on each port. On port 5001 the server uses write sequence numbers to ensure that all writes issued occur in the correct sequence (see the article on Read and Write Sequencing for more information). This server can be tested using the a configuration file that specifies that multiple messages should be sent before processing replies. Since write sequencing is being used and only a single Read() is being posted per connection we can guarentee that the server will echo the packets back in the order that they are received off of the wire. Use the PacketEchoServerTest1.xml file to test this server.

The server that listens on port 5002 does not use write sequencing, using PacketEchoServerTest1.xml will test this server with multiple messages turned on and some of the tests should fail. If they don't fail, you're lucky, try running the server on a multi-processor box... Because multiple messages are being received on a single connection in blocks, multiple writes can be outstanding on that connection. Since write sequencing is not being used the problem outlined in the Read and Write Sequencing article can occur and the test harness recognises this as the packets are echoed out of sequence. Use PacketEchoServerTest2.xml to test this behavior.

Running a test against the server on port 5002 with multiple message blocks turned off will work. This is because there can only ever be a single outstanding write request per connection, so there's no way for the writes to get out of sequence. Use PacketEchoServerTest3.xml to test this behavior.

An alternative strategy for dealing with the out of sequence writes is to make the client responsible for reordering the packets. We can test this strategy, and demonstrate the use of multiple entry points within a plug-in, by adding a second protocol test to our plug-in. This involves creating another class that implements ISocketServerTest and that uses a MessageExchange object which can determine which message a response is for. We do this matching by effectively extending the header to two bytes, though the server is oblivious to this. The second byte is a message number which we use when matching responses. When a response arrives we read the length and message number and the data packet, we then lookup the original message by message number before matching the data. This involves the MessageExchange objects being aware of each other and shows how state might be communicated between messages in an entire message block or conversation. Use PacketEchoServerTest4.xml to test this behavior.

Note that the difference in timings, or lack thereof, between running PacketEchoServerTest1.xml and PacketEchoServerTest4.xml should be taken with a pinch of salt. To realistically test the effect that adding write sequencing to a server has on performance you would need test clients which simply read and discarded the responses so that the client loading was comparable between the two server tests, we'll leave that as an exercise for the reader.

Notes about the source

The source archive contains a Visual Studio .Net solution which builds the test interface assembly, the test tool and the protolcol plug-ins to enable testing of all of the servers developed so far. The SocketServerTest project has references to the plug-ins but these references are not required to build the test tool, they are merely for convenience when running the tool in debug. By including a reference to the plug-in dlls the build of the test tool also builds the plug-ins and copies the plug-ins into the test tool's build directory, this makes it easier to run the test tool in the debugger. The archive also contains configuration files for all of the plug-ins.

Revision history

  • 15th July 2002 - Initial revision.
  • 16th July 2002 - Added EchoServer and EchoServerEx tests. EchoServerEx can also test UDP servers.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Len Holgate
Software Developer (Senior) JetByte Limited
United Kingdom United Kingdom
Len has been programming for over 30 years, having first started with a Sinclair ZX-80. Now he runs his own consulting company, JetByte Limited and has a technical blog here.
 
JetByte provides contract programming and consultancy services. We can provide experience in COM, Corba, C++, Windows NT and UNIX. Our speciality is the design and implementation of systems but we are happy to work with you throughout the entire project life-cycle. We are happy to quote for fixed price work, or, if required, can work for an hourly rate.
 
We are based in London, England, but, thanks to the Internet, we can work 'virtually' anywhere...
 
Please note that many of the articles here may have updated code available on Len's blog and that the IOCP socket server framework is also available in a licensed, much improved and fully supported version, see here for details.

Comments and Discussions

 
GeneralGood stuff Pinmemberconrad Braam11-Jun-09 9:34 
GeneralRe: Good stuff PinmemberLen Holgate11-Jun-09 10:25 
GeneralC# Server PinmemberMadmaximus27-Jan-06 5:40 
AnswerRe: C# Server PinmemberLen Holgate5-Feb-06 23:17 
GeneralInterface diagram PinsussJunaid Ahmed25-Oct-04 23:13 
GeneralRe: Interface diagram PinmemberLen Holgate26-Oct-04 10:01 
Generalif there was c++ version, that's nice Pinmemberliuliu26-Sep-04 21:15 
GeneralRe: if there was c++ version, that's nice PinmemberLen Holgate27-Sep-04 8:20 
Generalhelp... Pinmembertemp555626-Jan-04 17:23 
GeneralRe: help... PinmemberLen Holgate26-Jan-04 20:57 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.141015.1 | Last Updated 21 Jul 2002
Article Copyright 2002 by Len Holgate
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid