1. Problem
You need a Silverlight application to communicate with server-side applications using TCP sockets.
2. Solution
Use the System.Net.Sockets.Socket type and related types to connect and exchange data with a server-side TCP socket.
3. How It Works
Silverlight supports socket communication through the System.Net.Sockets.Socket
type. This class exposes an API to connect to a TCP endpoint at a
specified IP address/port combination, send data to that endpoint, and
receive data from that endpoint.
However, the Socket
type in Silverlight is slightly different from the equivalent type in
the desktop and server versions of the .NET Framework; it supports only
the client behavior and has no server abilities. In other words, unlike
the desktop or the server version, the Silverlight version does not
expose the ability to go into a listen mode and accept incoming
connections. Therefore, although Silverlight applications can easily use
TCP sockets to exchange data with server applications, a Silverlight
application cannot act as a socket-based server.
3.1. The Sockets API in Silverlight
All socket functionality
in Silverlight works asynchronously, thus avoiding any blocking calls
that would prevent the main thread from blocking execution while waiting
for any such call completion. However, the design pattern for the Socket's asynchronous APIs is somewhat different from the previously discussed Begin-End pattern, as you see in a moment.
The life of a socket connection begins by creating a new instance of a Socket and calling the ConnectAsync() method on the socket instance. The call to ConnectAsync()
is nonblocking and returns immediately. To be notified on completion of
the connection process, you can attach a handler to the Completed event of the SocketAsyncEventArgs parameter, which then is called back by the runtime. The following code excerpt shows a sample of this:
//create a new socket
Socket ClientSocket = new Socket(AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Tcp);
//create a new SocketEventArgs
SocketAsyncEventArgs sockEvtArgs = new SocketAsyncEventArgs {
RemoteEndPoint = new IPEndPoint(IPAddress.Parse("192.168.0.10"), 4502),
UserToken = MyData };
//connect a completion handler
sockEvtArgs.Completed += new EventHandler<SocketAsyncEventArgs>(
delegate(object sender, SocketAsyncEventArgs e)
{
if (e.SocketError == SocketError.Success)
{
//connection succeeded - do something
}
});
//connect asynchronously
ClientSocket.ConnectAsync(sockEvtArgs);
As you can see, the Socket construction parameters let you specify the following:
The type of addressing scheme used between IPv4 or IPv6 (which also enables IPv4) using the AddressFamily enumeration. To specify an IPv4 addressing scheme, use AddressFamily.InterNetwork; for IPv6, use AddressFamily.InterNetworkV6.
The SocketType (the only available value is Stream).
The ProtocolType (the only supported protocol is TCP).
Alternatively, you can set all the enumeration values to unspecified, and the values are inferred at runtime.
The endpoint being connected to is specified as the RemoteEndPoint property of the SocketEventArgs parameter. You can set it to an instance of IPEndPoint if you know the exact IP address or that of a DnsEndPoint
if you have a hostname and want the DNS system to translate it to an IP
address for you. Additionally, you need to supply the port. You can
also supply any user state in the UserToken parameter.
When the connection is made, the Completed event handler is called, and further information is made available to you through the SocketAsyncEventArgs instance passed into the handler. The SocketError property gives you a success status or the type of error that was encountered, and the UserToken parameter can be used to extract any supplied user state.
There is a static version of ConnectAsync(), which behaves similarly. Because you do not explicitly create a Socket instance to use the static version, a connected Socket instance is made available to you through the ConnectSocket property on the SocketEventArgs instance in the Completed handler.
After you are connected, you can begin sending and receiving data. To send data, you can use the SendAsync() method. The data to be sent must be represented as a byte[] and can be copied to the SocketAsynceventArgs.Buffer using the SetBuffer() method, as shown here:
SocketAsyncEventArgs sockEvtArgsSend = new SocketAsyncEventArgs();
sockEvtArgsSend.SetBuffer(MyData, 0, MyData.Length);
sockEvtArgsSend.Completed +=
new EventHandler<SocketAsyncEventArgs>(SendRequest_Completed);
ClientSocket.SendAsync(sockEvtArgsSend);
Receiving data uses a similar implementation. To receive data, you allocate a byte[] and assign it using the SocketAsyncEventargs.SetBuffer() method as the receiving buffer, followed by a call to ReceiveAsync().
Note that the Silverlight socket implementation gives you no indication
when you are about to receive data from a remote endpoint; nor can you
poll the socket from time to time. Consequently, when the call to ReceiveAsync() returns in the Completed
handler, you may want to execute the code to receive again, thus
keeping your client socket in a continuous receive mode. The following
code shows such an arrangement:
private void ReceiveMessage()
{
//allocate memory
byte[] ReceiveBuffer = new Byte[1024];
SocketAsyncEventArgs sockEvtArgsReceive = new SocketAsyncEventArgs();
//set the receive buffer
sockEvtArgsReceive.SetBuffer(ReceiveBuffer, 0, 1024);
sockEvtArgsReceive.Completed +=
new EventHandler<SocketAsyncEventArgs>(Receive_Completed);
//receive
ClientSocket.ReceiveAsync(sockEvtArgsReceive);
}
void Receive_Completed(object sender, SocketAsyncEventArgs e)
{
if (e.SocketError == SocketError.Success)
{
//switch context
ParentPage.Dispatcher.BeginInvoke(new Action(delegate
{
//access the received data
byte[] Message = new byte[e.BytesTransferred];
Array.Copy(e.Buffer, 0, Message, 0, e.BytesTransferred);
//do something to process the received message
//keep receiving
ReceiveMessage();
}));
}
}
NOTE
The Completed handlers
are called on a background thread, necessitating a context switch using
Dispatcher, before you can invoke code running on the main UI thread.
3.2. Cross-Domain Policy and Port Requirements
Silverlight applications using
sockets have to satisfy cross-domain policy requirements to access
remote socket servers. There is also a restriction on the range of ports that a Silverlight
client can connect to—the port must be within the inclusive range of
4502 to 4534.
4. The Code
The code sample for this
recipe builds a simple one-to-one chat application that consists of a
server program that acts as the listener and the gateway for exchanging
text-based messages between Silverlight clients.
4.1. Running the Sample Code
To start the whole
environment, you must first start up the sockets server and the policy
server. Both of these are console programs and can be started either
from the command line or from inside Visual Studio if you intend to
start them in debug mode. The sockets server, which is named ChatBroker.exe,
accepts one parameter on the command line: the port number you want it
to listen on. Ensure that this is within the allowed port range of 4502
to 4534, inclusive. If you are debugging this from within Visual Studio,
you can specify the parameter in your project's Debug properties page.
The policy server is called PolicyServer.exe and does not need any startup parameters.
When you have the server
instances up and running, you can then start (either in debug mode or by
browsing to the page) the client. Figure 1 shows the various states of the Silverlight client.
You can specify the IP address
and the port at which the sockets server is listening, as well as a name
that you want to use in the conversation. After the user logs in, the
client displays a list of all other participants currently connected to
the server. You can click a participant and start a conversation. To
simulate multiple participants, open multiple instances of the client
and log in with multiple names.