Pushing Data to a Silverlight Client with Sockets: Part I
Silverlight 2 has built-in support for sockets which creates some interesting possibilities. If you've ever worked on a client-side application that needed to receive up-to-date data then you're probably used to solving the problem by polling. With polling the client contacts the server on a consistent, timed basis to see if any updates are available. ASP.NET AJAX provides a Timer control that makes this process easy and straightforward.
The problem with polling is that unless the data is changing a lot, most of the client checks to the server are simply wasted bandwidth in cases where no changes have occurred. If you're calling a Web Service and passing JSON messages back and forth then the overhead is certainly minimal, but wouldn't it be nice if the server could push updates to the client as needed instead? In fact, wouldn't it be nice if multiple clients could receive the same updates on a consistent basis so everyone is in sync?
What are Sockets?
Sockets allow a listener server to listen for clients that would like to connect to a specific socket (an IP address combined with a port). As the clients connect the server can send data to them anytime and the clients can send data to the server as well. Data flows both directions which allows the server to push data if desired. The .NET framework provides direct support for using sockets through the System.Net.Sockets namespace and provides classes such as Socket and TcpListener that can be used to create a server application. The following image shows a Silverlight 2 application built around the concept of sockets that allows a server application to push data down to a client that displays the score of a basketball game (click the image to view an animated gif that shows the application in progress):
The game is simulated on the server and as each team scores the update is pushed to the client so that the score and action that occurred can be displayed.
Creating the Server
To create a server that uses sockets you'll need to import the System.Net and System.Net.Sockets namespace. You can use the Socket or TcpListener class to listen for client connection attempts but I opted for the TcpListener class since my Silverlight 2 clients would only be connecting over TCP anyway (as opposed to UDP or another protocol). The server application shown next listens on port 4530 which is in the range Silverlight 2 allows.
Here's the code that starts up the TcpListener class as well as a timer that is used to send data to the client on a random basis. The _TcpClientConnected object is a ManualResetEvent object that blocks threads until previous ones have connected. The key line of code is the BeginAcceptTcpClient() method which asynchronously routes client connection attempts to a callback method named OnBeginAccept().
public void StartSocketServer() { InitializeData(); _Timer = new System.Timers.Timer(); _Timer.Enabled = false; _Timer.Interval = 2000D; _Timer.Elapsed += new ElapsedEventHandler(_Timer_Elapsed); try { //Allowed port range 4502-4532 _Listener = new TcpListener(IPAddress.Any, 4530); _Listener.Start(); Console.WriteLine("Server listening..."); while (true) { _TcpClientConnected.Reset(); Console.WriteLine("Waiting for client connection..."); _Listener.BeginAcceptTcpClient(new AsyncCallback(OnBeginAccept),null); _TcpClientConnected.WaitOne(); //Block until client connects } } catch (Exception exp) { LogError(exp); } }
Once a client tries to connect the OnBeginAccept() method is called which sends team data down to the server and starts the timer which fakes the scores and game actions. The stream used to write to each client is added to a collection since it's used later as changes on the server occur.
private void OnBeginAccept(IAsyncResult ar) { _TcpClientConnected.Set(); //Allow waiting thread to proceed TcpListener listener = _Listener; TcpClient client = listener.EndAcceptTcpClient(ar); if (client.Connected) { Console.WriteLine("Client connected..."); StreamWriter writer = new StreamWriter(client.GetStream()); writer.AutoFlush = true; _ClientStreams.Add(writer); Console.WriteLine("Sending initial team data..."); writer.WriteLine(GetTeamData()); if (_Timer.Enabled == false) { _Timer.Start(); } } }
The initial team data that is sent from the server to the client immediately after a client connects is created in the GetTeamData() method:
private string GetTeamData() { StringWriter sw = new StringWriter(); using (XmlWriter writer = XmlWriter.Create(sw)) { writer.WriteStartElement("Teams"); foreach (string key in _Teams.Keys) { writer.WriteStartElement("Team"); writer.WriteAttributeString("Name", key); Dictionary<Guid, string> players = _Teams[key]; foreach (Guid playerKey in players.Keys) { writer.WriteStartElement("Player"); writer.WriteAttributeString("ID", playerKey.ToString()); writer.WriteAttributeString("Name", players[playerKey]); writer.WriteEndElement(); } writer.WriteEndElement(); } writer.WriteEndElement(); } return sw.ToString(); }
As the Timer object fires the _Timer_Elapsed() method is called which handles sending score updates to the client on a random basis to simulate plays in a game:
private void _Timer_Elapsed(object sender, ElapsedEventArgs e) { SendData(GetScoreData()); //Setting new interval to simulate different data feed times Random r = new Random(); _Timer.Interval = (double)r.Next(3000, 7000); } private void SendData(string data) { if (_ClientStreams != null) { foreach (StreamWriter writer in _ClientStreams) { if (writer != null) { writer.Write(data); } } } }
There's quite a bit more code for the server application which you can download here. The code shown above is really all that's involved for creating a server that listens for clients on a specific IP address and port. In the next post on this topic I'll cover the client-side and discuss how Silverlight can connect to a server using sockets.