Table of Contents IntroductionScale and PerformanceMessage ProtocolExample Solution Create Projects and SolutionAdd ReferencesXProtocol CodeAsyncTcpServer Code ConnectedClient ClassConnectedClientCollection ClassForm1 ClassAsyncTcpClient CodeSummaryCreditsAppendix Appendix A: XProtocol Complete CodeAppendix B: AsyncTcpServer Complete CodeAppendix C: AsyncTcpClient Complete CodeAppendix D: Code Gallery Sample
Public
Class
XMessage
Const
SOH
As
Byte
= 1
'define a start sequence
EOF
= 4
'define a stop sequence
Property
Element
XElement
'declare the object to hold the actual message contents
End
Sub
New
(xml
XElement)
Element = xml
'serialize the XElement content into a byte array according to the message protocol specification
Function
ToByteArray()
()
Dim
result
List(Of
)
data()
= 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
'define a method to check a series of bytes to determine if they conform to the protocol specification
Shared
IsMessageComplete(data
IEnumerable(Of
))
Boolean
length
Integer
= data.Count
'get the number of bytes
If
length > 5
Then
'ensure there are enough for at least the start, stop, and length
data(0) = SOH
AndAlso
data(length - 1) = EOF
'ensure the series begins and ends with start/stop identifiers
l
= BitConverter.ToInt32(data.ToArray, 1)
'interpret the data length by reading bytes 1 through 4 and converting to integer
(l = length - 6)
'ensure that the interpreted data length matches the number of bytes supplied
False
'parse the XElement content from the supplied data according to the message protocol specification
FromByteArray(data()
XMessage(XElement.Parse(System.Text.Encoding.UTF8.GetString(data, 5, data.Length - 6)))
Imports
System.Net
System.Net.Sockets
ConnectedClient
Implements
System.ComponentModel.INotifyPropertyChanged
Event
PropertyChanged(sender
Object
, e
System.ComponentModel.PropertyChangedEventArgs)
System.ComponentModel.INotifyPropertyChanged.PropertyChanged
'store the TcpClient instance
ReadOnly
TcpClient
'store a unique id for this client connection
Id
String
Task
'store the data received from the remote client
Received
'expose the received data as a string property to facilitate databinding
Private
_Text
=
.Empty
Text
Get
'Return Received.ToString
(client
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
AppendData(buffer()
, read
read = 0
Exit
'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
XProtocol.XMessage.IsMessageComplete(Received)
'if so, build a message from the byte data and then clear the byte data to prepare for the next message
message
XProtocol.XMessage = XProtocol.XMessage.FromByteArray(Received.ToArray)
Received.Clear()
'read data elements from the message as appropriate
Select
Case
message.Element.Name
"TextMessage"
_Text = message.Element.@text1
RaiseEvent
PropertyChanged(
Me
,
System.ComponentModel.PropertyChangedEventArgs(
"Text"
'implement primary object method overrides based on unique id
Overrides
Equals(obj
TypeOf
obj
Is
DirectCast
(obj, ConnectedClient).Id
MyBase
.Equals(obj)
GetHashCode()
Id.GetHashCode
ToString()
ConnectedClientCollection
Inherits
System.Collections.ObjectModel.KeyedCollection(Of
, ConnectedClient)
Protected
GetKeyForItem(item
ConnectedClient)
item.Id
System.IO
System.Threading
'specify the TCP/IP Port number that the server will listen on
portNumber
= 55001
'create the collection instance to store connected clients
clients
'declare a variable to hold the listener instance
listener
TcpListener
'declare a variable to hold the cancellation token source instance
tokenSource
CancellationTokenSource
'create a list to hold any processing tasks started when clients connect
clientTasks
List(Of Task)
Async
startButton_Click(sender
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
startButton.Text =
"Start"
'indicate that the server is running
"Stop"
'create a new cancellation token source instance
tokenSource =
'create a new listener instance bound to the desired address and port
listener =
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
socketClient
TcpClient = Await listener.AcceptTcpClientAsync
'record the new client connection
client
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
ObjectDisposedException
'listener stopped, so server is shutting down
'since NetworkStream.ReadAsync does not honor the cancellation signal we
'must manually close all connected clients
For
i
= 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
Else
'signal any processing of current clients to cancel (if listening)
tokenSource.Cancel()
'abort the current listening operation/prevent any new connections
listener.
Stop
ProcessClientAsync(client
ConnectedClient, cancel
CancellationToken)
'begin reading from the client's data stream
Using stream
NetworkStream = client.TcpClient.GetStream
buffer(client.TcpClient.ReceiveBufferSize - 1)
'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
read
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
'client gracefully closed the connection on the remote end
Using
ocex
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
'server disconnected client while reading
ioex
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)
'this method is a rough example of how you would implement secondary threading to handle
'client processing which requires significant CPU time
DoHeavyWork(buffer()
, client
'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(
'put the thread to sleep to simulate some long-running CPU-bound processing
System.Threading.Thread.Sleep(500)
sendButton_Click(sender
sendButton.Click
'ensure a client is selected in the UI
clientBindingSource.Current IsNot
Nothing
'disable send button and input text until the current message is sent
sendButton.Enabled =
inputTextBox.Enabled =
'get the current client, stream, and data to write
ConnectedClient =
(clientBindingSource.Current, ConnectedClient)
stream
XProtocol.XMessage(<TextMessage text1=<%= inputTextBox.Text %>/>)
buffer()
= 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()
received
connectButton_Click(sender
connectButton.Click
connectButton.Text =
"Connect"
client =
'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)
"Disconnect"
client.Connected
'get the client's data stream
NetworkStream = client.GetStream
'while the client is connected, continue to wait for and read data
buffer(client.ReceiveBufferSize - 1)
= Await stream.ReadAsync(buffer, 0, buffer.Length)
received.AddRange(buffer.Take(read))
XProtocol.XMessage.IsMessageComplete(received)
XProtocol.XMessage = XProtocol.XMessage.FromByteArray(received.ToArray)
received.Clear()
outputTextBox.AppendText(message.Element.@text1)
outputTextBox.AppendText(ControlChars.NewLine)
'server terminated connection
'client terminated connection
ex
Exception
MessageBox.Show(ex.Message)
client.Close()
client IsNot
System.IO.IOException
'unknown error occured
'Define a simple wrapper for XElement which implements our message protocol
Form1
'define the UI controls needed by the sample form
Friend
layoutSplit
SplitContainer
With
{.Dock = DockStyle.Fill, .FixedPanel = FixedPanel.Panel1}
layoutTable
TableLayoutPanel
{.Dock = DockStyle.Fill, .ColumnCount = 1, .RowCount = 4}
WithEvents
startButton
Button
{.AutoSize =
, .Text =
}
outputTextBox
RichTextBox
{.Anchor = 15, .
inputTextBox
{.Anchor = 15}
sendButton
"Send"
clientListBox
ListBox
{.Dock = DockStyle.Fill, .IntegralHeight =
clientBindingSource
BindingSource
'specificy the TCP/IP Port number that the server will listen on
Form1_Load(sender
.Load
'setup the sample's user interface
.Text =
"Server Example"
Controls.Add(layoutSplit)
layoutSplit.Panel1.Controls.Add(clientListBox)
layoutSplit.Panel2.Controls.Add(layoutTable)
layoutTable.RowStyles.Add(
RowStyle(SizeType.Absolute, startButton.Height + 8))
RowStyle(SizeType.Percent, 50.0!))
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(
, clientBindingSource,
''a real application would likely upate the UI at this point, based on the workResult value (which could
'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.
'implement property change notification to facilitate databinding
'expose a helper method for capturing and storing data received from the remote client
'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.
connectButton
"Client Example"
Controls.Add(layoutTable)
RowStyle(SizeType.Absolute, connectButton.Height + 8))
layoutTable.Controls.Add(connectButton)
'send message to server
'helper method for getting local IPv4 address
GetLocalIP()
IPAddress
Each
adapter
In
NetworkInformation.NetworkInterface.GetAllNetworkInterfaces
adapter.OperationalStatus = NetworkInformation.OperationalStatus.Up
adapter.Supports(NetworkInformation.NetworkInterfaceComponent.IPv4)
adapter.NetworkInterfaceType <> NetworkInformation.NetworkInterfaceType.Loopback
props
NetworkInformation.IPInterfaceProperties = adapter.GetIPProperties
address
props.UnicastAddresses
address.Address.AddressFamily = AddressFamily.InterNetwork
address.Address
IPAddress.None