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.

Published Thursday, April 10, 2008 11:58 PM by dwahlin
Filed under: , ,

Comments

# re: Pushing Data to a Silverlight Client with Sockets: Part I

Friday, April 11, 2008 9:51 AM by Gopinath

Cool work and interesting. Question> Is there a way to push real objects via sockets

instead of just strings? like in WCF contracts, i have customer objects that can be pushed regularly ?

Thanks,

Gopi

# re: Pushing Data to a Silverlight Client with Sockets: Part I

Friday, April 11, 2008 12:12 PM by dwahlin

Gopinath,

Great question.  You can push bytes so anything should be fair game over the wire.  On the client-side the Assembly.Load() method is also available it looks like.  Having never tried it, I'm not sure how the security works there and if Silverlight would allow that or not, but it looks like the players to make it happen are there.  If you end up trying it please post a comment as I'd be interested in knowing how it works out.

# re: Pushing Data to a Silverlight Client with Sockets: Part I

Thursday, April 24, 2008 4:56 PM by neil

Good article. Is there any reason why you are using async BeginAcceptTcpClient combined with a ManualResetEvent? I mean: Wouldn't do

while (true) { Socket client = _listener.AcceptSocket(); .... }

the same?

Regards

# re: Pushing Data to a Silverlight Client with Sockets: Part I

Monday, April 28, 2008 10:07 PM by dwahlin

Neil,

There are definitely other ways to do it.  I tried to keep it somewhat similar to the Silverlight side of things since everything there follows the BeginXXX/EndXXX pattern.  And...I kind of like ManualResetEvent for some reason since it makes it really clear when a thread is blocked and when it's available to handle new connections. One of those personal choice things. :-)