Basic network programming in .NET (part 3)

For this post, I’ll be showing network code that is about as simple as it can get. Frankly, when it comes to the i/o itself, it’s my opinion that the code never gets all that complicated. The complexities tend to be with respect to managing all the other stuff that’s hooked to the i/o code. But, this network code is really simple.

So, what does the most basic server look like? Well, first the server needs to make itself available for connections:
Socket sockListen = new Socket(AddressFamily.InterNetwork,
    SocketType.Stream, ProtocolType.Tcp);

sockListen.Bind(epListen);
sockListen.Listen(1);

Socket sockConnect = sockListen.Accept();

All TCP sockets start out the same, allocated as above. AddressFamily.InterNetwork refers to TCP/IP, while SocketType.Stream and ProtocolType.Tcp go hand in hand (all TCP sockets are also stream sockets). These parameters are passed to the System.Net.Socket constructor to instantiate a socket (SocketType.Datagram and ProtocolType.Udp would be used for a UDP socket; other address families have other combinations of socket and protocol types that are valid).

The Socket.Bind method assigns a specific address to the socket. The epListen variable references an instance of IPEndPoint that’s been initialized to the desired server address.

It’s very important that whatever address the server uses, the client knows how to get it. Usually the server will specify the IPAddress component of the address to IPAddress.Any (meaning any IP address on the local computer can be used), and a port appropriate to the server. Depending on the configuration of the network, the client may find the IP address using a name service (e.g. DNS) that maps a textual name to an IP address, may simply depend on the user to specify the IP address, or may have it hard-coded. In this code sample, it’s hard-coded in the client (see below) as the IPAddress.Loopback value, which simply means that the server is on the same computer as the client. Similarly, the port must either be a constant that the client simply always uses, or that the user can configure for both the server and the client (so that they match).

The call to Socket.Listen makes the socket actually available for connection requests. Until this is called, a client trying to connect will simply get an “unreachable” error. Once Listen is called, the network driver will allow a number of clients up to the number specified in the call to have pending connection requests. Clients that attempt to connect once this backlog queue has been filled will receive immediate rejections.

The queue is emptied as the server actually accepts connection requests, which it does by calling Socket.Accept. The Accept method returns a Socket instance that represents the actual connection to a client. Obviously, it can’t do that until some client tries to connect, so this method will block until that happens.

Once Accept does return, we can start receiving data from the client. In a most typical scenarios, the network i/o is a back-and-forth affair, but for this sample each end will do all of its sending and all of its receiving each as a single operation. The server will receive everything and then send it all back, while the client will send everything and then receive the response from the server. For the server, that looks like this:
while ((cb = sockConnect.Receive(rgb)) > 0)
{
    _ReceivedBytes(rgb, cb);
}

foreach (string strMessage in _lstrMessages)
{
    sockConnect.Send(_code.GetBytes(strMessage + ''));
}

We simply pass a byte[] (rgb) to the Socket.Receive method, which will return to the caller once there’s any amount of data available on the socket, having copied the data into the byte[] parameter. If more data is available than the length of the byte[], as much will fit will be copied; by calling Receive repeatedly, the caller can eventually get all of the bytes that are available. Once all of the available data has been received, as long as the sender hasn’t initiated a shutdown of the connection, the Receive method will block until data becomes available again.

There are a couple of things in there not specific to the Socket class, including a helper method called _ReceivedBytes that deals with processing the stream of bytes.

As I mentioned before, TCP is strictly a stream of bytes. There is no inherent delimiting of bytes, and one can’t count on receiving bytes grouped the same way in which they were sent. The bytes will be in the same order, but any given call to receive can return any number of bytes from 1 up to the number of bytes that have already been sent but not yet received.

In all of my examples, we will be using null-terminated strings to deal with this. .NET allows null characters in an actual String instance, so rather than scanning the bytes as they come in for bytes of value 0, we’ll go ahead and convert the bytes first, and then look for the nulls.

Speaking of converting the bytes, that’s the other thing in there. The _code variable references an instance of the Encoding class. It’s been initialized from the Encoding.UTF8 property. Generally speaking, the Encoding class is used for converting bytes to and from some specific character encoding. See the MSDN documentation for more details. The important things here are:
  • I’ve chosen UTF-8 as my character encoding for my application protocol.
  • Strings need to be converted to bytes before sending and from bytes after receiving.
  • Because UTF-8 is a character encoding that uses more than one byte to represent some characters, and because TCP may deliver the parts of a byte sequence that represents one of these characters in multiple receives, we need to maintain state between calls to Receive so that one of these broken characters can be reassembled once all the bytes have been received.

If we use the same Encoding instance for each receive, it will not only address the basic character encoding issues, it will also maintain the state we need in case a multi-byte character gets broken apart during transmission. So, I create a single instance and reuse it for all character encoding/decoding operations. This would be useful for performance anyway, but it’s critical for ensuring correct handling of the byte stream.

The _ReceivedBytes method takes care of both of these needs. It uses our single Encoding instance to convert the bytes to a string and then scans for nulls to break apart the received text into individual String instances:
private void _ReceivedBytes(byte[] rgb, int cb)
{
    int ichNull;

    _strMessage += _code.GetString(rgb, 0, cb);

    while ((ichNull = _strMessage.IndexOf('')) >= 0)
    {
        _lstrMessages.Add(_strMessage.Substring(0, ichNull));

        _strMessage = _strMessage.Substring(ichNull + 1);
    }
}

The method accumulates the text that’s received into a single string (_strMessage), and then extracts individual null-terminated strings, adding them to a list of strings (_lstrMessages). Those are then sent back to the client (the second loop in the previous code block), terminating each one with a null character and converting back to bytes so that the Socket.Send method, which deals only with bytes, can actually use the data.

Finally, once we have finished sending all the strings back to the client, we clean things up:
sockConnect.Shutdown(SocketShutdown.Both);
sockConnect.Close();

The call to Socket.Shutdown indicates to the network driver that we’re done with the connection. Different network protocols use this differently, and some not at all (e.g. UDP). At a minimum, the Socket class will disable sending, receiving, or both on the instance based on the SocketShutdown parameter (an exception will be thrown if a disabled operation is attempted). At the network driver level, for a TCP connection the call to Shutdown results in negotiation between the endpoints to indicate the end of the stream in each direction. Each endpoint that shuts down with SocketShutdown.Send or SocketShutdown.Both will be seen by the other endpoint as the end of the stream. That is, once all the bytes that have been sent by the endpoint calling Shutdown are received, a call to Receive method by the other endpoint will return 0, indicating the end of the stream.

That’s the server. What about the other end? Almost the same! The client code is nearly identical:
Socket sockConnect = new Socket(AddressFamily.InterNetwork,
    SocketType.Stream, ProtocolType.Tcp);

sockConnect.Connect(epConnect);

foreach (string strMessage in rgstrMessages)
{
    sockConnect.Send(_code.GetBytes(strMessage + ''));
}

sockConnect.Shutdown(SocketShutdown.Send);

while ((cb = sockConnect.Receive(rgb)) > 0)
{
    _ReceivedBytes(rgb, cb);
}

sockConnect.Close();

The Socket instance is created the same way, but instead of binding, listening, and then accepting, the code just calls Socket.Connect. The _ReceivedBytes method is even identical in this case. It decodes the received bytes in exactly the same way, adding each delimited string to a list of strings for later processing. (Obviously a more sophisticated network application would have significantly different handling for the received data between the server and client).

You can also see that the loops are swapped. The sending loop is first for the client, the receiving loop second. The client indicates that it’s done sending by calling Shutdown with just the SocketShutdown.Send value, because it still needs to receive data from the server. Which it does, following the call to Shutdown. Of course, as with the server, the Socket is closed once we’re done.

In this sample, all of the above code is wrapped up in a couple of classes, one for the server and one for the client, called ServerBasicEcho and ClientBasicEcho, respectively. For future samples, I’ll be adding a System.Windows.Forms GUI to allow for testing of the classes. But this sample is so simple, a console application suffices nicely. Here’s the Main method:
static void Main(string[] args)
{
    try
    {
        ServerBasicEcho server = new ServerBasicEcho();
        ClientBasicEcho client = new ClientBasicEcho();
        AutoResetEvent areServerStarted = new AutoResetEvent(false);
        IPEndPoint ep = new IPEndPoint(IPAddress.Any, 5005);

        Thread threadServer = new Thread(delegate() { server.Start(ep, areServerStarted); });

        threadServer.IsBackground = true;
        threadServer.Start();

        areServerStarted.WaitOne();

        client.Start(new IPEndPoint(IPAddress.Loopback, ep.Port), _krgstrTestData);

        threadServer.Join();

        Console.WriteLine("client-server test succeeded!");
    }
    catch (Exception exc)
    {
        Console.WriteLine("client-server test failed. error: \"" + exc.Message + "\"");
    }

    Console.ReadLine();
}

Unlike the snippets above, this really is the entire Main method. The basic steps implemented here are:
  • Create the server and client instances
  • Initialize the desired server IPEndPoint address
  • Start the server on a separate thread
  • Start the client on the current thread
  • Run until the client and server both exit

There’s some additional logic in there to ensure that the client isn’t started until we’re sure the server is, as well as a little bit of output to reassure the user that everything went according to plan.

And that’s it! As you can see, in spite of the length of this post, there’s really only about a half-dozen lines of code in each of the server and client that is directly related to doing the network i/o. The rest of the code is all just logic to deal with the actual bytes that were received, and to manage the server and client implementations themselves.

As we’ll see in future posts, things get iteratively more involved as we want to add features. An interactive network connection is more complicated, as is dealing with more than one client. Oddly enough though, as we get nearer the conceptually most complicated implementation, the code actually starts to get simpler again, at least with respect to how many lines of code there actually are. It will be a good demonstration of how even though multi-threaded code can be harder to reason about, in some ways it can actually simplify the design of the program.

But that’s for another day. For now, you can find the complete console application for this particular sample by clicking here.

In the next sample, I’ll continue the theme of having a single client and a single server, but add some interactivity, and show some techniques for connecting the network objects to a GUI.

Leave a Reply

Your email address will not be published. Required fields are marked *


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>