Simple Multi-User TCP/IP Client & Server using TAP

Introduction

In today's connected world it is no surprise that many new developers want to create some kind of client-server application soon after getting the hang of writing simple Windows Forms applications.  These tend to be chat clients, data sharing utilities, and simple games meant for use with a small group of friends, family, or colleagues.  It seems like it should be easy enough.  Classes like System.Net.Sockets.TcpClient and TcpListener allow communication with TCP/IP.  Threads or BackgroundWorkers allow multiple simultaneous processing routines.  There are loads of these sort of apps live which people use so often they take them for granted.  Everyone seems to be doing it - how hard can it be?

It turns out that it can be quite difficult, depending on how you go about it.  Interaction between the main Form's GUI thread and those background processing thread(s) and the client and listener instances can get complex in a hurry.  There are many pitfalls and it's easy for the inexperienced developer to find they fall into one after another along their way.  Taking numerous client connections, processing those and existing clients, and all the while keep a GUI responsive is quite tricky.

Thankfully, the majority of these issues can be eloquently handled through implementation of the Task-based Asynchronous Programming Pattern, or TAP.  Through the use of Task instances and the Async/Await keywords we can vastly simplify the processes of the server for scenarios where we have a reasonable number of clients sending reasonably sized messages in reasonable intervals.  But before we look at any code, perhaps we should define "reasonable".

(Return to Top)

Scale and Performance

Before beginning any kind of program meant to perform as a "server" communicating with multiple remote hosts, it is important to understand that writing a good server is hard.  There are many variables to consider such as the number of clients to be supported and their connection speeds, the size and frequency of the data to be transmitted between hosts, whether or not the server needs to initiate transmissions to the remote clients, and many other possible factors.  For scenarios where there will be a very large number of clients (more than a couple hundred), or where the data transmissions are extremely large (MBs) or extremely frequent (more than once a second) it will be best to utilize an existing, proven solution rather than try to write your own.  The primary reason for this is that there is a lot of work to do to support such scenarios and it took teams of people to create the existing solutions.  It is unlikely that one person working on their own could come up with an equally sound solution in any reasonable amount of time.

So the first step in designing a multi-user client/server application is determining if an existing framework can meet all of your application requirements.  Two of the most common solutions are:

These are robust frameworks and are the recommended solutions whenever the application requirements can be fitted to one.

However, being robust frameworks there is a learning-curve to using them and some specific scaffolding which must be performed to get a project ready to utilize the framework.  There is also overhead in the handling of the data transmissions in order to fit the content to the protocol used by the framework.  In the case of a very simple set of application requirements, the learning-curve and scaffolding can seem like a lot of effort.  In the case of a server which communicates with very light-weight, low-power clients (for example, micro-controllers instead of PCs) the message protocol overhead may require too much processing to be handled efficiently by the client. In these cases writing your own simple client/server framework may be preferable.

To summarize, you should only write your own client-server framework when one of the existing frameworks isn't suitable and when you have "reasonable" application requirements:

  • Reasonable number of clients (< 500)
  • Reasonable size of messages  (< MB)
  • Reasonable transmission frequency (< 1/second)

The numbers listed are just rough guidelines.  Your specific computer hardware, network infrastructure, and code design will determine the actual limits.  The design detailed in this article should generally be good for a couple hundred clients, with message sizes typically under 1KB, and a transmission frequency of one message every few seconds per client.

(Return to Top)

Message Protocol

After a client/server framework is designed and the server can accept multiple clients and they can send messages back and forth, the next most common issue is determining how to define the actual message content.  Most examples of client/server communication show sending a single string value between client and server.  The difficultly can then be determining how best to send multiple data values in a single transmission.

The solution is to define a message protocol to be used by your client and server applications.  The message protocol is a set of rules that define how a sequence of raw byte data is to be interpreted as a message.  With a single string value, the protocol is likely just a simple text encoding.  With multiple values, there needs to be a way of delineating the various pieces of data within the message.  It also isn't possible to be certain that a message is sent within a single transmission so there needs to be a way of knowing how many bytes make up a message.

While there is no single way to design such a message protocol, there are some commonly used procedures that can be followed.  A typical message protocol might contain:

  • A Start Sequence
  • Message or Data Length Header
  • Message Data
  • A Stop Sequence (optionally)

The Start Sequence is a series of one or more bytes which always exist at the start of a new message.  This indicates to the receiver that the following bytes contain header information and a message payload.

After reading the Start Sequence, which is a known fixed number of bytes, the receiver would then read the next 4 bytes and convert that to an Integer representing the length of the data (or total message).

The receiver now has enough information to know how many bytes to read before considering the message complete and beginning to look for another new message. The complete message data is read from the stream.

The optional Stop Sequence is also a series of one or more bytes, like the Start Sequence, and can be used to by the receiver to verify that the received data represents a real message.

Within the message data you can then further define other value indicators, lengths, and/or separators to combine multiple data values into a single message data blob. Alternatively, you can make use of an existing object that's already good at this job.  In this example we'll use the XElement object to define our message commands in XML strings which can be readily parsed and interpreted.  The protocol's message data will then simply be the text encoding of this XML string.

(Return to Top)

Example Solution

The code solution for this example is comprised of three projects:

  • AsyncTcpClient
  • AsyncTcpServer
  • XProtocol

Create Projects and Solution

To begin, open Visual Studio (2013 or later) and create a new Windows Forms project named "AsyncTcpClient".  Save the solution, renaming the solution to "AsyncTcpSample".  Click File -> Add new project on the menu bar in Visual Studio and add a second Windows Forms project named "AsyncTcpServer".  Again, click File -> Add new project and this time select a Class Library project and name it "XProtocol".  

Add References

In the solution explorer, right click the AsyncTcpClient project and select Add -> Reference from the context menu.  Select  Solutions -> Projects in the left panel and then check XProtocol in the list box to add a reference to the XProtocol project.  Repeat these steps to add the XProtocol reference to the AsyncTcpServer.

With these steps complete you are now ready to begin inserting the example code from this article.

XProtocol Code

This example defines the "XProtocol"; a message protocol based on XElement instances which are a convenient and powerful representation of an XML element.  The XProtocol allows us to easily define any number of arbitrary messages containing multiple data properties and/or embedded data elements.  This protocol design favors simplicity and ease of use over efficiency in terms of speed and size.  The size of a message is inflated due to XML markup text and to all data values being represented as strings; the latter affects speed as well since all other data types require parsing of the string values.  In many cases though (that is, reasonable cases where this kind of custom client/server application can be applied), these size and speed penalties have no discernible negative impact on the final operation of the programs.

The XProtocol namespace contains a single class. XMessage.  The XMessage class wraps an XElement instance and provides utility methods for converting the XElement to and from a byte array, according to the rules of the protocol.  The object begins with a simple class declaration containing two constants and one instance member:

Public Class  XMessage
    Const SOH As Byte  = 1 'define a start sequence
    Const EOF As Byte  = 4 'define a stop sequence
    Public Property  Element As  XElement 'declare the object to hold the actual message contents
End Class

The two constants define the protocol's start and stop bytes while the instance member provides a property to get and set the associated XElement content.

The class has a simple constructor which takes an XElement instance, allowing the construction of an XMessage directly from XML in code (more on this later).

Public Sub  New(xml As XElement)
    Element = xml
End Sub

In addition to the Element property, the class has only one other public instance member which is the method to convert the object instance into a byte array for transmission.

'serialize the XElement content into a byte array according to the message protocol specification
Public Function  ToByteArray() As  Byte()
    Dim result As New  List(Of Byte)
    Dim data() As Byte  = System.Text.Encoding.UTF8.GetBytes(Element.ToString) 'encode the XML string
    result.Add(SOH) 'add the message start indicator
    result.AddRange(BitConverter.GetBytes(data.Length)) 'add the data length
    result.AddRange(data) 'add the message data
    result.Add(EOF) 'add the message stop indicator
    Return result.ToArray 'return the data array
End Function

This method creates a new List(Of Byte) and uses it to build up the byte array representation of the message instance.  This includes writing the start indicator, adding the length of the encoded XML string, adding the encoded string itself, and finally adding the stop indicator.

Finally, the class contains two shared methods used during message processing.  The function IsMessageComplete is used to verify that a byte sequence contains a complete message, and the function FromByteArray converts raw message byte data into an XMessage instance.

'define a method to check a series of bytes to determine if they conform to the protocol specification
Public Shared  Function IsMessageComplete(data As IEnumerable(Of Byte)) As  Boolean
    Dim length As Integer  = data.Count 'get the number of bytes
    If length > 5 Then 'ensure there are enough for at least the start, stop, and length
        If data(0) = SOH AndAlso data(length - 1) = EOF Then 'ensure the series begins and ends with start/stop identifiers
            Dim l As Integer  = BitConverter.ToInt32(data.ToArray, 1) 'interpret the data length by reading bytes 1 through 4 and converting to integer
            Return (l = length - 6) 'ensure that the interpreted data length matches the number of bytes supplied
        End If
    End If
    Return False
End Function

The IsMessageComplete method analyzes a sequence of byte data to determine if it conforms to the protocol specification.  This includes checking for a minimum length (six bytes; the length of the start and stop indicators plus the data length value), checking for the start and stop indicators, and ensuring that the specified message length matches the number of supplied message data bytes.

The FromByteArray method assumes that the byte data has already been validated by IsMessageComplete and simply decodes the data bytes from the message into a string and parses a new XElement instance from that string.

'parse the XElement content from the supplied data according to the message protocol specification
Public Shared  Function FromByteArray(data() As Byte) As  XMessage
    Return New  XMessage(XElement.Parse(System.Text.Encoding.UTF8.GetString(data, 5, data.Length - 6)))
End Function

This completes the XMessage class and the XProtocol project.

AsyncTcpServer Code

The AsyncTcpServer project will require three classes:

  • Form1 - The main GUI Form
  • ConnectedClient - A utility object used to encapsulate the TcpClient instance and its related data and processing routines
  • ConnectedClientCollection - A utility object used to facilitate storing a list of connected clients and accessing individual clients by ID

ConnectedClient Class

The first class to design in AsyncTcpServer will be the ConnectedClient since both other classes depend on it.  The ConnectedClient class contains a few fields of associated objects and an event declaration used to facilitate databinding in the example's GUI interface.

Imports System.Net
Imports System.Net.Sockets
 
Public Class  ConnectedClient
    Implements System.ComponentModel.INotifyPropertyChanged
    Public Event  PropertyChanged(sender As Object, e As  System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
 
    'store the TcpClient instance
    Public ReadOnly  TcpClient As  TcpClient
    'store a unique id for this client connection
    Public ReadOnly  Id As  String
    Public Property  Task As  Task
    'store the data received from the remote client
    Public ReadOnly  Received As  New List(Of Byte)
 
    'expose the received data as a string property to facilitate databinding
    Private _Text As String  = String.Empty
    Public ReadOnly  Property Text As String
        Get
            'Return Received.ToString
            Return _Text
        End Get
    End Property
End Class

This class encapsulates the TcpClient instance along with a unique ID, the Task instance handling the client's processing, and the pending data received from the remote host.  The class also exposes a Text property with property change notification so that the GUI can easily display the client's received message data via databinding.

The class requires only a simple constructor to assign the associated TcpClient instance and generate the unique ID.

Public Sub  New(client As TcpClient)
    TcpClient = client
    'craft the unique id from the remote client's IP address and the port they connected from
    Id = CType(TcpClient.Client.RemoteEndPoint, IPEndPoint).ToString
End Sub

The ConnectedClient class exposes a single public method used to append newly received data bytes to the client's internal buffer, determine if a complete message has been received, and process the message if it has.

Public Sub  AppendData(buffer() As Byte, read As  Integer)
    If read = 0 Then Exit  Sub
    'add the bytes read this time to the collection of bytes read so far
    Received.AddRange(buffer.Take(read))
    'check to see if the bytes read so far represent a complete message
    If XProtocol.XMessage.IsMessageComplete(Received)  Then
        'if so, build a message from the byte data and then clear the byte data to prepare for the next message
        Dim message As XProtocol.XMessage = XProtocol.XMessage.FromByteArray(Received.ToArray)
        Received.Clear()
        'read data elements from the message as appropriate
        Select Case  message.Element.Name
            Case "TextMessage"
                _Text = message.Element.@text1
                RaiseEvent PropertyChanged(Me, New  System.ComponentModel.PropertyChangedEventArgs("Text"))
        End Select
    End If
End Sub

Here we see the XMessage in action.  The code analyzes the name of the XML node to determine how to process the message.  In this case the code is only processing messages named "TextMessage".  Once the kind of message has been determined, the associated property values (XML Attributes) can be read based on the known format of the message.  This may become more clear when we look at sending a message later in the article.

Note that processing the actual message contents at this point may not be desirable in your real application.  In a real application, this method may simply parse the XMessage instance and reveal that value through a "CurrentMessage" property.  The actual analysis and processing of the message instance would occur in the client's processing routine.  We'll see more on this later within the server's code for processing connected clients.

The remainder of the class simply involves implementing the standard object method overrides to utilize the ConnectedClient ID property.

'implement primary object method overrides based on unique id
Public Overrides  Function Equals(obj As Object) As  Boolean
    If TypeOf  obj Is  ConnectedClient Then  Return Id = DirectCast(obj, ConnectedClient).Id
    Return MyBase.Equals(obj)
End Function
 
Public Overrides  Function GetHashCode() As Integer
    Return Id.GetHashCode
End Function
 
Public Overrides  Function ToString() As String
    Return Id
End Function

With this code complete we can now write the ConnectedClientCollection class.

ConnectedClientCollection Class

The ConnectedClientCollection class is simply an implementation of System.Collection.ObjectModel.KeyedCollection which reveals the ConnectedClient ID property as the key value for clients in the collection.  This allows our code to access an individual client in the collection by ID if needed.

Public Class  ConnectedClientCollection
    Inherits System.Collections.ObjectModel.KeyedCollection(Of  String, ConnectedClient)
 
    Protected Overrides  Function GetKeyForItem(item As ConnectedClient) As String
        Return item.Id
    End Function
End Class

Form1 Class

The Form1 code example contains a number of lines of code used for setting up the GUI and its controls.  This article will only highlight the code involved in the actual program operation, and the complete code sample will be provided in an appendix at the end of the article.

The code of the Form1 class will depend on four system namespaces and these should be imported at the top of the code file.

Imports System.IO
Imports System.Net
Imports System.Net.Sockets
Imports System.Threading

The Form1 class contains all of the primary work to be performed by the server.  Most of the work occurs in the server's StartButton.Click event handler.  In order to perform it's work, the server first needs a handful of local field values:

'specify the TCP/IP Port number that the server will listen on
Private portNumber As Integer  = 55001
'create the collection instance to store connected clients
Private clients As New  ConnectedClientCollection
'declare a variable to hold the listener instance
Private listener As TcpListener
'declare a variable to hold the cancellation token source instance
Private tokenSource As CancellationTokenSource
'create a list to hold any processing tasks started when clients connect
Private clientTasks As New  List(Of Task)

The portNumber defines the TCP port used for communication and will need to be the same value for both the client and server.  The clients object is an instance of the ConnectedClientCollection and holds the list of connected client instances.  The listener object is the primary TcpListener instance used by the server to accept incoming client connections.  The tokenSource provides a CancellationTokenSource instance used to provide a CancellationToken instance to the individual processing tasks for each client.  The clientTasks object holds a list of running client task instances for the server to wait on while shutting down and terminating clients.  Note that this list isn't explicitly necessary as the required IEnumerable(Of Task) instance could be interpolated from the ConnectedClientCollection.  The list is used here to simplify the presentation of the example code.

Moving on to the actual work of the class, the Click event handler for the Start Button handles the server's primary routine for accepting new clients.

Private Async Sub startButton_Click(sender As Object, e As  EventArgs) Handles  startButton.Click
    'this example uses the button text as a state indicator for the server; your real
    'application may wish to provide a local boolean or enum field to indicate the server's operational state
    If startButton.Text = "Start" Then
        'indicate that the server is running
        startButton.Text = "Stop"
 
        'create a new cancellation token source instance
        tokenSource = New  CancellationTokenSource
        'create a new listener instance bound to the desired address and port
        listener = New  TcpListener(IPAddress.Any, portNumber)
        'start the listener
        listener.Start()
        'begin accepting clients until the listener is closed; closing the listener while
        'it is waiting for a client connection causes an ObjectDisposedException which can
        'be trapped and used to exit the listening routine
        While True
            Try
                'wait for a client
                Dim socketClient As TcpClient = Await listener.AcceptTcpClientAsync
                'record the new client connection
                Dim client As New  ConnectedClient(socketClient)
                clientBindingSource.Add(client)
                'begin executing an async task to process the client's data stream
                client.Task = ProcessClientAsync(client, tokenSource.Token)
                'store the task so that we can wait for any existing connections to close
                'while performing a server shutdown
                clientTasks.Add(client.Task)
            Catch odex As ObjectDisposedException
                'listener stopped, so server is shutting down
                Exit While
            End Try
        End While
        'since NetworkStream.ReadAsync does not honor the cancellation signal we
        'must manually close all connected clients
        For i As Integer  = clients.Count - 1 To 0 Step -1
            clients(i).TcpClient.Close()
        Next
        'wait for all of the clients to finish closing
        Await Task.WhenAll(clientTasks)
        'clean up the cancelation token
        tokenSource.Dispose()
        'reset the start button text, allowing the server to be started again
        startButton.Text = "Start"
    Else
        'signal any processing of current clients to cancel (if listening)
        tokenSource.Cancel()
        'abort the current listening operation/prevent any new connections
        listener.Stop()
    End If
End Sub

By utilizing Async/Await we are able to put the click event handler's code into an infinite loop of accepting new clients.  This is because the code loop will spend most of its time being suspended, waiting on a new client to connect.  When the listener is closed during server shutdown, the waiting AcceptClientAsync method call will throw an exception which we can catch to then gracefully exit out of the loop and proceed to release any remaining clients and tear down the server.

Note the code comments that the manual looping and closing of existing clients is only required due to an implementation flaw in the NetworkStream.ReadAsync method.  If that method honored the cancellation token, this loop would not be necessary as the clients would already have disconnected themselves when the token was signaled (see Connect Issue Report for more information).

The next major method in Form1 is the ProcessClientAsync method which is responsible for manging each connected client's data stream.  This method represents the code executed by each client's task.

Private Async Function ProcessClientAsync(client As ConnectedClient, cancel As CancellationToken) As Task
    Try
        'begin reading from the client's data stream
        Using stream As  NetworkStream = client.TcpClient.GetStream
            Dim buffer(client.TcpClient.ReceiveBufferSize - 1)  As  Byte
            'loop exits when read = 0 which occurs when the client closes the socket,
            'or it exits on ReadAsync exception when the connection terminates; exception type indicates termination cause
            Dim read As Integer  = 1
            While read > 0
                'wait for data to be read; depending on how you choose to read the data, the cancelation token
                'may or may not be honored by the particular method implementation on the chosen stream implementation
                read = Await stream.ReadAsync(buffer, 0, buffer.Length, cancel)
                'process the received data; in this case the data is simply appended to a StringBuilder; any light
                'work (that is, code which does not require a lot of CPU time) can be performed directly within
                'the current while loop:
                client.AppendData(buffer, read)
 
                '*NOTE: A real application may require significantly more processing of the received data. If lengthy, 
                'CPU-bound processing is required, a secondary worker method could be started on the thread pool;
                'if the processing is I/O-bound, you could continue to await calls to async methods.  The following code
                'demonstrates the handling of a CPU-bound processing routine (see additional comments in DoHeavyWork):
 
                'Dim workResult As Integer = Await Task.Run(Function() DoHeavyWork(buffer, read, client))
                ''a real application would likely update the UI at this point, based on the workResult value (which could
                ''be an object containing the UI data to update).
                ''TO TEST: uncomment this block; comment-out client.AppendData(buffer, read) above
            End While
            'client gracefully closed the connection on the remote end
        End Using
    Catch ocex As OperationCanceledException
        'the expected exception if this routines's async method calls honor signaling of the cancelation token
        '*NOTE: NetworkStream.ReadAsync() will not honor the cancelation signal
    Catch odex As ObjectDisposedException
        'server disconnected client while reading
    Catch ioex As IOException
        'client terminated (remote application terminated without socket close) while reading
    Finally
        'ensure the client is closed - this is typically a redundant call, but in the
        'case of an unhandled exception it may be necessary
        client.TcpClient.Close()
        'remove the client from the list of connected clients
        clientBindingSource.Remove(client)
        'remove the client's task from the list of running tasks
        clientTasks.Remove(client.Task)
    End Try
End Function

Again, thanks to Async/Await and the TAP model, we put the code routine into an infinite loop which will spend most of its time suspended awaiting more data from the remote host.  It is this part of the design which requires that no single connected client sends a continuous stream of bytes.  If a client were to start sending data and not stop, no other client would have a chance to be processed.

While this simple example code has all of it's processing in the call to client.AppendData(), a real application would likely call AppendData and then check to see if client contained a complete message (AppendData might even be a method which returns either a complete message or nothing, or a status indicator which can be checked to see if the message is complete).  If there was a complete message in the client, then the processing routine would consume that message, analyze it, and process it accordingly.

The note in the comments refers to applications which require a lot of time consuming processing of a message.  This could include additional message data processing, storing the message data in an external repository such as a database, forwarding the message to all other clients, and other such time consuming or processor intensive work.  If the server needs to perform these kinds of operations, and they are all time consuming I/O bound operations (writing to a database, or relaying messages for example) then the processing routine should continue to use Async/Await calls wherever possible.  If however the processing is CPU bound (detailed additional parsing of the message data for example), then the code can execute a secondary thread on the threadpool using Task.Run.

Using Task.Run adds an additional layer of complexity and overhead to the server and should only be used if it is proven that one client can block another due to lengthy processing.  Until your program actually experiences this problem, you should avoid executing additional processing in secondary tasks.  If it is necessary though, the example code includes comments showing how it can be done, and there is an example "DoHeavyWork" method associated with the code comments.

'this method is a rough example of how you would implement secondary threading to handle
'client processing which requires significant CPU time
Private Function  DoHeavyWork(buffer() As Byte, read As  Integer, client As ConnectedClient) As Integer
    'function return type is some kind of status indicator that the caller can use to determine
    'if the processing was successful, or just an empty object if no return value is needed (that is,
    'if you want to treat the function as a subroutine); although this sample uses Integer, you could
    'use any type of your choosing
 
    'function parameters are whatever is required for your program to process the received data
 
    'due to the fact that AppendData will raise the notify property changed event, we need to
    'ensure that the method is called from the UI thread; in your real application, the UI
    'update would likely occur after this method returns, perhaps based on the result value
    'returned by this method
    Invoke(Sub()
               client.AppendData(buffer, read)
           End Sub)
    'put the thread to sleep to simulate some long-running CPU-bound processing
    System.Threading.Thread.Sleep(500)
    Return 0
End Function

Note that the example server code does not use the DoHeavyWork method by default.

The final code block in the server application is for the Send Button to transmit a message to the client currently selected in the GUI.  This occurs in the sendButton.Click event handler.

Private Async Sub sendButton_Click(sender As Object, e As  EventArgs) Handles  sendButton.Click
    'ensure a client is selected in the UI
    If clientBindingSource.Current IsNot Nothing Then
        'disable send button and input text until the current message is sent
        sendButton.Enabled = False
        inputTextBox.Enabled = False
        'get the current client, stream, and data to write
        Dim client As ConnectedClient = CType(clientBindingSource.Current, ConnectedClient)
        Dim stream As NetworkStream = client.TcpClient.GetStream
 
        Dim message As New  XProtocol.XMessage(<TextMessage text1=<%= inputTextBox.Text %>/>)
        Dim buffer() As Byte  = message.ToByteArray
 
        'wait for the data to be sent to the remote client
        Await stream.WriteAsync(buffer, 0, buffer.Length)
        'reset and re-enable the input button and text
        inputTextBox.Clear()
        inputTextBox.Enabled = True
        sendButton.Enabled = True
    End If
End Sub

This method uses databinding in the form to get the currently selected ConnectedClient instance, access the client's network stream, and then write a XMessage instance to the stream.  Here we also see the XMessage in action.  A new XMessage instance is constructed by writing in-line XML in the code and inserting variable values into the XML with expressions.  For more information on this extremely powerful programming construct, see the LINQ to XML Overview.

With these three classes complete, the AsyncTcpServer is ready to run.  Now we just need to write a client and the solution will be complete.

AsyncTcpClient Code

The AsyncTcpClient project is much simpler than the server and only requires a single Form1 class.  As with the Server form, the code for setting up the GUI will be provided in an appendix at the end of the article and here we will concentrate on the code performing the actual work.  The Form1 class needs only three fields which are essentially subsets of the data needed by the server.

Private portNumber As Integer  = 55001
Private client As TcpClient
Private received As New  List(Of Byte)

This specifies the TCP port number used for communication (the same as the server), the TcpClient instance and the buffer of pending message data.  Notice that since the client program only deals with a single connection we do not necessarily need to create a separate class to encapsulate the client instance.

Similar to the server's design, the majority of the client's work is performed in the connectButton.Click event handler.

Private Async Sub connectButton_Click(sender As Object, e As  EventArgs) Handles  connectButton.Click
    If connectButton.Text = "Connect" Then
        client = New  TcpClient
        Try
            'The server and client examples are assumed to be running on the same computer;
            'in your real client application you would allow the user to specify the
            'server's address and then use that value here instead of GetLocalIP()
            Await client.ConnectAsync(GetLocalIP, portNumber)
            connectButton.Text = "Disconnect"
            If client.Connected Then
                'get the client's data stream
                Dim stream As NetworkStream = client.GetStream
                'while the client is connected, continue to wait for and read data
                While client.Connected
                    Dim buffer(client.ReceiveBufferSize - 1) As Byte
                    Dim read As Integer  = Await stream.ReadAsync(buffer, 0, buffer.Length)
                    If read > 0 Then
                        received.AddRange(buffer.Take(read))
                        If XProtocol.XMessage.IsMessageComplete(received)  Then
                            Dim message As XProtocol.XMessage = XProtocol.XMessage.FromByteArray(received.ToArray)
                            received.Clear()
                            Select Case  message.Element.Name
                                Case "TextMessage"
                                    outputTextBox.AppendText(message.Element.@text1)
                                    outputTextBox.AppendText(ControlChars.NewLine)
                            End Select
                        End If
                    Else
                        'server terminated connection
                        Exit While
                    End If
                End While
            End If
        Catch odex As ObjectDisposedException
            'client terminated connection
        Catch ex As Exception
            MessageBox.Show(ex.Message)
            client.Close()
        End Try
        connectButton.Text = "Connect"
    Else
        client.Close()
    End If
End Sub

As we've been doing all along, we put the code into an infinite loop which spends most of it's time suspended, awaiting more data from the remote host.  As you can see, the processing routine is very similar to that of the server, and if necessary could be extended in the same ways previously mentioned (additional Async/Await calls or utilizing Task.Run).

The only other work to do in the client is send a message to the server.  This is done in a sendButton.Click event handler, very much like the server's code.

Private Async Sub sendButton_Click(sender As Object, e As  EventArgs) Handles  sendButton.Click
    If client IsNot Nothing AndAlso  client.Connected Then
        Dim stream As NetworkStream = client.GetStream
        Dim message As New  XProtocol.XMessage(<TextMessage text1=<%= inputTextBox.Text %>/>)
        Dim buffer() As Byte  = message.ToByteArray
        Try
            Await stream.WriteAsync(buffer, 0, buffer.Length)
        Catch ioex As System.IO.IOException
            'server terminated connection
        Catch odex As ObjectDisposedException
            'client terminated connection
        Catch ex As Exception
            'unknown error occured
            MessageBox.Show(ex.Message)
        End Try
        inputTextBox.Clear()
    End If
End Sub

Here we construct and send a message, just like the server.  But unlike the server which is already executing the client process within a task wrapper, we need to handle the various exceptions which might occur while trying to write to the network stream (technically the server might still trap an exception on sending if it wanted to respond specifically to that condition).

This completes the work of the client form.  There is also a helper method used for getting the local IP address when both the client and server examples are being executed on the same PC, but it is unrelated the primary functionality of the client.  That code is available in the appendix at the end of the article.

This concludes the AsyncTcpClient project, as well as the AsyncTcpSample solution.  You can now run the solution, connect client instances to the server, and send messages between them.

(Return to Top)

Summary

By utilizing Async/Await features in TAP, we can significantly simplify the process of creating a multi-client TCP/IP server application which provides reasonable performance for most reasonable usage scenarios.  Many simple application scenarios can make use entirely of asynchronous method calls, allowing the program to perform well while utilizing only the single GUI thread provided by the application.

When sending structured data from one host to another via a stream of bytes, we can significantly simplify the process by taking advantage of LINQ to XML and wrapping an XElement instance in a class containing helper methods for binary serialization of the XML string into a protocol-compliant sequence of bytes.  This allows us to construct complex, yet arbitrary messages in infinite variety and complexity.

All of the designs in this example solution represent only the most basic approach and are meant to serve as scaffolding upon which you can build a real application; the example projects are not meant to be working solutions in-and-of themselves.

Credits

Special thanks to Lucian Wischik for his assistance on the TAP model and in isolating the issue found in NetworkStream's async implementation.

(Return to Top)

Appendix

Appendix A: XProtocol Complete Code

'Define a simple wrapper for XElement which implements our message protocol
Public Class  XMessage
    Const SOH As Byte  = 1 'define a start sequence
    Const EOF As Byte  = 4 'define a stop sequence
    Public Property  Element As  XElement 'declare the object to hold the actual message contents
 
    'define a method to check a series of bytes to determine if they conform to the protocol specification
    Public Shared  Function IsMessageComplete(data As IEnumerable(Of Byte)) As  Boolean
        Dim length As Integer  = data.Count 'get the number of bytes
        If length > 5 Then 'ensure there are enough for at least the start, stop, and length
            If data(0) = SOH AndAlso data(length - 1) = EOF Then 'ensure the series begins and ends with start/stop identifiers
                Dim l As Integer  = BitConverter.ToInt32(data.ToArray, 1) 'interpret the data length by reading bytes 1 through 4 and converting to integer
                Return (l = length - 6) 'ensure that the interpreted data length matches the number of bytes supplied
            End If
        End If
        Return False
    End Function
 
    'parse the XElement content from the supplied data according to the message protocol specification
    Public Shared  Function FromByteArray(data() As Byte) As  XMessage
        Return New  XMessage(XElement.Parse(System.Text.Encoding.UTF8.GetString(data, 5, data.Length - 6)))
    End Function
 
    'serialize the XElement content into a byte array according to the message protocol specification
    Public Function  ToByteArray() As  Byte()
        Dim result As New  List(Of Byte)
        Dim data() As Byte  = System.Text.Encoding.UTF8.GetBytes(Element.ToString) 'encode the XML string
        result.Add(SOH) 'add the message start indicator
        result.AddRange(BitConverter.GetBytes(data.Length)) 'add the data length
        result.AddRange(data) 'add the message data
        result.Add(EOF) 'add the message stop indicator
        Return result.ToArray 'return the data array
    End Function
 
    Public Sub  New(xml As XElement)
        Element = xml
    End Sub
End Class

Appendix B: AsyncTcpServer Complete Code

Imports System.IO
Imports System.Net
Imports System.Net.Sockets
Imports System.Threading
 
Public Class  Form1
    'define the UI controls needed by the sample form
    Friend layoutSplit As New  SplitContainer With  {.Dock = DockStyle.Fill, .FixedPanel = FixedPanel.Panel1}
    Friend layoutTable As New  TableLayoutPanel With {.Dock = DockStyle.Fill, .ColumnCount = 1, .RowCount = 4}
    Friend WithEvents  startButton As  New Button With {.AutoSize = True, .Text = "Start"}
    Friend outputTextBox As New  RichTextBox With  {.Anchor = 15, .ReadOnly = True}
    Friend inputTextBox As New  RichTextBox With  {.Anchor = 15}
    Friend WithEvents  sendButton As  New Button With {.AutoSize = True, .Text = "Send"}
    Friend clientListBox As New  ListBox With  {.Dock = DockStyle.Fill, .IntegralHeight = False}
    Friend WithEvents  clientBindingSource As New  BindingSource
 
    'specificy the TCP/IP Port number that the server will listen on
    Private portNumber As Integer  = 55001
 
    'create the collection instance to store connected clients
    Private clients As New  ConnectedClientCollection
    'declare a variable to hold the listener instance
    Private listener As TcpListener
    'declare a variable to hold the cancellation token source instance
    Private tokenSource As CancellationTokenSource
    'create a list to hold any processing tasks started when clients connect
    Private clientTasks As New  List(Of Task)
 
    Private Sub  Form1_Load(sender As  Object, e As EventArgs) Handles MyBase.Load
        'setup the sample's user interface
        Me.Text = "Server Example"
        Controls.Add(layoutSplit)
        layoutSplit.Panel1.Controls.Add(clientListBox)
        layoutSplit.Panel2.Controls.Add(layoutTable)
        layoutTable.RowStyles.Add(New RowStyle(SizeType.Absolute, startButton.Height + 8))
        layoutTable.RowStyles.Add(New RowStyle(SizeType.Percent, 50.0!))
        layoutTable.RowStyles.Add(New RowStyle(SizeType.Percent, 50.0!))
        layoutTable.RowStyles.Add(New RowStyle(SizeType.Absolute, sendButton.Height + 8))
        layoutTable.Controls.Add(startButton)
        layoutTable.Controls.Add(outputTextBox)
        layoutTable.Controls.Add(inputTextBox)
        layoutTable.Controls.Add(sendButton)
        'use databinding to facilitate displaying received data for each connected client
        clientBindingSource.DataSource = clients
        clientListBox.DataSource = clientBindingSource
        outputTextBox.DataBindings.Add("Text", clientBindingSource, "Text")
    End Sub
 
    Private Async Sub startButton_Click(sender As Object, e As  EventArgs) Handles  startButton.Click
        'this example uses the button text as a state indicator for the server; your real
        'application may wish to provide a local boolean or enum field to indicate the server's operational state 
        If startButton.Text = "Start" Then
            'indicate that the server is running
            startButton.Text = "Stop"
 
            'create a new cancellation token source instance
            tokenSource = New  CancellationTokenSource
            'create a new listener instance bound to the desired address and port
            listener = New  TcpListener(IPAddress.Any, portNumber)
            'start the listener
            listener.Start()
            'begin accepting clients until the listener is closed; closing the listener while
            'it is waiting for a client connection causes an ObjectDisposedException which can
            'be trapped and used to exit the listening routine
            While True
                Try
                    'wait for a client
                    Dim socketClient As TcpClient = Await listener.AcceptTcpClientAsync
                    'record the new client connection
                    Dim client As New  ConnectedClient(socketClient)
                    clientBindingSource.Add(client)
                    'begin executing an async task to process the client's data stream
                    client.Task = ProcessClientAsync(client, tokenSource.Token)
                    'store the task so that we can wait for any existing connections to close
                    'while performing a server shutdown
                    clientTasks.Add(client.Task)
                Catch odex As ObjectDisposedException
                    'listener stopped, so server is shutting down
                    Exit While
                End Try
            End While
            'since NetworkStream.ReadAsync does not honor the cancellation signal we
            'must manually close all connected clients
            For i As Integer  = clients.Count - 1 To 0 Step -1
                clients(i).TcpClient.Close()
            Next
            'wait for all of the clients to finish closing
            Await Task.WhenAll(clientTasks)
            'clean up the cancelation token
            tokenSource.Dispose()
            'reset the start button text, allowing the server to be started again
            startButton.Text = "Start"
        Else
            'signal any processing of current clients to cancel (if listening)
            tokenSource.Cancel()
            'abort the current listening operation/prevent any new connections
            listener.Stop()
        End If
    End Sub
 
    Private Async Sub sendButton_Click(sender As Object, e As  EventArgs) Handles  sendButton.Click
        'ensure a client is selected in the UI
        If clientBindingSource.Current IsNot Nothing Then
            'disable send button and input text until the current message is sent
            sendButton.Enabled = False
            inputTextBox.Enabled = False
            'get the current client, stream, and data to write
            Dim client As ConnectedClient = CType(clientBindingSource.Current, ConnectedClient)
            Dim stream As NetworkStream = client.TcpClient.GetStream
 
            Dim message As New  XProtocol.XMessage(<TextMessage text1=<%= inputTextBox.Text %>/>)
            Dim buffer() As Byte  = message.ToByteArray
 
            'wait for the data to be sent to the remote client
            Await stream.WriteAsync(buffer, 0, buffer.Length)
            'reset and re-enable the input button and text
            inputTextBox.Clear()
            inputTextBox.Enabled = True
            sendButton.Enabled = True
        End If
    End Sub
 
    Private Async Function ProcessClientAsync(client As ConnectedClient, cancel As CancellationToken) As Task
        Try
            'begin reading from the client's data stream
            Using stream As  NetworkStream = client.TcpClient.GetStream
                Dim buffer(client.TcpClient.ReceiveBufferSize - 1)  As  Byte
                'loop exits when read = 0 which occurs when the client closes the socket,
                'or it exits on ReadAsync exception when the connection terminates; exception type indicates termination cause
                Dim read As Integer  = 1
                While read > 0
                    'wait for data to be read; depending on how you choose to read the data, the cancelation token
                    'may or may not be honored by the particular method implementation on the chosen stream implementation
                    read = Await stream.ReadAsync(buffer, 0, buffer.Length, cancel)
                    'process the received data; in this case the data is simply appended to a StringBuilder; any light
                    'work (that is, code which does not require a lot of CPU time) can be performed directly within
                    'the current while loop:
                    client.AppendData(buffer, read)
 
                    '*NOTE: A real application may require significantly more processing of the received data. If lengthy, 
                    'CPU-bound processing is required, a secondary worker method could be started on the thread pool;
                    'if the processing is I/O-bound, you could continue to await calls to async methods.  The following code
                    'demonstrates the handling of a CPU-bound processing routine (see additional comments in DoHeavyWork):
 
                    'Dim workResult As Integer = Await Task.Run(Function() DoHeavyWork(buffer, read, client))
                    ''a real application would likely upate the UI at this point, based on the workResult value (which could
                    ''be an object containing the UI data to update).
                    ''TO TEST: uncomment this block; comment-out client.AppendData(buffer, read) above
                End While
                'client gracefully closed the connection on the remote end
            End Using
        Catch ocex As OperationCanceledException
            'the expected exception if this routines's async method calls honor signaling of the cancelation token
            '*NOTE: NetworkStream.ReadAsync() will not honor the cancelation signal
        Catch odex As ObjectDisposedException
            'server disconnected client while reading
        Catch ioex As IOException
            'client terminated (remote application terminated without socket close) while reading
        Finally
            'ensure the client is closed - this is typically a redundant call, but in the
            'case of an unhandled exception it may be necessary
            client.TcpClient.Close()
            'remove the client from the list of connected clients
            clientBindingSource.Remove(client)
            'remove the client's task from the list of running tasks
            clientTasks.Remove(client.Task)
        End Try
    End Function
 
    'this method is a rough example of how you would implement secondary threading to handle
    'client processing which requires significant CPU time
    Private Function  DoHeavyWork(buffer() As Byte, read As  Integer, client As ConnectedClient) As Integer
        'function return type is some kind of status indicator that the caller can use to determine
        'if the processing was successful, or just an empty object if no return value is needed (that is,
        'if you want to treat the function as a subroutine); although this sample uses Integer, you could
        'use any type of your choosing
 
        'function parameters are whatever is required for your program to process the received data
 
        'due to the fact that AppendData will raise the notify property changed event, we need to
        'ensure that the method is called from the UI thread; in your real application, the UI
        'update would likely occur after this method returns, perhaps based on the result value
        'returned by this method
        Invoke(Sub()
                   client.AppendData(buffer, read)
               End Sub)
        'put the thread to sleep to simulate some long-running CPU-bound processing
        System.Threading.Thread.Sleep(500)
        Return 0
    End Function
End Class
 
'Your program will typically require a custom object which encapsulates the TcpClient instance
'and the data received from that client.  There is no single way to design this class as it's
'requirements will depend entirely on the desired functionality of the application being developed.
Public Class  ConnectedClient
    'implement property change notification to facilitate databinding
    Implements System.ComponentModel.INotifyPropertyChanged
 
    'store the TcpClient instance
    Public ReadOnly  TcpClient As  TcpClient
    'store a unique id for this client connection
    Public ReadOnly  Id As  String
    Public Property  Task As  Task
 
    'store the data received from the remote client
    Public ReadOnly  Received As  New List(Of Byte)
 
    'expose the received data as a string property to facilitate databinding
    Private _Text As String  = String.Empty
    Public ReadOnly  Property Text As String
        Get
            'Return Received.ToString
            Return _Text
        End Get
    End Property
 
    Public Sub  New(client As TcpClient)
        TcpClient = client
        'craft the unique id from the remote client's IP address and the port they connected from
        Id = CType(TcpClient.Client.RemoteEndPoint, IPEndPoint).ToString
    End Sub
 
    'expose a helper method for capturing and storing data received from the remote client
    Public Sub  AppendData(buffer() As Byte, read As  Integer)
        If read = 0 Then Exit  Sub
        'add the bytes read this time to the collection of bytes read so far
        Received.AddRange(buffer.Take(read))
        'check to see if the bytes read so far represent a complete message
        If XProtocol.XMessage.IsMessageComplete(Received)  Then
            'if so, build a message from the byte data and then clear the byte data to prepare for the next message
            Dim message As XProtocol.XMessage = XProtocol.XMessage.FromByteArray(Received.ToArray)
            Received.Clear()
            'read data elements from the message as appropriate
            Select Case  message.Element.Name
                Case "TextMessage"
                    _Text = message.Element.@text1
                    RaiseEvent PropertyChanged(Me, New  System.ComponentModel.PropertyChangedEventArgs("Text"))
            End Select
        End If
    End Sub
 
    'implement primary object method overrides based on unique id
    Public Overrides  Function Equals(obj As Object) As  Boolean
        If TypeOf  obj Is  ConnectedClient Then  Return Id = DirectCast(obj, ConnectedClient).Id
        Return MyBase.Equals(obj)
    End Function
 
    Public Overrides  Function GetHashCode() As Integer
        Return Id.GetHashCode
    End Function
 
    Public Overrides  Function ToString() As String
        Return Id
    End Function
 
    Public Event  PropertyChanged(sender As Object, e As  System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
End Class
 
'This class provides a simple collection of clients where each can be accessed by
'unique id or index in the collection.  This is to facilitate working with connected
'clients from your actual application, if needed, and could be replaced with a simple
'List(Of ConnectedClient) if your application will not need to access individual clients
'by their unique id.  Typically though this kind of collection will be useful when
'writing your server application.  The list of client tasks could also be extrapolated
'from this collection rather than being stored in a separate list.
Public Class  ConnectedClientCollection
    Inherits System.Collections.ObjectModel.KeyedCollection(Of  String, ConnectedClient)
 
    Protected Overrides  Function GetKeyForItem(item As ConnectedClient) As String
        Return item.Id
    End Function
End Class

Appendix C: AsyncTcpClient Complete Code

Imports System.Net
Imports System.Net.Sockets
 
Public Class  Form1
    Friend layoutTable As New  TableLayoutPanel With {.Dock = DockStyle.Fill, .ColumnCount = 1, .RowCount = 4}
    Friend WithEvents  connectButton As  New Button With {.AutoSize = True, .Text = "Connect"}
    Friend outputTextBox As New  RichTextBox With  {.Anchor = 15, .ReadOnly = True}
    Friend inputTextBox As New  RichTextBox With  {.Anchor = 15}
    Friend WithEvents  sendButton As  New Button With {.AutoSize = True, .Text = "Send"}
 
    Private portNumber As Integer  = 55001
    Private client As TcpClient
    Private received As New  List(Of Byte)
 
    Private Sub  Form1_Load(sender As  Object, e As EventArgs) Handles MyBase.Load
        Me.Text = "Client Example"
        Controls.Add(layoutTable)
        layoutTable.RowStyles.Add(New RowStyle(SizeType.Absolute, connectButton.Height + 8))
        layoutTable.RowStyles.Add(New RowStyle(SizeType.Percent, 50.0!))
        layoutTable.RowStyles.Add(New RowStyle(SizeType.Percent, 50.0!))
        layoutTable.RowStyles.Add(New RowStyle(SizeType.Absolute, sendButton.Height + 8))
        layoutTable.Controls.Add(connectButton)
        layoutTable.Controls.Add(outputTextBox)
        layoutTable.Controls.Add(inputTextBox)
        layoutTable.Controls.Add(sendButton)
    End Sub
 
    Private Async Sub connectButton_Click(sender As Object, e As  EventArgs) Handles  connectButton.Click
        If connectButton.Text = "Connect" Then
            client = New  TcpClient
            Try
                'The server and client examples are assumed to be running on the same computer;
                'in your real client application you would allow the user to specify the
                'server's address and then use that value here instead of GetLocalIP()
                Await client.ConnectAsync(GetLocalIP, portNumber)
                connectButton.Text = "Disconnect"
                If client.Connected Then
                    'get the client's data stream
                    Dim stream As NetworkStream = client.GetStream
                    'while the client is connected, continue to wait for and read data
                    While client.Connected
                        Dim buffer(client.ReceiveBufferSize - 1) As Byte
                        Dim read As Integer  = Await stream.ReadAsync(buffer, 0, buffer.Length)
                        If read > 0 Then
                            received.AddRange(buffer.Take(read))
                            If XProtocol.XMessage.IsMessageComplete(received)  Then
                                Dim message As XProtocol.XMessage = XProtocol.XMessage.FromByteArray(received.ToArray)
                                received.Clear()
                                Select Case  message.Element.Name
                                    Case "TextMessage"
                                        outputTextBox.AppendText(message.Element.@text1)
                                        outputTextBox.AppendText(ControlChars.NewLine)
                                End Select
                            End If
                        Else
                            'server terminated connection
                            Exit While
                        End If
                    End While
                End If
            Catch odex As ObjectDisposedException
                'client terminated connection
            Catch ex As Exception
                MessageBox.Show(ex.Message)
                client.Close()
            End Try
            connectButton.Text = "Connect"
        Else
            client.Close()
        End If
    End Sub
 
    'send message to server
    Private Async Sub sendButton_Click(sender As Object, e As  EventArgs) Handles  sendButton.Click
        If client IsNot Nothing AndAlso  client.Connected Then
            Dim stream As NetworkStream = client.GetStream
            Dim message As New  XProtocol.XMessage(<TextMessage text1=<%= inputTextBox.Text %>/>)
            Dim buffer() As Byte  = message.ToByteArray
            Try
                Await stream.WriteAsync(buffer, 0, buffer.Length)
            Catch ioex As System.IO.IOException
                'server terminated connection
            Catch odex As ObjectDisposedException
                'client terminated connection
            Catch ex As Exception
                'unknown error occured
                MessageBox.Show(ex.Message)
            End Try
            inputTextBox.Clear()
        End If
    End Sub
 
    'helper method for getting local IPv4 address
    Private Function  GetLocalIP() As  IPAddress
        For Each  adapter In  NetworkInformation.NetworkInterface.GetAllNetworkInterfaces
            If adapter.OperationalStatus = NetworkInformation.OperationalStatus.Up AndAlso
                adapter.Supports(NetworkInformation.NetworkInterfaceComponent.IPv4) AndAlso
                adapter.NetworkInterfaceType <> NetworkInformation.NetworkInterfaceType.Loopback Then
                Dim props As NetworkInformation.IPInterfaceProperties = adapter.GetIPProperties
                For Each  address In  props.UnicastAddresses
                    If address.Address.AddressFamily = AddressFamily.InterNetwork Then  Return address.Address
                Next
            End If
        Next
        Return IPAddress.None
    End Function
End Class

The AsyncTcpSample solution is available for download on Code Gallery.

(Return to Top)